Usando Alembic API desde dentro del código de la aplicación

Estoy utilizando SQLite como un formato de archivo de aplicación (vea aquí por qué querría hacer esto) para mi aplicación de escritorio basada en PySide. Es decir, cuando un usuario utiliza mi aplicación, sus datos se guardan en un solo archivo de base de datos en su máquina. Estoy utilizando el SQLAlchemy ORM para comunicarme con las bases de datos.

Cuando publico nuevas versiones de la aplicación, puedo modificar el esquema de la base de datos. No quiero que los usuarios tengan que desechar sus datos cada vez que cambio el esquema, por lo que debo migrar sus bases de datos al formato más nuevo. Además, creo mucho bases de datos temporales para guardar subconjuntos de datos para usar con algunos procesos externos. Quiero crear estas bases de datos con alambique para que estén etiquetadas con la versión adecuada.

Tengo algunas preguntas:

Tengo un excelente trabajo para la única base de datos que utilizo para desarrollar en el directorio de mi proyecto. Quiero usar alembic para migrar y crear bases de datos en ubicaciones arbitrarias, preferiblemente a través de algún tipo de API de Python, y no a través de la línea de comandos. Esta aplicación también se congela con cx_Freeze, en caso de que eso haga una diferencia.

¡Gracias!

Esto es lo que he aprendido después de conectar mi software a alembic :

¿Hay alguna manera de llamar a alambique desde dentro de mi código Python?

Sí. A partir de este momento, el punto de entrada principal para alembic.config.main es alembic.config.main , por lo que puede importarlo y llamarlo usted mismo, por ejemplo:

 import alembic.config alembicArgs = [ '--raiseerr', 'upgrade', 'head', ] alembic.config.main(argv=alembicArgs) 

Tenga en cuenta que alembic busca migraciones en el directorio actual (es decir, os.getcwd ()). He manejado esto usando os.chdir(migration_directory) antes de llamar a alambique, pero puede haber una mejor solución.


¿Puedo especificar una nueva ubicación de base de datos desde la línea de comandos sin editar el archivo .ini?

Sí. La clave está en el argumento de línea de comando -x . De alembic -h (sorprendentemente, no pude encontrar una referencia de argumento de línea de comandos en los documentos):

 optional arguments: -x X Additional arguments consumed by custom env.py scripts, eg -x setting1=somesetting -x setting2=somesetting 

Así que puedes crear tu propio parámetro, por ejemplo, dbPath , y luego interceptarlo en env.py :

alembic -x dbPath=/path/to/sqlite.db upgrade head

entonces por ejemplo en env.py :

 def run_migrations_online(): # get the alembic section of the config file ini_section = config.get_section(config.config_ini_section) # if a database path was provided, override the one in alembic.ini db_path = context.get_x_argument(as_dictionary=True).get('dbPath') if db_path: ini_section['sqlalchemy.url'] = db_path # establish a connectable object as normal connectable = engine_from_config( ini_section, prefix='sqlalchemy.', poolclass=pool.NullPool) # etc 

Por supuesto, también puede suministrar el parámetro -x usando argv en alembic.config.main .

Estoy de acuerdo con @davidism sobre el uso de migraciones frente a metadata.create_all() 🙂

Esta es una pregunta muy amplia, y la implementación de su idea dependerá de usted, pero es posible.

Puede llamar a Alembic desde su código de Python sin usar los comandos, ya que también está implementado en Python. Solo necesitas recrear lo que los comandos están haciendo detrás de escena.

Es cierto que los documentos no están en muy buena forma ya que son versiones relativamente tempranas de la biblioteca, pero con un poco de investigación encontrará lo siguiente:

  1. Crear una configuración
  2. Utilice la configuración para crear un ScriptDirectory
  3. Utilice Config y ScriptDirectory para crear un EnvironmentContext
  4. Utilice el EnvironmentContext para crear un MigrationContext
  5. La mayoría de los comandos usan una combinación de métodos de Config y MigrationContext

He escrito una extensión para proporcionar este acceso programático a una base de datos Flask-SQLAlchemy. La implementación está vinculada a Flask y Flask-SQLAlchemy, pero debería ser un buen lugar para comenzar. Ver Frasco-Alambique aquí.

Con respecto a su último punto sobre cómo crear nuevas bases de datos, puede usar Alembic para crear las tablas, o puede usar metadata.create_all() luego un alembic stamp head (o un código Python equivalente). Recomiendo usar siempre la ruta de migración para crear las tablas e ignorar los metadata.create_all() procesar.

No tengo ninguna experiencia con cx_freeze, pero debería estar bien siempre y cuando las migraciones estén incluidas en la distribución y la ruta a ese directorio en el código sea correcta.

Aquí hay un ejemplo puramente programático de cómo configurar y llamar comandos alambicos mediante progtwigción.

La configuración del directorio (para facilitar la lectura del código)

 . # root dir |- alembic/ # directory with migrations |- tests/diy_alembic.py # example script |- alembic.ini # ini file 

Y aquí está diy_alembic.py

 import os import argparse from alembic.config import Config from alembic import command import inspect def alembic_set_stamp_head(user_parameter): # set the paths values this_file_directory = os.path.dirname(os.path.abspath(inspect.stack()[0][1])) root_directory = os.path.join(this_file_directory, '..') alembic_directory = os.path.join(root_directory, 'alembic') ini_path = os.path.join(root_directory, 'alembic.ini') # create Alembic config and feed it with paths config = Config(ini_path) config.set_main_option('script_location', alembic_directory) config.cmd_opts = argparse.Namespace() # arguments stub # If it is required to pass -x parameters to alembic x_arg = 'user_parameter=' + user_parameter 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) #prepare and run the command revision = 'head' sql = False tag = None command.stamp(config, revision, sql=sql, tag=tag) #upgrade command command.upgrade(config, revision, sql=sql, tag=tag) 

