feat: add warnings mode for import/export handlers, commands

can now specify `--warn` for import/export CLI, to get diff email when
changes occur.

this also adds `get_import_handler()` and friends, via app provider.

also declare email settings for the 2 existing importers
This commit is contained in:
Lance Edgar 2025-12-20 15:32:15 -06:00
parent 1e7722de91
commit 19574ea4a0
18 changed files with 1150 additions and 26 deletions

View file

@ -717,8 +717,9 @@ class TestFromSqlalchemy(DataTestCase):
)
query = imp.get_source_query()
self.assertIsInstance(query, orm.Query)
self.assertEqual(len(query.selectable.froms), 1)
table = query.selectable.froms[0]
froms = query.selectable.get_final_froms()
self.assertEqual(len(froms), 1)
table = froms[0]
self.assertEqual(table.name, "upgrade")
def test_get_source_objects(self):

View file

@ -2,12 +2,18 @@
from collections import OrderedDict
from unittest.mock import patch
from uuid import UUID
from wuttjamaican.testing import DataTestCase
from wuttasync.importing import handlers as mod, Importer, ToSqlalchemy
class FromFooToBar(mod.ImportHandler):
source_key = "foo"
target_key = "bar"
class TestImportHandler(DataTestCase):
def make_handler(self, **kwargs):
@ -30,10 +36,10 @@ class TestImportHandler(DataTestCase):
def test_get_key(self):
handler = self.make_handler()
self.assertEqual(handler.get_key(), "to_None.from_None.import")
self.assertEqual(handler.get_key(), "import.to_None.from_None")
with patch.multiple(mod.ImportHandler, source_key="csv", target_key="wutta"):
self.assertEqual(handler.get_key(), "to_wutta.from_csv.import")
self.assertEqual(handler.get_key(), "import.to_wutta.from_csv")
def test_get_spec(self):
handler = self.make_handler()
@ -149,15 +155,41 @@ class TestImportHandler(DataTestCase):
kw = {}
result = handler.consume_kwargs(kw)
self.assertIs(result, kw)
self.assertEqual(result, {})
# captures dry-run flag
# dry_run (not consumed)
self.assertFalse(handler.dry_run)
kw["dry_run"] = True
result = handler.consume_kwargs(kw)
self.assertIs(result, kw)
self.assertIn("dry_run", kw)
self.assertTrue(kw["dry_run"])
self.assertTrue(handler.dry_run)
# warnings (consumed)
self.assertFalse(handler.warnings)
kw["warnings"] = True
result = handler.consume_kwargs(kw)
self.assertIs(result, kw)
self.assertNotIn("warnings", kw)
self.assertTrue(handler.warnings)
# warnings_recipients (consumed)
self.assertIsNone(handler.warnings_recipients)
kw["warnings_recipients"] = "bob@example.com"
result = handler.consume_kwargs(kw)
self.assertIs(result, kw)
self.assertNotIn("warnings_recipients", kw)
self.assertEqual(handler.warnings_recipients, ["bob@example.com"])
# warnings_max_diffs (consumed)
self.assertEqual(handler.warnings_max_diffs, 15)
kw["warnings_max_diffs"] = 30
result = handler.consume_kwargs(kw)
self.assertIs(result, kw)
self.assertNotIn("warnings_max_diffs", kw)
self.assertEqual(handler.warnings_max_diffs, 30)
def test_define_importers(self):
handler = self.make_handler()
importers = handler.define_importers()
@ -187,6 +219,94 @@ class TestImportHandler(DataTestCase):
KeyError, handler.get_importer, "BunchOfNonsense", model_class=model.Setting
)
def test_get_warnings_email_key(self):
handler = FromFooToBar(self.config)
# default
key = handler.get_warnings_email_key()
self.assertEqual(key, "import_to_bar_from_foo_warning")
# override
handler.warnings_email_key = "from_foo_to_bar"
key = handler.get_warnings_email_key()
self.assertEqual(key, "from_foo_to_bar")
def test_process_changes(self):
model = self.app.model
handler = self.make_handler()
email_handler = self.app.get_email_handler()
handler.process_started = self.app.localtime()
alice = model.User(username="alice")
bob = model.User(username="bob")
charlie = model.User(username="charlie")
changes = {
"User": (
[
(
alice,
{
"uuid": UUID("06946d64-1ebf-79db-8000-ce40345044fe"),
"username": "alice",
},
),
],
[
(
bob,
{
"uuid": UUID("06946d64-1ebf-7a8c-8000-05d78792b084"),
"username": "bob",
},
{
"uuid": UUID("06946d64-1ebf-7a8c-8000-05d78792b084"),
"username": "bobbie",
},
),
],
[
(
charlie,
{
"uuid": UUID("06946d64-1ebf-7ad4-8000-1ba52f720c48"),
"username": "charlie",
},
),
],
),
}
# no email if not in warnings mode
self.assertFalse(handler.warnings)
with patch.object(self.app, "send_email") as send_email:
handler.process_changes(changes)
send_email.assert_not_called()
# email sent (to default recip) if in warnings mode
handler.warnings = True
self.config.setdefault("wutta.email.default.to", "admin@example.com")
with patch.object(email_handler, "deliver_message") as deliver_message:
handler.process_changes(changes)
deliver_message.assert_called_once()
args, kwargs = deliver_message.call_args
self.assertEqual(kwargs, {"recips": None})
self.assertEqual(len(args), 1)
msg = args[0]
self.assertEqual(msg.to, ["admin@example.com"])
# can override email recip
handler.warnings_recipients = ["bob@example.com"]
with patch.object(email_handler, "deliver_message") as deliver_message:
handler.process_changes(changes)
deliver_message.assert_called_once()
args, kwargs = deliver_message.call_args
self.assertEqual(kwargs, {"recips": None})
self.assertEqual(len(args), 1)
msg = args[0]
self.assertEqual(msg.to, ["bob@example.com"])
class TestFromFileHandler(DataTestCase):

