test_serpapi_client.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. from __future__ import annotations
  2. """测试SerpApiClient封装层的航班/酒店查询与数据规范化。"""
  3. import asyncio
  4. from datetime import date
  5. from rich.console import Console
  6. from rich.table import Table
  7. from app.clients.serpapi_client import (
  8. SerpApiClient,
  9. SerpApiError,
  10. )
  11. from app.config import get_settings
  12. from app.schemas.flight import FlightSearchQuery
  13. from app.schemas.hotel import HotelSearchQuery
  14. console = Console()
  15. def _format_duration(minutes: object) -> str:
  16. """把分钟数转成可读时长,例如 200 → 3小时20分。"""
  17. if not isinstance(minutes, (int, float)):
  18. return "?"
  19. total = int(minutes)
  20. hours = total // 60
  21. mins = total % 60
  22. if hours == 0:
  23. return f"{mins}分钟"
  24. if mins == 0:
  25. return f"{hours}小时"
  26. return f"{hours}小时{mins}分"
  27. def _format_stops(segments_count: int) -> str:
  28. """根据航段数返回直飞/中转描述。"""
  29. if segments_count <= 1:
  30. return "直飞"
  31. return f"中转{segments_count - 1}次"
  32. def _build_flight_route(segments: list[dict]) -> str:
  33. """从航段列表拼接路线,例如 PVG → CTU。"""
  34. if not segments:
  35. return "未知路线"
  36. codes: list[str] = []
  37. for i, seg in enumerate(segments):
  38. dep = seg.get("departure_airport", {}).get("id", "?")
  39. arr = seg.get("arrival_airport", {}).get("id", "?")
  40. if i == 0:
  41. codes.append(dep)
  42. codes.append(arr)
  43. return " → ".join(codes)
  44. async def test_flights(
  45. client: SerpApiClient,
  46. ) -> None:
  47. console.rule("[bold]测试SerpApiClient航班查询")
  48. query = FlightSearchQuery(
  49. departure_airports=["SHA", "PVG"],
  50. arrival_airports=["CTU", "TFU"],
  51. outbound_date=date(2026, 8, 10),
  52. flight_type="one_way",
  53. adults=2,
  54. )
  55. data = await client.search_flights_raw(query)
  56. best_flights = data.get("best_flights", [])
  57. other_flights = data.get("other_flights", [])
  58. all_flights = [
  59. *best_flights,
  60. *other_flights,
  61. ]
  62. console.print(
  63. {
  64. "查询人数(adults)": query.adults,
  65. "best_flights数量": len(best_flights),
  66. "other_flights数量": len(other_flights),
  67. "全部航班数量": len(all_flights),
  68. "说明": "price是SerpApi返回的总价,人均=总价/adults",
  69. }
  70. )
  71. table = Table(title="航班查询结果预览")
  72. table.add_column("#", justify="right", style="dim")
  73. table.add_column("来源", style="bold")
  74. table.add_column("航空公司 / 航班号")
  75. table.add_column("出发机场 · 起飞时间")
  76. table.add_column("到达机场 · 落地时间")
  77. table.add_column("总时长")
  78. table.add_column("直飞/中转")
  79. table.add_column("总价")
  80. table.add_column("人均价格")
  81. seq = 0
  82. for source_group, options in (
  83. ("best_flights", best_flights),
  84. ("other_flights", other_flights),
  85. ):
  86. for option in options[:3]:
  87. segments = option.get("flights", [])
  88. if not segments:
  89. continue
  90. seq += 1
  91. # --- 航空公司 + 航班号 ---
  92. airlines_flight_numbers: list[str] = []
  93. for seg in segments:
  94. airline = seg.get("airline", "?")
  95. fn_ = seg.get("flight_number", "?")
  96. airlines_flight_numbers.append(f"{airline} {fn_}")
  97. airline_flight_str = "\n".join(airlines_flight_numbers)
  98. # --- 出发 ---
  99. first_seg = segments[0]
  100. dep_airport = first_seg.get("departure_airport", {})
  101. dep_code = dep_airport.get("id", "?")
  102. dep_time = dep_airport.get("time", "?")
  103. departure_str = f"{dep_code}\n{dep_time}"
  104. # --- 到达 ---
  105. last_seg = segments[-1]
  106. arr_airport = last_seg.get("arrival_airport", {})
  107. arr_code = arr_airport.get("id", "?")
  108. arr_time = arr_airport.get("time", "?")
  109. arrival_str = f"{arr_code}\n{arr_time}"
  110. # --- 总时长 ---
  111. total_dur = option.get("total_duration", "?")
  112. # --- 直飞/中转 ---
  113. stops_str = _format_stops(len(segments))
  114. # --- 总价 & 人均 ---
  115. price_raw = option.get("price")
  116. if isinstance(price_raw, (int, float)):
  117. total_price = int(price_raw)
  118. per_person = round(total_price / query.adults)
  119. price_str = f"¥{total_price}"
  120. per_person_str = f"¥{per_person}"
  121. else:
  122. price_str = "?"
  123. per_person_str = "?"
  124. # --- 路线概要 ---
  125. route = _build_flight_route(segments)
  126. table.add_row(
  127. str(seq),
  128. f"{source_group}\n{route}",
  129. airline_flight_str,
  130. departure_str,
  131. arrival_str,
  132. _format_duration(total_dur),
  133. stops_str,
  134. price_str,
  135. per_person_str,
  136. )
  137. console.print(table)
  138. async def test_hotels(
  139. client: SerpApiClient,
  140. ) -> None:
  141. console.rule("[bold]测试SerpApiClient酒店查询")
  142. query = HotelSearchQuery(
  143. query="成都酒店",
  144. check_in_date=date(2026, 8, 10),
  145. check_out_date=date(2026, 8, 13),
  146. adults=2,
  147. )
  148. data = await client.search_hotels_raw(query)
  149. hotels = data.get("properties", [])
  150. console.print(
  151. {
  152. "全部酒店数量": len(hotels),
  153. }
  154. )
  155. table = Table(title="酒店查询结果预览")
  156. table.add_column("名称")
  157. table.add_column("评分")
  158. table.add_column("每晚价格")
  159. table.add_column("星级")
  160. # 这里只限制终端显示数量。
  161. for hotel in hotels[:5]:
  162. rate = hotel.get("rate_per_night", {})
  163. hotel_class = (
  164. hotel.get("extracted_hotel_class")
  165. or hotel.get("hotel_class")
  166. )
  167. table.add_row(
  168. str(hotel.get("name", "?")),
  169. str(hotel.get("overall_rating", "?")),
  170. str(rate.get("extracted_lowest", "?")),
  171. str(hotel_class or "?"),
  172. )
  173. console.print(table)
  174. async def main() -> None:
  175. settings = get_settings()
  176. api_key = settings.require(
  177. "serpapi_api_key"
  178. )
  179. async with SerpApiClient(
  180. api_key=api_key,
  181. timeout_seconds=(
  182. settings.request_timeout_seconds
  183. ),
  184. ) as client:
  185. await test_flights(client)
  186. await test_hotels(client)
  187. if __name__ == "__main__":
  188. try:
  189. asyncio.run(main())
  190. except SerpApiError as exc:
  191. console.print(
  192. f"[red]SerpApi测试失败:{exc}[/red]"
  193. )
  194. raise SystemExit(1) from exc