El código es más o menos un corte de este archivo Flask-Alembic . Es un buen lugar para ver el uso y los detalles de otros comandos.

¿Por qué esta solución? – Se escribió en la necesidad de crear un sello, actualizaciones y degradaciones cuando se ejecutan pruebas automatizadas.

  • os.chdir (migration_directory) interfirió con algunas pruebas.
  • Queríamos tener UNA fuente de creación y manipulación de bases de datos. “Si creamos y administramos bases de datos con un cofre de alambique, alambique pero no metadata.create_all (), también se utilizarán para las pruebas”.
  • Incluso si el código anterior es más largo que 4 líneas, el alambique se mostró como una buena bestia controlable si se maneja de esta manera.

Para cualquier otra persona que intente lograr un resultado al estilo de una ruta de vuelo con SQLAlchemy, esto funcionó para mí:

Agrega migration.py a tu proyecto:

 from flask_alembic import Alembic def migrate(app): alembic = Alembic() alembic.init_app(app) with app.app_context(): alembic.upgrade() 

Llámelo al inicio de la aplicación después de que su db se haya inicializado

 application = Flask(__name__) db = SQLAlchemy() db.init_app(application) migration.migrate(application) 

Entonces solo necesitas hacer el rest de los pasos de alambique estándar:

Inicializa tu proyecto como alambique.

 alembic init alembic 

Actualizar env.py:

 from models import MyModel target_metadata = [MyModel.Base.metadata] 

Actualizar alembic.ini

 sqlalchemy.url = postgresql://postgres:postgres@localhost:5432/my_db 

Suponiendo que sus modelos SQLAlchemy ya están definidos, puede autogenerar sus scripts ahora:

 alembic revision --autogenerate -m "descriptive migration message" 

Si recibe un error por no poder importar su modelo en env.py, puede ejecutar lo siguiente en su terminal para corregirlo

 export PYTHONPATH=/path/to/your/project 

Por último, mis scripts de migración se estaban generando en el directorio alembic / Versiones, y tuve que copiarlos en el directorio de migraciones para que Alamembic los recogiera.

 ├── alembic │  ├── env.py │  ├── README │  ├── script.py.mako │  └── versions │  ├── a5402f383da8_01_init.py # generated here... │  └── __pycache__ ├── alembic.ini ├── migrations │  ├── a5402f383da8_01_init.py # manually copied here │  └── script.py.mako 

Probablemente tengo algo mal configurado, pero está funcionando ahora.

Si observa la página de la API de comandos de los documentos de alambique, verá un ejemplo de cómo ejecutar los comandos de la CLI directamente desde una aplicación de Python. Sin pasar por el código CLI.

La ejecución de alembic.config.main tiene el inconveniente de que se env.py script env.py que puede no ser lo que usted desea. Por ejemplo, modificará su configuración de registro.

Otra forma muy simple es usar el “API de comando” vinculado anteriormente. Por ejemplo, aquí hay una pequeña función auxiliar que terminé escribiendo:

 from alembic.config import Config from alembic import command def run_migrations(script_location: str, dsn: str) -> None: LOG.info('Running DB migrations in %r on %r', script_location, dsn) alembic_cfg = Config() alembic_cfg.set_main_option('script_location', script_location) alembic_cfg.set_main_option('sqlalchemy.url', dsn) command.upgrade(alembic_cfg, 'head') 

Estoy usando el método set_main_option aquí para poder ejecutar las migraciones en una base de datos diferente si es necesario. Así que simplemente puedo llamar a esto de la siguiente manera:

 run_migrations('/path/to/migrations', 'postgresql:///my_database') 

El lugar de donde obtengas esos dos valores (ruta y DSN) depende de ti. Pero esto parece estar muy cerca de lo que quieres lograr. Los comandos API también tienen los métodos stamp () que le permiten marcar un DB determinado para que sea de una versión específica. El ejemplo anterior se puede adaptar fácilmente para llamar a esto.

No uso Flask, por lo que no pude utilizar la biblioteca Flask-Alembic que ya estaba recomendada. En cambio, después de un poco de retoques, codifiqué la siguiente función corta para ejecutar todas las migraciones aplicables. Guardo todos mis archivos relacionados con un miembro de un miembro bajo un submódulo (carpeta) llamado migraciones. De hecho, mantengo el alembic.ini junto con el env.py , que es quizás un poco poco ortodoxo. Aquí hay un fragmento de mi archivo alembic.ini para ajustarlo:

 [alembic] script_location = . 

Luego agregué el siguiente archivo en el mismo directorio y lo run.py Pero dondequiera que guarde sus scripts, todo lo que debe hacer es modificar el código a continuación para que apunte a las rutas correctas:

 from alembic.command import upgrade from alembic.config import Config import os def run_sql_migrations(): # retrieves the directory that *this* file is in migrations_dir = os.path.dirname(os.path.realpath(__file__)) # this assumes the alembic.ini is also contained in this same directory config_file = os.path.join(migrations_dir, "alembic.ini") config = Config(file_=config_file) config.set_main_option("script_location", migrations_dir) # upgrade the database to the latest revision upgrade(config, "head") 

Luego, con ese archivo run.py en su lugar, me permite hacer esto en mi código principal:

 from mymodule.migrations.run import run_sql_migrations run_sql_migrations() 

Consulte la documentación de alembic.operations.base.Operations:

  from alembic.migration import MigrationContext from alembic.operations import Operations conn = myengine.connect() ctx = MigrationContext.configure(conn) op = Operations(ctx) op.alter_column("t", "c", nullable=True)