Additonal validation and better type hints

This commit is contained in:
Jonas Linter
2025-10-07 16:28:43 +02:00
parent e605af1231
commit a69816baa4
3 changed files with 86 additions and 87 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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)