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 typing import Any
|
||||||
|
|
||||||
from alpine_bits_python.db import Customer, Reservation
|
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
|
# Import the generated classes
|
||||||
from .generated.alpinebits import (
|
from .generated.alpinebits import (
|
||||||
@@ -68,13 +75,6 @@ NotifHotelReservation = OtaHotelResNotifRq.HotelReservations.HotelReservation
|
|||||||
RetrieveHotelReservation = OtaResRetrieveRs.ReservationsList.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
|
# Enum to specify which OTA message type to use
|
||||||
class OtaMessageType(Enum):
|
class OtaMessageType(Enum):
|
||||||
NOTIF = "notification" # For OtaHotelResNotifRq
|
NOTIF = "notification" # For OtaHotelResNotifRq
|
||||||
@@ -88,37 +88,6 @@ class KidsAgeData:
|
|||||||
ages: list[int]
|
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:
|
class GuestCountsFactory:
|
||||||
"""Factory class to create GuestCounts instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
"""Factory class to create GuestCounts instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||||
|
|
||||||
@@ -129,6 +98,7 @@ class GuestCountsFactory:
|
|||||||
message_type: OtaMessageType = OtaMessageType.RETRIEVE,
|
message_type: OtaMessageType = OtaMessageType.RETRIEVE,
|
||||||
) -> NotifGuestCounts:
|
) -> NotifGuestCounts:
|
||||||
"""Create a GuestCounts object for OtaHotelResNotifRq or OtaResRetrieveRs.
|
"""Create a GuestCounts object for OtaHotelResNotifRq or OtaResRetrieveRs.
|
||||||
|
|
||||||
:param adults: Number of adults
|
:param adults: Number of adults
|
||||||
:param kids: List of ages for each kid (optional)
|
:param kids: List of ages for each kid (optional)
|
||||||
:return: GuestCounts instance
|
:return: GuestCounts instance
|
||||||
@@ -147,7 +117,8 @@ class GuestCountsFactory:
|
|||||||
def _create_guest_counts(
|
def _create_guest_counts(
|
||||||
adults: int, kids: list[int] | None, guest_counts_class: type
|
adults: int, kids: list[int] | None, guest_counts_class: type
|
||||||
) -> Any:
|
) -> 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 adults: Number of adults
|
||||||
:param kids: List of ages for each kid (optional)
|
:param kids: List of ages for each kid (optional)
|
||||||
:param guest_counts_class: The GuestCounts class to instantiate
|
:param guest_counts_class: The GuestCounts class to instantiate
|
||||||
@@ -173,7 +144,7 @@ class GuestCountsFactory:
|
|||||||
|
|
||||||
|
|
||||||
class CustomerFactory:
|
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
|
@staticmethod
|
||||||
def create_notif_customer(data: CustomerData) -> NotifCustomer:
|
def create_notif_customer(data: CustomerData) -> NotifCustomer:
|
||||||
@@ -186,8 +157,10 @@ class CustomerFactory:
|
|||||||
return CustomerFactory._create_customer(RetrieveCustomer, data)
|
return CustomerFactory._create_customer(RetrieveCustomer, data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_customer(customer_class: type, data: CustomerData) -> Any:
|
def _create_customer(
|
||||||
"""Internal method to create a customer of the specified type."""
|
customer_class: type[RetrieveCustomer | NotifCustomer], data: CustomerData
|
||||||
|
) -> Any:
|
||||||
|
"""Create a customer of the specified type."""
|
||||||
# Create PersonName
|
# Create PersonName
|
||||||
person_name = customer_class.PersonName(
|
person_name = customer_class.PersonName(
|
||||||
given_name=data.given_name,
|
given_name=data.given_name,
|
||||||
@@ -260,18 +233,20 @@ class CustomerFactory:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _customer_to_data(customer: Any) -> CustomerData:
|
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
|
# Extract phone numbers
|
||||||
phone_numbers = []
|
phone_numbers = []
|
||||||
if customer.telephone:
|
if customer.telephone:
|
||||||
for tel in customer.telephone:
|
phone_numbers.extend(
|
||||||
phone_numbers.append(
|
[
|
||||||
(
|
(
|
||||||
tel.phone_number,
|
tel.phone_number,
|
||||||
PhoneTechType(tel.phone_tech_type)
|
PhoneTechType(tel.phone_tech_type)
|
||||||
if tel.phone_tech_type
|
if tel.phone_tech_type
|
||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
|
for tel in customer.telephone
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract email info
|
# Extract email info
|
||||||
@@ -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:
|
class CommentFactory:
|
||||||
"""Factory class to create Comment instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
"""Factory class to create Comment instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||||
|
|
||||||
@@ -494,11 +436,7 @@ class CommentFactory:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract comment data
|
comments_data_list.append(comment)
|
||||||
comment_data = CommentData(
|
|
||||||
name=comment.name, text=comment.text, list_items=list_items_data
|
|
||||||
)
|
|
||||||
comments_data_list.append(comment_data)
|
|
||||||
|
|
||||||
return CommentsData(comments=comments_data_list)
|
return CommentsData(comments=comments_data_list)
|
||||||
|
|
||||||
@@ -527,7 +465,9 @@ class ResGuestFactory:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_res_guests(
|
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:
|
) -> Any:
|
||||||
"""Create the complete ResGuests structure."""
|
"""Create the complete ResGuests structure."""
|
||||||
# Create the customer using the existing CustomerFactory
|
# Create the customer using the existing CustomerFactory
|
||||||
|
|||||||
@@ -10,10 +10,18 @@ from XML generation (xsdata) follows clean architecture principles.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
|
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):
|
class PhoneNumber(BaseModel):
|
||||||
"""Phone number with optional type."""
|
"""Phone number with optional type."""
|
||||||
|
|
||||||
@@ -34,7 +42,7 @@ class CustomerData(BaseModel):
|
|||||||
surname: str = Field(..., min_length=1, max_length=100)
|
surname: str = Field(..., min_length=1, max_length=100)
|
||||||
name_prefix: str | None = Field(None, max_length=20)
|
name_prefix: str | None = Field(None, max_length=20)
|
||||||
name_title: 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_address: EmailStr | None = None
|
||||||
email_newsletter: bool | None = None
|
email_newsletter: bool | None = None
|
||||||
address_line: str | None = Field(None, max_length=255)
|
address_line: str | None = Field(None, max_length=255)
|
||||||
|
|||||||
Reference in New Issue
Block a user