mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-04-28 11:30:00 +00:00
Some checks failed
Development Build / extract-version (push) Has been cancelled
Tests / Backend Tests (push) Has been cancelled
Tests / Frontend Tests (push) Has been cancelled
Development Build / build-regular (push) Has been cancelled
Development Build / build-single (push) Has been cancelled
Development Build / summary (push) Has been cancelled
* feat(podcasts): integrate model registry for profiles and credential passthrough Replace loose provider/model string fields with record<model> references in podcast profiles, enabling credential passthrough to podcast-creator. Backend: - EpisodeProfile: outline_llm, transcript_llm (record<model>) replace outline_provider/outline_model strings. New language field (BCP 47). - SpeakerProfile: voice_model (record<model>) replaces tts_provider/ tts_model strings. Per-speaker voice_model override support. - Migration 14: schema changes making legacy fields optional, adding new record<model> fields. - Data migration (migration.py): auto-converts legacy profiles to model registry references on startup. Idempotent. - podcast_commands.py: resolves credentials for ALL profiles before calling podcast-creator. - New /api/languages endpoint (pycountry + babel) with BCP 47 locale codes (pt-BR, en-US, etc.). Frontend: - Episode/speaker profile forms use ModelSelector instead of manual provider/model dropdowns. - Language dropdown with BCP 47 codes in episode profile form. - Per-speaker TTS voice model override in speaker profile form. - "Templates" tab renamed to "Profiles". - Setup required badge on unconfigured profiles. - i18n updated across all 8 locales. Closes #486, closes #552 * fix(i18n): remove unused legacy podcast provider/model keys Remove 10 orphaned i18n keys across all 8 locales that were left behind after replacing manual provider/model dropdowns with ModelSelector. * fix: address review violations in podcast model registry - P1: Remove profiles with failed model resolution from dicts to prevent podcast-creator validation errors on unrelated profiles - P2: Use centralized QUERY_KEYS.languages instead of inline key - P3: Fix ISO 639-1 → BCP 47 in model field description and CLAUDE.md - P3: Update "templates" → "profiles" in locale string values (all 8) * chore: bump version to 1.8.0
234 lines
8.3 KiB
Python
234 lines
8.3 KiB
Python
"""
|
|
Async migration system for SurrealDB using the official Python client.
|
|
Based on patterns from sblpy migration system.
|
|
"""
|
|
|
|
from typing import List
|
|
|
|
from loguru import logger
|
|
|
|
from .repository import db_connection, repo_query
|
|
|
|
|
|
class AsyncMigration:
|
|
"""
|
|
Handles individual migration operations with async support.
|
|
"""
|
|
|
|
def __init__(self, sql: str) -> None:
|
|
"""Initialize migration with SQL content."""
|
|
self.sql = sql
|
|
|
|
@classmethod
|
|
def from_file(cls, file_path: str) -> "AsyncMigration":
|
|
"""Create migration from SQL file."""
|
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
raw_content = file.read()
|
|
# Clean up SQL content
|
|
lines = []
|
|
for line in raw_content.split("\n"):
|
|
line = line.strip()
|
|
if line and not line.startswith("--"):
|
|
lines.append(line)
|
|
sql = " ".join(lines)
|
|
return cls(sql)
|
|
|
|
async def run(self, bump: bool = True) -> None:
|
|
"""Run the migration."""
|
|
try:
|
|
async with db_connection() as connection:
|
|
await connection.query(self.sql)
|
|
|
|
if bump:
|
|
await bump_version()
|
|
else:
|
|
await lower_version()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Migration failed: {str(e)}")
|
|
raise
|
|
|
|
|
|
class AsyncMigrationRunner:
|
|
"""
|
|
Handles running multiple migrations in sequence.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
up_migrations: List[AsyncMigration],
|
|
down_migrations: List[AsyncMigration],
|
|
) -> None:
|
|
"""Initialize runner with migration lists."""
|
|
self.up_migrations = up_migrations
|
|
self.down_migrations = down_migrations
|
|
|
|
async def run_all(self) -> None:
|
|
"""Run all pending up migrations."""
|
|
current_version = await get_latest_version()
|
|
|
|
for i in range(current_version, len(self.up_migrations)):
|
|
logger.info(f"Running migration {i + 1}")
|
|
await self.up_migrations[i].run(bump=True)
|
|
|
|
async def run_one_up(self) -> None:
|
|
"""Run one up migration."""
|
|
current_version = await get_latest_version()
|
|
|
|
if current_version < len(self.up_migrations):
|
|
logger.info(f"Running migration {current_version + 1}")
|
|
await self.up_migrations[current_version].run(bump=True)
|
|
|
|
async def run_one_down(self) -> None:
|
|
"""Run one down migration."""
|
|
current_version = await get_latest_version()
|
|
|
|
if current_version > 0:
|
|
logger.info(f"Rolling back migration {current_version}")
|
|
await self.down_migrations[current_version - 1].run(bump=False)
|
|
|
|
|
|
class AsyncMigrationManager:
|
|
"""
|
|
Main migration manager with async support.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize migration manager."""
|
|
self.up_migrations = [
|
|
AsyncMigration.from_file("open_notebook/database/migrations/1.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/2.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/3.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/4.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/5.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/6.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/7.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/8.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/9.surrealql"),
|
|
AsyncMigration.from_file("open_notebook/database/migrations/10.surrealql"),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/11.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/12.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/13.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/14.surrealql"
|
|
),
|
|
]
|
|
self.down_migrations = [
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/1_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/2_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/3_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/4_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/5_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/6_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/7_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/8_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/9_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/10_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/11_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/12_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/13_down.surrealql"
|
|
),
|
|
AsyncMigration.from_file(
|
|
"open_notebook/database/migrations/14_down.surrealql"
|
|
),
|
|
]
|
|
self.runner = AsyncMigrationRunner(
|
|
up_migrations=self.up_migrations,
|
|
down_migrations=self.down_migrations,
|
|
)
|
|
|
|
async def get_current_version(self) -> int:
|
|
"""Get current database version."""
|
|
return await get_latest_version()
|
|
|
|
async def needs_migration(self) -> bool:
|
|
"""Check if migration is needed."""
|
|
current_version = await self.get_current_version()
|
|
return current_version < len(self.up_migrations)
|
|
|
|
async def run_migration_up(self):
|
|
"""Run all pending migrations."""
|
|
current_version = await self.get_current_version()
|
|
logger.info(f"Current version before migration: {current_version}")
|
|
|
|
if await self.needs_migration():
|
|
try:
|
|
await self.runner.run_all()
|
|
new_version = await self.get_current_version()
|
|
logger.info(f"Migration successful. New version: {new_version}")
|
|
except Exception as e:
|
|
logger.error(f"Migration failed: {str(e)}")
|
|
raise
|
|
else:
|
|
logger.info("Database is already at the latest version")
|
|
|
|
|
|
# Database version management functions
|
|
async def get_latest_version() -> int:
|
|
"""Get the latest version from the migrations table."""
|
|
try:
|
|
versions = await get_all_versions()
|
|
if not versions:
|
|
return 0
|
|
return max(version["version"] for version in versions)
|
|
except Exception:
|
|
# If migrations table doesn't exist, we're at version 0
|
|
return 0
|
|
|
|
|
|
async def get_all_versions() -> List[dict]:
|
|
"""Get all versions from the migrations table."""
|
|
try:
|
|
result = await repo_query("SELECT * FROM _sbl_migrations ORDER BY version;")
|
|
return result
|
|
except Exception:
|
|
# If table doesn't exist, return empty list
|
|
return []
|
|
|
|
|
|
async def bump_version() -> None:
|
|
"""Bump the version by adding a new entry to migrations table."""
|
|
current_version = await get_latest_version()
|
|
new_version = current_version + 1
|
|
|
|
await repo_query(
|
|
f"CREATE _sbl_migrations:{new_version} SET version = {new_version}, applied_at = time::now();",
|
|
)
|
|
|
|
|
|
async def lower_version() -> None:
|
|
"""Lower the version by removing the latest entry from migrations table."""
|
|
current_version = await get_latest_version()
|
|
if current_version > 0:
|
|
await repo_query(f"DELETE _sbl_migrations:{current_version};")
|