Merging schema_extension #9
@@ -14073,3 +14073,41 @@ IndexError: list index out of range
|
||||
2025-10-15 08:52:56 - root - INFO - Logging to file: alpinebits.log
|
||||
2025-10-15 08:52:56 - root - INFO - Logging configured at INFO level
|
||||
2025-10-15 08:52:58 - alpine_bits_python.email_service - INFO - Email service initialized: smtp.titan.email:465
|
||||
2025-10-16 16:15:42 - root - INFO - Logging to file: alpinebits.log
|
||||
2025-10-16 16:15:42 - root - INFO - Logging configured at INFO level
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - DailyReportScheduler initialized: send_time=08:00, recipients=[]
|
||||
2025-10-16 16:15:42 - root - INFO - Daily report scheduler configured for Pushover (primary worker)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT
|
||||
2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING
|
||||
2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS
|
||||
2025-10-16 16:15:42 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Starting database migrations...
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Running migration: add_room_types
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_type_code (VARCHAR)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_type_code
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_classification_code (VARCHAR)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_classification_code
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Adding column reservations.room_type (VARCHAR)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Successfully added column reservations.room_type
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Migration add_room_types: Added 3 columns
|
||||
2025-10-16 16:15:42 - alpine_bits_python.migrations - INFO - Database migrations completed successfully
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Database tables checked/created at startup.
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - All existing customers already have hashed data
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - ReservationStatsCollector initialized with 4 hotels
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Stats collector initialized and hooked up to report scheduler
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Sending test daily report on startup (last 24 hours)
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Collecting reservation stats from 2025-10-15 16:15:42 to 2025-10-16 16:15:42
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Collected stats: 9 total reservations across 1 hotels
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_service - WARNING - No recipients specified for email: AlpineBits Daily Report - 2025-10-16
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - ERROR - Failed to send test daily report via email on startup
|
||||
2025-10-16 16:15:42 - alpine_bits_python.pushover_service - INFO - Pushover notification sent successfully: AlpineBits Daily Report - 2025-10-16
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Test daily report sent via Pushover successfully on startup
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Daily report scheduler started
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Daily report scheduler started
|
||||
2025-10-16 16:15:42 - alpine_bits_python.api - INFO - Application startup complete
|
||||
2025-10-16 16:15:42 - alpine_bits_python.email_monitoring - INFO - Next daily report scheduled for 2025-10-17 08:00:00 (in 15.7 hours)
|
||||
|
||||
@@ -43,6 +43,7 @@ from .db import Reservation as DBReservation
|
||||
from .email_monitoring import ReservationStatsCollector
|
||||
from .email_service import create_email_service
|
||||
from .logging_config import get_logger, setup_logging
|
||||
from .migrations import run_all_migrations
|
||||
from .notification_adapters import EmailNotificationAdapter, PushoverNotificationAdapter
|
||||
from .notification_service import NotificationService
|
||||
from .pushover_service import create_pushover_service
|
||||
@@ -276,7 +277,13 @@ async def lifespan(app: FastAPI):
|
||||
elif hotel_id and not push_endpoint:
|
||||
_LOGGER.info("Hotel %s has no push_endpoint configured", hotel_id)
|
||||
|
||||
# Create tables
|
||||
# Run database migrations first (only primary worker to avoid race conditions)
|
||||
if is_primary:
|
||||
await run_all_migrations(engine)
|
||||
else:
|
||||
_LOGGER.info("Skipping migrations (non-primary worker)")
|
||||
|
||||
# Create tables (all workers)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
_LOGGER.info("Database tables checked/created at startup.")
|
||||
|
||||
115
src/alpine_bits_python/migrations.py
Normal file
115
src/alpine_bits_python/migrations.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Database migrations for AlpineBits.
|
||||
|
||||
This module contains migration functions that are automatically run at app startup
|
||||
to update existing database schemas without losing data.
|
||||
"""
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from .logging_config import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
async def check_column_exists(engine: AsyncEngine, table_name: str, column_name: str) -> bool:
|
||||
"""Check if a column exists in a table.
|
||||
|
||||
Args:
|
||||
engine: SQLAlchemy async engine
|
||||
table_name: Name of the table to check
|
||||
column_name: Name of the column to check
|
||||
|
||||
Returns:
|
||||
True if column exists, False otherwise
|
||||
"""
|
||||
async with engine.connect() as conn:
|
||||
def _check(connection):
|
||||
inspector = inspect(connection)
|
||||
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||||
return column_name in columns
|
||||
|
||||
result = await conn.run_sync(_check)
|
||||
return result
|
||||
|
||||
|
||||
async def add_column_if_not_exists(
|
||||
engine: AsyncEngine,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
column_type: str = "VARCHAR"
|
||||
) -> bool:
|
||||
"""Add a column to a table if it doesn't already exist.
|
||||
|
||||
Args:
|
||||
engine: SQLAlchemy async engine
|
||||
table_name: Name of the table
|
||||
column_name: Name of the column to add
|
||||
column_type: SQL type of the column (default: VARCHAR)
|
||||
|
||||
Returns:
|
||||
True if column was added, False if it already existed
|
||||
"""
|
||||
exists = await check_column_exists(engine, table_name, column_name)
|
||||
|
||||
if exists:
|
||||
_LOGGER.debug("Column %s.%s already exists, skipping", table_name, column_name)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Adding column %s.%s (%s)", table_name, column_name, column_type)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
|
||||
await conn.execute(text(sql))
|
||||
|
||||
_LOGGER.info("Successfully added column %s.%s", table_name, column_name)
|
||||
return True
|
||||
|
||||
|
||||
async def migrate_add_room_types(engine: AsyncEngine) -> None:
|
||||
"""Migration: Add RoomTypes fields to reservations table.
|
||||
|
||||
This migration adds three optional fields:
|
||||
- room_type_code: String (max 8 chars)
|
||||
- room_classification_code: String (numeric pattern)
|
||||
- room_type: String (enum: 1-5)
|
||||
|
||||
Safe to run multiple times - will skip if columns already exist.
|
||||
"""
|
||||
_LOGGER.info("Running migration: add_room_types")
|
||||
|
||||
added_count = 0
|
||||
|
||||
# Add each column if it doesn't exist
|
||||
if await add_column_if_not_exists(engine, "reservations", "room_type_code", "VARCHAR"):
|
||||
added_count += 1
|
||||
|
||||
if await add_column_if_not_exists(engine, "reservations", "room_classification_code", "VARCHAR"):
|
||||
added_count += 1
|
||||
|
||||
if await add_column_if_not_exists(engine, "reservations", "room_type", "VARCHAR"):
|
||||
added_count += 1
|
||||
|
||||
if added_count > 0:
|
||||
_LOGGER.info("Migration add_room_types: Added %d columns", added_count)
|
||||
else:
|
||||
_LOGGER.info("Migration add_room_types: No changes needed (already applied)")
|
||||
|
||||
|
||||
async def run_all_migrations(engine: AsyncEngine) -> None:
|
||||
"""Run all pending migrations.
|
||||
|
||||
This function should be called at app startup, before Base.metadata.create_all.
|
||||
Each migration function should be idempotent (safe to run multiple times).
|
||||
"""
|
||||
_LOGGER.info("Starting database migrations...")
|
||||
|
||||
try:
|
||||
# Add new migrations here in chronological order
|
||||
await migrate_add_room_types(engine)
|
||||
|
||||
_LOGGER.info("Database migrations completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.exception("Migration failed: %s", e)
|
||||
raise
|
||||
BIN
test_migration.db
Normal file
BIN
test_migration.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user