mpt-tool 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mpt_tool/__init__.py +0 -0
- mpt_tool/cli.py +76 -0
- mpt_tool/commands/__init__.py +4 -0
- mpt_tool/commands/base.py +27 -0
- mpt_tool/commands/data.py +23 -0
- mpt_tool/commands/errors.py +9 -0
- mpt_tool/commands/factory.py +45 -0
- mpt_tool/commands/fake.py +26 -0
- mpt_tool/commands/list.py +33 -0
- mpt_tool/commands/new_data.py +30 -0
- mpt_tool/commands/new_schema.py +30 -0
- mpt_tool/commands/schema.py +23 -0
- mpt_tool/commands/validators.py +24 -0
- mpt_tool/config.py +22 -0
- mpt_tool/constants.py +4 -0
- mpt_tool/enums.py +41 -0
- mpt_tool/errors.py +9 -0
- mpt_tool/managers/__init__.py +5 -0
- mpt_tool/managers/encoders.py +15 -0
- mpt_tool/managers/errors.py +25 -0
- mpt_tool/managers/file_migration.py +132 -0
- mpt_tool/managers/state/__init__.py +0 -0
- mpt_tool/managers/state/airtable.py +108 -0
- mpt_tool/managers/state/base.py +49 -0
- mpt_tool/managers/state/factory.py +32 -0
- mpt_tool/managers/state/file.py +64 -0
- mpt_tool/migration/__init__.py +4 -0
- mpt_tool/migration/base.py +25 -0
- mpt_tool/migration/data_base.py +10 -0
- mpt_tool/migration/mixins/__init__.py +4 -0
- mpt_tool/migration/mixins/airtable_client.py +29 -0
- mpt_tool/migration/mixins/mpt_client.py +31 -0
- mpt_tool/migration/schema_base.py +10 -0
- mpt_tool/models.py +140 -0
- mpt_tool/py.typed +0 -0
- mpt_tool/renders.py +30 -0
- mpt_tool/templates.py +12 -0
- mpt_tool/use_cases/__init__.py +11 -0
- mpt_tool/use_cases/apply_migration.py +42 -0
- mpt_tool/use_cases/errors.py +17 -0
- mpt_tool/use_cases/list_migrations.py +35 -0
- mpt_tool/use_cases/new_migration.py +22 -0
- mpt_tool/use_cases/run_migrations.py +98 -0
- mpt_tool-5.0.0.dist-info/METADATA +299 -0
- mpt_tool-5.0.0.dist-info/RECORD +48 -0
- mpt_tool-5.0.0.dist-info/WHEEL +4 -0
- mpt_tool-5.0.0.dist-info/entry_points.txt +2 -0
- mpt_tool-5.0.0.dist-info/licenses/LICENSE +201 -0
mpt_tool/__init__.py
ADDED
|
File without changes
|
mpt_tool/cli.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from mpt_tool.commands import CommandFactory, MigrateCommandValidator
|
|
7
|
+
from mpt_tool.commands.errors import BadParameterError, CommandNotFoundError
|
|
8
|
+
from mpt_tool.use_cases.errors import UseCaseError
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="MPT CLI - Migration tool for extensions.", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.callback()
|
|
14
|
+
def callback() -> None:
|
|
15
|
+
"""MPT CLI - Migration tool for extensions."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("migrate")
|
|
19
|
+
def migrate( # noqa: WPS211
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
data: Annotated[bool, typer.Option("--data", help="Run data migrations.")] = False, # noqa: FBT002
|
|
22
|
+
schema: Annotated[bool, typer.Option("--schema", help="Run schema migrations.")] = False, # noqa: FBT002
|
|
23
|
+
fake: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Option(
|
|
26
|
+
"--fake",
|
|
27
|
+
help="Mark the migration provided as applied without running it",
|
|
28
|
+
metavar="MIGRATION_ID",
|
|
29
|
+
),
|
|
30
|
+
] = None,
|
|
31
|
+
new_data: Annotated[
|
|
32
|
+
str | None,
|
|
33
|
+
typer.Option(
|
|
34
|
+
"--new-data",
|
|
35
|
+
metavar="FILENAME",
|
|
36
|
+
help="Scaffold a new data migration script with the provided filename.",
|
|
37
|
+
),
|
|
38
|
+
] = None,
|
|
39
|
+
new_schema: Annotated[
|
|
40
|
+
str | None,
|
|
41
|
+
typer.Option(
|
|
42
|
+
"--new-schema",
|
|
43
|
+
metavar="FILENAME",
|
|
44
|
+
help="Scaffold a new schema migration script with the provided filename.",
|
|
45
|
+
),
|
|
46
|
+
] = None,
|
|
47
|
+
list: Annotated[bool, typer.Option("--list", help="List all migrations.")] = False, # noqa: A002, FBT002
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Migrate command."""
|
|
50
|
+
try:
|
|
51
|
+
MigrateCommandValidator.validate(ctx.params)
|
|
52
|
+
except BadParameterError as error:
|
|
53
|
+
raise typer.BadParameter(str(error), param_hint="migrate")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
command_instance = CommandFactory.get_instance(ctx.params)
|
|
57
|
+
except CommandNotFoundError:
|
|
58
|
+
raise typer.BadParameter("No valid param provided.", param_hint="migrate")
|
|
59
|
+
|
|
60
|
+
typer.echo(command_instance.start_message)
|
|
61
|
+
try:
|
|
62
|
+
command_instance.run()
|
|
63
|
+
except UseCaseError as error:
|
|
64
|
+
typer.secho(
|
|
65
|
+
f"Error running {command_instance.name} command: {error!s}",
|
|
66
|
+
fg=typer.colors.RED,
|
|
67
|
+
)
|
|
68
|
+
raise typer.Abort
|
|
69
|
+
|
|
70
|
+
typer.secho(command_instance.success_message, fg=typer.colors.GREEN)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main() -> None:
|
|
74
|
+
"""Entry point for the CLI."""
|
|
75
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
76
|
+
app()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseCommand(ABC):
|
|
5
|
+
"""Base class for migration commands."""
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
"""The name of the command, used for CLI argument parsing."""
|
|
10
|
+
return self.__class__.__name__.replace("Command", "").lower()
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def start_message(self) -> str:
|
|
15
|
+
"""Message to display before starting the migration."""
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def success_message(self) -> str:
|
|
21
|
+
"""Message to display after the migration has finished."""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def run(self) -> None:
|
|
26
|
+
"""Executes the migration."""
|
|
27
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from mpt_tool.commands.base import BaseCommand
|
|
4
|
+
from mpt_tool.enums import MigrationTypeEnum
|
|
5
|
+
from mpt_tool.use_cases import RunMigrationsUseCase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataCommand(BaseCommand):
|
|
9
|
+
"""Runs all data migrations."""
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
@property
|
|
13
|
+
def start_message(self) -> str:
|
|
14
|
+
return f"Running {MigrationTypeEnum.DATA} migrations..."
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
@property
|
|
18
|
+
def success_message(self) -> str:
|
|
19
|
+
return "Migrations completed successfully."
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def run(self) -> None:
|
|
23
|
+
RunMigrationsUseCase().execute(MigrationTypeEnum.DATA)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from mpt_tool.commands.base import (
|
|
4
|
+
BaseCommand,
|
|
5
|
+
)
|
|
6
|
+
from mpt_tool.commands.data import DataCommand
|
|
7
|
+
from mpt_tool.commands.errors import CommandNotFoundError
|
|
8
|
+
from mpt_tool.commands.fake import FakeCommand
|
|
9
|
+
from mpt_tool.commands.list import ListCommand
|
|
10
|
+
from mpt_tool.commands.new_data import NewDataCommand
|
|
11
|
+
from mpt_tool.commands.new_schema import NewSchemaCommand
|
|
12
|
+
from mpt_tool.commands.schema import SchemaCommand
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandFactory:
|
|
16
|
+
"""Command factory to create the correct command."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_instance(cls, param_data: dict[str, bool | str | None]) -> BaseCommand: # noqa: C901, WPS212
|
|
20
|
+
"""Get the correct command instance based on the parameter data.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
param_data: The parameter data.
|
|
24
|
+
|
|
25
|
+
Return:
|
|
26
|
+
The command instance.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
CommandNotFoundError: If no command is found.
|
|
30
|
+
"""
|
|
31
|
+
match param_data:
|
|
32
|
+
case {"data": True}:
|
|
33
|
+
return DataCommand()
|
|
34
|
+
case {"schema": True}:
|
|
35
|
+
return SchemaCommand()
|
|
36
|
+
case {"fake": fake_value} if fake_value is not None:
|
|
37
|
+
return FakeCommand(migration_id=cast(str, fake_value))
|
|
38
|
+
case {"new_schema": new_schema_value} if new_schema_value is not None:
|
|
39
|
+
return NewSchemaCommand(migration_id=cast(str, new_schema_value))
|
|
40
|
+
case {"new_data": new_data_value} if new_data_value is not None:
|
|
41
|
+
return NewDataCommand(migration_id=cast(str, new_data_value))
|
|
42
|
+
case {"list": True}:
|
|
43
|
+
return ListCommand()
|
|
44
|
+
case _:
|
|
45
|
+
raise CommandNotFoundError("Command not found.")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from mpt_tool.commands.base import BaseCommand
|
|
4
|
+
from mpt_tool.use_cases import ApplyMigrationUseCase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FakeCommand(BaseCommand):
|
|
8
|
+
"""Applies a migration without running it."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, migration_id: str):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.migration_id = migration_id
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
@property
|
|
16
|
+
def start_message(self) -> str:
|
|
17
|
+
return f"Running migration {self.migration_id} in fake mode."
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
@property
|
|
21
|
+
def success_message(self) -> str:
|
|
22
|
+
return f"Migration {self.migration_id} applied successfully."
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def run(self) -> None:
|
|
26
|
+
ApplyMigrationUseCase().execute(migration_id=self.migration_id)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from mpt_tool.commands.base import BaseCommand
|
|
7
|
+
from mpt_tool.constants import CONSOLE_WIDTH
|
|
8
|
+
from mpt_tool.renders import MigrationRender
|
|
9
|
+
from mpt_tool.use_cases import ListMigrationsUseCase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ListCommand(BaseCommand):
|
|
13
|
+
"""Lists all migrations."""
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
@property
|
|
17
|
+
def start_message(self) -> str:
|
|
18
|
+
return "Listing migrations..."
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
@property
|
|
22
|
+
def success_message(self) -> str:
|
|
23
|
+
return "Migrations listed successfully."
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
def run(self) -> None:
|
|
27
|
+
migrations = ListMigrationsUseCase().execute()
|
|
28
|
+
if not migrations:
|
|
29
|
+
typer.echo("No migrations found.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
console = Console(width=CONSOLE_WIDTH)
|
|
33
|
+
console.print(MigrationRender(migration_items=migrations), overflow="fold")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from mpt_tool.commands.base import BaseCommand
|
|
6
|
+
from mpt_tool.enums import MigrationTypeEnum
|
|
7
|
+
from mpt_tool.use_cases import NewMigrationUseCase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NewDataCommand(BaseCommand):
|
|
11
|
+
"""Creates a new data migration file with the given id."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, migration_id: str) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.migration_id = migration_id
|
|
16
|
+
|
|
17
|
+
@override
|
|
18
|
+
@property
|
|
19
|
+
def start_message(self) -> str:
|
|
20
|
+
return f"Scaffolding migration: {self.migration_id}."
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
@property
|
|
24
|
+
def success_message(self) -> str:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def run(self) -> None:
|
|
29
|
+
filename = NewMigrationUseCase().execute(MigrationTypeEnum.DATA, self.migration_id)
|
|
30
|
+
typer.echo(f"Migration file: {filename} has been created.")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from mpt_tool.commands.base import BaseCommand
|
|
6
|
+
from mpt_tool.enums import MigrationTypeEnum
|
|
7
|
+
from mpt_tool.use_cases import NewMigrationUseCase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NewSchemaCommand(BaseCommand):
|
|
11
|
+
"""Creates a new schema migration file with the given id."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, migration_id: str) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.migration_id = migration_id
|
|
16
|
+
|
|
17
|
+
@override
|
|
18
|
+
@property
|
|
19
|
+
def start_message(self) -> str:
|
|
20
|
+
return f"Scaffolding migration: {self.migration_id}."
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
@property
|
|
24
|
+
def success_message(self) -> str:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def run(self) -> None:
|
|
29
|
+
filename = NewMigrationUseCase().execute(MigrationTypeEnum.SCHEMA, self.migration_id)
|
|
30
|
+
typer.echo(f"Migration file: {filename} has been created.")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from mpt_tool.commands.base import BaseCommand
|
|
4
|
+
from mpt_tool.enums import MigrationTypeEnum
|
|
5
|
+
from mpt_tool.use_cases import RunMigrationsUseCase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaCommand(BaseCommand):
|
|
9
|
+
"""Runs all schema migrations."""
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
@property
|
|
13
|
+
def start_message(self) -> str:
|
|
14
|
+
return f"Running {MigrationTypeEnum.SCHEMA} migrations..."
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
@property
|
|
18
|
+
def success_message(self) -> str:
|
|
19
|
+
return f"{MigrationTypeEnum.SCHEMA} migrations applied successfully."
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def run(self) -> None:
|
|
23
|
+
RunMigrationsUseCase().execute(MigrationTypeEnum.SCHEMA)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from mpt_tool.commands.errors import BadParameterError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MigrateCommandValidator:
|
|
7
|
+
"""Validator for the migrate command."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def validate(cls, command_params: dict[str, Any]) -> None:
|
|
11
|
+
"""Validate the migrate command parameters.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
command_params: The migrate command parameters.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
BadParameterError: When none or more than one param is used
|
|
18
|
+
"""
|
|
19
|
+
param_counts = sum(1 for param_value in command_params.values() if param_value)
|
|
20
|
+
if not param_counts:
|
|
21
|
+
raise BadParameterError("At least one param must be used.")
|
|
22
|
+
|
|
23
|
+
if param_counts > 1:
|
|
24
|
+
raise BadParameterError("Only one param can be used.")
|
mpt_tool/config.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_airtable_config(config_key: str) -> str | None:
|
|
5
|
+
"""Get Airtable configuration."""
|
|
6
|
+
config = {
|
|
7
|
+
"api_key": os.getenv("AIRTABLE_API_KEY"),
|
|
8
|
+
"base_id": os.getenv("STORAGE_AIRTABLE_BASE_ID"),
|
|
9
|
+
"table_name": os.getenv("STORAGE_AIRTABLE_TABLE_NAME", "Migrations"),
|
|
10
|
+
}
|
|
11
|
+
return config.get(config_key)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_mpt_config(config_key: str) -> str | None:
|
|
15
|
+
"""Get MPT configuration."""
|
|
16
|
+
config = {"api_token": os.getenv("MPT_API_TOKEN"), "base_url": os.getenv("MPT_API_BASE_URL")}
|
|
17
|
+
return config.get(config_key)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_storage_type() -> str:
|
|
21
|
+
"""Get storage type."""
|
|
22
|
+
return os.getenv("STORAGE_TYPE", "local")
|
mpt_tool/constants.py
ADDED
mpt_tool/enums.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import TYPE_CHECKING, Self
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from mpt_tool.models import Migration
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MigrationStatusEnum(StrEnum):
|
|
9
|
+
"""Enumeration of migration status values."""
|
|
10
|
+
|
|
11
|
+
RUNNING = "running"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
FAKE_APPLY = "faked"
|
|
14
|
+
APPLIED = "applied"
|
|
15
|
+
NOT_APPLIED = "not applied"
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_state(cls, migration_state: "Migration | None") -> Self:
|
|
19
|
+
"""Calculate migration status based on migration state."""
|
|
20
|
+
if migration_state is None:
|
|
21
|
+
return cls.NOT_APPLIED
|
|
22
|
+
if migration_state.started_at and migration_state.applied_at:
|
|
23
|
+
return cls.APPLIED
|
|
24
|
+
if migration_state.started_at and not migration_state.applied_at:
|
|
25
|
+
return cls.RUNNING
|
|
26
|
+
if migration_state.started_at is None and migration_state.applied_at:
|
|
27
|
+
return cls.FAKE_APPLY
|
|
28
|
+
|
|
29
|
+
return cls.FAILED
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MigrationTypeEnum(StrEnum):
|
|
33
|
+
"""Enumeration of migration types.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
DATA: Represents a data migration.
|
|
37
|
+
SCHEMA: Represents a schema migration.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
DATA = "data"
|
|
41
|
+
SCHEMA = "schema"
|
mpt_tool/errors.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, override
|
|
3
|
+
|
|
4
|
+
from mpt_tool.models import Migration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StateJSONEncoder(json.JSONEncoder):
|
|
8
|
+
"""JSON encoder for migration states."""
|
|
9
|
+
|
|
10
|
+
@override
|
|
11
|
+
def default(self, obj: object) -> Any: # noqa: WPS110
|
|
12
|
+
if isinstance(obj, Migration):
|
|
13
|
+
return obj.to_dict()
|
|
14
|
+
|
|
15
|
+
return super().default(obj)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from mpt_tool.errors import BaseError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ManagerError(BaseError):
|
|
5
|
+
"""Base error for all manager errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CreateMigrationError(ManagerError):
|
|
9
|
+
"""Error creating the migration file."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidStateError(ManagerError):
|
|
13
|
+
"""Error loading invalid state."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoadMigrationError(ManagerError):
|
|
17
|
+
"""Error loading migrations."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MigrationFolderError(ManagerError):
|
|
21
|
+
"""Error accessing migrations folder."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StateNotFoundError(ManagerError):
|
|
25
|
+
"""Error getting state from state file."""
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from importlib.util import module_from_spec, spec_from_file_location
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from mpt_tool.constants import MIGRATION_FOLDER
|
|
8
|
+
from mpt_tool.enums import MigrationTypeEnum
|
|
9
|
+
from mpt_tool.managers.errors import CreateMigrationError, LoadMigrationError, MigrationFolderError
|
|
10
|
+
from mpt_tool.migration.base import BaseMigration
|
|
11
|
+
from mpt_tool.models import MigrationFile
|
|
12
|
+
from mpt_tool.templates import MIGRATION_SCAFFOLDING_TEMPLATE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileMigrationManager:
|
|
16
|
+
"""Manages migration files."""
|
|
17
|
+
|
|
18
|
+
_migration_folder: Path = Path(MIGRATION_FOLDER)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def load_migration(cls, migration_file: MigrationFile) -> BaseMigration:
|
|
22
|
+
"""Loads a migration instance from a migration file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
migration_file: The migration file to load.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The migration instance.
|
|
29
|
+
|
|
30
|
+
Raise:
|
|
31
|
+
LoadMigrationError: If an error occurs during migration loading.
|
|
32
|
+
"""
|
|
33
|
+
spec = spec_from_file_location(migration_file.name, migration_file.full_path)
|
|
34
|
+
if spec is None or spec.loader is None:
|
|
35
|
+
raise LoadMigrationError(f"Failed to load migration file: {migration_file.full_path}")
|
|
36
|
+
|
|
37
|
+
migration_module = module_from_spec(spec)
|
|
38
|
+
try:
|
|
39
|
+
spec.loader.exec_module(migration_module)
|
|
40
|
+
except ImportError as error:
|
|
41
|
+
raise LoadMigrationError(f"Failed to import migration module: {error!s}") from error
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
migration_instance = cast(BaseMigration, migration_module.Migration())
|
|
45
|
+
except (TypeError, AttributeError) as error:
|
|
46
|
+
raise LoadMigrationError(f"Invalid migration: {error!s}") from error
|
|
47
|
+
|
|
48
|
+
return migration_instance
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def new_migration(cls, file_suffix: str, migration_type: MigrationTypeEnum) -> MigrationFile:
|
|
52
|
+
"""Creates a new migration file.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
file_suffix: The suffix to use for the migration file name.
|
|
56
|
+
migration_type: The type of migration to create.
|
|
57
|
+
|
|
58
|
+
Return:
|
|
59
|
+
The newly created migration file.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
CreateMigrationError: If an error occurs during migration creation.
|
|
63
|
+
"""
|
|
64
|
+
cls._migration_folder.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
try:
|
|
66
|
+
migration_file = MigrationFile.new(migration_id=file_suffix, path=cls._migration_folder)
|
|
67
|
+
except ValueError as error:
|
|
68
|
+
raise CreateMigrationError(f"Invalid migration ID: {error}") from error
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
migration_file.full_path.touch(exist_ok=False)
|
|
72
|
+
except FileExistsError as error:
|
|
73
|
+
raise CreateMigrationError(
|
|
74
|
+
f"File already exists: {migration_file.file_name}"
|
|
75
|
+
) from error
|
|
76
|
+
|
|
77
|
+
migration_file.full_path.write_text(
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
data=MIGRATION_SCAFFOLDING_TEMPLATE.substitute(
|
|
80
|
+
migration_name="DataBaseMigration"
|
|
81
|
+
if migration_type == MigrationTypeEnum.DATA
|
|
82
|
+
else "SchemaBaseMigration"
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return migration_file
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def retrieve_migration_files(cls) -> tuple[MigrationFile, ...]:
|
|
89
|
+
"""Retrieves all migration files."""
|
|
90
|
+
return cls._get_migration_files()
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def validate(cls) -> tuple[MigrationFile, ...]:
|
|
94
|
+
"""Validates the migration folder and returns a tuple of migration files.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The validated migration files.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
MigrationFolderError: If an error occurs during migration validation.
|
|
101
|
+
"""
|
|
102
|
+
if not cls._migration_folder.exists():
|
|
103
|
+
raise MigrationFolderError(f"Migration folder not found: {cls._migration_folder}")
|
|
104
|
+
|
|
105
|
+
migrations = cls._get_migration_files()
|
|
106
|
+
if not migrations:
|
|
107
|
+
raise MigrationFolderError(f"No migration files found in {cls._migration_folder}")
|
|
108
|
+
|
|
109
|
+
counter = Counter([migration.migration_id for migration in migrations])
|
|
110
|
+
duplicated_migrations = [element for element, count in counter.items() if count > 1]
|
|
111
|
+
if duplicated_migrations:
|
|
112
|
+
raise MigrationFolderError(
|
|
113
|
+
f"Duplicate migration filename found: {duplicated_migrations[0]}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return migrations
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _get_migration_files(cls) -> tuple[MigrationFile, ...]:
|
|
120
|
+
try:
|
|
121
|
+
migrations = sorted(
|
|
122
|
+
(
|
|
123
|
+
MigrationFile.build_from_path(path)
|
|
124
|
+
for path in cls._migration_folder.glob("*.py")
|
|
125
|
+
if re.match(r"\d+_.*\.py", path.name)
|
|
126
|
+
),
|
|
127
|
+
key=lambda migration_file: migration_file.order_id,
|
|
128
|
+
)
|
|
129
|
+
except ValueError as error:
|
|
130
|
+
raise MigrationFolderError(str(error)) from None
|
|
131
|
+
|
|
132
|
+
return tuple(migrations)
|
|
File without changes
|