Merging schema_extension #9

Merged
jonas merged 15 commits from schema_extension into main 2025-10-20 07:19:26 +00:00
5 changed files with 239 additions and 0 deletions
Showing only changes of commit 063ae3277f - Show all commits

View File

@@ -14059,3 +14059,87 @@ IndexError: list index out of range
2025-10-10 10:59:53 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured 2025-10-10 10:59:53 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured
2025-10-10 10:59:53 - alpine_bits_python.api - INFO - Database tables checked/created at startup. 2025-10-10 10:59:53 - alpine_bits_python.api - INFO - Database tables checked/created at startup.
2025-10-10 10:59:53 - httpx - INFO - HTTP Request: PUT http://testserver/api/hoteldata/conversions_import/test_reservation.xml "HTTP/1.1 401 Unauthorized" 2025-10-10 10:59:53 - httpx - INFO - HTTP Request: PUT http://testserver/api/hoteldata/conversions_import/test_reservation.xml "HTTP/1.1 401 Unauthorized"
2025-10-16 15:34:21 - root - INFO - Logging to file: alpinebits.log
2025-10-16 15:34:21 - root - INFO - Logging configured at INFO level
2025-10-16 15:34:21 - __main__ - INFO - ============================================================
2025-10-16 15:34:21 - __main__ - INFO - Starting RoomTypes Migration
2025-10-16 15:34:21 - __main__ - INFO - ============================================================
2025-10-16 15:34:21 - __main__ - INFO - Database URL: /alpinebits.db
2025-10-16 15:34:21 - __main__ - INFO - Checking which columns need to be added to reservations table...
2025-10-16 15:34:21 - __main__ - ERROR - Migration failed: 'Connection' object has no attribute 'sync_connection'
Traceback (most recent call last):
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 97, in main
await add_room_types_columns(engine)
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 53, in add_room_types_columns
columns_exist = await check_columns_exist(engine, table_name, columns_to_add)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 41, in check_columns_exist
result = await conn.run_sync(_check)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py", line 887, in run_sync
return await greenlet_spawn(
^^^^^^^^^^^^^^^^^^^^^
fn, self._proxied, *arg, _require_await=False, **kw
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 190, in greenlet_spawn
result = context.switch(*args, **kwargs)
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 37, in _check
inspector = inspect(connection.sync_connection)
^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Connection' object has no attribute 'sync_connection'
2025-10-16 15:34:36 - root - INFO - Logging to file: alpinebits.log
2025-10-16 15:34:36 - root - INFO - Logging configured at INFO level
2025-10-16 15:34:36 - __main__ - INFO - ============================================================
2025-10-16 15:34:36 - __main__ - INFO - Starting RoomTypes Migration
2025-10-16 15:34:36 - __main__ - INFO - ============================================================
2025-10-16 15:34:36 - __main__ - INFO - Database URL: /alpinebits.db
2025-10-16 15:34:36 - __main__ - INFO - Checking which columns need to be added to reservations table...
2025-10-16 15:34:36 - __main__ - ERROR - Migration failed: reservations
Traceback (most recent call last):
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 97, in main
await add_room_types_columns(engine)
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 53, in add_room_types_columns
columns_exist = await check_columns_exist(engine, table_name, columns_to_add)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 41, in check_columns_exist
result = await conn.run_sync(_check)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py", line 887, in run_sync
return await greenlet_spawn(
^^^^^^^^^^^^^^^^^^^^^
fn, self._proxied, *arg, _require_await=False, **kw
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 203, in greenlet_spawn
result = context.switch(value)
File "/home/divusjulius/repos/alpinebits_python/src/alpine_bits_python/util/migrate_add_room_types.py", line 38, in _check
existing_cols = [col['name'] for col in inspector.get_columns(table_name)]
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py", line 869, in get_columns
col_defs = self.dialect.get_columns(
conn, table_name, schema, info_cache=self.info_cache, **kw
)
File "<string>", line 2, in get_columns
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py", line 106, in cache
ret = fn(self, con, *args, **kw)
File "/home/divusjulius/repos/alpinebits_python/.venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/base.py", line 2412, in get_columns
raise exc.NoSuchTableError(
f"{schema}.{table_name}" if schema else table_name
)
sqlalchemy.exc.NoSuchTableError: reservations
2025-10-16 15:34:52 - root - INFO - Logging to file: alpinebits.log
2025-10-16 15:34:52 - root - INFO - Logging configured at INFO level
2025-10-16 15:34:52 - __main__ - INFO - ============================================================
2025-10-16 15:34:52 - __main__ - INFO - Starting RoomTypes Migration
2025-10-16 15:34:52 - __main__ - INFO - ============================================================
2025-10-16 15:34:52 - __main__ - INFO - Database URL: /alpinebits.db
2025-10-16 15:34:52 - __main__ - INFO - Ensuring database tables exist...
2025-10-16 15:34:52 - __main__ - INFO - Database tables checked/created.
2025-10-16 15:34:52 - __main__ - INFO - Checking which columns need to be added to reservations table...
2025-10-16 15:34:52 - __main__ - INFO - All RoomTypes columns already exist in reservations table. No migration needed.
2025-10-16 15:34:52 - __main__ - INFO - ============================================================
2025-10-16 15:34:52 - __main__ - INFO - Migration completed successfully!
2025-10-16 15:34:52 - __main__ - INFO - ============================================================

View File

@@ -25,6 +25,7 @@ from .generated.alpinebits import (
OtaHotelResNotifRq, OtaHotelResNotifRq,
OtaResRetrieveRs, OtaResRetrieveRs,
ProfileProfileType, ProfileProfileType,
RoomTypeRoomType,
UniqueIdType2, UniqueIdType2,
) )
@@ -76,6 +77,13 @@ RetrieveRoomStays = OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays
NotifHotelReservation = OtaHotelResNotifRq.HotelReservations.HotelReservation NotifHotelReservation = OtaHotelResNotifRq.HotelReservations.HotelReservation
RetrieveHotelReservation = OtaResRetrieveRs.ReservationsList.HotelReservation RetrieveHotelReservation = OtaResRetrieveRs.ReservationsList.HotelReservation
NotifRoomTypes = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays.RoomStay.RoomTypes
)
RetrieveRoomTypes = (
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.RoomTypes
)
from .const import RESERVATION_ID_TYPE from .const import RESERVATION_ID_TYPE
@@ -697,9 +705,29 @@ def _process_single_reservation(
start=reservation.start_date.isoformat() if reservation.start_date else None, start=reservation.start_date.isoformat() if reservation.start_date else None,
end=reservation.end_date.isoformat() if reservation.end_date else None, end=reservation.end_date.isoformat() if reservation.end_date else None,
) )
# RoomTypes (optional) - only create if at least one field is present
room_types = None
if any([reservation.room_type_code, reservation.room_classification_code, reservation.room_type]):
# Convert room_type string to enum if present
room_type_enum = None
if reservation.room_type:
room_type_enum = RoomTypeRoomType(reservation.room_type)
# Create RoomType instance
room_type_obj = RoomStays.RoomStay.RoomTypes.RoomType(
room_type_code=reservation.room_type_code,
room_classification_code=reservation.room_classification_code,
room_type=room_type_enum,
)
# Create RoomTypes container
room_types = RoomStays.RoomStay.RoomTypes(room_type=room_type_obj)
room_stay = RoomStays.RoomStay( room_stay = RoomStays.RoomStay(
time_span=time_span, time_span=time_span,
guest_counts=guest_counts, guest_counts=guest_counts,
room_types=room_types,
) )
room_stays = RoomStays( room_stays = RoomStays(
room_stay=[room_stay], room_stay=[room_stay],

View File

@@ -127,6 +127,10 @@ class Reservation(Base):
# Add hotel_code and hotel_name for XML # Add hotel_code and hotel_name for XML
hotel_code = Column(String) hotel_code = Column(String)
hotel_name = Column(String) hotel_name = Column(String)
# RoomTypes fields (optional)
room_type_code = Column(String)
room_classification_code = Column(String)
room_type = Column(String)
customer = relationship("Customer", back_populates="reservations") customer = relationship("Customer", back_populates="reservations")

View File

@@ -58,6 +58,10 @@ class ReservationData(BaseModel):
utm_campaign: str | None = Field(None, max_length=150) utm_campaign: str | None = Field(None, max_length=150)
utm_term: str | None = Field(None, max_length=150) utm_term: str | None = Field(None, max_length=150)
utm_content: str | None = Field(None, max_length=150) utm_content: str | None = Field(None, max_length=150)
# RoomTypes fields (optional)
room_type_code: str | None = Field(None, min_length=1, max_length=8)
room_classification_code: str | None = Field(None, pattern=r"[0-9]+")
room_type: str | None = Field(None, pattern=r"^[1-5]$")
@model_validator(mode="after") @model_validator(mode="after")
def ensure_md5(self) -> "ReservationData": def ensure_md5(self) -> "ReservationData":

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Migration script to add RoomTypes fields to Reservation table.
This migration adds three optional fields to the reservations table:
- room_type_code: String (max 8 chars)
- room_classification_code: String (numeric pattern)
- room_type: String (enum: 1-5)
This script can be run manually before starting the server, or the changes
will be applied automatically when the server starts via Base.metadata.create_all.
"""
import asyncio
import sys
from pathlib import Path
# Add parent directory to path so we can import alpine_bits_python
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import create_async_engine
from alpine_bits_python.config_loader import load_config
from alpine_bits_python.db import get_database_url
from alpine_bits_python.logging_config import get_logger, setup_logging
_LOGGER = get_logger(__name__)
async def check_columns_exist(engine, table_name: str, columns: list[str]) -> dict[str, bool]:
"""Check which columns exist in the table.
Returns a dict mapping column name to whether it exists.
"""
async with engine.connect() as conn:
def _check(connection):
inspector = inspect(connection)
existing_cols = [col['name'] for col in inspector.get_columns(table_name)]
return {col: col in existing_cols for col in columns}
result = await conn.run_sync(_check)
return result
async def add_room_types_columns(engine):
"""Add RoomTypes columns to reservations table if they don't exist."""
from alpine_bits_python.db import Base
table_name = "reservations"
columns_to_add = ["room_type_code", "room_classification_code", "room_type"]
# First, ensure the table exists by creating all tables if needed
_LOGGER.info("Ensuring database tables exist...")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
_LOGGER.info("Database tables checked/created.")
_LOGGER.info("Checking which columns need to be added to %s table...", table_name)
# Check which columns already exist
columns_exist = await check_columns_exist(engine, table_name, columns_to_add)
columns_to_create = [col for col, exists in columns_exist.items() if not exists]
if not columns_to_create:
_LOGGER.info("All RoomTypes columns already exist in %s table. No migration needed.", table_name)
return
_LOGGER.info("Adding columns to %s table: %s", table_name, ", ".join(columns_to_create))
# Build ALTER TABLE statements for missing columns
# Note: SQLite supports ALTER TABLE ADD COLUMN but not ADD MULTIPLE COLUMNS
async with engine.begin() as conn:
for column in columns_to_create:
sql = f"ALTER TABLE {table_name} ADD COLUMN {column} VARCHAR"
_LOGGER.info("Executing: %s", sql)
await conn.execute(text(sql))
_LOGGER.info("Successfully added %d columns to %s table", len(columns_to_create), table_name)
async def main():
"""Run the migration."""
try:
# Load config
config = load_config()
setup_logging(config)
except Exception as e:
_LOGGER.warning("Failed to load config: %s. Using defaults.", e)
config = {}
_LOGGER.info("=" * 60)
_LOGGER.info("Starting RoomTypes Migration")
_LOGGER.info("=" * 60)
# Get database URL
database_url = get_database_url(config)
_LOGGER.info("Database URL: %s", database_url.replace("://", "://***:***@").split("@")[-1])
# Create engine
engine = create_async_engine(database_url, echo=False)
try:
# Run migration
await add_room_types_columns(engine)
_LOGGER.info("=" * 60)
_LOGGER.info("Migration completed successfully!")
_LOGGER.info("=" * 60)
except Exception as e:
_LOGGER.exception("Migration failed: %s", e)
sys.exit(1)
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())