Compare commits

...

3 commits

Author SHA1 Message Date
e397890098 feat: add support for --comment CLI param, to set versioning comment
only relevant if Wutta-Continuum is enabled
2025-12-29 12:49:39 -06:00
6ee008e169 feat: add support for --runas CLI param, to set versioning authorship
only relevant if Wutta-Continuum is enabled
2025-12-29 11:10:57 -06:00
c6d1822f3b fix: accept either --recip or --recips param for import commands 2025-12-29 10:46:51 -06:00
10 changed files with 149 additions and 54 deletions

View file

@ -102,7 +102,7 @@ class ImportCommandHandler(GenericHandler):
elif key: elif key:
self.import_handler = self.app.get_import_handler(key, require=True) 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. 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 Unless ``--list-models`` was specified on the command line in
which case we do :meth:`list_models()` instead. which case we do :meth:`list_models()` instead.
:param params: Dict of params from command line. This must :param ctx: :class:`typer.Context` instance.
include a ``'models'`` key, the rest are optional.
:param progress: Optional progress indicator factory. :param progress: Optional progress indicator factory.
""" """
# maybe just list models and bail # maybe just list models and bail
if params.get("list_models"): if ctx.params.get("list_models"):
self.list_models(params) self.list_models(ctx.params)
return return
# otherwise process some data # otherwise we'll process some data
log.debug("using handler: %s", self.import_handler.get_spec()) 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") models = kw.pop("models")
if not models: if not models:
models = list(self.import_handler.importers) models = list(self.import_handler.importers)
@ -136,6 +145,8 @@ class ImportCommandHandler(GenericHandler):
self.import_handler.get_title(), self.import_handler.get_title(),
", ".join(models), ", ".join(models),
) )
# process data
log.debug("params are: %s", kw) log.debug("params are: %s", kw)
self.import_handler.process_data(*models, **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[ warnings_recipients: Annotated[
str, str,
typer.Option( typer.Option(
"--recip", help="Override the recipient(s) for diff warning email." "--recip",
"--recips",
help="Override the recipient(s) for diff warning email.",
), ),
] = None, ] = None,
warnings_max_diffs: Annotated[ warnings_max_diffs: Annotated[

View file

@ -39,4 +39,4 @@ def import_csv(ctx: typer.Context, **kwargs): # pylint: disable=unused-argument
""" """
config = ctx.parent.wutta_config config = ctx.parent.wutta_config
handler = ImportCommandHandler(config, key="import.to_wutta.from_csv") handler = ImportCommandHandler(config, key="import.to_wutta.from_csv")
handler.run(ctx.params) handler.run(ctx)

View file

@ -37,14 +37,7 @@ from .base import import_command, ImportCommandHandler
@wutta_typer.command() @wutta_typer.command()
@import_command @import_command
def import_versions( # pylint: disable=unused-argument def import_versions(ctx: typer.Context, **kwargs): # 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,
):
""" """
Import latest data to version tables, for Wutta DB Import latest data to version tables, for Wutta DB
""" """
@ -70,4 +63,4 @@ def import_versions( # pylint: disable=unused-argument
sys.exit(1) sys.exit(1)
handler = ImportCommandHandler(config, key="import.to_versions.from_wutta") handler = ImportCommandHandler(config, key="import.to_versions.from_wutta")
handler.run(ctx.params) handler.run(ctx)

View file

@ -166,6 +166,18 @@ class ImportHandler(GenericHandler): # pylint: disable=too-many-public-methods
See also :attr:`warnings`. 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 importers = None
""" """
This should be a dict of all importer/exporter classes available 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: if "warnings_max_diffs" in kwargs:
self.warnings_max_diffs = kwargs.pop("warnings_max_diffs") 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 return kwargs
def begin_transaction(self): def begin_transaction(self):
@ -946,11 +964,41 @@ class ToWuttaHandler(ToSqlalchemyHandler):
def make_target_session(self): def make_target_session(self):
""" """
Call This creates a typical :term:`db session` for the app by
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.make_session()` calling
and return the result. :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` :returns: :class:`~wuttjamaican:wuttjamaican.db.sess.Session`
instance. 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

View file

@ -92,13 +92,6 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler):
See also :attr:`continuum_uow`. 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): def begin_target_transaction(self):
# pylint: disable=line-too-long # pylint: disable=line-too-long
""" """
@ -128,8 +121,8 @@ class FromWuttaToVersions(FromWuttaHandler, ToWuttaHandler):
self.continuum_txn = self.continuum_uow.create_transaction(self.target_session) self.continuum_txn = self.continuum_uow.create_transaction(self.target_session)
if self.continuum_comment: if self.transaction_comment:
self.continuum_txn.meta = {"comment": self.continuum_comment} self.continuum_txn.meta = {"comment": self.transaction_comment}
def get_importer_kwargs(self, key, **kwargs): def get_importer_kwargs(self, key, **kwargs):
""" """

View file

@ -2,7 +2,7 @@
import inspect import inspect
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch 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
@ -44,12 +44,28 @@ class TestImportCommandHandler(DataTestCase):
) )
with patch.object(handler, "list_models") as list_models: with patch.object(handler, "list_models") as list_models:
handler.run({"list_models": True}) ctx = Mock(params={"list_models": True})
list_models.assert_called_once_with({"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: with patch.object(handler, "import_handler") as import_handler:
handler.run({"models": []}) parent = Mock(
import_handler.process_data.assert_called_once_with() 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): def test_list_models(self):
handler = self.make_handler( handler = self.make_handler(

View file

@ -19,4 +19,4 @@ class TestImportCsv(TestCase):
ctx = MagicMock(params=params) ctx = MagicMock(params=params)
with patch.object(ImportCommandHandler, "run") as run: with patch.object(ImportCommandHandler, "run") as run:
mod.import_csv(ctx) mod.import_csv(ctx)
run.assert_called_once_with(params) run.assert_called_once_with(ctx)

View file

@ -19,4 +19,4 @@ class TestImportCsv(TestCase):
ctx = MagicMock(params=params) ctx = MagicMock(params=params)
with patch.object(ImportCommandHandler, "run") as run: with patch.object(ImportCommandHandler, "run") as run:
mod.import_versions(ctx) mod.import_versions(ctx)
run.assert_called_once_with(params) run.assert_called_once_with(ctx)

View file

@ -190,6 +190,22 @@ class TestImportHandler(DataTestCase):
self.assertNotIn("warnings_max_diffs", kw) self.assertNotIn("warnings_max_diffs", kw)
self.assertEqual(handler.warnings_max_diffs, 30) 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): def test_define_importers(self):
handler = self.make_handler() handler = self.make_handler()
importers = handler.define_importers() importers = handler.define_importers()
@ -490,11 +506,41 @@ class TestToWuttaHandler(DataTestCase):
self.assertEqual(handler.get_target_title(), "what_about_this") self.assertEqual(handler.get_target_title(), "what_about_this")
def test_make_target_session(self): def test_make_target_session(self):
model = self.app.model
handler = self.make_handler() 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: with patch.object(self.app, "make_session") as make_session:
make_session.return_value = self.session make_session.return_value = self.session
session = handler.make_target_session() session = handler.make_target_session()
make_session.assert_called_once_with() make_session.assert_called_once_with()
self.assertIs(session, self.session) 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)

View file

@ -14,20 +14,6 @@ class TestFromWuttaToVersions(VersionTestCase):
def make_handler(self, **kwargs): def make_handler(self, **kwargs):
return mod.FromWuttaToVersions(self.config, **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): def test_begin_target_transaction(self):
model = self.app.model model = self.app.model
txncls = continuum.transaction_class(model.User) txncls = continuum.transaction_class(model.User)
@ -44,7 +30,7 @@ class TestFromWuttaToVersions(VersionTestCase):
# with comment # with comment
handler = self.make_handler() handler = self.make_handler()
handler.continuum_comment = "yeehaw" handler.transaction_comment = "yeehaw"
handler.begin_target_transaction() handler.begin_target_transaction()
self.assertIn("comment", handler.continuum_txn.meta) self.assertIn("comment", handler.continuum_txn.meta)
self.assertEqual(handler.continuum_txn.meta["comment"], "yeehaw") self.assertEqual(handler.continuum_txn.meta["comment"], "yeehaw")