是否可以将alembic连接字符串存储在alembic.ini之外?

时间:2014-03-04 17:06:23

标签: python sqlalchemy alembic

我正在使用Alembic和SQL Alchemy。使用SQL Alchemy,我倾向于遵循一种模式,即我不将连接字符串与版本化代码一起存储。相反,我的文件secret.py包含任何机密信息。我在.gitignore中抛出了这个文件名,所以它不会在GitHub上结束。

这种模式运行良好,但现在我开始使用Alembic进行迁移。看来我无法隐藏连接字符串。而是在alembic.ini中,将连接字符串设置为configuration parameter

# the 'revision' command, regardless of autogenerate
# revision_environment = false

sqlalchemy.url = driver://user:pass@localhost/dbname

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembi

我担心我会不小心为我的数据库提交一个包含用户名/密码信息的文件。我宁愿将这个连接字符串存储在一个地方,避免意外将其提交给版本控制的风险。

我有哪些选择?

12 个答案:

答案 0 :(得分:38)

我昨天遇到了同样的问题,并找到了以下解决方案。 我在alembic/env.py中执行以下操作:

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# this will overwrite the ini-file sqlalchemy.url path
# with the path given in the config of the main code
import config as ems_config
config.set_main_option('sqlalchemy.url', ems_config.config.get('sql', 'database'))

ems_config是一个保存配置数据的外部模块。

config.set_main_option(...)基本上会覆盖sqlalchemy.url文件的[alembic]部分中的alembic.ini密钥。在我的配置中,我只是将其留下黑色。

答案 1 :(得分:13)

Alembic documentation建议将@RiggsFolly与数据库网址(而不是modifying sqlalchemy.url in code)一起使用。

此外,您应修改run_migrations_offline以使用新URL。 Allan Simon在他的博客上有an example,但总的来说,将env.py修改为:

  1. 提供一个共享函数以某种方式获取URL(这里它来自命令行):

    create_engine
  2. 在离线模式下使用网址:

    def get_url():
        url = context.get_x_argument(as_dictionary=True).get('url')
        assert url, "Database URL must be specified on command line with -x url=<DB_URL>"
        return url
    
  3. 使用def run_migrations_offline(): ... url = get_url() context.configure( url=url, target_metadata=target_metadata, literal_binds=True) ... 代替create_engine在线模式使用网址:

    engine_from_config

答案 2 :(得分:8)

所以看起来有效的是在env.py中重新实现引擎创建,这显然是进行这种自定义的地方而不是在ini中使用sqlalchemy连接字符串:

engine = engine_from_config(
            config.get_section(config.config_ini_section),
            prefix='sqlalchemy.',
           poolclass=pool.NullPool)

您可以替换并指定自己的引擎配置:

import store
engine = store.engine

确实文档seems to imply这没关系:

  

sqlalchemy.url - 通过SQLAlchemy连接到数据库的URL。实际上,该密钥仅在env.py文件中引用,该文件特定于“通用”配置;可由开发人员自定义的文件。多数据库配置可以在此处响应多个键,或者可以引用文件的其他部分。

答案 3 :(得分:1)

为了避免提交用户/密码,我想出的最简单的方法是:a)将内插字符串添加到alembic.ini文件中,并b)在env.py中设置这些内插值

alembic.ini

sqlalchemy.url = postgresql://%(DB_USER)s:%(DB_PASS)s@35.197.196.146/nozzle-website

env.py

import os

from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

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

# here we allow ourselves to pass interpolation vars to alembic.ini
# fron the host env
section = config.config_ini_section
config.set_section_option(section, "DB_USER", os.environ.get("DB_USER"))
config.set_section_option(section, "DB_PASS", os.environ.get("DB_PASS"))

...

答案 4 :(得分:0)

另一个解决方案是创建模板alembic.ini.dist文件并使用您的版本化代码跟踪它,同时忽略VCS中的alembic.ini。

请勿在alembic.ini.dist中添加任何机密信息:

sqlalchemy.url = ...

将代码部署到平台时,将alembic.ini.dist复制到alembic.ini(您的VCS不会跟踪此文件)并使用平台的凭据修改alembic.ini。

答案 5 :(得分:0)

As Doug T. said您可以编辑env.py以提供除ini文件以外的其他位置的网址。您可以将额外的url参数传递给engine_from_config函数,而不是创建新引擎(kwargs稍后会合并到从ini文件中获取的选项)。在这种情况下你可以,例如将加密密码存储在ini文件中,并通过存储在ENV变量中的密码在运行时解密。

connectable = engine_from_config(                 
    config.get_section(config.config_ini_section),
    prefix='sqlalchemy.',                         
    poolclass=pool.NullPool,                      
    url=some_decrypted_endpoint)                                   

答案 6 :(得分:0)

我一直在寻找如何管理多个数据库的方法

这就是我所做的。我有两个数据库:日志 ohlc

根据doc, 我已经设置了像这样的淡啤酒

alembic init --template multidb

alembic.ini

databases = logs, ohlc
[logs]
sqlalchemy.url = postgresql://botcrypto:botcrypto@localhost/logs
[ohlc]
sqlalchemy.url = postgresql://botcrypto:botcrypto@localhost/ohlc

env.py

[...]
# 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')

# overwrite alembic.ini db urls from the config file
settings_path = os.environ.get('SETTINGS')
if settings_path:
    with open(settings_path) as fd:
        settings = conf.load(fd, context=os.environ) # loads the config.yml
    config.set_section_option("ohlc", "sqlalchemy.url", settings["databases"]["ohlc"])
    config.set_section_option("logs", "sqlalchemy.url", settings["databases"]["logs"])
else:
    logger.warning('Environment variable SETTINGS missing - use default alembic.ini configuration')
[...]

config.yml

