feat: add table/model for app upgrades
This commit is contained in:
parent
e855a84c37
commit
110ff69d6d
6
docs/api/wuttjamaican/db.model.upgrades.rst
Normal file
6
docs/api/wuttjamaican/db.model.upgrades.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.db.model.upgrades``
|
||||
==================================
|
||||
|
||||
.. automodule:: wuttjamaican.db.model.upgrades
|
||||
:members:
|
6
docs/api/wuttjamaican/enum.rst
Normal file
6
docs/api/wuttjamaican/enum.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.enum``
|
||||
=====================
|
||||
|
||||
.. automodule:: wuttjamaican.enum
|
||||
:members:
|
|
@ -15,7 +15,9 @@
|
|||
db.model
|
||||
db.model.auth
|
||||
db.model.base
|
||||
db.model.upgrades
|
||||
db.sess
|
||||
enum
|
||||
exc
|
||||
people
|
||||
testing
|
||||
|
|
|
@ -22,6 +22,7 @@ extensions = [
|
|||
'sphinxcontrib.programoutput',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo',
|
||||
'enum_tools.autoenum',
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
import alembic_postgresql_enum
|
||||
from alembic import context
|
||||
|
||||
from wuttjamaican.conf import make_config
|
||||
|
|
|
@ -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())
|
|
@ -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
|
||||
|
|
93
src/wuttjamaican/db/model/upgrades.py
Normal file
93
src/wuttjamaican/db/model/upgrades.py
Normal 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
38
src/wuttjamaican/enum.py
Normal 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'
|
15
tests/db/model/test_upgrades.py
Normal file
15
tests/db/model/test_upgrades.py
Normal 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")
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue