1

Archive Project

# Conflicts:
#	.gitignore
This commit is contained in:
Kim Wittenburg
2020-07-22 11:06:03 +02:00
parent 1d58ad24b4
commit 2f4c6d4908
7 changed files with 299 additions and 0 deletions

85
alembic_data/__init__.py Normal file
View File

@@ -0,0 +1,85 @@
"""Alembic-Data is an extension for alembic that creates automatic data migrations.
Data migrations (in contrast to schema migrations) are performed on the contents of database tables. They allow you to
create, update, and delete rows inside the database. Alembic-Data provides a framework to generate these migrations
automatically.
Using Alembic-Data is very simple. You can use the `register_object` function to register an object in the database.
Alembic data will make sure that the next migration creates the registered object. This is normally only useful for
tables that do not contain any user data (as the migrations would have to be generated depending on the actual data).
Instead the typical usecase is to combine the `register_object` function with the `@managed_model` decorator to tell
Alembic-Data that a model will be managed exclusively programmatically.
The `@managed_model` decorator is used on your declarative model classes. For any `@managed_model` Alembic-Data will
created your `register_object` and will additionally delete any rows from that database that do not belong to any
registered object.
"""
from typing import List, Any, Dict
from alembic.autogenerate import comparators
from alembic.autogenerate.api import AutogenContext
from alembic.operations.ops import UpgradeOps
from sqlalchemy.ext.declarative import DeclarativeMeta
from .ops import *
__all__ = ["managed_model", "register_object"]
def managed_model(cls: DeclarativeMeta) -> DeclarativeMeta:
"""Marks this model as a managed model.
Class decorator to be used on declarative SQLAlchemy models. Using this decorator causes alembic-data to assume that
objects for this model will only be created through code and the `register_object` function.
:param cls: The class to be decorated. Must be a SQLAlchemy declarative class.
:return: The decorated class, unchanged.
"""
cls.metadata.info.setdefault("managed models", set()).add(cls) # type: ignore
return cls
def register_object(cls: DeclarativeMeta, **kwargs) -> None:
"""
Registers an object to be created during a database migration.
:param cls: The class of the declarative model class. This is used to determine which table to operate on.
:param kwargs: Arguments used to create the object. This must uniquely identify the object as it is also used to
determine whether the object already exists.
"""
cls.metadata.info.setdefault("objects", dict()).setdefault(cls, list()).append(kwargs) # type: ignore
@comparators.dispatch_for("schema")
def compare_objects(autogen_context: AutogenContext, upgrade_ops: UpgradeOps, schemas: List[str]) -> None:
"""
Generates appropriate data migrations for the `autogen_context` and adds them to `upgrade_ops`.
"""
managed_models = autogen_context.metadata.info.get('managed models', set())
objects: Dict[DeclarativeMeta, Any] = autogen_context.metadata.info.get('objects', dict())
remaining_objects = {cls: set(cls.query.all()) # type: ignore
for cls in objects.keys()
if cls in managed_models
and autogen_context.dialect.has_table(autogen_context.connection.engine,
cls.__table__.name)} # type: ignore
# Add new objects
for cls, datas in objects.items():
for data in datas:
existing_object = (cls.query.filter_by(**data).first() # type: ignore
if autogen_context.dialect.has_table(autogen_context.connection.engine,
cls.__table__.name) # type: ignore
else None)
if existing_object:
remaining_objects.get(cls, set()).discard(existing_object)
else:
print("Adding " + cls.__name__ + ": " + str(data))
upgrade_ops.ops.append(InsertRowOp(cls.__table__.name, **data)) # type: ignore
# Remove old objects
for cls, object_set in remaining_objects.items():
for o in object_set:
values = {key: getattr(o, key) for key in o.__mapper__.column_attrs.keys()}
print("Deleting " + cls.__name__ + ": " + str(values))
upgrade_ops.ops.append(DeleteRowOp(cls.__table__.name, **values)) # type: ignore