Compare commits
	
		
			No commits in common. "53b35f1f5526f3cce60891880719fc32e58cb088" and "0e25cca0bac1e78fd8311e6e50045aa5208fd40b" have entirely different histories.
		
	
	
		
			53b35f1f55
			...
			0e25cca0ba
		
	
		
					 11 changed files with 14 additions and 290 deletions
				
			
		| 
						 | 
				
			
			@ -5,14 +5,6 @@ All notable changes to Wutta-Continuum will be documented in this file.
 | 
			
		|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 | 
			
		||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 | 
			
		||||
 | 
			
		||||
## v0.2.1 (2025-10-29)
 | 
			
		||||
 | 
			
		||||
### Fix
 | 
			
		||||
 | 
			
		||||
- add util module, w/ `model_transaction_query()`
 | 
			
		||||
- refactor some more for tests + pylint
 | 
			
		||||
- format all code with black
 | 
			
		||||
 | 
			
		||||
## v0.2.0 (2024-12-07)
 | 
			
		||||
 | 
			
		||||
### Feat
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
 | 
			
		||||
``wutta_continuum.testing``
 | 
			
		||||
===========================
 | 
			
		||||
 | 
			
		||||
.. automodule:: wutta_continuum.testing
 | 
			
		||||
   :members:
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
 | 
			
		||||
``wutta_continuum.util``
 | 
			
		||||
========================
 | 
			
		||||
 | 
			
		||||
.. automodule:: wutta_continuum.util
 | 
			
		||||
   :members:
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +27,6 @@ templates_path = ["_templates"]
 | 
			
		|||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 | 
			
		||||
 | 
			
		||||
