Starting to implement action_OTA_HotelResNotif_GuestRequests. Necessary to fully comply with spec
This commit is contained in:
@@ -12,7 +12,7 @@ import difflib
|
||||
import json
|
||||
import inspect
|
||||
import re
|
||||
from typing import Dict, List, Optional, Any, Union, Tuple, Type
|
||||
from typing import Dict, List, Optional, Any, Union, Tuple, Type, override
|
||||
from xml.etree import ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, IntEnum
|
||||
@@ -20,7 +20,6 @@ from enum import Enum, IntEnum
|
||||
from alpine_bits_python.alpine_bits_helpers import PhoneTechType, create_xml_from_db
|
||||
|
||||
|
||||
|
||||
from .generated.alpinebits import OtaPingRq, OtaPingRs, WarningStatus, OtaReadRq
|
||||
from xsdata_pydantic.bindings import XmlSerializer
|
||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||
@@ -37,7 +36,6 @@ logging.basicConfig(level=logging.INFO)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class HttpStatusCode(IntEnum):
|
||||
"""Allowed HTTP status codes for AlpineBits responses."""
|
||||
|
||||
@@ -114,6 +112,15 @@ class Version(str, Enum):
|
||||
# Add other versions as needed
|
||||
|
||||
|
||||
class AlpineBitsClientInfo:
|
||||
"""Wrapper for username, password, client_id"""
|
||||
|
||||
def __init__(self, username: str, password: str, client_id: str | None = None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.client_id = client_id
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlpineBitsResponse:
|
||||
"""Response data structure for AlpineBits actions."""
|
||||
@@ -139,7 +146,13 @@ class AlpineBitsAction(ABC):
|
||||
) # list of versions in case action supports multiple versions
|
||||
|
||||
async def handle(
|
||||
self, action: str, request_xml: str, version: Version, dbsession=None, server_capabilities=None, username=None, password=None, config: Dict = None
|
||||
self,
|
||||
action: str,
|
||||
request_xml: str,
|
||||
version: Version,
|
||||
client_info: AlpineBitsClientInfo,
|
||||
dbsession=None,
|
||||
server_capabilities=None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""
|
||||
Handle the incoming request XML and return response XML.
|
||||
@@ -268,7 +281,7 @@ class ServerCapabilities:
|
||||
class PingAction(AlpineBitsAction):
|
||||
"""Implementation for OTA_Ping action (handshaking)."""
|
||||
|
||||
def __init__(self, config: Dict = None):
|
||||
def __init__(self, config: Dict = {}):
|
||||
self.name = AlpineBitsActionName.OTA_PING
|
||||
self.version = [
|
||||
Version.V2024_10,
|
||||
@@ -276,11 +289,13 @@ class PingAction(AlpineBitsAction):
|
||||
] # Supports multiple versions
|
||||
self.config = config
|
||||
|
||||
@override
|
||||
async def handle(
|
||||
self,
|
||||
action: str,
|
||||
request_xml: str,
|
||||
version: Version,
|
||||
client_info: AlpineBitsClientInfo,
|
||||
server_capabilities: None | ServerCapabilities = None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""Handle ping requests."""
|
||||
@@ -352,7 +367,7 @@ class PingAction(AlpineBitsAction):
|
||||
capabilities_json = json.dumps(matching_capabilities, indent=2)
|
||||
|
||||
warning = OtaPingRs.Warnings.Warning(
|
||||
status=WarningStatus.ALPINEBITS_HANDSHAKE.value,
|
||||
status=WarningStatus.ALPINEBITS_HANDSHAKE,
|
||||
type_value="11",
|
||||
content=[capabilities_json],
|
||||
)
|
||||
@@ -379,19 +394,24 @@ class PingAction(AlpineBitsAction):
|
||||
)
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
def strip_control_chars(s):
|
||||
# Remove all control characters (ASCII < 32 and DEL)
|
||||
return re.sub(r'[\x00-\x1F\x7F]', '', s)
|
||||
return re.sub(r"[\x00-\x1F\x7F]", "", s)
|
||||
|
||||
def validate_hotel_authentication(username: str, password: str, hotelid: str, config: Dict) -> bool:
|
||||
""" Validate hotel authentication based on username, password, and hotel ID.
|
||||
|
||||
Example config
|
||||
alpine_bits_auth:
|
||||
- hotel_id: "123"
|
||||
hotel_name: "Frangart Inn"
|
||||
username: "alice"
|
||||
password: !secret ALICE_PASSWORD
|
||||
def validate_hotel_authentication(
|
||||
username: str, password: str, hotelid: str, config: Dict
|
||||
) -> bool:
|
||||
"""Validate hotel authentication based on username, password, and hotel ID.
|
||||
|
||||
Example config
|
||||
alpine_bits_auth:
|
||||
- hotel_id: "123"
|
||||
hotel_name: "Frangart Inn"
|
||||
username: "alice"
|
||||
password: !secret ALICE_PASSWORD
|
||||
"""
|
||||
|
||||
if not config or "alpine_bits_auth" not in config:
|
||||
@@ -409,20 +429,22 @@ def validate_hotel_authentication(username: str, password: str, hotelid: str, co
|
||||
# look for hotelid in config
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ReadAction(AlpineBitsAction):
|
||||
"""Implementation for OTA_Read action."""
|
||||
|
||||
def __init__(self, config: Dict = None):
|
||||
def __init__(self, config: Dict = {}):
|
||||
self.name = AlpineBitsActionName.OTA_READ
|
||||
self.version = [Version.V2024_10, Version.V2022_10]
|
||||
self.config = config
|
||||
|
||||
async def handle(
|
||||
self, action: str, request_xml: str, version: Version, dbsession=None, username=None, password=None
|
||||
self,
|
||||
action: str,
|
||||
request_xml: str,
|
||||
version: Version,
|
||||
client_info: AlpineBitsClientInfo,
|
||||
dbsession=None,
|
||||
server_capabilities=None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""Handle read requests."""
|
||||
|
||||
@@ -430,16 +452,16 @@ class ReadAction(AlpineBitsAction):
|
||||
clean_expected = strip_control_chars(self.name.value[1]).strip()
|
||||
|
||||
if clean_action != clean_expected:
|
||||
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Invalid action {action}, expected {self.name.value[1]}", HttpStatusCode.BAD_REQUEST
|
||||
f"Error: Invalid action {action}, expected {self.name.value[1]}",
|
||||
HttpStatusCode.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if dbsession is None:
|
||||
return AlpineBitsResponse(
|
||||
"Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
read_request = XmlParser().from_string(request_xml, OtaReadRq)
|
||||
|
||||
hotel_read_request = read_request.read_requests.hotel_read_request
|
||||
@@ -450,22 +472,24 @@ class ReadAction(AlpineBitsAction):
|
||||
if hotelname is None:
|
||||
hotelname = "unknown"
|
||||
|
||||
if username is None or password is None or hotelid is None:
|
||||
if hotelid is None:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unauthorized Read Request for this specific hotel {hotelname}. Check credentials", HttpStatusCode.UNAUTHORIZED
|
||||
f"Error: Unauthorized Read Request. No target hotel specified. Check credentials",
|
||||
HttpStatusCode.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
if not validate_hotel_authentication(username, password, hotelid, self.config):
|
||||
if not validate_hotel_authentication(client_info.username, client_info.password, hotelid, self.config):
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unauthorized Read Request for this specific hotel {hotelname}. Check credentials", HttpStatusCode.UNAUTHORIZED
|
||||
f"Error: Unauthorized Read Request for this specific hotel {hotelname}. Check credentials",
|
||||
HttpStatusCode.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
|
||||
start_date = None
|
||||
|
||||
|
||||
if hotel_read_request.selection_criteria is not None:
|
||||
start_date = datetime.fromisoformat(hotel_read_request.selection_criteria.start)
|
||||
|
||||
|
||||
start_date = datetime.fromisoformat(
|
||||
hotel_read_request.selection_criteria.start
|
||||
)
|
||||
|
||||
# query all reservations for this hotel from the database, where start_date is greater than or equal to the given start_date
|
||||
|
||||
@@ -478,12 +502,18 @@ class ReadAction(AlpineBitsAction):
|
||||
stmt = stmt.filter(Reservation.start_date >= start_date)
|
||||
|
||||
result = await dbsession.execute(stmt)
|
||||
reservation_customer_pairs: list[tuple[Reservation, Customer]] = result.all() # List of (Reservation, Customer) tuples
|
||||
reservation_customer_pairs: list[tuple[Reservation, Customer]] = (
|
||||
result.all()
|
||||
) # List of (Reservation, Customer) tuples
|
||||
|
||||
_LOGGER.info(f"Querying reservations and customers for hotel {hotelid} from database")
|
||||
_LOGGER.info(
|
||||
f"Querying reservations and customers for hotel {hotelid} from database"
|
||||
)
|
||||
for reservation, customer in reservation_customer_pairs:
|
||||
_LOGGER.info(f"Reservation: {reservation.id}, Customer: {customer.given_name}")
|
||||
|
||||
_LOGGER.info(
|
||||
f"Reservation: {reservation.id}, Customer: {customer.given_name}"
|
||||
)
|
||||
|
||||
res_retrive_rs = create_xml_from_db(reservation_customer_pairs)
|
||||
|
||||
config = SerializerConfig(
|
||||
@@ -495,60 +525,30 @@ class ReadAction(AlpineBitsAction):
|
||||
)
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class NotifReportReadAction(AlpineBitsAction):
|
||||
"""Necessary for read action to follow specification. Clients need to report acknowledgements"""
|
||||
|
||||
|
||||
def __init__(self, config: Dict = {}):
|
||||
self.name = AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS
|
||||
self.version = [Version.V2024_10, Version.V2022_10]
|
||||
self.config = config
|
||||
|
||||
|
||||
async def handle(
|
||||
self,
|
||||
action: str,
|
||||
request_xml: str,
|
||||
version: Version,
|
||||
dbsession=None,
|
||||
username=None,
|
||||
password=None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""Handle read requests."""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# For demonstration, just echo back a simple XML response
|
||||
response_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_ReadRS xmlns="http://www.opentravel.org/OTA/2003/
|
||||
05" Version="8.000">
|
||||
<Success/>
|
||||
</OTA_ReadRS>"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
# class HotelAvailNotifAction(AlpineBitsAction):
|
||||
# """Implementation for Hotel Availability Notification action with supports."""
|
||||
|
||||
# def __init__(self):
|
||||
# self.name = AlpineBitsActionName.OTA_HOTEL_AVAIL_NOTIF
|
||||
# self.version = Version.V2022_10
|
||||
# self.supports = [
|
||||
# "OTA_HotelAvailNotif_accept_rooms",
|
||||
# "OTA_HotelAvailNotif_accept_categories",
|
||||
# "OTA_HotelAvailNotif_accept_deltas",
|
||||
# "OTA_HotelAvailNotif_accept_BookingThreshold",
|
||||
# ]
|
||||
|
||||
# async def handle(
|
||||
# self, action: str, request_xml: str, version: Version
|
||||
# ) -> AlpineBitsResponse:
|
||||
# """Handle hotel availability notifications."""
|
||||
# response_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
# <OTA_HotelAvailNotifRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="8.000">
|
||||
# <Success/>
|
||||
# </OTA_HotelAvailNotifRS>"""
|
||||
# return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Action {action} not implemented", HttpStatusCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class GuestRequestsAction(AlpineBitsAction):
|
||||
@@ -575,7 +575,6 @@ class AlpineBitsServer:
|
||||
self._action_instances = {}
|
||||
self.config = config
|
||||
self._initialize_action_instances()
|
||||
|
||||
|
||||
def _initialize_action_instances(self):
|
||||
"""Initialize instances of all discovered action classes."""
|
||||
@@ -591,7 +590,12 @@ class AlpineBitsServer:
|
||||
return self.capabilities.get_capabilities_json()
|
||||
|
||||
async def handle_request(
|
||||
self, request_action_name: str, request_xml: str, version: str = "2024-10", dbsession=None, username=None, password=None
|
||||
self,
|
||||
request_action_name: str,
|
||||
request_xml: str,
|
||||
client_info: AlpineBitsClientInfo,
|
||||
version: str = "2024-10",
|
||||
dbsession=None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""
|
||||
Handle an incoming AlpineBits request by routing to appropriate action handler.
|
||||
@@ -642,11 +646,15 @@ class AlpineBitsServer:
|
||||
# Special case for ping action - pass server capabilities
|
||||
if capability_name == "action_OTA_Ping":
|
||||
return await action_instance.handle(
|
||||
request_action_name, request_xml, version_enum, self.capabilities
|
||||
action=request_action_name, request_xml=request_xml, version=version_enum, server_capabilities=self.capabilities, client_info=client_info
|
||||
)
|
||||
else:
|
||||
return await action_instance.handle(
|
||||
request_action_name, request_xml, version_enum, dbsession=dbsession, username=username, password=password
|
||||
action=request_action_name,
|
||||
request_xml=request_xml,
|
||||
version=version_enum,
|
||||
dbsession=dbsession,
|
||||
client_info=client_info,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error handling request {request_action_name}: {str(e)}")
|
||||
@@ -669,7 +677,7 @@ class AlpineBitsServer:
|
||||
return sorted(request_names)
|
||||
|
||||
def is_action_supported(
|
||||
self, request_action_name: str, version: str = None
|
||||
self, request_action_name: str, version: str | None = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a request action is supported.
|
||||
|
||||
Reference in New Issue
Block a user