Added addittonal section to the schema. Can now add RoomTypes but they are optional
This commit is contained in:
@@ -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 - ============================================================
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
119
src/alpine_bits_python/util/migrate_add_room_types.py
Normal file
119
src/alpine_bits_python/util/migrate_add_room_types.py
Normal 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())
|
||||||
Reference in New Issue
Block a user