feat: add basic db handler, for tracking counter values
more to come i'm sure, this is all i need so far
This commit is contained in:
		
							parent
							
								
									80a983f812
								
							
						
					
					
						commit
						51accc5a93
					
				
					 7 changed files with 177 additions and 2 deletions
				
			
		
							
								
								
									
										6
									
								
								docs/api/wuttjamaican.db.handler.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								docs/api/wuttjamaican.db.handler.rst
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
 | 
			
		||||
``wuttjamaican.db.handler``
 | 
			
		||||
===========================
 | 
			
		||||
 | 
			
		||||
.. automodule:: wuttjamaican.db.handler
 | 
			
		||||
   :members:
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +128,14 @@ Glossary
 | 
			
		|||
     Most :term:`apps<app>` will have at least one :term:`app
 | 
			
		||||
     database`.  See also :doc:`narr/db/index`.
 | 
			
		||||
 | 
			
		||||
   db handler
 | 
			
		||||
     The :term:`handler` responsible for various operations involving
 | 
			
		||||
     the :term:`app database` (and possibly other :term:`databases
 | 
			
		||||
     <database>`).
 | 
			
		||||
 | 
			
		||||
     See also the :class:`~wuttjamaican.db.handler.DatabaseHandler`
 | 
			
		||||
     base class.
 | 
			
		||||
 | 
			
		||||
   db session
 | 
			
		||||
     The "session" is a SQLAlchemy abstraction for an open database
 | 
			
		||||
     connection, essentially.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,6 +71,7 @@ Contents
 | 
			
		|||
   api/wuttjamaican.conf
 | 
			
		||||
   api/wuttjamaican.db
 | 
			
		||||
   api/wuttjamaican.db.conf
 | 
			
		||||
   api/wuttjamaican.db.handler
 | 
			
		||||
   api/wuttjamaican.db.model
 | 
			
		||||
   api/wuttjamaican.db.model.auth
 | 
			
		||||
   api/wuttjamaican.db.model.base
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,6 +81,7 @@ class AppHandler:
 | 
			
		|||
    default_model_spec = 'wuttjamaican.db.model'
 | 
			
		||||
    default_enum_spec = 'wuttjamaican.enum'
 | 
			
		||||
    default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler'
 | 
			
		||||
    default_db_handler_spec = 'wuttjamaican.db.handler:DatabaseHandler'
 | 
			
		||||
    default_email_handler_spec = 'wuttjamaican.email:EmailHandler'
 | 
			
		||||
    default_install_handler_spec = 'wuttjamaican.install:InstallHandler'
 | 
			
		||||
    default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
 | 
			
		||||
| 
						 | 
				
			
			@ -493,7 +494,7 @@ class AppHandler:
 | 
			
		|||
 | 
			
		||||
    def make_true_uuid(self):
 | 
			
		||||
        """
 | 
			
		||||
        Generate a new v7 UUID value.
 | 
			
		||||
        Generate a new UUID value.
 | 
			
		||||
 | 
			
		||||
        By default this simply calls
 | 
			
		||||
        :func:`wuttjamaican.util.make_true_uuid()`.
 | 
			
		||||
| 
						 | 
				
			
			@ -514,7 +515,7 @@ class AppHandler:
 | 
			
		|||
 | 
			
		||||
    def make_uuid(self):
 | 
			
		||||
        """
 | 
			
		||||
        Generate a new v7 UUID value.
 | 
			
		||||
        Generate a new UUID value.
 | 
			
		||||
 | 
			
		||||
        By default this simply calls
 | 
			
		||||
        :func:`wuttjamaican.util.make_uuid()`.
 | 
			
		||||
