ytptube/app/features/conditions/migration.py
2026-05-28 19:07:11 +03:00

118 lines
4.4 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
from app.features.conditions.models import ConditionModel
from app.features.core.migration import Migration as FeatureMigration
from app.library.config import Config
from app.library.log import get_logger
if TYPE_CHECKING:
from app.features.conditions.repository import ConditionsRepository
LOG = get_logger()
class Migration(FeatureMigration):
name: str = "conditions"
def __init__(self, repo: ConditionsRepository, config: Config | None = None):
self._config: Config = config or Config.get_instance()
super().__init__(config=self._config)
self._repo: ConditionsRepository = repo
self._source_file: Path = Path(self._config.config_path) / "conditions.json"
async def should_run(self) -> bool:
return self._source_file.exists()
async def migrate(self) -> None:
if await self._repo.count() > 0:
LOG.warning("Conditions already exist in the database; skipping migration.")
await self._move_file(self._source_file)
return
try:
items: list[dict] | None = json.loads(self._source_file.read_text())
except Exception as exc:
LOG.exception(
"Failed to read conditions migration file '%s'. Ignoring.",
self._source_file,
extra={"file_path": str(self._source_file), "exception_type": type(exc).__name__},
)
await self._move_file(self._source_file)
return
if items is None:
LOG.warning("No conditions found in %s; skipping migration.", self._source_file)
await self._move_file(self._source_file)
return
inserted = 0
seen_names: dict[str, int] = {}
for index, item in enumerate(items):
if not (normalized := await self._normalize(item, index, seen_names)):
continue
try:
await self._repo.create(normalized)
inserted += 1
except Exception as exc:
LOG.exception(
"Failed to insert condition '%s'.",
normalized.name,
extra={"condition_name": normalized.name, "exception_type": type(exc).__name__},
)
LOG.info("Migrated %s condition(s) from %s.", inserted, self._source_file)
await self._move_file(self._source_file)
async def _normalize(self, item: Any, index: int, seen_names: dict[str, int]) -> ConditionModel | None:
if not isinstance(item, dict):
LOG.warning("Skipping condition at index %s due to invalid type.", index)
return None
assert isinstance(item, dict)
name: str | None = item.get("name")
if not name or not isinstance(name, str):
LOG.warning("Skipping condition at index %s due to missing name.", index)
return None
normalized_name = name.strip()
if not normalized_name:
LOG.warning("Skipping condition at index %s due to empty name.", index)
return None
name = self._unique_name(normalized_name, seen_names)
extras = item.get("extras") if isinstance(item.get("extras"), dict) else {}
extras = cast("dict[str, Any]", extras)
filter_value: str | None = item.get("filter")
if (not filter_value or not isinstance(filter_value, str)) and len(extras) == 0:
LOG.warning("Skipping condition '%s' due to missing filter.", name)
return None
cli: str | None = item.get("cli")
if not isinstance(cli, str):
cli = ""
enabled_value = item.get("enabled")
enabled: bool = enabled_value if isinstance(enabled_value, bool) else True
priority_value = item.get("priority")
priority: int = priority_value if isinstance(priority_value, int) else 0
if isinstance(priority, bool) or 0 > priority:
priority = 0
description_value = item.get("description")
description: str = description_value if isinstance(description_value, str) else ""
return ConditionModel(
id=index,
name=name,
filter=filter_value,
cli=cli,
extras=extras,
enabled=bool(enabled),
priority=priority,
description=description,
)