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.
Files changed (48) hide show
  1. mpt_tool/__init__.py +0 -0
  2. mpt_tool/cli.py +76 -0
  3. mpt_tool/commands/__init__.py +4 -0
  4. mpt_tool/commands/base.py +27 -0
  5. mpt_tool/commands/data.py +23 -0
  6. mpt_tool/commands/errors.py +9 -0
  7. mpt_tool/commands/factory.py +45 -0
  8. mpt_tool/commands/fake.py +26 -0
  9. mpt_tool/commands/list.py +33 -0
  10. mpt_tool/commands/new_data.py +30 -0
  11. mpt_tool/commands/new_schema.py +30 -0
  12. mpt_tool/commands/schema.py +23 -0
  13. mpt_tool/commands/validators.py +24 -0
  14. mpt_tool/config.py +22 -0
  15. mpt_tool/constants.py +4 -0
  16. mpt_tool/enums.py +41 -0
  17. mpt_tool/errors.py +9 -0
  18. mpt_tool/managers/__init__.py +5 -0
  19. mpt_tool/managers/encoders.py +15 -0
  20. mpt_tool/managers/errors.py +25 -0
  21. mpt_tool/managers/file_migration.py +132 -0
  22. mpt_tool/managers/state/__init__.py +0 -0
  23. mpt_tool/managers/state/airtable.py +108 -0
  24. mpt_tool/managers/state/base.py +49 -0
  25. mpt_tool/managers/state/factory.py +32 -0
  26. mpt_tool/managers/state/file.py +64 -0
  27. mpt_tool/migration/__init__.py +4 -0
  28. mpt_tool/migration/base.py +25 -0
  29. mpt_tool/migration/data_base.py +10 -0
  30. mpt_tool/migration/mixins/__init__.py +4 -0
  31. mpt_tool/migration/mixins/airtable_client.py +29 -0
  32. mpt_tool/migration/mixins/mpt_client.py +31 -0
  33. mpt_tool/migration/schema_base.py +10 -0
  34. mpt_tool/models.py +140 -0
  35. mpt_tool/py.typed +0 -0
  36. mpt_tool/renders.py +30 -0
  37. mpt_tool/templates.py +12 -0
  38. mpt_tool/use_cases/__init__.py +11 -0
  39. mpt_tool/use_cases/apply_migration.py +42 -0
  40. mpt_tool/use_cases/errors.py +17 -0
  41. mpt_tool/use_cases/list_migrations.py +35 -0
  42. mpt_tool/use_cases/new_migration.py +22 -0
  43. mpt_tool/use_cases/run_migrations.py +98 -0
  44. mpt_tool-5.0.0.dist-info/METADATA +299 -0
  45. mpt_tool-5.0.0.dist-info/RECORD +48 -0
  46. mpt_tool-5.0.0.dist-info/WHEEL +4 -0
  47. mpt_tool-5.0.0.dist-info/entry_points.txt +2 -0
  48. 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,4 @@
1
+ from mpt_tool.commands.factory import CommandFactory
2
+ from mpt_tool.commands.validators import MigrateCommandValidator
3
+
4
+ __all__ = ["CommandFactory", "MigrateCommandValidator"]
@@ -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,9 @@
1
+ from mpt_tool.errors import BaseError
2
+
3
+
4
+ class BadParameterError(BaseError):
5
+ """Bad parameter error."""
6
+
7
+
8
+ class CommandNotFoundError(BaseError):
9
+ """Command not found error."""
@@ -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
@@ -0,0 +1,4 @@
1
+ CONSOLE_WIDTH: int = 200
2
+ MAX_LEN_MIGRATION_ID: int = 125
3
+ MIGRATION_FOLDER: str = "migrations"
4
+ MIGRATION_STATE_FILE: str = ".migrations-state.json"
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,9 @@
1
+ from typing import override
2
+
3
+
4
+ class BaseError(Exception):
5
+ """Base error."""
6
+
7
+ @override
8
+ def __init__(self, message: str):
9
+ super().__init__(message)
@@ -0,0 +1,5 @@
1
+ from mpt_tool.managers.file_migration import FileMigrationManager
2
+ from mpt_tool.managers.state.base import StateManager
3
+ from mpt_tool.managers.state.factory import StateManagerFactory
4
+
5
+ __all__ = ["FileMigrationManager", "StateManager", "StateManagerFactory"]
@@ -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