View file

@ -132,8 +132,8 @@ class TestFromWuttaToVersionBase(VersionTestCase):
# version object should be embedded in data dict
data = imp.normalize_target_object(version)
self.assertIsInstance(data, dict)
self.assertIn("_version", data)
self.assertIs(data["_version"], version)
self.assertIn("_objref", data)
self.assertIs(data["_objref"], version)
# but normal object is not embedded
data = imp.normalize_target_object(user)

126
tests/test_app.py Normal file
View file

@ -0,0 +1,126 @@
# -*- coding: utf-8; -*-
from wuttjamaican.testing import ConfigTestCase
from wuttasync import app as mod
from wuttasync.importing import ImportHandler
from wuttasync.importing.csv import FromCsvToWutta
class FromFooToBar(ImportHandler):
source_key = "foo"
target_key = "bar"
class FromCsvToPoser(FromCsvToWutta):
pass
class TestWuttaSyncAppProvider(ConfigTestCase):
def test_get_all_import_handlers(self):
# by default our custom handler is not found
handlers = self.app.get_all_import_handlers()
self.assertIn(FromCsvToWutta, handlers)
self.assertNotIn(FromFooToBar, handlers)
# make sure if we configure a custom handler, it is found
self.config.setdefault(
"wuttasync.importing.import.to_wutta.from_csv.handler",
"tests.test_app:FromFooToBar",
)
handlers = self.app.get_all_import_handlers()
self.assertIn(FromCsvToWutta, handlers)
self.assertIn(FromFooToBar, handlers)
def test_get_designated_import_handler_spec(self):
# fetch of unknown key returns none
spec = self.app.get_designated_import_handler_spec("test01")
self.assertIsNone(spec)
# unless we require it, in which case, error
self.assertRaises(
ValueError,
self.app.get_designated_import_handler_spec,
"test01",
require=True,
)
# we configure one for whatever key we like
self.config.setdefault(
"wuttasync.importing.test02.handler", "tests.test_app:FromBarToFoo"
)
spec = self.app.get_designated_import_handler_spec("test02")
self.assertEqual(spec, "tests.test_app:FromBarToFoo")
# we can also define a "default" designated handler
self.config.setdefault(
"wuttasync.importing.test03.default_handler",
"tests.test_app:FromBarToFoo",
)
spec = self.app.get_designated_import_handler_spec("test03")
self.assertEqual(spec, "tests.test_app:FromBarToFoo")
def test_get_designated_import_handlers(self):
# some designated handlers exist, but not our custom handler
handlers = self.app.get_designated_import_handlers()
csv_handlers = [
h for h in handlers if h.get_key() == "import.to_wutta.from_csv"
]
self.assertEqual(len(csv_handlers), 1)
csv_handler = csv_handlers[0]
self.assertIsInstance(csv_handler, FromCsvToWutta)
self.assertFalse(isinstance(csv_handler, FromCsvToPoser))
self.assertFalse(
any([h.get_key() == "import.to_bar.from_foo" for h in handlers])
)
self.assertFalse(any([isinstance(h, FromFooToBar) for h in handlers]))
self.assertFalse(any([isinstance(h, FromCsvToPoser) for h in handlers]))
self.assertTrue(
any([h.get_key() == "import.to_versions.from_wutta" for h in handlers])
)
# but we can make custom designated
self.config.setdefault(
"wuttasync.importing.import.to_wutta.from_csv.handler",
"tests.test_app:FromCsvToPoser",
)
handlers = self.app.get_designated_import_handlers()
csv_handlers = [
h for h in handlers if h.get_key() == "import.to_wutta.from_csv"
]
self.assertEqual(len(csv_handlers), 1)
csv_handler = csv_handlers[0]
self.assertIsInstance(csv_handler, FromCsvToWutta)
self.assertIsInstance(csv_handler, FromCsvToPoser)
self.assertTrue(
any([h.get_key() == "import.to_versions.from_wutta" for h in handlers])
)
def test_get_import_handler(self):
# make sure a basic fetch works
handler = self.app.get_import_handler("import.to_wutta.from_csv")
self.assertIsInstance(handler, FromCsvToWutta)
self.assertFalse(isinstance(handler, FromCsvToPoser))
# and make sure custom override works
self.config.setdefault(
"wuttasync.importing.import.to_wutta.from_csv.handler",
"tests.test_app:FromCsvToPoser",
)
handler = self.app.get_import_handler("import.to_wutta.from_csv")
self.assertIsInstance(handler, FromCsvToWutta)
self.assertIsInstance(handler, FromCsvToPoser)
# unknown importer cannot be found
handler = self.app.get_import_handler("bogus")
self.assertIsNone(handler)
# and if we require it, error will raise
self.assertRaises(
ValueError, self.app.get_import_handler, "bogus", require=True
)

