Alembic experiments
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"""Service for handling conversion data from hotel PMS XML files."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@@ -11,7 +10,14 @@ from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from .db import Conversion, RoomReservation, Customer, HashedCustomer, Reservation, SessionMaker
|
||||
from .db import (
|
||||
Conversion,
|
||||
ConversionRoom,
|
||||
Customer,
|
||||
HashedCustomer,
|
||||
Reservation,
|
||||
SessionMaker,
|
||||
)
|
||||
from .logging_config import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
@@ -45,17 +51,23 @@ class ConversionService:
|
||||
# Cache for reservation and customer data within a single XML processing run
|
||||
# Maps hotel_code -> list of (reservation, customer) tuples
|
||||
# This significantly speeds up matching when processing large XML files
|
||||
self._reservation_cache: dict[str | None, list[tuple[Reservation, Customer | None]]] = {}
|
||||
self._reservation_cache: dict[
|
||||
str | None, list[tuple[Reservation, Customer | None]]
|
||||
] = {}
|
||||
self._cache_initialized = False
|
||||
|
||||
if isinstance(session, SessionMaker):
|
||||
self.session_maker = session
|
||||
self.supports_concurrent = True
|
||||
_LOGGER.info("ConversionService initialized in concurrent mode with SessionMaker")
|
||||
_LOGGER.info(
|
||||
"ConversionService initialized in concurrent mode with SessionMaker"
|
||||
)
|
||||
elif isinstance(session, AsyncSession):
|
||||
self.session = session
|
||||
self.supports_concurrent = False
|
||||
_LOGGER.info("ConversionService initialized in sequential mode with single session")
|
||||
_LOGGER.info(
|
||||
"ConversionService initialized in sequential mode with single session"
|
||||
)
|
||||
elif session is not None:
|
||||
raise TypeError(
|
||||
f"session must be AsyncSession or SessionMaker, got {type(session)}"
|
||||
@@ -202,9 +214,7 @@ class ConversionService:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for reservation in reservations:
|
||||
tg.create_task(
|
||||
self._process_reservation_safe(
|
||||
reservation, semaphore, stats
|
||||
)
|
||||
self._process_reservation_safe(reservation, semaphore, stats)
|
||||
)
|
||||
|
||||
async def _process_reservations_concurrent(
|
||||
@@ -227,9 +237,7 @@ class ConversionService:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for reservation in reservations:
|
||||
tg.create_task(
|
||||
self._process_reservation_safe(
|
||||
reservation, semaphore, stats
|
||||
)
|
||||
self._process_reservation_safe(reservation, semaphore, stats)
|
||||
)
|
||||
|
||||
async def _process_reservation_safe(
|
||||
@@ -247,6 +255,7 @@ class ConversionService:
|
||||
reservation_elem: XML element for the reservation
|
||||
semaphore: Semaphore to limit concurrent operations
|
||||
stats: Shared stats dictionary (thread-safe due to GIL)
|
||||
|
||||
"""
|
||||
pms_reservation_id = reservation_elem.get("id")
|
||||
|
||||
@@ -295,18 +304,19 @@ class ConversionService:
|
||||
if self.session_maker:
|
||||
await session.close()
|
||||
|
||||
async def _handle_deleted_reservation(self, pms_reservation_id: str, session: AsyncSession):
|
||||
async def _handle_deleted_reservation(
|
||||
self, pms_reservation_id: str, session: AsyncSession
|
||||
):
|
||||
"""Handle deleted reservation by marking conversions as deleted or removing them.
|
||||
|
||||
Args:
|
||||
pms_reservation_id: PMS reservation ID to delete
|
||||
session: AsyncSession to use for the operation
|
||||
|
||||
"""
|
||||
# For now, we'll just log it. You could add a 'deleted' flag to the Conversion table
|
||||
# or actually delete the conversion records
|
||||
_LOGGER.info(
|
||||
"Processing deleted reservation: PMS ID %s", pms_reservation_id
|
||||
)
|
||||
_LOGGER.info("Processing deleted reservation: PMS ID %s", pms_reservation_id)
|
||||
|
||||
# Option 1: Delete conversion records
|
||||
result = await session.execute(
|
||||
@@ -337,6 +347,7 @@ class ConversionService:
|
||||
In concurrent mode, each task passes its own session.
|
||||
|
||||
Returns statistics about what was matched.
|
||||
|
||||
"""
|
||||
if session is None:
|
||||
session = self.session
|
||||
@@ -394,9 +405,7 @@ class ConversionService:
|
||||
creation_time_str.replace("Z", "+00:00")
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Invalid creation time format: %s", creation_time_str
|
||||
)
|
||||
_LOGGER.warning("Invalid creation time format: %s", creation_time_str)
|
||||
|
||||
# Find matching reservation, customer, and hashed_customer using advertising data and guest details
|
||||
matched_reservation = None
|
||||
@@ -515,18 +524,15 @@ class ConversionService:
|
||||
|
||||
# Batch-load existing room reservations to avoid N+1 queries
|
||||
room_numbers = [
|
||||
rm.get("roomNumber")
|
||||
for rm in room_reservations.findall("roomReservation")
|
||||
rm.get("roomNumber") for rm in room_reservations.findall("roomReservation")
|
||||
]
|
||||
pms_hotel_reservation_ids = [
|
||||
f"{pms_reservation_id}_{room_num}" for room_num in room_numbers
|
||||
]
|
||||
|
||||
existing_rooms_result = await session.execute(
|
||||
select(RoomReservation).where(
|
||||
RoomReservation.pms_hotel_reservation_id.in_(
|
||||
pms_hotel_reservation_ids
|
||||
)
|
||||
select(ConversionRoom).where(
|
||||
ConversionRoom.pms_hotel_reservation_id.in_(pms_hotel_reservation_ids)
|
||||
)
|
||||
)
|
||||
existing_rooms = {
|
||||
@@ -556,9 +562,7 @@ class ConversionService:
|
||||
departure_date = None
|
||||
if departure_str:
|
||||
try:
|
||||
departure_date = datetime.strptime(
|
||||
departure_str, "%Y-%m-%d"
|
||||
).date()
|
||||
departure_date = datetime.strptime(departure_str, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid departure date format: %s", departure_str)
|
||||
|
||||
@@ -576,7 +580,7 @@ class ConversionService:
|
||||
# Process daily sales and extract total revenue
|
||||
daily_sales_elem = room_reservation.find("dailySales")
|
||||
daily_sales_list = []
|
||||
total_revenue = Decimal("0")
|
||||
total_revenue = Decimal(0)
|
||||
|
||||
if daily_sales_elem is not None:
|
||||
for daily_sale in daily_sales_elem.findall("dailySale"):
|
||||
@@ -642,7 +646,7 @@ class ConversionService:
|
||||
)
|
||||
else:
|
||||
# Create new room reservation
|
||||
room_reservation_record = RoomReservation(
|
||||
room_reservation_record = ConversionRoom(
|
||||
conversion_id=conversion.id,
|
||||
pms_hotel_reservation_id=pms_hotel_reservation_id,
|
||||
arrival_date=arrival_date,
|
||||
@@ -734,7 +738,9 @@ class ConversionService:
|
||||
)
|
||||
|
||||
# Strategy 2: If no advertising match, try email/name-based matching
|
||||
if not result["reservation"] and (guest_email or guest_first_name or guest_last_name):
|
||||
if not result["reservation"] and (
|
||||
guest_email or guest_first_name or guest_last_name
|
||||
):
|
||||
matched_reservation = await self._match_by_guest_details(
|
||||
hotel_id, guest_first_name, guest_last_name, guest_email, session
|
||||
)
|
||||
@@ -798,6 +804,7 @@ class ConversionService:
|
||||
|
||||
Returns:
|
||||
Matched Reservation or None
|
||||
|
||||
"""
|
||||
if session is None:
|
||||
session = self.session
|
||||
@@ -882,6 +889,7 @@ class ConversionService:
|
||||
|
||||
Returns:
|
||||
Matched Reservation or None
|
||||
|
||||
"""
|
||||
if session is None:
|
||||
session = self.session
|
||||
@@ -892,9 +900,7 @@ class ConversionService:
|
||||
|
||||
# Get reservations from cache for this hotel
|
||||
if hotel_id and hotel_id in self._reservation_cache:
|
||||
all_reservations = [
|
||||
res for res, _ in self._reservation_cache[hotel_id]
|
||||
]
|
||||
all_reservations = [res for res, _ in self._reservation_cache[hotel_id]]
|
||||
elif not hotel_id:
|
||||
# If no hotel_id specified, use all cached reservations
|
||||
for reservations_list in self._reservation_cache.values():
|
||||
@@ -947,6 +953,7 @@ class ConversionService:
|
||||
|
||||
Returns:
|
||||
Matched Reservation or None
|
||||
|
||||
"""
|
||||
# Filter by guest details
|
||||
candidates = []
|
||||
@@ -1019,6 +1026,7 @@ class ConversionService:
|
||||
|
||||
Returns:
|
||||
Single best-match Reservation, or None if no good match found
|
||||
|
||||
"""
|
||||
candidates = reservations
|
||||
|
||||
|
||||
Reference in New Issue
Block a user