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:
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[

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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):
"""

View file

@ -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(

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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")