81
tests/test_emails.py Normal file
View file

@ -0,0 +1,81 @@
# -*- coding: utf-8; -*-
from wuttjamaican.testing import ConfigTestCase
from wuttasync import emails as mod
from wuttasync.importing import ImportHandler
from wuttasync.testing import ImportExportWarningTestCase
class FromFooToWutta(ImportHandler):
pass
class TestImportExportWarning(ConfigTestCase):
def make_setting(self, factory=None):
if not factory:
factory = mod.ImportExportWarning
setting = factory(self.config)
return setting
def test_get_description(self):
self.config.setdefault("wutta.app_title", "Wutta Poser")
setting = self.make_setting()
setting.import_handler_key = "import.to_wutta.from_csv"
self.assertEqual(
setting.get_description(),
"Diff warning email for importing CSV → Wutta Poser",
)
def test_get_default_subject(self):
self.config.setdefault("wutta.app_title", "Wutta Poser")
setting = self.make_setting()
setting.import_handler_key = "import.to_wutta.from_csv"
self.assertEqual(setting.get_default_subject(), "Changes for CSV → Wutta Poser")
def test_get_import_handler(self):
# nb. typical name pattern
class import_to_wutta_from_foo_warning(mod.ImportExportWarning):
pass
# nb. name does not match spec pattern
class import_to_wutta_from_bar_blah(mod.ImportExportWarning):
pass
# register our import handler
self.config.setdefault(
"wuttasync.importing.import.to_wutta.from_foo.handler",
"tests.test_emails:FromFooToWutta",
)
# error if spec/key not discoverable
setting = self.make_setting(import_to_wutta_from_bar_blah)
self.assertRaises(ValueError, setting.get_import_handler)
# can lookup by name (auto-spec)
setting = self.make_setting(import_to_wutta_from_foo_warning)
handler = setting.get_import_handler()
self.assertIsInstance(handler, FromFooToWutta)
# can lookup by explicit spec
setting = self.make_setting(import_to_wutta_from_bar_blah)
setting.import_handler_spec = "tests.test_emails:FromFooToWutta"
handler = setting.get_import_handler()
self.assertIsInstance(handler, FromFooToWutta)
# can lookup by explicit key
setting = self.make_setting(import_to_wutta_from_bar_blah)
setting.import_handler_key = "import.to_wutta.from_foo"
handler = setting.get_import_handler()
self.assertIsInstance(handler, FromFooToWutta)
class TestEmailSettings(ImportExportWarningTestCase):
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")