fix: add util module, w/ model_transaction_query()
				
					
				
			just a basic implementation, will have to improve later
This commit is contained in:
		
							parent
							
								
									0e25cca0ba
								
							
						
					
					
						commit
						4db3fa5962
					
				
					 9 changed files with 281 additions and 13 deletions
				
			
		
							
								
								
									
										6
									
								
								docs/api/wutta_continuum.testing.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wutta_continuum.testing.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| 
 | ||||
| ``wutta_continuum.testing`` | ||||
| =========================== | ||||
| 
 | ||||
| .. automodule:: wutta_continuum.testing | ||||
|    :members: | ||||
							
								
								
									
										6
									
								
								docs/api/wutta_continuum.util.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wutta_continuum.util.rst
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| 
 | ||||
| ``wutta_continuum.util`` | ||||
| ======================== | ||||
| 
 | ||||
| .. automodule:: wutta_continuum.util | ||||
|    :members: | ||||
|  | @ -27,6 +27,7 @@ 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,3 +29,5 @@ 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 | ||||
|  |  | |||
|  | @ -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() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										92
									
								
								src/wutta_continuum/testing.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/wutta_continuum/testing.py
									
										
									
									
									
										Normal 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 | ||||
							
								
								
									
										85
									
								
								src/wutta_continuum/util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/wutta_continuum/util.py
									
										
									
									
									
										Normal 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 | ||||
|  | @ -4,33 +4,45 @@ import socket | |||
| 
 | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| from wuttjamaican.testing import DataTestCase | ||||
| from wuttjamaican.testing import ConfigTestCase, DataTestCase | ||||
| 
 | ||||
| from wutta_continuum import conf as mod | ||||
| 
 | ||||
| 
 | ||||
| class TestWuttaContinuumConfigExtension(DataTestCase): | ||||
| class TestWuttaContinuumConfigExtension(ConfigTestCase): | ||||
| 
 | ||||
|     def make_extension(self): | ||||
|         return mod.WuttaContinuumConfigExtension() | ||||
| 
 | ||||
|     def test_startup(self): | ||||
|     def test_startup_without_versioning(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() | ||||
| 
 | ||||
|                 # but will if we enable it in config | ||||
|     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: | ||||
|                 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): | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										40
									
								
								tests/test_util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tests/test_util.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| # -*- 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