diff --git a/src/wuttasync/cli/base.py b/src/wuttasync/cli/base.py index 56d6421..34be0e7 100644 --- a/src/wuttasync/cli/base.py +++ b/src/wuttasync/cli/base.py @@ -102,7 +102,7 @@ class ImportCommandHandler(GenericHandler): elif key: self.import_handler = self.app.get_import_handler(key, require=True) - def run(self, params, progress=None): # pylint: disable=unused-argument + def run(self, ctx, progress=None): # pylint: disable=unused-argument """ Run the import/export job(s) based on command line params. @@ -113,20 +113,29 @@ class ImportCommandHandler(GenericHandler): Unless ``--list-models`` was specified on the command line in which case we do :meth:`list_models()` instead. - :param params: Dict of params from command line. This must - include a ``'models'`` key, the rest are optional. + :param ctx: :class:`typer.Context` instance. :param progress: Optional progress indicator factory. """ # maybe just list models and bail - if params.get("list_models"): - self.list_models(params) + if ctx.params.get("list_models"): + self.list_models(ctx.params) return - # otherwise process some data + # otherwise we'll process some data log.debug("using handler: %s", self.import_handler.get_spec()) - kw = dict(params) + + # all params from caller will be passed along + kw = dict(ctx.params) + + # runas user and comment also, but they come from root command + if username := ctx.parent.params.get("runas_username"): + kw["runas_username"] = username + if comment := ctx.parent.params.get("comment"): + kw["transaction_comment"] = comment + + # sort out which models to process models = kw.pop("models") if not models: models = list(self.import_handler.importers) @@ -136,6 +145,8 @@ class ImportCommandHandler(GenericHandler): self.import_handler.get_title(), ", ".join(models), ) + + # process data log.debug("params are: %s", kw) self.import_handler.process_data(*models, **kw) @@ -248,7 +259,9 @@ def import_command_template( # pylint: disable=unused-argument,too-many-argumen warnings_recipients: Annotated[ str, typer.Option( - "--recip", help="Override the recipient(s) for diff warning email." + "--recip", + "--recips", + help="Override the recipient(s) for diff warning email.", ), ] = None, warnings_max_diffs: Annotated[ diff --git a/src/wuttasync/cli/import_csv.py b/src/wuttasync/cli/import_csv.py index 4c5694a..1a55523 100644 --- a/src/wuttasync/cli/import_csv.py +++ b/src/wuttasync/cli/import_csv.py @@ -39,4 +39,4 @@ def import_csv(ctx: typer.Context, **kwargs): # pylint: disable=unused-argument """ config = ctx.parent.wutta_config handler = ImportCommandHandler(config, key="import.to_wutta.from_csv") - handler.run(ctx.params) + handler.run(ctx) diff --git a/src/wuttasync/cli/import_versions.py b/src/wuttasync/cli/import_versions.py index aa82088..0057e88 100644 --- a/src/wuttasync/cli/import_versions.py +++ b/src/wuttasync/cli/import_versions.py @@ -37,14 +37,7 @@ from .base import import_command, ImportCommandHandler @wutta_typer.command() @import_command -def import_versions( # pylint: disable=unused-argument - ctx: typer.Context, - comment: Annotated[ - str, - typer.Option("--comment", "-m", help="Comment to set on the transaction."), - ] = "import catch-up versions", - **kwargs, -): +def import_versions(ctx: typer.Context, **kwargs): # pylint: disable=unused-argument """ Import latest data to version tables, for Wutta DB """ @@ -70,4 +63,4 @@ def import_versions( # pylint: disable=unused-argument sys.exit(1) handler = ImportCommandHandler(config, key="import.to_versions.from_wutta") - handler.run(ctx.params) + handler.run(ctx) diff --git a/src/wuttasync/importing/handlers.py b/src/wuttasync/importing/handlers.py index ac13f28..384fbd3 100644 --- a/src/wuttasync/importing/handlers.py +++ b/src/wuttasync/importing/handlers.py @@ -166,6 +166,18 @@ class ImportHandler(GenericHandler): # pylint: disable=too-many-public-methods See also :attr:`warnings`. """ + runas_username = None + """ + Username responsible for running the import/export job. This is + mostly used for Continuum versioning. + """ + + transaction_comment = None + """ + Optional comment to apply to the transaction, where applicable. + This is mostly used for Continuum versioning. + """ + importers = None """ This should be a dict of all importer/exporter classes available @@ -416,6 +428,12 @@ class ImportHandler(GenericHandler): # pylint: disable=too-many-public-methods if "warnings_max_diffs" in kwargs: self.warnings_max_diffs = kwargs.pop("warnings_max_diffs") + if "runas_username" in kwargs: + self.runas_username = kwargs.pop("runas_username") + + if "transaction_comment" in kwargs: + self.transaction_comment = kwargs.pop("transaction_comment") + return kwargs def begin_transaction(self): @@ -946,11 +964,41 @@ class ToWuttaHandler(ToSqlalchemyHandler): def make_target_session(self): """ - Call - :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.make_session()` - and return the result. + This creates a typical :term:`db session` for the app by + calling + :meth:`~wuttjamaican:wuttjamaican.app.AppHandler.make_session()`. + + It then may "customize" the session slightly. These + customizations only are relevant if Wutta-Continuum versioning + is enabled: + + If :attr:`~ImportHandler.runas_username` is set, the + responsible user (``continuum_user_id``) will be set for the + new session as well. + + Similarly, if :attr:`~ImportHandler.transaction_comment` is + set, it (``continuum_comment``) will also be set for the new + session. :returns: :class:`~wuttjamaican:wuttjamaican.db.sess.Session` instance. """ - return self.app.make_session() + model = self.app.model + session = self.app.make_session() + + # set runas user in case continuum versioning is enabled + if self.runas_username: + if user := ( + session.query(model.User) + .filter_by(username=self.runas_username) + .first() + ): + session.info["continuum_user_id"] = user.uuid + else: + log.warning("runas username not found: %s", self.runas_username) + + # set comment in case continuum versioning is enabled + if self.transaction_comment: + session.info["continuum_comment"] = self.transaction_comment + + return session diff --git a/src/wuttasync/importing/versions.py b/src/wuttasync/importing/versions.py index cda77c9..d558c36 100644 --- a/src/wuttasync/importing/versions.py +++ b/src/wuttasync/importing/versions.py @@ -92,13 +92,6 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler): See also :attr:`continuum_uow`. """ - continuum_comment = None - - def consume_kwargs(self, kwargs): - kwargs = super().consume_kwargs(kwargs) - self.continuum_comment = kwargs.pop("comment", None) - return kwargs - def begin_target_transaction(self): # pylint: disable=line-too-long """ @@ -128,8 +121,8 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler): self.continuum_txn = self.continuum_uow.create_transaction(self.target_session) - if self.continuum_comment: - self.continuum_txn.meta = {"comment": self.continuum_comment} + if self.transaction_comment: + self.continuum_txn.meta = {"comment": self.transaction_comment} def get_importer_kwargs(self, key, **kwargs): """ diff --git a/tests/cli/test_base.py b/tests/cli/test_base.py index b8fc954..2370bdd 100644 --- a/tests/cli/test_base.py +++ b/tests/cli/test_base.py @@ -2,7 +2,7 @@ import inspect from unittest import TestCase -from unittest.mock import patch +from unittest.mock import patch, Mock from wuttasync.cli import base as mod from wuttjamaican.testing import DataTestCase @@ -44,12 +44,28 @@ class TestImportCommandHandler(DataTestCase): ) with patch.object(handler, "list_models") as list_models: - handler.run({"list_models": True}) - list_models.assert_called_once_with({"list_models": True}) + ctx = Mock(params={"list_models": True}) + handler.run(ctx) + list_models.assert_called_once_with(ctx.params) + + class Object: + def __init__(self, **kw): + self.__dict__.update(kw) with patch.object(handler, "import_handler") as import_handler: - handler.run({"models": []}) - import_handler.process_data.assert_called_once_with() + parent = Mock( + params={ + "runas_username": "fred", + "comment": "hello world", + } + ) + # TODO: why can't we just use Mock here? the parent attr is problematic + ctx = Object(params={"models": []}, parent=parent) + handler.run(ctx) + import_handler.process_data.assert_called_once_with( + runas_username="fred", + transaction_comment="hello world", + ) def test_list_models(self): handler = self.make_handler( diff --git a/tests/cli/test_import_csv.py b/tests/cli/test_import_csv.py index 5623176..2529380 100644 --- a/tests/cli/test_import_csv.py +++ b/tests/cli/test_import_csv.py @@ -19,4 +19,4 @@ class TestImportCsv(TestCase): ctx = MagicMock(params=params) with patch.object(ImportCommandHandler, "run") as run: mod.import_csv(ctx) - run.assert_called_once_with(params) + run.assert_called_once_with(ctx) diff --git a/tests/cli/test_import_versions.py b/tests/cli/test_import_versions.py index ea1617d..506a5e9 100644 --- a/tests/cli/test_import_versions.py +++ b/tests/cli/test_import_versions.py @@ -19,4 +19,4 @@ class TestImportCsv(TestCase): ctx = MagicMock(params=params) with patch.object(ImportCommandHandler, "run") as run: mod.import_versions(ctx) - run.assert_called_once_with(params) + run.assert_called_once_with(ctx) diff --git a/tests/importing/test_handlers.py b/tests/importing/test_handlers.py index c01b405..21fcaeb 100644 --- a/tests/importing/test_handlers.py +++ b/tests/importing/test_handlers.py @@ -190,6 +190,22 @@ class TestImportHandler(DataTestCase): self.assertNotIn("warnings_max_diffs", kw) self.assertEqual(handler.warnings_max_diffs, 30) + # runas_username (consumed) + self.assertIsNone(handler.runas_username) + kw["runas_username"] = "fred" + result = handler.consume_kwargs(kw) + self.assertIs(result, kw) + self.assertNotIn("runas_username", kw) + self.assertEqual(handler.runas_username, "fred") + + # transaction_comment (consumed) + self.assertIsNone(handler.transaction_comment) + kw["transaction_comment"] = "hello world" + result = handler.consume_kwargs(kw) + self.assertIs(result, kw) + self.assertNotIn("transaction_comment", kw) + self.assertEqual(handler.transaction_comment, "hello world") + def test_define_importers(self): handler = self.make_handler() importers = handler.define_importers() @@ -490,11 +506,41 @@ class TestToWuttaHandler(DataTestCase): self.assertEqual(handler.get_target_title(), "what_about_this") def test_make_target_session(self): + model = self.app.model handler = self.make_handler() - # makes "new" (mocked in our case) app session + fred = model.User(username="fred") + self.session.add(fred) + self.session.commit() + + # makes "new" (mocked in our case) app session, with no runas + # username set by default with patch.object(self.app, "make_session") as make_session: make_session.return_value = self.session session = handler.make_target_session() make_session.assert_called_once_with() self.assertIs(session, self.session) + self.assertNotIn("continuum_user_id", session.info) + self.assertNotIn("continuum_user_id", self.session.info) + + # runas user also should not be set, if username is not valid + handler.runas_username = "freddie" + with patch.object(self.app, "make_session") as make_session: + make_session.return_value = self.session + session = handler.make_target_session() + make_session.assert_called_once_with() + self.assertIs(session, self.session) + self.assertNotIn("continuum_user_id", session.info) + self.assertNotIn("continuum_user_id", self.session.info) + + # this time we should have runas user properly set + handler.runas_username = "fred" + with patch.object(self.app, "make_session") as make_session: + make_session.return_value = self.session + session = handler.make_target_session() + make_session.assert_called_once_with() + self.assertIs(session, self.session) + self.assertIn("continuum_user_id", session.info) + self.assertEqual(session.info["continuum_user_id"], fred.uuid) + self.assertIn("continuum_user_id", self.session.info) + self.assertEqual(self.session.info["continuum_user_id"], fred.uuid) diff --git a/tests/importing/test_versions.py b/tests/importing/test_versions.py index 2cd4ec0..eea6171 100644 --- a/tests/importing/test_versions.py +++ b/tests/importing/test_versions.py @@ -14,20 +14,6 @@ class TestFromWuttaToVersions(VersionTestCase): def make_handler(self, **kwargs): return mod.FromWuttaToVersions(self.config, **kwargs) - def test_consume_kwargs(self): - - # no comment by default - handler = self.make_handler() - kw = handler.consume_kwargs({}) - self.assertEqual(kw, {}) - self.assertIsNone(handler.continuum_comment) - - # but can provide one - handler = self.make_handler() - kw = handler.consume_kwargs({"comment": "yeehaw"}) - self.assertEqual(kw, {}) - self.assertEqual(handler.continuum_comment, "yeehaw") - def test_begin_target_transaction(self): model = self.app.model txncls = continuum.transaction_class(model.User) @@ -44,7 +30,7 @@ class TestFromWuttaToVersions(VersionTestCase): # with comment handler = self.make_handler() - handler.continuum_comment = "yeehaw" + handler.transaction_comment = "yeehaw" handler.begin_target_transaction() self.assertIn("comment", handler.continuum_txn.meta) self.assertEqual(handler.continuum_txn.meta["comment"], "yeehaw")