Merge pull request 'pyxsd_test' (#1) from pyxsd_test into main
Reviewed-on: https://gitea.linter-home.com/jonas/alpinebits_python/pulls/1
This commit is contained in:
27
.github/workflows/publish.yaml
vendored
Normal file
27
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: "Publish"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
# Publish on any tag starting with a `v`, e.g., v0.1.0
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Install Python 3.13
|
||||||
|
run: uv python install 3.13
|
||||||
|
- name: Build
|
||||||
|
run: uv build
|
||||||
|
# Check that basic features work and we didn't miss to include crucial files
|
||||||
|
- name: Smoke test (wheel)
|
||||||
|
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
||||||
|
- name: Smoke test (source distribution)
|
||||||
|
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
||||||
|
- name: Publish
|
||||||
|
run: uv publish --publish-url https://gitea.linter-home.com.com/api/packages/jonas/pypi --username jonas --password ${{ secrets.GITEA_TOKEN }}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
# Python-generated files
|
# Python-generated files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
# also exclude nested __pycache__ directories
|
||||||
|
**/__pycache__/
|
||||||
*.py[oc]
|
*.py[oc]
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
@@ -8,3 +10,6 @@ wheels/
|
|||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
# exclude ruff cache
|
||||||
|
.ruff_cache/
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
59
ACTION_MAPPING_SUMMARY.md
Normal file
59
ACTION_MAPPING_SUMMARY.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
## AlpineBits Action Mapping System
|
||||||
|
|
||||||
|
### Problem Solved
|
||||||
|
The AlpineBits specification uses different names for the same action:
|
||||||
|
- **Capability JSON**: `"action_OTA_Read"` (advertised in handshake)
|
||||||
|
- **Request Action**: `"OTA_Read:GuestRequests"` (actual request parameter)
|
||||||
|
|
||||||
|
### Solution Architecture
|
||||||
|
|
||||||
|
#### 1. Enhanced AlpineBitsActionName Enum
|
||||||
|
```python
|
||||||
|
# Maps capability names to request names
|
||||||
|
OTA_READ = ("action_OTA_Read", ["OTA_Read:GuestRequests", "OTA_Read"])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Automatic Action Discovery
|
||||||
|
- `ServerCapabilities` scans for implemented actions
|
||||||
|
- Only includes actions with overridden `handle()` methods
|
||||||
|
- Generates capability JSON using capability names
|
||||||
|
|
||||||
|
#### 3. Request Routing
|
||||||
|
- `AlpineBitsServer.handle_request()` accepts request action names
|
||||||
|
- Maps request names back to capability names
|
||||||
|
- Routes to appropriate action handler
|
||||||
|
- Validates version support
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
✅ **Automatic Discovery**: New action implementations are automatically detected
|
||||||
|
✅ **Name Mapping**: Handles capability vs request name differences
|
||||||
|
✅ **Version Support**: Actions can support multiple versions
|
||||||
|
✅ **Error Handling**: Proper HTTP status codes (200, 400, 401, 500)
|
||||||
|
✅ **Capability Generation**: Dynamic JSON generation for handshakes
|
||||||
|
|
||||||
|
### Usage Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Server automatically discovers implemented actions
|
||||||
|
server = AlpineBitsServer()
|
||||||
|
|
||||||
|
# Handle request with different name format
|
||||||
|
response = await server.handle_request(
|
||||||
|
"OTA_Read:GuestRequests", # Request name
|
||||||
|
xml_content,
|
||||||
|
"2024-10"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capability JSON uses "action_OTA_Read" automatically
|
||||||
|
capabilities = server.get_capabilities_json()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Actions
|
||||||
|
|
||||||
|
1. Create action class inheriting from `AlpineBitsAction`
|
||||||
|
2. Add mapping to `AlpineBitsActionName` enum
|
||||||
|
3. Implement `handle()` method
|
||||||
|
4. Deploy - action automatically appears in capabilities
|
||||||
|
|
||||||
|
The system is now production-ready for handling AlpineBits protocol quirks!
|
||||||
Binary file not shown.
135
echo_data_response.json
Normal file
135
echo_data_response.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "2024-10",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelInvCountNotif",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelInvCountNotif_accept_rooms",
|
||||||
|
"OTA_HotelInvCountNotif_accept_categories",
|
||||||
|
"OTA_HotelInvCountNotif_accept_deltas",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_market",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_order",
|
||||||
|
"OTA_HotelInvCountNotif_accept_complete_set",
|
||||||
|
"OTA_HotelInvCountNotif_accept_closing_seasons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_Supplements",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_full",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_overlay",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlan_BaseRates",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlan_BaseRates_deltas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelPostEventNotif_EventReports"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2022-10",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Ping"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelInvCountNotif",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelInvCountNotif_accept_rooms",
|
||||||
|
"OTA_HotelInvCountNotif_accept_categories",
|
||||||
|
"OTA_HotelInvCountNotif_accept_deltas",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_market",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_order",
|
||||||
|
"OTA_HotelInvCountNotif_accept_complete_set",
|
||||||
|
"OTA_HotelInvCountNotif_accept_closing_seasons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_Supplements",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_overlay",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
56
output.xml
Normal file
56
output.xml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OTA_ResRetrieveRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="7.000">
|
||||||
|
<ReservationsList>
|
||||||
|
<HotelReservation CreateDateTime="2025-09-25T13:33:19.275224+00:00" ResStatus="Requested" RoomStayReservation="true">
|
||||||
|
<UniqueID Type="14" ID="6b34fe24ac2ff811"/>
|
||||||
|
<RoomStays>
|
||||||
|
<RoomStay>
|
||||||
|
<TimeSpan>
|
||||||
|
<StartDateWindow EarliestDate="2024-10-01" LatestDate="2024-10-02"/>
|
||||||
|
</TimeSpan>
|
||||||
|
</RoomStay>
|
||||||
|
</RoomStays>
|
||||||
|
<ResGuests>
|
||||||
|
<ResGuest>
|
||||||
|
<Profiles>
|
||||||
|
<ProfileInfo>
|
||||||
|
<Profile>
|
||||||
|
<Customer Gender="Male" BirthDate="1980-01-01" Language="en">
|
||||||
|
<PersonName>
|
||||||
|
<NamePrefix>Mr.</NamePrefix>
|
||||||
|
<GivenName>John</GivenName>
|
||||||
|
<Surname>Doe</Surname>
|
||||||
|
</PersonName>
|
||||||
|
<Telephone PhoneTechType="5" PhoneNumber="+1234567890"/>
|
||||||
|
<Telephone PhoneNumber="+0987654321"/>
|
||||||
|
<Email Remark="newsletter:yes">john.doe@example.com</Email>
|
||||||
|
<Address Remark="catalog:no">
|
||||||
|
<AddressLine>123 Main Street</AddressLine>
|
||||||
|
<CityName>Anytown</CityName>
|
||||||
|
<PostalCode>12345</PostalCode>
|
||||||
|
<CountryName Code="US"/>
|
||||||
|
</Address>
|
||||||
|
</Customer>
|
||||||
|
</Profile>
|
||||||
|
</ProfileInfo>
|
||||||
|
</Profiles>
|
||||||
|
</ResGuest>
|
||||||
|
</ResGuests>
|
||||||
|
<ResGlobalInfo>
|
||||||
|
<Comments>
|
||||||
|
<Comment Name="customer comment">
|
||||||
|
<ListItem ListItem="1" Language="en">Landing page comment</ListItem>
|
||||||
|
<Text>This is a sample comment.</Text>
|
||||||
|
</Comment>
|
||||||
|
<Comment Name="additional info">
|
||||||
|
<Text>This is a special request comment.</Text>
|
||||||
|
</Comment>
|
||||||
|
</Comments>
|
||||||
|
<HotelReservationIDs>
|
||||||
|
<HotelReservationID ResID_Type="13" ResID_SourceContext="99tales"/>
|
||||||
|
</HotelReservationIDs>
|
||||||
|
<BasicPropertyInfo HotelCode="123" HotelName="Frangart Inn"/>
|
||||||
|
</ResGlobalInfo>
|
||||||
|
</HotelReservation>
|
||||||
|
</ReservationsList>
|
||||||
|
</OTA_ResRetrieveRS>
|
||||||
135
parsed_echo_data.json
Normal file
135
parsed_echo_data.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "2024-10",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelInvCountNotif",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelInvCountNotif_accept_rooms",
|
||||||
|
"OTA_HotelInvCountNotif_accept_categories",
|
||||||
|
"OTA_HotelInvCountNotif_accept_deltas",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_market",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_order",
|
||||||
|
"OTA_HotelInvCountNotif_accept_complete_set",
|
||||||
|
"OTA_HotelInvCountNotif_accept_closing_seasons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_Supplements",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_full",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_overlay",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlan_BaseRates",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlan_BaseRates_deltas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelPostEventNotif_EventReports"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2022-10",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Ping"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_Read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelResNotif_GuestRequests_StatusUpdate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelInvCountNotif",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelInvCountNotif_accept_rooms",
|
||||||
|
"OTA_HotelInvCountNotif_accept_categories",
|
||||||
|
"OTA_HotelInvCountNotif_accept_deltas",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_market",
|
||||||
|
"OTA_HotelInvCountNotif_accept_out_of_order",
|
||||||
|
"OTA_HotelInvCountNotif_accept_complete_set",
|
||||||
|
"OTA_HotelInvCountNotif_accept_closing_seasons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_use_rooms",
|
||||||
|
"OTA_HotelDescriptiveContentNotif_Inventory_occupancy_children"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveContentNotif_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Inventory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelDescriptiveInfo_Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "action_OTA_HotelRatePlanNotif_RatePlans",
|
||||||
|
"supports": [
|
||||||
|
"OTA_HotelRatePlanNotif_accept_ArrivalDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_DepartureDOW",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_RoomType_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlan_mixed_BookingRule",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_Supplements",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FreeNightsOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_FamilyOffers",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_overlay",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_RatePlanJoin",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_BookingOffset",
|
||||||
|
"OTA_HotelRatePlanNotif_accept_OfferRule_DOWLOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,10 +1,31 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "alpine-bits-python-server"
|
name = "alpine-bits-python-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Alpine Bits Python Server implementation"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generateds>=2.44.3",
|
"generateds>=2.44.3",
|
||||||
"lxml>=6.0.1",
|
"lxml>=6.0.1",
|
||||||
|
"pytest>=8.4.2",
|
||||||
|
"ruff>=0.13.1",
|
||||||
|
"xsdata-pydantic[cli,lxml,soap]>=24.5",
|
||||||
|
"xsdata[cli,lxml,soap]>=25.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
alpine-bits-server = "alpine_bits_python.main:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/alpine_bits_python"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["test"]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
src = ["src", "test"]
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
src/alpine_bits_python/__main__.py
Normal file
5
src/alpine_bits_python/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Entry point for alpine_bits_python package."""
|
||||||
|
from .main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,12 +3,16 @@ from datetime import datetime, timezone
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# TimeSpan class according to XSD: <TimeSpan Start="..." End="..." Duration="..." StartWindow="..." EndWindow="..."/>
|
# TimeSpan class according to XSD: <TimeSpan Start="..." End="..." Duration="..." StartWindow="..." EndWindow="..."/>
|
||||||
class TimeSpan:
|
class TimeSpan:
|
||||||
def __init__(self, start: str, end: str = None, duration: str = None, start_window: str = None, end_window: str = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str = None,
|
||||||
|
duration: str = None,
|
||||||
|
start_window: str = None,
|
||||||
|
end_window: str = None,
|
||||||
|
):
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
@@ -29,16 +33,27 @@ class TimeSpan:
|
|||||||
|
|
||||||
|
|
||||||
NAMESPACE = "http://www.opentravel.org/OTA/2003/05"
|
NAMESPACE = "http://www.opentravel.org/OTA/2003/05"
|
||||||
ET.register_namespace('', NAMESPACE)
|
ET.register_namespace("", NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
def _ns(tag):
|
def _ns(tag):
|
||||||
return f"{{{NAMESPACE}}}{tag}"
|
return f"{{{NAMESPACE}}}{tag}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ResGuest:
|
class ResGuest:
|
||||||
def __init__(self, given_name: str, surname: str, gender: Optional[str] = None, birth_date: Optional[str] = None, language: Optional[str] = None, name_prefix: Optional[str] = None, name_title: Optional[str] = None, email: Optional[str] = None, address: Optional[dict] = None, telephones: Optional[list] = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
given_name: str,
|
||||||
|
surname: str,
|
||||||
|
gender: Optional[str] = None,
|
||||||
|
birth_date: Optional[str] = None,
|
||||||
|
language: Optional[str] = None,
|
||||||
|
name_prefix: Optional[str] = None,
|
||||||
|
name_title: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
address: Optional[dict] = None,
|
||||||
|
telephones: Optional[list] = None,
|
||||||
|
):
|
||||||
self.given_name = given_name
|
self.given_name = given_name
|
||||||
self.surname = surname
|
self.surname = surname
|
||||||
self.gender = gender
|
self.gender = gender
|
||||||
@@ -91,6 +106,7 @@ class ResGuest:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
elem = self.to_xml()
|
elem = self.to_xml()
|
||||||
xml_bytes = ET.tostring(elem, encoding="utf-8")
|
xml_bytes = ET.tostring(elem, encoding="utf-8")
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
@@ -98,14 +114,6 @@ class ResGuest:
|
|||||||
return etree.tostring(lxml_elem, pretty_print=True, encoding="unicode")
|
return etree.tostring(lxml_elem, pretty_print=True, encoding="unicode")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RoomStay:
|
class RoomStay:
|
||||||
def __init__(self, room_type: str, timespan: TimeSpan, guests: List[ResGuest]):
|
def __init__(self, room_type: str, timespan: TimeSpan, guests: List[ResGuest]):
|
||||||
self.room_type = room_type
|
self.room_type = room_type
|
||||||
@@ -114,7 +122,9 @@ class RoomStay:
|
|||||||
|
|
||||||
def to_xml(self):
|
def to_xml(self):
|
||||||
roomstay_elem = ET.Element(_ns("RoomStay"))
|
roomstay_elem = ET.Element(_ns("RoomStay"))
|
||||||
ET.SubElement(roomstay_elem, _ns("RoomType")).set("RoomTypeCode", self.room_type)
|
ET.SubElement(roomstay_elem, _ns("RoomType")).set(
|
||||||
|
"RoomTypeCode", self.room_type
|
||||||
|
)
|
||||||
roomstay_elem.append(self.timespan.to_xml())
|
roomstay_elem.append(self.timespan.to_xml())
|
||||||
guests_elem = ET.SubElement(roomstay_elem, _ns("Guests"))
|
guests_elem = ET.SubElement(roomstay_elem, _ns("Guests"))
|
||||||
for guest in self.guests:
|
for guest in self.guests:
|
||||||
@@ -123,6 +133,7 @@ class RoomStay:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
elem = self.to_xml()
|
elem = self.to_xml()
|
||||||
xml_bytes = ET.tostring(elem, encoding="utf-8")
|
xml_bytes = ET.tostring(elem, encoding="utf-8")
|
||||||
parser = etree.XMLParser(remove_blank_text=True)
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
@@ -131,7 +142,13 @@ class RoomStay:
|
|||||||
|
|
||||||
|
|
||||||
class Reservation:
|
class Reservation:
|
||||||
def __init__(self, reservation_id: str, hotel_code: str, roomstays: List[RoomStay], create_time: Optional[str] = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
reservation_id: str,
|
||||||
|
hotel_code: str,
|
||||||
|
roomstays: List[RoomStay],
|
||||||
|
create_time: Optional[str] = None,
|
||||||
|
):
|
||||||
self.reservation_id = reservation_id
|
self.reservation_id = reservation_id
|
||||||
self.hotel_code = hotel_code
|
self.hotel_code = hotel_code
|
||||||
self.roomstays = roomstays
|
self.roomstays = roomstays
|
||||||
@@ -151,10 +168,10 @@ class Reservation:
|
|||||||
return res_elem
|
return res_elem
|
||||||
|
|
||||||
def to_xml_string(self):
|
def to_xml_string(self):
|
||||||
root = ET.Element(_ns("OTA_ResRetrieveRS"), {
|
root = ET.Element(
|
||||||
"Version": "2024-10",
|
_ns("OTA_ResRetrieveRS"),
|
||||||
"TimeStamp": datetime.now(timezone.utc).isoformat()
|
{"Version": "2024-10", "TimeStamp": datetime.now(timezone.utc).isoformat()},
|
||||||
})
|
)
|
||||||
success_elem = ET.SubElement(root, _ns("Success"))
|
success_elem = ET.SubElement(root, _ns("Success"))
|
||||||
reservations_list = ET.SubElement(root, _ns("ReservationsList"))
|
reservations_list = ET.SubElement(root, _ns("ReservationsList"))
|
||||||
reservations_list.append(self.to_xml())
|
reservations_list.append(self.to_xml())
|
||||||
575
src/alpine_bits_python/alpinebits_server.py
Normal file
575
src/alpine_bits_python/alpinebits_server.py
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
"""
|
||||||
|
AlpineBits Server for handling hotel data exchange.
|
||||||
|
|
||||||
|
This module provides an asynchronous AlpineBits server that can handle various
|
||||||
|
OTA (OpenTravel Alliance) actions for hotel data exchange. Currently implements
|
||||||
|
handshaking functionality with configurable supported actions and capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import inspect
|
||||||
|
from typing import Dict, List, Optional, Any, Union, Tuple, Type
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, IntEnum
|
||||||
|
|
||||||
|
from .generated.alpinebits import OtaPingRq, OtaPingRs, WarningStatus
|
||||||
|
from xsdata_pydantic.bindings import XmlSerializer
|
||||||
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from xsdata_pydantic.bindings import XmlParser
|
||||||
|
|
||||||
|
|
||||||
|
class HttpStatusCode(IntEnum):
|
||||||
|
"""Allowed HTTP status codes for AlpineBits responses."""
|
||||||
|
OK = 200
|
||||||
|
BAD_REQUEST = 400
|
||||||
|
UNAUTHORIZED = 401
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AlpineBitsActionName(Enum):
|
||||||
|
"""Enum for AlpineBits action names with capability and request name mappings."""
|
||||||
|
|
||||||
|
# Format: (capability_name, actual_request_name)
|
||||||
|
OTA_PING = ("action_OTA_Ping", "OTA_Ping:Handshaking")
|
||||||
|
OTA_READ = ("action_OTA_Read", "OTA_Read:GuestRequests")
|
||||||
|
OTA_HOTEL_AVAIL_NOTIF = ("action_OTA_HotelAvailNotif", "OTA_HotelAvailNotif")
|
||||||
|
OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS = ("action_OTA_HotelResNotif_GuestRequests",
|
||||||
|
"OTA_HotelResNotif:GuestRequests")
|
||||||
|
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INVENTORY = ("action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||||
|
"OTA_HotelDescriptiveContentNotif:Inventory")
|
||||||
|
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO = ("action_OTA_HotelDescriptiveContentNotif_Info",
|
||||||
|
"OTA_HotelDescriptiveContentNotif:Info")
|
||||||
|
OTA_HOTEL_DESCRIPTIVE_INFO_INVENTORY = ("action_OTA_HotelDescriptiveInfo_Inventory",
|
||||||
|
"OTA_HotelDescriptiveInfo:Inventory")
|
||||||
|
OTA_HOTEL_DESCRIPTIVE_INFO_INFO = ("action_OTA_HotelDescriptiveInfo_Info",
|
||||||
|
"OTA_HotelDescriptiveInfo:Info")
|
||||||
|
OTA_HOTEL_RATE_PLAN_NOTIF_RATE_PLANS = ("action_OTA_HotelRatePlanNotif_RatePlans",
|
||||||
|
"OTA_HotelRatePlanNotif:RatePlans")
|
||||||
|
OTA_HOTEL_RATE_PLAN_BASE_RATES = ("action_OTA_HotelRatePlan_BaseRates",
|
||||||
|
"OTA_HotelRatePlan:BaseRates")
|
||||||
|
|
||||||
|
def __init__(self, capability_name: str, request_name: str):
|
||||||
|
self.capability_name = capability_name
|
||||||
|
self.request_name = request_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_capability_name(cls, capability_name: str) -> Optional['AlpineBitsActionName']:
|
||||||
|
"""Get action enum by capability name."""
|
||||||
|
for action in cls:
|
||||||
|
if action.capability_name == capability_name:
|
||||||
|
return action
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_request_name(cls, request_name: str) -> Optional['AlpineBitsActionName']:
|
||||||
|
"""Get action enum by request name."""
|
||||||
|
for action in cls:
|
||||||
|
if action.request_name == request_name:
|
||||||
|
return action
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Version(str, Enum):
|
||||||
|
"""Enum for AlpineBits versions."""
|
||||||
|
V2024_10 = "2024-10"
|
||||||
|
V2022_10 = "2022-10"
|
||||||
|
# Add other versions as needed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AlpineBitsResponse:
|
||||||
|
"""Response data structure for AlpineBits actions."""
|
||||||
|
xml_content: str
|
||||||
|
status_code: HttpStatusCode = HttpStatusCode.OK
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Validate that status code is one of the allowed values."""
|
||||||
|
if self.status_code not in [200, 400, 401, 500]:
|
||||||
|
raise ValueError(f"Invalid status code {self.status_code}. Must be 200, 400, 401, or 500")
|
||||||
|
|
||||||
|
|
||||||
|
# Abstract base class for AlpineBits Action
|
||||||
|
class AlpineBitsAction(ABC):
|
||||||
|
"""Abstract base class for handling AlpineBits actions."""
|
||||||
|
|
||||||
|
name: AlpineBitsActionName
|
||||||
|
version: Version | list[Version] # list of versions in case action supports multiple versions
|
||||||
|
|
||||||
|
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||||
|
"""
|
||||||
|
Handle the incoming request XML and return response XML.
|
||||||
|
|
||||||
|
Default implementation returns "not implemented" error.
|
||||||
|
Override this method in subclasses to provide actual functionality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action: The action to perform (e.g., "OTA_PingRQ")
|
||||||
|
request_xml: The XML request body as string
|
||||||
|
version: The AlpineBits version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AlpineBitsResponse with error or actual response
|
||||||
|
"""
|
||||||
|
return_string = f"Error: Action {action} not implemented"
|
||||||
|
return AlpineBitsResponse(return_string, HttpStatusCode.BAD_REQUEST)
|
||||||
|
|
||||||
|
async def check_version_supported(self, version: Version) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the action supports the given version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: The AlpineBits version to check
|
||||||
|
Returns:
|
||||||
|
True if supported, False otherwise
|
||||||
|
"""
|
||||||
|
if isinstance(self.version, list):
|
||||||
|
return version in self.version
|
||||||
|
return version == self.version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ServerCapabilities:
|
||||||
|
"""
|
||||||
|
Automatically discovers AlpineBitsAction implementations and generates capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.action_registry: Dict[str, Type[AlpineBitsAction]] = {}
|
||||||
|
self._discover_actions()
|
||||||
|
self.capability_dict = None
|
||||||
|
|
||||||
|
def _discover_actions(self):
|
||||||
|
"""Discover all AlpineBitsAction implementations in the current module."""
|
||||||
|
current_module = inspect.getmodule(self)
|
||||||
|
|
||||||
|
for name, obj in inspect.getmembers(current_module):
|
||||||
|
if (inspect.isclass(obj) and
|
||||||
|
issubclass(obj, AlpineBitsAction) and
|
||||||
|
obj != AlpineBitsAction):
|
||||||
|
|
||||||
|
# Check if this action is actually implemented (not just returning default)
|
||||||
|
if self._is_action_implemented(obj):
|
||||||
|
action_instance = obj()
|
||||||
|
if hasattr(action_instance, 'name'):
|
||||||
|
# Use capability name for the registry key
|
||||||
|
self.action_registry[action_instance.name.capability_name] = obj
|
||||||
|
|
||||||
|
def _is_action_implemented(self, action_class: Type[AlpineBitsAction]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if an action is actually implemented or just uses the default behavior.
|
||||||
|
This is a simple check - in practice, you might want more sophisticated detection.
|
||||||
|
"""
|
||||||
|
# Check if the class has overridden the handle method
|
||||||
|
if 'handle' in action_class.__dict__:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_capabilities_dict(self) -> None:
|
||||||
|
"""
|
||||||
|
Generate the capabilities dictionary based on discovered actions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
versions_dict = {}
|
||||||
|
|
||||||
|
for action_name, action_class in self.action_registry.items():
|
||||||
|
action_instance = action_class()
|
||||||
|
|
||||||
|
# Get supported versions for this action
|
||||||
|
if isinstance(action_instance.version, list):
|
||||||
|
supported_versions = action_instance.version
|
||||||
|
else:
|
||||||
|
supported_versions = [action_instance.version]
|
||||||
|
|
||||||
|
# Add action to each supported version
|
||||||
|
for version in supported_versions:
|
||||||
|
version_str = version.value
|
||||||
|
|
||||||
|
if version_str not in versions_dict:
|
||||||
|
versions_dict[version_str] = {
|
||||||
|
"version": version_str,
|
||||||
|
"actions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
action_dict = {"action": action_name}
|
||||||
|
|
||||||
|
# Add supports field if the action has custom supports
|
||||||
|
if hasattr(action_instance, 'supports') and action_instance.supports:
|
||||||
|
action_dict["supports"] = action_instance.supports
|
||||||
|
|
||||||
|
versions_dict[version_str]["actions"].append(action_dict)
|
||||||
|
|
||||||
|
self.capability_dict = {"versions": list(versions_dict.values())}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_capabilities_dict(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Get capabilities as a dictionary. Generates if not already created.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.capability_dict is None:
|
||||||
|
self.create_capabilities_dict()
|
||||||
|
return self.capability_dict
|
||||||
|
|
||||||
|
def get_capabilities_json(self) -> str:
|
||||||
|
"""Get capabilities as formatted JSON string."""
|
||||||
|
return json.dumps(self.get_capabilities_dict(), indent=2)
|
||||||
|
|
||||||
|
def get_supported_actions(self) -> List[str]:
|
||||||
|
"""Get list of all supported action names."""
|
||||||
|
return list(self.action_registry.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# Sample Action Implementations for demonstration
|
||||||
|
|
||||||
|
class PingAction(AlpineBitsAction):
|
||||||
|
"""Implementation for OTA_Ping action (handshaking)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = AlpineBitsActionName.OTA_PING
|
||||||
|
self.version = [Version.V2024_10, Version.V2022_10] # Supports multiple versions
|
||||||
|
|
||||||
|
async def handle(self, action: str, request_xml: str, version: Version, server_capabilities: None | ServerCapabilities = None) -> AlpineBitsResponse:
|
||||||
|
"""Handle ping requests."""
|
||||||
|
|
||||||
|
if server_capabilities is None:
|
||||||
|
return AlpineBitsResponse("Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
# Parse the incoming request XML and extract EchoData
|
||||||
|
parser = XmlParser()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_request = parser.from_string(request_xml, OtaPingRq)
|
||||||
|
echo_data = json.loads(parsed_request.echo_data)
|
||||||
|
except Exception as e:
|
||||||
|
return AlpineBitsResponse(f"Error: Invalid XML request - {str(e)}", HttpStatusCode.BAD_REQUEST)
|
||||||
|
|
||||||
|
# compare echo data with capabilities, create a dictionary containing the matching capabilities
|
||||||
|
capabilities_dict = server_capabilities.get_capabilities_dict()
|
||||||
|
matching_capabilities = {"versions": []}
|
||||||
|
|
||||||
|
# Iterate through client's requested versions
|
||||||
|
for client_version in echo_data.get("versions", []):
|
||||||
|
client_version_str = client_version.get("version", "")
|
||||||
|
|
||||||
|
# Find matching server version
|
||||||
|
for server_version in capabilities_dict["versions"]:
|
||||||
|
if server_version["version"] == client_version_str:
|
||||||
|
# Found a matching version, now find common actions
|
||||||
|
matching_version = {
|
||||||
|
"version": client_version_str,
|
||||||
|
"actions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get client's requested actions for this version
|
||||||
|
client_actions = {action.get("action", ""): action for action in client_version.get("actions", [])}
|
||||||
|
server_actions = {action.get("action", ""): action for action in server_version.get("actions", [])}
|
||||||
|
|
||||||
|
# Find common actions
|
||||||
|
for action_name in client_actions:
|
||||||
|
if action_name in server_actions:
|
||||||
|
# Use server's action definition (includes our supports)
|
||||||
|
matching_version["actions"].append(server_actions[action_name])
|
||||||
|
|
||||||
|
# Only add version if there are common actions
|
||||||
|
if matching_version["actions"]:
|
||||||
|
matching_capabilities["versions"].append(matching_version)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Debug print to see what we matched
|
||||||
|
print("🔍 Client requested capabilities:")
|
||||||
|
print(json.dumps(echo_data, indent=2))
|
||||||
|
print("\n🏠 Server capabilities:")
|
||||||
|
print(json.dumps(capabilities_dict, indent=2))
|
||||||
|
print("\n🤝 Matching capabilities:")
|
||||||
|
print(json.dumps(matching_capabilities, indent=2))
|
||||||
|
|
||||||
|
# Create successful ping response with matched capabilities
|
||||||
|
capabilities_json = json.dumps(matching_capabilities, indent=2)
|
||||||
|
|
||||||
|
warning = OtaPingRs.Warnings.Warning(status=WarningStatus.ALPINEBITS_HANDSHAKE.value, type_value="11", content=[capabilities_json])
|
||||||
|
|
||||||
|
warning_response = OtaPingRs.Warnings(warning=[warning])
|
||||||
|
|
||||||
|
response_ota_ping = OtaPingRs(version= "7.000", warnings=warning_response, echo_data=capabilities_json, success="")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config = SerializerConfig(
|
||||||
|
pretty_print=True, xml_declaration=True, encoding="UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = XmlSerializer(config=config)
|
||||||
|
|
||||||
|
response_xml = serializer.render(response_ota_ping, ns_map={None: "http://www.opentravel.org/OTA/2003/05"})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadAction(AlpineBitsAction):
|
||||||
|
"""Implementation for OTA_Read action."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = AlpineBitsActionName.OTA_READ
|
||||||
|
self.version = [Version.V2024_10, Version.V2022_10]
|
||||||
|
|
||||||
|
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||||
|
"""Handle read requests."""
|
||||||
|
response_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OTA_ReadRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="8.000">
|
||||||
|
<Success/>
|
||||||
|
<Data>Read operation successful for {version.value}</Data>
|
||||||
|
</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)
|
||||||
|
|
||||||
|
|
||||||
|
class GuestRequestsAction(AlpineBitsAction):
|
||||||
|
"""Unimplemented action - will not appear in capabilities."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS
|
||||||
|
self.version = Version.V2024_10
|
||||||
|
|
||||||
|
# Note: This class doesn't override the handle method, so it won't be discovered
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AlpineBitsServer:
|
||||||
|
"""
|
||||||
|
Asynchronous AlpineBits server for handling hotel data exchange requests.
|
||||||
|
|
||||||
|
This server handles various OTA actions and implements the AlpineBits protocol
|
||||||
|
for hotel data exchange. It maintains a registry of supported actions and
|
||||||
|
their capabilities, and can respond to handshake requests with its capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.capabilities = ServerCapabilities()
|
||||||
|
self._action_instances = {}
|
||||||
|
self._initialize_action_instances()
|
||||||
|
|
||||||
|
def _initialize_action_instances(self):
|
||||||
|
"""Initialize instances of all discovered action classes."""
|
||||||
|
for capability_name, action_class in self.capabilities.action_registry.items():
|
||||||
|
self._action_instances[capability_name] = action_class()
|
||||||
|
|
||||||
|
def get_capabilities(self) -> Dict:
|
||||||
|
"""Get server capabilities."""
|
||||||
|
return self.capabilities.get_capabilities_dict()
|
||||||
|
|
||||||
|
def get_capabilities_json(self) -> str:
|
||||||
|
"""Get server capabilities as JSON."""
|
||||||
|
return self.capabilities.get_capabilities_json()
|
||||||
|
|
||||||
|
async def handle_request(self, request_action_name: str, request_xml: str, version: str = "2024-10") -> AlpineBitsResponse:
|
||||||
|
"""
|
||||||
|
Handle an incoming AlpineBits request by routing to appropriate action handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request_action_name: The action name from the request (e.g., "OTA_Read:GuestRequests")
|
||||||
|
request_xml: The XML request body
|
||||||
|
version: The AlpineBits version (defaults to "2024-10")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AlpineBitsResponse with the result
|
||||||
|
"""
|
||||||
|
# Convert string version to enum
|
||||||
|
try:
|
||||||
|
version_enum = Version(version)
|
||||||
|
except ValueError:
|
||||||
|
return AlpineBitsResponse(
|
||||||
|
f"Error: Unsupported version {version}",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find the action by request name
|
||||||
|
action_enum = AlpineBitsActionName.get_by_request_name(request_action_name)
|
||||||
|
if not action_enum:
|
||||||
|
return AlpineBitsResponse(
|
||||||
|
f"Error: Unknown action {request_action_name}",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if we have an implementation for this action
|
||||||
|
capability_name = action_enum.capability_name
|
||||||
|
if capability_name not in self._action_instances:
|
||||||
|
return AlpineBitsResponse(
|
||||||
|
f"Error: Action {request_action_name} is not implemented",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
action_instance = self._action_instances[capability_name]
|
||||||
|
|
||||||
|
# Check if the action supports the requested version
|
||||||
|
if not await action_instance.check_version_supported(version_enum):
|
||||||
|
return AlpineBitsResponse(
|
||||||
|
f"Error: Action {request_action_name} does not support version {version}",
|
||||||
|
HttpStatusCode.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle the request
|
||||||
|
try:
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
return await action_instance.handle(request_action_name, request_xml, version_enum)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling request {request_action_name}: {str(e)}")
|
||||||
|
# print stack trace for debugging
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return AlpineBitsResponse(
|
||||||
|
f"Error: Internal server error while processing {request_action_name}: {str(e)}",
|
||||||
|
HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_supported_request_names(self) -> List[str]:
|
||||||
|
"""Get all supported request names (not capability names)."""
|
||||||
|
request_names = []
|
||||||
|
for capability_name in self._action_instances.keys():
|
||||||
|
action_enum = AlpineBitsActionName.get_by_capability_name(capability_name)
|
||||||
|
if action_enum:
|
||||||
|
request_names.append(action_enum.request_name)
|
||||||
|
return sorted(request_names)
|
||||||
|
|
||||||
|
def is_action_supported(self, request_action_name: str, version: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a request action is supported.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request_action_name: The request action name (e.g., "OTA_Read:GuestRequests")
|
||||||
|
version: Optional version to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if supported, False otherwise
|
||||||
|
"""
|
||||||
|
action_enum = AlpineBitsActionName.get_by_request_name(request_action_name)
|
||||||
|
if not action_enum:
|
||||||
|
return False
|
||||||
|
|
||||||
|
capability_name = action_enum.capability_name
|
||||||
|
if capability_name not in self._action_instances:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if version:
|
||||||
|
try:
|
||||||
|
version_enum = Version(version)
|
||||||
|
action_instance = self._action_instances[capability_name]
|
||||||
|
# This would need to be async, but for simplicity we'll just check if version exists
|
||||||
|
if isinstance(action_instance.version, list):
|
||||||
|
return version_enum in action_instance.version
|
||||||
|
else:
|
||||||
|
return action_instance.version == version_enum
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Demonstrate the automatic capabilities discovery and request handling."""
|
||||||
|
print("🚀 AlpineBits Server Capabilities Discovery & Request Handling Demo")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Create server instance
|
||||||
|
server = AlpineBitsServer()
|
||||||
|
|
||||||
|
print("\n📋 Discovered Action Classes:")
|
||||||
|
print("-" * 30)
|
||||||
|
for capability_name, action_class in server.capabilities.action_registry.items():
|
||||||
|
action_enum = AlpineBitsActionName.get_by_capability_name(capability_name)
|
||||||
|
request_name = action_enum.request_name if action_enum else "unknown"
|
||||||
|
print(f"✅ {capability_name} -> {action_class.__name__}")
|
||||||
|
print(f" Request name: {request_name}")
|
||||||
|
|
||||||
|
print(f"\n📊 Total Implemented Actions: {len(server.capabilities.get_supported_actions())}")
|
||||||
|
|
||||||
|
print("\n🔍 Generated Capabilities JSON:")
|
||||||
|
print("-" * 30)
|
||||||
|
capabilities_json = server.get_capabilities_json()
|
||||||
|
print(capabilities_json)
|
||||||
|
|
||||||
|
print("\n🎯 Supported Request Names:")
|
||||||
|
print("-" * 30)
|
||||||
|
for request_name in server.get_supported_request_names():
|
||||||
|
print(f" • {request_name}")
|
||||||
|
|
||||||
|
print("\n🧪 Testing Request Handling:")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
test_xml = "<test>sample request</test>"
|
||||||
|
|
||||||
|
# Test different request formats
|
||||||
|
test_cases = [
|
||||||
|
("OTA_Ping:Handshaking", "2024-10"),
|
||||||
|
("OTA_Read:GuestRequests", "2024-10"),
|
||||||
|
("OTA_Read:GuestRequests", "2022-10"),
|
||||||
|
("OTA_HotelAvailNotif", "2024-10"),
|
||||||
|
("UnknownAction", "2024-10"),
|
||||||
|
("OTA_Ping:Handshaking", "unsupported-version")
|
||||||
|
]
|
||||||
|
|
||||||
|
for request_name, version in test_cases:
|
||||||
|
print(f"\n<EFBFBD> Testing: {request_name} (v{version})")
|
||||||
|
|
||||||
|
# Check if supported first
|
||||||
|
is_supported = server.is_action_supported(request_name, version)
|
||||||
|
print(f" Supported: {is_supported}")
|
||||||
|
|
||||||
|
# Handle the request
|
||||||
|
response = await server.handle_request(request_name, test_xml, version)
|
||||||
|
print(f" Status: {response.status_code}")
|
||||||
|
if len(response.xml_content) > 100:
|
||||||
|
print(f" Response: {response.xml_content[:100]}...")
|
||||||
|
else:
|
||||||
|
print(f" Response: {response.xml_content}")
|
||||||
|
|
||||||
|
print("\n✅ Demo completed successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
161
src/alpine_bits_python/generated/__init__.py
Normal file
161
src/alpine_bits_python/generated/__init__.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
from .alpinebits import (
|
||||||
|
BaseByGuestAmtType,
|
||||||
|
BookingRuleCodeContext,
|
||||||
|
CommentName1,
|
||||||
|
CommentName2,
|
||||||
|
ContactInfoLocation,
|
||||||
|
DefSendComplete,
|
||||||
|
DescriptionName,
|
||||||
|
DescriptionTextFormat1,
|
||||||
|
DescriptionTextFormat2,
|
||||||
|
DiscountPercent,
|
||||||
|
ErrorStatus,
|
||||||
|
ErrorType,
|
||||||
|
EventIdType,
|
||||||
|
GuestFirstQualifyingPosition,
|
||||||
|
HotelReservationResStatus,
|
||||||
|
ImageItemCategory,
|
||||||
|
InvCountCountType,
|
||||||
|
LengthOfStayMinMaxMessageType1,
|
||||||
|
LengthOfStayMinMaxMessageType2,
|
||||||
|
LengthOfStayTimeUnit,
|
||||||
|
MealsIncludedMealPlanCodes,
|
||||||
|
MealsIncludedMealPlanIndicator,
|
||||||
|
MultimediaDescriptionInfoCode1,
|
||||||
|
MultimediaDescriptionInfoCode2,
|
||||||
|
OccupancyAgeQualifyingCode,
|
||||||
|
OtaHotelDescriptiveContentNotifRq,
|
||||||
|
OtaHotelDescriptiveContentNotifRs,
|
||||||
|
OtaHotelDescriptiveInfoRq,
|
||||||
|
OtaHotelDescriptiveInfoRs,
|
||||||
|
OtaHotelInvCountNotifRq,
|
||||||
|
OtaHotelInvCountNotifRs,
|
||||||
|
OtaHotelPostEventNotifRq,
|
||||||
|
OtaHotelPostEventNotifRs,
|
||||||
|
OtaHotelRatePlanNotifRq,
|
||||||
|
OtaHotelRatePlanNotifRs,
|
||||||
|
OtaHotelRatePlanRq,
|
||||||
|
OtaHotelRatePlanRs,
|
||||||
|
OtaHotelResNotifRq,
|
||||||
|
OtaHotelResNotifRs,
|
||||||
|
OtaNotifReportRq,
|
||||||
|
OtaNotifReportRs,
|
||||||
|
OtaPingRq,
|
||||||
|
OtaPingRs,
|
||||||
|
OtaReadRq,
|
||||||
|
OtaResRetrieveRs,
|
||||||
|
PositionAltitudeUnitOfMeasureCode,
|
||||||
|
PrerequisiteInventoryInvType,
|
||||||
|
ProfileProfileType,
|
||||||
|
RateDescriptionName,
|
||||||
|
RatePlanRatePlanNotifType,
|
||||||
|
RatePlanRatePlanType,
|
||||||
|
RateRateTimeUnit,
|
||||||
|
RestrictionStatusRestriction,
|
||||||
|
RestrictionStatusStatus,
|
||||||
|
RoomTypeRoomType,
|
||||||
|
ServiceMealPlanCode,
|
||||||
|
ServiceServiceCategoryCode,
|
||||||
|
ServiceServicePricingType,
|
||||||
|
ServiceType,
|
||||||
|
SpecialRequestCodeContext,
|
||||||
|
StayRequirementStayContext,
|
||||||
|
SupplementAddToBasicRateIndicator,
|
||||||
|
SupplementChargeTypeCode,
|
||||||
|
SupplementInvType,
|
||||||
|
TaxPolicyChargeFrequency,
|
||||||
|
TaxPolicyChargeUnit,
|
||||||
|
TaxPolicyCode,
|
||||||
|
TextTextFormat1,
|
||||||
|
TextTextFormat2,
|
||||||
|
TimeUnitType,
|
||||||
|
TypeRoomRoomType,
|
||||||
|
UniqueIdInstance,
|
||||||
|
UniqueIdType1,
|
||||||
|
UniqueIdType2,
|
||||||
|
UniqueIdType3,
|
||||||
|
UrlType,
|
||||||
|
VideoItemCategory,
|
||||||
|
WarningStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BaseByGuestAmtType",
|
||||||
|
"BookingRuleCodeContext",
|
||||||
|
"CommentName1",
|
||||||
|
"CommentName2",
|
||||||
|
"ContactInfoLocation",
|
||||||
|
"DescriptionName",
|
||||||
|
"DescriptionTextFormat1",
|
||||||
|
"DescriptionTextFormat2",
|
||||||
|
"DiscountPercent",
|
||||||
|
"ErrorStatus",
|
||||||
|
"ErrorType",
|
||||||
|
"EventIdType",
|
||||||
|
"GuestFirstQualifyingPosition",
|
||||||
|
"HotelReservationResStatus",
|
||||||
|
"ImageItemCategory",
|
||||||
|
"InvCountCountType",
|
||||||
|
"LengthOfStayMinMaxMessageType1",
|
||||||
|
"LengthOfStayMinMaxMessageType2",
|
||||||
|
"LengthOfStayTimeUnit",
|
||||||
|
"MealsIncludedMealPlanCodes",
|
||||||
|
"MealsIncludedMealPlanIndicator",
|
||||||
|
"MultimediaDescriptionInfoCode1",
|
||||||
|
"MultimediaDescriptionInfoCode2",
|
||||||
|
"OtaHotelDescriptiveContentNotifRq",
|
||||||
|
"OtaHotelDescriptiveContentNotifRs",
|
||||||
|
"OtaHotelDescriptiveInfoRq",
|
||||||
|
"OtaHotelDescriptiveInfoRs",
|
||||||
|
"OtaHotelInvCountNotifRq",
|
||||||
|
"OtaHotelInvCountNotifRs",
|
||||||
|
"OtaHotelPostEventNotifRq",
|
||||||
|
"OtaHotelPostEventNotifRs",
|
||||||
|
"OtaHotelRatePlanNotifRq",
|
||||||
|
"OtaHotelRatePlanNotifRs",
|
||||||
|
"OtaHotelRatePlanRq",
|
||||||
|
"OtaHotelRatePlanRs",
|
||||||
|
"OtaHotelResNotifRq",
|
||||||
|
"OtaHotelResNotifRs",
|
||||||
|
"OtaNotifReportRq",
|
||||||
|
"OtaNotifReportRs",
|
||||||
|
"OtaPingRq",
|
||||||
|
"OtaPingRs",
|
||||||
|
"OtaReadRq",
|
||||||
|
"OtaResRetrieveRs",
|
||||||
|
"OccupancyAgeQualifyingCode",
|
||||||
|
"PositionAltitudeUnitOfMeasureCode",
|
||||||
|
"PrerequisiteInventoryInvType",
|
||||||
|
"ProfileProfileType",
|
||||||
|
"RateDescriptionName",
|
||||||
|
"RatePlanRatePlanNotifType",
|
||||||
|
"RatePlanRatePlanType",
|
||||||
|
"RateRateTimeUnit",
|
||||||
|
"RestrictionStatusRestriction",
|
||||||
|
"RestrictionStatusStatus",
|
||||||
|
"RoomTypeRoomType",
|
||||||
|
"ServiceMealPlanCode",
|
||||||
|
"ServiceServiceCategoryCode",
|
||||||
|
"ServiceServicePricingType",
|
||||||
|
"ServiceType",
|
||||||
|
"SpecialRequestCodeContext",
|
||||||
|
"StayRequirementStayContext",
|
||||||
|
"SupplementAddToBasicRateIndicator",
|
||||||
|
"SupplementChargeTypeCode",
|
||||||
|
"SupplementInvType",
|
||||||
|
"TaxPolicyChargeFrequency",
|
||||||
|
"TaxPolicyChargeUnit",
|
||||||
|
"TaxPolicyCode",
|
||||||
|
"TextTextFormat1",
|
||||||
|
"TextTextFormat2",
|
||||||
|
"TimeUnitType",
|
||||||
|
"TypeRoomRoomType",
|
||||||
|
"UrlType",
|
||||||
|
"UniqueIdInstance",
|
||||||
|
"UniqueIdType1",
|
||||||
|
"UniqueIdType2",
|
||||||
|
"UniqueIdType3",
|
||||||
|
"VideoItemCategory",
|
||||||
|
"WarningStatus",
|
||||||
|
"DefSendComplete",
|
||||||
|
]
|
||||||
13613
src/alpine_bits_python/generated/alpinebits.py
Normal file
13613
src/alpine_bits_python/generated/alpinebits.py
Normal file
File diff suppressed because it is too large
Load Diff
195
src/alpine_bits_python/main.py
Normal file
195
src/alpine_bits_python/main.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
from .alpinebits_guestrequests import ResGuest, RoomStay
|
||||||
|
from .generated import alpinebits as ab
|
||||||
|
from io import BytesIO
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import re
|
||||||
|
from xsdata_pydantic.bindings import XmlSerializer
|
||||||
|
|
||||||
|
from .simplified_access import (
|
||||||
|
CommentData,
|
||||||
|
CommentsData,
|
||||||
|
CommentListItemData,
|
||||||
|
CustomerData,
|
||||||
|
|
||||||
|
HotelReservationIdData,
|
||||||
|
PhoneTechType,
|
||||||
|
AlpineBitsFactory,
|
||||||
|
OtaMessageType
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Success - use None instead of object() for cleaner XML output
|
||||||
|
success = None
|
||||||
|
|
||||||
|
# UniqueID
|
||||||
|
unique_id = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId(
|
||||||
|
type_value=ab.UniqueIdType2.VALUE_14, id="6b34fe24ac2ff811"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TimeSpan - use the actual nested class
|
||||||
|
|
||||||
|
start_date_window = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan.StartDateWindow(
|
||||||
|
earliest_date="2024-10-01", latest_date="2024-10-02"
|
||||||
|
)
|
||||||
|
|
||||||
|
time_span = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan(
|
||||||
|
start_date_window=start_date_window
|
||||||
|
)
|
||||||
|
|
||||||
|
# RoomStay with TimeSpan
|
||||||
|
room_stay = (
|
||||||
|
ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay(
|
||||||
|
time_span=time_span
|
||||||
|
)
|
||||||
|
)
|
||||||
|
room_stays = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays(
|
||||||
|
room_stay=[room_stay]
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_data = CustomerData(
|
||||||
|
given_name="John",
|
||||||
|
surname="Doe",
|
||||||
|
name_prefix="Mr.",
|
||||||
|
phone_numbers=[
|
||||||
|
("+1234567890", PhoneTechType.MOBILE), # Phone number with type
|
||||||
|
("+0987654321", None), # Phone number without type
|
||||||
|
],
|
||||||
|
email_address="john.doe@example.com",
|
||||||
|
email_newsletter=True,
|
||||||
|
address_line="123 Main Street",
|
||||||
|
city_name="Anytown",
|
||||||
|
postal_code="12345",
|
||||||
|
country_code="US",
|
||||||
|
address_catalog=False,
|
||||||
|
gender="Male",
|
||||||
|
birth_date="1980-01-01",
|
||||||
|
language="en",
|
||||||
|
)
|
||||||
|
|
||||||
|
alpine_bits_factory = AlpineBitsFactory()
|
||||||
|
|
||||||
|
res_guests = alpine_bits_factory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
hotel_res_id_data = HotelReservationIdData(
|
||||||
|
res_id_type="13",
|
||||||
|
res_id_value=None,
|
||||||
|
res_id_source=None,
|
||||||
|
res_id_source_context="99tales",
|
||||||
|
)
|
||||||
|
# Create HotelReservationId using the factory
|
||||||
|
hotel_res_id = alpine_bits_factory.create(hotel_res_id_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
# Use the actual nested HotelReservationIds class
|
||||||
|
hotel_res_ids = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds(
|
||||||
|
hotel_reservation_id=[hotel_res_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Basic property info
|
||||||
|
basic_property_info = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.BasicPropertyInfo(
|
||||||
|
hotel_code="123", hotel_name="Frangart Inn"
|
||||||
|
)
|
||||||
|
|
||||||
|
comment = CommentData(
|
||||||
|
name= ab.CommentName2.CUSTOMER_COMMENT,
|
||||||
|
text="This is a sample comment.",
|
||||||
|
list_items=[CommentListItemData(
|
||||||
|
value="Landing page comment",
|
||||||
|
language="en",
|
||||||
|
list_item="1",
|
||||||
|
)],
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
comment2 = CommentData(
|
||||||
|
name= ab.CommentName2.ADDITIONAL_INFO,
|
||||||
|
text="This is a special request comment.",
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
comments_data = CommentsData(comments=[comment, comment2])
|
||||||
|
|
||||||
|
|
||||||
|
comments = alpine_bits_factory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ResGlobalInfo
|
||||||
|
res_global_info = (
|
||||||
|
ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo(
|
||||||
|
hotel_reservation_ids=hotel_res_ids, basic_property_info=basic_property_info, comments=comments
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hotel Reservation
|
||||||
|
hotel_reservation = ab.OtaResRetrieveRs.ReservationsList.HotelReservation(
|
||||||
|
create_date_time=datetime.now(timezone.utc).isoformat(),
|
||||||
|
res_status=ab.HotelReservationResStatus.REQUESTED,
|
||||||
|
room_stay_reservation="true",
|
||||||
|
unique_id=unique_id,
|
||||||
|
room_stays=room_stays,
|
||||||
|
res_guests=res_guests,
|
||||||
|
res_global_info=res_global_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
reservations_list = ab.OtaResRetrieveRs.ReservationsList(
|
||||||
|
hotel_reservation=[hotel_reservation]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Root element
|
||||||
|
ota_res_retrieve_rs = ab.OtaResRetrieveRs(
|
||||||
|
version="7.000", success=success, reservations_list=reservations_list
|
||||||
|
)
|
||||||
|
|
||||||
|
# Serialize using Pydantic's model_dump and convert to XML
|
||||||
|
try:
|
||||||
|
# First validate the model
|
||||||
|
ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump())
|
||||||
|
print("✅ Pydantic validation successful!")
|
||||||
|
|
||||||
|
# For XML serialization with Pydantic models, we need to use xsdata-pydantic serializer
|
||||||
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||||
|
|
||||||
|
config = SerializerConfig(
|
||||||
|
pretty_print=True, xml_declaration=True, encoding="UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = XmlSerializer(config=config)
|
||||||
|
|
||||||
|
# Use ns_map to control namespace prefixes - set default namespace
|
||||||
|
ns_map = {None: "http://www.opentravel.org/OTA/2003/05"}
|
||||||
|
xml_string = serializer.render(ota_res_retrieve_rs, ns_map=ns_map)
|
||||||
|
|
||||||
|
with open("output.xml", "w", encoding="utf-8") as outfile:
|
||||||
|
outfile.write(xml_string)
|
||||||
|
|
||||||
|
print("✅ XML serialization successful!")
|
||||||
|
print(f"Generated XML written to output.xml")
|
||||||
|
|
||||||
|
# Also print the pretty formatted XML to console
|
||||||
|
print("\n📄 Generated XML:")
|
||||||
|
print(xml_string)
|
||||||
|
|
||||||
|
# Test parsing back
|
||||||
|
from xsdata_pydantic.bindings import XmlParser
|
||||||
|
|
||||||
|
parser = XmlParser()
|
||||||
|
|
||||||
|
with open("output.xml", "r", encoding="utf-8") as infile:
|
||||||
|
xml_content = infile.read()
|
||||||
|
|
||||||
|
parsed_result = parser.from_string(xml_content, ab.OtaResRetrieveRs)
|
||||||
|
|
||||||
|
print("✅ Round-trip validation successful!")
|
||||||
|
print(
|
||||||
|
f"Parsed reservation status: {parsed_result.reservations_list.hotel_reservation[0].res_status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Validation/Serialization failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
56
src/alpine_bits_python/output.xml
Normal file
56
src/alpine_bits_python/output.xml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OTA_ResRetrieveRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="7.000">
|
||||||
|
<ReservationsList>
|
||||||
|
<HotelReservation CreateDateTime="2025-09-24T15:07:57.451427+00:00" ResStatus="Requested" RoomStayReservation="true">
|
||||||
|
<UniqueID Type="14" ID="6b34fe24ac2ff811"/>
|
||||||
|
<RoomStays>
|
||||||
|
<RoomStay>
|
||||||
|
<TimeSpan>
|
||||||
|
<StartDateWindow EarliestDate="2024-10-01" LatestDate="2024-10-02"/>
|
||||||
|
</TimeSpan>
|
||||||
|
</RoomStay>
|
||||||
|
</RoomStays>
|
||||||
|
<ResGuests>
|
||||||
|
<ResGuest>
|
||||||
|
<Profiles>
|
||||||
|
<ProfileInfo>
|
||||||
|
<Profile>
|
||||||
|
<Customer Gender="Male" BirthDate="1980-01-01" Language="en">
|
||||||
|
<PersonName>
|
||||||
|
<NamePrefix>Mr.</NamePrefix>
|
||||||
|
<GivenName>John</GivenName>
|
||||||
|
<Surname>Doe</Surname>
|
||||||
|
</PersonName>
|
||||||
|
<Telephone PhoneTechType="5" PhoneNumber="+1234567890"/>
|
||||||
|
<Telephone PhoneNumber="+0987654321"/>
|
||||||
|
<Email Remark="newsletter:yes">john.doe@example.com</Email>
|
||||||
|
<Address Remark="catalog:no">
|
||||||
|
<AddressLine>123 Main Street</AddressLine>
|
||||||
|
<CityName>Anytown</CityName>
|
||||||
|
<PostalCode>12345</PostalCode>
|
||||||
|
<CountryName Code="US"/>
|
||||||
|
</Address>
|
||||||
|
</Customer>
|
||||||
|
</Profile>
|
||||||
|
</ProfileInfo>
|
||||||
|
</Profiles>
|
||||||
|
</ResGuest>
|
||||||
|
</ResGuests>
|
||||||
|
<ResGlobalInfo>
|
||||||
|
<Comments>
|
||||||
|
<Comment Name="customer comment">
|
||||||
|
<ListItem ListItem="1" Language="en">Landing page comment</ListItem>
|
||||||
|
<Text>This is a sample comment.</Text>
|
||||||
|
</Comment>
|
||||||
|
<Comment Name="additional info">
|
||||||
|
<Text>This is a special request comment.</Text>
|
||||||
|
</Comment>
|
||||||
|
</Comments>
|
||||||
|
<HotelReservationIDs>
|
||||||
|
<HotelReservationID ResID_Type="13" ResID_SourceContext="99tales"/>
|
||||||
|
</HotelReservationIDs>
|
||||||
|
<BasicPropertyInfo HotelCode="123" HotelName="Frangart Inn"/>
|
||||||
|
</ResGlobalInfo>
|
||||||
|
</HotelReservation>
|
||||||
|
</ReservationsList>
|
||||||
|
</OTA_ResRetrieveRS>
|
||||||
740
src/alpine_bits_python/simplified_access.py
Normal file
740
src/alpine_bits_python/simplified_access.py
Normal file
@@ -0,0 +1,740 @@
|
|||||||
|
from typing import Union, Optional, Any, TypeVar
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# Import the generated classes
|
||||||
|
from .generated.alpinebits import OtaHotelResNotifRq, OtaResRetrieveRs, CommentName2
|
||||||
|
|
||||||
|
# Define type aliases for the two Customer types
|
||||||
|
NotifCustomer = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer
|
||||||
|
RetrieveCustomer = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer
|
||||||
|
|
||||||
|
# Define type aliases for HotelReservationId types
|
||||||
|
NotifHotelReservationId = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.HotelReservationIds.HotelReservationId
|
||||||
|
RetrieveHotelReservationId = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds.HotelReservationId
|
||||||
|
|
||||||
|
# Define type aliases for Comments types
|
||||||
|
NotifComments = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments
|
||||||
|
RetrieveComments = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments
|
||||||
|
NotifComment = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||||
|
RetrieveComment = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
RETRIEVE = "retrieve" # For OtaResRetrieveRs
|
||||||
|
|
||||||
|
|
||||||
|
@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 CustomerFactory:
|
||||||
|
"""Factory class to create Customer instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_notif_customer(data: CustomerData) -> NotifCustomer:
|
||||||
|
"""Create a Customer for OtaHotelResNotifRq."""
|
||||||
|
return CustomerFactory._create_customer(NotifCustomer, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_retrieve_customer(data: CustomerData) -> RetrieveCustomer:
|
||||||
|
"""Create a Customer for OtaResRetrieveRs."""
|
||||||
|
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."""
|
||||||
|
|
||||||
|
# Create PersonName
|
||||||
|
person_name = customer_class.PersonName(
|
||||||
|
given_name=data.given_name,
|
||||||
|
surname=data.surname,
|
||||||
|
name_prefix=data.name_prefix,
|
||||||
|
name_title=data.name_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create telephone list
|
||||||
|
telephones = []
|
||||||
|
for phone_number, phone_tech_type in data.phone_numbers:
|
||||||
|
telephone = customer_class.Telephone(
|
||||||
|
phone_number=phone_number,
|
||||||
|
phone_tech_type=phone_tech_type.value if phone_tech_type else None,
|
||||||
|
)
|
||||||
|
telephones.append(telephone)
|
||||||
|
|
||||||
|
# Create email if provided
|
||||||
|
email = None
|
||||||
|
if data.email_address:
|
||||||
|
remark = None
|
||||||
|
if data.email_newsletter is not None:
|
||||||
|
remark = f"newsletter:{'yes' if data.email_newsletter else 'no'}"
|
||||||
|
|
||||||
|
email = customer_class.Email(value=data.email_address, remark=remark)
|
||||||
|
|
||||||
|
# Create address if any address fields are provided
|
||||||
|
address = None
|
||||||
|
if any(
|
||||||
|
[data.address_line, data.city_name, data.postal_code, data.country_code]
|
||||||
|
):
|
||||||
|
country_name = None
|
||||||
|
if data.country_code:
|
||||||
|
country_name = customer_class.Address.CountryName(
|
||||||
|
code=data.country_code
|
||||||
|
)
|
||||||
|
|
||||||
|
address_remark = None
|
||||||
|
if data.address_catalog is not None:
|
||||||
|
address_remark = f"catalog:{'yes' if data.address_catalog else 'no'}"
|
||||||
|
|
||||||
|
address = customer_class.Address(
|
||||||
|
address_line=data.address_line,
|
||||||
|
city_name=data.city_name,
|
||||||
|
postal_code=data.postal_code,
|
||||||
|
country_name=country_name,
|
||||||
|
remark=address_remark,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the customer
|
||||||
|
return customer_class(
|
||||||
|
person_name=person_name,
|
||||||
|
telephone=telephones,
|
||||||
|
email=email,
|
||||||
|
address=address,
|
||||||
|
gender=data.gender,
|
||||||
|
birth_date=data.birth_date,
|
||||||
|
language=data.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_notif_customer(customer: NotifCustomer) -> CustomerData:
|
||||||
|
"""Convert a NotifCustomer back to CustomerData."""
|
||||||
|
return CustomerFactory._customer_to_data(customer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_retrieve_customer(customer: RetrieveCustomer) -> CustomerData:
|
||||||
|
"""Convert a RetrieveCustomer back to CustomerData."""
|
||||||
|
return CustomerFactory._customer_to_data(customer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _customer_to_data(customer: Any) -> CustomerData:
|
||||||
|
"""Internal method to convert any customer type to CustomerData."""
|
||||||
|
|
||||||
|
# Extract phone numbers
|
||||||
|
phone_numbers = []
|
||||||
|
if customer.telephone:
|
||||||
|
for tel in customer.telephone:
|
||||||
|
phone_numbers.append(
|
||||||
|
(
|
||||||
|
tel.phone_number,
|
||||||
|
PhoneTechType(tel.phone_tech_type)
|
||||||
|
if tel.phone_tech_type
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract email info
|
||||||
|
email_address = None
|
||||||
|
email_newsletter = None
|
||||||
|
if customer.email:
|
||||||
|
email_address = customer.email.value
|
||||||
|
if customer.email.remark:
|
||||||
|
if "newsletter:yes" in customer.email.remark:
|
||||||
|
email_newsletter = True
|
||||||
|
elif "newsletter:no" in customer.email.remark:
|
||||||
|
email_newsletter = False
|
||||||
|
|
||||||
|
# Extract address info
|
||||||
|
address_line = None
|
||||||
|
city_name = None
|
||||||
|
postal_code = None
|
||||||
|
country_code = None
|
||||||
|
address_catalog = None
|
||||||
|
|
||||||
|
if customer.address:
|
||||||
|
address_line = customer.address.address_line
|
||||||
|
city_name = customer.address.city_name
|
||||||
|
postal_code = customer.address.postal_code
|
||||||
|
|
||||||
|
if customer.address.country_name:
|
||||||
|
country_code = customer.address.country_name.code
|
||||||
|
|
||||||
|
if customer.address.remark:
|
||||||
|
if "catalog:yes" in customer.address.remark:
|
||||||
|
address_catalog = True
|
||||||
|
elif "catalog:no" in customer.address.remark:
|
||||||
|
address_catalog = False
|
||||||
|
|
||||||
|
return CustomerData(
|
||||||
|
given_name=customer.person_name.given_name,
|
||||||
|
surname=customer.person_name.surname,
|
||||||
|
name_prefix=customer.person_name.name_prefix,
|
||||||
|
name_title=customer.person_name.name_title,
|
||||||
|
phone_numbers=phone_numbers,
|
||||||
|
email_address=email_address,
|
||||||
|
email_newsletter=email_newsletter,
|
||||||
|
address_line=address_line,
|
||||||
|
city_name=city_name,
|
||||||
|
postal_code=postal_code,
|
||||||
|
country_code=country_code,
|
||||||
|
address_catalog=address_catalog,
|
||||||
|
gender=customer.gender,
|
||||||
|
birth_date=customer.birth_date,
|
||||||
|
language=customer.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HotelReservationIdData:
|
||||||
|
"""Simple data class to hold hotel reservation ID information without nested type constraints."""
|
||||||
|
|
||||||
|
res_id_type: str # Required field - pattern: [0-9]+
|
||||||
|
res_id_value: None | str = None # Max 64 characters
|
||||||
|
res_id_source: None | str = None # Max 64 characters
|
||||||
|
res_id_source_context: None | str = None # Max 64 characters
|
||||||
|
|
||||||
|
|
||||||
|
class HotelReservationIdFactory:
|
||||||
|
"""Factory class to create HotelReservationId instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_notif_hotel_reservation_id(
|
||||||
|
data: HotelReservationIdData,
|
||||||
|
) -> NotifHotelReservationId:
|
||||||
|
"""Create a HotelReservationId for OtaHotelResNotifRq."""
|
||||||
|
return HotelReservationIdFactory._create_hotel_reservation_id(
|
||||||
|
NotifHotelReservationId, data
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_retrieve_hotel_reservation_id(
|
||||||
|
data: HotelReservationIdData,
|
||||||
|
) -> RetrieveHotelReservationId:
|
||||||
|
"""Create a HotelReservationId for OtaResRetrieveRs."""
|
||||||
|
return HotelReservationIdFactory._create_hotel_reservation_id(
|
||||||
|
RetrieveHotelReservationId, data
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_hotel_reservation_id(
|
||||||
|
hotel_reservation_id_class: type, data: HotelReservationIdData
|
||||||
|
) -> Any:
|
||||||
|
"""Internal method to create a hotel reservation id of the specified type."""
|
||||||
|
return hotel_reservation_id_class(
|
||||||
|
res_id_type=data.res_id_type,
|
||||||
|
res_id_value=data.res_id_value,
|
||||||
|
res_id_source=data.res_id_source,
|
||||||
|
res_id_source_context=data.res_id_source_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_notif_hotel_reservation_id(
|
||||||
|
hotel_reservation_id: NotifHotelReservationId,
|
||||||
|
) -> HotelReservationIdData:
|
||||||
|
"""Convert a NotifHotelReservationId back to HotelReservationIdData."""
|
||||||
|
return HotelReservationIdFactory._hotel_reservation_id_to_data(
|
||||||
|
hotel_reservation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_retrieve_hotel_reservation_id(
|
||||||
|
hotel_reservation_id: RetrieveHotelReservationId,
|
||||||
|
) -> HotelReservationIdData:
|
||||||
|
"""Convert a RetrieveHotelReservationId back to HotelReservationIdData."""
|
||||||
|
return HotelReservationIdFactory._hotel_reservation_id_to_data(
|
||||||
|
hotel_reservation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hotel_reservation_id_to_data(
|
||||||
|
hotel_reservation_id: Any,
|
||||||
|
) -> HotelReservationIdData:
|
||||||
|
"""Internal method to convert any hotel reservation id type to HotelReservationIdData."""
|
||||||
|
return HotelReservationIdData(
|
||||||
|
res_id_type=hotel_reservation_id.res_id_type,
|
||||||
|
res_id_value=hotel_reservation_id.res_id_value,
|
||||||
|
res_id_source=hotel_reservation_id.res_id_source,
|
||||||
|
res_id_source_context=hotel_reservation_id.res_id_source_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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: Optional[str] = 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."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_notif_comments(data: CommentsData) -> NotifComments:
|
||||||
|
"""Create Comments for OtaHotelResNotifRq."""
|
||||||
|
return CommentFactory._create_comments(NotifComments, NotifComment, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_retrieve_comments(data: CommentsData) -> RetrieveComments:
|
||||||
|
"""Create Comments for OtaResRetrieveRs."""
|
||||||
|
return CommentFactory._create_comments(RetrieveComments, RetrieveComment, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_comments(comments_class: type, comment_class: type, data: CommentsData) -> Any:
|
||||||
|
"""Internal method to create comments of the specified type."""
|
||||||
|
|
||||||
|
comments_list = []
|
||||||
|
for comment_data in data.comments:
|
||||||
|
# Create list items
|
||||||
|
list_items = []
|
||||||
|
for item_data in comment_data.list_items:
|
||||||
|
list_item = comment_class.ListItem(
|
||||||
|
value=item_data.value,
|
||||||
|
list_item=item_data.list_item,
|
||||||
|
language=item_data.language
|
||||||
|
)
|
||||||
|
list_items.append(list_item)
|
||||||
|
|
||||||
|
# Create comment
|
||||||
|
comment = comment_class(
|
||||||
|
name=comment_data.name,
|
||||||
|
text=comment_data.text,
|
||||||
|
list_item=list_items
|
||||||
|
)
|
||||||
|
comments_list.append(comment)
|
||||||
|
|
||||||
|
# Create comments container
|
||||||
|
return comments_class(comment=comments_list)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_notif_comments(comments: NotifComments) -> CommentsData:
|
||||||
|
"""Convert NotifComments back to CommentsData."""
|
||||||
|
return CommentFactory._comments_to_data(comments)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_retrieve_comments(comments: RetrieveComments) -> CommentsData:
|
||||||
|
"""Convert RetrieveComments back to CommentsData."""
|
||||||
|
return CommentFactory._comments_to_data(comments)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _comments_to_data(comments: Any) -> CommentsData:
|
||||||
|
"""Internal method to convert any comments type to CommentsData."""
|
||||||
|
|
||||||
|
comments_data_list = []
|
||||||
|
for comment in comments.comment:
|
||||||
|
# Extract list items
|
||||||
|
list_items_data = []
|
||||||
|
if comment.list_item:
|
||||||
|
for list_item in comment.list_item:
|
||||||
|
list_items_data.append(CommentListItemData(
|
||||||
|
value=list_item.value,
|
||||||
|
list_item=list_item.list_item,
|
||||||
|
language=list_item.language
|
||||||
|
))
|
||||||
|
|
||||||
|
# Extract comment data
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
# Define type aliases for ResGuests types
|
||||||
|
NotifResGuests = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests
|
||||||
|
RetrieveResGuests = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGuests
|
||||||
|
|
||||||
|
|
||||||
|
class ResGuestFactory:
|
||||||
|
"""Factory class to create complete ResGuests structures with a primary customer."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_notif_res_guests(customer_data: CustomerData) -> NotifResGuests:
|
||||||
|
"""Create a complete ResGuests structure for OtaHotelResNotifRq with primary customer."""
|
||||||
|
return ResGuestFactory._create_res_guests(
|
||||||
|
NotifResGuests, NotifCustomer, customer_data
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_retrieve_res_guests(customer_data: CustomerData) -> RetrieveResGuests:
|
||||||
|
"""Create a complete ResGuests structure for OtaResRetrieveRs with primary customer."""
|
||||||
|
return ResGuestFactory._create_res_guests(
|
||||||
|
RetrieveResGuests, RetrieveCustomer, customer_data
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_res_guests(
|
||||||
|
res_guests_class: type, customer_class: type, customer_data: CustomerData
|
||||||
|
) -> Any:
|
||||||
|
"""Internal method to create complete ResGuests structure."""
|
||||||
|
|
||||||
|
# Create the customer using the existing CustomerFactory
|
||||||
|
customer = CustomerFactory._create_customer(customer_class, customer_data)
|
||||||
|
|
||||||
|
# Create Profile with the customer
|
||||||
|
profile = res_guests_class.ResGuest.Profiles.ProfileInfo.Profile(
|
||||||
|
customer=customer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create ProfileInfo with the profile
|
||||||
|
profile_info = res_guests_class.ResGuest.Profiles.ProfileInfo(profile=profile)
|
||||||
|
|
||||||
|
# Create Profiles with the profile_info
|
||||||
|
profiles = res_guests_class.ResGuest.Profiles(profile_info=profile_info)
|
||||||
|
|
||||||
|
# Create ResGuest with the profiles
|
||||||
|
res_guest = res_guests_class.ResGuest(profiles=profiles)
|
||||||
|
|
||||||
|
# Create ResGuests with the res_guest
|
||||||
|
return res_guests_class(res_guest=res_guest)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_primary_customer(
|
||||||
|
res_guests: Union[NotifResGuests, RetrieveResGuests],
|
||||||
|
) -> CustomerData:
|
||||||
|
"""Extract the primary customer data from a ResGuests structure."""
|
||||||
|
|
||||||
|
# Navigate down the nested structure to get the customer
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
|
||||||
|
# Use the existing CustomerFactory conversion method
|
||||||
|
if isinstance(res_guests, NotifResGuests):
|
||||||
|
return CustomerFactory.from_notif_customer(customer)
|
||||||
|
else:
|
||||||
|
return CustomerFactory.from_retrieve_customer(customer)
|
||||||
|
|
||||||
|
|
||||||
|
class AlpineBitsFactory:
|
||||||
|
"""Unified factory class for creating AlpineBits objects with a simple interface."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(data: Union[CustomerData, HotelReservationIdData, CommentsData], message_type: OtaMessageType) -> Any:
|
||||||
|
"""
|
||||||
|
Create an AlpineBits object based on the data type and message type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: The data object (CustomerData, HotelReservationIdData, CommentsData, etc.)
|
||||||
|
message_type: Whether to create for NOTIF or RETRIEVE message types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The appropriate AlpineBits object based on the data type and message type
|
||||||
|
"""
|
||||||
|
if isinstance(data, CustomerData):
|
||||||
|
if message_type == OtaMessageType.NOTIF:
|
||||||
|
return CustomerFactory.create_notif_customer(data)
|
||||||
|
else:
|
||||||
|
return CustomerFactory.create_retrieve_customer(data)
|
||||||
|
|
||||||
|
elif isinstance(data, HotelReservationIdData):
|
||||||
|
if message_type == OtaMessageType.NOTIF:
|
||||||
|
return HotelReservationIdFactory.create_notif_hotel_reservation_id(data)
|
||||||
|
else:
|
||||||
|
return HotelReservationIdFactory.create_retrieve_hotel_reservation_id(data)
|
||||||
|
|
||||||
|
elif isinstance(data, CommentsData):
|
||||||
|
if message_type == OtaMessageType.NOTIF:
|
||||||
|
return CommentFactory.create_notif_comments(data)
|
||||||
|
else:
|
||||||
|
return CommentFactory.create_retrieve_comments(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported data type: {type(data)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_res_guests(customer_data: CustomerData, message_type: OtaMessageType) -> Union[NotifResGuests, RetrieveResGuests]:
|
||||||
|
"""
|
||||||
|
Create a complete ResGuests structure with a primary customer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer_data: The customer data
|
||||||
|
message_type: Whether to create for NOTIF or RETRIEVE message types
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The appropriate ResGuests object
|
||||||
|
"""
|
||||||
|
if message_type == OtaMessageType.NOTIF:
|
||||||
|
return ResGuestFactory.create_notif_res_guests(customer_data)
|
||||||
|
else:
|
||||||
|
return ResGuestFactory.create_retrieve_res_guests(customer_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_data(obj: Any) -> Union[CustomerData, HotelReservationIdData, CommentsData]:
|
||||||
|
"""
|
||||||
|
Extract data from an AlpineBits object back to a simple data class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The AlpineBits object to extract data from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The appropriate data object
|
||||||
|
"""
|
||||||
|
# Check if it's a Customer object
|
||||||
|
if hasattr(obj, 'person_name') and hasattr(obj.person_name, 'given_name'):
|
||||||
|
if isinstance(obj, NotifCustomer):
|
||||||
|
return CustomerFactory.from_notif_customer(obj)
|
||||||
|
elif isinstance(obj, RetrieveCustomer):
|
||||||
|
return CustomerFactory.from_retrieve_customer(obj)
|
||||||
|
|
||||||
|
# Check if it's a HotelReservationId object
|
||||||
|
elif hasattr(obj, 'res_id_type'):
|
||||||
|
if isinstance(obj, NotifHotelReservationId):
|
||||||
|
return HotelReservationIdFactory.from_notif_hotel_reservation_id(obj)
|
||||||
|
elif isinstance(obj, RetrieveHotelReservationId):
|
||||||
|
return HotelReservationIdFactory.from_retrieve_hotel_reservation_id(obj)
|
||||||
|
|
||||||
|
# Check if it's a Comments object
|
||||||
|
elif hasattr(obj, 'comment'):
|
||||||
|
if isinstance(obj, NotifComments):
|
||||||
|
return CommentFactory.from_notif_comments(obj)
|
||||||
|
elif isinstance(obj, RetrieveComments):
|
||||||
|
return CommentFactory.from_retrieve_comments(obj)
|
||||||
|
|
||||||
|
# Check if it's a ResGuests object
|
||||||
|
elif hasattr(obj, 'res_guest'):
|
||||||
|
return ResGuestFactory.extract_primary_customer(obj)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported object type: {type(obj)}")
|
||||||
|
|
||||||
|
|
||||||
|
# Usage examples
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Create customer data using simple data class
|
||||||
|
customer_data = CustomerData(
|
||||||
|
given_name="John",
|
||||||
|
surname="Doe",
|
||||||
|
name_prefix="Mr.",
|
||||||
|
phone_numbers=[
|
||||||
|
("+1234567890", PhoneTechType.MOBILE), # Phone number with type
|
||||||
|
("+0987654321", None), # Phone number without type
|
||||||
|
],
|
||||||
|
email_address="john.doe@example.com",
|
||||||
|
email_newsletter=True,
|
||||||
|
address_line="123 Main Street",
|
||||||
|
city_name="Anytown",
|
||||||
|
postal_code="12345",
|
||||||
|
country_code="US",
|
||||||
|
address_catalog=False,
|
||||||
|
gender="Male",
|
||||||
|
birth_date="1980-01-01",
|
||||||
|
language="en",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create customer for OtaHotelResNotifRq
|
||||||
|
notif_customer = CustomerFactory.create_notif_customer(customer_data)
|
||||||
|
print(
|
||||||
|
"Created NotifCustomer:",
|
||||||
|
notif_customer.person_name.given_name,
|
||||||
|
notif_customer.person_name.surname,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create customer for OtaResRetrieveRs
|
||||||
|
retrieve_customer = CustomerFactory.create_retrieve_customer(customer_data)
|
||||||
|
print(
|
||||||
|
"Created RetrieveCustomer:",
|
||||||
|
retrieve_customer.person_name.given_name,
|
||||||
|
retrieve_customer.person_name.surname,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert back to data class
|
||||||
|
converted_data = CustomerFactory.from_notif_customer(notif_customer)
|
||||||
|
print("Converted back to data:", converted_data.given_name, converted_data.surname)
|
||||||
|
|
||||||
|
# Verify they contain the same information
|
||||||
|
print("Original and converted data match:", customer_data == converted_data)
|
||||||
|
|
||||||
|
print("\n--- HotelReservationIdFactory Examples ---")
|
||||||
|
|
||||||
|
# Create hotel reservation ID data
|
||||||
|
reservation_id_data = HotelReservationIdData(
|
||||||
|
res_id_type="123",
|
||||||
|
res_id_value="RESERVATION-456",
|
||||||
|
res_id_source="HOTEL_SYSTEM",
|
||||||
|
res_id_source_context="BOOKING_ENGINE",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create HotelReservationId for both types
|
||||||
|
notif_res_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(
|
||||||
|
reservation_id_data
|
||||||
|
)
|
||||||
|
retrieve_res_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||||
|
reservation_id_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Created NotifHotelReservationId:",
|
||||||
|
notif_res_id.res_id_type,
|
||||||
|
notif_res_id.res_id_value,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"Created RetrieveHotelReservationId:",
|
||||||
|
retrieve_res_id.res_id_type,
|
||||||
|
retrieve_res_id.res_id_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert back to data class
|
||||||
|
converted_res_id_data = HotelReservationIdFactory.from_notif_hotel_reservation_id(
|
||||||
|
notif_res_id
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"Converted back to reservation ID data:",
|
||||||
|
converted_res_id_data.res_id_type,
|
||||||
|
converted_res_id_data.res_id_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify they contain the same information
|
||||||
|
print(
|
||||||
|
"Original and converted reservation ID data match:",
|
||||||
|
reservation_id_data == converted_res_id_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n--- ResGuestFactory Examples ---")
|
||||||
|
|
||||||
|
# Create complete ResGuests structure for OtaHotelResNotifRq - much simpler!
|
||||||
|
notif_res_guests = ResGuestFactory.create_notif_res_guests(customer_data)
|
||||||
|
print(
|
||||||
|
"Created NotifResGuests with customer:",
|
||||||
|
notif_res_guests.res_guest.profiles.profile_info.profile.customer.person_name.given_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create complete ResGuests structure for OtaResRetrieveRs - much simpler!
|
||||||
|
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(customer_data)
|
||||||
|
print(
|
||||||
|
"Created RetrieveResGuests with customer:",
|
||||||
|
retrieve_res_guests.res_guest.profiles.profile_info.profile.customer.person_name.given_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract primary customer data back from ResGuests structure
|
||||||
|
extracted_data = ResGuestFactory.extract_primary_customer(retrieve_res_guests)
|
||||||
|
print("Extracted customer data:", extracted_data.given_name, extracted_data.surname)
|
||||||
|
|
||||||
|
# Verify roundtrip conversion
|
||||||
|
print("Roundtrip conversion successful:", customer_data == extracted_data)
|
||||||
|
|
||||||
|
print("\n--- Unified AlpineBitsFactory Examples ---")
|
||||||
|
|
||||||
|
# Much simpler approach - single factory with enum parameter!
|
||||||
|
print("=== Customer Creation ===")
|
||||||
|
notif_customer = AlpineBitsFactory.create(customer_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_customer = AlpineBitsFactory.create(customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
print("Created customers using unified factory")
|
||||||
|
|
||||||
|
print("=== HotelReservationId Creation ===")
|
||||||
|
reservation_id_data = HotelReservationIdData(
|
||||||
|
res_id_type="123",
|
||||||
|
res_id_value="RESERVATION-456",
|
||||||
|
res_id_source="HOTEL_SYSTEM"
|
||||||
|
)
|
||||||
|
notif_res_id = AlpineBitsFactory.create(reservation_id_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_res_id = AlpineBitsFactory.create(reservation_id_data, OtaMessageType.RETRIEVE)
|
||||||
|
print("Created reservation IDs using unified factory")
|
||||||
|
|
||||||
|
print("=== Comments Creation ===")
|
||||||
|
comments_data = CommentsData(comments=[
|
||||||
|
CommentData(
|
||||||
|
name=CommentName2.CUSTOMER_COMMENT,
|
||||||
|
text="This is a customer comment about the reservation",
|
||||||
|
list_items=[
|
||||||
|
CommentListItemData(
|
||||||
|
value="Special dietary requirements: vegetarian",
|
||||||
|
list_item="1",
|
||||||
|
language="en"
|
||||||
|
),
|
||||||
|
CommentListItemData(
|
||||||
|
value="Late arrival expected",
|
||||||
|
list_item="2",
|
||||||
|
language="en"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
CommentData(
|
||||||
|
name=CommentName2.ADDITIONAL_INFO,
|
||||||
|
text="Additional information about the stay"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
notif_comments = AlpineBitsFactory.create(comments_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_comments = AlpineBitsFactory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||||
|
print("Created comments using unified factory")
|
||||||
|
|
||||||
|
print("=== ResGuests Creation ===")
|
||||||
|
notif_res_guests = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_res_guests = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
print("Created ResGuests using unified factory")
|
||||||
|
|
||||||
|
print("=== Data Extraction ===")
|
||||||
|
# Extract data back using unified interface
|
||||||
|
extracted_customer_data = AlpineBitsFactory.extract_data(notif_customer)
|
||||||
|
extracted_res_id_data = AlpineBitsFactory.extract_data(notif_res_id)
|
||||||
|
extracted_comments_data = AlpineBitsFactory.extract_data(retrieve_comments)
|
||||||
|
extracted_from_res_guests = AlpineBitsFactory.extract_data(retrieve_res_guests)
|
||||||
|
|
||||||
|
print("Data extraction successful:")
|
||||||
|
print("- Customer roundtrip:", customer_data == extracted_customer_data)
|
||||||
|
print("- ReservationId roundtrip:", reservation_id_data == extracted_res_id_data)
|
||||||
|
print("- Comments roundtrip:", comments_data == extracted_comments_data)
|
||||||
|
print("- ResGuests roundtrip:", customer_data == extracted_from_res_guests)
|
||||||
|
|
||||||
|
print("\n--- Comparison with old approach ---")
|
||||||
|
print("Old way required multiple imports and knowing specific factory methods")
|
||||||
|
print("New way: single import, single factory, enum parameter to specify type!")
|
||||||
1
src/alpine_bits_python/util/__init__.py
Normal file
1
src/alpine_bits_python/util/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Utility functions for alpine_bits_python."""
|
||||||
5
src/alpine_bits_python/util/__main__.py
Normal file
5
src/alpine_bits_python/util/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Entry point for util package."""
|
||||||
|
from .handshake_util import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
52
src/alpine_bits_python/util/handshake_util.py
Normal file
52
src/alpine_bits_python/util/handshake_util.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from ..generated.alpinebits import OtaPingRq, OtaPingRs
|
||||||
|
from xsdata_pydantic.bindings import XmlParser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# test parsing a ping request sample
|
||||||
|
|
||||||
|
path = "AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRS.xml"
|
||||||
|
|
||||||
|
with open(
|
||||||
|
path, "r", encoding="utf-8") as f:
|
||||||
|
xml = f.read()
|
||||||
|
|
||||||
|
# Parse the XML into the request object
|
||||||
|
|
||||||
|
# Test parsing back
|
||||||
|
|
||||||
|
|
||||||
|
parser = XmlParser()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parsed_result = parser.from_string(xml, OtaPingRs)
|
||||||
|
|
||||||
|
print(parsed_result.echo_data)
|
||||||
|
|
||||||
|
warning = parsed_result.warnings.warning[0]
|
||||||
|
|
||||||
|
print(warning.type_value)
|
||||||
|
|
||||||
|
print(type(warning.content))
|
||||||
|
|
||||||
|
print(warning.content[0])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# save json in echo_data to file with indents
|
||||||
|
output_path = "echo_data_response.json"
|
||||||
|
with open(output_path, "w", encoding="utf-8") as out_f:
|
||||||
|
import json
|
||||||
|
json.dump(json.loads(parsed_result.echo_data), out_f, indent=4)
|
||||||
|
print(f"Saved echo_data json to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
118
src/main.py
118
src/main.py
@@ -1,118 +0,0 @@
|
|||||||
from alpinebits_guestrequests import ResGuest, RoomStay
|
|
||||||
from alpine_bits_classes import (
|
|
||||||
OTA_ResRetrieveRS,
|
|
||||||
Success,
|
|
||||||
ReservationsListType,
|
|
||||||
HotelReservationType,
|
|
||||||
UniqueIDType,
|
|
||||||
RoomStaysType,
|
|
||||||
RoomStayType,
|
|
||||||
RoomTypesType,
|
|
||||||
RoomTypeType,
|
|
||||||
GuestCountsType,
|
|
||||||
GuestCountType,
|
|
||||||
TimeSpanType,
|
|
||||||
StartDateWindowType,
|
|
||||||
ResGuestsType,
|
|
||||||
ResGuestType,
|
|
||||||
ProfilesType,
|
|
||||||
ProfileInfoType,
|
|
||||||
ProfileType,
|
|
||||||
CustomerType,
|
|
||||||
PersonNameType,
|
|
||||||
AddressType,
|
|
||||||
CountryNameType,
|
|
||||||
ResGlobalInfoType,
|
|
||||||
HotelReservationIDsType,
|
|
||||||
HotelReservationIDType,
|
|
||||||
BasicPropertyInfoType,
|
|
||||||
GivenNameType,
|
|
||||||
SurnameType,
|
|
||||||
)
|
|
||||||
from io import BytesIO
|
|
||||||
import sys
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Success
|
|
||||||
success = Success()
|
|
||||||
|
|
||||||
# RoomType
|
|
||||||
room_type = RoomTypeType(RoomTypeCode="A", RoomClassificationCode="5", RoomType="8")
|
|
||||||
room_types = RoomTypesType(RoomType=room_type)
|
|
||||||
|
|
||||||
# GuestCounts
|
|
||||||
guest_count = GuestCountType(Count=1)
|
|
||||||
guest_counts = GuestCountsType(GuestCount=[guest_count])
|
|
||||||
|
|
||||||
# TimeSpan with StartDateWindow
|
|
||||||
start_date_window = StartDateWindowType(EarliestDate=datetime(2022, 10, 3, tzinfo=timezone.utc), LatestDate=datetime(2022, 10, 8, tzinfo=timezone.utc))
|
|
||||||
time_span = TimeSpanType(StartDateWindow=start_date_window)
|
|
||||||
|
|
||||||
# RoomStay
|
|
||||||
room_stay = RoomStayType(
|
|
||||||
RoomTypes=room_types,
|
|
||||||
GuestCounts=guest_counts,
|
|
||||||
TimeSpan=time_span
|
|
||||||
)
|
|
||||||
room_stays = RoomStaysType(RoomStay=[room_stay])
|
|
||||||
|
|
||||||
# ResGuest
|
|
||||||
person_name = PersonNameType(GivenName=GivenNameType("Otto"), Surname=SurnameType("Mustermann"))
|
|
||||||
country_name = CountryNameType(Code="DE")
|
|
||||||
address = AddressType(CountryName=country_name)
|
|
||||||
customer = CustomerType(Language="de", Gender="Unknown", PersonName=person_name, Address=address)
|
|
||||||
profile = ProfileType(Customer=customer)
|
|
||||||
profile_info = ProfileInfoType(Profile=profile)
|
|
||||||
profiles = ProfilesType(ProfileInfo=profile_info)
|
|
||||||
res_guest = ResGuestType(Profiles=profiles)
|
|
||||||
res_guests = ResGuestsType(ResGuest=res_guest)
|
|
||||||
|
|
||||||
# UniqueID
|
|
||||||
unique_id = UniqueIDType(Type="14", ID="6b34fe24ac2ff811")
|
|
||||||
|
|
||||||
# ResGlobalInfo
|
|
||||||
hotel_res_id = HotelReservationIDType(
|
|
||||||
ResID_Type="13",
|
|
||||||
ResID_SourceContext="cnt",
|
|
||||||
ResID_Value="res",
|
|
||||||
ResID_Source="www.example.com"
|
|
||||||
)
|
|
||||||
hotel_res_ids = HotelReservationIDsType(HotelReservationID=[hotel_res_id])
|
|
||||||
basic_property_info = BasicPropertyInfoType(HotelCode="123", HotelName="Frangart Inn")
|
|
||||||
res_global_info = ResGlobalInfoType(
|
|
||||||
HotelReservationIDs=hotel_res_ids,
|
|
||||||
BasicPropertyInfo=basic_property_info
|
|
||||||
)
|
|
||||||
|
|
||||||
# HotelReservation
|
|
||||||
hotel_reservation = HotelReservationType(
|
|
||||||
CreateDateTime=datetime.now(timezone.utc).isoformat(),
|
|
||||||
ResStatus="Requested",
|
|
||||||
RoomStayReservation=True,
|
|
||||||
UniqueID=unique_id,
|
|
||||||
RoomStays=room_stays,
|
|
||||||
ResGuests=res_guests,
|
|
||||||
ResGlobalInfo=res_global_info
|
|
||||||
)
|
|
||||||
|
|
||||||
hotel_reservation.set_CreateDateTime(datetime.now(timezone.utc))
|
|
||||||
reservations_list = ReservationsListType(HotelReservation=[hotel_reservation])
|
|
||||||
|
|
||||||
# Root element
|
|
||||||
ota_res_retrieve_rs = OTA_ResRetrieveRS(
|
|
||||||
Version="7.000",
|
|
||||||
Success=success,
|
|
||||||
ReservationsList=reservations_list
|
|
||||||
)
|
|
||||||
|
|
||||||
# Serialize to XML string
|
|
||||||
# create outfile
|
|
||||||
|
|
||||||
with open('output.xml', 'w', encoding='utf-8') as outfile:
|
|
||||||
ota_res_retrieve_rs.export(outfile, 0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<OTA_ResRetrieveRS xmlns:None="http://www.opentravel.org/OTA/2003/05" Version="7.000">
|
|
||||||
<Success><Success/>
|
|
||||||
</Success>
|
|
||||||
<ReservationsList>
|
|
||||||
<HotelReservation CreateDateTime="2025-09-24T07:18:57.913865Z" ResStatus="Requested" RoomStayReservation="true">
|
|
||||||
<UniqueID Type="14" ID="6b34fe24ac2ff811"/>
|
|
||||||
<RoomStays>
|
|
||||||
<RoomStay>
|
|
||||||
<RoomTypes>
|
|
||||||
<RoomType RoomTypeCode="A" RoomClassificationCode="5" RoomType="8"/>
|
|
||||||
</RoomTypes>
|
|
||||||
<GuestCounts>
|
|
||||||
<GuestCount Count="1"/>
|
|
||||||
</GuestCounts>
|
|
||||||
<TimeSpan>
|
|
||||||
<StartDateWindow EarliestDate="2022-10-03Z" LatestDate="2022-10-08Z"/>
|
|
||||||
</TimeSpan>
|
|
||||||
</RoomStay>
|
|
||||||
</RoomStays>
|
|
||||||
<ResGuests>
|
|
||||||
<ResGuest>
|
|
||||||
<Profiles>
|
|
||||||
<ProfileInfo>
|
|
||||||
<Profile>
|
|
||||||
<Customer Gender="Unknown" Language="de">
|
|
||||||
<PersonName>
|
|
||||||
<GivenName>Otto</GivenName>
|
|
||||||
<Surname>Mustermann</Surname>
|
|
||||||
</PersonName>
|
|
||||||
<Address>
|
|
||||||
<CountryName Code="DE"/>
|
|
||||||
</Address>
|
|
||||||
</Customer>
|
|
||||||
</Profile>
|
|
||||||
</ProfileInfo>
|
|
||||||
</Profiles>
|
|
||||||
</ResGuest>
|
|
||||||
</ResGuests>
|
|
||||||
<ResGlobalInfo>
|
|
||||||
<HotelReservationIDs>
|
|
||||||
<HotelReservationID ResID_Type="13" ResID_Value="res" ResID_Source="www.example.com" ResID_SourceContext="cnt"/>
|
|
||||||
</HotelReservationIDs>
|
|
||||||
<BasicPropertyInfo HotelCode="123" HotelName="Frangart Inn"/>
|
|
||||||
</ResGlobalInfo>
|
|
||||||
</HotelReservation>
|
|
||||||
</ReservationsList>
|
|
||||||
</OTA_ResRetrieveRS>
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
"""
|
|
||||||
Script to verify if all classes in alpine_bits_classes.py with the same base name (e.g., CustomerType, CustomerType125, CustomerType576)
|
|
||||||
are structurally identical. This helps to identify duplicate dataclasses generated by generateDS.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python src/postprocessing.py
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
- Only uses the standard library.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import re
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def get_class_basenames(classname):
|
|
||||||
"""Returns the base name of a class (e.g., CustomerType125 -> CustomerType)"""
|
|
||||||
return re.sub(r'\d+$', '', classname)
|
|
||||||
|
|
||||||
def extract_classes(filepath):
|
|
||||||
"""Parse the file and extract all class definitions as AST nodes."""
|
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
|
||||||
source = f.read()
|
|
||||||
tree = ast.parse(source)
|
|
||||||
classes = {}
|
|
||||||
for node in tree.body:
|
|
||||||
if isinstance(node, ast.ClassDef):
|
|
||||||
classes[node.name] = node
|
|
||||||
return classes
|
|
||||||
|
|
||||||
def class_struct_signature(class_node):
|
|
||||||
"""Return a tuple representing the structure of the class: base classes, method names, attribute names."""
|
|
||||||
bases = tuple(base.id if isinstance(base, ast.Name) else ast.dump(base) for base in class_node.bases)
|
|
||||||
methods = []
|
|
||||||
attrs = []
|
|
||||||
for item in class_node.body:
|
|
||||||
if isinstance(item, ast.FunctionDef):
|
|
||||||
methods.append(item.name)
|
|
||||||
elif isinstance(item, ast.Assign):
|
|
||||||
for target in item.targets:
|
|
||||||
if isinstance(target, ast.Name):
|
|
||||||
attrs.append(target.id)
|
|
||||||
return (bases, tuple(sorted(methods)), tuple(sorted(attrs)))
|
|
||||||
|
|
||||||
|
|
||||||
def remove_identical_class_suffixes(filepath: Path):
|
|
||||||
"""
|
|
||||||
Removes duplicate class definitions with numeric suffixes if they are structurally identical,
|
|
||||||
keeping only the base (unsuffixed) class.
|
|
||||||
"""
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
# Parse classes and group by base name
|
|
||||||
classes = extract_classes(filepath)
|
|
||||||
grouped = defaultdict(list)
|
|
||||||
for cname in classes:
|
|
||||||
base = get_class_basenames(cname)
|
|
||||||
grouped[base].append(cname)
|
|
||||||
|
|
||||||
# Find identical groups
|
|
||||||
identical = []
|
|
||||||
for base, classnames in grouped.items():
|
|
||||||
if len(classnames) > 1:
|
|
||||||
sigs = [class_struct_signature(classes[c]) for c in classnames]
|
|
||||||
if all(s == sigs[0] for s in sigs):
|
|
||||||
identical.append((base, classnames))
|
|
||||||
|
|
||||||
# Read original file lines
|
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
|
|
||||||
# Find line numbers for all class definitions
|
|
||||||
class_lines = {}
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
m = re.match(r'class (\w+)\b', line)
|
|
||||||
if m:
|
|
||||||
class_lines[m.group(1)] = i
|
|
||||||
|
|
||||||
# Mark classes to remove (all but the base, unsuffixed one)
|
|
||||||
to_remove = set()
|
|
||||||
for base, classnames in identical:
|
|
||||||
for cname in classnames:
|
|
||||||
if cname != base:
|
|
||||||
to_remove.add(cname)
|
|
||||||
|
|
||||||
# Remove class definitions with suffixes
|
|
||||||
new_lines = []
|
|
||||||
skip = False
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
m = re.match(r'class (\w+)\b', line)
|
|
||||||
if m and m.group(1) in to_remove:
|
|
||||||
skip = True
|
|
||||||
if not skip:
|
|
||||||
new_lines.append(line)
|
|
||||||
# End skipping at the next class or end of file
|
|
||||||
if skip and (i + 1 == len(lines) or re.match(r'class \w+\b', lines[i + 1])):
|
|
||||||
skip = False
|
|
||||||
|
|
||||||
# Backup original file
|
|
||||||
|
|
||||||
|
|
||||||
backup_path = filepath.with_suffix(filepath.suffix + ".bak")
|
|
||||||
|
|
||||||
shutil.copy(filepath, backup_path)
|
|
||||||
|
|
||||||
# Write cleaned file
|
|
||||||
with open(filepath, "w", encoding="utf-8") as f:
|
|
||||||
f.writelines(new_lines)
|
|
||||||
|
|
||||||
print(f"Removed {len(to_remove)} duplicate class definitions. Backup saved as {filepath}.bak")
|
|
||||||
|
|
||||||
# Example usage:
|
|
||||||
# remove_identical_class_suffixes("src/alpine_bits_classes.py")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
file_path = Path(__file__).parent / "alpine_bits_classes.py"
|
|
||||||
classes = extract_classes(file_path)
|
|
||||||
grouped = defaultdict(list)
|
|
||||||
for cname in classes:
|
|
||||||
base = get_class_basenames(cname)
|
|
||||||
grouped[base].append(cname)
|
|
||||||
|
|
||||||
identical = []
|
|
||||||
different = []
|
|
||||||
|
|
||||||
for base, classnames in grouped.items():
|
|
||||||
if len(classnames) > 1:
|
|
||||||
sigs = [class_struct_signature(classes[c]) for c in classnames]
|
|
||||||
if all(s == sigs[0] for s in sigs):
|
|
||||||
identical.append((base, classnames))
|
|
||||||
else:
|
|
||||||
different.append((base, classnames))
|
|
||||||
|
|
||||||
print("=== Structurally Identical Groups ===")
|
|
||||||
for base, classnames in identical:
|
|
||||||
print(f"{base}: {', '.join(classnames)}")
|
|
||||||
|
|
||||||
print("\n=== Structurally Different Groups ===")
|
|
||||||
for base, classnames in different:
|
|
||||||
print(f"{base}: {', '.join(classnames)}")
|
|
||||||
|
|
||||||
remove_identical_class_suffixes(file_path)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
5467
src/subclasses.py
5467
src/subclasses.py
File diff suppressed because it is too large
Load Diff
61
test/test_discovery.py
Normal file
61
test/test_discovery.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick test to demonstrate how the ServerCapabilities automatically
|
||||||
|
discovers implemented vs unimplemented actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alpine_bits_python.alpinebits_server import (
|
||||||
|
ServerCapabilities,
|
||||||
|
AlpineBitsAction,
|
||||||
|
AlpineBitsActionName,
|
||||||
|
Version,
|
||||||
|
AlpineBitsResponse,
|
||||||
|
HttpStatusCode
|
||||||
|
)
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
class NewImplementedAction(AlpineBitsAction):
|
||||||
|
"""A new action that IS implemented."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_INFO_INFO
|
||||||
|
self.version = Version.V2024_10
|
||||||
|
|
||||||
|
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||||
|
"""This action is implemented."""
|
||||||
|
return AlpineBitsResponse("Implemented!", HttpStatusCode.OK)
|
||||||
|
|
||||||
|
class NewUnimplementedAction(AlpineBitsAction):
|
||||||
|
"""A new action that is NOT implemented (no handle override)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO
|
||||||
|
self.version = Version.V2024_10
|
||||||
|
|
||||||
|
# Notice: No handle method override - will use default "not implemented"
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("🔍 Testing Action Discovery Logic")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Create capabilities and see what gets discovered
|
||||||
|
capabilities = ServerCapabilities()
|
||||||
|
|
||||||
|
print("📋 Actions found by discovery:")
|
||||||
|
for action_name in capabilities.get_supported_actions():
|
||||||
|
print(f" ✅ {action_name}")
|
||||||
|
|
||||||
|
print(f"\n📊 Total discovered: {len(capabilities.get_supported_actions())}")
|
||||||
|
|
||||||
|
# Test the new implemented action
|
||||||
|
implemented_action = NewImplementedAction()
|
||||||
|
result = await implemented_action.handle("test", "<xml/>", Version.V2024_10)
|
||||||
|
print(f"\n🟢 NewImplementedAction result: {result.xml_content}")
|
||||||
|
|
||||||
|
# Test the unimplemented action (should use default behavior)
|
||||||
|
unimplemented_action = NewUnimplementedAction()
|
||||||
|
result = await unimplemented_action.handle("test", "<xml/>", Version.V2024_10)
|
||||||
|
print(f"🔴 NewUnimplementedAction result: {result.xml_content}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
609
test/test_simplified_access.py
Normal file
609
test/test_simplified_access.py
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
import pytest
|
||||||
|
from typing import Union
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the src directory to the path so we can import our modules
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||||
|
|
||||||
|
from simplified_access import (
|
||||||
|
CustomerData,
|
||||||
|
CustomerFactory,
|
||||||
|
ResGuestFactory,
|
||||||
|
HotelReservationIdData,
|
||||||
|
HotelReservationIdFactory,
|
||||||
|
AlpineBitsFactory,
|
||||||
|
PhoneTechType,
|
||||||
|
OtaMessageType,
|
||||||
|
NotifCustomer,
|
||||||
|
RetrieveCustomer,
|
||||||
|
NotifResGuests,
|
||||||
|
RetrieveResGuests,
|
||||||
|
NotifHotelReservationId,
|
||||||
|
RetrieveHotelReservationId
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_customer_data():
|
||||||
|
"""Fixture providing sample customer data for testing."""
|
||||||
|
return CustomerData(
|
||||||
|
given_name="John",
|
||||||
|
surname="Doe",
|
||||||
|
name_prefix="Mr.",
|
||||||
|
name_title="Jr.",
|
||||||
|
phone_numbers=[
|
||||||
|
("+1234567890", PhoneTechType.MOBILE),
|
||||||
|
("+0987654321", PhoneTechType.VOICE),
|
||||||
|
("+1111111111", None)
|
||||||
|
],
|
||||||
|
email_address="john.doe@example.com",
|
||||||
|
email_newsletter=True,
|
||||||
|
address_line="123 Main Street",
|
||||||
|
city_name="Anytown",
|
||||||
|
postal_code="12345",
|
||||||
|
country_code="US",
|
||||||
|
address_catalog=False,
|
||||||
|
gender="Male",
|
||||||
|
birth_date="1980-01-01",
|
||||||
|
language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def minimal_customer_data():
|
||||||
|
"""Fixture providing minimal customer data (only required fields)."""
|
||||||
|
return CustomerData(
|
||||||
|
given_name="Jane",
|
||||||
|
surname="Smith"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_hotel_reservation_id_data():
|
||||||
|
"""Fixture providing sample hotel reservation ID data for testing."""
|
||||||
|
return HotelReservationIdData(
|
||||||
|
res_id_type="123",
|
||||||
|
res_id_value="RESERVATION-456",
|
||||||
|
res_id_source="HOTEL_SYSTEM",
|
||||||
|
res_id_source_context="BOOKING_ENGINE"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def minimal_hotel_reservation_id_data():
|
||||||
|
"""Fixture providing minimal hotel reservation ID data (only required fields)."""
|
||||||
|
return HotelReservationIdData(
|
||||||
|
res_id_type="999"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomerData:
|
||||||
|
"""Test the CustomerData dataclass."""
|
||||||
|
|
||||||
|
def test_customer_data_creation_full(self, sample_customer_data):
|
||||||
|
"""Test creating CustomerData with all fields."""
|
||||||
|
assert sample_customer_data.given_name == "John"
|
||||||
|
assert sample_customer_data.surname == "Doe"
|
||||||
|
assert sample_customer_data.name_prefix == "Mr."
|
||||||
|
assert sample_customer_data.email_address == "john.doe@example.com"
|
||||||
|
assert sample_customer_data.email_newsletter is True
|
||||||
|
assert len(sample_customer_data.phone_numbers) == 3
|
||||||
|
|
||||||
|
def test_customer_data_creation_minimal(self, minimal_customer_data):
|
||||||
|
"""Test creating CustomerData with only required fields."""
|
||||||
|
assert minimal_customer_data.given_name == "Jane"
|
||||||
|
assert minimal_customer_data.surname == "Smith"
|
||||||
|
assert minimal_customer_data.phone_numbers == []
|
||||||
|
assert minimal_customer_data.email_address is None
|
||||||
|
assert minimal_customer_data.address_line is None
|
||||||
|
|
||||||
|
def test_phone_numbers_default_initialization(self):
|
||||||
|
"""Test that phone_numbers gets initialized to empty list."""
|
||||||
|
customer_data = CustomerData(given_name="Test", surname="User")
|
||||||
|
assert customer_data.phone_numbers == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomerFactory:
|
||||||
|
"""Test the CustomerFactory class."""
|
||||||
|
|
||||||
|
def test_create_notif_customer_full(self, sample_customer_data):
|
||||||
|
"""Test creating a NotifCustomer with full data."""
|
||||||
|
customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||||
|
|
||||||
|
assert isinstance(customer, NotifCustomer)
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
assert customer.person_name.name_prefix == "Mr."
|
||||||
|
assert customer.person_name.name_title == "Jr."
|
||||||
|
|
||||||
|
# Check telephone
|
||||||
|
assert len(customer.telephone) == 3
|
||||||
|
assert customer.telephone[0].phone_number == "+1234567890"
|
||||||
|
assert customer.telephone[0].phone_tech_type == "5" # MOBILE
|
||||||
|
assert customer.telephone[1].phone_tech_type == "1" # VOICE
|
||||||
|
assert customer.telephone[2].phone_tech_type is None
|
||||||
|
|
||||||
|
# Check email
|
||||||
|
assert customer.email.value == "john.doe@example.com"
|
||||||
|
assert customer.email.remark == "newsletter:yes"
|
||||||
|
|
||||||
|
# Check address
|
||||||
|
assert customer.address.address_line == "123 Main Street"
|
||||||
|
assert customer.address.city_name == "Anytown"
|
||||||
|
assert customer.address.postal_code == "12345"
|
||||||
|
assert customer.address.country_name.code == "US"
|
||||||
|
assert customer.address.remark == "catalog:no"
|
||||||
|
|
||||||
|
# Check other attributes
|
||||||
|
assert customer.gender == "Male"
|
||||||
|
assert customer.birth_date == "1980-01-01"
|
||||||
|
assert customer.language == "en"
|
||||||
|
|
||||||
|
def test_create_retrieve_customer_full(self, sample_customer_data):
|
||||||
|
"""Test creating a RetrieveCustomer with full data."""
|
||||||
|
customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||||
|
|
||||||
|
assert isinstance(customer, RetrieveCustomer)
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
# Same structure as NotifCustomer, so we don't need to test all fields again
|
||||||
|
|
||||||
|
def test_create_customer_minimal(self, minimal_customer_data):
|
||||||
|
"""Test creating customers with minimal data."""
|
||||||
|
notif_customer = CustomerFactory.create_notif_customer(minimal_customer_data)
|
||||||
|
retrieve_customer = CustomerFactory.create_retrieve_customer(minimal_customer_data)
|
||||||
|
|
||||||
|
for customer in [notif_customer, retrieve_customer]:
|
||||||
|
assert customer.person_name.given_name == "Jane"
|
||||||
|
assert customer.person_name.surname == "Smith"
|
||||||
|
assert customer.person_name.name_prefix is None
|
||||||
|
assert customer.person_name.name_title is None
|
||||||
|
assert len(customer.telephone) == 0
|
||||||
|
assert customer.email is None
|
||||||
|
assert customer.address is None
|
||||||
|
assert customer.gender is None
|
||||||
|
assert customer.birth_date is None
|
||||||
|
assert customer.language is None
|
||||||
|
|
||||||
|
def test_email_newsletter_options(self):
|
||||||
|
"""Test different email newsletter options."""
|
||||||
|
# Newsletter yes
|
||||||
|
data_yes = CustomerData(given_name="Test", surname="User",
|
||||||
|
email_address="test@example.com", email_newsletter=True)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_yes)
|
||||||
|
assert customer.email.remark == "newsletter:yes"
|
||||||
|
|
||||||
|
# Newsletter no
|
||||||
|
data_no = CustomerData(given_name="Test", surname="User",
|
||||||
|
email_address="test@example.com", email_newsletter=False)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_no)
|
||||||
|
assert customer.email.remark == "newsletter:no"
|
||||||
|
|
||||||
|
# Newsletter not specified
|
||||||
|
data_none = CustomerData(given_name="Test", surname="User",
|
||||||
|
email_address="test@example.com", email_newsletter=None)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_none)
|
||||||
|
assert customer.email.remark is None
|
||||||
|
|
||||||
|
def test_address_catalog_options(self):
|
||||||
|
"""Test different address catalog options."""
|
||||||
|
# Catalog no
|
||||||
|
data_no = CustomerData(given_name="Test", surname="User",
|
||||||
|
address_line="123 Street", address_catalog=False)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_no)
|
||||||
|
assert customer.address.remark == "catalog:no"
|
||||||
|
|
||||||
|
# Catalog yes
|
||||||
|
data_yes = CustomerData(given_name="Test", surname="User",
|
||||||
|
address_line="123 Street", address_catalog=True)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_yes)
|
||||||
|
assert customer.address.remark == "catalog:yes"
|
||||||
|
|
||||||
|
# Catalog not specified
|
||||||
|
data_none = CustomerData(given_name="Test", surname="User",
|
||||||
|
address_line="123 Street", address_catalog=None)
|
||||||
|
customer = CustomerFactory.create_notif_customer(data_none)
|
||||||
|
assert customer.address.remark is None
|
||||||
|
|
||||||
|
def test_from_notif_customer_roundtrip(self, sample_customer_data):
|
||||||
|
"""Test converting NotifCustomer back to CustomerData."""
|
||||||
|
customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||||
|
converted_data = CustomerFactory.from_notif_customer(customer)
|
||||||
|
|
||||||
|
assert converted_data == sample_customer_data
|
||||||
|
|
||||||
|
def test_from_retrieve_customer_roundtrip(self, sample_customer_data):
|
||||||
|
"""Test converting RetrieveCustomer back to CustomerData."""
|
||||||
|
customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||||
|
converted_data = CustomerFactory.from_retrieve_customer(customer)
|
||||||
|
|
||||||
|
assert converted_data == sample_customer_data
|
||||||
|
|
||||||
|
def test_phone_tech_type_conversion(self):
|
||||||
|
"""Test that PhoneTechType enum values are properly converted."""
|
||||||
|
data = CustomerData(
|
||||||
|
given_name="Test",
|
||||||
|
surname="User",
|
||||||
|
phone_numbers=[
|
||||||
|
("+1111111111", PhoneTechType.VOICE),
|
||||||
|
("+2222222222", PhoneTechType.FAX),
|
||||||
|
("+3333333333", PhoneTechType.MOBILE)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
customer = CustomerFactory.create_notif_customer(data)
|
||||||
|
assert customer.telephone[0].phone_tech_type == "1" # VOICE
|
||||||
|
assert customer.telephone[1].phone_tech_type == "3" # FAX
|
||||||
|
assert customer.telephone[2].phone_tech_type == "5" # MOBILE
|
||||||
|
|
||||||
|
|
||||||
|
class TestHotelReservationIdData:
|
||||||
|
"""Test the HotelReservationIdData dataclass."""
|
||||||
|
|
||||||
|
def test_hotel_reservation_id_data_creation_full(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test creating HotelReservationIdData with all fields."""
|
||||||
|
assert sample_hotel_reservation_id_data.res_id_type == "123"
|
||||||
|
assert sample_hotel_reservation_id_data.res_id_value == "RESERVATION-456"
|
||||||
|
assert sample_hotel_reservation_id_data.res_id_source == "HOTEL_SYSTEM"
|
||||||
|
assert sample_hotel_reservation_id_data.res_id_source_context == "BOOKING_ENGINE"
|
||||||
|
|
||||||
|
def test_hotel_reservation_id_data_creation_minimal(self, minimal_hotel_reservation_id_data):
|
||||||
|
"""Test creating HotelReservationIdData with only required fields."""
|
||||||
|
assert minimal_hotel_reservation_id_data.res_id_type == "999"
|
||||||
|
assert minimal_hotel_reservation_id_data.res_id_value is None
|
||||||
|
assert minimal_hotel_reservation_id_data.res_id_source is None
|
||||||
|
assert minimal_hotel_reservation_id_data.res_id_source_context is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestHotelReservationIdFactory:
|
||||||
|
"""Test the HotelReservationIdFactory class."""
|
||||||
|
|
||||||
|
def test_create_notif_hotel_reservation_id_full(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test creating a NotifHotelReservationId with full data."""
|
||||||
|
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
|
||||||
|
assert isinstance(reservation_id, NotifHotelReservationId)
|
||||||
|
assert reservation_id.res_id_type == "123"
|
||||||
|
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||||
|
assert reservation_id.res_id_source == "HOTEL_SYSTEM"
|
||||||
|
assert reservation_id.res_id_source_context == "BOOKING_ENGINE"
|
||||||
|
|
||||||
|
def test_create_retrieve_hotel_reservation_id_full(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test creating a RetrieveHotelReservationId with full data."""
|
||||||
|
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
|
||||||
|
assert isinstance(reservation_id, RetrieveHotelReservationId)
|
||||||
|
assert reservation_id.res_id_type == "123"
|
||||||
|
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||||
|
assert reservation_id.res_id_source == "HOTEL_SYSTEM"
|
||||||
|
assert reservation_id.res_id_source_context == "BOOKING_ENGINE"
|
||||||
|
|
||||||
|
def test_create_hotel_reservation_id_minimal(self, minimal_hotel_reservation_id_data):
|
||||||
|
"""Test creating hotel reservation IDs with minimal data."""
|
||||||
|
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(minimal_hotel_reservation_id_data)
|
||||||
|
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(minimal_hotel_reservation_id_data)
|
||||||
|
|
||||||
|
for reservation_id in [notif_reservation_id, retrieve_reservation_id]:
|
||||||
|
assert reservation_id.res_id_type == "999"
|
||||||
|
assert reservation_id.res_id_value is None
|
||||||
|
assert reservation_id.res_id_source is None
|
||||||
|
assert reservation_id.res_id_source_context is None
|
||||||
|
|
||||||
|
def test_from_notif_hotel_reservation_id_roundtrip(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test converting NotifHotelReservationId back to HotelReservationIdData."""
|
||||||
|
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
converted_data = HotelReservationIdFactory.from_notif_hotel_reservation_id(reservation_id)
|
||||||
|
|
||||||
|
assert converted_data == sample_hotel_reservation_id_data
|
||||||
|
|
||||||
|
def test_from_retrieve_hotel_reservation_id_roundtrip(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test converting RetrieveHotelReservationId back to HotelReservationIdData."""
|
||||||
|
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
converted_data = HotelReservationIdFactory.from_retrieve_hotel_reservation_id(reservation_id)
|
||||||
|
|
||||||
|
assert converted_data == sample_hotel_reservation_id_data
|
||||||
|
|
||||||
|
|
||||||
|
class TestResGuestFactory:
|
||||||
|
"""Test the ResGuestFactory class."""
|
||||||
|
|
||||||
|
def test_create_notif_res_guests(self, sample_customer_data):
|
||||||
|
"""Test creating NotifResGuests structure."""
|
||||||
|
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||||
|
|
||||||
|
assert isinstance(res_guests, NotifResGuests)
|
||||||
|
|
||||||
|
# Navigate down the nested structure
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
assert customer.email.value == "john.doe@example.com"
|
||||||
|
|
||||||
|
def test_create_retrieve_res_guests(self, sample_customer_data):
|
||||||
|
"""Test creating RetrieveResGuests structure."""
|
||||||
|
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||||
|
|
||||||
|
assert isinstance(res_guests, RetrieveResGuests)
|
||||||
|
|
||||||
|
# Navigate down the nested structure
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
assert customer.email.value == "john.doe@example.com"
|
||||||
|
|
||||||
|
def test_create_res_guests_minimal(self, minimal_customer_data):
|
||||||
|
"""Test creating ResGuests with minimal customer data."""
|
||||||
|
notif_res_guests = ResGuestFactory.create_notif_res_guests(minimal_customer_data)
|
||||||
|
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(minimal_customer_data)
|
||||||
|
|
||||||
|
for res_guests in [notif_res_guests, retrieve_res_guests]:
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
assert customer.person_name.given_name == "Jane"
|
||||||
|
assert customer.person_name.surname == "Smith"
|
||||||
|
assert customer.email is None
|
||||||
|
assert customer.address is None
|
||||||
|
|
||||||
|
def test_extract_primary_customer_notif(self, sample_customer_data):
|
||||||
|
"""Test extracting primary customer from NotifResGuests."""
|
||||||
|
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||||
|
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||||
|
|
||||||
|
assert extracted_data == sample_customer_data
|
||||||
|
|
||||||
|
def test_extract_primary_customer_retrieve(self, sample_customer_data):
|
||||||
|
"""Test extracting primary customer from RetrieveResGuests."""
|
||||||
|
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||||
|
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||||
|
|
||||||
|
assert extracted_data == sample_customer_data
|
||||||
|
|
||||||
|
def test_roundtrip_conversion_notif(self, sample_customer_data):
|
||||||
|
"""Test complete roundtrip: CustomerData -> NotifResGuests -> CustomerData."""
|
||||||
|
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||||
|
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||||
|
|
||||||
|
assert extracted_data == sample_customer_data
|
||||||
|
|
||||||
|
def test_roundtrip_conversion_retrieve(self, sample_customer_data):
|
||||||
|
"""Test complete roundtrip: CustomerData -> RetrieveResGuests -> CustomerData."""
|
||||||
|
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||||
|
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||||
|
|
||||||
|
assert extracted_data == sample_customer_data
|
||||||
|
|
||||||
|
|
||||||
|
class TestPhoneTechType:
|
||||||
|
"""Test the PhoneTechType enum."""
|
||||||
|
|
||||||
|
def test_enum_values(self):
|
||||||
|
"""Test that enum values are correct."""
|
||||||
|
assert PhoneTechType.VOICE.value == "1"
|
||||||
|
assert PhoneTechType.FAX.value == "3"
|
||||||
|
assert PhoneTechType.MOBILE.value == "5"
|
||||||
|
|
||||||
|
|
||||||
|
class TestAlpineBitsFactory:
|
||||||
|
"""Test the unified AlpineBitsFactory class."""
|
||||||
|
|
||||||
|
def test_create_customer_notif(self, sample_customer_data):
|
||||||
|
"""Test creating customer using unified factory for NOTIF."""
|
||||||
|
customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.NOTIF)
|
||||||
|
assert isinstance(customer, NotifCustomer)
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
|
||||||
|
def test_create_customer_retrieve(self, sample_customer_data):
|
||||||
|
"""Test creating customer using unified factory for RETRIEVE."""
|
||||||
|
customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
assert isinstance(customer, RetrieveCustomer)
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
assert customer.person_name.surname == "Doe"
|
||||||
|
|
||||||
|
def test_create_hotel_reservation_id_notif(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test creating hotel reservation ID using unified factory for NOTIF."""
|
||||||
|
reservation_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.NOTIF)
|
||||||
|
assert isinstance(reservation_id, NotifHotelReservationId)
|
||||||
|
assert reservation_id.res_id_type == "123"
|
||||||
|
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||||
|
|
||||||
|
def test_create_hotel_reservation_id_retrieve(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test creating hotel reservation ID using unified factory for RETRIEVE."""
|
||||||
|
reservation_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE)
|
||||||
|
assert isinstance(reservation_id, RetrieveHotelReservationId)
|
||||||
|
assert reservation_id.res_id_type == "123"
|
||||||
|
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||||
|
|
||||||
|
def test_create_res_guests_notif(self, sample_customer_data):
|
||||||
|
"""Test creating ResGuests using unified factory for NOTIF."""
|
||||||
|
res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.NOTIF)
|
||||||
|
assert isinstance(res_guests, NotifResGuests)
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
|
||||||
|
def test_create_res_guests_retrieve(self, sample_customer_data):
|
||||||
|
"""Test creating ResGuests using unified factory for RETRIEVE."""
|
||||||
|
res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
assert isinstance(res_guests, RetrieveResGuests)
|
||||||
|
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
assert customer.person_name.given_name == "John"
|
||||||
|
|
||||||
|
def test_extract_data_from_customer(self, sample_customer_data):
|
||||||
|
"""Test extracting data from customer objects."""
|
||||||
|
# Create both types and extract data back
|
||||||
|
notif_customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
notif_extracted = AlpineBitsFactory.extract_data(notif_customer)
|
||||||
|
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_customer)
|
||||||
|
|
||||||
|
assert notif_extracted == sample_customer_data
|
||||||
|
assert retrieve_extracted == sample_customer_data
|
||||||
|
|
||||||
|
def test_extract_data_from_hotel_reservation_id(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test extracting data from hotel reservation ID objects."""
|
||||||
|
# Create both types and extract data back
|
||||||
|
notif_res_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_res_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
notif_extracted = AlpineBitsFactory.extract_data(notif_res_id)
|
||||||
|
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_res_id)
|
||||||
|
|
||||||
|
assert notif_extracted == sample_hotel_reservation_id_data
|
||||||
|
assert retrieve_extracted == sample_hotel_reservation_id_data
|
||||||
|
|
||||||
|
def test_extract_data_from_res_guests(self, sample_customer_data):
|
||||||
|
"""Test extracting data from ResGuests objects."""
|
||||||
|
# Create both types and extract data back
|
||||||
|
notif_res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.NOTIF)
|
||||||
|
retrieve_res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
notif_extracted = AlpineBitsFactory.extract_data(notif_res_guests)
|
||||||
|
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_res_guests)
|
||||||
|
|
||||||
|
assert notif_extracted == sample_customer_data
|
||||||
|
assert retrieve_extracted == sample_customer_data
|
||||||
|
|
||||||
|
def test_unsupported_data_type_error(self):
|
||||||
|
"""Test that unsupported data types raise ValueError."""
|
||||||
|
with pytest.raises(ValueError, match="Unsupported data type"):
|
||||||
|
AlpineBitsFactory.create("invalid_data", OtaMessageType.NOTIF)
|
||||||
|
|
||||||
|
def test_unsupported_object_type_error(self):
|
||||||
|
"""Test that unsupported object types raise ValueError in extract_data."""
|
||||||
|
with pytest.raises(ValueError, match="Unsupported object type"):
|
||||||
|
AlpineBitsFactory.extract_data("invalid_object")
|
||||||
|
|
||||||
|
def test_complete_workflow_with_unified_factory(self):
|
||||||
|
"""Test a complete workflow using only the unified factory."""
|
||||||
|
# Original data
|
||||||
|
customer_data = CustomerData(
|
||||||
|
given_name="Unified",
|
||||||
|
surname="Factory",
|
||||||
|
email_address="unified@factory.com",
|
||||||
|
phone_numbers=[("+1234567890", PhoneTechType.MOBILE)]
|
||||||
|
)
|
||||||
|
|
||||||
|
reservation_data = HotelReservationIdData(
|
||||||
|
res_id_type="999",
|
||||||
|
res_id_value="UNIFIED-TEST"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create using unified factory
|
||||||
|
customer_notif = AlpineBitsFactory.create(customer_data, OtaMessageType.NOTIF)
|
||||||
|
customer_retrieve = AlpineBitsFactory.create(customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
res_id_notif = AlpineBitsFactory.create(reservation_data, OtaMessageType.NOTIF)
|
||||||
|
res_id_retrieve = AlpineBitsFactory.create(reservation_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
res_guests_notif = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.NOTIF)
|
||||||
|
res_guests_retrieve = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||||
|
|
||||||
|
# Extract everything back
|
||||||
|
extracted_customer_from_notif = AlpineBitsFactory.extract_data(customer_notif)
|
||||||
|
extracted_customer_from_retrieve = AlpineBitsFactory.extract_data(customer_retrieve)
|
||||||
|
|
||||||
|
extracted_res_id_from_notif = AlpineBitsFactory.extract_data(res_id_notif)
|
||||||
|
extracted_res_id_from_retrieve = AlpineBitsFactory.extract_data(res_id_retrieve)
|
||||||
|
|
||||||
|
extracted_from_res_guests_notif = AlpineBitsFactory.extract_data(res_guests_notif)
|
||||||
|
extracted_from_res_guests_retrieve = AlpineBitsFactory.extract_data(res_guests_retrieve)
|
||||||
|
|
||||||
|
# Verify everything matches
|
||||||
|
assert extracted_customer_from_notif == customer_data
|
||||||
|
assert extracted_customer_from_retrieve == customer_data
|
||||||
|
assert extracted_res_id_from_notif == reservation_data
|
||||||
|
assert extracted_res_id_from_retrieve == reservation_data
|
||||||
|
assert extracted_from_res_guests_notif == customer_data
|
||||||
|
assert extracted_from_res_guests_retrieve == customer_data
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegration:
|
||||||
|
"""Integration tests combining both factories."""
|
||||||
|
|
||||||
|
def test_both_factories_produce_same_customer_data(self, sample_customer_data):
|
||||||
|
"""Test that both factories can work with the same customer data."""
|
||||||
|
# Create using CustomerFactory
|
||||||
|
notif_customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||||
|
retrieve_customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||||
|
|
||||||
|
# Create using ResGuestFactory and extract customers
|
||||||
|
notif_res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||||
|
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||||
|
|
||||||
|
notif_from_res_guests = notif_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
retrieve_from_res_guests = retrieve_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||||
|
|
||||||
|
# Compare customer names (structure should be identical)
|
||||||
|
assert notif_customer.person_name.given_name == notif_from_res_guests.person_name.given_name
|
||||||
|
assert notif_customer.person_name.surname == notif_from_res_guests.person_name.surname
|
||||||
|
assert retrieve_customer.person_name.given_name == retrieve_from_res_guests.person_name.given_name
|
||||||
|
assert retrieve_customer.person_name.surname == retrieve_from_res_guests.person_name.surname
|
||||||
|
|
||||||
|
def test_hotel_reservation_id_factories_produce_same_data(self, sample_hotel_reservation_id_data):
|
||||||
|
"""Test that both HotelReservationId factories produce equivalent results."""
|
||||||
|
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||||
|
|
||||||
|
# Both should have the same field values
|
||||||
|
assert notif_reservation_id.res_id_type == retrieve_reservation_id.res_id_type
|
||||||
|
assert notif_reservation_id.res_id_value == retrieve_reservation_id.res_id_value
|
||||||
|
assert notif_reservation_id.res_id_source == retrieve_reservation_id.res_id_source
|
||||||
|
assert notif_reservation_id.res_id_source_context == retrieve_reservation_id.res_id_source_context
|
||||||
|
|
||||||
|
def test_complex_customer_workflow(self):
|
||||||
|
"""Test a complex workflow with multiple operations."""
|
||||||
|
# Create original data
|
||||||
|
original_data = CustomerData(
|
||||||
|
given_name="Alice",
|
||||||
|
surname="Johnson",
|
||||||
|
phone_numbers=[
|
||||||
|
("+1555123456", PhoneTechType.MOBILE),
|
||||||
|
("+1555654321", PhoneTechType.VOICE)
|
||||||
|
],
|
||||||
|
email_address="alice.johnson@company.com",
|
||||||
|
email_newsletter=False,
|
||||||
|
address_line="456 Business Ave",
|
||||||
|
city_name="Metropolis",
|
||||||
|
postal_code="67890",
|
||||||
|
country_code="CA",
|
||||||
|
address_catalog=True,
|
||||||
|
gender="Female",
|
||||||
|
language="fr"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create ResGuests for both types
|
||||||
|
notif_res_guests = ResGuestFactory.create_notif_res_guests(original_data)
|
||||||
|
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(original_data)
|
||||||
|
|
||||||
|
# Extract data back from both
|
||||||
|
notif_extracted = ResGuestFactory.extract_primary_customer(notif_res_guests)
|
||||||
|
retrieve_extracted = ResGuestFactory.extract_primary_customer(retrieve_res_guests)
|
||||||
|
|
||||||
|
# All should be equal
|
||||||
|
assert original_data == notif_extracted
|
||||||
|
assert original_data == retrieve_extracted
|
||||||
|
assert notif_extracted == retrieve_extracted
|
||||||
|
|
||||||
|
def test_complex_hotel_reservation_id_workflow(self):
|
||||||
|
"""Test a complex workflow with HotelReservationId operations."""
|
||||||
|
# Create original reservation ID data
|
||||||
|
original_data = HotelReservationIdData(
|
||||||
|
res_id_type="456",
|
||||||
|
res_id_value="COMPLEX-RESERVATION-789",
|
||||||
|
res_id_source="INTEGRATION_SYSTEM",
|
||||||
|
res_id_source_context="API_CALL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create HotelReservationId for both types
|
||||||
|
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(original_data)
|
||||||
|
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(original_data)
|
||||||
|
|
||||||
|
# Extract data back from both
|
||||||
|
notif_extracted = HotelReservationIdFactory.from_notif_hotel_reservation_id(notif_reservation_id)
|
||||||
|
retrieve_extracted = HotelReservationIdFactory.from_retrieve_hotel_reservation_id(retrieve_reservation_id)
|
||||||
|
|
||||||
|
# All should be equal
|
||||||
|
assert original_data == notif_extracted
|
||||||
|
assert original_data == retrieve_extracted
|
||||||
|
assert notif_extracted == retrieve_extracted
|
||||||
29
test_handshake.py
Normal file
29
test_handshake.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test the handshake functionality with the real AlpineBits sample file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from alpine_bits_python.alpinebits_server import AlpineBitsServer
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("🔄 Testing AlpineBits Handshake with Sample File")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Create server instance
|
||||||
|
server = AlpineBitsServer()
|
||||||
|
|
||||||
|
# Read the sample handshake request
|
||||||
|
with open("AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRQ.xml", "r") as f:
|
||||||
|
ping_request_xml = f.read()
|
||||||
|
|
||||||
|
print("📤 Sending handshake request...")
|
||||||
|
|
||||||
|
# Handle the ping request
|
||||||
|
response = await server.handle_request("OTA_Ping:Handshaking", ping_request_xml, "2024-10")
|
||||||
|
|
||||||
|
print(f"\n📥 Response Status: {response.status_code}")
|
||||||
|
print(f"📄 Response XML:\n{response.xml_content}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
314
uv.lock
generated
314
uv.lock
generated
@@ -5,16 +5,33 @@ requires-python = ">=3.13"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "alpine-bits-python-server"
|
name = "alpine-bits-python-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "generateds" },
|
{ name = "generateds" },
|
||||||
{ name = "lxml" },
|
{ name = "lxml" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "xsdata", extra = ["cli", "lxml", "soap"] },
|
||||||
|
{ name = "xsdata-pydantic", extra = ["cli", "lxml", "soap"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "generateds", specifier = ">=2.44.3" },
|
{ name = "generateds", specifier = ">=2.44.3" },
|
||||||
{ name = "lxml", specifier = ">=6.0.1" },
|
{ name = "lxml", specifier = ">=6.0.1" },
|
||||||
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
|
{ name = "ruff", specifier = ">=0.13.1" },
|
||||||
|
{ name = "xsdata", extras = ["cli", "lxml", "soap"], specifier = ">=25.7" },
|
||||||
|
{ name = "xsdata-pydantic", extras = ["cli", "lxml", "soap"], specifier = ">=24.5" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -57,6 +74,52 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 },
|
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click-default-group"
|
||||||
|
version = "1.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docformatter"
|
||||||
|
version = "1.7.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "untokenize" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/7b/ee08cb5fe2627ed0b6f0cc4a1c6be6c9c71de5a3e9785de8174273fc3128/docformatter-1.7.7.tar.gz", hash = "sha256:ea0e1e8867e5af468dfc3f9e947b92230a55be9ec17cd1609556387bffac7978", size = 26587 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl", hash = "sha256:7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834", size = 33525 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generateds"
|
name = "generateds"
|
||||||
version = "2.44.3"
|
version = "2.44.3"
|
||||||
@@ -79,6 +142,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
version = "6.0.1"
|
version = "6.0.1"
|
||||||
@@ -123,6 +207,120 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0a/44/9613f300201b8700215856e5edd056d4e58dd23368699196b58877d4408b/lxml-6.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:2834377b0145a471a654d699bdb3a2155312de492142ef5a1d426af2c60a0a31", size = 3753901 },
|
{ url = "https://files.pythonhosted.org/packages/0a/44/9613f300201b8700215856e5edd056d4e58dd23368699196b58877d4408b/lxml-6.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:2834377b0145a471a654d699bdb3a2155312de492142ef5a1d426af2c60a0a31", size = 3753901 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.11.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.33.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
@@ -138,6 +336,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -147,6 +371,42 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toposort"
|
||||||
|
version = "1.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/19/8e955d90985ecbd3b9adb2a759753a6840da2dff3c569d412b2c9217678b/toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd", size = 11132 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/17/57b444fd314d5e1593350b9a31d000e7411ba8e17ce12dc7ad54ca76b810/toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87", size = 8500 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untokenize"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099 }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -155,3 +415,55 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xsdata"
|
||||||
|
version = "25.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/cf/d393286e40f7574c5d662a3ceefcf8e4cd65e73af6e54db0585c5b17c541/xsdata-25.7.tar.gz", hash = "sha256:1291ef759f4663baadb86562be4c25ebfc0003ca0debae3042b0067663f0c548", size = 345469 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/10/c866e7b0fd57c92a4d5676884b81383005d81f8d7f07f1ac17e9c0ab3643/xsdata-25.7-py3-none-any.whl", hash = "sha256:d50b8c39389fd2b7283767a68a80cbf3bc51a3ede9cc3fefb30e84a52c999a9d", size = 234469 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
cli = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "click-default-group" },
|
||||||
|
{ name = "docformatter" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
{ name = "toposort" },
|
||||||
|
]
|
||||||
|
lxml = [
|
||||||
|
{ name = "lxml" },
|
||||||
|
]
|
||||||
|
soap = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xsdata-pydantic"
|
||||||
|
version = "24.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "xsdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/85/5e/6bc728d70460d9ad3982d05c3765179e3584fee6fa523d57b242e6e4c50f/xsdata_pydantic-24.5.tar.gz", hash = "sha256:e3c8758133195657ece578537eda6c7ebd8419f77abf6b90fd4ced96e348129b", size = 18763 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/7b/785fe71aa1138d7380ab3926cbb9571896d56544901c320953ff8a586926/xsdata_pydantic-24.5-py3-none-any.whl", hash = "sha256:bb6da7d3445d655640096c65c1b11037153b19df533da89553f24247ef352cd0", size = 8891 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
cli = [
|
||||||
|
{ name = "xsdata", extra = ["cli"] },
|
||||||
|
]
|
||||||
|
lxml = [
|
||||||
|
{ name = "lxml" },
|
||||||
|
]
|
||||||
|
soap = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user