feat: initial release

This commit is contained in:
Lance Edgar 2024-08-27 19:18:35 -05:00
commit 9dfe2a8d6b
24 changed files with 1370 additions and 0 deletions

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-Continuum -- SQLAlchemy Versioning for WuttJamaican
# 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/>.
#
################################################################################
"""
Wutta-Continuum -- SQLAlchemy-Continuum versioning for WuttJamaican
"""
from ._version import __version__

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8; -*-
from importlib.metadata import version
__version__ = version('Wutta-Continuum')

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-Continuum -- SQLAlchemy Versioning for WuttJamaican
# 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/>.
#
################################################################################
"""
App Provider
"""
from wuttjamaican.app import AppProvider
class WuttaContinuumAppProvider(AppProvider):
"""
The :term:`app provider` for WuttaWeb. This adds some methods to
the :term:`app handler`, which are specific to Wutta-Continuum.
"""
def continuum_is_enabled(self):
"""
Returns boolean indicating if Wutta-Continuum is enabled.
This checks the config value as described in
:doc:`/narr/install`; default will be ``False``.
"""
return self.config.get_bool('wutta_continuum.enable_versioning',
usedb=False, default=False)

118
src/wutta_continuum/conf.py Normal file
View file

@ -0,0 +1,118 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-Continuum -- SQLAlchemy Versioning for WuttJamaican
# 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/>.
#
################################################################################
"""
App Configuration
"""
import datetime
import socket
from sqlalchemy.orm import configure_mappers
from sqlalchemy_continuum import make_versioned
from sqlalchemy_continuum.plugins import Plugin
from wuttjamaican.conf import WuttaConfigExtension
from wuttjamaican.util import load_object
class WuttaContinuumConfigExtension(WuttaConfigExtension):
"""
App :term:`config extension` for Wutta-Continuum.
This adds a startup hook, which can optionally turn on the
SQLAlchemy-Continuum versioning features for the main app DB.
"""
key = 'wutta_continuum'
def startup(self, config):
""" """
# only do this if config enables it
if not config.get_bool('wutta_continuum.enable_versioning',
usedb=False, default=False):
return
# create wutta plugin, to assign user and ip address
spec = config.get('wutta_continuum.wutta_plugin_spec',
usedb=False,
default='wutta_continuum.conf:WuttaContinuumPlugin')
WuttaPlugin = load_object(spec)
# tell sqlalchemy-continuum to do its thing
make_versioned(plugins=[WuttaPlugin()])
# nb. must load the model before configuring mappers
app = config.get_app()
model = app.model
# tell sqlalchemy to do its thing
configure_mappers()
class WuttaContinuumPlugin(Plugin):
"""
SQLAlchemy-Continuum manager plugin for Wutta-Continuum.
This tries to assign the current user and IP address to the
transaction.
It will assume the "current machine" IP address, which may be
suitable for some apps but not all (e.g. web apps, where IP
address should reflect an arbitrary client machine).
However it does not actually have a way to determine the current
user. WuttaWeb therefore uses a different plugin, based on this
one, to get both the user and IP address from current request.
You can override this to use a custom plugin for this purpose; if
so you must specify in your config file:
.. code-block:: ini
[wutta_continuum]
wutta_plugin_spec = poser.db.continuum:PoserContinuumPlugin
See also the SQLAlchemy-Continuum docs for
:doc:`sqlalchemy-continuum:plugins`.
"""
def get_remote_addr(self, uow, session):
""" """
host = socket.gethostname()
return socket.gethostbyname(host)
def get_user_id(self, uow, session):
""" """
def transaction_args(self, uow, session):
""" """
kwargs = {}
remote_addr = self.get_remote_addr(uow, session)
if remote_addr:
kwargs['remote_addr'] = remote_addr
user_id = self.get_user_id(uow, session)
if user_id:
kwargs['user_id'] = user_id
return kwargs

View file

View file