intersphinx_mapping = {
 | 
			
		||||
    "sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None),
 | 
			
		||||
    "sqlalchemy-continuum": (
 | 
			
		||||
        "https://sqlalchemy-continuum.readthedocs.io/en/latest/",
 | 
			
		||||
        None,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,5 +29,3 @@ This package adds data versioning/history for `WuttJamaican`_, using
 | 
			
		|||
   api/wutta_continuum
 | 
			
		||||
   api/wutta_continuum.app
 | 
			
		||||
   api/wutta_continuum.conf
 | 
			
		||||
   api/wutta_continuum.testing
 | 
			
		||||
   api/wutta_continuum.util
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ build-backend = "hatchling.build"
 | 
			
		|||
 | 
			
		||||
[project]
 | 
			
		||||
name = "Wutta-Continuum"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
description = "SQLAlchemy-Continuum versioning for Wutta Framework"
 | 
			
		||||
readme = "README.md"
 | 
			
		||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,28 +45,7 @@ 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
 | 
			
		||||
| 
						 | 
				
			
			@ -81,17 +60,14 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
 | 
			
		|||
        )
 | 
			
		||||
        plugin = load_object(spec)
 | 
			
		||||
 | 
			
		||||
        app = config.get_app()
 | 
			
		||||
        if "model" in app.__dict__:
 | 
			
		||||
            raise RuntimeError("something not right, app already has model")
 | 
			
		||||
 | 
			
		||||
        # let sqlalchemy-continuum do its thing
 | 
			
		||||
        # tell sqlalchemy-continuum to do its thing
 | 
			
		||||
        make_versioned(plugins=[plugin()])
 | 
			
		||||
 | 
			
		||||
        # must load model *between* prev and next calls
 | 
			
		||||
        app.get_model()
 | 
			
		||||
        # nb. must load the model before configuring mappers
 | 
			
		||||
        app = config.get_app()
 | 
			
		||||
        model = app.model  # pylint: disable=unused-variable
 | 
			
		||||
 | 
			
		||||
        # let sqlalchemy do its thing
 | 
			
		||||
        # tell sqlalchemy to do its thing
 | 
			
		||||
        configure_mappers()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
# -*- 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
 | 
			
		||||
| 
						 | 
				
			
			@ -1,85 +0,0 @@
 | 
			
		|||
# -*- 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
 | 
			
		||||
| 
						 | 
				
			
			@ -4,45 +4,33 @@ import socket
 | 
			
		|||
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.testing import ConfigTestCase, DataTestCase
 | 
			
		||||
from wuttjamaican.testing import DataTestCase
 | 
			
		||||
 | 
			
		||||
from wutta_continuum import conf as mod
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestWuttaContinuumConfigExtension(ConfigTestCase):
 | 
			
		||||
class TestWuttaContinuumConfigExtension(DataTestCase):
 | 
			
		||||
 | 
			
		||||
    def make_extension(self):
 | 
			
		||||
        return mod.WuttaContinuumConfigExtension()
 | 
			
		||||
 | 
			
		||||
    def test_startup_without_versioning(self):
 | 
			
		||||
    def test_startup(self):
 | 
			
		||||
        ext = self.make_extension()
 | 
			
		||||
 | 
			
		||||
        with patch.object(mod, "make_versioned") as make_versioned:
 | 
			
		||||
            with patch.object(mod, "configure_mappers") as configure_mappers:
 | 
			
		||||
 | 
			
		||||
                # nothing happens by default
 | 
			
		||||
                ext.startup(self.config)
 | 
			
		||||
                make_versioned.assert_not_called()
 | 
			
		||||
                configure_mappers.assert_not_called()
 | 
			
		||||
 | 
			
		||||
    def test_startup_with_versioning(self):
 | 
			
		||||
        ext = self.make_extension()
 | 
			
		||||
        with patch.object(mod, "make_versioned") as make_versioned:
 | 
			
		||||
            with patch.object(mod, "configure_mappers") as configure_mappers:
 | 
			
		||||
                # but will if we enable it in config
 | 
			
		||||
                self.config.setdefault("wutta_continuum.enable_versioning", "true")
 | 
			
		||||
                ext.startup(self.config)
 | 
			
		||||
                make_versioned.assert_called_once()
 | 
			
		||||
                configure_mappers.assert_called_once_with()
 | 
			
		||||
 | 
			
		||||
    def test_startup_with_error(self):
 | 
			
		||||
        ext = self.make_extension()
 | 
			
		||||
        with patch.object(mod, "make_versioned") as make_versioned:
 | 
			
		||||
            with patch.object(mod, "configure_mappers") as configure_mappers:
 | 
			
		||||
                self.config.setdefault("wutta_continuum.enable_versioning", "true")
 | 
			
		||||
                # nb. it is an error for the model to be loaded prior to
 | 
			
		||||
                # calling make_versioned() for sqlalchemy-continuum
 | 
			
		||||
                self.app.get_model()
 | 
			
		||||
                self.assertRaises(RuntimeError, ext.startup, self.config)
 | 
			
		||||
                make_versioned.assert_not_called()
 | 
			
		||||
                configure_mappers.assert_not_called()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestWuttaContinuumPlugin(DataTestCase):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,40 +0,0 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
 | 
			
		||||
from unittest import TestCase
 | 
			
		||||
 | 
			
		||||
import sqlalchemy_continuum as continuum
 | 
			
		||||
 | 
			
		||||
from wutta_continuum import util as mod
 | 
			
		||||
from wutta_continuum.testing import VersionTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRenderOperationType(TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_basic(self):
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            mod.render_operation_type(continuum.Operation.INSERT), "INSERT"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            mod.render_operation_type(continuum.Operation.UPDATE), "UPDATE"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            mod.render_operation_type(continuum.Operation.DELETE), "DELETE"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestModelTransactionQuery(VersionTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_basic(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
 | 
			
		||||
        user = model.User(username="fred")
 | 
			
		||||
        self.session.add(user)
 | 
			
		||||
        self.session.commit()
 | 
			
		||||
 | 
			
		||||
        query = mod.model_transaction_query(user)
 | 
			
		||||
        self.assertEqual(query.count(), 1)
 | 
			
		||||
        txn = query.one()
 | 
			
		||||
 | 
			
		||||
        UserVersion = continuum.version_class(model.User)
 | 
			
		||||
        version = self.session.query(UserVersion).one()
 | 
			
		||||
        self.assertIs(version.transaction, txn)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue