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
|
||||||
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
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.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
|
||||||
|
|
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 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
|
||||||
|
|
Loading…
Reference in a new issue