Additonal validation and better type hints
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_HotelResNotifRQ xmlns="http://www.opentravel.org/OTA/2003/05" Version="7.000">
|
||||
<HotelReservations>
|
||||
<HotelReservation CreateDateTime="2025-10-07T14:24:04.943026+00:00" ResStatus="Requested" RoomStayReservation="true">
|
||||
<UniqueID Type="14" ID="c52702c9-55b9-44e1-b158-ec9544c7"/>
|
||||
<RoomStays>
|
||||
<RoomStay>
|
||||
<GuestCounts>
|
||||
<GuestCount Count="3"/>
|
||||
<GuestCount Count="1" Age="12"/>
|
||||
</GuestCounts>
|
||||
<TimeSpan Start="2026-01-02" End="2026-01-07"/>
|
||||
</RoomStay>
|
||||
</RoomStays>
|
||||
<ResGuests>
|
||||
<ResGuest>
|
||||
<Profiles>
|
||||
<ProfileInfo>
|
||||
<Profile>
|
||||
<Customer Language="it">
|
||||
<PersonName>
|
||||
<NamePrefix>Frau</NamePrefix>
|
||||
<GivenName>Genesia</GivenName>
|
||||
<Surname>Supino</Surname>
|
||||
</PersonName>
|
||||
<Telephone PhoneTechType="5" PhoneNumber="+393406259979"/>
|
||||
<Email Remark="newsletter:yes">supinogenesia@gmail.com</Email>
|
||||
</Customer>
|
||||
</Profile>
|
||||
</ProfileInfo>
|
||||
</Profiles>
|
||||
</ResGuest>
|
||||
</ResGuests>
|
||||
<ResGlobalInfo>
|
||||
<HotelReservationIDs>
|
||||
<HotelReservationID ResID_Type="13" ResID_Value="IwZXh0bgNhZW0BMABhZGlkAassWPh1b8QBHoRc2S24gMktdNKiPwEvGYMK3rB-mn" ResID_Source="Facebook_Mobile_Feed" ResID_SourceContext="99tales"/>
|
||||
</HotelReservationIDs>
|
||||
<Profiles>
|
||||
<ProfileInfo>
|
||||
<Profile ProfileType="4">
|
||||
<CompanyInfo>
|
||||
<CompanyName Code="who knows?" CodeContext="who knows?">99tales GmbH</CompanyName>
|
||||
</CompanyInfo>
|
||||
</Profile>
|
||||
</ProfileInfo>
|
||||
</Profiles>
|
||||
<BasicPropertyInfo HotelCode="12345" HotelName="Bemelmans Post"/>
|
||||
</ResGlobalInfo>
|
||||
</HotelReservation>
|
||||
</HotelReservations>
|
||||
</OTA_HotelResNotifRQ>
|
||||
@@ -6,7 +6,14 @@ from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from alpine_bits_python.db import Customer, Reservation
|
||||
from alpine_bits_python.schemas import HotelReservationIdData
|
||||
from alpine_bits_python.schemas import (
|
||||
CommentData,
|
||||
CommentListItemData,
|
||||
CommentsData,
|
||||
CustomerData,
|
||||
HotelReservationIdData,
|
||||
PhoneTechType,
|
||||
)
|
||||
|
||||
# Import the generated classes
|
||||
from .generated.alpinebits import (
|
||||
@@ -68,13 +75,6 @@ NotifHotelReservation = OtaHotelResNotifRq.HotelReservations.HotelReservation
|
||||
RetrieveHotelReservation = OtaResRetrieveRs.ReservationsList.HotelReservation
|
||||
|
||||
|
||||
# phonetechtype enum 1,3,5 voice, fax, mobile
|
||||
class PhoneTechType(Enum):
|
||||
VOICE = "1"
|
||||
FAX = "3"
|
||||
MOBILE = "5"
|
||||
|
||||
|
||||
# Enum to specify which OTA message type to use
|
||||
class OtaMessageType(Enum):
|
||||
NOTIF = "notification" # For OtaHotelResNotifRq
|
||||
@@ -88,37 +88,6 @@ class KidsAgeData:
|
||||
ages: list[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomerData:
|
||||
"""Simple data class to hold customer information without nested type constraints."""
|
||||
|
||||
given_name: str
|
||||
surname: str
|
||||
name_prefix: None | str = None
|
||||
name_title: None | str = None
|
||||
phone_numbers: list[tuple[str, None | PhoneTechType]] = (
|
||||
None # (phone_number, phone_tech_type)
|
||||
)
|
||||
email_address: None | str = None
|
||||
email_newsletter: None | bool = (
|
||||
None # True for "yes", False for "no", None for not specified
|
||||
)
|
||||
address_line: None | str = None
|
||||
city_name: None | str = None
|
||||
postal_code: None | str = None
|
||||
country_code: None | str = None # Two-letter country code
|
||||
address_catalog: None | bool = (
|
||||
None # True for "yes", False for "no", None for not specified
|
||||
)
|
||||
gender: None | str = None # "Unknown", "Male", "Female"
|
||||
birth_date: None | str = None
|
||||
language: None | str = None # Two-letter language code
|
||||
|
||||
def __post_init__(self):
|
||||
if self.phone_numbers is None:
|
||||
self.phone_numbers = []
|
||||
|
||||
|
||||
class GuestCountsFactory:
|
||||
"""Factory class to create GuestCounts instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||
|
||||
@@ -129,6 +98,7 @@ class GuestCountsFactory:
|
||||
message_type: OtaMessageType = OtaMessageType.RETRIEVE,
|
||||
) -> NotifGuestCounts:
|
||||
"""Create a GuestCounts object for OtaHotelResNotifRq or OtaResRetrieveRs.
|
||||
|
||||
:param adults: Number of adults
|
||||
:param kids: List of ages for each kid (optional)
|
||||
:return: GuestCounts instance
|
||||
@@ -147,7 +117,8 @@ class GuestCountsFactory:
|
||||
def _create_guest_counts(
|
||||
adults: int, kids: list[int] | None, guest_counts_class: type
|
||||
) -> Any:
|
||||
"""Internal method to create a GuestCounts object of the specified type.
|
||||
"""Create a GuestCounts object of the specified type.
|
||||
|
||||
:param adults: Number of adults
|
||||
:param kids: List of ages for each kid (optional)
|
||||
:param guest_counts_class: The GuestCounts class to instantiate
|
||||
@@ -173,7 +144,7 @@ class GuestCountsFactory:
|
||||
|
||||
|
||||
class CustomerFactory:
|
||||
"""Factory class to create Customer instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||
"""Factory class to create Customer instances for both Retrieve and Notif."""
|
||||
|
||||
@staticmethod
|
||||
def create_notif_customer(data: CustomerData) -> NotifCustomer:
|
||||
@@ -186,8 +157,10 @@ class CustomerFactory:
|
||||
return CustomerFactory._create_customer(RetrieveCustomer, data)
|
||||
|
||||
@staticmethod
|
||||
def _create_customer(customer_class: type, data: CustomerData) -> Any:
|
||||
"""Internal method to create a customer of the specified type."""
|
||||
def _create_customer(
|
||||
customer_class: type[RetrieveCustomer | NotifCustomer], data: CustomerData
|
||||
) -> Any:
|
||||
"""Create a customer of the specified type."""
|
||||
# Create PersonName
|
||||
person_name = customer_class.PersonName(
|
||||
given_name=data.given_name,
|
||||
@@ -260,19 +233,21 @@ class CustomerFactory:
|
||||
|
||||
@staticmethod
|
||||
def _customer_to_data(customer: Any) -> CustomerData:
|
||||
"""Internal method to convert any customer type to CustomerData."""
|
||||
"""Convert any customer type to CustomerData."""
|
||||
# Extract phone numbers
|
||||
phone_numbers = []
|
||||
if customer.telephone:
|
||||
for tel in customer.telephone:
|
||||
phone_numbers.append(
|
||||
phone_numbers.extend(
|
||||
[
|
||||
(
|
||||
tel.phone_number,
|
||||
PhoneTechType(tel.phone_tech_type)
|
||||
if tel.phone_tech_type
|
||||
else None,
|
||||
)
|
||||
)
|
||||
for tel in customer.telephone
|
||||
]
|
||||
)
|
||||
|
||||
# Extract email info
|
||||
email_address = None
|
||||
@@ -389,39 +364,6 @@ class HotelReservationIdFactory:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommentListItemData:
|
||||
"""Simple data class to hold comment list item information."""
|
||||
|
||||
value: str # The text content of the list item
|
||||
list_item: str # Numeric identifier (pattern: [0-9]+)
|
||||
language: str # Two-letter language code (pattern: [a-z][a-z])
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommentData:
|
||||
"""Simple data class to hold comment information without nested type constraints."""
|
||||
|
||||
name: CommentName2 # Required: "included services", "customer comment", "additional info"
|
||||
text: str | None = None # Optional text content
|
||||
list_items: list[CommentListItemData] = None # Optional list items
|
||||
|
||||
def __post_init__(self):
|
||||
if self.list_items is None:
|
||||
self.list_items = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommentsData:
|
||||
"""Simple data class to hold multiple comments (1-3 max)."""
|
||||
|
||||
comments: list[CommentData] = None # 1-3 comments maximum
|
||||
|
||||
def __post_init__(self):
|
||||
if self.comments is None:
|
||||
self.comments = []
|
||||
|
||||
|
||||
class CommentFactory:
|
||||
"""Factory class to create Comment instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||
|
||||
@@ -494,11 +436,7 @@ class CommentFactory:
|
||||
)
|
||||
)
|
||||
|
||||
# Extract comment data
|
||||
comment_data = CommentData(
|
||||
name=comment.name, text=comment.text, list_items=list_items_data
|
||||
)
|
||||
comments_data_list.append(comment_data)
|
||||
comments_data_list.append(comment)
|
||||
|
||||
return CommentsData(comments=comments_data_list)
|
||||
|
||||
@@ -527,7 +465,9 @@ class ResGuestFactory:
|
||||
|
||||
@staticmethod
|
||||
def _create_res_guests(
|
||||
res_guests_class: type, customer_class: type, customer_data: CustomerData
|
||||
res_guests_class: type[RetrieveResGuests] | type[NotifResGuests],
|
||||
customer_class: type[NotifCustomer | RetrieveCustomer],
|
||||
customer_data: CustomerData,
|
||||
) -> Any:
|
||||
"""Create the complete ResGuests structure."""
|
||||
# Create the customer using the existing CustomerFactory
|
||||
|
||||
@@ -10,10 +10,18 @@ from XML generation (xsdata) follows clean architecture principles.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
|
||||
|
||||
|
||||
# phonetechtype enum 1,3,5 voice, fax, mobile
|
||||
class PhoneTechType(Enum):
|
||||
VOICE = "1"
|
||||
FAX = "3"
|
||||
MOBILE = "5"
|
||||
|
||||
|
||||
class PhoneNumber(BaseModel):
|
||||
"""Phone number with optional type."""
|
||||
|
||||
@@ -34,7 +42,7 @@ class CustomerData(BaseModel):
|
||||
surname: str = Field(..., min_length=1, max_length=100)
|
||||
name_prefix: str | None = Field(None, max_length=20)
|
||||
name_title: str | None = Field(None, max_length=20)
|
||||
phone_numbers: list[PhoneNumber] = Field(default_factory=list)
|
||||
phone_numbers: list[tuple[str, None | PhoneTechType]] = Field(default_factory=list)
|
||||
email_address: EmailStr | None = None
|
||||
email_newsletter: bool | None = None
|
||||
address_line: str | None = Field(None, max_length=255)
|
||||
|
||||
Reference in New Issue
Block a user