Compare commits
No commits in common. "ad80bc81137c045e62b84e68aa67033367ae6279" and "bfc45bd0f0725fe6e0ac11a4fc6b92b8ae97c0f5" have entirely different histories.
ad80bc8113
...
bfc45bd0f0
26 changed files with 38 additions and 876 deletions
|
|
@ -5,13 +5,6 @@ All notable changes to WuttaSync will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## v0.6.0 (2026-03-17)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add concept of (non-)default importers for handler
|
|
||||||
- add support for Wutta <-> Wutta import/export
|
|
||||||
|
|
||||||
## v0.5.1 (2026-02-13)
|
## v0.5.1 (2026-02-13)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``wuttasync.cli.export_wutta``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: wuttasync.cli.export_wutta
|
|
||||||
:members:
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``wuttasync.cli.import_wutta``
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. automodule:: wuttasync.cli.import_wutta
|
|
||||||
:members:
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``wuttasync.conf``
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. automodule:: wuttasync.conf
|
|
||||||
:members:
|
|
||||||
|
|
@ -74,11 +74,8 @@ cf. :doc:`rattail-manual:data/sync/index`.
|
||||||
api/wuttasync.cli
|
api/wuttasync.cli
|
||||||
api/wuttasync.cli.base
|
api/wuttasync.cli.base
|
||||||
api/wuttasync.cli.export_csv
|
api/wuttasync.cli.export_csv
|
||||||
api/wuttasync.cli.export_wutta
|
|
||||||
api/wuttasync.cli.import_csv
|
api/wuttasync.cli.import_csv
|
||||||
api/wuttasync.cli.import_versions
|
api/wuttasync.cli.import_versions
|
||||||
api/wuttasync.cli.import_wutta
|
|
||||||
api/wuttasync.conf
|
|
||||||
api/wuttasync.emails
|
api/wuttasync.emails
|
||||||
api/wuttasync.exporting
|
api/wuttasync.exporting
|
||||||
api/wuttasync.exporting.base
|
api/wuttasync.exporting.base
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,6 @@ Defined in: :mod:`wuttasync.cli.export_csv`
|
||||||
.. program-output:: wutta export-csv --help
|
.. program-output:: wutta export-csv --help
|
||||||
|
|
||||||
|
|
||||||
.. _wutta-export-wutta:
|
|
||||||
|
|
||||||
``wutta export-wutta``
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
Export data to another Wutta :term:`app database`, from the local one.
|
|
||||||
|
|
||||||
Defined in: :mod:`wuttasync.cli.export_wutta`
|
|
||||||
|
|
||||||
.. program-output:: wutta export-wutta --help
|
|
||||||
|
|
||||||
|
|
||||||
.. _wutta-import-csv:
|
.. _wutta-import-csv:
|
||||||
|
|
||||||
``wutta import-csv``
|
``wutta import-csv``
|
||||||
|
|
@ -76,15 +64,3 @@ in the :term:`app model`.
|
||||||
Defined in: :mod:`wuttasync.cli.import_versions`
|
Defined in: :mod:`wuttasync.cli.import_versions`
|
||||||
|
|
||||||
.. program-output:: wutta import-versions --help
|
.. program-output:: wutta import-versions --help
|
||||||
|
|
||||||
|
|
||||||
.. _wutta-import-wutta:
|
|
||||||
|
|
||||||
``wutta import-wutta``
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
Import data from another Wutta :term:`app database`, to the local one.
|
|
||||||
|
|
||||||
Defined in: :mod:`wuttasync.cli.import_wutta`
|
|
||||||
|
|
||||||
.. program-output:: wutta import-wutta --help
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "WuttaSync"
|
name = "WuttaSync"
|
||||||
version = "0.6.0"
|
version = "0.5.1"
|
||||||
description = "Wutta Framework for data import/export and real-time sync"
|
description = "Wutta Framework for data import/export and real-time sync"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||||
|
|
@ -30,7 +30,7 @@ dependencies = [
|
||||||
"makefun",
|
"makefun",
|
||||||
"rich",
|
"rich",
|
||||||
"SQLAlchemy-Utils",
|
"SQLAlchemy-Utils",
|
||||||
"WuttJamaican[db]>=0.28.10",
|
"WuttJamaican[db]>=0.28.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,15 +42,10 @@ tests = ["pylint", "pytest", "pytest-cov", "tox", "Wutta-Continuum>=0.3.0"]
|
||||||
[project.entry-points."wutta.app.providers"]
|
[project.entry-points."wutta.app.providers"]
|
||||||
wuttasync = "wuttasync.app:WuttaSyncAppProvider"
|
wuttasync = "wuttasync.app:WuttaSyncAppProvider"
|
||||||
|
|
||||||
[project.entry-points."wutta.config.extensions"]
|
|
||||||
"wuttasync" = "wuttasync.conf:WuttaSyncConfig"
|
|
||||||
|
|
||||||
[project.entry-points."wuttasync.importing"]
|
[project.entry-points."wuttasync.importing"]
|
||||||
"export.to_csv.from_wutta" = "wuttasync.exporting.csv:FromWuttaToCsv"
|
"export.to_csv.from_wutta" = "wuttasync.exporting.csv:FromWuttaToCsv"
|
||||||
"export.to_wutta.from_wutta" = "wuttasync.importing.wutta:FromWuttaToWuttaExport"
|
|
||||||
"import.to_versions.from_wutta" = "wuttasync.importing.versions:FromWuttaToVersions"
|
"import.to_versions.from_wutta" = "wuttasync.importing.versions:FromWuttaToVersions"
|
||||||
"import.to_wutta.from_csv" = "wuttasync.importing.csv:FromCsvToWutta"
|
"import.to_wutta.from_csv" = "wuttasync.importing.csv:FromCsvToWutta"
|
||||||
"import.to_wutta.from_wutta" = "wuttasync.importing.wutta:FromWuttaToWuttaImport"
|
|
||||||
|
|
||||||
[project.entry-points."wutta.typer_imports"]
|
[project.entry-points."wutta.typer_imports"]
|
||||||
wuttasync = "wuttasync.cli"
|
wuttasync = "wuttasync.cli"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
|
@ -87,32 +87,18 @@ class WuttaSyncAppProvider(AppProvider):
|
||||||
|
|
||||||
:returns: List of all import/export handler classes
|
:returns: List of all import/export handler classes
|
||||||
"""
|
"""
|
||||||
# first load all "registered" Handler classes. note we must
|
# first load all "registered" Handler classes
|
||||||
# specify lists=True since handlers from different projects
|
factories = load_entry_points("wuttasync.importing", ignore_errors=True)
|
||||||
# can be registered with the same key.
|
|
||||||
factory_lists = load_entry_points(
|
|
||||||
"wuttasync.importing", lists=True, ignore_errors=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# organize registered classes by spec
|
# organize registered classes by spec
|
||||||
specs = {}
|
specs = {factory.get_spec(): factory for factory in factories.values()}
|
||||||
all_factories = []
|
|
||||||
for factories in factory_lists.values():
|
|
||||||
for factory in factories:
|
|
||||||
specs[factory.get_spec()] = factory
|
|
||||||
all_factories.append(factory)
|
|
||||||
|
|
||||||
# many handlers may not be registered per se, but may be
|
# many handlers may not be registered per se, but may be
|
||||||
# designated via config. so try to include those too
|
# designated via config. so try to include those too
|
||||||
seen = set()
|
for factory in factories.values():
|
||||||
for factory in all_factories:
|
spec = self.get_designated_import_handler_spec(factory.get_key())
|
||||||
key = factory.get_key()
|
|
||||||
if key in seen:
|
|
||||||
continue
|
|
||||||
spec = self.get_designated_import_handler_spec(key)
|
|
||||||
if spec and spec not in specs:
|
if spec and spec not in specs:
|
||||||
specs[spec] = self.app.load_object(spec)
|
specs[spec] = self.app.load_object(spec)
|
||||||
seen.add(key)
|
|
||||||
|
|
||||||
# flatten back to simple list of classes
|
# flatten back to simple list of classes
|
||||||
factories = list(specs.values())
|
factories = list(specs.values())
|
||||||
|
|
@ -217,26 +203,22 @@ class WuttaSyncAppProvider(AppProvider):
|
||||||
:param require: Set this to true if you want an error raised
|
:param require: Set this to true if you want an error raised
|
||||||
when no handler is found.
|
when no handler is found.
|
||||||
|
|
||||||
:param \\**kwargs: Remaining kwargs are passed as-is to the
|
|
||||||
handler constructor.
|
|
||||||
|
|
||||||
:returns: The import/export handler instance. If no handler
|
:returns: The import/export handler instance. If no handler
|
||||||
is found, then ``None`` is returned, unless ``require``
|
is found, then ``None`` is returned, unless ``require``
|
||||||
param is true, in which case error is raised.
|
param is true, in which case error is raised.
|
||||||
"""
|
"""
|
||||||
# first try to fetch the handler per designated spec
|
# first try to fetch the handler per designated spec
|
||||||
spec = self.get_designated_import_handler_spec(key)
|
spec = self.get_designated_import_handler_spec(key, **kwargs)
|
||||||
if spec:
|
if spec:
|
||||||
factory = self.app.load_object(spec)
|
factory = self.app.load_object(spec)
|
||||||
return factory(self.config, **kwargs)
|
return factory(self.config)
|
||||||
|
|
||||||
# nothing was designated, so leverage logic which already
|
# nothing was designated, so leverage logic which already
|
||||||
# sorts out which handler is "designated" for given key
|
# sorts out which handler is "designated" for given key
|
||||||
designated = self.get_designated_import_handlers()
|
designated = self.get_designated_import_handlers()
|
||||||
for handler in designated:
|
for handler in designated:
|
||||||
if handler.get_key() == key:
|
if handler.get_key() == key:
|
||||||
factory = type(handler)
|
return handler
|
||||||
return factory(self.config, **kwargs)
|
|
||||||
|
|
||||||
if require:
|
if require:
|
||||||
raise ValueError(f"Cannot locate import handler for key: {key}")
|
raise ValueError(f"Cannot locate import handler for key: {key}")
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,5 @@ from .base import (
|
||||||
|
|
||||||
# nb. must bring in all modules for discovery to work
|
# nb. must bring in all modules for discovery to work
|
||||||
from . import export_csv
|
from . import export_csv
|
||||||
from . import export_wutta
|
|
||||||
from . import import_csv
|
from . import import_csv
|
||||||
from . import import_versions
|
from . import import_versions
|
||||||
from . import import_wutta
|
|
||||||
|
|
|
||||||
|
|
@ -65,13 +65,7 @@ class ImportCommandHandler(GenericHandler):
|
||||||
:param key: Optional :term:`import/export key` to use for handler
|
:param key: Optional :term:`import/export key` to use for handler
|
||||||
lookup. Only used if ``import_handler`` param is not set.
|
lookup. Only used if ``import_handler`` param is not set.
|
||||||
|
|
||||||
:param \\**kwargs: Remaining kwargs are passed as-is to the
|
Typical usage for custom commands will be to provide the spec::
|
||||||
import/export handler constructor, i.e. when making the
|
|
||||||
:attr:`import_handler`. Note that if the ``import_handler``
|
|
||||||
*instance* is specified, these kwargs will be ignored.
|
|
||||||
|
|
||||||
Typical usage for custom commands will be to provide the spec
|
|
||||||
(please note the *colon*)::
|
|
||||||
|
|
||||||
handler = ImportCommandHandler(
|
handler = ImportCommandHandler(
|
||||||
config, "poser.importing.foo:FromFooToPoser"
|
config, "poser.importing.foo:FromFooToPoser"
|
||||||
|
|
@ -87,14 +81,6 @@ class ImportCommandHandler(GenericHandler):
|
||||||
See also
|
See also
|
||||||
:meth:`~wuttasync.app.WuttaSyncAppProvider.get_import_handler()`
|
:meth:`~wuttasync.app.WuttaSyncAppProvider.get_import_handler()`
|
||||||
which does the lookup by key.
|
which does the lookup by key.
|
||||||
|
|
||||||
Additional kwargs may be specified as needed. Typically these
|
|
||||||
should wind up as attributes on the import/export handler
|
|
||||||
instance::
|
|
||||||
|
|
||||||
handler = ImportCommandHandler(
|
|
||||||
config, "poser.importing.foo:FromFooToPoser", dbkey="remote"
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import_handler = None
|
import_handler = None
|
||||||
|
|
@ -103,22 +89,20 @@ class ImportCommandHandler(GenericHandler):
|
||||||
invoked when command runs. See also :meth:`run()`.
|
invoked when command runs. See also :meth:`run()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config, import_handler=None, key=None, **kwargs):
|
def __init__(self, config, import_handler=None, key=None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
if import_handler:
|
if import_handler:
|
||||||
if isinstance(import_handler, ImportHandler):
|
if isinstance(import_handler, ImportHandler):
|
||||||
self.import_handler = import_handler
|
self.import_handler = import_handler
|
||||||
elif callable(import_handler):
|
elif callable(import_handler):
|
||||||
self.import_handler = import_handler(self.config, **kwargs)
|
self.import_handler = import_handler(self.config)
|
||||||
else: # spec
|
else: # spec
|
||||||
factory = self.app.load_object(import_handler)
|
factory = self.app.load_object(import_handler)
|
||||||
self.import_handler = factory(self.config, **kwargs)
|
self.import_handler = factory(self.config)
|
||||||
|
|
||||||
elif key:
|
elif key:
|
||||||
self.import_handler = self.app.get_import_handler(
|
self.import_handler = self.app.get_import_handler(key, require=True)
|
||||||
key, require=True, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self, ctx, progress=None): # pylint: disable=unused-argument
|
def run(self, ctx, progress=None): # pylint: disable=unused-argument
|
||||||
"""
|
"""
|
||||||
|
|
@ -177,7 +161,7 @@ class ImportCommandHandler(GenericHandler):
|
||||||
# sort out which models to process
|
# sort out which models to process
|
||||||
models = kw.pop("models", None)
|
models = kw.pop("models", None)
|
||||||
if not models:
|
if not models:
|
||||||
models = self.import_handler.get_default_importer_keys()
|
models = list(self.import_handler.importers)
|
||||||
log.debug(
|
log.debug(
|
||||||
"%s %s for models: %s",
|
"%s %s for models: %s",
|
||||||
self.import_handler.actioning,
|
self.import_handler.actioning,
|
||||||
|
|
@ -196,31 +180,13 @@ class ImportCommandHandler(GenericHandler):
|
||||||
|
|
||||||
This is what happens when command line has ``--list-models``.
|
This is what happens when command line has ``--list-models``.
|
||||||
"""
|
"""
|
||||||
all_keys = list(self.import_handler.importers)
|
sys.stdout.write("\nALL MODELS:\n")
|
||||||
default_keys = [k for k in all_keys if self.import_handler.is_default(k)]
|
sys.stdout.write("==============================\n")
|
||||||
extra_keys = [k for k in all_keys if k not in default_keys]
|
for key in self.import_handler.importers:
|
||||||
|
sys.stdout.write(key)
|
||||||
sys.stdout.write("\n")
|
sys.stdout.write("\n")
|
||||||
sys.stdout.write("==============================\n")
|
sys.stdout.write("==============================\n")
|
||||||
sys.stdout.write(" DEFAULT MODELS:\n")
|
sys.stdout.write(f"for {self.import_handler.get_title()}\n\n")
|
||||||
sys.stdout.write("==============================\n")
|
|
||||||
if default_keys:
|
|
||||||
for key in default_keys:
|
|
||||||
sys.stdout.write(f"{key}\n")
|
|
||||||
else:
|
|
||||||
sys.stdout.write("(none)\n")
|
|
||||||
|
|
||||||
sys.stdout.write("==============================\n")
|
|
||||||
sys.stdout.write(" EXTRA MODELS:\n")
|
|
||||||
sys.stdout.write("==============================\n")
|
|
||||||
if extra_keys:
|
|
||||||
for key in extra_keys:
|
|
||||||
sys.stdout.write(f"{key}\n")
|
|
||||||
else:
|
|
||||||
sys.stdout.write("(none)\n")
|
|
||||||
|
|
||||||
sys.stdout.write("==============================\n")
|
|
||||||
sys.stdout.write(f" for {self.import_handler.get_title()}\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
def import_command_template( # pylint: disable=unused-argument,too-many-arguments,too-many-positional-arguments,too-many-locals
|
def import_command_template( # pylint: disable=unused-argument,too-many-arguments,too-many-positional-arguments,too-many-locals
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Wutta Framework.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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
|
|
||||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
See also: :ref:`wutta-export-wutta`
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing_extensions import Annotated
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from wuttjamaican.cli import wutta_typer
|
|
||||||
|
|
||||||
from .base import import_command, ImportCommandHandler
|
|
||||||
|
|
||||||
|
|
||||||
@wutta_typer.command()
|
|
||||||
@import_command
|
|
||||||
def export_wutta(
|
|
||||||
ctx: typer.Context,
|
|
||||||
dbkey: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option(help="Config key for app db engine to be used as data target."),
|
|
||||||
] = None,
|
|
||||||
**kwargs,
|
|
||||||
): # pylint: disable=unused-argument
|
|
||||||
"""
|
|
||||||
Export data to another Wutta DB
|
|
||||||
"""
|
|
||||||
config = ctx.parent.wutta_config
|
|
||||||
handler = ImportCommandHandler(
|
|
||||||
config, key="export.to_wutta.from_wutta", dbkey=ctx.params["dbkey"]
|
|
||||||
)
|
|
||||||
handler.run(ctx)
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Wutta Framework.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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
|
|
||||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
See also: :ref:`wutta-import-wutta`
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing_extensions import Annotated
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from wuttjamaican.cli import wutta_typer
|
|
||||||
|
|
||||||
from .base import import_command, ImportCommandHandler
|
|
||||||
|
|
||||||
|
|
||||||
@wutta_typer.command()
|
|
||||||
@import_command
|
|
||||||
def import_wutta(
|
|
||||||
ctx: typer.Context,
|
|
||||||
dbkey: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option(help="Config key for app db engine to be used as data source."),
|
|
||||||
] = None,
|
|
||||||
**kwargs,
|
|
||||||
): # pylint: disable=unused-argument
|
|
||||||
"""
|
|
||||||
Import data from another Wutta DB
|
|
||||||
"""
|
|
||||||
config = ctx.parent.wutta_config
|
|
||||||
handler = ImportCommandHandler(
|
|
||||||
config, key="import.to_wutta.from_wutta", dbkey=ctx.params["dbkey"]
|
|
||||||
)
|
|
||||||
handler.run(ctx)
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Wutta Framework.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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.
|
|
||||||
#
|
|
||||||
# Wutta Framework 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
|
|
||||||
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
WuttaSync config extension
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfigExtension
|
|
||||||
|
|
||||||
|
|
||||||
class WuttaSyncConfig(WuttaConfigExtension):
|
|
||||||
"""
|
|
||||||
Config extension for WuttaSync.
|
|
||||||
|
|
||||||
This just configures some default import/export handlers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "wuttasync"
|
|
||||||
|
|
||||||
def configure(self, config): # pylint: disable=empty-docstring
|
|
||||||
""" """
|
|
||||||
|
|
||||||
# default import/export handlers
|
|
||||||
config.setdefault(
|
|
||||||
"wuttasync.importing.import.to_wutta.from_wutta.default_handler",
|
|
||||||
"wuttasync.importing.wutta:FromWuttaToWuttaImport",
|
|
||||||
)
|
|
||||||
config.setdefault(
|
|
||||||
"wuttasync.importing.export.to_wutta.from_wutta.default_handler",
|
|
||||||
"wuttasync.importing.wutta:FromWuttaToWuttaExport",
|
|
||||||
)
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
|
@ -150,14 +150,6 @@ class ImportExportWarning(EmailSetting):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class export_to_wutta_from_wutta_warning( # pylint: disable=invalid-name
|
|
||||||
ImportExportWarning
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Diff warning for Wutta → Wutta export.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class import_to_versions_from_wutta_warning( # pylint: disable=invalid-name
|
class import_to_versions_from_wutta_warning( # pylint: disable=invalid-name
|
||||||
ImportExportWarning
|
ImportExportWarning
|
||||||
):
|
):
|
||||||
|
|
@ -172,11 +164,3 @@ class import_to_wutta_from_csv_warning( # pylint: disable=invalid-name
|
||||||
"""
|
"""
|
||||||
Diff warning for CSV → Wutta import.
|
Diff warning for CSV → Wutta import.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class import_to_wutta_from_wutta_warning( # pylint: disable=invalid-name
|
|
||||||
ImportExportWarning
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Diff warning for Wutta → Wutta import.
|
|
||||||
"""
|
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ class FromCsvToSqlalchemyHandlerMixin:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# TODO: pylint (correctly) flags this as duplicate code, matching
|
# TODO: pylint (correctly) flags this as duplicate code, matching
|
||||||
# on the wuttasync.importing.versions/wutta module - should fix?
|
# on the wuttasync.importing.versions module - should fix?
|
||||||
def define_importers(self):
|
def define_importers(self):
|
||||||
"""
|
"""
|
||||||
This mixin overrides typical (manual) importer definition, and
|
This mixin overrides typical (manual) importer definition, and
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
|
@ -203,12 +203,7 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc
|
||||||
|
|
||||||
def __init__(self, config, **kwargs):
|
def __init__(self, config, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
super().__init__(config)
|
super().__init__(config, **kwargs)
|
||||||
|
|
||||||
# callers can set any attrs they want
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
self.importers = self.define_importers()
|
self.importers = self.define_importers()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
@ -371,7 +366,7 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc
|
||||||
changes = OrderedDict()
|
changes = OrderedDict()
|
||||||
|
|
||||||
if not keys:
|
if not keys:
|
||||||
keys = self.get_default_importer_keys()
|
keys = list(self.importers)
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
|
|
@ -655,36 +650,6 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc
|
||||||
"""
|
"""
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def is_default(self, key):
|
|
||||||
"""
|
|
||||||
Return a boolean indicating whether the importer corresponding
|
|
||||||
to ``key`` should be considered "default" - i.e. included as
|
|
||||||
part of a typical "import all" job.
|
|
||||||
|
|
||||||
The default logic here returns ``True`` in all cases; subclass can
|
|
||||||
override as needed.
|
|
||||||
|
|
||||||
:param key: Key indicating the importer.
|
|
||||||
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_default_importer_keys(self):
|
|
||||||
"""
|
|
||||||
Return the list of importer keys which should be considered
|
|
||||||
"default" - i.e. which should be included as part of a typical
|
|
||||||
"import all" job.
|
|
||||||
|
|
||||||
This inspects :attr:`importers` and calls :meth:`is_default()`
|
|
||||||
for each, to determine the result.
|
|
||||||
|
|
||||||
:returns: List of importer keys (strings).
|
|
||||||
"""
|
|
||||||
keys = list(self.importers)
|
|
||||||
keys = [k for k in keys if self.is_default(k)]
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def process_changes(self, changes):
|
def process_changes(self, changes):
|
||||||
"""
|
"""
|
||||||
Run post-processing operations on the given changes, if
|
Run post-processing operations on the given changes, if
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
# TODO: pylint (correctly) flags this as duplicate code, matching
|
# TODO: pylint (correctly) flags this as duplicate code, matching
|
||||||
# on the wuttasync.importing.csv/wutta module - should fix?
|
# on the wuttasync.importing.csv module - should fix?
|
||||||
def define_importers(self):
|
def define_importers(self):
|
||||||
"""
|
"""
|
||||||
This overrides typical (manual) importer definition, instead
|
This overrides typical (manual) importer definition, instead
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
# WuttaSync -- Wutta Framework for data import/export and real-time sync
|
||||||
# Copyright © 2024-2026 Lance Edgar
|
# Copyright © 2024-2025 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
|
@ -24,172 +24,10 @@
|
||||||
Wutta → Wutta import/export
|
Wutta → Wutta import/export
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from sqlalchemy_utils.functions import get_primary_keys
|
|
||||||
|
|
||||||
from wuttjamaican.db.util import make_topo_sortkey
|
|
||||||
|
|
||||||
from .base import FromSqlalchemyMirror
|
from .base import FromSqlalchemyMirror
|
||||||
from .model import ToWutta
|
|
||||||
from .handlers import FromWuttaHandler, ToWuttaHandler, Orientation
|
|
||||||
|
|
||||||
|
|
||||||
class FromWuttaMirror(FromSqlalchemyMirror): # pylint: disable=abstract-method
|
class FromWuttaMirror(FromSqlalchemyMirror): # pylint: disable=abstract-method
|
||||||
"""
|
"""
|
||||||
Base class for Wutta → Wutta data :term:`importers/exporters
|
Base class for Wutta -> Wutta data importers.
|
||||||
<importer>`.
|
|
||||||
|
|
||||||
This inherits from
|
|
||||||
:class:`~wuttasync.importing.base.FromSqlalchemyMirror`.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class FromWuttaToWuttaBase(FromWuttaHandler, ToWuttaHandler):
|
|
||||||
"""
|
|
||||||
Base class for Wutta → Wutta data :term:`import/export handlers
|
|
||||||
<import handler>`.
|
|
||||||
|
|
||||||
This inherits from
|
|
||||||
:class:`~wuttasync.importing.handlers.FromWuttaHandler` and
|
|
||||||
:class:`~wuttasync.importing.handlers.ToWuttaHandler`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
dbkey = None
|
|
||||||
"""
|
|
||||||
Config key for the "other" (non-local) :term:`app database`.
|
|
||||||
Depending on context this will represent either the source or
|
|
||||||
target for import/export.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_target_model(self): # pylint: disable=missing-function-docstring
|
|
||||||
return self.app.model
|
|
||||||
|
|
||||||
# TODO: pylint (correctly) flags this as duplicate code, matching
|
|
||||||
# on the wuttasync.importing.csv/versions module - should fix?
|
|
||||||
def define_importers(self):
|
|
||||||
"""
|
|
||||||
This overrides typical (manual) importer definition, and
|
|
||||||
instead dynamically generates a set of importers, e.g. one per
|
|
||||||
table in the target DB.
|
|
||||||
|
|
||||||
It does this by calling :meth:`make_importer_factory()` for
|
|
||||||
each class found in the :term:`app model`.
|
|
||||||
"""
|
|
||||||
importers = {}
|
|
||||||
model = self.get_target_model()
|
|
||||||
|
|
||||||
# pylint: disable=duplicate-code
|
|
||||||
# mostly try to make an importer for every data model
|
|
||||||
for name in dir(model):
|
|
||||||
cls = getattr(model, name)
|
|
||||||
if (
|
|
||||||
isinstance(cls, type)
|
|
||||||
and issubclass(cls, model.Base)
|
|
||||||
and cls is not model.Base
|
|
||||||
):
|
|
||||||
importers[name] = self.make_importer_factory(cls, name)
|
|
||||||
|
|
||||||
# sort importers according to schema topography
|
|
||||||
topo_sortkey = make_topo_sortkey(model)
|
|
||||||
importers = OrderedDict(
|
|
||||||
[(name, importers[name]) for name in sorted(importers, key=topo_sortkey)]
|
|
||||||
)
|
|
||||||
|
|
||||||
return importers
|
|
||||||
|
|
||||||
def make_importer_factory(self, model_class, name):
|
|
||||||
"""
|
|
||||||
Generate and return a new :term:`importer` class, targeting
|
|
||||||
the given :term:`data model` class.
|
|
||||||
|
|
||||||
The newly-created class will inherit from:
|
|
||||||
|
|
||||||
* :class:`FromWuttaMirror`
|
|
||||||
* :class:`~wuttasync.importing.model.ToWutta`
|
|
||||||
|
|
||||||
:param model_class: A data model class.
|
|
||||||
|
|
||||||
:param name: The "model name" for the importer/exporter. New
|
|
||||||
class name will be based on this, so e.g. ``Widget`` model
|
|
||||||
name becomes ``WidgetImporter`` class name.
|
|
||||||
|
|
||||||
:returns: The new class, meant to process import/export
|
|
||||||
targeting the given data model.
|
|
||||||
"""
|
|
||||||
return type(
|
|
||||||
f"{name}Importer",
|
|
||||||
(FromWuttaMirror, ToWutta),
|
|
||||||
{
|
|
||||||
"model_class": model_class,
|
|
||||||
"key": list(get_primary_keys(model_class)),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_default(self, key):
|
|
||||||
""" """
|
|
||||||
special = [
|
|
||||||
"Setting",
|
|
||||||
"Role",
|
|
||||||
"Permission",
|
|
||||||
"User",
|
|
||||||
"UserRole",
|
|
||||||
"UserAPIToken",
|
|
||||||
"Upgrade",
|
|
||||||
]
|
|
||||||
if key in special:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class FromWuttaToWuttaImport(FromWuttaToWuttaBase):
|
|
||||||
"""
|
|
||||||
Handler for Wutta (other) → Wutta (local) data import.
|
|
||||||
|
|
||||||
This inherits from :class:`FromWuttaToWuttaBase`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
orientation = Orientation.IMPORT
|
|
||||||
""" """ # nb. suppress docs
|
|
||||||
|
|
||||||
def make_source_session(self):
|
|
||||||
"""
|
|
||||||
This makes a "normal" :term:`db session`, but will use the
|
|
||||||
engine corresponding to the
|
|
||||||
:attr:`~FromWuttaToWuttaBase.dbkey`.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
not self.dbkey
|
|
||||||
or self.dbkey == "default"
|
|
||||||
or self.dbkey not in self.config.appdb_engines
|
|
||||||
):
|
|
||||||
raise ValueError(f"dbkey is not valid: {self.dbkey}")
|
|
||||||
engine = self.config.appdb_engines[self.dbkey]
|
|
||||||
return self.app.make_session(bind=engine)
|
|
||||||
|
|
||||||
|
|
||||||
class FromWuttaToWuttaExport(FromWuttaToWuttaBase):
|
|
||||||
"""
|
|
||||||
Handler for Wutta (local) → Wutta (other) data export.
|
|
||||||
|
|
||||||
This inherits from :class:`FromWuttaToWuttaBase`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
orientation = Orientation.EXPORT
|
|
||||||
""" """ # nb. suppress docs
|
|
||||||
|
|
||||||
def make_target_session(self):
|
|
||||||
"""
|
|
||||||
This makes a "normal" :term:`db session`, but will use the
|
|
||||||
engine corresponding to the
|
|
||||||
:attr:`~FromWuttaToWuttaBase.dbkey`.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
not self.dbkey
|
|
||||||
or self.dbkey == "default"
|
|
||||||
or self.dbkey not in self.config.appdb_engines
|
|
||||||
):
|
|
||||||
raise ValueError(f"dbkey is not valid: {self.dbkey}")
|
|
||||||
engine = self.config.appdb_engines[self.dbkey]
|
|
||||||
return self.app.make_session(bind=engine)
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch, Mock, call
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from wuttasync.cli import base as mod
|
from wuttasync.cli import base as mod
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
@ -25,55 +25,19 @@ class TestImportCommandHandler(DataTestCase):
|
||||||
# as spec
|
# as spec
|
||||||
handler = self.make_handler(import_handler=FromCsvToWutta.get_spec())
|
handler = self.make_handler(import_handler=FromCsvToWutta.get_spec())
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertFalse(hasattr(handler.import_handler, "foo"))
|
|
||||||
|
|
||||||
# as spec, w/ kwargs
|
|
||||||
handler = self.make_handler(import_handler=FromCsvToWutta.get_spec(), foo="bar")
|
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertTrue(hasattr(handler.import_handler, "foo"))
|
|
||||||
self.assertEqual(handler.import_handler.foo, "bar")
|
|
||||||
|
|
||||||
# as factory
|
# as factory
|
||||||
handler = self.make_handler(import_handler=FromCsvToWutta)
|
handler = self.make_handler(import_handler=FromCsvToWutta)
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertFalse(hasattr(handler.import_handler, "foo"))
|
|
||||||
|
|
||||||
# as factory, w/ kwargs
|
|
||||||
handler = self.make_handler(import_handler=FromCsvToWutta, foo="bar")
|
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertTrue(hasattr(handler.import_handler, "foo"))
|
|
||||||
self.assertEqual(handler.import_handler.foo, "bar")
|
|
||||||
|
|
||||||
# as instance
|
# as instance
|
||||||
myhandler = FromCsvToWutta(self.config)
|
myhandler = FromCsvToWutta(self.config)
|
||||||
handler = self.make_handler(import_handler=myhandler)
|
handler = self.make_handler(import_handler=myhandler)
|
||||||
self.assertIs(handler.import_handler, myhandler)
|
self.assertIs(handler.import_handler, myhandler)
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertFalse(hasattr(handler.import_handler, "foo"))
|
|
||||||
|
|
||||||
# as instance, w/ kwargs (which are ignored)
|
|
||||||
myhandler = FromCsvToWutta(self.config)
|
|
||||||
handler = self.make_handler(import_handler=myhandler, foo="bar")
|
|
||||||
self.assertIs(handler.import_handler, myhandler)
|
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertFalse(hasattr(handler.import_handler, "foo"))
|
|
||||||
|
|
||||||
# as key
|
# as key
|
||||||
handler = self.make_handler(key="import.to_wutta.from_csv")
|
handler = self.make_handler(key="import.to_wutta.from_csv")
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertFalse(hasattr(handler.import_handler, "foo"))
|
|
||||||
|
|
||||||
# as key, w/ kwargs
|
|
||||||
handler = self.make_handler(key="import.to_wutta.from_csv", foo="bar")
|
|
||||||
self.assertIsInstance(handler.import_handler, FromCsvToWutta)
|
|
||||||
self.assertFalse(hasattr(handler, "foo"))
|
|
||||||
self.assertTrue(hasattr(handler.import_handler, "foo"))
|
|
||||||
self.assertEqual(handler.import_handler.foo, "bar")
|
|
||||||
|
|
||||||
def test_run(self):
|
def test_run(self):
|
||||||
handler = self.make_handler(
|
handler = self.make_handler(
|
||||||
|
|
@ -169,60 +133,22 @@ class TestImportCommandHandler(DataTestCase):
|
||||||
params={},
|
params={},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
# self.assertRaises(FileNotFoundError, handler.run, ctx)
|
||||||
handler.run(ctx)
|
handler.run(ctx)
|
||||||
exit_.assert_not_called()
|
exit_.assert_not_called()
|
||||||
|
|
||||||
def test_list_models(self):
|
def test_list_models(self):
|
||||||
|
|
||||||
# CSV -> Wutta (all importers are default)
|
|
||||||
handler = self.make_handler(
|
handler = self.make_handler(
|
||||||
import_handler="wuttasync.importing.csv:FromCsvToWutta"
|
import_handler="wuttasync.importing.csv:FromCsvToWutta"
|
||||||
)
|
)
|
||||||
with patch.object(mod, "sys") as sys:
|
|
||||||
handler.list_models({})
|
|
||||||
sys.stdout.write.assert_has_calls(
|
|
||||||
[
|
|
||||||
call("==============================\n"),
|
|
||||||
call(" EXTRA MODELS:\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
call("(none)\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wutta -> Wutta (only Person importer is default)
|
|
||||||
handler = self.make_handler(
|
|
||||||
import_handler="wuttasync.importing.wutta:FromWuttaToWuttaImport"
|
|
||||||
)
|
|
||||||
with patch.object(mod, "sys") as sys:
|
with patch.object(mod, "sys") as sys:
|
||||||
handler.list_models({})
|
handler.list_models({})
|
||||||
sys.stdout.write.assert_has_calls(
|
# just test a few random things we expect to see
|
||||||
[
|
self.assertTrue(sys.stdout.write.has_call("ALL MODELS:\n"))
|
||||||
call("==============================\n"),
|
self.assertTrue(sys.stdout.write.has_call("Person"))
|
||||||
call(" DEFAULT MODELS:\n"),
|
self.assertTrue(sys.stdout.write.has_call("User"))
|
||||||
call("==============================\n"),
|
self.assertTrue(sys.stdout.write.has_call("Upgrade"))
|
||||||
call("Person\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
call(" EXTRA MODELS:\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# again, but pretend there are no default importers
|
|
||||||
with patch.object(handler.import_handler, "is_default", return_value=False):
|
|
||||||
with patch.object(mod, "sys") as sys:
|
|
||||||
handler.list_models({})
|
|
||||||
sys.stdout.write.assert_has_calls(
|
|
||||||
[
|
|
||||||
call("==============================\n"),
|
|
||||||
call(" DEFAULT MODELS:\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
call("(none)\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
call(" EXTRA MODELS:\n"),
|
|
||||||
call("==============================\n"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestImporterCommand(TestCase):
|
class TestImporterCommand(TestCase):
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from wuttasync.cli import export_wutta as mod, ImportCommandHandler
|
|
||||||
|
|
||||||
|
|
||||||
class TestExportWutta(TestCase):
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
params = {
|
|
||||||
"dbkey": "another",
|
|
||||||
"models": [],
|
|
||||||
"create": True,
|
|
||||||
"update": True,
|
|
||||||
"delete": False,
|
|
||||||
"dry_run": True,
|
|
||||||
}
|
|
||||||
ctx = MagicMock(params=params)
|
|
||||||
with patch.object(ImportCommandHandler, "run") as run:
|
|
||||||
mod.export_wutta(ctx)
|
|
||||||
run.assert_called_once_with(ctx)
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from wuttasync.cli import import_wutta as mod, ImportCommandHandler
|
|
||||||
|
|
||||||
|
|
||||||
class TestImportWutta(TestCase):
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
params = {
|
|
||||||
"dbkey": "another",
|
|
||||||
"models": [],
|
|
||||||
"create": True,
|
|
||||||
"update": True,
|
|
||||||
"delete": False,
|
|
||||||
"dry_run": True,
|
|
||||||
}
|
|
||||||
ctx = MagicMock(params=params)
|
|
||||||
with patch.object(ImportCommandHandler, "run") as run:
|
|
||||||
mod.import_wutta(ctx)
|
|
||||||
run.assert_called_once_with(ctx)
|
|
||||||
|
|
@ -7,7 +7,6 @@ from uuid import UUID
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
||||||
from wuttasync.importing import handlers as mod, Importer, ToSqlalchemy
|
from wuttasync.importing import handlers as mod, Importer, ToSqlalchemy
|
||||||
from wuttasync.importing.wutta import FromWuttaToWuttaImport
|
|
||||||
|
|
||||||
|
|
||||||
class FromFooToBar(mod.ImportHandler):
|
class FromFooToBar(mod.ImportHandler):
|
||||||
|
|
@ -20,17 +19,6 @@ class TestImportHandler(DataTestCase):
|
||||||
def make_handler(self, **kwargs):
|
def make_handler(self, **kwargs):
|
||||||
return mod.ImportHandler(self.config, **kwargs)
|
return mod.ImportHandler(self.config, **kwargs)
|
||||||
|
|
||||||
def test_constructor(self):
|
|
||||||
|
|
||||||
# attr missing by default
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertFalse(hasattr(handler, "some_foo_attr"))
|
|
||||||
|
|
||||||
# but constructor can set it
|
|
||||||
handler = self.make_handler(some_foo_attr="bar")
|
|
||||||
self.assertTrue(hasattr(handler, "some_foo_attr"))
|
|
||||||
self.assertEqual(handler.some_foo_attr, "bar")
|
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
self.assertEqual(str(handler), "None → None")
|
self.assertEqual(str(handler), "None → None")
|
||||||
|
|
@ -254,25 +242,6 @@ class TestImportHandler(DataTestCase):
|
||||||
KeyError, handler.get_importer, "BunchOfNonsense", model_class=model.Setting
|
KeyError, handler.get_importer, "BunchOfNonsense", model_class=model.Setting
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_is_default(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
# nb. anything is considered default, by default
|
|
||||||
self.assertTrue(handler.is_default("there_is_no_way_this_is_valid"))
|
|
||||||
|
|
||||||
def test_get_default_importer_keys(self):
|
|
||||||
|
|
||||||
# use handler which already has some non/default keys
|
|
||||||
handler = FromWuttaToWuttaImport(self.config)
|
|
||||||
|
|
||||||
# it supports many importers
|
|
||||||
self.assertIn("Person", handler.importers)
|
|
||||||
self.assertIn("User", handler.importers)
|
|
||||||
self.assertIn("Setting", handler.importers)
|
|
||||||
|
|
||||||
# but only Person is default
|
|
||||||
keys = handler.get_default_importer_keys()
|
|
||||||
self.assertEqual(keys, ["Person"])
|
|
||||||
|
|
||||||
def test_get_warnings_email_key(self):
|
def test_get_warnings_email_key(self):
|
||||||
handler = FromFooToBar(self.config)
|
handler = FromFooToBar(self.config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,3 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
|
||||||
|
|
||||||
from wuttasync.importing import wutta as mod
|
from wuttasync.importing import wutta as mod
|
||||||
from wuttasync.importing import ToWutta
|
|
||||||
|
|
||||||
|
|
||||||
class TestFromWuttaMirror(DataTestCase):
|
|
||||||
|
|
||||||
def make_importer(self, **kwargs):
|
|
||||||
return mod.FromWuttaMirror(self.config, **kwargs)
|
|
||||||
|
|
||||||
def test_basic(self):
|
|
||||||
importer = self.make_importer()
|
|
||||||
self.assertIsInstance(importer, mod.FromWuttaMirror)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFromWuttaToWuttaBase(DataTestCase):
|
|
||||||
|
|
||||||
def make_handler(self, **kwargs):
|
|
||||||
return mod.FromWuttaToWuttaBase(self.config, **kwargs)
|
|
||||||
|
|
||||||
def test_dbkey(self):
|
|
||||||
|
|
||||||
# null by default
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertIsNone(handler.dbkey)
|
|
||||||
|
|
||||||
# but caller can specify
|
|
||||||
handler = self.make_handler(dbkey="another")
|
|
||||||
self.assertEqual(handler.dbkey, "another")
|
|
||||||
|
|
||||||
def test_make_importer_factory(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# returns a typical importer
|
|
||||||
factory = handler.make_importer_factory(model.User, "User")
|
|
||||||
self.assertTrue(issubclass(factory, mod.FromWuttaMirror))
|
|
||||||
self.assertTrue(issubclass(factory, ToWutta))
|
|
||||||
self.assertIs(factory.model_class, model.User)
|
|
||||||
self.assertEqual(factory.__name__, "UserImporter")
|
|
||||||
|
|
||||||
def test_define_importers(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# all models are included
|
|
||||||
importers = handler.define_importers()
|
|
||||||
self.assertIn("Setting", importers)
|
|
||||||
self.assertIn("Person", importers)
|
|
||||||
self.assertIn("Role", importers)
|
|
||||||
self.assertIn("Permission", importers)
|
|
||||||
self.assertIn("User", importers)
|
|
||||||
self.assertIn("UserRole", importers)
|
|
||||||
self.assertIn("UserAPIToken", importers)
|
|
||||||
self.assertIn("Upgrade", importers)
|
|
||||||
self.assertNotIn("BatchMixin", importers)
|
|
||||||
self.assertNotIn("BatchRowMixin", importers)
|
|
||||||
self.assertNotIn("Base", importers)
|
|
||||||
|
|
||||||
# also, dependencies are implied by sort order
|
|
||||||
models = list(importers)
|
|
||||||
self.assertLess(models.index("Person"), models.index("User"))
|
|
||||||
self.assertLess(models.index("User"), models.index("UserRole"))
|
|
||||||
self.assertLess(models.index("User"), models.index("Upgrade"))
|
|
||||||
|
|
||||||
|
|
||||||
class TestFromWuttaToWuttaImport(DataTestCase):
|
|
||||||
|
|
||||||
def make_handler(self, **kwargs):
|
|
||||||
return mod.FromWuttaToWuttaImport(self.config, **kwargs)
|
|
||||||
|
|
||||||
def test_make_source_session(self):
|
|
||||||
|
|
||||||
# error if null dbkey
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertIsNone(handler.dbkey)
|
|
||||||
self.assertRaises(ValueError, handler.make_source_session)
|
|
||||||
|
|
||||||
# error if dbkey not found
|
|
||||||
handler = self.make_handler(dbkey="another")
|
|
||||||
self.assertEqual(handler.dbkey, "another")
|
|
||||||
self.assertNotIn("another", self.config.appdb_engines)
|
|
||||||
self.assertRaises(ValueError, handler.make_source_session)
|
|
||||||
|
|
||||||
# error if dbkey is 'default'
|
|
||||||
handler = self.make_handler(dbkey="default")
|
|
||||||
self.assertEqual(handler.dbkey, "default")
|
|
||||||
self.assertIn("default", self.config.appdb_engines)
|
|
||||||
self.assertRaises(ValueError, handler.make_source_session)
|
|
||||||
|
|
||||||
# expected behavior
|
|
||||||
another_engine = sa.create_engine("sqlite://")
|
|
||||||
handler = self.make_handler(dbkey="another")
|
|
||||||
with patch.dict(self.config.appdb_engines, {"another": another_engine}):
|
|
||||||
session = handler.make_source_session()
|
|
||||||
self.assertIs(session.bind, another_engine)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFromWuttaToWuttaExport(DataTestCase):
|
|
||||||
|
|
||||||
def make_handler(self, **kwargs):
|
|
||||||
return mod.FromWuttaToWuttaExport(self.config, **kwargs)
|
|
||||||
|
|
||||||
def test_make_target_session(self):
|
|
||||||
|
|
||||||
# error if null dbkey
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertIsNone(handler.dbkey)
|
|
||||||
self.assertRaises(ValueError, handler.make_target_session)
|
|
||||||
|
|
||||||
# error if dbkey not found
|
|
||||||
handler = self.make_handler(dbkey="another")
|
|
||||||
self.assertEqual(handler.dbkey, "another")
|
|
||||||
self.assertNotIn("another", self.config.appdb_engines)
|
|
||||||
self.assertRaises(ValueError, handler.make_target_session)
|
|
||||||
|
|
||||||
# error if dbkey is 'default'
|
|
||||||
handler = self.make_handler(dbkey="default")
|
|
||||||
self.assertEqual(handler.dbkey, "default")
|
|
||||||
self.assertIn("default", self.config.appdb_engines)
|
|
||||||
self.assertRaises(ValueError, handler.make_target_session)
|
|
||||||
|
|
||||||
# expected behavior
|
|
||||||
another_engine = sa.create_engine("sqlite://")
|
|
||||||
handler = self.make_handler(dbkey="another")
|
|
||||||
with patch.dict(self.config.appdb_engines, {"another": another_engine}):
|
|
||||||
session = handler.make_target_session()
|
|
||||||
self.assertIs(session.bind, another_engine)
|
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,6 @@ class TestWuttaSyncAppProvider(ConfigTestCase):
|
||||||
self.assertIn(FromCsvToWutta, handlers)
|
self.assertIn(FromCsvToWutta, handlers)
|
||||||
self.assertIn(FromFooToBar, handlers)
|
self.assertIn(FromFooToBar, handlers)
|
||||||
|
|
||||||
# now for something completely different..here we pretend there
|
|
||||||
# are multiple handler entry points with same key. all should
|
|
||||||
# be returned, including both which share the key.
|
|
||||||
entry_points = {
|
|
||||||
"import.to_baz.from_foo": [FromFooToBaz1, FromFooToBaz2],
|
|
||||||
}
|
|
||||||
with patch.object(mod, "load_entry_points", return_value=entry_points):
|
|
||||||
handlers = self.app.get_all_import_handlers()
|
|
||||||
self.assertEqual(len(handlers), 2)
|
|
||||||
self.assertIn(FromFooToBaz1, handlers)
|
|
||||||
self.assertIn(FromFooToBaz2, handlers)
|
|
||||||
|
|
||||||
def test_get_designated_import_handler_spec(self):
|
def test_get_designated_import_handler_spec(self):
|
||||||
|
|
||||||
# fetch of unknown key returns none
|
# fetch of unknown key returns none
|
||||||
|
|
@ -151,14 +139,6 @@ class TestWuttaSyncAppProvider(ConfigTestCase):
|
||||||
handler = self.app.get_import_handler("import.to_wutta.from_csv")
|
handler = self.app.get_import_handler("import.to_wutta.from_csv")
|
||||||
self.assertIsInstance(handler, FromCsvToWutta)
|
self.assertIsInstance(handler, FromCsvToWutta)
|
||||||
self.assertIsInstance(handler, FromCsvToPoser)
|
self.assertIsInstance(handler, FromCsvToPoser)
|
||||||
self.assertFalse(hasattr(handler, "foo_attr"))
|
|
||||||
|
|
||||||
# can pass extra kwargs
|
|
||||||
handler = self.app.get_import_handler(
|
|
||||||
"import.to_wutta.from_csv", foo_attr="whatever"
|
|
||||||
)
|
|
||||||
self.assertTrue(hasattr(handler, "foo_attr"))
|
|
||||||
self.assertEqual(handler.foo_attr, "whatever")
|
|
||||||
|
|
||||||
# unknown importer cannot be found
|
# unknown importer cannot be found
|
||||||
handler = self.app.get_import_handler("bogus")
|
handler = self.app.get_import_handler("bogus")
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from wuttjamaican.testing import ConfigTestCase
|
|
||||||
|
|
||||||
from wuttasync import conf as mod
|
|
||||||
|
|
||||||
|
|
||||||
class TestWuttaSyncConfig(ConfigTestCase):
|
|
||||||
|
|
||||||
def make_extension(self):
|
|
||||||
return mod.WuttaSyncConfig()
|
|
||||||
|
|
||||||
def test_default_import_handlers(self):
|
|
||||||
|
|
||||||
# base config has no default handlers
|
|
||||||
spec = self.config.get(
|
|
||||||
"wuttasync.importing.import.to_wutta.from_wutta.default_handler"
|
|
||||||
)
|
|
||||||
self.assertIsNone(spec)
|
|
||||||
spec = self.config.get(
|
|
||||||
"wuttasync.importing.export.to_wutta.from_wutta.default_handler"
|
|
||||||
)
|
|
||||||
self.assertIsNone(spec)
|
|
||||||
|
|
||||||
# extend config
|
|
||||||
ext = self.make_extension()
|
|
||||||
ext.configure(self.config)
|
|
||||||
|
|
||||||
# config now has default handlers
|
|
||||||
spec = self.config.get(
|
|
||||||
"wuttasync.importing.import.to_wutta.from_wutta.default_handler"
|
|
||||||
)
|
|
||||||
self.assertIsNotNone(spec)
|
|
||||||
self.assertEqual(spec, "wuttasync.importing.wutta:FromWuttaToWuttaImport")
|
|
||||||
spec = self.config.get(
|
|
||||||
"wuttasync.importing.export.to_wutta.from_wutta.default_handler"
|
|
||||||
)
|
|
||||||
self.assertIsNotNone(spec)
|
|
||||||
self.assertEqual(spec, "wuttasync.importing.wutta:FromWuttaToWuttaExport")
|
|
||||||
|
|
@ -5,7 +5,6 @@ from wuttjamaican.testing import ConfigTestCase
|
||||||
from wuttasync import emails as mod
|
from wuttasync import emails as mod
|
||||||
from wuttasync.importing import ImportHandler
|
from wuttasync.importing import ImportHandler
|
||||||
from wuttasync.testing import ImportExportWarningTestCase
|
from wuttasync.testing import ImportExportWarningTestCase
|
||||||
from wuttasync.conf import WuttaSyncConfig
|
|
||||||
|
|
||||||
|
|
||||||
class FromFooToWutta(ImportHandler):
|
class FromFooToWutta(ImportHandler):
|
||||||
|
|
@ -75,24 +74,8 @@ class TestImportExportWarning(ConfigTestCase):
|
||||||
|
|
||||||
class TestEmailSettings(ImportExportWarningTestCase):
|
class TestEmailSettings(ImportExportWarningTestCase):
|
||||||
|
|
||||||
def make_config(self, files=None, **kwargs):
|
|
||||||
config = super().make_config(files, **kwargs)
|
|
||||||
|
|
||||||
# need this to ensure default import/export handlers. since
|
|
||||||
# behavior can vary depending on what packages are installed.
|
|
||||||
ext = WuttaSyncConfig()
|
|
||||||
ext.configure(config)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
def test_export_to_wutta_from_wutta_warning(self):
|
|
||||||
self.do_test_preview("export_to_wutta_from_wutta_warning")
|
|
||||||
|
|
||||||
def test_import_to_versions_from_wutta_warning(self):
|
def test_import_to_versions_from_wutta_warning(self):
|
||||||
self.do_test_preview("import_to_versions_from_wutta_warning")
|
self.do_test_preview("import_to_versions_from_wutta_warning")
|
||||||
|
|
||||||
def test_import_to_wutta_from_csv_warning(self):
|
def test_import_to_wutta_from_csv_warning(self):
|
||||||
self.do_test_preview("import_to_wutta_from_csv_warning")
|
self.do_test_preview("import_to_wutta_from_csv_warning")
|
||||||
|
|
||||||
def test_import_to_wutta_from_wutta_warning(self):
|
|
||||||
self.do_test_preview("import_to_wutta_from_wutta_warning")
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue