Skip to content

Commit 4f001ff

Browse files
committed
refactor: add NewMigrationUseCase
1 parent bc879bc commit 4f001ff

File tree

5 files changed

+93
-23
lines changed

5 files changed

+93
-23
lines changed

mpt_tool/cli.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import datetime as dt
21
import logging
3-
from pathlib import Path
4-
from typing import Annotated
2+
from typing import Annotated, cast
53

64
import typer
75

8-
from mpt_tool.constants import MIGRATION_FOLDER
96
from mpt_tool.enums import MigrationTypeEnum
10-
from mpt_tool.errors import RunMigrationError
11-
from mpt_tool.templates import MIGRATION_SCAFFOLDING_TEMPLATE
7+
from mpt_tool.errors import NewMigrationError, RunMigrationError
128
from mpt_tool.use_cases import RunMigrationsUseCase
9+
from mpt_tool.use_cases.new_migration import NewMigrationUseCase
1310

1411
app = typer.Typer(help="MPT CLI - Migration tool for extensions.", no_args_is_help=True)
1512

@@ -20,7 +17,7 @@ def callback() -> None:
2017

2118

2219
@app.command("migrate")
23-
def migrate( # noqa: C901, WPS238, WPS210, WPS213, WPS231
20+
def migrate( # noqa: C901, WPS238, WPS231
2421
data: Annotated[bool, typer.Option("--data", help="Run data migrations.")] = False, # noqa: FBT002
2522
schema: Annotated[bool, typer.Option("--schema", help="Run schema migrations.")] = False, # noqa: FBT002
2623
new_data: Annotated[
@@ -63,25 +60,14 @@ def migrate( # noqa: C901, WPS238, WPS210, WPS213, WPS231
6360

6461
if new_schema or new_data:
6562
filename_suffix = new_data or new_schema
63+
migration_type = MigrationTypeEnum.DATA if new_data else MigrationTypeEnum.SCHEMA
6664
typer.echo(f"Scaffolding migration: {filename_suffix}.")
67-
migration_folder = Path(MIGRATION_FOLDER)
68-
migration_folder.mkdir(parents=True, exist_ok=True)
69-
timestamp = dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S")
70-
# TODO: add filename validation
71-
filename = f"{timestamp}_{filename_suffix}.py"
72-
full_filename_path = migration_folder / filename
7365
try:
74-
full_filename_path.touch(exist_ok=False)
75-
except FileExistsError:
76-
typer.secho(f"File already exists: {filename}", fg=typer.colors.RED)
66+
filename = NewMigrationUseCase().execute(migration_type, cast(str, filename_suffix))
67+
except NewMigrationError as error:
68+
typer.secho(f"Error creating migration: {error!s}", fg=typer.colors.RED)
7769
raise typer.Abort
7870

79-
full_filename_path.write_text(
80-
encoding="utf-8",
81-
data=MIGRATION_SCAFFOLDING_TEMPLATE.substitute(
82-
command_name="DataBaseCommand" if new_data else "SchemaBaseCommand"
83-
),
84-
)
8571
typer.secho(f"Migration file: {filename} has been created.", fg=typer.colors.GREEN)
8672

8773

mpt_tool/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ def __init__(self, message: str):
99
super().__init__(message)
1010

1111

12+
class CreateMigrationError(BaseError):
13+
"""Error creating the migration file."""
14+
15+
1216
class LoadMigrationError(BaseError):
1317
"""Error loading migrations."""
1418

1519

20+
class NewMigrationError(BaseError):
21+
"""Error creating new migration."""
22+
23+
1624
class MigrationFolderError(BaseError):
1725
"""Error accessing migrations folder."""
1826

mpt_tool/managers.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88

99
from mpt_tool.constants import MIGRATION_FOLDER, MIGRATION_STATE_FILE
1010
from mpt_tool.enums import MigrationTypeEnum
11-
from mpt_tool.errors import LoadMigrationError, MigrationFolderError, StateNotFoundError
11+
from mpt_tool.errors import (
12+
CreateMigrationError,
13+
LoadMigrationError,
14+
MigrationFolderError,
15+
StateNotFoundError,
16+
)
1217
from mpt_tool.models import Migration, MigrationFile
18+
from mpt_tool.templates import MIGRATION_SCAFFOLDING_TEMPLATE
1319

1420

1521
class FileMigrationManager:
@@ -63,6 +69,32 @@ def load_migration(cls, migration_file: MigrationFile) -> ModuleType:
6369
spec.loader.exec_module(migration_module)
6470
return migration_module
6571

72+
@classmethod
73+
def new_migration(cls, file_suffix: str, migration_type: MigrationTypeEnum) -> MigrationFile:
74+
"""Creates a new migration file."""
75+
cls._migration_folder.mkdir(parents=True, exist_ok=True)
76+
try:
77+
migration_file = MigrationFile.new(migration_id=file_suffix, path=cls._migration_folder)
78+
except ValueError as error:
79+
raise CreateMigrationError(f"Invalid migration ID: {error}") from error
80+
81+
try:
82+
migration_file.full_path.touch(exist_ok=False)
83+
except FileExistsError as error:
84+
raise CreateMigrationError(
85+
f"File already exists: {migration_file.file_name}"
86+
) from error
87+
88+
migration_file.full_path.write_text(
89+
encoding="utf-8",
90+
data=MIGRATION_SCAFFOLDING_TEMPLATE.substitute(
91+
command_name="DataBaseCommand"
92+
if migration_type == MigrationTypeEnum.DATA
93+
else "SchemaBaseCommand"
94+
),
95+
)
96+
return migration_file
97+
6698

6799
class StateJSONEncoder(json.JSONEncoder):
68100
"""JSON encoder for migration states."""

mpt_tool/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ class MigrationFile:
6363
migration_id: str
6464
order_id: int
6565

66+
@property
67+
def file_name(self) -> str:
68+
"""Migration file name."""
69+
return f"{self.order_id}_{self.migration_id}.py"
70+
71+
def __post_init__(self):
72+
if not self.migration_id.isidentifier():
73+
raise ValueError(
74+
"Migration ID must contain only alphanumeric letters and numbers, or underscores."
75+
)
76+
6677
@property
6778
def name(self) -> str:
6879
"""Migration file name."""
@@ -77,3 +88,15 @@ def build_from_path(cls, path: Path) -> Self:
7788
"""
7889
order_id, migration_id = path.stem.split("_", maxsplit=1)
7990
return cls(full_path=path, order_id=int(order_id), migration_id=migration_id)
91+
92+
@classmethod
93+
def new(cls, migration_id: str, path: Path) -> Self:
94+
"""Create a new migration file.
95+
96+
Args:
97+
migration_id: The migration ID.
98+
path: The path to the migration folder.
99+
"""
100+
timestamp = dt.datetime.now(tz=dt.UTC).strftime("%Y%m%d%H%M%S")
101+
full_path = path / f"{timestamp}_{migration_id}.py"
102+
return cls(full_path=full_path, migration_id=migration_id, order_id=int(timestamp))
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from mpt_tool.enums import MigrationTypeEnum
2+
from mpt_tool.errors import CreateMigrationError, NewMigrationError
3+
from mpt_tool.managers import FileMigrationManager
4+
5+
6+
class NewMigrationUseCase:
7+
"""Service to create a new migration file."""
8+
9+
def __init__(self, file_manager: FileMigrationManager | None = None):
10+
self.file_manager = file_manager or FileMigrationManager()
11+
12+
def execute(self, migration_type: MigrationTypeEnum, file_name: str) -> str:
13+
"""Create a new migration file."""
14+
try:
15+
migration_file = self.file_manager.new_migration(
16+
file_suffix=file_name, migration_type=migration_type
17+
)
18+
except CreateMigrationError as error:
19+
raise NewMigrationError(str(error)) from error
20+
21+
return migration_file.file_name

0 commit comments

Comments
 (0)