fix: add util module, w/ model_transaction_query()

just a basic implementation, will have to improve later
This commit is contained in:
Lance Edgar 2025-10-21 13:32:35 -05:00
parent 0e25cca0ba
commit 4db3fa5962
9 changed files with 281 additions and 13 deletions

View file

@ -45,7 +45,28 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
key = "wutta_continuum"
def startup(self, config): # pylint: disable=empty-docstring
""" """
"""
Perform final configuration setup for app startup.
This will do nothing at all, unless config enables the
versioning feature. This must be done in config file and not
in DB settings table:
.. code-block:: ini
[wutta_continuum]
enable_versioning = true
Once enabled, this method will configure the integration, via
these steps:
1. call :func:`sqlalchemy-continuum:sqlalchemy_continuum.make_versioned()`
2. call :meth:`wuttjamaican:wuttjamaican.app.AppHandler.get_model()`
3. call :func:`sqlalchemy:sqlalchemy.orm.configure_mappers()`
For more about SQLAlchemy-Continuum see
:doc:`sqlalchemy-continuum:intro`.
"""
# only do this if config enables it
if not config.get_bool(
"wutta_continuum.enable_versioning", usedb=False, default=False
@ -60,14 +81,17 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
)
plugin = load_object(spec)
# tell sqlalchemy-continuum to do its thing
app = config.get_app()
if "model" in app.__dict__:
raise RuntimeError("something not right, app already has model")
# let sqlalchemy-continuum do its thing
make_versioned(plugins=[plugin()])
# nb. must load the model before configuring mappers
app = config.get_app()
model = app.model # pylint: disable=unused-variable
# must load model *between* prev and next calls
app.get_model()
# tell sqlalchemy to do its thing
# let sqlalchemy do its thing
configure_mappers()

View file

@ -0,0 +1,92 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-Continuum -- SQLAlchemy Versioning for Wutta Framework
# Copyright © 2024-2025 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Testing utilities
"""
import sys
import sqlalchemy_continuum as continuum
from wuttjamaican.testing import DataTestCase
from wutta_continuum.conf import WuttaContinuumConfigExtension
class VersionTestCase(DataTestCase):
"""
Base class for test suites requiring the SQLAlchemy-Continuum
versioning feature.
This inherits from
:class:`~wuttjamaican:wuttjamaican.testing.DataTestCase`.
"""
def setUp(self):
self.setup_versioning()
def setup_versioning(self):
"""
Do setup tasks relating to this class, as well as its parent(s):
* call :meth:`wuttjamaican:wuttjamaican.testing.DataTestCase.setup_db()`
* this will in turn call :meth:`make_config()`
"""
self.setup_db()
def tearDown(self):
self.teardown_versioning()
def teardown_versioning(self):
"""
Do teardown tasks relating to this class, as well as its parent(s):
* call :func:`sqlalchemy-continuum:sqlalchemy_continuum.remove_versioning()`
* call :meth:`wuttjamaican:wuttjamaican.testing.DataTestCase.teardown_db()`
"""
continuum.remove_versioning()
continuum.versioning_manager.transaction_cls = continuum.TransactionFactory()
self.teardown_db()
def make_config(self, **kwargs):
"""
Make and customize the config object.
We override this to explicitly enable the versioning feature.
"""
config = super().make_config(**kwargs)
config.setdefault("wutta_continuum.enable_versioning", "true")
# nb. must purge model classes from sys.modules, so they will
# be reloaded and sqlalchemy-continuum can reconfigure
if "wuttjamaican.db.model" in sys.modules:
del sys.modules["wuttjamaican.db.model.batch"]
del sys.modules["wuttjamaican.db.model.upgrades"]
del sys.modules["wuttjamaican.db.model.auth"]
del sys.modules["wuttjamaican.db.model.base"]
del sys.modules["wuttjamaican.db.model"]
ext = WuttaContinuumConfigExtension()
ext.startup(config)
return config

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-Continuum -- SQLAlchemy Versioning for Wutta Framework
# Copyright © 2024-2025 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
SQLAlchemy-Continuum utilities
"""
import sqlalchemy as sa
from sqlalchemy import orm
import sqlalchemy_continuum as continuum
OPERATION_TYPES = {
continuum.Operation.INSERT: "INSERT",
continuum.Operation.UPDATE: "UPDATE",
continuum.Operation.DELETE: "DELETE",
}
def render_operation_type(operation_type):
"""
Render a SQLAlchemy-Continuum ``operation_type`` from a version
record, for display to user.
:param operation_type: Value of same name from a version record.
Must be one of:
* :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.INSERT`
* :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.UPDATE`
* :attr:`sqlalchemy_continuum:sqlalchemy_continuum.operation.Operation.DELETE`
:returns: Display name for the operation type, as string.
"""
return OPERATION_TYPES[operation_type]
def model_transaction_query(instance, session=None, model_class=None):
"""
Make a query capable of finding all SQLAlchemy-Continuum
``transaction`` records associated with the given model instance.
:param instance: Instance of a versioned :term:`data model`.
:param session: Optional :term:`db session` to use for the query.
If not specified, will be obtained from the ``instance``.
:param model_class: Optional :term:`data model` class to query.
If not specified, will be obtained from the ``instance``.
:returns: SQLAlchemy query object. Note that it will *not* have an
``ORDER BY`` clause yet.
"""
if not session:
session = orm.object_session(instance)
if not model_class:
model_class = type(instance)
txncls = continuum.transaction_class(model_class)
vercls = continuum.version_class(model_class)
query = session.query(txncls).join(
vercls,
sa.and_(vercls.uuid == instance.uuid, vercls.transaction_id == txncls.id),
)
return query