databases:
    logs: postgresql://botcrypto:botcrypto@127.0.0.1:5432/logs
    ohlc: postgresql://botcrypto:botcrypto@127.0.0.1:5432/ohlc

用法

SETTINGS=config.yml alembic upgrade head

希望它可以帮助您!

答案 7 :(得分:0)

我在这里尝试了所有答案,但是没有用。然后,我尝试自己处理,如下所示:

.ini文件:

# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = alembic

# template used to generate migration files
file_template = %%(rev)s_%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d_%%(minute).2d_%%(second).2d

# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; this defaults
# to alembic/versions.  When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

databases = auth_engine


[auth_engine]
sqlalchemy.url = mysql+mysqldb://{}:{}@{}:{}/auth_db

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[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

[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

.env文件(位于我项目的根文件夹中):

DB_USER='root'
DB_PASS='12345678'
DB_HOST='127.0.0.1'
DB_PORT='3306'

env.py文件:

from __future__ import with_statement

import os
import re
import sys
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS")
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT")

# 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)

# gather section names referring to different
# databases.  These are named "engine1", "engine2"
# in the sample .ini file.
db_names = config.get_main_option('databases')

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata

sys.path.append(os.path.join(os.path.dirname(__file__), "../../../"))
from db_models.auth_db import auth_db_base

target_metadata = {
    'auth_engine': auth_db_base.auth_metadata
}


# 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 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.

    """
    engines = {}
    for name in re.split(r',\s*', db_names):
        engines[name] = rec = {}
        section = context.config.get_section(name)
        url = section['sqlalchemy.url'].format(DB_USER, DB_PASS, DB_HOST, DB_PORT)
        section['sqlalchemy.url'] = url
        rec['url'] = url
        # rec['url'] = context.config.get_section_option(name, "sqlalchemy.url")

    for name, rec in engines.items():
        print("Migrating database %s" % name)
        file_ = "%s.sql" % name
        print("Writing output to %s" % file_)
        with open(file_, 'w') as buffer:
            context.configure(url=rec['url'], output_buffer=buffer,
                              target_metadata=target_metadata.get(name),
                              compare_type=True,
                              compare_server_default=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.

    """
    engines = {}
    for name in re.split(r',\s*', db_names):
        engines[name] = rec = {}
        section = context.config.get_section(name)
        url = section['sqlalchemy.url'].format(DB_USER, DB_PASS, DB_HOST, DB_PORT)
        section['sqlalchemy.url'] = url
        rec['engine'] = engine_from_config(
            section,
            prefix='sqlalchemy.',
            poolclass=pool.NullPool)

    for name, rec in engines.items():
        engine = rec['engine']
        rec['connection'] = conn = engine.connect()
        rec['transaction'] = conn.begin()

    try:
        for name, rec in engines.items():
            print("Migrating database %s" % name)
            context.configure(
                connection=rec['connection'],
                upgrade_token="%s_upgrades" % name,
                downgrade_token="%s_downgrades" % name,
                target_metadata=target_metadata.get(name),
                compare_type=True,
                compare_server_default=True
            )
            context.run_migrations(engine_name=name)

        for rec in engines.values():
            rec['transaction'].commit()
    except:
        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()

希望可以帮助别人。

答案 8 :(得分:0)

由于我们正在从本地计算机运行迁移,因此我也遇到了这个问题。我的解决方案是将环境部分放在存储数据库配置(减去凭据)的alembic.ini中:

[local]
host = localhost
db = dbname

[test]
host = x.x.x.x
db = dbname

[prod]
host = x.x.x.x
db = dbname

然后我将以下内容放入env.py中,以便用户可以选择其环境并提示输入凭据:

from alembic import context
from getpass import getpass

...

envs = ['local', 'test', 'prod']

print('Warning: Do not commit your database credentials to source control!')
print(f'Available migration environments: {", ".join(envs)}')

env = input('Environment: ')

if env not in envs:
    print(f'{env} is not a valid environment')
    exit(0)

env_config = context.config.get_section(env)
host = env_config['host']
db = env_config['db']

username = input('Username: ')
password = getpass()
connection_string = f'postgresql://{username}:{password}@{host}/{db}'

context.config.set_main_option('sqlalchemy.url', connection_string)

您应该将凭据存储在整个团队都可以访问的密码管理器中,或者存储在可用的任何配置/秘密存储中。不过,通过这种方法,密码公开给您的本地剪贴板-一种更好的方法是让env.py直接连接到您的config / secret store API并直接拉出用户名/密码,但这增加了第三个政党依赖性。

答案 9 :(得分:0)

env.py:

from alembic.config import Config

alembic_cfg = Config()
alembic_cfg.set_main_option("sqlalchemy.url", getenv('PG_URI'))

https://alembic.sqlalchemy.org/en/latest/api/config.html

答案 10 :(得分:0)

MultiDB设置的情况下(SingleDB也一样),你可以使用config.set_section_option('section_name', 'variable_name', 'db_URL') 修改 alembic.ini 文件中数据库 URL 的值。

例如:

alembic.init

[engine1]
sqlalchemy.url = 

[engine2]
sqlalchemy.url = 

那么,

env.py

config = context.config

config.set_section_option('engine1', 'sqlalchemy.url', os.environ.get('URL_DB1'))
config.set_section_option('engine2', 'sqlalchemy.url', os.environ.get('URL_DB2'))

答案 11 :(得分:-1)

env.py中添加

config.set_main_option('sqlalchemy.url', os.environ['DB_URL'])

之后

config = context.config

喜欢

config = context.config
config.set_main_option('sqlalchemy.url', os.environ['DB_URL'])

然后像这样执行:

DB_URL="mysql://atuamae:de4@127.0.0.1/db" \
  alembic upgrade head