diff --git a/README.md b/README.md index 81b1b0b67f3762753a5688ce4fabd4e0edefcc62..dd9a7dce6b1534fbd04577e8af437a232c3e6334 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,13 @@ survey-publisher-2022 ## Creating a db migration after editing the sqlalchemy models ```bash -cd compendium_v2 -alembic revision --autogenerate -m "description" +flask db migrate -m "description" ``` Then go to the created migration file to make any necessary additions, for example to migrate data. Also see https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect +Flask-migrate sets `compare_type=True` by default. Note that starting the application applies all upgrades. +This also happens when running `flask db` commands such as `flask db downgrade`, +so if you want to downgrade 2 or more versions you need to do so in one command, eg by specifying the revision number. diff --git a/compendium_v2/__init__.py b/compendium_v2/__init__.py index 7a69bf9eb602680352c2328d8f228a27ee008432..4717a4864e29c398106b79605661e0441d3ea009 100644 --- a/compendium_v2/__init__.py +++ b/compendium_v2/__init__.py @@ -6,15 +6,11 @@ import os from flask import Flask from flask_cors import CORS # for debugging +# the currently available stubs for flask_migrate are old (they depend on sqlalchemy 1.4 types) +from flask_migrate import Migrate, upgrade # type: ignore from compendium_v2 import config, environment from compendium_v2.db import db -from compendium_v2.migrations import migration_utils - - -def migrate_database(config: dict) -> None: - dsn = config['SQLALCHEMY_DATABASE_URI'] - migration_utils.upgrade(dsn) def _create_app(app_config) -> Flask: @@ -56,11 +52,14 @@ def create_app() -> Flask: app = _create_app_with_db(app_config) + Migrate(app, db, directory=os.path.join(os.path.dirname(__file__), 'migrations')) + logging.info('Flask app initialized') environment.setup_logging() # run migrations on startup - migrate_database(app_config) + with app.app_context(): + upgrade() return app diff --git a/compendium_v2/alembic.ini b/compendium_v2/alembic.ini deleted file mode 100644 index 2145863baa95551a588dd229ee525abd06f32742..0000000000000000000000000000000000000000 --- a/compendium_v2/alembic.ini +++ /dev/null @@ -1,10 +0,0 @@ -# A generic, single database configuration. - -# only needed for generating new revision scripts -[alembic] -# make sure the right line is un / commented depending on which schema you want -# a migration for -script_location = migrations -# script_location = cachedb_migrations -# change this to run migrations from the command line -sqlalchemy.url = postgresql://compendium:compendium321@localhost:65000/compendium diff --git a/compendium_v2/migrations/README b/compendium_v2/migrations/README new file mode 100644 index 0000000000000000000000000000000000000000..0e048441597444a7e2850d6d7c4ce15550f79bda --- /dev/null +++ b/compendium_v2/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/compendium_v2/migrations/__init__.py b/compendium_v2/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/compendium_v2/migrations/alembic.ini b/compendium_v2/migrations/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..ec9d45c26a6bb54e833fd4e6ce2de29343894f4b --- /dev/null +++ b/compendium_v2/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/compendium_v2/migrations/env.py b/compendium_v2/migrations/env.py index 5ea9c8d33d0c06ae30cc8e05740a8f3758d18357..e2408681ba289bd300144dddf1c47f754ba931d1 100644 --- a/compendium_v2/migrations/env.py +++ b/compendium_v2/migrations/env.py @@ -1,10 +1,9 @@ import logging +from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool +from flask import current_app from alembic import context -from compendium_v2.db import metadata_obj # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -12,13 +11,34 @@ config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. -logging.basicConfig(level=logging.INFO) +if config.config_file_name is not None: + fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = metadata_obj +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db # other values from the config, defined by the needs of env.py, # can be acquired: @@ -26,6 +46,12 @@ target_metadata = metadata_obj # ... etc. +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -40,10 +66,7 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, + url=url, target_metadata=get_metadata(), literal_binds=True ) with context.begin_transaction(): @@ -57,15 +80,25 @@ def run_migrations_online(): and associate a connection with the context. """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = get_engine() with connectable.connect() as connection: context.configure( - connection=connection, target_metadata=target_metadata + connection=connection, + target_metadata=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args ) with context.begin_transaction(): diff --git a/compendium_v2/migrations/migration_utils.py b/compendium_v2/migrations/migration_utils.py deleted file mode 100644 index f25f03c922761f6c036125d2b06857729aeee828..0000000000000000000000000000000000000000 --- a/compendium_v2/migrations/migration_utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import os - -from alembic.config import Config -from alembic import command - -logger = logging.getLogger(__name__) -DEFAULT_MIGRATIONS_DIRECTORY = os.path.dirname(__file__) - - -def upgrade(dsn, migrations_directory=DEFAULT_MIGRATIONS_DIRECTORY): - """ - migrate db to head version - - cf. https://stackoverflow.com/a/43530495, - https://stackoverflow.com/a/54402853 - - :param dsn: dsn string, passed to alembic - :param migrations_directory: full path to migrations directory - (default is this directory) - :return: - """ - alembic_config = Config() - alembic_config.set_main_option('script_location', migrations_directory) - alembic_config.set_main_option('sqlalchemy.url', dsn) - command.upgrade(alembic_config, 'head') - - -def postgresql_dsn(db_username, db_password, db_hostname, db_name, port=5432): - return (f'postgresql://{db_username}:{db_password}' - f'@{db_hostname}:{port}/{db_name}') - - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - upgrade(postgresql_dsn( - db_username='compendium', - db_password='compendium321', - db_hostname='localhost', - db_name='compendium', - port=65000)) diff --git a/requirements.txt b/requirements.txt index 98804e02708e010e91e5add5daafa792bc4e0428..f49a79607d14fd2eed998d4429390cc95acb18b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ click~=8.1 jsonschema~=4.17 flask~=2.2 flask-cors~=3.0 +flask-migrate~=4.0 flask-sqlalchemy~=3.0 openpyxl~=3.1 psycopg2-binary~=2.9 diff --git a/setup.py b/setup.py index 48a37f1b4ad2ecc95203d262b259d951529a265e..52acb8cb8ef2714e7f7b93352b260b4ce5b35974 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ setup( 'jsonschema~=4.17', 'flask~=2.2', 'flask-cors~=3.0', + 'flask-migrate~=4.0', 'flask-sqlalchemy~=3.0', 'openpyxl~=3.1', 'psycopg2-binary~=2.9',