Search code examples
pythonalembic

Distribute Alembic migration scripts in application package


I have an application that uses SQLAlchemy and Alembic for migrations.

The repository looks like this:

my-app/
    my_app/
        ... # Source code
    migrations/
        versions/
            ...  # Migration scripts
        env.py
    alembic.ini
    MANIFEST.in
    README.rst
    setup.py

When in the repo, I can call alembic commands (alembic revision, alembic upgrade).

I want to ship the app as a package to allow users to pip install, and I would like them to be able to just alembic upgrade head to migrate their DB.

How can I achieve this?

alembic is listed as a dependency. What I don't know is how to ensure alembic.ini and revision files are accessible to the alembic command without the user having to pull the repo.

Adding them to MANIFEST.in will add them to the source package but AFAIU, when installing with pip, only my_app and subfolders end up in the (virtual) environment (this plus entry points).

Note: the notions of source dist, wheel, MANIFEST.in and include_package_data are still a bit blurry to me but hopefully the description above makes the use case clear.


Solution

  • The obvious part of the answer is "include migration files in app directory".

    my-app/
        my_app/
            ... # Source code
            migrations/
                versions/
                    ...  # Migration scripts
                env.py
            alembic.ini
        MANIFEST.in
        README.rst
        setup.py
    

    The not so obvious part is that when users install the package, they are not in the app directory, so they would need to specify the location of the alembic.ini file as a command line argument to use alembic commands (and this path is somewhere deep into a virtualenv). Not so nice.

    From a discussion with Alembic author, the recommended way is to provide user commands using the Alembic Python API internally to expose only a subset of user commands.

    Here's what I did.

    In migrations directory, I added this __init__.py file:

    """DB migrations"""
    from pathlib import Path
    
    from alembic.config import Config
    from alembic import command
    
    
    ROOT_PATH = Path(__file__).parent.parent
    ALEMBIC_CFG = Config(ROOT_PATH / "alembic.ini")
    
    
    def current(verbose=False):
        command.current(ALEMBIC_CFG, verbose=verbose)
    
    
    def upgrade(revision="head"):
        command.upgrade(ALEMBIC_CFG, revision)
    
    
    def downgrade(revision):
        command.downgrade(ALEMBIC_CFG, revision)
    

    Then in a commands.py file in application root, I added a few commands:

    @click.command()
    @click.option("-v", "--verbose", is_flag=True, default=False, help="Verbose mode")
    def db_current_cmd(verbose):
        """Display current database revision"""
        migrations.current(verbose)
    
    
    @click.command()
    @click.option("-r", "--revision", default="head", help="Revision target")
    def db_upgrade_cmd(revision):
        """Upgrade to a later database revision"""
        migrations.upgrade(revision)
    
    
    @click.command()
    @click.option("-r", "--revision", required=True, help="Revision target")
    def db_downgrade_cmd(revision):
        """Revert to a previous database revision"""
        migrations.downgrade(revision)
    

    And of course, in setup.py

    setup(
        ...
        entry_points={
            "console_scripts": [
                ...
                "db_current = my_app.commands:db_current_cmd",
                "db_upgrade = my_app.commands:db_upgrade_cmd",
                "db_downgrade = my_app.commands:db_downgrade_cmd",
            ],
        },
    )