"""Flight-related models for Duffel API."""
from datetime import datetime
from enum import Enum
from typing import Literal
from pydantic import BaseModel, Field, field_validator, ConfigDict
from .common import ResponseFormat, PaymentType
class PassengerType(str, Enum):
"""Passenger types supported by Duffel API."""
ADULT = "adult"
CHILD = "child"
INFANT_WITHOUT_SEAT = "infant_without_seat"
class CabinClass(str, Enum):
"""Cabin class options for flights."""
ECONOMY = "economy"
PREMIUM_ECONOMY = "premium_economy"
BUSINESS = "business"
FIRST = "first"
class Slice(BaseModel):
"""A slice represents one leg of a journey (e.g., outbound or return)."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
origin: str = Field(
...,
description="IATA airport or city code for departure (e.g., 'JFK', 'NYC', 'LON')",
min_length=3,
max_length=3
)
destination: str = Field(
...,
description="IATA airport or city code for arrival (e.g., 'LAX', 'SFO', 'PAR')",
min_length=3,
max_length=3
)
departure_date: str = Field(
...,
description="Departure date in YYYY-MM-DD format (e.g., '2025-12-25')"
)
@field_validator('departure_date')
@classmethod
def validate_date_format(cls, v: str) -> str:
"""Validate date is in correct format and not in the past."""
try:
date = datetime.strptime(v, "%Y-%m-%d").date()
if date < datetime.now().date():
raise ValueError("Departure date cannot be in the past")
return v
except ValueError as e:
raise ValueError(f"Invalid date format. Use YYYY-MM-DD: {e}")
class Passenger(BaseModel):
"""Passenger information for flight search."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
age: int | None = Field(
default=None,
description="Age of passenger. Recommended over 'type' for accuracy (e.g., 35, 12, 1)",
ge=0,
le=120
)
type: PassengerType | None = Field(
default=None,
description="Passenger type: 'adult', 'child', or 'infant_without_seat'. Use 'age' instead for better accuracy"
)
@field_validator('type')
@classmethod
def check_age_or_type(cls, v, info):
"""Ensure either age or type is provided."""
if v is None and info.data.get('age') is None:
raise ValueError("Either 'age' or 'type' must be provided")
return v
class FlightSearchInput(BaseModel):
"""Input for searching flights."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
slices: list[Slice] = Field(
...,
description="List of journey slices (legs). One-way = 1 slice, round-trip = 2 slices",
min_length=1,
max_length=4
)
passengers: list[Passenger] = Field(
...,
description="List of passengers traveling. Include all travelers",
min_length=1,
max_length=9
)
cabin_class: CabinClass | None = Field(
default=None,
description="Preferred cabin class: 'economy', 'premium_economy', 'business', or 'first'"
)
max_connections: int | None = Field(
default=None,
description="Maximum number of connections/stops per slice (e.g., 0 for direct flights, 1 for one stop)",
ge=0,
le=3
)
response_format: ResponseFormat = Field(
default=ResponseFormat.MARKDOWN,
description="Output format: 'json' for raw data or 'markdown' for readable summary"
)
class GetOfferInput(BaseModel):
"""Input for retrieving a specific flight offer."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
offer_id: str = Field(
...,
description="Duffel offer ID (e.g., 'off_00009htYpSCXrwaB9DnUm0')",
pattern="^off_[a-zA-Z0-9]+$"
)
response_format: ResponseFormat = Field(
default=ResponseFormat.MARKDOWN,
description="Output format: 'json' for raw data or 'markdown' for readable summary"
)
class PassengerDetails(BaseModel):
"""Detailed passenger information for booking."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
id: str = Field(
...,
description="Passenger ID from the offer (e.g., 'pas_00009hj8USM7Ncg31cBCL')"
)
given_name: str = Field(
...,
description="First/given name as shown on ID (e.g., 'John')",
min_length=1,
max_length=50
)
family_name: str = Field(
...,
description="Last/family name as shown on ID (e.g., 'Smith')",
min_length=1,
max_length=50
)
born_on: str = Field(
...,
description="Date of birth in YYYY-MM-DD format (e.g., '1985-03-15')"
)
email: str = Field(
...,
description="Email address for booking confirmation (e.g., 'john.smith@example.com')"
)
phone_number: str = Field(
...,
description="Phone number with country code (e.g., '+14155551234')"
)
gender: Literal["m", "f"] = Field(
...,
description="Gender: 'm' for male, 'f' for female"
)
title: str | None = Field(
default=None,
description="Title (e.g., 'mr', 'mrs', 'ms', 'dr')"
)
class Payment(BaseModel):
"""Payment information for booking."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
type: PaymentType = Field(
...,
description="Payment type: 'balance' (Duffel Balance), 'arc_bsp_cash' (IATA agent), or 'card' (credit card)"
)
amount: str = Field(
...,
description="Payment amount matching offer total_amount (e.g., '520.00')"
)
currency: str = Field(
...,
description="Payment currency matching offer total_currency (e.g., 'USD', 'EUR')",
min_length=3,
max_length=3
)
class CreateOrderInput(BaseModel):
"""Input for creating a flight order (booking)."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
offer_id: str = Field(
...,
description="Offer ID to book (e.g., 'off_00009htYpSCXrwaB9DnUm0')",
pattern="^off_[a-zA-Z0-9]+$"
)
passengers: list[PassengerDetails] = Field(
...,
description="Complete details for all passengers",
min_length=1,
max_length=9
)
payments: list[Payment] = Field(
...,
description="Payment information (typically one payment object)",
min_length=1,
max_length=1
)
response_format: ResponseFormat = Field(
default=ResponseFormat.MARKDOWN,
description="Output format: 'json' for raw data or 'markdown' for readable summary"
)
class GetOrderInput(BaseModel):
"""Input for retrieving order details."""
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, extra='forbid')
order_id: str = Field(
...,
description="Duffel order ID (e.g., 'ord_00009hthhsUZ8W4LxQgkjo')",
pattern="^ord_[a-zA-Z0-9]+$"
)
response_format: ResponseFormat = Field(
default=ResponseFormat.MARKDOWN,
description="Output format: 'json' for raw data or 'markdown' for readable summary"
)