Init project

This commit is contained in:
2026-01-15 09:47:43 +03:00
commit 3e515c66ec
59 changed files with 5101 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import importlib.util
import pathlib
from types import ModuleType
from typing import Generator, Iterable
from alembic import command as alembic_command
from alembic.config import Config as AlembicConfig
from birthday_pool_bot.repositories import RepositorySettings
class Migrator:
MIGRATION_FILENAME_TEMPLATE = (
"%%(year)d_%%(month)02d_%%(day)02d_%%(hour)02dh%%(minute)02dm%%(second)02ds_%%(slug)s"
)
def __init__(self, settings: RepositorySettings):
self._settings = settings
self._alembic_config = self._generate_alembic_config()
def _generate_alembic_config(self) -> AlembicConfig:
migrations_path = self.get_migrations_directory_path()
config = AlembicConfig()
config.set_main_option("script_location", str(migrations_path))
config.set_main_option("file_template", self.MIGRATION_FILENAME_TEMPLATE)
config.set_main_option("sqlalchemy.url", str(self._settings.dsn).replace("%", "%%"))
return config
def squash_migrations(
self,
new_migration_id: str | None = None,
**kwargs,
) -> str:
new_message = kwargs.get("new_message", None)
if new_migration_id is None:
last_migration_id = self.get_latest_migration()
new_migration_id = last_migration_id
self.rollback(migration_id="base")
self._delete_all_migrations()
new_migration_id = self.create_migration(
migration_id=new_migration_id,
message=new_message,
)
return new_migration_id
def migrate(self, migration_id: str | None = None, **kwargs) -> str | None:
alembic_revision = migration_id or "head"
alembic_command.upgrade(config=self._alembic_config, revision=alembic_revision)
return self.get_current_migration()
def rollback(self, migration_id: str | None = None, **kwargs) -> str | None:
alembic_revision = migration_id or "-1"
alembic_command.downgrade(config=self._alembic_config, revision=alembic_revision)
return self.get_current_migration()
def get_current_migration(self) -> str | None:
return None
def create_migration(
self,
migration_id: str | None = None,
**kwargs,
) -> str:
message = kwargs.get("message", None)
alembic_command.revision(
self._alembic_config,
message=message,
rev_id=migration_id,
autogenerate=True,
)
last_migration_id = self.get_latest_migration()
if last_migration_id is None:
raise RuntimeError("Migration not created")
return last_migration_id
def get_latest_migration(self) -> str | None:
last_migration_id = None
for migration_id in self.get_migrations():
last_migration_id = migration_id
return last_migration_id
def get_migrations(self) -> Generator[str, None, None]:
migrations_modules = self._get_migrations_modules()
sorted_migrations_modules = self._sort_migrations_modules(modules=migrations_modules)
for migration_module in sorted_migrations_modules:
yield migration_module.revision
def _get_migrations_modules(self) -> Iterable[ModuleType]:
migrations_path = self.get_migrations_directory_path()
versions_path = migrations_path / "versions"
for index, migration_module_path in enumerate(versions_path.glob("*.py")):
migration_module_name = f"migration_{index:08}"
migration_module_spec = importlib.util.spec_from_file_location(
name=migration_module_name,
location=migration_module_path,
)
if migration_module_spec is None or migration_module_spec.loader is None:
raise ValueError(f"Cannot load migration module '{migration_module_path}'")
migration_module = importlib.util.module_from_spec(spec=migration_module_spec)
migration_module_spec.loader.exec_module(migration_module)
yield migration_module
def _sort_migrations_modules(self, modules: Iterable[ModuleType]) -> list[ModuleType]:
next_module_map = {}
first_module = None
for module in modules:
if module.down_revision is None:
if first_module is not None:
raise ValueError((
"Found multiple first migrations: "
f"'{module.revision}' and '{first_module.revision}'"
))
first_module = module
else:
next_module_map[module.down_revision] = module
if first_module is None:
raise ValueError("Doesn't found first migration")
current_module = first_module
sorted_modules = [current_module]
while next_module_map:
current_module = next_module_map.pop(current_module.revision)
sorted_modules.append(current_module)
return sorted_modules
def _delete_all_migrations(self):
migrations_path = self.get_migrations_directory_path()
versions_path = migrations_path / "versions"
for migration_file_path in versions_path.glob("*.py"):
migration_file_path.unlink()
def get_migrations_directory_path(self) -> pathlib.Path:
directory_path = pathlib.Path(__file__).parent
return directory_path