@ -0,0 +1,142 @@
"""first versioning tables
Revision ID: 71406251b8e7
Revises:
Create Date: 2024-08-27 18:28:31.488291
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '71406251b8e7'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = ('wutta_continuum',)
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# transaction
op.create_table('transaction',
sa.Column('issued_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('remote_addr', sa.String(length=50), nullable=True),
sa.Column('user_id', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], name=op.f('fk_transaction_user_id_user')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_transaction'))
)
op.create_index(op.f('ix_transaction_user_id'), 'transaction', ['user_id'], unique=False)
# person
op.create_table('person_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('full_name', sa.String(length=100), autoincrement=False, nullable=False),
sa.Column('first_name', sa.String(length=50), autoincrement=False, nullable=True),
sa.Column('middle_name', sa.String(length=50), autoincrement=False, nullable=True),
sa.Column('last_name', sa.String(length=50), autoincrement=False, nullable=True),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id', name=op.f('pk_person_version'))
)
op.create_index(op.f('ix_person_version_end_transaction_id'), 'person_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_person_version_operation_type'), 'person_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_person_version_transaction_id'), 'person_version', ['transaction_id'], unique=False)
# user
op.create_table('user_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('username', sa.String(length=25), autoincrement=False, nullable=False),
sa.Column('password', sa.String(length=60), autoincrement=False, nullable=True),
sa.Column('person_uuid', sa.String(length=32), autoincrement=False, nullable=True),
sa.Column('active', sa.Boolean(), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id', name=op.f('pk_user_version'))
)
op.create_index(op.f('ix_user_version_end_transaction_id'), 'user_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_user_version_operation_type'), 'user_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_user_version_transaction_id'), 'user_version', ['transaction_id'], unique=False)
# role
op.create_table('role_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('name', sa.String(length=100), autoincrement=False, nullable=False),
sa.Column('notes', sa.Text(), autoincrement=False, nullable=True),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id', name=op.f('pk_role_version'))
)
op.create_index(op.f('ix_role_version_end_transaction_id'), 'role_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_role_version_operation_type'), 'role_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_role_version_transaction_id'), 'role_version', ['transaction_id'], unique=False)
# user_x_role
op.create_table('user_x_role_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('user_uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('role_uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id', name=op.f('pk_user_x_role_version'))
)
op.create_index(op.f('ix_user_x_role_version_end_transaction_id'), 'user_x_role_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_user_x_role_version_operation_type'), 'user_x_role_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_user_x_role_version_transaction_id'), 'user_x_role_version', ['transaction_id'], unique=False)
# permission
op.create_table('permission_version',
sa.Column('role_uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('permission', sa.String(length=254), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('role_uuid', 'permission', 'transaction_id', name=op.f('pk_permission_version'))
)
op.create_index(op.f('ix_permission_version_end_transaction_id'), 'permission_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_permission_version_operation_type'), 'permission_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_permission_version_transaction_id'), 'permission_version', ['transaction_id'], unique=False)
def downgrade() -> None:
# permission
op.drop_index(op.f('ix_permission_version_transaction_id'), table_name='permission_version')
op.drop_index(op.f('ix_permission_version_operation_type'), table_name='permission_version')
op.drop_index(op.f('ix_permission_version_end_transaction_id'), table_name='permission_version')
op.drop_table('permission_version')
# user_x_role
op.drop_index(op.f('ix_user_x_role_version_transaction_id'), table_name='user_x_role_version')
op.drop_index(op.f('ix_user_x_role_version_operation_type'), table_name='user_x_role_version')
op.drop_index(op.f('ix_user_x_role_version_end_transaction_id'), table_name='user_x_role_version')
op.drop_table('user_x_role_version')
# role
op.drop_index(op.f('ix_role_version_transaction_id'), table_name='role_version')
op.drop_index(op.f('ix_role_version_operation_type'), table_name='role_version')
op.drop_index(op.f('ix_role_version_end_transaction_id'), table_name='role_version')
op.drop_table('role_version')
# user
op.drop_index(op.f('ix_user_version_transaction_id'), table_name='user_version')
op.drop_index(op.f('ix_user_version_operation_type'), table_name='user_version')
op.drop_index(op.f('ix_user_version_end_transaction_id'), table_name='user_version')
op.drop_table('user_version')
# person
op.drop_index(op.f('ix_person_version_transaction_id'), table_name='person_version')
op.drop_index(op.f('ix_person_version_operation_type'), table_name='person_version')
op.drop_index(op.f('ix_person_version_end_transaction_id'), table_name='person_version')
op.drop_table('person_version')
# transaction
op.drop_index(op.f('ix_transaction_user_id'), table_name='transaction')
op.drop_table('transaction')