Skip to content

Commit 18c6fdc

Browse files
Merge pull request #32 from RudolfCardinal/improve_alembic_automigration
Allow db_url parameter to create_database_migration_numbered_style
2 parents 0768fe3 + 92c160a commit 18c6fdc

File tree

3 files changed

+83
-88
lines changed

3 files changed

+83
-88
lines changed

cardinal_pythonlib/sqlalchemy/alembic_func.py

Lines changed: 78 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@
2626
2727
"""
2828

29+
import logging
2930
import os
3031
import re
31-
import subprocess
3232
from typing import Tuple
3333

34-
from alembic.config import Config
35-
from alembic.util.exc import CommandError
34+
from alembic.command import revision as mk_revision
35+
from alembic.config import CommandLine, Config as AlembicConfig
3636
from alembic.runtime.migration import MigrationContext
3737
from alembic.runtime.environment import EnvironmentContext
3838
from alembic.script import ScriptDirectory
39+
from alembic.util.exc import CommandError
3940
from sqlalchemy.engine import create_engine
4041

4142
from cardinal_pythonlib.fileops import preserve_cwd
42-
from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
4343

44-
log = get_brace_style_log_with_null_handler(__name__)
44+
log = logging.getLogger(__name__)
4545

4646

4747
# =============================================================================
@@ -80,7 +80,7 @@ def get_head_revision_from_alembic(
8080
8181
Arguments:
8282
alembic_config_filename:
83-
config filename
83+
config filename (usually a full path to an alembic.ini file)
8484
alembic_base_dir:
8585
directory to start in, so relative paths in the config file work.
8686
version_table:
@@ -89,9 +89,9 @@ def get_head_revision_from_alembic(
8989
if alembic_base_dir is None:
9090
alembic_base_dir = os.path.dirname(alembic_config_filename)
9191
os.chdir(alembic_base_dir) # so the directory in the config file works
92-
config = Config(alembic_config_filename)
93-
script = ScriptDirectory.from_config(config)
94-
with EnvironmentContext(config, script, version_table=version_table):
92+
alembic_cfg = AlembicConfig(alembic_config_filename)
93+
script = ScriptDirectory.from_config(alembic_cfg)
94+
with EnvironmentContext(alembic_cfg, script, version_table=version_table):
9595
return script.get_current_head()
9696

9797

@@ -123,25 +123,28 @@ def get_current_and_head_revision(
123123
:func:`get_current_revision` and :func:`get_head_revision_from_alembic`.
124124
125125
Arguments:
126-
database_url: SQLAlchemy URL for the database
127-
alembic_config_filename: config filename
128-
alembic_base_dir: directory to start in, so relative paths in the
129-
config file work.
130-
version_table: table name for Alembic versions
126+
database_url:
127+
SQLAlchemy URL for the database
128+
alembic_config_filename:
129+
config filename (usually a full path to an alembic.ini file)
130+
alembic_base_dir:
131+
directory to start in, so relative paths in the config file work.
132+
version_table:
133+
table name for Alembic versions
131134
"""
132135
# Where we are
133136
head_revision = get_head_revision_from_alembic(
134137
alembic_config_filename=alembic_config_filename,
135138
alembic_base_dir=alembic_base_dir,
136139
version_table=version_table,
137140
)
138-
log.debug("Intended database version: {}", head_revision)
141+
log.debug(f"Intended database version: {head_revision}")
139142

140143
# Where we want to be
141144
current_revision = get_current_revision(
142145
database_url=database_url, version_table=version_table
143146
)
144-
log.debug("Current database version: {}", current_revision)
147+
log.debug(f"Current database version: {current_revision}")
145148

