diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af025c..e100544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/) 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) ### Fix diff --git a/docs/api/wuttasync.cli.export_wutta.rst b/docs/api/wuttasync.cli.export_wutta.rst deleted file mode 100644 index cb3caf8..0000000 --- a/docs/api/wuttasync.cli.export_wutta.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttasync.cli.export_wutta`` -============================== - -.. automodule:: wuttasync.cli.export_wutta - :members: diff --git a/docs/api/wuttasync.cli.import_wutta.rst b/docs/api/wuttasync.cli.import_wutta.rst deleted file mode 100644 index 466a726..0000000 --- a/docs/api/wuttasync.cli.import_wutta.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttasync.cli.import_wutta`` -============================== - -.. automodule:: wuttasync.cli.import_wutta - :members: diff --git a/docs/api/wuttasync.conf.rst b/docs/api/wuttasync.conf.rst deleted file mode 100644 index 7533c9f..0000000 --- a/docs/api/wuttasync.conf.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttasync.conf`` -================== - -.. automodule:: wuttasync.conf - :members: diff --git a/docs/index.rst b/docs/index.rst index 36d0e25..215e892 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,11 +74,8 @@ cf. :doc:`rattail-manual:data/sync/index`. api/wuttasync.cli api/wuttasync.cli.base api/wuttasync.cli.export_csv - api/wuttasync.cli.export_wutta api/wuttasync.cli.import_csv api/wuttasync.cli.import_versions - api/wuttasync.cli.import_wutta - api/wuttasync.conf api/wuttasync.emails api/wuttasync.exporting api/wuttasync.exporting.base diff --git a/docs/narr/cli/builtin.rst b/docs/narr/cli/builtin.rst index 399c2d7..5cb3123 100644 --- a/docs/narr/cli/builtin.rst +++ b/docs/narr/cli/builtin.rst @@ -27,18 +27,6 @@ Defined in: :mod:`wuttasync.cli.export_csv` .. 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`` @@ -76,15 +64,3 @@ in the :term:`app model`. Defined in: :mod:`wuttasync.cli.import_versions` .. 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 diff --git a/pyproject.toml b/pyproject.toml index 4e6e0a4..d3db422 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttaSync" -version = "0.6.0" +version = "0.5.1" description = "Wutta Framework for data import/export and real-time sync" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] @@ -30,7 +30,7 @@ dependencies = [ "makefun", "rich", "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"] wuttasync = "wuttasync.app:WuttaSyncAppProvider" -[project.entry-points."wutta.config.extensions"] -"wuttasync" = "wuttasync.conf:WuttaSyncConfig" - [project.entry-points."wuttasync.importing"] "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_wutta.from_csv" = "wuttasync.importing.csv:FromCsvToWutta" -"import.to_wutta.from_wutta" = "wuttasync.importing.wutta:FromWuttaToWuttaImport" [project.entry-points."wutta.typer_imports"] wuttasync = "wuttasync.cli" diff --git a/src/wuttasync/app.py b/src/wuttasync/app.py index a73b26e..0fa19fd 100644 --- a/src/wuttasync/app.py +++ b/src/wuttasync/app.py @@ -2,7 +2,7 @@ ################################################################################ # # 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. # @@ -87,32 +87,18 @@ class WuttaSyncAppProvider(AppProvider): :returns: List of all import/export handler classes """ - # first load all "registered" Handler classes. note we must - # specify lists=True since handlers from different projects - # can be registered with the same key. - factory_lists = load_entry_points( - "wuttasync.importing", lists=True, ignore_errors=True - ) + # first load all "registered" Handler classes + factories = load_entry_points("wuttasync.importing", ignore_errors=True) # organize registered classes by spec - specs = {} - all_factories = [] - for factories in factory_lists.values(): - for factory in factories: - specs[factory.get_spec()] = factory - all_factories.append(factory) + specs = {factory.get_spec(): factory for factory in factories.values()} # many handlers may not be registered per se, but may be # designated via config. so try to include those too - seen = set() - for factory in all_factories: - key = factory.get_key() - if key in seen: - continue - spec = self.get_designated_import_handler_spec(key) + for factory in factories.values(): + spec = self.get_designated_import_handler_spec(factory.get_key()) if spec and spec not in specs: specs[spec] = self.app.load_object(spec) - seen.add(key) # flatten back to simple list of classes factories = list(specs.values()) @@ -217,26 +203,22 @@ class WuttaSyncAppProvider(AppProvider): :param require: Set this to true if you want an error raised 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 is found, then ``None`` is returned, unless ``require`` param is true, in which case error is raised. """ # 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: factory = self.app.load_object(spec) - return factory(self.config, **kwargs) + return factory(self.config) # nothing was designated, so leverage logic which already # sorts out which handler is "designated" for given key designated = self.get_designated_import_handlers() for handler in designated: if handler.get_key() == key: - factory = type(handler) - return factory(self.config, **kwargs) + return handler if require: raise ValueError(f"Cannot locate import handler for key: {key}") diff --git a/src/wuttasync/cli/__init__.py b/src/wuttasync/cli/__init__.py index 231f072..a3fa82b 100644 --- a/src/wuttasync/cli/__init__.py +++ b/src/wuttasync/cli/__init__.py @@ -40,7 +40,5 @@ from .base import ( # nb. must bring in all modules for discovery to work from . import export_csv -from . import export_wutta from . import import_csv from . import import_versions -from . import import_wutta diff --git a/src/wuttasync/cli/base.py b/src/wuttasync/cli/base.py index 188555e..68bb536 100644 --- a/src/wuttasync/cli/base.py +++ b/src/wuttasync/cli/base.py @@ -65,13 +65,7 @@ class ImportCommandHandler(GenericHandler): :param key: Optional :term:`import/export key` to use for handler lookup. Only used if ``import_handler`` param is not set. - :param \\**kwargs: Remaining kwargs are passed as-is to the - 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*):: + Typical usage for custom commands will be to provide the spec:: handler = ImportCommandHandler( config, "poser.importing.foo:FromFooToPoser" @@ -87,14 +81,6 @@ class ImportCommandHandler(GenericHandler): See also :meth:`~wuttasync.app.WuttaSyncAppProvider.get_import_handler()` 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 @@ -103,22 +89,20 @@ class ImportCommandHandler(GenericHandler): 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) if import_handler: if isinstance(import_handler, ImportHandler): self.import_handler = import_handler elif callable(import_handler): - self.import_handler = import_handler(self.config, **kwargs) + self.import_handler = import_handler(self.config) else: # spec factory = self.app.load_object(import_handler) - self.import_handler = factory(self.config, **kwargs) + self.import_handler = factory(self.config) elif key: - self.import_handler = self.app.get_import_handler( - key, require=True, **kwargs - ) + self.import_handler = self.app.get_import_handler(key, require=True) def run(self, ctx, progress=None): # pylint: disable=unused-argument """ @@ -177,7 +161,7 @@ class ImportCommandHandler(GenericHandler): # sort out which models to process models = kw.pop("models", None) if not models: - models = self.import_handler.get_default_importer_keys() + models = list(self.import_handler.importers) log.debug( "%s %s for models: %s", self.import_handler.actioning, @@ -196,31 +180,13 @@ class ImportCommandHandler(GenericHandler): This is what happens when command line has ``--list-models``. """ - all_keys = list(self.import_handler.importers) - default_keys = [k for k in all_keys if self.import_handler.is_default(k)] - extra_keys = [k for k in all_keys if k not in default_keys] - - sys.stdout.write("\n") + sys.stdout.write("\nALL MODELS:\n") sys.stdout.write("==============================\n") - sys.stdout.write(" DEFAULT MODELS:\n") + for key in self.import_handler.importers: + sys.stdout.write(key) + sys.stdout.write("\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") + 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 diff --git a/src/wuttasync/cli/export_wutta.py b/src/wuttasync/cli/export_wutta.py deleted file mode 100644 index 2c89b3e..0000000 --- a/src/wuttasync/cli/export_wutta.py +++ /dev/null @@ -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 . -# -################################################################################ -""" -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) diff --git a/src/wuttasync/cli/import_wutta.py b/src/wuttasync/cli/import_wutta.py deleted file mode 100644 index 02101e0..0000000 --- a/src/wuttasync/cli/import_wutta.py +++ /dev/null @@ -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 . -# -################################################################################ -""" -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) diff --git a/src/wuttasync/conf.py b/src/wuttasync/conf.py deleted file mode 100644 index 3a980d5..0000000 --- a/src/wuttasync/conf.py +++ /dev/null @@ -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 . -# -################################################################################ -""" -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", - ) diff --git a/src/wuttasync/emails.py b/src/wuttasync/emails.py index 0c96f4f..b34112d 100644 --- a/src/wuttasync/emails.py +++ b/src/wuttasync/emails.py @@ -2,7 +2,7 @@ ################################################################################ # # 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. # @@ -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 ImportExportWarning ): @@ -172,11 +164,3 @@ class import_to_wutta_from_csv_warning( # pylint: disable=invalid-name """ Diff warning for CSV → Wutta import. """ - - -class import_to_wutta_from_wutta_warning( # pylint: disable=invalid-name - ImportExportWarning -): - """ - Diff warning for Wutta → Wutta import. - """ diff --git a/src/wuttasync/importing/csv.py b/src/wuttasync/importing/csv.py index ca39830..60c51eb 100644 --- a/src/wuttasync/importing/csv.py +++ b/src/wuttasync/importing/csv.py @@ -239,7 +239,7 @@ class FromCsvToSqlalchemyHandlerMixin: raise NotImplementedError # 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): """ This mixin overrides typical (manual) importer definition, and diff --git a/src/wuttasync/importing/handlers.py b/src/wuttasync/importing/handlers.py index 2aa8ffe..cc53bdf 100644 --- a/src/wuttasync/importing/handlers.py +++ b/src/wuttasync/importing/handlers.py @@ -2,7 +2,7 @@ ################################################################################ # # 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. # @@ -203,12 +203,7 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc def __init__(self, config, **kwargs): """ """ - super().__init__(config) - - # callers can set any attrs they want - for k, v in kwargs.items(): - setattr(self, k, v) - + super().__init__(config, **kwargs) self.importers = self.define_importers() def __str__(self): @@ -371,7 +366,7 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc changes = OrderedDict() if not keys: - keys = self.get_default_importer_keys() + keys = list(self.importers) success = False try: @@ -655,36 +650,6 @@ class ImportHandler( # pylint: disable=too-many-public-methods,too-many-instanc """ 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): """ Run post-processing operations on the given changes, if diff --git a/src/wuttasync/importing/versions.py b/src/wuttasync/importing/versions.py index 07a03a3..d558c36 100644 --- a/src/wuttasync/importing/versions.py +++ b/src/wuttasync/importing/versions.py @@ -138,7 +138,7 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler): return kwargs # 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): """ This overrides typical (manual) importer definition, instead diff --git a/src/wuttasync/importing/wutta.py b/src/wuttasync/importing/wutta.py index 10e6e6c..882f7df 100644 --- a/src/wuttasync/importing/wutta.py +++ b/src/wuttasync/importing/wutta.py @@ -2,7 +2,7 @@ ################################################################################ # # 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. # @@ -24,172 +24,10 @@ 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 .model import ToWutta -from .handlers import FromWuttaHandler, ToWuttaHandler, Orientation class FromWuttaMirror(FromSqlalchemyMirror): # pylint: disable=abstract-method """ - Base class for Wutta → Wutta data :term:`importers/exporters - `. - - This inherits from - :class:`~wuttasync.importing.base.FromSqlalchemyMirror`. + Base class for Wutta -> Wutta data importers. """ - - -class FromWuttaToWuttaBase(FromWuttaHandler, ToWuttaHandler): - """ - Base class for Wutta → Wutta data :term:`import/export handlers - `. - - 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) diff --git a/tests/cli/test_base.py b/tests/cli/test_base.py index d2b98e6..209dbca 100644 --- a/tests/cli/test_base.py +++ b/tests/cli/test_base.py @@ -3,7 +3,7 @@ import inspect import sys 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 wuttjamaican.testing import DataTestCase @@ -25,55 +25,19 @@ class TestImportCommandHandler(DataTestCase): # as spec handler = self.make_handler(import_handler=FromCsvToWutta.get_spec()) 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 handler = self.make_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 myhandler = FromCsvToWutta(self.config) handler = self.make_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 handler = self.make_handler(key="import.to_wutta.from_csv") 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): handler = self.make_handler( @@ -169,60 +133,22 @@ class TestImportCommandHandler(DataTestCase): params={}, ), ) + # self.assertRaises(FileNotFoundError, handler.run, ctx) handler.run(ctx) exit_.assert_not_called() def test_list_models(self): - - # CSV -> Wutta (all importers are default) handler = self.make_handler( 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: - handler.list_models({}) - sys.stdout.write.assert_has_calls( - [ - call("==============================\n"), - call(" DEFAULT MODELS:\n"), - call("==============================\n"), - 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"), - ] - ) + # 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): diff --git a/tests/cli/test_export_wutta.py b/tests/cli/test_export_wutta.py deleted file mode 100644 index 73e4ab2..0000000 --- a/tests/cli/test_export_wutta.py +++ /dev/null @@ -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) diff --git a/tests/cli/test_import_wutta.py b/tests/cli/test_import_wutta.py deleted file mode 100644 index 3887c56..0000000 --- a/tests/cli/test_import_wutta.py +++ /dev/null @@ -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) diff --git a/tests/importing/test_handlers.py b/tests/importing/test_handlers.py index ad8a9a3..659cda1 100644 --- a/tests/importing/test_handlers.py +++ b/tests/importing/test_handlers.py @@ -7,7 +7,6 @@ from uuid import UUID from wuttjamaican.testing import DataTestCase from wuttasync.importing import handlers as mod, Importer, ToSqlalchemy -from wuttasync.importing.wutta import FromWuttaToWuttaImport class FromFooToBar(mod.ImportHandler): @@ -20,17 +19,6 @@ class TestImportHandler(DataTestCase): def make_handler(self, **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): handler = self.make_handler() self.assertEqual(str(handler), "None → None") @@ -254,25 +242,6 @@ class TestImportHandler(DataTestCase): 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): handler = FromFooToBar(self.config) diff --git a/tests/importing/test_wutta.py b/tests/importing/test_wutta.py index cd43df0..1533605 100644 --- a/tests/importing/test_wutta.py +++ b/tests/importing/test_wutta.py @@ -1,134 +1,3 @@ # -*- 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 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) diff --git a/tests/test_app.py b/tests/test_app.py index 23eb4bd..560d89d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -46,18 +46,6 @@ class TestWuttaSyncAppProvider(ConfigTestCase): self.assertIn(FromCsvToWutta, 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): # fetch of unknown key returns none @@ -151,14 +139,6 @@ class TestWuttaSyncAppProvider(ConfigTestCase): handler = self.app.get_import_handler("import.to_wutta.from_csv") self.assertIsInstance(handler, FromCsvToWutta) 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 handler = self.app.get_import_handler("bogus") diff --git a/tests/test_conf.py b/tests/test_conf.py deleted file mode 100644 index eefa5b7..0000000 --- a/tests/test_conf.py +++ /dev/null @@ -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") diff --git a/tests/test_emails.py b/tests/test_emails.py index bdaebed..9494753 100644 --- a/tests/test_emails.py +++ b/tests/test_emails.py @@ -5,7 +5,6 @@ from wuttjamaican.testing import ConfigTestCase from wuttasync import emails as mod from wuttasync.importing import ImportHandler from wuttasync.testing import ImportExportWarningTestCase -from wuttasync.conf import WuttaSyncConfig class FromFooToWutta(ImportHandler): @@ -75,24 +74,8 @@ class TestImportExportWarning(ConfigTestCase): 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): self.do_test_preview("import_to_versions_from_wutta_warning") def test_import_to_wutta_from_csv_warning(self): 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")