-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #374 from ral-facilities/add-migration-script-#339
Add `ims-migrate` script #339
- Loading branch information
Showing
9 changed files
with
271 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
160 changes: 160 additions & 0 deletions
160
inventory_management_system_api/migrations/migration.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
"""Module for providing a migration script""" | ||
|
||
import argparse | ||
import importlib | ||
import logging | ||
from abc import ABC, abstractmethod | ||
|
||
from pymongo.client_session import ClientSession | ||
from pymongo.database import Database | ||
|
||
from inventory_management_system_api.core.database import get_database, mongodb_client | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
class BaseMigration(ABC): | ||
"""Base class for a migration with a forward and backward step""" | ||
|
||
@abstractmethod | ||
def __init__(self, database: Database): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def description(self) -> str: | ||
"""Description of this migration""" | ||
return "" | ||
|
||
@abstractmethod | ||
def forward(self, session: ClientSession): | ||
"""Method for executing the migration""" | ||
|
||
def forward_after_transaction(self, session: ClientSession): | ||
"""Method called after the forward function is called to do anything that can't be done inside a transaction | ||
(ONLY USE IF NECESSARY e.g. dropping a collection)""" | ||
|
||
@abstractmethod | ||
def backward(self, session: ClientSession): | ||
"""Method for reversing the migration""" | ||
|
||
def backward_after_transaction(self, session: ClientSession): | ||
"""Method called after the backward function is called to do anything that can't be done inside a transaction | ||
(ONLY USE IF NECESSARY e.g. dropping a collection)""" | ||
|
||
|
||
class SubCommand(ABC): | ||
"""Base class for a sub command""" | ||
|
||
def __init__(self, help_message: str): | ||
self.help_message = help_message | ||
|
||
@abstractmethod | ||
def setup(self, parser: argparse.ArgumentParser): | ||
"""Setup the parser by adding any parameters here""" | ||
|
||
@abstractmethod | ||
def run(self, args: argparse.Namespace): | ||
"""Run the command with the given parameters as added by 'setup'""" | ||
|
||
|
||
def load_migration(name: str) -> BaseMigration: | ||
"""Loads a migration script from the scripts module""" | ||
|
||
migration_module = importlib.import_module(f"inventory_management_system_api.migrations.scripts.{name}") | ||
migration_class = getattr(migration_module, "Migration", None) | ||
|
||
database = get_database() | ||
return migration_class(database) | ||
|
||
|
||
class CommandList(SubCommand): | ||
"""Command that lists available database migrations""" | ||
|
||
def __init__(self): | ||
super().__init__(help_message="List all available database migrations") | ||
|
||
def setup(self, parser: argparse.ArgumentParser): | ||
pass | ||
|
||
def run(self, args: argparse.Namespace): | ||
# Find a list of all available migration scripts | ||
with importlib.resources.path("inventory_management_system_api.migrations.scripts", "") as scripts_path: | ||
files_in_scripts = list(scripts_path.iterdir()) | ||
available_migrations = list( | ||
filter(lambda name: "__" not in name, [file.name.replace(".py", "") for file in files_in_scripts]) | ||
) | ||
for migration_name in available_migrations: | ||
migration = load_migration(migration_name) | ||
|
||
print(f"{migration_name} - {migration.description}") | ||
|
||
|
||
class CommandForward(SubCommand): | ||
"""Command that performs a forward database migration""" | ||
|
||
def __init__(self): | ||
super().__init__(help_message="Performs a forward database migration") | ||
|
||
def setup(self, parser: argparse.ArgumentParser): | ||
parser.add_argument("migration", help="Name of the migration to perform") | ||
|
||
def run(self, args: argparse.Namespace): | ||
migration_instance: BaseMigration = load_migration(args.migration) | ||
|
||
# Run migration inside a session to lock writes and revert the changes if it fails | ||
with mongodb_client.start_session() as session: | ||
with session.start_transaction(): | ||
logger.info("Performing forward migration...") | ||
migration_instance.forward(session) | ||
# Run some things outside the transaction e.g. if needing to drop a collection | ||
migration_instance.forward_after_transaction(session) | ||
logger.info("Done!") | ||
|
||
|
||
class CommandBackward(SubCommand): | ||
"""Command that performs a backward database migration""" | ||
|
||
def __init__(self): | ||
super().__init__(help_message="Performs a backward database migration") | ||
|
||
def setup(self, parser: argparse.ArgumentParser): | ||
parser.add_argument("migration", help="Name of the migration to revert") | ||
|
||
def run(self, args: argparse.Namespace): | ||
migration_instance: BaseMigration = load_migration(args.migration) | ||
|
||
# Run migration inside a session to lock writes and revert the changes if it fails | ||
with mongodb_client.start_session() as session: | ||
with session.start_transaction(): | ||
logger.info("Performing backward migration...") | ||
migration_instance.backward(session) | ||
# Run some things outside the transaction e.g. if needing to drop a collection | ||
migration_instance.backward_after_transaction(session) | ||
logger.info("Done!") | ||
|
||
|
||
# List of subcommands | ||
commands: dict[str, SubCommand] = { | ||
"list": CommandList(), | ||
"forward": CommandForward(), | ||
"backward": CommandBackward(), | ||
} | ||
|
||
|
||
def main(): | ||
"""Entrypoint for the ims-migrate script""" | ||
|
||
parser = argparse.ArgumentParser() | ||
|
||
subparser = parser.add_subparsers(dest="command") | ||
|
||
for command_name, command in commands.items(): | ||
command_parser = subparser.add_parser(command_name, help=command.help_message) | ||
command.setup(command_parser) | ||
|
||
args = parser.parse_args() | ||
|
||
logging.basicConfig(level=logging.INFO) | ||
|
||
commands[args.command].run(args) |
Empty file.
65 changes: 65 additions & 0 deletions
65
inventory_management_system_api/migrations/scripts/example_migration.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
""" | ||
Module providing an example migration that does nothing | ||
""" | ||
|
||
import logging | ||
from typing import Collection | ||
|
||
from pymongo.client_session import ClientSession | ||
from pymongo.database import Database | ||
|
||
from inventory_management_system_api.migrations.migration import BaseMigration | ||
|
||
logger = logging.getLogger() | ||
|
||
# When the migration will modify database models by adding data may be a good idea to put the old here and pass any data | ||
# between them and the new ones before updating them in the database, to ensure all modifications are as expected | ||
# e.g. | ||
|
||
|
||
# class OldUnit(BaseModel): | ||
# """ | ||
# Old database model for a Unit | ||
# """ | ||
|
||
# value: str | ||
# code: str | ||
|
||
|
||
class Migration(BaseMigration): | ||
"""Example migration that does nothing""" | ||
|
||
description = "Example migration that does nothing" | ||
|
||
def __init__(self, database: Database): | ||
"""Obtain any collections required for the migration here e.g.""" | ||
|
||
self._units_collection: Collection = database.units | ||
|
||
def forward(self, session: ClientSession): | ||
"""This function should actually perform the migration | ||
All database functions should be given the session in order to ensure all updates are done within a transaction | ||
""" | ||
|
||
# Perform any database updates here e.g. for renaming a field | ||
|
||
# self._units_collection.update_many( | ||
# {}, {"$rename": {"value": "renamed_value"}}, session=session | ||
# ) | ||
|
||
logger.info("example_migration forward migration (that does nothing)") | ||
|
||
def backward(self, session: ClientSession): | ||
"""This function should reverse the migration | ||
All database functions should be given the session in order to ensure all updates are done within a transaction | ||
""" | ||
|
||
# Perform any database updates here e.g. for renaming a field | ||
|
||
# self._units_collection.update_many( | ||
# {}, {"$rename": {"renamed_value": "value"}}, session=session | ||
# ) | ||
|
||
logger.info("example_migration backward migration (that does nothing)") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters