2
0
Fork 0

feat: add table/model for app upgrades

This commit is contained in:
Lance Edgar 2024-08-24 10:20:05 -05:00
parent e855a84c37
commit 110ff69d6d
14 changed files with 275 additions and 2 deletions

View file

@ -0,0 +1,6 @@
``wuttjamaican.db.model.upgrades``
==================================
.. automodule:: wuttjamaican.db.model.upgrades
:members:

View file

@ -0,0 +1,6 @@
``wuttjamaican.enum``
=====================
.. automodule:: wuttjamaican.enum
:members:

View file

@ -15,7 +15,9 @@
db.model
db.model.auth
db.model.base
db.model.upgrades
db.sess
enum
exc
people
testing

View file

@ -22,6 +22,7 @@ extensions = [
'sphinxcontrib.programoutput',
'sphinx.ext.viewcode',
'sphinx.ext.todo',
'enum_tools.autoenum',
]
templates_path = ['_templates']

View file

@ -25,6 +25,11 @@ Glossary
Usually this is named ``app`` and is located at the root of the
virtual environment.
app enum
Python module whose namespace contains all the "enum" values
used by the :term:`app`. Available on the :term:`app handler`
as :attr:`~wuttjamaican.app.AppHandler.enum`.
app handler
Python object representing the core :term:`handler` for the
:term:`app`. There is normally just one "global" app handler;

View file

@ -32,8 +32,8 @@ dependencies = [
[project.optional-dependencies]
db = ["SQLAlchemy<2", "alembic", "passlib"]
docs = ["Sphinx", "sphinxcontrib-programoutput", "furo"]
db = ["SQLAlchemy<2", "alembic", "alembic-postgresql-enum", "passlib"]
docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"]
tests = ["pytest-cov", "tox"]

View file

@ -59,6 +59,16 @@ class AppHandler:
need to call :meth:`get_model()` yourself - that part will
happen automatically.
.. attribute:: enum
Reference to the :term:`app enum` module.
Note that :meth:`get_enum()` is responsible for determining
which module this will point to. However you can always get
the model using this attribute (e.g. ``app.enum``) and do not
need to call :meth:`get_enum()` yourself - that part will
happen automatically.
.. attribute:: providers
Dictionary of :class:`AppProvider` instances, as returned by
@ -66,6 +76,7 @@ class AppHandler:
"""
default_app_title = "WuttJamaican"
default_model_spec = 'wuttjamaican.db.model'
default_enum_spec = 'wuttjamaican.enum'
default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler'
default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
@ -103,6 +114,9 @@ class AppHandler:
if name == 'model':
return self.get_model()
if name == 'enum':
return self.get_enum()
if name == 'providers':
self.providers = self.get_all_providers()
return self.providers
@ -298,6 +312,30 @@ class AppHandler:
self.model = importlib.import_module(spec)
return self.model
def get_enum(self):
"""
Returns the :term:`app enum` module.
Note that you don't actually need to call this method; you can
get the module by simply accessing :attr:`enum`
(e.g. ``app.enum``) instead.
By default this will return :mod:`wuttjamaican.enum` unless
the config class or some :term:`config extension` has provided
another default.
A custom app can override the default like so (within a config
extension)::
config.setdefault('wutta.enum_spec', 'poser.enum')
"""
if 'enum' not in self.__dict__:
spec = self.config.get(f'{self.appname}.enum_spec',
usedb=False,
default=self.default_enum_spec)
self.enum = importlib.import_module(spec)
return self.enum
def load_object(self, spec):
"""
Import and/or load and return the object designated by the

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8; -*-
import alembic_postgresql_enum
from alembic import context
from wuttjamaican.conf import make_config

View file

@ -0,0 +1,46 @@
"""add upgrades
Revision ID: ebd75b9feaa7
Revises: 3abcc44f7f91
Create Date: 2024-08-24 09:42:21.199679
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'ebd75b9feaa7'
down_revision: Union[str, None] = '3abcc44f7f91'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# upgrade
sa.Enum('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus').create(op.get_bind())
op.create_table('upgrade',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by_uuid', sa.String(length=32), nullable=False),
sa.Column('description', sa.String(length=255), nullable=False),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('executing', sa.Boolean(), nullable=False),
sa.Column('status', postgresql.ENUM('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus', create_type=False), nullable=False),
sa.Column('executed', sa.DateTime(timezone=True), nullable=True),
sa.Column('executed_by_uuid', sa.String(length=32), nullable=True),
sa.Column('exit_code', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_upgrade_created_by_uuid_user')),
sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_upgrade_executed_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_upgrade'))
)
def downgrade() -> None:
# upgrade
op.drop_table('upgrade')
sa.Enum('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus').drop(op.get_bind())

View file

@ -36,7 +36,9 @@ The ``wuttjamaican.db.model`` namespace contains the following:
* :class:`~wuttjamaican.db.model.auth.Permission`
* :class:`~wuttjamaican.db.model.auth.User`
* :class:`~wuttjamaican.db.model.auth.UserRole`
* :class:`~wuttjamaican.db.model.upgrades.Upgrade`
"""
from .base import uuid_column, uuid_fk_column, Base, Setting, Person
from .auth import Role, Permission, User, UserRole
from .upgrades import Upgrade

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-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/>.
#
################################################################################
"""
Upgrade Model
"""
import datetime
import sqlalchemy as sa
from sqlalchemy import orm
from .base import Base, uuid_column, uuid_fk_column
from wuttjamaican.enum import UpgradeStatus
class Upgrade(Base):
"""
Represents an app upgrade.
"""
__tablename__ = 'upgrade'
uuid = uuid_column()
created = sa.Column(sa.DateTime(timezone=True), nullable=False,
default=datetime.datetime.now, doc="""
When the upgrade record was created.
""")
created_by_uuid = uuid_fk_column('user.uuid', nullable=False)
created_by = orm.relationship(
'User',
foreign_keys=[created_by_uuid],
doc="""
:class:`~wuttjamaican.db.model.auth.User` who created the
upgrade record.
""")
description = sa.Column(sa.String(length=255), nullable=False, doc="""
Basic (identifying) description for the upgrade.
""")
notes = sa.Column(sa.Text(), nullable=True, doc="""
Notes for the upgrade.
""")
executing = sa.Column(sa.Boolean(), nullable=False, default=False, doc="""
Whether or not the upgrade is currently being performed.
""")
status = sa.Column(sa.Enum(UpgradeStatus), nullable=False, doc="""
Current status for the upgrade. This field uses an enum,
:class:`~wuttjamaican.enum.UpgradeStatus`.
""")
executed = sa.Column(sa.DateTime(timezone=True), nullable=True, doc="""
When the upgrade was executed.
""")
executed_by_uuid = uuid_fk_column('user.uuid', nullable=True)
executed_by = orm.relationship(
'User',
foreign_keys=[executed_by_uuid],
doc="""
:class:`~wuttjamaican.db.model.auth.User` who executed the
upgrade.
""")
exit_code = sa.Column(sa.Integer(), nullable=True, doc="""
Exit code for the upgrade execution process, if applicable.
""")
def __str__(self):
return str(self.description or "")

38
src/wuttjamaican/enum.py Normal file
View file

@ -0,0 +1,38 @@
# -*- coding: utf-8; -*-
################################################################################
#
# WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-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/>.
#
################################################################################
"""
Enum Values
"""
from enum import Enum
class UpgradeStatus(Enum):
"""
Enum values for
:attr:`wuttjamaican.db.model.upgrades.Upgrade.status`.
"""
PENDING = 'pending'
EXECUTING = 'executing'
SUCCESS = 'success'
FAILURE = 'failure'

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8; -*-
from unittest import TestCase
try:
from wuttjamaican.db.model import upgrades as mod
except ImportError:
pass
else:
class TestUpgrade(TestCase):
def test_str(self):
upgrade = mod.Upgrade(description="upgrade foo")
self.assertEqual(str(upgrade), "upgrade foo")

View file

@ -10,6 +10,7 @@ from unittest.mock import patch, MagicMock
import pytest
import wuttjamaican.enum
from wuttjamaican import app
from wuttjamaican.conf import WuttaConfig
from wuttjamaican.util import UNSPECIFIED
@ -27,6 +28,9 @@ class TestAppHandler(TestCase):
self.assertEqual(self.app.handlers, {})
self.assertEqual(self.app.appname, 'wuttatest')
def test_get_enum(self):
self.assertIs(self.app.get_enum(), wuttjamaican.enum)
def test_load_object(self):
# just confirm the method works on a basic level; the
@ -403,6 +407,12 @@ class TestAppProvider(TestCase):
def test_getattr(self):
# enum
self.assertNotIn('enum', self.app.__dict__)
self.assertIs(self.app.enum, wuttjamaican.enum)
# now we test that providers are loaded...
class FakeProvider(app.AppProvider):
def fake_foo(self):
return 42
@ -417,6 +427,16 @@ class TestAppProvider(TestCase):
self.assertIs(self.app.providers, fake_providers)
get_all_providers.assert_called_once_with()
def test_getattr_model(self):
try:
import wuttjamaican.db.model
except ImportError:
pytest.skip("test not relevant without sqlalchemy")
# model
self.assertNotIn('model', self.app.__dict__)
self.assertIs(self.app.model, wuttjamaican.db.model)
def test_getattr_providers(self):
# collection of providers is loaded on demand