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
db.model.auth db.model.auth
db.model.base db.model.base
db.model.upgrades
db.sess db.sess
enum
exc exc
people people
testing testing

View file

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

View file

@ -25,6 +25,11 @@ Glossary
Usually this is named ``app`` and is located at the root of the Usually this is named ``app`` and is located at the root of the
virtual environment. 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 app handler
Python object representing the core :term:`handler` for the Python object representing the core :term:`handler` for the
:term:`app`. There is normally just one "global" app handler; :term:`app`. There is normally just one "global" app handler;

View file

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

View file

@ -59,6 +59,16 @@ class AppHandler:
need to call :meth:`get_model()` yourself - that part will need to call :meth:`get_model()` yourself - that part will
happen automatically. 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 .. attribute:: providers
Dictionary of :class:`AppProvider` instances, as returned by Dictionary of :class:`AppProvider` instances, as returned by
@ -66,6 +76,7 @@ class AppHandler:
""" """
default_app_title = "WuttJamaican" default_app_title = "WuttJamaican"
default_model_spec = 'wuttjamaican.db.model' default_model_spec = 'wuttjamaican.db.model'
default_enum_spec = 'wuttjamaican.enum'
default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler' default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler'
default_people_handler_spec = 'wuttjamaican.people:PeopleHandler' default_people_handler_spec = 'wuttjamaican.people:PeopleHandler'
@ -103,6 +114,9 @@ class AppHandler:
if name == 'model': if name == 'model':
return self.get_model() return self.get_model()
if name == 'enum':
return self.get_enum()
if name == 'providers': if name == 'providers':
self.providers = self.get_all_providers() self.providers = self.get_all_providers()
return self.providers return self.providers
@ -298,6 +312,30 @@ class AppHandler:
self.model = importlib.import_module(spec) self.model = importlib.import_module(spec)
return self.model 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): def load_object(self, spec):
""" """
Import and/or load and return the object designated by the Import and/or load and return the object designated by the

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
import alembic_postgresql_enum
from alembic import context from alembic import context
from wuttjamaican.conf import make_config 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.Permission`
* :class:`~wuttjamaican.db.model.auth.User` * :class:`~wuttjamaican.db.model.auth.User`
* :class:`~wuttjamaican.db.model.auth.UserRole` * :class:`~wuttjamaican.db.model.auth.UserRole`
* :class:`~wuttjamaican.db.model.upgrades.Upgrade`
""" """
from .base import uuid_column, uuid_fk_column, Base, Setting, Person from .base import uuid_column, uuid_fk_column, Base, Setting, Person
from .auth import Role, Permission, User, UserRole 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 pytest
import wuttjamaican.enum
from wuttjamaican import app from wuttjamaican import app
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttjamaican.util import UNSPECIFIED from wuttjamaican.util import UNSPECIFIED
@ -27,6 +28,9 @@ class TestAppHandler(TestCase):
self.assertEqual(self.app.handlers, {}) self.assertEqual(self.app.handlers, {})
self.assertEqual(self.app.appname, 'wuttatest') self.assertEqual(self.app.appname, 'wuttatest')
def test_get_enum(self):
self.assertIs(self.app.get_enum(), wuttjamaican.enum)
def test_load_object(self): def test_load_object(self):
# just confirm the method works on a basic level; the # just confirm the method works on a basic level; the
@ -403,6 +407,12 @@ class TestAppProvider(TestCase):
def test_getattr(self): 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): class FakeProvider(app.AppProvider):
def fake_foo(self): def fake_foo(self):
return 42 return 42
@ -417,6 +427,16 @@ class TestAppProvider(TestCase):
self.assertIs(self.app.providers, fake_providers) self.assertIs(self.app.providers, fake_providers)
get_all_providers.assert_called_once_with() 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): def test_getattr_providers(self):
# collection of providers is loaded on demand # collection of providers is loaded on demand