diff options
author | morpheus65535 <[email protected]> | 2023-07-26 19:34:49 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2023-07-26 19:34:49 -0400 |
commit | bccded275c3cb09dc001d66858f3200c78723935 (patch) | |
tree | 14f4409119ff7ca8af5f4827dca249bc0b25f768 /libs/flask_migrate | |
parent | 486d2f9481982fef0ff0a30c314f74e9268cc7fd (diff) | |
download | bazarr-bccded275c3cb09dc001d66858f3200c78723935.tar.gz bazarr-bccded275c3cb09dc001d66858f3200c78723935.zip |
Replaced peewee with sqlalchemy as ORM. This is a major change, please report related issues on Discord.v1.2.5-beta.3
Diffstat (limited to 'libs/flask_migrate')
18 files changed, 1487 insertions, 0 deletions
diff --git a/libs/flask_migrate/__init__.py b/libs/flask_migrate/__init__.py new file mode 100644 index 000000000..c61b7f5eb --- /dev/null +++ b/libs/flask_migrate/__init__.py @@ -0,0 +1,266 @@ +import argparse +from functools import wraps +import logging +import os +import sys +from flask import current_app +from alembic import __version__ as __alembic_version__ +from alembic.config import Config as AlembicConfig +from alembic import command +from alembic.util import CommandError + +alembic_version = tuple([int(v) for v in __alembic_version__.split('.')[0:3]]) +log = logging.getLogger(__name__) + + +class _MigrateConfig(object): + def __init__(self, migrate, db, **kwargs): + self.migrate = migrate + self.db = db + self.directory = migrate.directory + self.configure_args = kwargs + + @property + def metadata(self): + """ + Backwards compatibility, in old releases app.extensions['migrate'] + was set to db, and env.py accessed app.extensions['migrate'].metadata + """ + return self.db.metadata + + +class Config(AlembicConfig): + def __init__(self, *args, **kwargs): + self.template_directory = kwargs.pop('template_directory', None) + super().__init__(*args, **kwargs) + + def get_template_directory(self): + if self.template_directory: + return self.template_directory + package_dir = os.path.abspath(os.path.dirname(__file__)) + return os.path.join(package_dir, 'templates') + + +class Migrate(object): + def __init__(self, app=None, db=None, directory='migrations', command='db', + compare_type=True, render_as_batch=True, **kwargs): + self.configure_callbacks = [] + self.db = db + self.command = command + self.directory = str(directory) + self.alembic_ctx_kwargs = kwargs + self.alembic_ctx_kwargs['compare_type'] = compare_type + self.alembic_ctx_kwargs['render_as_batch'] = render_as_batch + if app is not None and db is not None: + self.init_app(app, db, directory) + + def init_app(self, app, db=None, directory=None, command=None, + compare_type=None, render_as_batch=None, **kwargs): + self.db = db or self.db + self.command = command or self.command + self.directory = str(directory or self.directory) + self.alembic_ctx_kwargs.update(kwargs) + if compare_type is not None: + self.alembic_ctx_kwargs['compare_type'] = compare_type + if render_as_batch is not None: + self.alembic_ctx_kwargs['render_as_batch'] = render_as_batch + if not hasattr(app, 'extensions'): + app.extensions = {} + app.extensions['migrate'] = _MigrateConfig( + self, self.db, **self.alembic_ctx_kwargs) + + from flask_migrate.cli import db as db_cli_group + app.cli.add_command(db_cli_group, name=self.command) + + def configure(self, f): + self.configure_callbacks.append(f) + return f + + def call_configure_callbacks(self, config): + for f in self.configure_callbacks: + config = f(config) + return config + + def get_config(self, directory=None, x_arg=None, opts=None): + if directory is None: + directory = self.directory + directory = str(directory) + config = Config(os.path.join(directory, 'alembic.ini')) + config.set_main_option('script_location', directory) + if config.cmd_opts is None: + config.cmd_opts = argparse.Namespace() + for opt in opts or []: + setattr(config.cmd_opts, opt, True) + if not hasattr(config.cmd_opts, 'x'): + if x_arg is not None: + setattr(config.cmd_opts, 'x', []) + if isinstance(x_arg, list) or isinstance(x_arg, tuple): + for x in x_arg: + config.cmd_opts.x.append(x) + else: + config.cmd_opts.x.append(x_arg) + else: + setattr(config.cmd_opts, 'x', None) + return self.call_configure_callbacks(config) + + +def catch_errors(f): + @wraps(f) + def wrapped(*args, **kwargs): + try: + f(*args, **kwargs) + except (CommandError, RuntimeError) as exc: + log.error('Error: ' + str(exc)) + sys.exit(1) + return wrapped + + +@catch_errors +def list_templates(): + """List available templates.""" + config = Config() + config.print_stdout("Available templates:\n") + for tempname in sorted(os.listdir(config.get_template_directory())): + with open( + os.path.join(config.get_template_directory(), tempname, "README") + ) as readme: + synopsis = next(readme).strip() + config.print_stdout("%s - %s", tempname, synopsis) + + +@catch_errors +def init(directory=None, multidb=False, template=None, package=False): + """Creates a new migration repository""" + if directory is None: + directory = current_app.extensions['migrate'].directory + template_directory = None + if template is not None and ('/' in template or '\\' in template): + template_directory, template = os.path.split(template) + config = Config(template_directory=template_directory) + config.set_main_option('script_location', directory) + config.config_file_name = os.path.join(directory, 'alembic.ini') + config = current_app.extensions['migrate'].\ + migrate.call_configure_callbacks(config) + if multidb and template is None: + template = 'flask-multidb' + elif template is None: + template = 'flask' + command.init(config, directory, template=template, package=package) + + +@catch_errors +def revision(directory=None, message=None, autogenerate=False, sql=False, + head='head', splice=False, branch_label=None, version_path=None, + rev_id=None): + """Create a new revision file.""" + opts = ['autogenerate'] if autogenerate else None + config = current_app.extensions['migrate'].migrate.get_config( + directory, opts=opts) + command.revision(config, message, autogenerate=autogenerate, sql=sql, + head=head, splice=splice, branch_label=branch_label, + version_path=version_path, rev_id=rev_id) + + +@catch_errors +def migrate(directory=None, message=None, sql=False, head='head', splice=False, + branch_label=None, version_path=None, rev_id=None, x_arg=None): + """Alias for 'revision --autogenerate'""" + config = current_app.extensions['migrate'].migrate.get_config( + directory, opts=['autogenerate'], x_arg=x_arg) + command.revision(config, message, autogenerate=True, sql=sql, + head=head, splice=splice, branch_label=branch_label, + version_path=version_path, rev_id=rev_id) + + +@catch_errors +def edit(directory=None, revision='current'): + """Edit current revision.""" + if alembic_version >= (0, 8, 0): + config = current_app.extensions['migrate'].migrate.get_config( + directory) + command.edit(config, revision) + else: + raise RuntimeError('Alembic 0.8.0 or greater is required') + + +@catch_errors +def merge(directory=None, revisions='', message=None, branch_label=None, + rev_id=None): + """Merge two revisions together. Creates a new migration file""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.merge(config, revisions, message=message, + branch_label=branch_label, rev_id=rev_id) + + +@catch_errors +def upgrade(directory=None, revision='head', sql=False, tag=None, x_arg=None): + """Upgrade to a later version""" + config = current_app.extensions['migrate'].migrate.get_config(directory, + x_arg=x_arg) + command.upgrade(config, revision, sql=sql, tag=tag) + + +@catch_errors +def downgrade(directory=None, revision='-1', sql=False, tag=None, x_arg=None): + """Revert to a previous version""" + config = current_app.extensions['migrate'].migrate.get_config(directory, + x_arg=x_arg) + if sql and revision == '-1': + revision = 'head:-1' + command.downgrade(config, revision, sql=sql, tag=tag) + + +@catch_errors +def show(directory=None, revision='head'): + """Show the revision denoted by the given symbol.""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.show(config, revision) + + +@catch_errors +def history(directory=None, rev_range=None, verbose=False, + indicate_current=False): + """List changeset scripts in chronological order.""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + if alembic_version >= (0, 9, 9): + command.history(config, rev_range, verbose=verbose, + indicate_current=indicate_current) + else: + command.history(config, rev_range, verbose=verbose) + + +@catch_errors +def heads(directory=None, verbose=False, resolve_dependencies=False): + """Show current available heads in the script directory""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.heads(config, verbose=verbose, + resolve_dependencies=resolve_dependencies) + + +@catch_errors +def branches(directory=None, verbose=False): + """Show current branch points""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.branches(config, verbose=verbose) + + +@catch_errors +def current(directory=None, verbose=False): + """Display the current revision for each database.""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.current(config, verbose=verbose) + + +@catch_errors +def stamp(directory=None, revision='head', sql=False, tag=None): + """'stamp' the revision table with the given revision; don't run any + migrations""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.stamp(config, revision, sql=sql, tag=tag) + + +@catch_errors +def check(directory=None): + """Check if there are any new operations to migrate""" + config = current_app.extensions['migrate'].migrate.get_config(directory) + command.check(config) diff --git a/libs/flask_migrate/cli.py b/libs/flask_migrate/cli.py new file mode 100644 index 000000000..176672c58 --- /dev/null +++ b/libs/flask_migrate/cli.py @@ -0,0 +1,251 @@ +import click +from flask.cli import with_appcontext +from flask_migrate import list_templates as _list_templates +from flask_migrate import init as _init +from flask_migrate import revision as _revision +from flask_migrate import migrate as _migrate +from flask_migrate import edit as _edit +from flask_migrate import merge as _merge +from flask_migrate import upgrade as _upgrade +from flask_migrate import downgrade as _downgrade +from flask_migrate import show as _show +from flask_migrate import history as _history +from flask_migrate import heads as _heads +from flask_migrate import branches as _branches +from flask_migrate import current as _current +from flask_migrate import stamp as _stamp +from flask_migrate import check as _check + + +def db(): + """Perform database migrations.""" + pass + + +@with_appcontext +def list_templates(): + """List available templates.""" + _list_templates() + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('--multidb', is_flag=True, + help=('Support multiple databases')) [email protected]('-t', '--template', default=None, + help=('Repository template to use (default is "flask")')) [email protected]('--package', is_flag=True, + help=('Write empty __init__.py files to the environment and ' + 'version locations')) +@with_appcontext +def init(directory, multidb, template, package): + """Creates a new migration repository.""" + _init(directory, multidb, template, package) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-m', '--message', default=None, help='Revision message') [email protected]('--autogenerate', is_flag=True, + help=('Populate revision script with candidate migration ' + 'operations, based on comparison of database to model')) [email protected]('--sql', is_flag=True, + help=('Don\'t emit SQL to database - dump to standard output ' + 'instead')) [email protected]('--head', default='head', + help=('Specify head revision or <branchname>@head to base new ' + 'revision on')) [email protected]('--splice', is_flag=True, + help=('Allow a non-head revision as the "head" to splice onto')) [email protected]('--branch-label', default=None, + help=('Specify a branch label to apply to the new revision')) [email protected]('--version-path', default=None, + help=('Specify specific path from config for version file')) [email protected]('--rev-id', default=None, + help=('Specify a hardcoded revision id instead of generating ' + 'one')) +@with_appcontext +def revision(directory, message, autogenerate, sql, head, splice, branch_label, + version_path, rev_id): + """Create a new revision file.""" + _revision(directory, message, autogenerate, sql, head, splice, + branch_label, version_path, rev_id) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-m', '--message', default=None, help='Revision message') [email protected]('--sql', is_flag=True, + help=('Don\'t emit SQL to database - dump to standard output ' + 'instead')) [email protected]('--head', default='head', + help=('Specify head revision or <branchname>@head to base new ' + 'revision on')) [email protected]('--splice', is_flag=True, + help=('Allow a non-head revision as the "head" to splice onto')) [email protected]('--branch-label', default=None, + help=('Specify a branch label to apply to the new revision')) [email protected]('--version-path', default=None, + help=('Specify specific path from config for version file')) [email protected]('--rev-id', default=None, + help=('Specify a hardcoded revision id instead of generating ' + 'one')) [email protected]('-x', '--x-arg', multiple=True, + help='Additional arguments consumed by custom env.py scripts') +@with_appcontext +def migrate(directory, message, sql, head, splice, branch_label, version_path, + rev_id, x_arg): + """Autogenerate a new revision file (Alias for + 'revision --autogenerate')""" + _migrate(directory, message, sql, head, splice, branch_label, version_path, + rev_id, x_arg) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('revision', default='head') +@with_appcontext +def edit(directory, revision): + """Edit a revision file""" + _edit(directory, revision) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-m', '--message', default=None, help='Merge revision message') [email protected]('--branch-label', default=None, + help=('Specify a branch label to apply to the new revision')) [email protected]('--rev-id', default=None, + help=('Specify a hardcoded revision id instead of generating ' + 'one')) [email protected]('revisions', nargs=-1) +@with_appcontext +def merge(directory, message, branch_label, rev_id, revisions): + """Merge two revisions together, creating a new revision file""" + _merge(directory, revisions, message, branch_label, rev_id) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('--sql', is_flag=True, + help=('Don\'t emit SQL to database - dump to standard output ' + 'instead')) [email protected]('--tag', default=None, + help=('Arbitrary "tag" name - can be used by custom env.py ' + 'scripts')) [email protected]('-x', '--x-arg', multiple=True, + help='Additional arguments consumed by custom env.py scripts') [email protected]('revision', default='head') +@with_appcontext +def upgrade(directory, sql, tag, x_arg, revision): + """Upgrade to a later version""" + _upgrade(directory, revision, sql, tag, x_arg) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('--sql', is_flag=True, + help=('Don\'t emit SQL to database - dump to standard output ' + 'instead')) [email protected]('--tag', default=None, + help=('Arbitrary "tag" name - can be used by custom env.py ' + 'scripts')) [email protected]('-x', '--x-arg', multiple=True, + help='Additional arguments consumed by custom env.py scripts') [email protected]('revision', default='-1') +@with_appcontext +def downgrade(directory, sql, tag, x_arg, revision): + """Revert to a previous version""" + _downgrade(directory, revision, sql, tag, x_arg) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('revision', default='head') +@with_appcontext +def show(directory, revision): + """Show the revision denoted by the given symbol.""" + _show(directory, revision) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-r', '--rev-range', default=None, + help='Specify a revision range; format is [start]:[end]') [email protected]('-v', '--verbose', is_flag=True, help='Use more verbose output') [email protected]('-i', '--indicate-current', is_flag=True, + help=('Indicate current version (Alembic 0.9.9 or greater is ' + 'required)')) +@with_appcontext +def history(directory, rev_range, verbose, indicate_current): + """List changeset scripts in chronological order.""" + _history(directory, rev_range, verbose, indicate_current) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-v', '--verbose', is_flag=True, help='Use more verbose output') [email protected]('--resolve-dependencies', is_flag=True, + help='Treat dependency versions as down revisions') +@with_appcontext +def heads(directory, verbose, resolve_dependencies): + """Show current available heads in the script directory""" + _heads(directory, verbose, resolve_dependencies) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-v', '--verbose', is_flag=True, help='Use more verbose output') +@with_appcontext +def branches(directory, verbose): + """Show current branch points""" + _branches(directory, verbose) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('-v', '--verbose', is_flag=True, help='Use more verbose output') +@with_appcontext +def current(directory, verbose): + """Display the current revision for each database.""" + _current(directory, verbose) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) [email protected]('--sql', is_flag=True, + help=('Don\'t emit SQL to database - dump to standard output ' + 'instead')) [email protected]('--tag', default=None, + help=('Arbitrary "tag" name - can be used by custom env.py ' + 'scripts')) [email protected]('revision', default='head') +@with_appcontext +def stamp(directory, sql, tag, revision): + """'stamp' the revision table with the given revision; don't run any + migrations""" + _stamp(directory, revision, sql, tag) + + [email protected]('-d', '--directory', default=None, + help=('Migration script directory (default is "migrations")')) +@with_appcontext +def check(directory): + """Check if there are any new operations to migrate""" + _check(directory) diff --git a/libs/flask_migrate/templates/aioflask-multidb/README b/libs/flask_migrate/templates/aioflask-multidb/README new file mode 100644 index 000000000..02cce84ee --- /dev/null +++ b/libs/flask_migrate/templates/aioflask-multidb/README @@ -0,0 +1 @@ +Multi-database configuration for aioflask. diff --git a/libs/flask_migrate/templates/aioflask-multidb/alembic.ini.mako b/libs/flask_migrate/templates/aioflask-multidb/alembic.ini.mako new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask-multidb/alembic.ini.mako @@ -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/libs/flask_migrate/templates/aioflask-multidb/env.py b/libs/flask_migrate/templates/aioflask-multidb/env.py new file mode 100644 index 000000000..299f544f5 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask-multidb/env.py @@ -0,0 +1,199 @@ +import asyncio +import logging +from logging.config import fileConfig + +from sqlalchemy import MetaData +from flask import current_app + +from alembic import context + +USE_TWOPHASE = False + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(bind_key=None): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine(bind=bind_key) + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engines.get(bind_key) + + +def get_engine_url(bind_key=None): + try: + return get_engine(bind_key).url.render_as_string( + hide_password=False).replace('%', '%%') + except AttributeError: + return str(get_engine(bind_key).url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +bind_names = [] +if current_app.config.get('SQLALCHEMY_BINDS') is not None: + bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) +else: + get_bind_names = getattr(current_app.extensions['migrate'].db, + 'bind_names', None) + if get_bind_names: + bind_names = get_bind_names() +for bind in bind_names: + context.config.set_section_option( + bind, "sqlalchemy.url", get_engine_url(bind_key=bind)) +target_db = current_app.extensions['migrate'].db + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(bind): + """Return the metadata for a bind.""" + if bind == '': + bind = None + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[bind] + + # legacy, less flexible implementation + m = MetaData() + for t in target_db.metadata.tables.values(): + if t.info.get('bind_key') == bind: + t.tometadata(m) + return m + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + # for the --sql use case, run migrations for each URL into + # individual files. + + engines = { + '': { + 'url': context.config.get_main_option('sqlalchemy.url') + } + } + for name in bind_names: + engines[name] = rec = {} + rec['url'] = context.config.get_section_option(name, "sqlalchemy.url") + + for name, rec in engines.items(): + logger.info("Migrating database %s" % (name or '<default>')) + file_ = "%s.sql" % name + logger.info("Writing output to %s" % file_) + with open(file_, 'w') as buffer: + context.configure( + url=rec['url'], + output_buffer=buffer, + target_metadata=get_metadata(name), + literal_binds=True, + ) + with context.begin_transaction(): + context.run_migrations(engine_name=name) + + +def do_run_migrations(_, engines): + # 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 len(script.upgrade_ops_list) >= len(bind_names) + 1: + empty = True + for upgrade_ops in script.upgrade_ops_list: + if not upgrade_ops.is_empty(): + empty = False + if empty: + directives[:] = [] + logger.info('No changes in schema detected.') + + for name, rec in engines.items(): + rec['sync_connection'] = conn = rec['connection']._sync_connection() + if USE_TWOPHASE: + rec['transaction'] = conn.begin_twophase() + else: + rec['transaction'] = conn.begin() + + try: + for name, rec in engines.items(): + logger.info("Migrating database %s" % (name or '<default>')) + context.configure( + connection=rec['sync_connection'], + upgrade_token="%s_upgrades" % name, + downgrade_token="%s_downgrades" % name, + target_metadata=get_metadata(name), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + context.run_migrations(engine_name=name) + + if USE_TWOPHASE: + for rec in engines.values(): + rec['transaction'].prepare() + + for rec in engines.values(): + rec['transaction'].commit() + except: # noqa: E722 + for rec in engines.values(): + rec['transaction'].rollback() + raise + finally: + for rec in engines.values(): + rec['sync_connection'].close() + + +async def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # for the direct-to-DB use case, start a transaction on all + # engines, then run all migrations, then commit all transactions. + engines = { + '': {'engine': get_engine()} + } + for name in bind_names: + engines[name] = rec = {} + rec['engine'] = get_engine(bind_key=name) + + for name, rec in engines.items(): + engine = rec['engine'] + rec['connection'] = await engine.connect().start() + + await engines['']['connection'].run_sync(do_run_migrations, engines) + + for rec in engines.values(): + await rec['connection'].close() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.get_event_loop().run_until_complete(run_migrations_online()) diff --git a/libs/flask_migrate/templates/aioflask-multidb/script.py.mako b/libs/flask_migrate/templates/aioflask-multidb/script.py.mako new file mode 100644 index 000000000..3beabc463 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask-multidb/script.py.mako @@ -0,0 +1,53 @@ +<%! +import re + +%>"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(engine_name): + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name): + globals()["downgrade_%s" % engine_name]() + +<% + from flask import current_app + bind_names = [] + if current_app.config.get('SQLALCHEMY_BINDS') is not None: + bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) + else: + get_bind_names = getattr(current_app.extensions['migrate'].db, 'bind_names', None) + if get_bind_names: + bind_names = get_bind_names() + db_names = [''] + bind_names +%> + +## generate an "upgrade_<xyz>() / downgrade_<xyz>()" function +## for each database name in the ini file. + +% for db_name in db_names: + +def upgrade_${db_name}(): + ${context.get("%s_upgrades" % db_name, "pass")} + + +def downgrade_${db_name}(): + ${context.get("%s_downgrades" % db_name, "pass")} + +% endfor diff --git a/libs/flask_migrate/templates/aioflask/README b/libs/flask_migrate/templates/aioflask/README new file mode 100644 index 000000000..6ed8020e0 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask/README @@ -0,0 +1 @@ +Single-database configuration for aioflask. diff --git a/libs/flask_migrate/templates/aioflask/alembic.ini.mako b/libs/flask_migrate/templates/aioflask/alembic.ini.mako new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask/alembic.ini.mako @@ -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/libs/flask_migrate/templates/aioflask/env.py b/libs/flask_migrate/templates/aioflask/env.py new file mode 100644 index 000000000..e33496bb0 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask/env.py @@ -0,0 +1,115 @@ +import asyncio +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +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 +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: +# my_important_option = config.get_main_option("my_important_option") +# ... 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. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection): + # 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.') + + context.configure( + connection=connection, + target_metadata=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = get_engine() + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.get_event_loop().run_until_complete(run_migrations_online()) diff --git a/libs/flask_migrate/templates/aioflask/script.py.mako b/libs/flask_migrate/templates/aioflask/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/libs/flask_migrate/templates/aioflask/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/libs/flask_migrate/templates/flask-multidb/README b/libs/flask_migrate/templates/flask-multidb/README new file mode 100644 index 000000000..eaae2511a --- /dev/null +++ b/libs/flask_migrate/templates/flask-multidb/README @@ -0,0 +1 @@ +Multi-database configuration for Flask. diff --git a/libs/flask_migrate/templates/flask-multidb/alembic.ini.mako b/libs/flask_migrate/templates/flask-multidb/alembic.ini.mako new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/libs/flask_migrate/templates/flask-multidb/alembic.ini.mako @@ -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/libs/flask_migrate/templates/flask-multidb/env.py b/libs/flask_migrate/templates/flask-multidb/env.py new file mode 100644 index 000000000..8f3101fec --- /dev/null +++ b/libs/flask_migrate/templates/flask-multidb/env.py @@ -0,0 +1,188 @@ +import logging +from logging.config import fileConfig + +from sqlalchemy import MetaData +from flask import current_app + +from alembic import context + +USE_TWOPHASE = False + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(bind_key=None): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine(bind=bind_key) + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engines.get(bind_key) + + +def get_engine_url(bind_key=None): + try: + return get_engine(bind_key).url.render_as_string( + hide_password=False).replace('%', '%%') + except AttributeError: + return str(get_engine(bind_key).url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +bind_names = [] +if current_app.config.get('SQLALCHEMY_BINDS') is not None: + bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) +else: + get_bind_names = getattr(current_app.extensions['migrate'].db, + 'bind_names', None) + if get_bind_names: + bind_names = get_bind_names() +for bind in bind_names: + context.config.set_section_option( + bind, "sqlalchemy.url", get_engine_url(bind_key=bind)) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(bind): + """Return the metadata for a bind.""" + if bind == '': + bind = None + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[bind] + + # legacy, less flexible implementation + m = MetaData() + for t in target_db.metadata.tables.values(): + if t.info.get('bind_key') == bind: + t.tometadata(m) + return m + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + # for the --sql use case, run migrations for each URL into + # individual files. + + engines = { + '': { + 'url': context.config.get_main_option('sqlalchemy.url') + } + } + for name in bind_names: + engines[name] = rec = {} + rec['url'] = context.config.get_section_option(name, "sqlalchemy.url") + + for name, rec in engines.items(): + logger.info("Migrating database %s" % (name or '<default>')) + file_ = "%s.sql" % name + logger.info("Writing output to %s" % file_) + with open(file_, 'w') as buffer: + context.configure( + url=rec['url'], + output_buffer=buffer, + target_metadata=get_metadata(name), + literal_binds=True, + ) + with context.begin_transaction(): + context.run_migrations(engine_name=name) + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # 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 len(script.upgrade_ops_list) >= len(bind_names) + 1: + empty = True + for upgrade_ops in script.upgrade_ops_list: + if not upgrade_ops.is_empty(): + empty = False + if empty: + directives[:] = [] + logger.info('No changes in schema detected.') + + # for the direct-to-DB use case, start a transaction on all + # engines, then run all migrations, then commit all transactions. + engines = { + '': {'engine': get_engine()} + } + for name in bind_names: + engines[name] = rec = {} + rec['engine'] = get_engine(bind_key=name) + + for name, rec in engines.items(): + engine = rec['engine'] + rec['connection'] = conn = engine.connect() + + if USE_TWOPHASE: + rec['transaction'] = conn.begin_twophase() + else: + rec['transaction'] = conn.begin() + + try: + for name, rec in engines.items(): + logger.info("Migrating database %s" % (name or '<default>')) + context.configure( + connection=rec['connection'], + upgrade_token="%s_upgrades" % name, + downgrade_token="%s_downgrades" % name, + target_metadata=get_metadata(name), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + context.run_migrations(engine_name=name) + + if USE_TWOPHASE: + for rec in engines.values(): + rec['transaction'].prepare() + + for rec in engines.values(): + rec['transaction'].commit() + except: # noqa: E722 + for rec in engines.values(): + rec['transaction'].rollback() + raise + finally: + for rec in engines.values(): + rec['connection'].close() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/libs/flask_migrate/templates/flask-multidb/script.py.mako b/libs/flask_migrate/templates/flask-multidb/script.py.mako new file mode 100644 index 000000000..3beabc463 --- /dev/null +++ b/libs/flask_migrate/templates/flask-multidb/script.py.mako @@ -0,0 +1,53 @@ +<%! +import re + +%>"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(engine_name): + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name): + globals()["downgrade_%s" % engine_name]() + +<% + from flask import current_app + bind_names = [] + if current_app.config.get('SQLALCHEMY_BINDS') is not None: + bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) + else: + get_bind_names = getattr(current_app.extensions['migrate'].db, 'bind_names', None) + if get_bind_names: + bind_names = get_bind_names() + db_names = [''] + bind_names +%> + +## generate an "upgrade_<xyz>() / downgrade_<xyz>()" function +## for each database name in the ini file. + +% for db_name in db_names: + +def upgrade_${db_name}(): + ${context.get("%s_upgrades" % db_name, "pass")} + + +def downgrade_${db_name}(): + ${context.get("%s_downgrades" % db_name, "pass")} + +% endfor diff --git a/libs/flask_migrate/templates/flask/README b/libs/flask_migrate/templates/flask/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/libs/flask_migrate/templates/flask/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/libs/flask_migrate/templates/flask/alembic.ini.mako b/libs/flask_migrate/templates/flask/alembic.ini.mako new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/libs/flask_migrate/templates/flask/alembic.ini.mako @@ -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/libs/flask_migrate/templates/flask/env.py b/libs/flask_migrate/templates/flask/env.py new file mode 100644 index 000000000..89f80b211 --- /dev/null +++ b/libs/flask_migrate/templates/flask/env.py @@ -0,0 +1,110 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +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 +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: +# my_important_option = config.get_main_option("my_important_option") +# ... 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. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # 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=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/libs/flask_migrate/templates/flask/script.py.mako b/libs/flask_migrate/templates/flask/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/libs/flask_migrate/templates/flask/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} |