| 
						 | 
				
			
			@ -730,6 +731,19 @@ class AppHandler:
 | 
			
		|||
            self.handlers['auth'] = factory(self.config, **kwargs)
 | 
			
		||||
        return self.handlers['auth']
 | 
			
		||||
 | 
			
		||||
    def get_db_handler(self, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Get the configured :term:`db handler`.
 | 
			
		||||
 | 
			
		||||
        :rtype: :class:`~wuttjamaican.db.handler.DatabaseHandler`
 | 
			
		||||
        """
 | 
			
		||||
        if 'db' not in self.handlers:
 | 
			
		||||
            spec = self.config.get(f'{self.appname}.db.handler',
 | 
			
		||||
                                   default=self.default_db_handler_spec)
 | 
			
		||||
            factory = self.load_object(spec)
 | 
			
		||||
            self.handlers['db'] = factory(self.config, **kwargs)
 | 
			
		||||
        return self.handlers['db']
 | 
			
		||||
 | 
			
		||||
    def get_email_handler(self, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Get the configured :term:`email handler`.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										73
									
								
								src/wuttjamaican/db/handler.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/wuttjamaican/db/handler.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2024 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/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
"""
 | 
			
		||||
Database Handler
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.app import GenericHandler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DatabaseHandler(GenericHandler):
 | 
			
		||||
    """
 | 
			
		||||
    Base class and default implementation for the :term:`db handler`.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def next_counter_value(self, session, key):
 | 
			
		||||
        """
 | 
			
		||||
        Return the next counter value for the given key.
 | 
			
		||||
 | 
			
		||||
        If the DB backend is PostgreSQL, then a proper "sequence" is
 | 
			
		||||
        used for the counter.
 | 
			
		||||
 | 
			
		||||
        All other backends use a "fake" sequence by creating a
 | 
			
		||||
        dedicated table with auto-increment primary key, to provide
 | 
			
		||||
        the counter.
 | 
			
		||||
 | 
			
		||||
        :param session: Current :term:`db session`.
 | 
			
		||||
 | 
			
		||||
        :param key: Unique key indicating the counter for which the
 | 
			
		||||
           next value should be fetched.
 | 
			
		||||
 | 
			
		||||
        :returns: Next value as integer.
 | 
			
		||||
        """
 | 
			
		||||
        dialect = session.bind.url.get_dialect().name
 | 
			
		||||
 | 
			
		||||
        # postgres uses "true" native sequence
 | 
			
		||||
        if dialect == 'postgresql':
 | 
			
		||||
            sql = f"create sequence if not exists {key}_seq"
 | 
			
		||||
            session.execute(sa.text(sql))
 | 
			
		||||
            sql = f"select nextval('{key}_seq')"
 | 
			
		||||
            value = session.execute(sa.text(sql)).scalar()
 | 
			
		||||
            return value
 | 
			
		||||
 | 
			
		||||
        # otherwise use "magic" workaround
 | 
			
		||||
        engine = session.bind
 | 
			
		||||
        metadata = sa.MetaData()
 | 
			
		||||
        table = sa.Table(f'_counter_{key}', metadata,
 | 
			
		||||
                         sa.Column('value', sa.Integer(), primary_key=True))
 | 
			
		||||
        table.create(engine, checkfirst=True)
 | 
			
		||||
        with engine.begin() as cxn:
 | 
			
		||||
            result = cxn.execute(table.insert())
 | 
			
		||||
            return result.lastrowid
 | 
			
		||||
							
								
								
									
										64
									
								
								tests/db/test_handler.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								tests/db/test_handler.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,64 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
 | 
			
		||||
from unittest.mock import patch, MagicMock
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.testing import DataTestCase
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    import sqlalchemy as sa
 | 
			
		||||
    from wuttjamaican.db import handler as mod
 | 
			
		||||
except ImportError:
 | 
			
		||||
    pass
 | 
			
		||||
else:
 | 
			
		||||
 | 
			
		||||
    class TestDatabaseHandler(DataTestCase):
 | 
			
		||||
 | 
			
		||||
        def make_handler(self, **kwargs):
 | 
			
		||||
            return mod.DatabaseHandler(self.config, **kwargs)
 | 
			
		||||
 | 
			
		||||
        def test_next_counter_value_sqlite(self):
 | 
			
		||||
            handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
            # counter table should not exist yet
 | 
			
		||||
            metadata = sa.MetaData()
 | 
			
		||||
            metadata.reflect(self.session.bind)
 | 
			
		||||
            self.assertNotIn('_counter_testing', metadata.tables)
 | 
			
		||||
 | 
			
		||||
            # using sqlite as backend, should make table for counter
 | 
			
		||||
            value = handler.next_counter_value(self.session, 'testing')
 | 
			
		||||
            self.assertEqual(value, 1)
 | 
			
		||||
 | 
			
		||||
            # counter table should exist now
 | 
			
		||||
            metadata.reflect(self.session.bind)
 | 
			
		||||
            self.assertIn('_counter_testing', metadata.tables)
 | 
			
		||||
 | 
			
		||||
            # counter increments okay
 | 
			
		||||
            value = handler.next_counter_value(self.session, 'testing')
 | 
			
		||||
            self.assertEqual(value, 2)
 | 
			
		||||
            value = handler.next_counter_value(self.session, 'testing')
 | 
			
		||||
            self.assertEqual(value, 3)
 | 
			
		||||
 | 
			
		||||
        def test_next_counter_value_postgres(self):
 | 
			
		||||
            handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
            # counter table should not exist
 | 
			
		||||
            metadata = sa.MetaData()
 | 
			
		||||
            metadata.reflect(self.session.bind)
 | 
			
		||||
            self.assertNotIn('_counter_testing', metadata.tables)
 | 
			
		||||
 | 
			
		||||
            # nb. we have to pretty much mock this out, can't really
 | 
			
		||||
            # test true sequence behavior for postgres since tests are
 | 
			
		||||
            # using sqlite backend.
 | 
			
		||||
 | 
			
		||||
            # using postgres as backend, should use "sequence"
 | 
			
		||||
            with patch.object(self.session.bind.url, 'get_dialect') as get_dialect:
 | 
			
		||||
                get_dialect.return_value.name = 'postgresql'
 | 
			
		||||
                with patch.object(self.session, 'execute') as execute:
 | 
			
		||||
                    execute.return_value.scalar.return_value = 1
 | 
			
		||||
                    value = handler.next_counter_value(self.session, 'testing')
 | 
			
		||||
                    self.assertEqual(value, 1)
 | 
			
		||||
                    execute.return_value.scalar.assert_called_once_with()
 | 
			
		||||
 | 
			
		||||
            # counter table should still not exist
 | 
			
		||||
            metadata.reflect(self.session.bind)
 | 
			
		||||
            self.assertNotIn('_counter_testing', metadata.tables)
 | 
			
		||||
| 
						 | 
				
			
			@ -448,6 +448,15 @@ app_title = WuttaTest
 | 
			
		|||
        auth = self.app.get_auth_handler()
 | 
			
		||||
        self.assertIsInstance(auth, AuthHandler)
 | 
			
		||||
 | 
			
		||||
    def test_get_db_handler(self):
 | 
			
		||||
        try:
 | 
			
		||||
            from wuttjamaican.db.handler import DatabaseHandler
 | 
			
		||||
        except ImportError:
 | 
			
		||||
            pytest.skip("test not relevant without sqlalchemy")
 | 
			
		||||
 | 
			
		||||
        db = self.app.get_db_handler()
 | 
			
		||||
        self.assertIsInstance(db, DatabaseHandler)
 | 
			
		||||
 | 
			
		||||
    def test_get_email_handler(self):
 | 
			
		||||
        from wuttjamaican.email import EmailHandler
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue