Compare commits
No commits in common. "0588a0a2d39f90cea9e26ec34d0652cdbb4b4c8e" and "5a2748c7751f182882673a1431ea4d222233f1ac" have entirely different histories.
0588a0a2d3
...
5a2748c775
8 changed files with 42 additions and 313 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,17 +5,6 @@ All notable changes to WuttJamaican will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.28.0 (2025-12-28)
|
||||
|
||||
### Feat
|
||||
|
||||
- add alembic config/utility functions, for migrations admin
|
||||
|
||||
### Fix
|
||||
|
||||
- add empty migration script, to avoid test problems
|
||||
- show deprecation warnings by default for 'wutt*' packages
|
||||
|
||||
## v0.27.1 (2025-12-21)
|
||||
|
||||
### Fix
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ invoke other app/handler logic::
|
|||
# invoke secondary handler to make new user account
|
||||
auth = app.get_auth_handler()
|
||||
user = auth.make_user(session=session, username='barney')
|
||||
assert isinstance(user, model.User)
|
||||
|
||||
# commit changes to DB
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "WuttJamaican"
|
||||
version = "0.28.0"
|
||||
version = "0.27.1"
|
||||
description = "Base package for Wutta Framework"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import logging.config
|
|||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
|
||||
import config as configuration
|
||||
|
||||
|
|
@ -1021,54 +1020,41 @@ def make_config( # pylint: disable=too-many-arguments,too-many-positional-argum
|
|||
|
||||
:returns: The new config object.
|
||||
"""
|
||||
# collect file paths
|
||||
files = get_config_paths(
|
||||
files=files,
|
||||
plus_files=plus_files,
|
||||
appname=appname,
|
||||
env_files_name=env_files_name,
|
||||
env_plus_files_name=env_plus_files_name,
|
||||
env=env,
|
||||
default_files=default_files,
|
||||
winsvc=winsvc,
|
||||
)
|
||||
|
||||
# nb. always show deprecation warnings when making config
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("default", category=DeprecationWarning, module=r"^wutt")
|
||||
# make config object
|
||||
if not factory:
|
||||
factory = WuttaConfig
|
||||
config = factory(files, appname=appname, usedb=usedb, preferdb=preferdb, **kwargs)
|
||||
|
||||
# collect file paths
|
||||
files = get_config_paths(
|
||||
files=files,
|
||||
plus_files=plus_files,
|
||||
appname=appname,
|
||||
env_files_name=env_files_name,
|
||||
env_plus_files_name=env_plus_files_name,
|
||||
env=env,
|
||||
default_files=default_files,
|
||||
winsvc=winsvc,
|
||||
)
|
||||
# maybe extend config object
|
||||
if extend:
|
||||
if not extension_entry_points:
|
||||
# nb. must not use appname here, entry points must be
|
||||
# consistent regardless of appname
|
||||
extension_entry_points = "wutta.config.extensions"
|
||||
|
||||
# make config object
|
||||
if not factory:
|
||||
factory = WuttaConfig
|
||||
config = factory(
|
||||
files, appname=appname, usedb=usedb, preferdb=preferdb, **kwargs
|
||||
)
|
||||
# apply all registered extensions
|
||||
# TODO: maybe let config disable some extensions?
|
||||
extensions = load_entry_points(extension_entry_points)
|
||||
extensions = [ext() for ext in extensions.values()]
|
||||
for extension in extensions:
|
||||
log.debug("applying config extension: %s", extension.key)
|
||||
extension.configure(config)
|
||||
|
||||
# maybe extend config object
|
||||
if extend:
|
||||
if not extension_entry_points:
|
||||
# nb. must not use appname here, entry points must be
|
||||
# consistent regardless of appname
|
||||
extension_entry_points = "wutta.config.extensions"
|
||||
|
||||
# apply all registered extensions
|
||||
# TODO: maybe let config disable some extensions?
|
||||
extensions = load_entry_points(extension_entry_points)
|
||||
extensions = [ext() for ext in extensions.values()]
|
||||
for extension in extensions:
|
||||
log.debug("applying config extension: %s", extension.key)
|
||||
extension.configure(config)
|
||||
|
||||
# let extensions run startup hooks if needed
|
||||
for extension in extensions:
|
||||
extension.startup(config)
|
||||
|
||||
# maybe show deprecation warnings from now on
|
||||
if config.get_bool(
|
||||
f"{config.appname}.show_deprecation_warnings", usedb=False, default=True
|
||||
):
|
||||
warnings.filterwarnings("default", category=DeprecationWarning, module=r"^wutt")
|
||||
# let extensions run startup hooks if needed
|
||||
for extension in extensions:
|
||||
extension.startup(config)
|
||||
|
||||
return config
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
"""empty migration
|
||||
|
||||
Revision ID: 4d3696b894d5
|
||||
Revises: b59a34266288
|
||||
Create Date: 2025-12-28 13:56:20.900043
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import wuttjamaican.db.util
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "4d3696b894d5"
|
||||
down_revision: Union[str, None] = "b59a34266288"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
# NOTE: this empty revision exists simply to ensure that its down
|
||||
# revision (b59a34266288) is not the branch head. and that is needed
|
||||
# because some of the tests in wuttaweb now run commands like these:
|
||||
#
|
||||
# alembic downgrade wutta@-1
|
||||
# alembic upgrade heads
|
||||
#
|
||||
# which is actually fine for postgres but not so for sqlite, due to
|
||||
# the particular contents of the b59a34266288 revision.
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023-2025 Lance Edgar
|
||||
# Copyright © 2023-2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
|
|
@ -27,9 +27,6 @@ WuttJamaican - database configuration
|
|||
from collections import OrderedDict
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from alembic.script import ScriptDirectory
|
||||
from alembic.migration import MigrationContext
|
||||
|
||||
from wuttjamaican.util import load_object, parse_bool, parse_list
|
||||
|
||||
|
|
@ -153,93 +150,3 @@ def make_engine_from_config(config_dict, prefix="sqlalchemy.", **kwargs):
|
|||
engine = sa.engine_from_config(config_dict, prefix, **kwargs)
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
##############################
|
||||
# alembic functions
|
||||
##############################
|
||||
|
||||
|
||||
def make_alembic_config(config):
|
||||
"""
|
||||
Make and return a new Alembic config object, based on current app
|
||||
config.
|
||||
|
||||
This tries to set the following on the Alembic config:
|
||||
|
||||
* :attr:`~alembic:alembic.config.Config.config_file_name` - set to
|
||||
app's primary config file
|
||||
* main option ``script_location``
|
||||
* main option ``version_locations``
|
||||
|
||||
The latter 2 are read normally from app config, then set on the
|
||||
Alembic config via
|
||||
:meth:`~alembic:alembic.config.Config.set_main_option()`.
|
||||
|
||||
.. note::
|
||||
|
||||
IIUC, Alembic should not need to attempt to read config values
|
||||
from file, as long as we're able to set the above explicitly.
|
||||
However we set the ``config_file_name`` "just in case" Alembic
|
||||
needs it, but also to ensure it is discoverable from within the
|
||||
``env.py`` script...
|
||||
|
||||
When a migration script runs, code within ``env.py`` will call
|
||||
:func:`make_config()` using the filename which it inspects from
|
||||
the Alembic config.
|
||||
|
||||
(Confused yet?!)
|
||||
|
||||
:returns: :class:`alembic:alembic.config.Config` instance
|
||||
"""
|
||||
alembic_config = AlembicConfig()
|
||||
|
||||
# TODO: not sure what we can do here besides assume the "primary"
|
||||
# config file should be used?
|
||||
if config.files_read:
|
||||
alembic_config.config_file_name = config.get_prioritized_files()[0]
|
||||
|
||||
if script_location := config.get("alembic.script_location", usedb=False):
|
||||
alembic_config.set_main_option("script_location", script_location)
|
||||
|
||||
if version_locations := config.get("alembic.version_locations", usedb=False):
|
||||
alembic_config.set_main_option("version_locations", version_locations)
|
||||
|
||||
return alembic_config
|
||||
|
||||
|
||||
def get_alembic_scriptdir(config, alembic_config=None):
|
||||
"""
|
||||
Get a "Script Directory" object for Alembic.
|
||||
|
||||
This allows for inspection of the migration scripts.
|
||||
|
||||
:param config: App config object.
|
||||
|
||||
:param alembic_config: Alembic config object, if you have one.
|
||||
Otherwise :func:`make_alembic_config()` will be called.
|
||||
|
||||
:returns: :class:`~alembic:alembic.script.ScriptDirectory` instance
|
||||
"""
|
||||
if not alembic_config:
|
||||
alembic_config = make_alembic_config(config)
|
||||
return ScriptDirectory.from_config(alembic_config)
|
||||
|
||||
|
||||
def check_alembic_current(config, alembic_config=None):
|
||||
"""
|
||||
Compare the current revisions in the :term:`app database` to those
|
||||
found in the migration scripts.
|
||||
|
||||
:param config: App config object.
|
||||
|
||||
:param alembic_config: Alembic config object, if you have one.
|
||||
Otherwise :func:`make_alembic_config()` will be called.
|
||||
|
||||
:returns: ``True`` if the DB already has all migrations applied;
|
||||
``False`` if not.
|
||||
"""
|
||||
script = get_alembic_scriptdir(config, alembic_config)
|
||||
with config.appdb_engine.begin() as conn:
|
||||
context = MigrationContext.configure(conn)
|
||||
return set(context.get_current_heads()) == set(script.get_heads())
|
||||
|
|
|
|||
|
|
@ -176,10 +176,9 @@ class ConfigTestCase(FileTestCase):
|
|||
"""
|
||||
self.teardown_files()
|
||||
|
||||
def make_config( # pylint: disable=missing-function-docstring
|
||||
self, files=None, **kwargs
|
||||
):
|
||||
return WuttaConfig(files, **kwargs)
|
||||
def make_config(self, **kwargs): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
return WuttaConfig(**kwargs)
|
||||
|
||||
|
||||
class DataTestCase(ConfigTestCase):
|
||||
|
|
@ -214,8 +213,6 @@ class DataTestCase(ConfigTestCase):
|
|||
teardown methods, as this class handles that automatically.
|
||||
"""
|
||||
|
||||
sqlite_engine_url = "sqlite://"
|
||||
|
||||
def setUp(self): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
self.setup_db()
|
||||
|
|
@ -240,7 +237,8 @@ class DataTestCase(ConfigTestCase):
|
|||
"""
|
||||
self.teardown_config()
|
||||
|
||||
def make_config(self, files=None, **kwargs):
|
||||
def make_config(self, **kwargs): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
defaults = kwargs.setdefault("defaults", {})
|
||||
defaults.setdefault("wutta.db.default.url", self.sqlite_engine_url)
|
||||
return super().make_config(files, **kwargs)
|
||||
defaults.setdefault("wutta.db.default.url", "sqlite://")
|
||||
return super().make_config(**kwargs)
|
||||
|
|
|
|||
|
|
@ -6,18 +6,13 @@ import tempfile
|
|||
from unittest import TestCase
|
||||
|
||||
from wuttjamaican.conf import WuttaConfig
|
||||
from wuttjamaican.testing import ConfigTestCase, DataTestCase
|
||||
|
||||
try:
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
from alembic import command as alembic_command
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from alembic.script import ScriptDirectory
|
||||
from wuttjamaican.db import conf
|
||||
from wuttjamaican.db import conf as mod
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -159,111 +154,3 @@ else:
|
|||
}
|
||||
)
|
||||
self.assertTrue(engine.pool._pre_ping)
|
||||
|
||||
class TestMakeAlembicConfig(ConfigTestCase):
|
||||
|
||||
def test_defaults(self):
|
||||
|
||||
# without config file
|
||||
self.assertFalse(self.config.files_read)
|
||||
alembic = mod.make_alembic_config(self.config)
|
||||
self.assertIsInstance(alembic, AlembicConfig)
|
||||
self.assertIsNone(alembic.config_file_name)
|
||||
self.assertIsNone(alembic.get_main_option("script_location"))
|
||||
self.assertIsNone(alembic.get_main_option("version_locations"))
|
||||
|
||||
# with config file
|
||||
path = self.write_file("test.ini", "[alembic]")
|
||||
self.config.files_read = [path]
|
||||
alembic = mod.make_alembic_config(self.config)
|
||||
self.assertIsInstance(alembic, AlembicConfig)
|
||||
self.assertEqual(alembic.config_file_name, path)
|
||||
self.assertIsNone(alembic.get_main_option("script_location"))
|
||||
self.assertIsNone(alembic.get_main_option("version_locations"))
|
||||
|
||||
def test_configured(self):
|
||||
self.config.setdefault("alembic.script_location", "wuttjamaican.db:alembic")
|
||||
self.config.setdefault(
|
||||
"alembic.version_locations", "wuttjamaican.db:alembic/versions"
|
||||
)
|
||||
|
||||
alembic = mod.make_alembic_config(self.config)
|
||||
self.assertIsInstance(alembic, AlembicConfig)
|
||||
self.assertEqual(
|
||||
alembic.get_main_option("script_location"), "wuttjamaican.db:alembic"
|
||||
)
|
||||
self.assertEqual(
|
||||
alembic.get_main_option("version_locations"),
|
||||
"wuttjamaican.db:alembic/versions",
|
||||
)
|
||||
|
||||
class TestGetAlembicScriptdir(ConfigTestCase):
|
||||
|
||||
def test_basic(self):
|
||||
self.config.setdefault("alembic.script_location", "wuttjamaican.db:alembic")
|
||||
self.config.setdefault(
|
||||
"alembic.version_locations", "wuttjamaican.db:alembic/versions"
|
||||
)
|
||||
|
||||
# can provide alembic config
|
||||
alembic = mod.make_alembic_config(self.config)
|
||||
script = mod.get_alembic_scriptdir(self.config, alembic)
|
||||
self.assertIsInstance(script, ScriptDirectory)
|
||||
|
||||
# but also can omit it
|
||||
script = mod.get_alembic_scriptdir(self.config)
|
||||
self.assertIsInstance(script, ScriptDirectory)
|
||||
|
||||
class TestCheckAlembicCurrent(DataTestCase):
|
||||
|
||||
def make_config(self, **kwargs):
|
||||
sqlite_path = self.write_file("test.sqlite", "")
|
||||
self.sqlite_engine_url = f"sqlite:///{sqlite_path}"
|
||||
|
||||
config_path = self.write_file(
|
||||
"test.ini",
|
||||
f"""
|
||||
[wutta.db]
|
||||
default.url = {self.sqlite_engine_url}
|
||||
|
||||
[alembic]
|
||||
script_location = wuttjamaican.db:alembic
|
||||
version_locations = wuttjamaican.db:alembic/versions
|
||||
""",
|
||||
)
|
||||
|
||||
return super().make_config([config_path], **kwargs)
|
||||
|
||||
def test_basic(self):
|
||||
alembic = mod.make_alembic_config(self.config)
|
||||
self.assertIsNotNone(alembic.get_main_option("script_location"))
|
||||
self.assertIsNotNone(alembic.get_main_option("version_locations"))
|
||||
|
||||
# false by default, since tests use MetaData.create_all()
|
||||
# instead of migrations for setup
|
||||
self.assertFalse(mod.check_alembic_current(self.config, alembic))
|
||||
|
||||
# and to further prove the point, alembic_version table is missing
|
||||
self.assertEqual(
|
||||
self.session.execute(sa.text("select count(*) from person")).scalar(),
|
||||
0,
|
||||
)
|
||||
self.assertRaises(
|
||||
sa.exc.OperationalError,
|
||||
self.session.execute,
|
||||
sa.text("select count(*) from alembic_version"),
|
||||
)
|
||||
|
||||
# but we can 'stamp' the DB to declare its current revision
|
||||
alembic_command.stamp(alembic, "heads")
|
||||
|
||||
# now the alembic_version table exists
|
||||
self.assertEqual(
|
||||
self.session.execute(
|
||||
sa.text("select count(*) from alembic_version")
|
||||
).scalar(),
|
||||
1,
|
||||
)
|
||||
|
||||
# and now Alembic knows we are current
|
||||
self.assertTrue(mod.check_alembic_current(self.config, alembic))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue