Skip to content

Commit ec5a458

Browse files
committed
Use model to insert all fixtures in the same transaction. Initial async fixtures implementation.
1 parent 630988d commit ec5a458

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

.idea/dataSources.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/alembic/env.py

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import hashlib
2+
import importlib
13
import logging
4+
import sys
25
from asyncio import get_event_loop
3-
from datetime import datetime
6+
from datetime import datetime, timezone
7+
from os import listdir, path
8+
from os.path import isfile, join
49

5-
from sqlalchemy import Table, Column, String, DateTime
6-
from sqlalchemy.ext.asyncio import AsyncEngine
7-
from sqlalchemy.orm import registry
10+
from sqlalchemy import Table, Column, String, DateTime, UniqueConstraint, text, select
11+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
12+
from sqlalchemy.orm import registry, sessionmaker, Mapped, mapped_column
813

914
from alembic import context
1015
from common.bootstrap import application_init
@@ -35,17 +40,21 @@
3540
db_names = target_metadata.keys()
3641
config.set_main_option("databases", ",".join(db_names))
3742

38-
def inject_fixture_tables(registry_mapper: registry):
39-
Table(
40-
"alembic_fixtures",
41-
registry_mapper.metadata,
42-
Column("filename", String(), primary_key=True),
43-
Column("signature", String(), nullable=False),
44-
Column("processed_at", DateTime(timezone=True), nullable=False, default=datetime.now),
45-
)
43+
def generate_fixture_migration_model(declarative_base: type):
44+
class FixtureMigration(declarative_base):
45+
__tablename__ = "alembic_fixtures"
46+
47+
bind: Mapped[str] = mapped_column(String(), primary_key=True)
48+
filename: Mapped[str] = mapped_column(String(), primary_key=True)
49+
signature: Mapped[str] = mapped_column(String(), nullable=False)
50+
processed_at: Mapped[datetime] = mapped_column(DateTime(), nullable=False, default=datetime.now)
51+
return FixtureMigration
4652

53+
fixture_migration_models = {}
4754
for name in db_names:
48-
inject_fixture_tables(sa_manager.get_bind(name).registry_mapper)
55+
fixture_migration_models[name] = generate_fixture_migration_model(
56+
sa_manager.get_bind(name).declarative_base
57+
)
4958

5059

5160
# add your model's MetaData objects here
@@ -66,6 +75,51 @@ def inject_fixture_tables(registry_mapper: registry):
6675
# my_important_option = config.get_main_option("my_important_option")
6776
# ... etc.
6877

78+
def calculate_md5(file_path):
79+
hasher = hashlib.md5()
80+
with open(file_path, 'rb') as file:
81+
hasher.update(file.read())
82+
return hasher.hexdigest()
83+
84+
async def a_migrate_fixtures(bind_name: str, Session: async_sessionmaker[AsyncSession]) -> str:
85+
alembic_path = path.dirname(path.realpath(__file__))
86+
fixtures_path = alembic_path + "/fixtures"
87+
sys.path.append(alembic_path)
88+
module_names = [
89+
f[:-3] for f in listdir(fixtures_path)
90+
if isfile(join(fixtures_path, f))
91+
and f.endswith(".py")
92+
and f != "__init__.py"
93+
]
94+
async with Session() as session:
95+
for module_name in module_names:
96+
logging.debug(f"Creating {module_name} fixtures for {bind_name}")
97+
m = importlib.import_module(f"fixtures.{module_name}")
98+
fixture_migration = await session.get(
99+
fixture_migration_models[bind_name],
100+
(bind_name, f"{module_name}.py")
101+
)
102+
signature = calculate_md5(f"{fixtures_path}/{module_name}.py")
103+
if fixture_migration:
104+
if signature != fixture_migration.signature:
105+
logging.warning(f"Signature mismatch for {fixture_migration.filename} fixture. The file has been modified after being processed.")
106+
logging.debug(f"{module_name} fixtures already migrated for {bind_name}")
107+
continue
108+
109+
session.add_all(m.fixtures().get(bind_name, []))
110+
session.add(fixture_migration_models[bind_name](
111+
bind=bind_name,
112+
filename=f"{module_name}.py",
113+
signature=signature,
114+
))
115+
try:
116+
await session.commit()
117+
logging.info(f"{module_name} fixtures correctly created for {bind_name}")
118+
except:
119+
await session.rollback()
120+
logging.error(f"{module_name} fixtures failed to apply to {bind_name}")
121+
122+
69123

70124
def run_migrations_offline() -> None:
71125
"""Run migrations in 'offline' mode.
@@ -149,13 +203,17 @@ async def run_migrations_online() -> None:
149203
for name, rec in engines.items():
150204
logger.info(f"Migrating database {name}")
151205
if isinstance(rec["engine"], AsyncEngine):
152-
153206
def migration_callable(*args, **kwargs):
154207
return do_run_migration(*args, name=name, **kwargs)
155208

156209
await rec["connection"].run_sync(migration_callable)
210+
Session = async_sessionmaker(bind=rec["connection"])
211+
await a_migrate_fixtures(bind_name=name, Session=Session)
212+
157213
else:
158214
do_run_migration(rec["connection"], name)
215+
# Session = sessionmaker(bind=rec["connection"])
216+
# session = Session()
159217

160218
if USE_TWOPHASE:
161219
for rec in engines.values():

src/alembic/fixtures/__init__.py

Whitespace-only changes.

src/alembic/fixtures/books_example.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
`fixtures` is a dictionary following the format:
3+
4+
"BIND_NAME": "LIST_OF_FACTORIES"
5+
"""
6+
from typing import Dict, List
7+
8+
from factory import Factory
9+
10+
from domains.books._models import BookModel
11+
12+
13+
def fixtures() -> Dict[str, List]:
14+
class BookFactory(Factory):
15+
class Meta:
16+
model = BookModel
17+
18+
return {
19+
"default": [
20+
BookFactory(
21+
title="The Shining",
22+
author_name="Stephen King",
23+
),
24+
],
25+
}

src/alembic/versions/2025-01-26-212326-52b1246eda46_initialize_fixture_tables.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ def downgrade(engine_name: str) -> None:
2727

2828
def upgrade_default() -> None:
2929
op.create_table('alembic_fixtures',
30+
sa.Column('bind', sa.String(), nullable=False),
3031
sa.Column('filename', sa.String(), nullable=False),
3132
sa.Column('signature', sa.String(), nullable=False),
3233
sa.Column('processed_at', sa.DateTime(timezone=True), nullable=False),
33-
sa.PrimaryKeyConstraint('filename')
34+
sa.PrimaryKeyConstraint('bind', 'filename'),
3435
)
3536

3637

0 commit comments

Comments
 (0)