| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- from __future__ import annotations
- """测试SerpApiClient封装层的航班/酒店查询与数据规范化。"""
- import asyncio
- from datetime import date
- from rich.console import Console
- from rich.table import Table
- from app.clients.serpapi_client import (
- SerpApiClient,
- SerpApiError,
- )
- from app.config import get_settings
- from app.schemas.flight import FlightSearchQuery
- from app.schemas.hotel import HotelSearchQuery
- console = Console()
- def _format_duration(minutes: object) -> str:
- """把分钟数转成可读时长,例如 200 → 3小时20分。"""
- if not isinstance(minutes, (int, float)):
- return "?"
- total = int(minutes)
- hours = total // 60
- mins = total % 60
- if hours == 0:
- return f"{mins}分钟"
- if mins == 0:
- return f"{hours}小时"
- return f"{hours}小时{mins}分"
- def _format_stops(segments_count: int) -> str:
- """根据航段数返回直飞/中转描述。"""
- if segments_count <= 1:
- return "直飞"
- return f"中转{segments_count - 1}次"
- def _build_flight_route(segments: list[dict]) -> str:
- """从航段列表拼接路线,例如 PVG → CTU。"""
- if not segments:
- return "未知路线"
- codes: list[str] = []
- for i, seg in enumerate(segments):
- dep = seg.get("departure_airport", {}).get("id", "?")
- arr = seg.get("arrival_airport", {}).get("id", "?")
- if i == 0:
- codes.append(dep)
- codes.append(arr)
- return " → ".join(codes)
- async def test_flights(
- client: SerpApiClient,
- ) -> None:
- console.rule("[bold]测试SerpApiClient航班查询")
- query = FlightSearchQuery(
- departure_airports=["SHA", "PVG"],
- arrival_airports=["CTU", "TFU"],
- outbound_date=date(2026, 8, 10),
- flight_type="one_way",
- adults=2,
- )
- data = await client.search_flights_raw(query)
- best_flights = data.get("best_flights", [])
- other_flights = data.get("other_flights", [])
- all_flights = [
- *best_flights,
- *other_flights,
- ]
- console.print(
- {
- "查询人数(adults)": query.adults,
- "best_flights数量": len(best_flights),
- "other_flights数量": len(other_flights),
- "全部航班数量": len(all_flights),
- "说明": "price是SerpApi返回的总价,人均=总价/adults",
- }
- )
- table = Table(title="航班查询结果预览")
- table.add_column("#", justify="right", style="dim")
- table.add_column("来源", style="bold")
- table.add_column("航空公司 / 航班号")
- table.add_column("出发机场 · 起飞时间")
- table.add_column("到达机场 · 落地时间")
- table.add_column("总时长")
- table.add_column("直飞/中转")
- table.add_column("总价")
- table.add_column("人均价格")
- seq = 0
- for source_group, options in (
- ("best_flights", best_flights),
- ("other_flights", other_flights),
- ):
- for option in options[:3]:
- segments = option.get("flights", [])
- if not segments:
- continue
- seq += 1
- # --- 航空公司 + 航班号 ---
- airlines_flight_numbers: list[str] = []
- for seg in segments:
- airline = seg.get("airline", "?")
- fn_ = seg.get("flight_number", "?")
- airlines_flight_numbers.append(f"{airline} {fn_}")
- airline_flight_str = "\n".join(airlines_flight_numbers)
- # --- 出发 ---
- first_seg = segments[0]
- dep_airport = first_seg.get("departure_airport", {})
- dep_code = dep_airport.get("id", "?")
- dep_time = dep_airport.get("time", "?")
- departure_str = f"{dep_code}\n{dep_time}"
- # --- 到达 ---
- last_seg = segments[-1]
- arr_airport = last_seg.get("arrival_airport", {})
- arr_code = arr_airport.get("id", "?")
- arr_time = arr_airport.get("time", "?")
- arrival_str = f"{arr_code}\n{arr_time}"
- # --- 总时长 ---
- total_dur = option.get("total_duration", "?")
- # --- 直飞/中转 ---
- stops_str = _format_stops(len(segments))
- # --- 总价 & 人均 ---
- price_raw = option.get("price")
- if isinstance(price_raw, (int, float)):
- total_price = int(price_raw)
- per_person = round(total_price / query.adults)
- price_str = f"¥{total_price}"
- per_person_str = f"¥{per_person}"
- else:
- price_str = "?"
- per_person_str = "?"
- # --- 路线概要 ---
- route = _build_flight_route(segments)
- table.add_row(
- str(seq),
- f"{source_group}\n{route}",
- airline_flight_str,
- departure_str,
- arrival_str,
- _format_duration(total_dur),
- stops_str,
- price_str,
- per_person_str,
- )
- console.print(table)
- async def test_hotels(
- client: SerpApiClient,
- ) -> None:
- console.rule("[bold]测试SerpApiClient酒店查询")
- query = HotelSearchQuery(
- query="成都酒店",
- check_in_date=date(2026, 8, 10),
- check_out_date=date(2026, 8, 13),
- adults=2,
- )
- data = await client.search_hotels_raw(query)
- hotels = data.get("properties", [])
- console.print(
- {
- "全部酒店数量": len(hotels),
- }
- )
- table = Table(title="酒店查询结果预览")
- table.add_column("名称")
- table.add_column("评分")
- table.add_column("每晚价格")
- table.add_column("星级")
- # 这里只限制终端显示数量。
- for hotel in hotels[:5]:
- rate = hotel.get("rate_per_night", {})
- hotel_class = (
- hotel.get("extracted_hotel_class")
- or hotel.get("hotel_class")
- )
- table.add_row(
- str(hotel.get("name", "?")),
- str(hotel.get("overall_rating", "?")),
- str(rate.get("extracted_lowest", "?")),
- str(hotel_class or "?"),
- )
- console.print(table)
- async def main() -> None:
- settings = get_settings()
- api_key = settings.require(
- "serpapi_api_key"
- )
- async with SerpApiClient(
- api_key=api_key,
- timeout_seconds=(
- settings.request_timeout_seconds
- ),
- ) as client:
- await test_flights(client)
- await test_hotels(client)
- if __name__ == "__main__":
- try:
- asyncio.run(main())
- except SerpApiError as exc:
- console.print(
- f"[red]SerpApi测试失败:{exc}[/red]"
- )
- raise SystemExit(1) from exc
|