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
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…
Reference in a new issue