146149
# Are we where we want to be?
147150
return current_revision, head_revision
@@ -165,49 +168,43 @@ def upgrade_database(
165168
166169
Arguments:
167170
alembic_config_filename:
168-
config filename
169-
171+
config filename (usually a full path to an alembic.ini file)
170172
db_url:
171173
Optional database URL to use, by way of override.
172-
173174
alembic_base_dir:
174175
directory to start in, so relative paths in the config file work
175-
176176
starting_revision:
177177
revision to start at (typically ``None`` to ask the database)
178-
179178
destination_revision:
180179
revision to aim for (typically ``"head"`` to migrate to the latest
181180
structure)
182-
183-
version_table: table name for Alembic versions
184-
181+
version_table:
182+
table name for Alembic versions
185183
as_sql:
186184
run in "offline" mode: print the migration SQL, rather than
187185
modifying the database. See
188186
https://alembic.zzzcomputing.com/en/latest/offline.html
189-
190187
"""
191188

192189
if alembic_base_dir is None:
193190
alembic_base_dir = os.path.dirname(alembic_config_filename)
194191
os.chdir(alembic_base_dir) # so the directory in the config file works
195-
config = Config(alembic_config_filename)
192+
alembic_cfg = AlembicConfig(alembic_config_filename)
196193
if db_url:
197-
config.set_main_option("sqlalchemy.url", db_url)
198-
script = ScriptDirectory.from_config(config)
194+
alembic_cfg.set_main_option("sqlalchemy.url", db_url)
195+
script = ScriptDirectory.from_config(alembic_cfg)
199196

200197
# noinspection PyUnusedLocal,PyProtectedMember
201198
def upgrade(rev, context):
202199
return script._upgrade_revs(destination_revision, rev)
203200

204201
log.info(
205-
"Upgrading database to revision {!r} using Alembic",
206-
destination_revision,
202+
f"Upgrading database to revision {destination_revision!r} "
203+
f"using Alembic"
207204
)
208205

209206
with EnvironmentContext(
210-
config,
207+
alembic_cfg,
211208
script,
212209
fn=upgrade,
213210
as_sql=as_sql,
@@ -240,48 +237,42 @@ def downgrade_database(
240237
241238
Arguments:
242239
alembic_config_filename:
243-
config filename
244-
240+
config filename (usually a full path to an alembic.ini file)
245241
db_url:
246242
Optional database URL to use, by way of override.
247-
248243
alembic_base_dir:
249244
directory to start in, so relative paths in the config file work
250-
251245
starting_revision:
252246
revision to start at (typically ``None`` to ask the database)
253-
254247
destination_revision:
255248
revision to aim for
256-
257-
version_table: table name for Alembic versions
258-
249+
version_table:
250+
table name for Alembic versions
259251
as_sql:
260252
run in "offline" mode: print the migration SQL, rather than
261253
modifying the database. See
262254
https://alembic.zzzcomputing.com/en/latest/offline.html
263-
264255
"""
265256

266257
if alembic_base_dir is None:
267258
alembic_base_dir = os.path.dirname(alembic_config_filename)
268259
os.chdir(alembic_base_dir) # so the directory in the config file works
269-
config = Config(alembic_config_filename)
260+
alembic_cfg = AlembicConfig(alembic_config_filename)
270261
if db_url:
271-
config.set_main_option("sqlalchemy.url", db_url)
272-
script = ScriptDirectory.from_config(config)
262+
alembic_cfg.set_main_option("sqlalchemy.url", db_url)
263+
script = ScriptDirectory.from_config(alembic_cfg)
273264

274265
# noinspection PyUnusedLocal,PyProtectedMember
275266
def downgrade(rev, context):
276267
return script._downgrade_revs(destination_revision, rev)
277268

278269
log.info(
279-
"Downgrading database to revision {!r} using Alembic",
280-
destination_revision,
270+
f"Downgrading database to revision {destination_revision!r} "
271+
f"using Alembic"
281272
)
282273

283274
with EnvironmentContext(
284-
config,
275+
alembic_cfg,
285276
script,
286277
fn=downgrade,
287278
as_sql=as_sql,
@@ -301,6 +292,7 @@ def create_database_migration_numbered_style(
301292
alembic_versions_dir: str,
302293
message: str,
303294
n_sequence_chars: int = 4,
295+
db_url: str = None,
304296
) -> None:
305297
"""
306298
Create a new Alembic migration script.
@@ -331,35 +323,43 @@ def create_database_migration_numbered_style(
331323
332324
See https://alembic.zzzcomputing.com/en/latest/autogenerate.html.
333325
334-
Regarding filenames: the default ``n_sequence_chars`` of 4 is like Django
335-
and gives files with names like
326+
Regarding filenames: the default ``n_sequence_chars`` of 4 is like Django
327+
and gives files with names like
336328
337-
.. code-block:: none
329+
.. code-block:: none
338330
339-
0001_x.py, 0002_y.py, ...
331+
0001_x.py, 0002_y.py, ...
340332
341-
NOTE THAT TO USE A NON-STANDARD ALEMBIC VERSION TABLE, YOU MUST SPECIFY
342-
THAT IN YOUR ``env.py`` (see e.g. CamCOPS).
333+
NOTE THAT TO USE A NON-STANDARD ALEMBIC VERSION TABLE, YOU MUST SPECIFY
334+
THAT IN YOUR ``env.py`` (see e.g. CamCOPS).
343335
344-
Args:
345-
alembic_ini_file: filename of Alembic ``alembic.ini`` file
346-
alembic_versions_dir: directory in which you keep your Python scripts,
347-
one per Alembic revision
348-
message: message to be associated with this revision
349-
n_sequence_chars: number of numerical sequence characters to use in the
350-
filename/revision (see above).
336+
Args:
337+
alembic_ini_file:
338+
filename (full path) of Alembic ``alembic.ini`` file
339+
alembic_versions_dir:
340+
directory in which you keep your Python scripts, one per Alembic
341+
revision
342+
message:
343+
message to be associated with this revision
344+
n_sequence_chars:
345+
number of numerical sequence characters to use in the
346+
filename/revision (see above).
347+
db_url:
348+
Optional database URL to use, by way of override. We achieve this
349+
via a temporary config file; not ideal.
351350
""" # noqa: E501
352-
file_regex = r"\d{" + str(n_sequence_chars) + r"}_\S*\.py$"
353351

352+
# Calculate current_seq_str, new_seq_str:
353+
file_regex = r"\d{" + str(n_sequence_chars) + r"}_\S*\.py$"
354354
_, _, existing_version_filenames = next(
355355
os.walk(alembic_versions_dir), (None, None, [])
356356
)
357357
existing_version_filenames = [
358358
x for x in existing_version_filenames if re.match(file_regex, x)
359359
]
360360
log.debug(
361-
"Existing Alembic version script filenames: {!r}",
362-
existing_version_filenames,
361+
f"Existing Alembic version script filenames: "
362+
f"{existing_version_filenames!r}"
363363
)
364364
current_seq_strs = [
365365
x[:n_sequence_chars] for x in existing_version_filenames
@@ -374,37 +374,29 @@ def create_database_migration_numbered_style(
374374
new_seq_str = str(new_seq_no).zfill(n_sequence_chars)
375375

376376
log.info(
377-
"""
378-
Generating new revision with Alembic...
379-
Last revision was: {}
380-
New revision will be: {}
381-
[If it fails with "Can't locate revision identified by...", you might need
382-
to DROP the Alembic version table (by default named 'alembic_version', but
383-
you may have elected to change that in your env.py.]
384-
""",
385-
current_seq_str,
386-
new_seq_str,
377+
f"Generating new revision with Alembic. "
378+
f"Last revision was: {current_seq_str}. "
379+
f"New revision will be: {new_seq_str}. "
380+
f"(If the process fails with \"Can't locate revision identified "
381+
f'by...", you might need to DROP the Alembic version table; by '
382+
f"default that is named {DEFAULT_ALEMBIC_VERSION_TABLE!r}, but you "
383+
f"may have elected to change that in your 'env.py' file.)"
387384
)
388385

389386
alembic_ini_dir = os.path.dirname(alembic_ini_file)
390387
os.chdir(alembic_ini_dir)
391-
cmdargs = [
392-
"alembic",
393-
"-c",
394-
alembic_ini_file,
395-
"revision",
396-
"--autogenerate",
397-
"-m",
398-
message,
399-
"--rev-id",
400-
new_seq_str,
401-
]
402-
log.info("From directory {!r}, calling: {!r}", alembic_ini_dir, cmdargs)
403-
subprocess.call(cmdargs)
388+
389+
# https://github.com/sqlalchemy/alembic/discussions/1089
390+
namespace = CommandLine().parser.parse_args(["revision", "--autogenerate"])
391+
config = AlembicConfig(alembic_ini_file, cmd_opts=namespace)
392+
if db_url:
393+
config.set_main_option("sqlalchemy.url", db_url)
394+
395+
mk_revision(config, message=message, autogenerate=True, rev_id=new_seq_str)
404396

405397

406398
def stamp_allowing_unusual_version_table(
407-
config: Config,
399+
config: AlembicConfig,
408400
revision: str,
409401
sql: bool = False,
410402
tag: str = None,

cardinal_pythonlib/version_string.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@
3131
3232
"""
3333

34-
VERSION_STRING = "2.0.2"
34+
VERSION_STRING = "2.0.3"
3535
# Use semantic versioning: https://semver.org/

docs/source/changelog.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,10 @@ Quick links:
873873

874874
- Improve ability of Alembic support code to take a database URL.
875875

876-
**2.0.3**
876+
**2.0.3 (2023-03-11)**
877877

878878
- Reinstate BIT and similar datatypes in the list of valid datatypes. Broken
879879
since v2.0.0.
880+
881+
- Allow ``db_url`` parameter to
882+
``cardinal_pythonlib.sqlalchemy.alembic_func.create_database_migration_numbered_style``.

0 commit comments

Comments
 (0)