Просмотр исходного кода

init: 提交 travel-mind 项目全部代码(不含 .env 敏感配置)

daphne-chen 1 месяц назад
Родитель
Сommit
340cb20837
81 измененных файлов с 18092 добавлено и 0 удалено
  1. 37 0
      .env.example
  2. 36 0
      .gitignore
  3. 0 0
      README.md
  4. 0 0
      app/__init__.py
  5. 0 0
      app/agents/__init__.py
  6. 494 0
      app/agents/itinerary_agent.py
  7. 195 0
      app/agents/map_agent.py
  8. 227 0
      app/agents/requirement_agent.py
  9. 132 0
      app/agents/resource_agent.py
  10. 351 0
      app/agents/reviewer_agent.py
  11. 0 0
      app/clients/__init__.py
  12. 307 0
      app/clients/serpapi_client.py
  13. 50 0
      app/config.py
  14. 0 0
      app/graph/__init__.py
  15. 75 0
      app/graph/builder.py
  16. 100 0
      app/graph/final_nodes.py
  17. 190 0
      app/graph/map_nodes.py
  18. 287 0
      app/graph/nodes.py
  19. 317 0
      app/graph/planner_builder.py
  20. 150 0
      app/graph/planner_nodes.py
  21. 116 0
      app/graph/research_builder.py
  22. 372 0
      app/graph/resource_nodes.py
  23. 198 0
      app/graph/review_nodes.py
  24. 210 0
      app/graph/route_builder.py
  25. 586 0
      app/graph/route_nodes.py
  26. 129 0
      app/graph/selection_builder.py
  27. 426 0
      app/graph/selection_nodes.py
  28. 108 0
      app/graph/state.py
  29. 230 0
      app/graph/supervisor_nodes.py
  30. 96 0
      app/graph/travel_builder.py
  31. 115 0
      app/graph/validation_nodes.py
  32. 40 0
      app/llm.py
  33. 95 0
      app/logging_config.py
  34. 102 0
      app/mcp_client.py
  35. 0 0
      app/schemas/__init__.py
  36. 30 0
      app/schemas/common.py
  37. 176 0
      app/schemas/flight.py
  38. 131 0
      app/schemas/hotel.py
  39. 241 0
      app/schemas/itinerary.py
  40. 68 0
      app/schemas/place.py
  41. 107 0
      app/schemas/review.py
  42. 107 0
      app/schemas/route.py
  43. 118 0
      app/schemas/selection.py
  44. 221 0
      app/schemas/travel_request.py
  45. 93 0
      app/schemas/validation.py
  46. 0 0
      app/services/__init__.py
  47. 1134 0
      app/services/candidate_selection_service.py
  48. 533 0
      app/services/finalization_service.py
  49. 1442 0
      app/services/plan_validation_service.py
  50. 434 0
      app/services/route_evaluation_service.py
  51. 920 0
      app/services/travel_search_service.py
  52. 198 0
      main.py
  53. 0 0
      mcp_servers/__init__.py
  54. 328 0
      mcp_servers/travel_search_server.py
  55. 28 0
      pyproject.toml
  56. 0 0
      scripts/__init__.py
  57. 182 0
      scripts/run_regression.py
  58. 149 0
      scripts/test_amap_mcp.py
  59. 24 0
      scripts/test_connections.py
  60. 86 0
      scripts/test_final_graph.py
  61. 60 0
      scripts/test_mcp_registry.py
  62. 125 0
      scripts/test_planner_graph.py
  63. 70 0
      scripts/test_requirement_graph.py
  64. 108 0
      scripts/test_research_graph.py
  65. 67 0
      scripts/test_resource_graph.py
  66. 149 0
      scripts/test_reviewer_graph.py
  67. 142 0
      scripts/test_route_graph.py
  68. 158 0
      scripts/test_selection_graph.py
  69. 246 0
      scripts/test_serpapi.py
  70. 242 0
      scripts/test_serpapi_client.py
  71. 259 0
      scripts/test_travel_mcp.py
  72. 249 0
      scripts/test_travel_service.py
  73. 129 0
      scripts/test_validation_graph.py
  74. 186 0
      tests/test_candidate_selection_service.py
  75. 102 0
      tests/test_itinerary_schema.py
  76. 433 0
      tests/test_plan_validation_service.py
  77. 0 0
      tests/test_requirement_validation.py
  78. 95 0
      tests/test_review_schema.py
  79. 178 0
      tests/test_route_evaluation_service.py
  80. 193 0
      tests/test_supervisor_nodes.py
  81. 2380 0
      uv.lock

+ 37 - 0
.env.example

@@ -0,0 +1,37 @@
+# ==============================
+# LLM
+# ==============================
+
+# OpenAI或OpenAI兼容模型服务Key
+MODEL_API_KEY=replace_me
+
+# 例如:
+# gpt-4.1-mini
+# qwen-plus
+# deepseek-chat
+MODEL_NAME=replace_me
+
+# OpenAI官方接口可留空。
+# 使用兼容接口时填写对应Base URL。
+MODEL_BASE_URL=
+
+
+# ==============================
+# Travel resource API
+# ==============================
+
+SERPAPI_API_KEY=replace_me
+
+
+# ==============================
+# Amap MCP
+# ==============================
+
+AMAP_API_KEY=replace_me
+
+
+# ==============================
+# Network
+# ==============================
+
+REQUEST_TIMEOUT_SECONDS=60

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
+# Python
+__pycache__/
+*.py[cod]
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+
+# Virtual environment
+.venv/
+venv/
+
+# Environment secrets
+.env
+.env.local
+.env.*.local
+
+# IDE
+.idea/
+.vscode/
+
+# macOS
+.DS_Store
+
+# Logs
+*.log
+logs/
+
+# Coverage
+.coverage
+htmlcov/
+coverage.xml
+
+# Build
+build/
+dist/
+*.egg-info/


+ 0 - 0
app/__init__.py


+ 0 - 0
app/agents/__init__.py


+ 494 - 0
app/agents/itinerary_agent.py

@@ -0,0 +1,494 @@
+from __future__ import annotations
+
+"""行程规划Agent:基于候选资源(航班/酒店/景点)与地图研究结果,生成包含每日活动安排的结构化多日行程。"""
+
+import json
+from typing import Any
+
+from langchain_core.language_models.chat_models import (
+    BaseChatModel,
+)
+from langchain_core.messages import (
+    HumanMessage,
+    SystemMessage,
+)
+
+from app.llm import get_chat_model
+from app.schemas.itinerary import ItineraryPlan
+from app.schemas.place import MapResearchResult
+from app.schemas.route import (
+    RouteEvaluationResult,
+)
+from app.schemas.selection import (
+    CandidateSelectionResult,
+)
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+
+
+# ITINERARY_PLANNER_PROMPT:行程规划的系统提示词,定义每日行程的JSON输出结构与修改要求。
+ITINERARY_PLANNER_PROMPT = """
+你是多角色旅行规划系统中的行程规划专家。
+
+上游角色已经完成:
+
+1. Requirement Analyst:解析用户需求。
+2. Resource Search Agent:查询真实航班和酒店。
+3. Destination Research Agent:查询真实景点、
+   餐厅和天气。
+4. Candidate Selector:筛选航班和酒店。
+5. Route Evaluator:评估酒店到景点的距离。
+
+你的职责是基于上游结果,选择一个航班方案、
+一家酒店,并生成逐日旅行安排。
+
+严格规则:
+
+一、真实性
+
+1. 只能选择输入候选中存在的航班、酒店、
+   景点和餐厅。
+2. 不得编造候选中不存在的名称、ID、地址、
+   坐标、航班号或价格。
+3. 景点和餐厅的reference_id使用输入中的poi_id。
+4. 如果poi_id为空,可以保留reference_id=null,
+   但名称必须和输入候选完全一致。
+5. 不确定的信息放入assumptions或warnings,
+   不得伪装为确定事实。
+
+二、航班
+
+1. 往返旅行必须选择一个完整round_trip_options。
+2. combination_id、outbound_option_id和
+   return_option_id必须复制候选中的真实ID。
+3. quoted_price采用候选中的接口报价。
+4. 不得把去程和返程价格再次相加。
+5. 到达日需要考虑航班到达时间。
+6. 返程日必须为前往机场预留合理时间。
+
+三、酒店
+
+1. 优先使用Route Evaluator排名第一的酒店。
+2. 如选择其他酒店,必须给出明确原因。
+3. check_in_date必须等于用户的departure_date
+   (不是航班实际到达日期)。
+4. check_out_date必须等于用户的return_date。
+5. nights必须等于
+   (check_out_date - check_in_date)的天数。
+   例如:8月10日入住、8月14日退房
+   → nights = 4。
+6. price_per_night从酒店候选复制。
+7. estimated_total_price = price_per_night × nights;
+   该值后续还会由Validator重新计算。
+
+四、每日行程
+
+1. 必须覆盖旅行期间的每一个自然日。
+2. 日期必须连续。
+3. 到达日和返程日不能安排过满。
+4. 同一个景点不得重复安排。
+5. 单个活动的结束时间必须晚于开始时间。
+6. 景点尽量按照位置和主题组合。
+7. 仅可使用输入中存在的餐厅。
+8. 活动名称必须与候选名称完全一致,
+   不得添加"早餐:"、"午餐:"、"晚餐:"等前缀。
+9. 不需要强行为每一餐指定餐厅。
+10. 未查询到真实交通时间时,
+   不得编造精确交通时间。
+
+五、节奏
+
+- relaxed:
+  每天原则上不超过2个主要景点,
+  保留休息和自由活动时间。
+- normal:
+  每天原则上2至3个主要景点。
+- intensive:
+  每天可安排3至4个主要景点,
+  但仍需考虑交通和开放时间风险。
+
+六、天气
+
+1. 有真实天气数据时,可以据此安排室内外活动。
+2. 没有天气数据时,不得编造天气。
+3. 在warnings中提示用户出发前再次确认天气。
+
+七、费用
+
+1. 只能使用已有的航班和酒店报价。
+2. 未查询门票或餐饮价格时,
+   estimated_cost应为null。
+3. 总预算是否超支由后续Validator负责。
+
+八、输出格式(必须严格遵守字段名)
+
+你必须返回一个 JSON 对象,顶层字段如下:
+
+{
+  "title": "行程标题(字符串)",
+  "overview": "行程概览(字符串)",
+  "selected_flight": {
+    "flight_type": "round_trip 或 one_way",
+    "combination_id": "来自 round_trip_options 的组合ID(仅往返)",
+    "outbound_option_id": "去程航班ID(必填)",
+    "return_option_id": "返程航班ID(往返必填,单程为null)",
+    "quoted_price": 接口报价数字,
+    "currency": "CNY",
+    "outbound_flight_numbers": ["MU001"],
+    "return_flight_numbers": ["MU002"],
+    "outbound_departure_time": "去程出发时间字符串",
+    "outbound_arrival_time": "去程到达时间字符串",
+    "return_departure_time": "返程出发时间字符串",
+    "return_arrival_time": "返程到达时间字符串",
+    "selection_reason": "选择理由"
+  },
+  "selected_hotel": {
+    "hotel_id": "来自候选的酒店ID",
+    "name": "酒店名称",
+    "check_in_date": "入住日期 YYYY-MM-DD(必须等于 departure_date)",
+    "check_out_date": "退房日期 YYYY-MM-DD(必须等于 return_date)",
+    "nights": "check_out_date - check_in_date 的天数,如 8/10→8/14 为 4",
+    "price_per_night": 每晚价格,
+    "estimated_total_price": price_per_night × nights,
+    "currency": "CNY",
+    "address": "地址文本(可选)",
+    "location": "经纬度字符串如 \"104.07,30.66\"(可选,不是对象)",
+    "selection_reason": "选择理由"
+  },
+  "days": [
+    {
+      "day_index": 从1开始的整数,
+      "date": "YYYY-MM-DD",
+      "theme": "当日主题",
+      "activities": [
+        {
+          "sequence": 从1开始递增的整数,
+          "activity_type": "flight|hotel_check_in|hotel_check_out|attraction|restaurant|transport|free_time|other",
+          "reference_id": "景点或餐厅的poi_id(航班/入住等为null)",
+          "name": "活动名称",
+          "start_time": "HH:MM",
+          "end_time": "HH:MM",
+          "address": "地址(可选)",
+          "location": "经纬度(可选)",
+          "transport_mode": "walking|public_transit|driving|taxi|unknown(可选)",
+          "estimated_transport_minutes": 预估交通分钟(可选),
+          "estimated_cost": 预估费用(可选),
+          "notes": "备注(可选)"
+        }
+      ],
+      "daily_notes": ["当日备注(可选)"]
+    }
+  ],
+  "highlights": ["行程亮点"],
+  "assumptions": ["不确定的假设"],
+  "warnings": ["需要用户注意的点"]
+}
+
+注意:
+- 不要在最外层包裹 {"itineraryPlan": {...}},直接输出上述 JSON。
+- 所有日期格式为 YYYY-MM-DD,时间格式为 HH:MM。
+- 必须从输入候选中复制真实数据,不得编造。
+"""
+
+
+def _compact_flight(
+    flight: dict[str, Any],
+) -> dict[str, Any]:
+    """保留Planner真正需要的航班字段。"""
+
+    return {
+        "option_id": flight.get("option_id"),
+        "flight_numbers": flight.get(
+            "flight_numbers",
+            [],
+        ),
+        "airlines": flight.get("airlines", []),
+        "departure_airport": flight.get(
+            "departure_airport"
+        ),
+        "arrival_airport": flight.get(
+            "arrival_airport"
+        ),
+        "departure_time": flight.get(
+            "departure_time"
+        ),
+        "arrival_time": flight.get(
+            "arrival_time"
+        ),
+        "duration_minutes": flight.get(
+            "duration_minutes"
+        ),
+        "stop_count": flight.get("stop_count"),
+        "price": flight.get("price"),
+        "currency": flight.get("currency"),
+    }
+
+
+def build_planner_context(
+    request: TravelRequest,
+    selection: CandidateSelectionResult,
+    map_result: MapResearchResult,
+    route_result: RouteEvaluationResult,
+    revision_feedback: list[str] | None = None,
+) -> dict[str, Any]:
+    """把共享State压缩为Planner需要的上下文。
+
+    避免将几十条原始航班和酒店全部放入Prompt。
+    """
+
+    round_trip_options: list[
+        dict[str, Any]
+    ] = []
+
+    for option in selection.round_trip_options[:5]:
+        round_trip_options.append(
+            {
+                "combination_id": (
+                    option.combination_id
+                ),
+                "score": option.score,
+                "quoted_price": option.quoted_price,
+                "currency": option.currency,
+                "outbound": _compact_flight(
+                    option.outbound
+                ),
+                "return_flight": _compact_flight(
+                    option.return_flight
+                ),
+                "reasons": option.reasons,
+                "warnings": option.warnings,
+            }
+        )
+
+    one_way_options: list[
+        dict[str, Any]
+    ] = []
+
+    for candidate in selection.one_way_options[:5]:
+        one_way_options.append(
+            {
+                "score": candidate.score,
+                "flight": _compact_flight(
+                    candidate.flight
+                ),
+                "reasons": candidate.reasons,
+                "warnings": candidate.warnings,
+            }
+        )
+
+    evaluated_hotels: list[
+        dict[str, Any]
+    ] = []
+
+    for evaluated in (
+        route_result.evaluated_hotels[:3]
+    ):
+        hotel = evaluated.hotel
+
+        evaluated_hotels.append(
+            {
+                "hotel_id": evaluated.hotel_id,
+                "name": evaluated.hotel_name,
+                "final_score": (
+                    evaluated.final_score
+                ),
+                "base_hotel_score": (
+                    evaluated.base_hotel_score
+                ),
+                "average_distance_meters": (
+                    evaluated
+                    .average_distance_meters
+                ),
+                "average_duration_seconds": (
+                    evaluated
+                    .average_duration_seconds
+                ),
+                "nearest_subway_distance_meters": (
+                    evaluated
+                    .nearest_subway_distance_meters
+                ),
+                "price_per_night": hotel.get(
+                    "price_per_night"
+                ),
+                "total_price": hotel.get(
+                    "total_price"
+                ),
+                "currency": hotel.get("currency"),
+                "coordinates": hotel.get(
+                    "coordinates"
+                ),
+                "reasons": evaluated.reasons,
+                "warnings": evaluated.warnings,
+                "route_legs": [
+                    {
+                        "destination_name": (
+                            leg.destination_name
+                        ),
+                        "distance_meters": (
+                            leg.distance_meters
+                        ),
+                        "duration_seconds": (
+                            leg.duration_seconds
+                        ),
+                        "source": leg.source,
+                    }
+                    for leg in evaluated.route_legs
+                ],
+            }
+        )
+
+    attractions = [
+        place.model_dump(mode="json")
+        for place in map_result.attractions[:10]
+    ]
+
+    restaurants = [
+        place.model_dump(mode="json")
+        for place in map_result.restaurants[:6]
+    ]
+
+    weather = [
+        item.model_dump(mode="json")
+        for item in map_result.weather
+    ]
+
+    return {
+        "travel_request": request.model_dump(
+            mode="json"
+        ),
+        "flight_candidates": {
+            "flight_type": selection.flight_type,
+            "round_trip_options": (
+                round_trip_options
+            ),
+            "one_way_options": one_way_options,
+            "warnings": selection.warnings,
+        },
+        "evaluated_hotels": evaluated_hotels,
+        "selected_hotel_id_from_route_evaluator": (
+            route_result.selected_hotel_id
+        ),
+        "attractions": attractions,
+        "restaurants": restaurants,
+        "weather": weather,
+        "map_notes": map_result.notes,
+        "route_warnings": route_result.warnings,
+        "revision_feedback": (
+            revision_feedback or []
+        ),
+    }
+
+
+class ItineraryPlannerAgent:
+    """负责综合上游角色结果生成逐日行程。"""
+
+    def __init__(
+        self,
+        model: BaseChatModel | None = None,
+    ) -> None:
+        """初始化行程规划Agent,支持注入自定义模型。"""
+        base_model = model or get_chat_model(
+            timeout=180.0
+        )
+
+        # json_mode 通过 response_format 约束模型输出 JSON,
+        # 对 DeepSeek 等推理模型的兼容性更好。
+        self._structured_model = (
+            base_model.with_structured_output(
+                ItineraryPlan,
+                method="json_mode",
+            )
+        )
+
+    async def plan(
+        self,
+        request: TravelRequest,
+        selection: CandidateSelectionResult,
+        map_result: MapResearchResult,
+        route_result: RouteEvaluationResult,
+        revision_feedback: list[str] | None = None,
+    ) -> dict[str, Any]:
+        """生成结构化旅行方案。"""
+
+        context = build_planner_context(
+            request=request,
+            selection=selection,
+            map_result=map_result,
+            route_result=route_result,
+            revision_feedback=revision_feedback,
+        )
+
+        context_json = json.dumps(
+            context,
+            ensure_ascii=False,
+            indent=2,
+        )
+
+        feedback_instruction = ""
+
+        if revision_feedback:
+            feedback_instruction = (
+                "\n这是审查角色上一轮提出的修改要求,"
+                "新方案必须逐项处理:\n"
+                + "\n".join(
+                    f"- {item}"
+                    for item in revision_feedback
+                )
+            )
+
+        user_message = f"""
+请根据以下上游角色提供的真实数据生成旅行方案。
+
+{context_json}
+
+{feedback_instruction}
+
+最终必须以 JSON 格式返回 ItineraryPlan 结构化结果。
+不要输出结构化结果之外的解释。
+"""
+
+        response = await self._structured_model.ainvoke(
+            [
+                SystemMessage(
+                    content=(
+                        ITINERARY_PLANNER_PROMPT
+                    )
+                ),
+                HumanMessage(
+                    content=user_message
+                ),
+            ]
+        )
+
+        if isinstance(response, ItineraryPlan):
+            structured = response
+        else:
+            # 兼容 LLM 可能包裹在 {"itineraryPlan": {...}} 中的情况。
+            if (
+                isinstance(response, dict)
+                and "itineraryPlan" in response
+                and len(response) == 1
+            ):
+                response = response["itineraryPlan"]
+
+            # 兼容 LLM 将 coordinates 对象误填到
+            # selected_hotel.location 的情况。
+            if isinstance(response, dict):
+                hotel = response.get("selected_hotel")
+                if isinstance(hotel, dict):
+                    loc = hotel.get("location")
+                    if isinstance(loc, dict):
+                        lat = loc.get("latitude")
+                        lng = loc.get("longitude")
+                        if lat is not None and lng is not None:
+                            hotel["location"] = (
+                                f"{lng},{lat}"
+                            )
+
+            structured = ItineraryPlan.model_validate(response)
+
+        return {
+            "structured_response": structured,
+            "messages": [],
+        }

+ 195 - 0
app/agents/map_agent.py

@@ -0,0 +1,195 @@
+from __future__ import annotations
+
+"""地图研究Agent:调用高德地图MCP工具搜索目的地周边景点、餐饮与天气,返回结构化POI数据。"""
+
+from typing import Any
+
+from langchain.agents import create_agent
+from langchain_core.language_models.chat_models import (
+    BaseChatModel,
+)
+from langchain_core.tools import BaseTool
+
+from app.llm import get_chat_model
+from app.schemas.travel_request import TravelRequest
+
+
+# MAP_AGENT_PROMPT:地图研究的系统提示词,定义高德工具调用规则、候选数量约束与输出JSON结构。
+MAP_AGENT_PROMPT = """
+你是旅行规划系统中的地图研究Agent。
+
+你的职责:
+
+1. 使用高德地图MCP查询真实景点。
+2. 根据用户兴趣查询相关地点。
+3. 查询适合游客的当地餐饮地点。
+4. 查询目的城市天气。
+5. 返回结构化候选结果。
+6. 不负责查询机票和酒店。
+7. 不负责生成最终行程。
+8. 不负责计算完整路线。
+
+工具使用规则:
+
+1. 必须调用 maps_text_search 查询真实POI。
+2. 必须调用 maps_weather 查询目的城市天气。
+3. maps_search_detail 只在需要补充POI详情时调用。
+4. 不允许根据模型知识编造地点、地址或坐标。
+5. 地点名称、POI ID、地址和坐标必须来自工具结果。
+6. 查询景点时,需要覆盖:
+   - 目的城市热门景点;
+   - 用户明确表达的兴趣。
+7. 如果用户喜欢美食,需要额外查询当地特色餐饮。
+8. 最多执行5次关键词搜索,避免无意义重复调用。
+
+候选数量与多样性:
+
+- 景点保留6至10个;
+- 餐饮地点保留4至6个;
+- 同一地点不能重复。
+- 景点必须覆盖不同类型,同类景点最多选1个
+  (例如:动物园/熊猫类只选1个、寺庙/道观类只选1个、
+  古街/古镇类只选1个、自然山水类分散到不同山脉);
+- 餐饮必须优先选择不同品牌/餐厅,
+  同一连锁品牌只选1家分店,
+  避免同一品牌出现两次(如饕林餐厅各分店只选1家);
+- 优先覆盖目的城市不同行政区,
+  避免所有候选集中在同一区域。
+
+天气规则:
+
+1. 如果旅行日期超过天气工具可查询范围,
+   不得编造未来天气。
+2. 此时weather返回空列表,并在notes说明
+   "旅行日期超过当前天气预报范围"。
+3. 天气数据必须来自maps_weather。
+
+match_reason只能描述该地点与用户兴趣的匹配关系,
+不能声称"距离酒店近"或"交通最方便",
+因为路线尚未计算。
+
+完成工具查询后,请将所有结果整理为以下JSON格式,
+放在最终消息的最末尾,用```json代码块包裹:
+
+```json
+{
+  "city": "城市名称",
+  "attractions": [
+    {
+      "poi_id": "高德POI ID或null",
+      "name": "景点名称",
+      "category": "类别",
+      "address": "地址",
+      "district": "区",
+      "location": "经度,纬度",
+      "telephone": "电话或null",
+      "source_query": "使用的搜索关键词",
+      "match_reason": "匹配用户兴趣的原因"
+    }
+  ],
+  "restaurants": [
+    {
+      "poi_id": "高德POI ID或null",
+      "name": "餐厅名称",
+      "category": "类别",
+      "address": "地址",
+      "district": "区",
+      "location": "经度,纬度",
+      "telephone": "电话或null",
+      "source_query": "使用的搜索关键词",
+      "match_reason": "匹配用户兴趣的原因"
+    }
+  ],
+  "weather": [
+    {
+      "date": "日期 YYYY-MM-DD",
+      "week": "星期几",
+      "day_weather": "白天天气",
+      "night_weather": "夜间天气",
+      "day_temperature": "白天温度",
+      "night_temperature": "夜间温度",
+      "day_wind_direction": "白天风向",
+      "night_wind_direction": "夜间风向"
+    }
+  ],
+  "notes": ["备注1", "备注2"]
+}
+```
+"""
+
+
+class MapResearchAgent:
+    """查询景点、餐饮和天气的地图Agent。"""
+
+    def __init__(
+        self,
+        tools: list[BaseTool],
+        model: BaseChatModel | None = None,
+    ) -> None:
+        """初始化地图研究Agent,校验必需的高德工具是否已加载。"""
+        allowed_names = {
+            "maps_text_search",
+            "maps_weather",
+            "maps_search_detail",
+        }
+
+        selected_tools = [
+            tool
+            for tool in tools
+            if tool.name in allowed_names
+        ]
+
+        loaded_names = {
+            tool.name
+            for tool in selected_tools
+        }
+
+        required_names = {
+            "maps_text_search",
+            "maps_weather",
+        }
+
+        missing_names = required_names - loaded_names
+
+        if missing_names:
+            raise RuntimeError(
+                "地图Agent缺少高德工具:"
+                + "、".join(sorted(missing_names))
+            )
+
+        self._agent = create_agent(
+            model=model or get_chat_model(timeout=180.0),
+            tools=selected_tools,
+            system_prompt=MAP_AGENT_PROMPT,
+        )
+
+    async def research(
+        self,
+        request: TravelRequest,
+    ) -> dict[str, Any]:
+        """执行目的地地图研究。"""
+
+        request_json = request.model_dump_json(
+            indent=2
+        )
+
+        user_message = f"""
+请根据下面的结构化旅行需求,
+查询真实景点、餐饮地点和天气:
+
+{request_json}
+
+必须实际调用高德地图工具。
+查询结束后,输出 JSON 结构化结果。
+"""
+
+        return await self._agent.ainvoke(
+            {
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": user_message,
+                    }
+                ]
+            }
+        )

+ 227 - 0
app/agents/requirement_agent.py

@@ -0,0 +1,227 @@
+from __future__ import annotations
+
+"""需求解析Agent:把用户自然语言输入转换为结构化TravelRequest,负责意图识别与字段提取(仅解析,不规划)。"""
+
+from datetime import date
+
+from langchain_core.language_models.chat_models import (
+    BaseChatModel,
+)
+from langchain_core.messages import (
+    HumanMessage,
+    SystemMessage,
+)
+
+from app.llm import get_chat_model
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+
+
+# SYSTEM_PROMPT:需求解析的系统提示词,包含字段提取规则、约束条件与Few-shot示例;运行时在prompt中注入current_date。
+SYSTEM_PROMPT = """
+你是旅行规划系统中的需求解析模块。
+
+你的任务是把用户自然语言转换成结构化旅行需求,
+不要生成旅行方案,也不要调用任何外部工具。
+请以 JSON 格式输出。
+
+当前日期:{current_date}
+
+规则:
+
+1. 不要编造用户没有提供的信息。
+2. 出发城市、目的城市缺失时返回 null。
+3. 日期必须输出为 YYYY-MM-DD。
+4. 可以根据当前日期解析"明天、下周、下个月"等相对日期。
+5. 用户提供"5天4晚"时:trip_days=5, nights=4。
+6. 如果用户给出出发日期和旅行天数,允许计算返程日期。
+7. 用户提到具体机场时(如"双流机场"、"浦东机场"、"虹桥"),
+   提取机场名到 preferred_arrival_airports 或
+   preferred_departure_airports。
+   用户没有表达机场偏好时,返回空列表。
+8. 用户明确要求"直达""直飞"时,max_stops=0。
+   用户没有限制直飞或中转时,max_stops 返回 null。
+9. 不要因为某个机场离市区近,自动添加机场偏好。
+10. 用户明确表示价格优先时,priority="price"。
+11. 用户明确表示时间、机场或舒适度优先时,
+    priority="convenience"。
+12. 用户没有明确优先级时,priority="balanced"。
+13. pace取值(用户未提及时默认 normal):
+    - 不赶、轻松、休闲:relaxed
+    - 普通、适中:normal
+    - 紧凑、多安排景点:intensive
+14. 酒店靠近地铁时,hotel_preferences.near_subway=true。
+    用户提到想住在特定区域时(如"市中心"、"春熙路"),
+    提取到 hotel_preferences.preferred_areas。
+15. 总预算 total_budget 是纯数字(float),
+    货币单位用单独的 currency 字段(默认 CNY)。
+    不要输出 {{"amount": ..., "currency": ...}} 对象。
+16. 出发城市字段名是 origin_city。
+17. adults 和 children 必须是整数(int),
+    绝不能输出空列表 []。用户未提儿童时
+    children=0,未提成人人数时 adults=1。
+
+────────────────────────────────────────────
+Few-shot 示例(严格参照以下提取方式):
+────────────────────────────────────────────
+
+示例1:
+输入:"8月10日从上海去成都,5天4晚,预算5000元,2个大人。喜欢熊猫,轻松一点。酒店靠近地铁,每晚不超300元。优先落地双流机场,直达。"
+输出:
+{{
+  "origin_city": "上海",
+  "destination_city": "成都",
+  "departure_date": "2026-08-10",
+  "return_date": "2026-08-14",
+  "trip_days": 5,
+  "nights": 4,
+  "adults": 2,
+  "children": 0,
+  "total_budget": 5000.0,
+  "currency": "CNY",
+  "interests": ["熊猫"],
+  "pace": "relaxed",
+  "flight_preferences": {{
+    "priority": "balanced",
+    "preferred_departure_airports": [],
+    "preferred_arrival_airports": ["双流机场"],
+    "earliest_departure_time": null,
+    "latest_departure_time": null,
+    "earliest_arrival_time": null,
+    "latest_arrival_time": null,
+    "max_stops": 0
+  }},
+  "hotel_preferences": {{
+    "max_price_per_night": 300.0,
+    "minimum_rating": null,
+    "hotel_classes": [],
+    "near_subway": true,
+    "preferred_areas": [],
+    "amenities": []
+  }},
+  "special_requirements": []
+}}
+
+示例2:
+输入:"想从北京去三亚,喜欢海滩和海鲜,希望住在海边。"
+输出:
+{{
+  "origin_city": "北京",
+  "destination_city": "三亚",
+  "departure_date": null,
+  "return_date": null,
+  "trip_days": null,
+  "nights": null,
+  "adults": 1,
+  "children": 0,
+  "total_budget": null,
+  "currency": "CNY",
+  "interests": ["海滩", "海鲜"],
+  "pace": "normal",
+  "flight_preferences": {{
+    "priority": "balanced",
+    "preferred_departure_airports": [],
+    "preferred_arrival_airports": [],
+    "earliest_departure_time": null,
+    "latest_departure_time": null,
+    "earliest_arrival_time": null,
+    "latest_arrival_time": null,
+    "max_stops": null
+  }},
+  "hotel_preferences": {{
+    "max_price_per_night": null,
+    "minimum_rating": null,
+    "hotel_classes": [],
+    "near_subway": null,
+    "preferred_areas": ["海边"],
+    "amenities": []
+  }},
+  "special_requirements": []
+}}
+
+示例3:
+输入:"下周五从广州出发去丽江,玩4天,一个人,机票要最便宜的。"
+输出(假设当前日期算出下周五=2026-08-07):
+{{
+  "origin_city": "广州",
+  "destination_city": "丽江",
+  "departure_date": "2026-08-07",
+  "return_date": "2026-08-10",
+  "trip_days": 4,
+  "nights": 3,
+  "adults": 1,
+  "children": 0,
+  "total_budget": null,
+  "currency": "CNY",
+  "interests": [],
+  "pace": "normal",
+  "flight_preferences": {{
+    "priority": "price",
+    "preferred_departure_airports": [],
+    "preferred_arrival_airports": [],
+    "earliest_departure_time": null,
+    "latest_departure_time": null,
+    "earliest_arrival_time": null,
+    "latest_arrival_time": null,
+    "max_stops": null
+  }},
+  "hotel_preferences": {{
+    "max_price_per_night": null,
+    "minimum_rating": null,
+    "hotel_classes": [],
+    "near_subway": null,
+    "preferred_areas": [],
+    "amenities": []
+  }},
+  "special_requirements": []
+}}
+"""
+
+
+class RequirementAgent:
+    """负责把自然语言解析为TravelRequest。"""
+
+    def __init__(
+        self,
+        model: BaseChatModel | None = None,
+    ) -> None:
+        """初始化需求解析Agent,支持注入自定义模型。"""
+        base_model = model or get_chat_model()
+
+        # json_mode 通过 response_format 约束模型输出 JSON,
+        # 对 DeepSeek 等推理模型的兼容性更好。
+        self._structured_model = (
+            base_model.with_structured_output(
+                TravelRequest,
+                method="json_mode",
+            )
+        )
+
+    async def parse(
+        self,
+        user_query: str,
+    ) -> TravelRequest:
+        """解析用户自然语言需求为结构化TravelRequest,遇到不明确字段时设置needs_clarification标志。"""
+        query = user_query.strip()
+
+        if not query:
+            raise ValueError("用户旅行需求不能为空。")
+
+        messages = [
+            SystemMessage(
+                content=SYSTEM_PROMPT.format(
+                    current_date=date.today().isoformat()
+                )
+            ),
+            HumanMessage(content=query),
+        ]
+
+        response = await self._structured_model.ainvoke(
+            messages
+        )
+
+        if isinstance(response, TravelRequest):
+            return response
+
+        return TravelRequest.model_validate(response)

+ 132 - 0
app/agents/resource_agent.py

@@ -0,0 +1,132 @@
+from __future__ import annotations
+
+"""资源查询Agent:调用航班/酒店MCP工具搜索外部资源,指导工具调用顺序并汇总结果为结构化数据。"""
+
+from typing import Any
+
+from langchain.agents import create_agent
+from langchain_core.language_models.chat_models import (
+    BaseChatModel,
+)
+from langchain_core.tools import BaseTool
+
+from app.llm import get_chat_model
+from app.schemas.travel_request import TravelRequest
+
+
+# RESOURCE_AGENT_PROMPT:资源查询的系统提示词,定义航班与酒店搜索的工具调用策略。
+RESOURCE_AGENT_PROMPT = """
+你是旅行系统中的资源查询Agent。
+
+你只负责查询机场、航班和酒店,不负责生成最终行程。
+
+必须按照以下顺序执行:
+
+1. 使用search_airports查询出发城市的机场。
+2. 使用search_airports查询目的城市的机场。
+3. 优先选择type为city且城市名称最匹配的结果。
+4. 提取该城市包含的全部机场代码。
+5. 使用search_flights查询真实航班。
+6. 使用search_hotels查询目的城市的真实酒店。
+7. 当前阶段不要调用search_return_flights。
+8. 用户没有提出限制时,不得自行限制:
+   - 是否直飞;
+   - 起飞或落地时间;
+   - 机场;
+   - 机票最高价格;
+   - 酒店星级和评分。
+9. 不得编造机场代码、价格、航班或酒店。
+10. 工具查询结束后,只输出简短的查询完成摘要。
+
+航班查询规则:
+
+- 用户有返程日期时,flight_type使用round_trip。
+- departure_airports使用出发城市全部有效机场代码。
+- arrival_airports使用目的城市全部有效机场代码。
+- 日期必须使用YYYY-MM-DD。
+- 成人和儿童数量严格使用用户需求。
+- 默认币种使用CNY。
+
+酒店查询规则:
+
+- query使用“目的城市 + 酒店”。
+- 入住日期使用出发日期。
+- 退房日期使用返程日期。
+- 不要自行增加价格、评分或星级限制。
+"""
+
+
+class ResourceSearchAgent:
+    """查询机场、航班和酒店的专业Agent。"""
+
+    def __init__(
+        self,
+        tools: list[BaseTool],
+        model: BaseChatModel | None = None,
+    ) -> None:
+        """初始化资源查询Agent,支持注入自定义模型和MCP工具集。"""
+        allowed_names = {
+            "search_airports",
+            "search_flights",
+            "search_hotels",
+        }
+
+        selected_tools = [
+            tool
+            for tool in tools
+            if tool.name in allowed_names
+        ]
+
+        loaded_names = {
+            tool.name
+            for tool in selected_tools
+        }
+
+        missing_names = (
+            allowed_names
+            - loaded_names
+        )
+
+        if missing_names:
+            raise RuntimeError(
+                "资源查询Agent缺少工具:"
+                + "、".join(
+                    sorted(missing_names)
+                )
+            )
+
+        self._agent = create_agent(
+            model=model or get_chat_model(),
+            tools=selected_tools,
+            system_prompt=RESOURCE_AGENT_PROMPT,
+        )
+
+    async def search(
+        self,
+        request: TravelRequest,
+    ) -> dict[str, Any]:
+        """根据结构化旅行需求调用真实工具。"""
+
+        request_json = request.model_dump_json(
+            indent=2,
+        )
+
+        user_message = f"""
+请根据下面的结构化旅行需求执行真实资源查询:
+
+{request_json}
+
+必须实际调用机场、航班和酒店工具。
+不要只根据已有知识直接回答。
+"""
+
+        return await self._agent.ainvoke(
+            {
+                "messages": [
+                    {
+                        "role": "user",
+                        "content": user_message,
+                    }
+                ]
+            }
+        )

+ 351 - 0
app/agents/reviewer_agent.py

@@ -0,0 +1,351 @@
+from __future__ import annotations
+
+"""行程评审Agent:从风险控制角度独立评审已规划行程,识别节奏、地理、预算等维度的问题并给出修改意见。"""
+
+import json
+from typing import Any
+
+from langchain_core.language_models.chat_models import (
+    BaseChatModel,
+)
+from langchain_core.messages import (
+    HumanMessage,
+    SystemMessage,
+)
+
+from app.llm import get_chat_model
+from app.schemas.itinerary import ItineraryPlan
+from app.schemas.place import MapResearchResult
+from app.schemas.review import TripReviewResult
+from app.schemas.route import RouteEvaluationResult
+from app.schemas.travel_request import TravelRequest
+from app.schemas.validation import (
+    PlanValidationResult,
+)
+
+
+# REVIEWER_PROMPT:行程评审的系统提示词,定义多维度评审标准与JSON输出结构。
+REVIEWER_PROMPT = """
+你是多角色旅行规划系统中的独立Trip Reviewer。
+
+你的职责是审查Itinerary Planner生成的旅行方案。
+你不负责重新规划,也不负责查询新数据。
+
+上游已经完成:
+
+1. Requirement Analyst解析用户需求;
+2. Resource Agent查询真实航班和酒店;
+3. Destination Agent查询景点、餐厅和天气;
+4. Route Evaluator评估酒店位置;
+5. Itinerary Planner生成行程;
+6. Plan Validator完成确定性校验。
+
+审查原则:
+
+一、尊重确定性校验
+
+1. Plan Validator发现的error属于阻塞问题。
+2. 不能否定或忽略Validator的确定性结果。
+3. Validator只有warning时,可以根据实际影响判断
+   是否需要阻塞方案。
+4. 不要重新计算价格、日期或ID真实性。
+
+二、用户需求匹配
+
+检查方案是否符合:
+
+1. 用户兴趣;
+2. 用户旅行节奏;
+3. 总预算要求;
+4. 酒店偏好;
+5. 航班偏好;
+6. 特殊要求。
+
+三、行程体验
+
+检查:
+
+1. 到达日是否安排过满;
+2. 返程日是否预留足够时间;
+3. 每天活动数量是否与pace一致;
+4. 是否留有合理休息和用餐时间;
+5. 每日主题是否连贯;
+6. 是否存在明显来回折返;
+7. 酒店选择是否有充分理由;
+8. 航班选择是否有充分理由。
+
+四、真实性
+
+1. 不得提出输入候选之外的新景点、餐厅、
+   酒店或航班。
+2. 不得编造营业时间、门票、天气、距离或交通时间。
+3. 地理判断只能依据输入提供的路线数据。
+4. 没有景点之间路线数据时,
+   不得断言两个景点一定很近或很远。
+
+五、通过标准
+
+下列问题可标记为blocking:
+
+1. 与用户核心需求明显冲突;
+2. Validator存在error;
+3. 到达日或返程日安排明显不可执行;
+4. relaxed行程仍然非常紧凑;
+5. 方案遗漏旅行日期或核心住宿安排;
+6. 选择理由与实际候选信息明显矛盾;
+7. 大量活动缺乏真实数据依据。
+
+下列问题通常只标记warning:
+
+1. 个别活动说明不够详细;
+2. 存在更好的表达方式;
+3. 预算没有覆盖餐饮或门票;
+4. 天气暂时无法查询;
+5. 个别时间需要用户出发前确认。
+
+六、输出要求
+
+1. approved=true表示不存在blocking问题。
+2. approved=false时,必须给出明确、
+   可执行的revision_feedback。
+3. revision_feedback要告诉Planner具体修改什么,
+   不能只写”优化行程”。
+4. 不通过时最多给出6条核心修改要求。
+5. 通过后可以在recommendations中给出
+   非阻塞建议。
+
+七、输出格式(必须严格遵守字段名)
+
+你必须返回一个 JSON 对象,顶层字段如下:
+
+{
+  “approved”: true或false,
+  “summary”: “审查总结(必填,1-3句话概括审查结论)”,
+  “strengths”: [“方案优点”],
+  “issues”: [
+    {
+      “category”: “requirement_fit|pace|geography|flight|hotel|budget|completeness|grounding|user_experience|other”,
+      “severity”: “blocking或warning”,
+      “message”: “问题描述”,
+      “day_index”: 日期序号(可选),
+      “activity_name”: “相关活动名称(可选)”,
+      “evidence”: “证据(可选)”,
+      “suggestion”: “修改建议(可选)”
+    }
+  ],
+  “recommendations”: [“非阻塞建议”],
+  “revision_feedback”: [“不通过时的具体修改指令”]
+}
+
+注意:
+- 不要在最外层包裹 {“tripReviewResult”: {...}},直接输出上述 JSON。
+- 如果 Validator 有 error,必须 approved=false 并在 revision_feedback 中引用这些 error。
+"""
+
+
+def build_reviewer_context(
+    *,
+    request: TravelRequest,
+    plan: ItineraryPlan,
+    validation: PlanValidationResult,
+    map_result: MapResearchResult,
+    route_result: RouteEvaluationResult,
+) -> dict[str, Any]:
+    """压缩Reviewer需要的上下文。"""
+
+    selected_hotel_route = next(
+        (
+            hotel
+            for hotel
+            in route_result.evaluated_hotels
+            if hotel.hotel_id
+            == plan.selected_hotel.hotel_id
+        ),
+        None,
+    )
+
+    route_context: dict[str, Any] | None = None
+
+    if selected_hotel_route is not None:
+        route_context = {
+            "hotel_id": (
+                selected_hotel_route.hotel_id
+            ),
+            "hotel_name": (
+                selected_hotel_route.hotel_name
+            ),
+            "final_score": (
+                selected_hotel_route.final_score
+            ),
+            "average_distance_meters": (
+                selected_hotel_route
+                .average_distance_meters
+            ),
+            "average_duration_seconds": (
+                selected_hotel_route
+                .average_duration_seconds
+            ),
+            "nearest_subway_distance_meters": (
+                selected_hotel_route
+                .nearest_subway_distance_meters
+            ),
+            "reasons": (
+                selected_hotel_route.reasons
+            ),
+            "warnings": (
+                selected_hotel_route.warnings
+            ),
+            "route_legs": [
+                {
+                    "destination_name": (
+                        leg.destination_name
+                    ),
+                    "distance_meters": (
+                        leg.distance_meters
+                    ),
+                    "duration_seconds": (
+                        leg.duration_seconds
+                    ),
+                    "source": leg.source,
+                }
+                for leg
+                in selected_hotel_route.route_legs
+            ],
+        }
+
+    attraction_catalog = [
+        {
+            "poi_id": place.poi_id,
+            "name": place.name,
+            "category": place.category,
+            "address": place.address,
+            "location": place.location,
+            "match_reason": place.match_reason,
+        }
+        for place in map_result.attractions
+    ]
+
+    restaurant_catalog = [
+        {
+            "poi_id": place.poi_id,
+            "name": place.name,
+            "category": place.category,
+            "address": place.address,
+            "location": place.location,
+        }
+        for place in map_result.restaurants
+    ]
+
+    return {
+        "travel_request": request.model_dump(
+            mode="json"
+        ),
+        "itinerary_plan": plan.model_dump(
+            mode="json"
+        ),
+        "deterministic_validation": (
+            validation.model_dump(mode="json")
+        ),
+        "selected_hotel_route_evaluation": (
+            route_context
+        ),
+        "available_attractions": (
+            attraction_catalog
+        ),
+        "available_restaurants": (
+            restaurant_catalog
+        ),
+        "weather": [
+            item.model_dump(mode="json")
+            for item in map_result.weather
+        ],
+        "map_notes": map_result.notes,
+        "route_warnings": route_result.warnings,
+    }
+
+
+class TripReviewerAgent:
+    """独立审查Planner生成的行程。"""
+
+    def __init__(
+        self,
+        model: BaseChatModel | None = None,
+    ) -> None:
+        """初始化行程评审Agent,支持注入自定义模型。"""
+        base_model = model or get_chat_model(
+            timeout=180.0
+        )
+
+        # json_mode 对 DeepSeek 推理模型兼容性更好。
+        self._structured_model = (
+            base_model.with_structured_output(
+                TripReviewResult,
+                method="json_mode",
+            )
+        )
+
+    async def review(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        validation: PlanValidationResult,
+        map_result: MapResearchResult,
+        route_result: RouteEvaluationResult,
+    ) -> dict[str, Any]:
+        """对行程方案做独立评审,返回包含阻塞问题、警告与修改意见的结构化评审结果。"""
+        context = build_reviewer_context(
+            request=request,
+            plan=plan,
+            validation=validation,
+            map_result=map_result,
+            route_result=route_result,
+        )
+
+        context_json = json.dumps(
+            context,
+            ensure_ascii=False,
+            indent=2,
+        )
+
+        user_message = f"""
+请独立审查下面的旅行方案和上游数据:
+
+{context_json}
+
+重点关注用户体验、偏好匹配、节奏、
+安排合理性和确定性校验结果。
+
+必须以 JSON 格式返回 TripReviewResult 结构化结果。
+不要输出结构化结果之外的解释。
+"""
+
+        response = await self._structured_model.ainvoke(
+            [
+                SystemMessage(
+                    content=REVIEWER_PROMPT
+                ),
+                HumanMessage(
+                    content=user_message
+                ),
+            ]
+        )
+
+        if isinstance(response, TripReviewResult):
+            structured = response
+        else:
+            if (
+                isinstance(response, dict)
+                and "tripReviewResult" in response
+                and len(response) == 1
+            ):
+                response = response["tripReviewResult"]
+
+            structured = TripReviewResult.model_validate(
+                response
+            )
+
+        return {
+            "structured_response": structured,
+            "messages": [],
+        }

+ 0 - 0
app/clients/__init__.py


+ 307 - 0
app/clients/serpapi_client.py

@@ -0,0 +1,307 @@
+"""
+这个 Client 只负责:
+    Pydantic查询参数
+    → SerpApi HTTP参数
+    → 发送请求
+    → 返回原始JSON
+"""
+from __future__ import annotations
+from typing import Any
+import httpx
+from app.schemas.flight import FlightSearchQuery
+from app.schemas.hotel import HotelSearchQuery
+
+SERPAPI_BASE_URL = "https://serpapi.com/search.json"
+
+FLIGHT_TYPE_MAP = {
+    "round_trip":1,
+    "one_way":2,
+}
+
+TRAVEL_CLASS_MAP = {
+    "economy":1,
+    "business":3,
+    "first":4,
+    "premium_economy":2,
+}
+
+FLIGHT_SORT_MAP = {
+    "top":1,
+    "price":2,
+    "departure_time":3,
+    "arrival_time":4,
+    "duration":5,
+    "emissions":6,
+}
+
+STOPS_MAP = {
+    "any":0,
+    "nonstop":1,
+    "one_or_fewer":2,
+    "two_or_fewer":3,
+}
+
+HOTEL_SORT_MAP = {
+    "lowest_price":3,
+    "highest_rating":8,
+    "most_reviewed":13,
+}
+
+HOTEL_RATING_MAP = {
+    "3.5+":7,
+    "4.0+":8,
+    "4.5+":9,
+}
+
+class SerpApiError(RuntimeError):
+    """SerApi调用错误。"""
+
+class SerpApiClient:
+    """SerpApi异步HTTP客户端"""
+
+    def __init__(self,api_key:str,timeout_seconds:int | float=30.0)->None:
+        """初始化SerpApiClient,验证API Key并提供可选的自定义base_url。"""
+
+        if not api_key.strip():
+            raise ValueError("SerpApi API Key不能为空。")
+        self.api_key = api_key
+        self.timeout_seconds = timeout_seconds
+        self._client:httpx.AsyncClient | None = None
+
+    async def __aenter__(self)->"SerpApiClient":
+        """异步上下文管理器协议,支持async with用法。"""
+
+        self._ensure_client()
+        return self
+    
+    async def __aexit__(self, exc_type:object, exc_value:object, traceback:object)->None:
+        """异步上下文管理器协议,支持async with用法。"""
+
+        await self.aclose()
+
+    def _ensure_client(self)->httpx.AsyncClient:
+        """惰性创建并缓存httpx.AsyncClient实例。"""
+
+        if self._client is None:
+            timeout = httpx.Timeout(
+                timeout=self.timeout_seconds,
+                connect=min(10.0,self.timeout_seconds),
+                )
+            self._client=httpx.AsyncClient(
+                timeout=timeout,
+                follow_redirects=True
+            )
+        return self._client
+    
+    async def aclose(self)->None:
+        """关闭底层httpx连接,释放资源。"""
+
+        if self._client is not None:
+            await self._client.aclose()
+            self._client = None
+        
+    async def _request(self,params:dict[str,Any],)->dict[str,Any]:
+        """
+        执行SerpApi请求并统一处理错误。
+        错误消息中不会打印完整请求URL,
+        避免API Key通过查询参数泄漏。
+        """
+        client = self._ensure_client()
+        request_params = {
+            **params,
+            "api_key":self.api_key,
+        }
+        try:
+            response = await client.get(
+                SERPAPI_BASE_URL,
+                params=request_params
+            )
+        except httpx.TimeoutException as e:
+            raise SerpApiError(
+                f"SerpApi请求超时,超时时间为{self.timeout_seconds}秒"
+            ) from e
+        except httpx.RequestError as e:
+            raise SerpApiError(
+                f"SerpApi网络请求失败:{type(e).__name__}"
+            ) from e
+        
+        if response.status_code >= 400:
+            error_text = response.text[:500]
+
+            raise SerpApiError(f"SerpApi返回HTTP错误:{response.status_code},响应内容:{error_text}")
+        try:
+            data:dict[str, Any] = response.json()
+        except ValueError as e:
+            raise SerpApiError("SerpApi返回内容不是合法JSON。") from e
+        
+        if error :=data.get("error"):
+            raise SerpApiError(f"SerpApi查询失败:{error}")
+        
+        metadata = data.get("search_metadata",{})
+        status = metadata.get("status")
+
+        if status is not None and str(status).lower() != "success":
+            raise SerpApiError(f"SerpApi返回异常状态:{status}")
+        return data
+    
+    def _build_flight_params(
+    self,
+    query: FlightSearchQuery,
+    ) -> dict[str, Any]:
+        """把内部航班查询模型转换为 SerpApi 参数。"""
+
+        params: dict[str, Any] = {
+            "engine": "google_flights",
+            "departure_id": ",".join(
+                query.departure_airports
+            ),
+            "arrival_id": ",".join(
+                query.arrival_airports
+            ),
+            "outbound_date": query.outbound_date.isoformat(),
+            "type": FLIGHT_TYPE_MAP[query.flight_type],
+            "adults": query.adults,
+            "children": query.children,
+            "travel_class": TRAVEL_CLASS_MAP[
+                query.travel_class
+            ],
+            "currency": query.currency,
+            "hl": query.language,
+            "gl": query.country,
+        }
+
+        if query.return_date is not None:
+            params["return_date"] = (
+                query.return_date.isoformat()
+            )
+
+        if query.sort_by is not None:
+            params["sort_by"] = FLIGHT_SORT_MAP[
+                query.sort_by
+            ]
+
+        if query.stops is not None:
+            params["stops"] = STOPS_MAP[query.stops]
+
+        if query.max_price is not None:
+            params["max_price"] = query.max_price
+
+        if query.outbound_times is not None:
+            params["outbound_times"] = (
+                query.outbound_times
+            )
+
+        if query.show_hidden:
+            params["show_hidden"] = True
+
+        if query.deep_search:
+            params["deep_search"] = True
+
+        if query.no_cache:
+            params["no_cache"] = True
+
+        return params
+
+    async def search_flights_raw(
+        self,
+        query: FlightSearchQuery,
+    ) -> dict[str, Any]:
+        """查询首次航班结果。
+
+        单程搜索返回完整单程候选。
+        往返搜索首先返回去程候选。
+        """
+
+        params = self._build_flight_params(query)
+
+        return await self._request(params)
+
+
+    async def search_return_flights_raw(
+        self,
+        query: FlightSearchQuery,
+        departure_token: str,
+    ) -> dict[str, Any]:
+        """根据选定去程查询对应的返程航班。"""
+
+        if query.flight_type != "round_trip":
+            raise ValueError(
+                "只有 round_trip 查询才能获取返程航班。"
+            )
+
+        token = departure_token.strip()
+
+        if not token:
+            raise ValueError(
+                "departure_token 不能为空。"
+            )
+
+        params = self._build_flight_params(query)
+        params["departure_token"] = token
+
+        return await self._request(params)
+
+    async def search_hotels_raw(self, query: HotelSearchQuery) -> dict[str, Any]:
+        """查询酒店并返回SerpApi原始JSON。"""
+        params:dict[str,Any] = {
+            "engine": "google_hotels",
+            "q": query.query,
+            "check_in_date": query.check_in_date.isoformat(),
+            "check_out_date": query.check_out_date.isoformat(),
+            "adults": query.adults,
+            "children": len(query.children_ages),
+            "currency": query.currency,
+            "hl": query.language,
+            "gl": query.country,
+        }
+
+        if query.children_ages:
+            params["children_ages"] = ",".join(
+                str(age) for age in query.children_ages
+            )
+
+        if query.min_price is not None:
+            params["min_price"] = query.min_price
+
+        if query.max_price is not None:
+            params["max_price"] = query.max_price
+
+        if query.rating_filter is not None:
+            params["rating"] = HOTEL_RATING_MAP[query.rating_filter]
+
+        if query.hotel_class:
+            params["hotel_class"] = ",".join(
+                str(hotel_class) for hotel_class in query.hotel_class
+            )
+
+        if query.free_cancellation is not None:
+            params["free_cancellation"] = query.free_cancellation
+
+        if query.sort is not None:
+            params["sort"] = HOTEL_SORT_MAP[query.sort]
+
+        if query.no_cache:
+            params["no_cache"] = True
+
+        return await self._request(params)
+    
+
+    async def search_airports_raw(
+        self,
+        query: str,
+        language: str = "zh-cn",
+    ) -> dict[str, Any]:
+        """根据城市或机场关键词查询机场信息。"""
+
+        keyword = query.strip()
+
+        if not keyword:
+            raise ValueError("机场搜索关键词不能为空。")
+
+        params: dict[str, Any] = {
+            "engine": "google_flights_autocomplete",
+            "q": keyword,
+            "hl": language,
+        }
+
+        return await self._request(params)

+ 50 - 0
app/config.py

@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+"""项目配置中心:从环境变量或.env读取API密钥、模型参数等敏感配置,通过pydantic-settings统一管理。"""
+
+from functools import lru_cache
+from pathlib import Path
+from pydantic_settings import BaseSettings,SettingsConfigDict
+
+# 项目根目录(app 的上级),用于定位 .env,避免受运行时工作目录(CWD)影响
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+class Settings(BaseSettings):
+    """
+    项目配置:
+        所有敏感信息从环境变量或.env文件中读取,
+        不允许直接写死在业务代码中。
+    """
+    serpapi_api_key:str = ""
+    amap_api_key:str = ""
+
+    openai_api_key:str = ""
+    base_url:str = ""
+    model:str = ""
+    request_timeout_seconds:float = 120.0
+
+    model_config = SettingsConfigDict(
+        env_file = BASE_DIR / ".env",
+        env_file_encoding = "utf-8",
+        case_sensitive = False,
+        extra = "ignore",
+    )
+
+    def require(self, field_name:str)->str:
+        """读取必填配置,缺失时给出明确错误。"""
+
+        value = getattr(self,field_name,None)
+
+        if not isinstance(value,str) or not value.strip():
+            env_name = field_name.upper()
+            raise RuntimeError(
+                f"缺少配置{env_name},请检查项目根目录下的.env文件"
+            )
+        
+        return value.strip()
+    
+@lru_cache
+def get_settings()->Settings:
+    """缓存配置对象,避免反复读取.env。"""
+
+    return Settings()

+ 0 - 0
app/graph/__init__.py


+ 75 - 0
app/graph/builder.py

@@ -0,0 +1,75 @@
+from __future__ import annotations
+"""需求解析阶段子图:从用户原始输入解析为结构化TravelRequest,路由到澄清/就绪/错误节点。"""
+
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    ready_node,
+    route_after_parse,
+)
+from app.graph.state import TravelState
+
+
+def build_requirement_graph(
+    agent: RequirementAgent | None = None,
+):
+    """构建需求解析阶段的LangGraph。"""
+
+    requirement_agent = (
+        agent or RequirementAgent()
+    )
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "ready",
+        ready_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "ready": "ready",
+            "error": "error",
+        },
+    )
+
+    builder.add_edge("clarify", END)
+    builder.add_edge("ready", END)
+    builder.add_edge("error", END)
+
+    return builder.compile()

+ 100 - 0
app/graph/final_nodes.py

@@ -0,0 +1,100 @@
+from __future__ import annotations
+"""定稿渲染节点:调用FinalizationService将审核通过的方案渲染为面向用户的Markdown最终答复。"""
+
+from typing import Any
+
+from app.graph.state import TravelState
+from app.services.finalization_service import (
+    FinalizationService,
+)
+
+
+def make_finalization_node(
+    service: FinalizationService | None = None,
+):
+    """创建最终回答生成节点。"""
+
+    finalization_service = (
+        service or FinalizationService()
+    )
+
+    def finalization_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """定稿节点:收集请求、方案、校验与审查结果,渲染为完整的最终旅行方案文本。"""
+
+        request = state.get("travel_request")
+        plan = state.get("itinerary_plan")
+        validation = state.get(
+            "plan_validation_result"
+        )
+        review = state.get(
+            "trip_review_result"
+        )
+
+        missing: list[str] = []
+
+        if request is None:
+            missing.append("travel_request")
+
+        if plan is None:
+            missing.append("itinerary_plan")
+
+        if validation is None:
+            missing.append(
+                "plan_validation_result"
+            )
+
+        if review is None:
+            missing.append(
+                "trip_review_result"
+            )
+
+        if missing:
+            message = (
+                "最终输出阶段缺少必要状态:"
+                + "、".join(missing)
+            )
+
+            existing_errors = list(
+                state.get("errors", [])
+            )
+
+            existing_errors.append(message)
+
+            return {
+                "errors": existing_errors,
+                "workflow_status": "failed",
+                "final_answer": message,
+            }
+
+        workflow_status = state.get(
+            "workflow_status",
+            "completed_with_risks",
+        )
+
+        approved = (
+            workflow_status == "approved"
+        )
+
+        answer = finalization_service.render(
+            request=request,
+            plan=plan,
+            validation=validation,
+            review=review,
+            approved=approved,
+            planning_attempts=state.get(
+                "planning_attempts",
+                0,
+            ),
+            supervisor_reason=state.get(
+                "supervisor_reason",
+                "未提供Supervisor说明。",
+            ),
+        )
+
+        return {
+            "final_answer": answer,
+        }
+
+    return finalization_node

+ 190 - 0
app/graph/map_nodes.py

@@ -0,0 +1,190 @@
+from __future__ import annotations
+"""地图研究节点:调用MapResearchAgent通过高德MCP搜索目的地周边景点、餐饮与天气。"""
+
+import json
+import re
+from typing import Any, Literal
+
+from langchain_core.messages import AIMessage
+
+from app.agents.map_agent import MapResearchAgent
+from app.graph.state import TravelState
+from app.schemas.place import MapResearchResult
+
+
+def _extract_json_from_messages(
+    messages: list[Any],
+) -> dict[str, Any] | None:
+    """从 Agent 最后一条 AI 消息中提取 JSON 代码块。"""
+
+    for message in reversed(messages):
+        if not isinstance(message, AIMessage):
+            continue
+
+        content = message.content
+
+        if not isinstance(content, str):
+            continue
+
+        # 优先匹配 ```json ... ``` 代码块
+        json_match = re.search(
+            r"```json\s*([\s\S]*?)\s*```",
+            content,
+        )
+
+        if json_match:
+            try:
+                return json.loads(
+                    json_match.group(1)
+                )
+            except json.JSONDecodeError:
+                continue
+
+        # 尝试匹配 ``` ... ``` 无标注代码块
+        code_match = re.search(
+            r"```\s*(\{[\s\S]*?\})\s*```",
+            content,
+        )
+
+        if code_match:
+            try:
+                return json.loads(
+                    code_match.group(1)
+                )
+            except json.JSONDecodeError:
+                continue
+
+        # 最后尝试在整段内容中查找 JSON 对象
+        json_match = re.search(
+            r"\{[\s\S]*\"city\"[\s\S]*\}",
+            content,
+        )
+
+        if json_match:
+            try:
+                return json.loads(
+                    json_match.group(0)
+                )
+            except json.JSONDecodeError:
+                continue
+
+    return None
+
+
+def route_after_resources(
+    state: TravelState,
+) -> Literal[
+    "research_destination",
+    "end",
+]:
+    """资源查询成功后进入地图研究。"""
+
+    if state.get("errors"):
+        return "end"
+
+    flight_result = state.get(
+        "flight_search_result",
+        {},
+    )
+
+    hotel_result = state.get(
+        "hotel_search_result",
+        {},
+    )
+
+    if not flight_result or not hotel_result:
+        return "end"
+
+    return "research_destination"
+
+
+def make_map_research_node(
+    agent: MapResearchAgent,
+):
+    """创建地图研究节点。"""
+
+    async def map_research_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """地图研究节点:调用Agent获取高德地图POI数据,从消息中提取JSON结构化结果写入state。"""
+
+        request = state["travel_request"]
+
+        try:
+            agent_result = await agent.research(
+                request
+            )
+
+            messages = agent_result.get(
+                "messages",
+                [],
+            )
+
+            parsed = _extract_json_from_messages(
+                messages
+            )
+
+            if parsed is None:
+                # 调试:打印最后一条消息的内容
+                last_content = ""
+                for msg in reversed(messages):
+                    if isinstance(msg, AIMessage):
+                        last_content = str(
+                            msg.content
+                        )[:500]
+                        break
+
+                return {
+                    "errors": [
+                        "地图Agent未返回有效的JSON结构化结果。"
+                    ],
+                    "map_messages": messages,
+                    "final_answer": (
+                        "地图Agent未返回有效结果。"
+                        f"最后一条消息预览:{last_content}"
+                    ),
+                }
+
+            structured_response = (
+                MapResearchResult.model_validate(
+                    parsed
+                )
+            )
+
+            attraction_count = len(
+                structured_response.attractions
+            )
+
+            restaurant_count = len(
+                structured_response.restaurants
+            )
+
+            weather_count = len(
+                structured_response.weather
+            )
+
+            return {
+                "map_research_result": (
+                    structured_response
+                ),
+                "map_messages": messages,
+                "final_answer": (
+                    "真实地图资源查询完成:"
+                    f"获得{attraction_count}个景点候选,"
+                    f"{restaurant_count}个餐饮候选,"
+                    f"{weather_count}天天气信息。"
+                ),
+            }
+
+        except Exception as exc:
+            message = (
+                "地图Agent执行失败:"
+                f"{type(exc).__name__}: {exc}"
+            )
+
+            return {
+                "errors": [message],
+                "final_answer": message,
+            }
+
+    return map_research_node

+ 287 - 0
app/graph/nodes.py

@@ -0,0 +1,287 @@
+from __future__ import annotations
+"""需求解析节点及基础控制节点:解析/标准化TravelRequest、澄清、错误处理与路由判断。"""
+
+from datetime import date, timedelta
+from typing import Literal
+
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.graph.state import TravelState
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+
+
+FIELD_LABELS = {
+    "origin_city": "出发城市",
+    "destination_city": "目的城市",
+    "departure_date": "出发日期",
+    "return_date": "返程日期",
+}
+
+
+def normalize_and_validate_request(
+    request: TravelRequest,
+) -> tuple[
+    TravelRequest,
+    list[str],
+    list[str],
+]:
+    """补全可确定推导的字段,并检查关键信息。
+
+    返回:
+    1. 标准化后的需求;
+    2. 缺失字段;
+    3. 错误信息。
+    """
+
+    updates: dict[str, object] = {}
+    errors: list[str] = []
+
+    departure_date = request.departure_date
+    return_date = request.return_date
+
+    # 根据出发日期和天数推算返程日期。
+    if (
+        departure_date is not None
+        and return_date is None
+    ):
+        if request.nights is not None:
+            updates["return_date"] = (
+                departure_date
+                + timedelta(days=request.nights)
+            )
+
+        elif request.trip_days is not None:
+            updates["return_date"] = (
+                departure_date
+                + timedelta(
+                    days=request.trip_days - 1
+                )
+            )
+
+    normalized = request.model_copy(
+        update=updates
+    )
+
+    # 日期完整时,补全天数和晚数。
+    if (
+        normalized.departure_date is not None
+        and normalized.return_date is not None
+    ):
+        nights = (
+            normalized.return_date
+            - normalized.departure_date
+        ).days
+
+        if normalized.nights is None:
+            normalized = normalized.model_copy(
+                update={"nights": nights}
+            )
+
+        if normalized.trip_days is None:
+            normalized = normalized.model_copy(
+                update={"trip_days": nights + 1}
+            )
+
+    missing_fields: list[str] = []
+
+    for field_name in (
+        "origin_city",
+        "destination_city",
+        "departure_date",
+        "return_date",
+    ):
+        if getattr(normalized, field_name) is None:
+            missing_fields.append(field_name)
+
+    if (
+        normalized.origin_city
+        and normalized.destination_city
+        and normalized.origin_city
+        == normalized.destination_city
+    ):
+        errors.append(
+            "出发城市和目的城市不能相同。"
+        )
+
+    if (
+        normalized.departure_date is not None
+        and normalized.departure_date
+        < date.today()
+    ):
+        errors.append(
+            "出发日期早于当前日期。"
+        )
+
+    if (
+        normalized.departure_date is not None
+        and normalized.return_date is not None
+        and normalized.return_date
+        <= normalized.departure_date
+    ):
+        errors.append(
+            "返程日期必须晚于出发日期。"
+        )
+
+    return normalized, missing_fields, errors
+
+
+def make_parse_request_node(
+    agent: RequirementAgent,
+):
+    """创建带依赖的需求解析节点。"""
+
+    async def parse_request_node(
+        state: TravelState,
+    ) -> dict:
+        """需求解析节点:调用RequirementAgent解析用户输入为结构化TravelRequest,失败时写入errors。"""
+
+        user_query = state.get(
+            "user_query",
+            "",
+        ).strip()
+
+        if not user_query:
+            return {
+                "errors": [
+                    "用户旅行需求不能为空。"
+                ],
+                "missing_fields": [],
+            }
+
+        try:
+            request = await agent.parse(user_query)
+
+            (
+                normalized_request,
+                missing_fields,
+                errors,
+            ) = normalize_and_validate_request(
+                request
+            )
+
+            return {
+                "travel_request": (
+                    normalized_request
+                ),
+                "missing_fields": missing_fields,
+                "errors": errors,
+            }
+
+        except Exception as exc:
+            return {
+                "missing_fields": [],
+                "errors": [
+                    "需求解析失败:"
+                    f"{type(exc).__name__}: {exc}"
+                ],
+            }
+
+    return parse_request_node
+
+
+def route_after_parse(
+    state: TravelState,
+) -> Literal[
+    "clarify",
+    "ready",
+    "error",
+]:
+    """根据需求解析结果决定下一节点。"""
+
+    if state.get("errors"):
+        return "error"
+
+    if state.get("missing_fields"):
+        return "clarify"
+
+    return "ready"
+
+
+def clarification_node(
+    state: TravelState,
+) -> dict:
+    """生成需要用户补充的问题。"""
+
+    missing_fields = state.get(
+        "missing_fields",
+        [],
+    )
+
+    labels = [
+        FIELD_LABELS.get(field, field)
+        for field in missing_fields
+    ]
+
+    question = (
+        "为了继续规划,请补充:"
+        + "、".join(labels)
+        + "。"
+    )
+
+    return {
+        "needs_clarification": True,
+        "clarification_question": question,
+        "final_answer": question,
+    }
+
+
+def error_node(
+    state: TravelState,
+) -> dict:
+    """向用户返回确定性校验错误。"""
+
+    errors = state.get("errors", [])
+
+    message = (
+        "旅行需求存在以下问题:"
+        + ";".join(errors)
+        + " 请修改后重新提交。"
+    )
+
+    return {
+        "needs_clarification": True,
+        "final_answer": message,
+    }
+
+
+def ready_node(
+    state: TravelState,
+) -> dict:
+    """本阶段的完成节点。
+
+    后续这里会连接航班、酒店和景点查询节点。
+    """
+
+    request = state["travel_request"]
+
+    travelers = (
+        f"{request.adults}位成人"
+        f"、{request.children}位儿童"
+    )
+
+    budget_text = (
+        f"{request.total_budget:.0f}"
+        f"{request.currency}"
+        if request.total_budget is not None
+        else "未设置总预算"
+    )
+
+    message = (
+        "需求解析完成:"
+        f"{request.origin_city}"
+        f" → {request.destination_city},"
+        f"{request.departure_date}"
+        f" 至 {request.return_date},"
+        f"{request.trip_days}天"
+        f"{request.nights}晚,"
+        f"{travelers},"
+        f"{budget_text}。"
+    )
+
+    return {
+        "needs_clarification": False,
+        "final_answer": message,
+    }

+ 317 - 0
app/graph/planner_builder.py

@@ -0,0 +1,317 @@
+from __future__ import annotations
+"""完整多角色工作流主图:编排从需求解析→资源查询→候选筛选→地图研究→路线评估→行程规划→校验→评审→最终定稿的全部节点与条件边。"""
+
+from langchain_core.tools import BaseTool
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.itinerary_agent import (
+    ItineraryPlannerAgent,
+)
+from app.agents.map_agent import (
+    MapResearchAgent,
+)
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.map_nodes import (
+    make_map_research_node,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    route_after_parse,
+)
+from app.graph.planner_nodes import (
+    make_itinerary_planner_node,
+    route_after_route_evaluation,
+)
+from app.graph.resource_nodes import (
+    make_resource_search_node,
+)
+from app.graph.route_nodes import (
+    make_route_evaluation_node,
+    route_after_map_research,
+    route_after_selection,
+)
+from app.graph.selection_nodes import (
+    make_candidate_selection_node,
+    route_after_resources_to_selection,
+)
+from app.graph.state import TravelState
+from app.mcp_client import load_mcp_tools
+from app.graph.validation_nodes import (
+    make_plan_validation_node,
+    route_after_planning,
+)
+from app.agents.reviewer_agent import (
+    TripReviewerAgent,
+)
+from app.graph.review_nodes import (
+    make_trip_reviewer_node,
+    route_after_validation,
+)
+from app.graph.final_nodes import (
+    make_finalization_node,
+)
+
+from app.graph.supervisor_nodes import (
+    make_supervisor_node,
+    route_after_supervisor,
+)
+
+
+def find_tool(
+    tools: list[BaseTool],
+    *names: str,
+) -> BaseTool | None:
+    """按名称查找已经加载的MCP工具。"""
+
+    for name in names:
+        for tool in tools:
+            if tool.name == name:
+                return tool
+
+    return None
+
+
+async def build_planner_graph():
+    """构建直到行程规划完成的多角色工作流。"""
+
+    tool_bundle = await load_mcp_tools()
+
+    return_flight_tool = find_tool(
+        tool_bundle.travel_tools,
+        "search_return_flights",
+    )
+
+    distance_tool = find_tool(
+        tool_bundle.amap_tools,
+        "maps_distance",
+    )
+
+    around_search_tool = find_tool(
+        tool_bundle.amap_tools,
+        "maps_around_search",
+        "maps_search_around",
+    )
+
+    if return_flight_tool is None:
+        raise RuntimeError(
+            "没有加载search_return_flights工具。"
+        )
+
+    if distance_tool is None:
+        raise RuntimeError(
+            "没有加载maps_distance工具。"
+        )
+
+    requirement_agent = RequirementAgent()
+
+    resource_agent = ResourceSearchAgent(
+        tools=tool_bundle.travel_tools,
+    )
+
+    map_agent = MapResearchAgent(
+        tools=tool_bundle.amap_tools,
+    )
+
+    planner_agent = ItineraryPlannerAgent()
+    reviewer_agent = TripReviewerAgent()
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_node(
+        "search_resources",
+        make_resource_search_node(
+            resource_agent
+        ),
+    )
+
+    builder.add_node(
+        "select_candidates",
+        make_candidate_selection_node(
+            return_flight_tool,
+            max_outbound_queries=2,
+            max_returns_per_outbound=5,
+        ),
+    )
+
+    builder.add_node(
+        "research_destination",
+        make_map_research_node(
+            map_agent
+        ),
+    )
+
+    builder.add_node(
+        "evaluate_routes",
+        make_route_evaluation_node(
+            distance_tool=distance_tool,
+            around_search_tool=(
+                around_search_tool
+            ),
+            max_hotels=3,
+            max_attractions=4,
+        ),
+    )
+
+    builder.add_node(
+        "plan_itinerary",
+        make_itinerary_planner_node(
+            planner_agent
+        ),
+    )
+
+    builder.add_node(
+        "validate_plan",
+        make_plan_validation_node(),
+    )
+
+    builder.add_node(
+    "review_plan",
+    make_trip_reviewer_node(
+        reviewer_agent
+    ),
+    )
+
+    builder.add_node(
+    "supervise",
+    make_supervisor_node(
+        max_planning_attempts=3,
+    ),
+)
+
+    builder.add_node(
+        "finalize_response",
+        make_finalization_node(),
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "error": "error",
+            "ready": "search_resources",
+        },
+    )
+
+    builder.add_edge("clarify", END)
+    builder.add_edge("error", END)
+
+    builder.add_conditional_edges(
+        "search_resources",
+        route_after_resources_to_selection,
+        {
+            "select_candidates": (
+                "select_candidates"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "select_candidates",
+        route_after_selection,
+        {
+            "research_destination": (
+                "research_destination"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "research_destination",
+        route_after_map_research,
+        {
+            "evaluate_routes": (
+                "evaluate_routes"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "evaluate_routes",
+        route_after_route_evaluation,
+        {
+            "plan_itinerary": (
+                "plan_itinerary"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "plan_itinerary",
+        route_after_planning,
+        {
+            "validate_plan": "validate_plan",
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+    "validate_plan",
+    route_after_validation,
+    {
+        "review_plan": "review_plan",
+        "end": END,
+    },
+    )
+
+    builder.add_edge(
+    "review_plan",
+    "supervise",
+    )
+
+    builder.add_conditional_edges(
+        "supervise",
+        route_after_supervisor,
+        {
+            "replan": "plan_itinerary",
+            "finalize": "finalize_response",
+            "finalize_with_risks": (
+                "finalize_response"
+            ),
+            "terminate": END,
+        },
+        )
+
+    builder.add_edge(
+        "finalize_response",
+        END,
+    )
+
+    return builder.compile()

+ 150 - 0
app/graph/planner_nodes.py

@@ -0,0 +1,150 @@
+from __future__ import annotations
+"""行程规划节点:调用ItineraryPlannerAgent根据候选资源和修改意见生成多日行程。"""
+
+from typing import Any, Literal
+
+from app.agents.itinerary_agent import (
+    ItineraryPlannerAgent,
+)
+from app.graph.state import TravelState
+from app.schemas.itinerary import ItineraryPlan
+
+
+def route_after_route_evaluation(
+    state: TravelState,
+) -> Literal[
+    "plan_itinerary",
+    "end",
+]:
+    """路线评估成功后进入行程规划。"""
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get(
+        "candidate_selection_result"
+    ):
+        return "end"
+
+    if not state.get(
+        "map_research_result"
+    ):
+        return "end"
+
+    if not state.get(
+        "route_evaluation_result"
+    ):
+        return "end"
+
+    return "plan_itinerary"
+
+
+def make_itinerary_planner_node(
+    agent: ItineraryPlannerAgent,
+):
+    """创建行程规划角色节点。"""
+
+    async def itinerary_planner_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """行程规划节点:调用Agent生成ItineraryPlan,必要时合并revision_feedback进行重规划。"""
+
+        attempts = (
+            state.get("planning_attempts", 0)
+            + 1
+        )
+
+        try:
+            result = await agent.plan(
+                request=state["travel_request"],
+                selection=state[
+                    "candidate_selection_result"
+                ],
+                map_result=state[
+                    "map_research_result"
+                ],
+                route_result=state[
+                    "route_evaluation_result"
+                ],
+                revision_feedback=state.get(
+                    "revision_feedback",
+                    [],
+                ),
+            )
+
+            structured_response = result.get(
+                "structured_response"
+            )
+
+            if structured_response is None:
+                message = (
+                    "行程规划Agent没有返回"
+                    "结构化结果。"
+                )
+
+                return {
+                    "planning_attempts": attempts,
+                    "planner_messages": result.get(
+                        "messages",
+                        [],
+                    ),
+                    "errors": [message],
+                    "final_answer": message,
+                }
+
+            if not isinstance(
+                structured_response,
+                ItineraryPlan,
+            ):
+                structured_response = (
+                    ItineraryPlan.model_validate(
+                        structured_response
+                    )
+                )
+
+            day_count = len(
+                structured_response.days
+            )
+
+            activity_count = sum(
+                len(day.activities)
+                for day
+                in structured_response.days
+            )
+
+            return {
+                "itinerary_plan": (
+                    structured_response
+                ),
+                "planner_messages": result.get(
+                    "messages",
+                    [],
+                ),
+                "planning_attempts": attempts,
+                "final_answer": (
+                    "行程规划角色执行完成:"
+                    f"生成{day_count}天行程,"
+                    f"共安排{activity_count}项活动;"
+                    "等待预算和规则校验。"
+                ),
+            }
+
+        except Exception as exc:
+            message = (
+                "行程规划Agent执行失败:"
+                f"{type(exc).__name__}: {exc}"
+            )
+
+            existing_errors = list(
+                state.get("errors", [])
+            )
+
+            existing_errors.append(message)
+
+            return {
+                "planning_attempts": attempts,
+                "errors": existing_errors,
+                "final_answer": message,
+            }
+
+    return itinerary_planner_node

+ 116 - 0
app/graph/research_builder.py

@@ -0,0 +1,116 @@
+from __future__ import annotations
+"""地图研究阶段子图:对目的地执行高德地图POI搜索与天气查询,为路线评估准备数据。"""
+
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.map_agent import MapResearchAgent
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.map_nodes import (
+    make_map_research_node,
+    route_after_resources,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    route_after_parse,
+)
+from app.graph.resource_nodes import (
+    make_resource_search_node,
+)
+from app.graph.state import TravelState
+from app.mcp_client import load_mcp_tools
+
+
+async def build_research_graph():
+    """构建需求、旅行资源和地图研究工作流。"""
+
+    tool_bundle = await load_mcp_tools()
+
+    requirement_agent = RequirementAgent()
+
+    resource_agent = ResourceSearchAgent(
+        tools=tool_bundle.travel_tools,
+    )
+
+    map_agent = MapResearchAgent(
+        tools=tool_bundle.amap_tools,
+    )
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_node(
+        "search_resources",
+        make_resource_search_node(
+            resource_agent
+        ),
+    )
+
+    builder.add_node(
+        "research_destination",
+        make_map_research_node(
+            map_agent
+        ),
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "error": "error",
+            "ready": "search_resources",
+        },
+    )
+
+    builder.add_edge("clarify", END)
+    builder.add_edge("error", END)
+
+    builder.add_conditional_edges(
+        "search_resources",
+        route_after_resources,
+        {
+            "research_destination": (
+                "research_destination"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_edge(
+        "research_destination",
+        END,
+    )
+
+    return builder.compile()

+ 372 - 0
app/graph/resource_nodes.py

@@ -0,0 +1,372 @@
+from __future__ import annotations
+"""资源查询节点:调用ResourceSearchAgent执行航班与酒店搜索,汇总MCP工具返回结果。"""
+
+import json
+from typing import Any
+
+from langchain_core.messages import (
+    AIMessage,
+    ToolMessage,
+)
+
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.state import TravelState
+
+
+def extract_tool_payload(
+    message: ToolMessage,
+) -> dict[str, Any] | None:
+    """从MCP ToolMessage中提取结构化结果。"""
+
+    artifact = getattr(
+        message,
+        "artifact",
+        None,
+    )
+
+    if isinstance(artifact, dict):
+        structured = artifact.get(
+            "structured_content"
+        )
+
+        if isinstance(structured, dict):
+            return structured
+
+        # 部分版本直接将结构放入artifact。
+        if (
+            "flights" in artifact
+            or "hotels" in artifact
+            or "suggestions" in artifact
+        ):
+            return artifact
+
+    content = message.content
+
+    if isinstance(content, str):
+        try:
+            parsed = json.loads(content)
+
+            if isinstance(parsed, dict):
+                return parsed
+        except json.JSONDecodeError:
+            return None
+
+    if isinstance(content, list):
+        for block in content:
+            if not isinstance(block, dict):
+                continue
+
+            text = block.get("text")
+
+            if not isinstance(text, str):
+                continue
+
+            try:
+                parsed = json.loads(text)
+
+                if isinstance(parsed, dict):
+                    return parsed
+            except json.JSONDecodeError:
+                continue
+
+    return None
+
+
+def collect_tool_results(
+    messages: list[Any],
+) -> list[dict[str, Any]]:
+    """收集Agent执行过的全部工具结果。"""
+
+    tool_calls: dict[str, dict[str, Any]] = {}
+
+    for message in messages:
+        if not isinstance(message, AIMessage):
+            continue
+
+        for tool_call in message.tool_calls:
+            tool_call_id = tool_call.get("id")
+
+            if tool_call_id:
+                tool_calls[tool_call_id] = {
+                    "name": tool_call.get("name"),
+                    "args": tool_call.get(
+                        "args",
+                        {},
+                    ),
+                }
+
+    records: list[dict[str, Any]] = []
+
+    for message in messages:
+        if not isinstance(message, ToolMessage):
+            continue
+
+        payload = extract_tool_payload(message)
+
+        if payload is None:
+            continue
+
+        call_info = tool_calls.get(
+            message.tool_call_id,
+            {},
+        )
+
+        records.append(
+            {
+                "name": (
+                    message.name
+                    or call_info.get("name")
+                ),
+                "args": call_info.get(
+                    "args",
+                    {},
+                ),
+                "payload": payload,
+            }
+        )
+
+    return records
+
+
+def extract_airport_codes(
+    payload: dict[str, Any],
+) -> list[str]:
+    """从机场自动补全结果中提取城市机场代码。"""
+
+    suggestions = payload.get(
+        "suggestions",
+        [],
+    )
+
+    if not isinstance(suggestions, list):
+        return []
+
+    city_suggestions = [
+        suggestion
+        for suggestion in suggestions
+        if isinstance(suggestion, dict)
+        and suggestion.get("type") == "city"
+    ]
+
+    candidates = (
+        city_suggestions
+        if city_suggestions
+        else suggestions
+    )
+
+    for suggestion in candidates:
+        if not isinstance(suggestion, dict):
+            continue
+
+        airports = suggestion.get(
+            "airports",
+            [],
+        )
+
+        if not isinstance(airports, list):
+            continue
+
+        codes: list[str] = []
+
+        for airport in airports:
+            if not isinstance(airport, dict):
+                continue
+
+            code = str(
+                airport.get("code", "")
+            ).strip().upper()
+
+            if code and code not in codes:
+                codes.append(code)
+
+        if codes:
+            return codes
+
+    return []
+
+
+def make_resource_search_node(
+    agent: ResourceSearchAgent,
+):
+    """创建LangGraph资源查询节点。"""
+
+    async def resource_search_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """资源查询节点:调用Agent并行搜索航班与酒店,将结构化结果写入state对应字段。"""
+
+        request = state["travel_request"]
+
+        try:
+            agent_result = await agent.search(
+                request
+            )
+
+            messages = agent_result.get(
+                "messages",
+                [],
+            )
+
+            records = collect_tool_results(
+                messages
+            )
+
+            airport_records = [
+                record
+                for record in records
+                if record["name"]
+                == "search_airports"
+            ]
+
+            flight_records = [
+                record
+                for record in records
+                if record["name"]
+                == "search_flights"
+            ]
+
+            hotel_records = [
+                record
+                for record in records
+                if record["name"]
+                == "search_hotels"
+            ]
+
+            origin_payload = next(
+                (
+                    record["payload"]
+                    for record in airport_records
+                    if record["payload"].get(
+                        "query"
+                    )
+                    == request.origin_city
+                ),
+                (
+                    airport_records[0]["payload"]
+                    if airport_records
+                    else {}
+                ),
+            )
+
+            destination_payload = next(
+                (
+                    record["payload"]
+                    for record in airport_records
+                    if record["payload"].get(
+                        "query"
+                    )
+                    == request.destination_city
+                ),
+                (
+                    airport_records[1]["payload"]
+                    if len(airport_records) > 1
+                    else {}
+                ),
+            )
+
+            origin_airports = (
+                extract_airport_codes(
+                    origin_payload
+                )
+            )
+
+            destination_airports = (
+                extract_airport_codes(
+                    destination_payload
+                )
+            )
+
+            flight_result = (
+                flight_records[-1]["payload"]
+                if flight_records
+                else {}
+            )
+
+            hotel_result = (
+                hotel_records[-1]["payload"]
+                if hotel_records
+                else {}
+            )
+
+            errors: list[str] = []
+
+            if not origin_airports:
+                errors.append(
+                    "未解析出出发城市机场。"
+                )
+
+            if not destination_airports:
+                errors.append(
+                    "未解析出目的城市机场。"
+                )
+
+            if not flight_result:
+                errors.append(
+                    "资源Agent未返回航班结果。"
+                )
+
+            if not hotel_result:
+                errors.append(
+                    "资源Agent未返回酒店结果。"
+                )
+
+            if errors:
+                return {
+                    "resource_messages": messages,
+                    "errors": errors,
+                    "final_answer": (
+                        "资源查询失败:"
+                        + ";".join(errors)
+                    ),
+                }
+
+            flight_count = flight_result.get(
+                "total_count",
+                0,
+            )
+
+            hotel_count = hotel_result.get(
+                "total_count",
+                0,
+            )
+
+            return {
+                "origin_airports": (
+                    origin_airports
+                ),
+                "destination_airports": (
+                    destination_airports
+                ),
+                "flight_search_result": (
+                    flight_result
+                ),
+                "hotel_search_result": (
+                    hotel_result
+                ),
+                "resource_messages": messages,
+                "final_answer": (
+                    "真实旅行资源查询完成:"
+                    f"出发机场"
+                    f"{'、'.join(origin_airports)},"
+                    f"目的机场"
+                    f"{'、'.join(destination_airports)};"
+                    f"获得{flight_count}条航班候选,"
+                    f"{hotel_count}家酒店候选。"
+                ),
+            }
+
+        except Exception as exc:
+            return {
+                "errors": [
+                    "资源查询Agent执行失败:"
+                    f"{type(exc).__name__}: {exc}"
+                ],
+                "final_answer": (
+                    "资源查询Agent执行失败:"
+                    f"{type(exc).__name__}: {exc}"
+                ),
+            }
+
+    return resource_search_node

+ 198 - 0
app/graph/review_nodes.py

@@ -0,0 +1,198 @@
+from __future__ import annotations
+"""Trip Reviewer节点:调用TripReviewerAgent从风险角度独立评审行程,生成阻塞问题与优化建议。"""
+
+from typing import Any, Literal
+
+from app.agents.reviewer_agent import (
+    TripReviewerAgent,
+)
+from app.graph.state import TravelState
+from app.schemas.review import TripReviewResult
+
+
+def route_after_validation(
+    state: TravelState,
+) -> Literal[
+    "review_plan",
+    "end",
+]:
+    """确定性校验执行后进入Reviewer。
+
+    即使方案校验失败,也允许Reviewer读取错误,
+    生成更完整的修改意见。
+    """
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get(
+        "plan_validation_result"
+    ):
+        return "end"
+
+    if not state.get("itinerary_plan"):
+        return "end"
+
+    return "review_plan"
+
+
+def _unique_feedback(
+    values: list[str],
+) -> list[str]:
+    """对修改意见列表去重并去除空字符串,保持原有顺序。"""
+
+    result: list[str] = []
+
+    for value in values:
+        normalized = value.strip()
+
+        if (
+            normalized
+            and normalized not in result
+        ):
+            result.append(normalized)
+
+    return result
+
+
+def make_trip_reviewer_node(
+    agent: TripReviewerAgent,
+):
+    """创建Trip Reviewer节点。"""
+
+    async def trip_reviewer_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """评审节点:调用ReviewerAgent独立审查行程方案,将评审结论与修改意见写入state。"""
+
+        attempts = (
+            state.get("review_attempts", 0)
+            + 1
+        )
+
+        validation = state[
+            "plan_validation_result"
+        ]
+
+        try:
+            result = await agent.review(
+                request=state["travel_request"],
+                plan=state["itinerary_plan"],
+                validation=validation,
+                map_result=state[
+                    "map_research_result"
+                ],
+                route_result=state[
+                    "route_evaluation_result"
+                ],
+            )
+
+            structured_response = result.get(
+                "structured_response"
+            )
+
+            if structured_response is None:
+                message = (
+                    "Trip Reviewer没有返回"
+                    "结构化结果。"
+                )
+
+                existing_errors = list(
+                    state.get("errors", [])
+                )
+
+                existing_errors.append(message)
+
+                return {
+                    "review_attempts": attempts,
+                    "reviewer_messages": (
+                        result.get("messages", [])
+                    ),
+                    "review_passed": False,
+                    "errors": existing_errors,
+                    "final_answer": message,
+                }
+
+            if not isinstance(
+                structured_response,
+                TripReviewResult,
+            ):
+                structured_response = (
+                    TripReviewResult.model_validate(
+                        structured_response
+                    )
+                )
+
+            # Reviewer不能覆盖Validator结果。
+            review_passed = (
+                validation.is_valid
+                and structured_response.approved
+            )
+
+            combined_feedback = (
+                validation.revision_feedback
+                + structured_response
+                .revision_feedback
+            )
+
+            combined_feedback = _unique_feedback(
+                combined_feedback
+            )
+
+            if review_passed:
+                combined_feedback = []
+
+                message = (
+                    "Trip Reviewer审查通过:"
+                    f"发现"
+                    f"{len(structured_response.issues)}"
+                    "项非阻塞问题,"
+                    "方案可以进入最终输出阶段。"
+                )
+            else:
+                message = (
+                    "Trip Reviewer审查未通过:"
+                    f"Validator错误"
+                    f"{validation.error_count}项,"
+                    "Reviewer发现"
+                    f"{len(structured_response.issues)}"
+                    "项问题;"
+                    "方案需要重新规划。"
+                )
+
+            return {
+                "trip_review_result": (
+                    structured_response
+                ),
+                "reviewer_messages": result.get(
+                    "messages",
+                    [],
+                ),
+                "review_passed": review_passed,
+                "review_attempts": attempts,
+                "revision_feedback": (
+                    combined_feedback
+                ),
+                "final_answer": message,
+            }
+
+        except Exception as exc:
+            message = (
+                "Trip Reviewer执行失败:"
+                f"{type(exc).__name__}: {exc}"
+            )
+
+            existing_errors = list(
+                state.get("errors", [])
+            )
+
+            existing_errors.append(message)
+
+            return {
+                "review_attempts": attempts,
+                "review_passed": False,
+                "errors": existing_errors,
+                "final_answer": message,
+            }
+
+    return trip_reviewer_node

+ 210 - 0
app/graph/route_builder.py

@@ -0,0 +1,210 @@
+from __future__ import annotations
+"""路线评估阶段子图:组合候选航班与酒店,调用高德距离工具评估酒店位置与景点路线。"""
+
+from langchain_core.tools import BaseTool
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.map_agent import (
+    MapResearchAgent,
+)
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.map_nodes import (
+    make_map_research_node,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    route_after_parse,
+)
+from app.graph.resource_nodes import (
+    make_resource_search_node,
+)
+from app.graph.route_nodes import (
+    make_route_evaluation_node,
+    route_after_map_research,
+    route_after_selection,
+)
+from app.graph.selection_nodes import (
+    make_candidate_selection_node,
+    route_after_resources_to_selection,
+)
+from app.graph.state import TravelState
+from app.mcp_client import load_mcp_tools
+
+
+def find_tool(
+    tools: list[BaseTool],
+    *names: str,
+) -> BaseTool | None:
+    """按名称在MCP工具列表中查找目标工具,未找到返回None。"""
+
+    for name in names:
+        for tool in tools:
+            if tool.name == name:
+                return tool
+
+    return None
+
+
+async def build_route_graph():
+    """构建从需求解析到酒店路线评估的完整流程。"""
+
+    tool_bundle = await load_mcp_tools()
+
+    return_flight_tool = find_tool(
+        tool_bundle.travel_tools,
+        "search_return_flights",
+    )
+
+    distance_tool = find_tool(
+        tool_bundle.amap_tools,
+        "maps_distance",
+    )
+
+    around_search_tool = find_tool(
+        tool_bundle.amap_tools,
+        "maps_around_search",
+        "maps_search_around",
+    )
+
+    if return_flight_tool is None:
+        raise RuntimeError(
+            "没有加载search_return_flights工具。"
+        )
+
+    if distance_tool is None:
+        raise RuntimeError(
+            "没有加载maps_distance工具。"
+        )
+
+    requirement_agent = RequirementAgent()
+
+    resource_agent = ResourceSearchAgent(
+        tools=tool_bundle.travel_tools,
+    )
+
+    map_agent = MapResearchAgent(
+        tools=tool_bundle.amap_tools,
+    )
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_node(
+        "search_resources",
+        make_resource_search_node(
+            resource_agent
+        ),
+    )
+
+    builder.add_node(
+        "select_candidates",
+        make_candidate_selection_node(
+            return_flight_tool,
+            max_outbound_queries=2,
+            max_returns_per_outbound=5,
+        ),
+    )
+
+    builder.add_node(
+        "research_destination",
+        make_map_research_node(
+            map_agent
+        ),
+    )
+
+    builder.add_node(
+        "evaluate_routes",
+        make_route_evaluation_node(
+            distance_tool=distance_tool,
+            around_search_tool=(
+                around_search_tool
+            ),
+            max_hotels=3,
+            max_attractions=4,
+        ),
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "error": "error",
+            "ready": "search_resources",
+        },
+    )
+
+    builder.add_edge("clarify", END)
+    builder.add_edge("error", END)
+
+    builder.add_conditional_edges(
+        "search_resources",
+        route_after_resources_to_selection,
+        {
+            "select_candidates": (
+                "select_candidates"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "select_candidates",
+        route_after_selection,
+        {
+            "research_destination": (
+                "research_destination"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_conditional_edges(
+        "research_destination",
+        route_after_map_research,
+        {
+            "evaluate_routes": (
+                "evaluate_routes"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_edge(
+        "evaluate_routes",
+        END,
+    )
+
+    return builder.compile()

+ 586 - 0
app/graph/route_nodes.py

@@ -0,0 +1,586 @@
+from __future__ import annotations
+"""路线评估节点:组合候选航班与酒店结果,调用高德距离工具评估酒店与景点的实际交通距离。"""
+
+import json
+from typing import Any, Literal
+
+from langchain_core.messages import ToolMessage
+from langchain_core.tools import BaseTool
+
+from app.graph.state import TravelState
+from app.schemas.route import (
+    RouteEvaluationResult,
+    RouteLeg,
+)
+from app.services.route_evaluation_service import (
+    RouteEvaluationService,
+)
+
+
+def _to_mapping(
+    value: Any,
+) -> dict[str, Any] | None:
+    """把dict或Pydantic模型统一转换为dict,方便取structuredContent。"""
+
+    if isinstance(value, dict):
+        return value
+
+    model_dump = getattr(
+        value,
+        "model_dump",
+        None,
+    )
+
+    if callable(model_dump):
+        dumped = model_dump()
+
+        if isinstance(dumped, dict):
+            return dumped
+
+    return None
+
+
+def extract_mcp_payload(
+    result: Any,
+) -> dict[str, Any]:
+    """提取直接调用MCP工具后的结构化内容。
+
+    LangChain Adapter可能把structuredContent
+    放入ToolMessage.artifact。
+    """
+
+    if isinstance(result, ToolMessage):
+        artifact = getattr(
+            result,
+            "artifact",
+            None,
+        )
+
+        artifact_mapping = _to_mapping(
+            artifact
+        )
+
+        if artifact_mapping:
+            for key in (
+                "structured_content",
+                "structuredContent",
+            ):
+                structured = (
+                    artifact_mapping.get(key)
+                )
+
+                if isinstance(
+                    structured,
+                    dict,
+                ):
+                    return structured
+
+            return artifact_mapping
+
+        structured_attr = getattr(
+            artifact,
+            "structured_content",
+            None,
+        )
+
+        if isinstance(
+            structured_attr,
+            dict,
+        ):
+            return structured_attr
+
+        result = result.content
+
+    mapping = _to_mapping(result)
+
+    if mapping:
+        for key in (
+            "structured_content",
+            "structuredContent",
+        ):
+            structured = mapping.get(key)
+
+            if isinstance(structured, dict):
+                return structured
+
+        return mapping
+
+    if isinstance(result, str):
+        parsed = json.loads(result)
+
+        if isinstance(parsed, dict):
+            return parsed
+
+    if isinstance(result, list):
+        for block in result:
+            if not isinstance(block, dict):
+                continue
+
+            text = block.get("text")
+
+            if not isinstance(text, str):
+                continue
+
+            try:
+                parsed = json.loads(text)
+            except json.JSONDecodeError:
+                continue
+
+            if isinstance(parsed, dict):
+                return parsed
+
+    raise RuntimeError(
+        "无法从高德MCP结果中提取结构化数据。"
+    )
+
+def find_numeric_values(
+    value: Any,
+    target_key: str,
+) -> list[float]:
+    """递归提取指定字段中的数字。"""
+
+    results: list[float] = []
+
+    if isinstance(value, dict):
+        for key, item in value.items():
+            if key == target_key:
+                if (
+                    isinstance(
+                        item,
+                        (int, float, str),
+                    )
+                    and not isinstance(item, bool)
+                ):
+                    try:
+                        results.append(
+                            float(item)
+                        )
+                    except ValueError:
+                        pass
+
+            results.extend(
+                find_numeric_values(
+                    item,
+                    target_key,
+                )
+            )
+
+    elif isinstance(value, list):
+        for item in value:
+            results.extend(
+                find_numeric_values(
+                    item,
+                    target_key,
+                )
+            )
+
+    return results
+
+
+def get_hotel_location(
+    hotel: dict[str, Any],
+) -> str | None:
+    """从标准化酒店结果提取高德坐标字符串。"""
+
+    coordinates = hotel.get("coordinates")
+
+    if not isinstance(coordinates, dict):
+        return None
+
+    longitude = coordinates.get("longitude")
+    latitude = coordinates.get("latitude")
+
+    if longitude is None or latitude is None:
+        return None
+
+    return f"{longitude},{latitude}"
+
+
+def route_after_selection(
+    state: TravelState,
+) -> Literal[
+    "research_destination",
+    "end",
+]:
+    """筛选完成后路由:有结果进入地图研究,无结果结束。"""
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get(
+        "candidate_selection_result"
+    ):
+        return "end"
+
+    return "research_destination"
+
+
+def route_after_map_research(
+    state: TravelState,
+) -> Literal[
+    "evaluate_routes",
+    "end",
+]:
+    """地图研究完成后路由:有结果进入路线评估,无结果结束。"""
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get(
+        "map_research_result"
+    ):
+        return "end"
+
+    if not state.get(
+        "candidate_selection_result"
+    ):
+        return "end"
+
+    return "evaluate_routes"
+
+def make_route_evaluation_node(
+    distance_tool: BaseTool,
+    around_search_tool: BaseTool | None,
+    *,
+    service: RouteEvaluationService
+    | None = None,
+    max_hotels: int = 3,
+    max_attractions: int = 4,
+):
+    """创建酒店位置与路线评估节点。
+
+    为控制调用量,仅评估前3家酒店和前4个景点。
+    """
+
+    route_service = (
+        service
+        or RouteEvaluationService()
+    )
+
+    async def route_evaluation_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """路线评估节点:调用RouteEvaluationService评估酒店位置并计算景点间路线距离。"""
+
+        request = state["travel_request"]
+
+        selection = state[
+            "candidate_selection_result"
+        ]
+
+        map_result = state[
+            "map_research_result"
+        ]
+
+        hotel_candidates = (
+            selection.hotels[:max_hotels]
+        )
+
+        attractions = [
+            attraction
+            for attraction
+            in map_result.attractions
+            if attraction.location
+        ][:max_attractions]
+
+        warnings: list[str] = []
+
+        if not hotel_candidates:
+            message = "没有可用于路线评估的酒店。"
+
+            return {
+                "errors": [message],
+                "route_warnings": [message],
+                "final_answer": message,
+            }
+
+        if not attractions:
+            message = (
+                "没有带有效坐标的景点候选。"
+            )
+
+            return {
+                "errors": [message],
+                "route_warnings": [message],
+                "final_answer": message,
+            }
+
+        legs_by_hotel: dict[
+            str,
+            list[RouteLeg],
+        ] = {}
+
+        subway_distance_by_hotel: dict[
+            str,
+            float | None,
+        ] = {}
+
+        distance_call_count = 0
+        around_search_call_count = 0
+
+        for hotel_candidate in hotel_candidates:
+            hotel = hotel_candidate.hotel
+
+            hotel_id = hotel_candidate.hotel_id
+
+            hotel_name = str(
+                hotel.get("name")
+                or hotel_id
+            )
+
+            hotel_location = (
+                get_hotel_location(hotel)
+            )
+
+            hotel_legs: list[RouteLeg] = []
+
+            if hotel_location is None:
+                warnings.append(
+                    f"{hotel_name}缺少经纬度,"
+                    "无法执行真实距离查询。"
+                )
+
+                legs_by_hotel[hotel_id] = []
+                subway_distance_by_hotel[
+                    hotel_id
+                ] = None
+
+                continue
+
+            for attraction in attractions:
+                attraction_location = (
+                    attraction.location
+                )
+
+                if attraction_location is None:
+                    continue
+
+                try:
+                    distance_call_count += 1
+
+                    tool_result = (
+                        await distance_tool.ainvoke(
+                            {
+                                "origins": (
+                                    hotel_location
+                                ),
+                                "destination": (
+                                    attraction_location
+                                ),
+                                # 1代表驾车距离。
+                                "type": "1",
+                            }
+                        )
+                    )
+
+                    payload = extract_mcp_payload(
+                        tool_result
+                    )
+
+                    distances = (
+                        find_numeric_values(
+                            payload,
+                            "distance",
+                        )
+                    )
+
+                    durations = (
+                        find_numeric_values(
+                            payload,
+                            "duration",
+                        )
+                    )
+
+                    if not distances:
+                        raise RuntimeError(
+                            "距离结果中没有distance字段"
+                        )
+
+                    hotel_legs.append(
+                        RouteLeg(
+                            origin_name=hotel_name,
+                            origin_location=(
+                                hotel_location
+                            ),
+                            destination_name=(
+                                attraction.name
+                            ),
+                            destination_location=(
+                                attraction_location
+                            ),
+                            distance_meters=(
+                                distances[0]
+                            ),
+                            duration_seconds=(
+                                durations[0]
+                                if durations
+                                else None
+                            ),
+                            source="amap",
+                        )
+                    )
+
+                except Exception as exc:
+                    warnings.append(
+                        f"{hotel_name} → "
+                        f"{attraction.name}:"
+                        "高德距离查询失败,"
+                        "已使用直线距离降级。"
+                    )
+
+                    try:
+                        fallback_leg = (
+                            route_service
+                            .build_haversine_leg(
+                                origin_name=(
+                                    hotel_name
+                                ),
+                                origin_location=(
+                                    hotel_location
+                                ),
+                                destination_name=(
+                                    attraction.name
+                                ),
+                                destination_location=(
+                                    attraction_location
+                                ),
+                            )
+                        )
+
+                        hotel_legs.append(
+                            fallback_leg
+                        )
+
+                    except Exception as fallback_exc:
+                        warnings.append(
+                            "直线距离降级也失败:"
+                            f"{type(fallback_exc).__name__}: "
+                            f"{fallback_exc}"
+                        )
+
+            legs_by_hotel[
+                hotel_id
+            ] = hotel_legs
+
+            subway_distance: float | None = None
+
+            if (
+                request
+                .hotel_preferences
+                .near_subway
+                and around_search_tool
+                is not None
+            ):
+                try:
+                    around_search_call_count += 1
+
+                    tool_result = (
+                        await around_search_tool.ainvoke(
+                            {
+                                "location": (
+                                    hotel_location
+                                ),
+                                "radius": "1500",
+                                "keywords": "地铁站",
+                            }
+                        )
+                    )
+
+                    payload = extract_mcp_payload(
+                        tool_result
+                    )
+
+                    subway_distances = (
+                        find_numeric_values(
+                            payload,
+                            "distance",
+                        )
+                    )
+
+                    if subway_distances:
+                        subway_distance = min(
+                            subway_distances
+                        )
+
+                    else:
+                        warnings.append(
+                            f"{hotel_name}周边搜索"
+                            "没有返回可解析的地铁距离。"
+                        )
+
+                except Exception as exc:
+                    warnings.append(
+                        f"{hotel_name}周边地铁查询失败:"
+                        f"{type(exc).__name__}: {exc}"
+                    )
+
+            subway_distance_by_hotel[
+                hotel_id
+            ] = subway_distance
+
+        evaluated_hotels = (
+            route_service.rank_hotels(
+                hotel_candidates,
+                legs_by_hotel,
+                subway_distance_by_hotel,
+                require_near_subway=bool(
+                    request
+                    .hotel_preferences
+                    .near_subway
+                ),
+                limit=max_hotels,
+            )
+        )
+
+        selected_hotel_id = (
+            evaluated_hotels[0].hotel_id
+            if evaluated_hotels
+            else None
+        )
+
+        result = RouteEvaluationResult(
+            evaluated_hotels=evaluated_hotels,
+            reference_attractions=[
+                attraction.name
+                for attraction in attractions
+            ],
+            selected_hotel_id=selected_hotel_id,
+            distance_call_count=(
+                distance_call_count
+            ),
+            around_search_call_count=(
+                around_search_call_count
+            ),
+            warnings=warnings,
+        )
+
+        if not evaluated_hotels:
+            message = (
+                "路线评估完成,但没有得到"
+                "有效酒店排序结果。"
+            )
+
+            return {
+                "route_evaluation_result": result,
+                "route_warnings": warnings,
+                "errors": [message],
+                "final_answer": message,
+            }
+
+        selected_hotel = evaluated_hotels[0]
+
+        return {
+            "route_evaluation_result": result,
+            "route_warnings": warnings,
+            "final_answer": (
+                "酒店路线评估完成:"
+                f"评估{len(evaluated_hotels)}家酒店,"
+                f"参考{len(attractions)}个景点;"
+                "当前综合排名第一为"
+                f"“{selected_hotel.hotel_name}”,"
+                f"综合得分"
+                f"{selected_hotel.final_score:.2f}。"
+            ),
+        }
+
+    return route_evaluation_node

+ 129 - 0
app/graph/selection_builder.py

@@ -0,0 +1,129 @@
+from __future__ import annotations
+"""候选筛选阶段子图:对航班/酒店候选做确定性筛选与排名,为路线评估提供输入。"""
+
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    route_after_parse,
+)
+from app.graph.resource_nodes import (
+    make_resource_search_node,
+)
+from app.graph.selection_nodes import (
+    make_candidate_selection_node,
+    route_after_resources_to_selection,
+)
+from app.graph.state import TravelState
+from app.mcp_client import load_mcp_tools
+
+
+async def build_selection_graph():
+    """构建资源查询与候选筛选工作流。"""
+
+    tool_bundle = await load_mcp_tools()
+
+    return_flight_tool = next(
+        (
+            tool
+            for tool in tool_bundle.travel_tools
+            if tool.name
+            == "search_return_flights"
+        ),
+        None,
+    )
+
+    if return_flight_tool is None:
+        raise RuntimeError(
+            "没有加载到"
+            "search_return_flights工具。"
+        )
+
+    requirement_agent = RequirementAgent()
+
+    resource_agent = ResourceSearchAgent(
+        tools=tool_bundle.travel_tools,
+    )
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_node(
+        "search_resources",
+        make_resource_search_node(
+            resource_agent
+        ),
+    )
+
+    builder.add_node(
+        "select_candidates",
+        make_candidate_selection_node(
+            return_flight_tool,
+            max_outbound_queries=2,
+            max_returns_per_outbound=5,
+        ),
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "error": "error",
+            "ready": "search_resources",
+        },
+    )
+
+    builder.add_edge("clarify", END)
+    builder.add_edge("error", END)
+
+    builder.add_conditional_edges(
+        "search_resources",
+        route_after_resources_to_selection,
+        {
+            "select_candidates": (
+                "select_candidates"
+            ),
+            "end": END,
+        },
+    )
+
+    builder.add_edge(
+        "select_candidates",
+        END,
+    )
+
+    return builder.compile()

+ 426 - 0
app/graph/selection_nodes.py

@@ -0,0 +1,426 @@
+from __future__ import annotations
+"""候选筛选节点:调用CandidateSelectionService对航班/酒店/往返组合做确定性排序。"""
+
+import json
+from typing import Any, Literal
+
+from langchain_core.messages import ToolMessage
+from langchain_core.tools import BaseTool
+
+from app.graph.state import TravelState
+from app.schemas.selection import (
+    CandidateSelectionResult,
+)
+from app.services.candidate_selection_service import (
+    CandidateSelectionService,
+)
+
+
+def extract_direct_tool_payload(
+    result: Any,
+) -> dict[str, Any]:
+    """提取直接调用LangChain Tool后的结构化结果。"""
+
+    if isinstance(result, ToolMessage):
+        artifact = getattr(
+            result,
+            "artifact",
+            None,
+        )
+
+        if isinstance(artifact, dict):
+            structured = artifact.get(
+                "structured_content"
+            )
+
+            if isinstance(structured, dict):
+                return structured
+
+            if "flights" in artifact:
+                return artifact
+
+        result = result.content
+
+    if isinstance(result, dict):
+        structured = result.get(
+            "structured_content"
+        )
+
+        if isinstance(structured, dict):
+            return structured
+
+        if (
+            "flights" in result
+            or "hotels" in result
+            or "suggestions" in result
+        ):
+            return result
+
+    if isinstance(result, str):
+        try:
+            parsed = json.loads(result)
+        except json.JSONDecodeError as exc:
+            raise RuntimeError(
+                "MCP工具返回了无法解析的文本。"
+            ) from exc
+
+        if isinstance(parsed, dict):
+            return parsed
+
+    if isinstance(result, list):
+        for block in result:
+            if not isinstance(block, dict):
+                continue
+
+            text = block.get("text")
+
+            if not isinstance(text, str):
+                continue
+
+            try:
+                parsed = json.loads(text)
+            except json.JSONDecodeError:
+                continue
+
+            if isinstance(parsed, dict):
+                return parsed
+
+    raise RuntimeError(
+        "无法从MCP工具结果中提取结构化数据。"
+    )
+
+
+def get_return_stops_filter(
+    max_stops: int | None,
+) -> str | None:
+    """将内部中转次数转换为MCP查询参数。"""
+
+    if max_stops is None:
+        return None
+
+    if max_stops == 0:
+        return "nonstop"
+
+    if max_stops == 1:
+        return "one_or_fewer"
+
+    if max_stops == 2:
+        return "two_or_fewer"
+
+    # 接口没有“三次以内”的精确枚举,
+    # 留给本地确定性代码过滤。
+    return None
+
+
+def route_after_resources_to_selection(
+    state: TravelState,
+) -> Literal[
+    "select_candidates",
+    "end",
+]:
+    """资源查询后路由:有结果进入候选筛选,无结果结束工作流。"""
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get("flight_search_result"):
+        return "end"
+
+    if not state.get("hotel_search_result"):
+        return "end"
+
+    return "select_candidates"
+
+
+def make_candidate_selection_node(
+    return_flight_tool: BaseTool,
+    *,
+    selector: CandidateSelectionService
+    | None = None,
+    max_outbound_queries: int = 2,
+    max_returns_per_outbound: int = 5,
+):
+    """创建航班和酒店候选筛选节点。
+
+    为控制接口调用成本,默认只对排名最高的
+    两个去程候选查询返程。
+    """
+
+    selection_service = (
+        selector
+        or CandidateSelectionService()
+    )
+
+    async def candidate_selection_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """候选筛选节点:对航班和酒店候选排序,生成排名理由与警告。"""
+
+        request = state["travel_request"]
+
+        flight_result = state.get(
+            "flight_search_result",
+            {},
+        )
+
+        hotel_result = state.get(
+            "hotel_search_result",
+            {},
+        )
+
+        raw_flights = flight_result.get(
+            "flights",
+            [],
+        )
+
+        raw_hotels = hotel_result.get(
+            "hotels",
+            [],
+        )
+
+        if not isinstance(raw_flights, list):
+            raw_flights = []
+
+        if not isinstance(raw_hotels, list):
+            raw_hotels = []
+
+        (
+            ranked_hotels,
+            hotel_warnings,
+        ) = selection_service.rank_hotels(
+            raw_hotels,
+            request,
+            limit=10,
+        )
+
+        warnings = list(hotel_warnings)
+
+        # 单程场景不需要调用返程工具。
+        if request.return_date is None:
+            (
+                ranked_flights,
+                flight_warnings,
+            ) = selection_service.rank_flights(
+                raw_flights,
+                request,
+                limit=10,
+            )
+
+            warnings.extend(flight_warnings)
+
+            result = CandidateSelectionResult(
+                flight_type="one_way",
+                outbound_candidates=(
+                    ranked_flights
+                ),
+                one_way_options=ranked_flights,
+                hotels=ranked_hotels,
+                warnings=warnings,
+            )
+
+            return {
+                "candidate_selection_result": result,
+                "selection_warnings": warnings,
+                "final_answer": (
+                    "候选筛选完成:"
+                    f"保留{len(ranked_flights)}条"
+                    "单程航班和"
+                    f"{len(ranked_hotels)}家酒店。"
+                ),
+            }
+
+        (
+            outbound_candidates,
+            outbound_warnings,
+        ) = selection_service.rank_flights(
+            raw_flights,
+            request,
+            limit=max_outbound_queries,
+        )
+
+        warnings.extend(outbound_warnings)
+
+        origin_airports = state.get(
+            "origin_airports",
+            [],
+        )
+
+        destination_airports = state.get(
+            "destination_airports",
+            [],
+        )
+
+        combinations: list[
+            dict[str, dict[str, Any]]
+        ] = []
+
+        stops_filter = get_return_stops_filter(
+            request.flight_preferences.max_stops
+        )
+
+        for outbound_candidate in (
+            outbound_candidates
+        ):
+            outbound = (
+                outbound_candidate.flight
+            )
+
+            departure_token = outbound.get(
+                "departure_token"
+            )
+
+            if not departure_token:
+                warnings.append(
+                    "去程候选"
+                    f"{outbound_candidate.option_id}"
+                    "缺少departure_token,"
+                    "已跳过返程查询。"
+                )
+
+                continue
+
+            tool_args: dict[str, Any] = {
+                "departure_token": (
+                    departure_token
+                ),
+                "departure_airports": (
+                    origin_airports
+                ),
+                "arrival_airports": (
+                    destination_airports
+                ),
+                "outbound_date": (
+                    request.departure_date
+                    .isoformat()
+                ),
+                "return_date": (
+                    request.return_date
+                    .isoformat()
+                ),
+                "adults": request.adults,
+                "children": request.children,
+                "travel_class": "economy",
+                "currency": request.currency,
+                "show_hidden": False,
+                "deep_search": False,
+                "no_cache": False,
+            }
+
+            if stops_filter is not None:
+                tool_args["stops"] = (
+                    stops_filter
+                )
+
+            try:
+                tool_result = (
+                    await return_flight_tool.ainvoke(
+                        tool_args
+                    )
+                )
+
+                payload = (
+                    extract_direct_tool_payload(
+                        tool_result
+                    )
+                )
+
+                return_flights = payload.get(
+                    "flights",
+                    [],
+                )
+
+                if not isinstance(
+                    return_flights,
+                    list,
+                ):
+                    return_flights = []
+
+                (
+                    ranked_returns,
+                    return_warnings,
+                ) = (
+                    selection_service.rank_flights(
+                        return_flights,
+                        request,
+                        limit=(
+                            max_returns_per_outbound
+                        ),
+                        reverse_direction=True,
+                        apply_time_preferences=False,
+                    )
+                )
+
+                warnings.extend(return_warnings)
+
+                for return_candidate in (
+                    ranked_returns
+                ):
+                    combinations.append(
+                        {
+                            "outbound": outbound,
+                            "return_flight": (
+                                return_candidate.flight
+                            ),
+                        }
+                    )
+
+            except Exception as exc:
+                warnings.append(
+                    "返程查询失败:"
+                    f"{type(exc).__name__}: {exc}"
+                )
+
+        round_trip_options = (
+            selection_service
+            .rank_round_trip_combinations(
+                combinations,
+                request,
+                limit=10,
+            )
+        )
+
+        result = CandidateSelectionResult(
+            flight_type="round_trip",
+            outbound_candidates=(
+                outbound_candidates
+            ),
+            round_trip_options=(
+                round_trip_options
+            ),
+            hotels=ranked_hotels,
+            warnings=warnings,
+        )
+
+        if not round_trip_options:
+            message = (
+                "没有获得有效的往返航班组合。"
+            )
+
+            existing_errors = list(
+                state.get("errors", [])
+            )
+
+            existing_errors.append(message)
+
+            return {
+                "candidate_selection_result": result,
+                "selection_warnings": warnings,
+                "errors": existing_errors,
+                "final_answer": message,
+            }
+
+        return {
+            "candidate_selection_result": result,
+            "selection_warnings": warnings,
+            "final_answer": (
+                "候选筛选完成:"
+                f"查询了"
+                f"{len(outbound_candidates)}个"
+                "去程候选,获得"
+                f"{len(round_trip_options)}个"
+                "往返组合,并保留"
+                f"{len(ranked_hotels)}家酒店。"
+            ),
+        }
+
+    return candidate_selection_node

+ 108 - 0
app/graph/state.py

@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+"""多角色工作流共享状态:定义TravelState(TypedDict)及Supervisor决策、Workflow状态等类型枚举。"""
+
+from app.schemas.place import MapResearchResult
+from typing import Any, Literal
+from typing_extensions import TypedDict
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+from app.schemas.selection import (
+    CandidateSelectionResult,
+)
+from app.schemas.route import (
+    RouteEvaluationResult,
+)
+from app.schemas.itinerary import ItineraryPlan
+from app.schemas.validation import (
+    PlanValidationResult,
+)
+from app.schemas.review import (
+    TripReviewResult,
+)
+
+
+SupervisorDecision = Literal[
+    "replan",
+    "finalize",
+    "finalize_with_risks",
+    "terminate",
+]
+
+WorkflowStatus = Literal[
+    "running",
+    "approved",
+    "completed_with_risks",
+    "failed",
+]
+
+class TravelState(TypedDict, total=False):
+    """旅行规划工作流共享状态。
+
+    后面会逐步加入航班、酒店、景点和路线结果。
+    """
+
+    user_query: str
+
+    travel_request: TravelRequest
+
+    missing_fields: list[str]
+    errors: list[str]
+
+    needs_clarification: bool
+    clarification_question: str
+
+    # 资源查询阶段新增
+    origin_airports: list[str]
+    destination_airports: list[str]
+
+    flight_search_result: dict[str, Any]
+    hotel_search_result: dict[str, Any]
+
+    resource_messages: list[Any]
+
+    final_answer: str
+
+    # 本阶段新增
+    map_research_result: MapResearchResult
+    map_messages: list[Any]
+
+    candidate_selection_result: (
+        CandidateSelectionResult
+    )
+
+    selection_warnings: list[str]
+
+    route_evaluation_result: RouteEvaluationResult
+    route_warnings: list[str]
+
+    itinerary_plan: ItineraryPlan
+
+    planner_messages: list[Any]
+
+    planning_attempts: int
+
+    # 后续Reviewer要求重新规划时使用。
+    revision_feedback: list[str]
+
+    plan_validation_result: PlanValidationResult
+
+    validation_passed: bool
+
+    validation_warnings: list[str]
+    trip_review_result: TripReviewResult
+
+    reviewer_messages: list[Any]
+
+    review_passed: bool
+
+    review_attempts: int
+
+    supervisor_decision: SupervisorDecision
+    supervisor_reason: str
+
+    workflow_status: WorkflowStatus
+    final_answer: str
+
+    

+ 230 - 0
app/graph/supervisor_nodes.py

@@ -0,0 +1,230 @@
+from __future__ import annotations
+"""监督协调节点:汇总Validator和Reviewer的意见,根据校验结果与重试次数决定replan/finalize/terminate去向。"""
+
+from typing import Any, Literal
+
+from app.graph.state import TravelState
+
+
+SupervisorRoute = Literal[
+    "replan",
+    "finalize",
+    "finalize_with_risks",
+    "terminate",
+]
+
+
+def _unique_messages(
+    values: list[str],
+) -> list[str]:
+    """去除空字符串和重复修改意见。"""
+
+    result: list[str] = []
+
+    for value in values:
+        normalized = value.strip()
+
+        if normalized and normalized not in result:
+            result.append(normalized)
+
+    return result
+
+
+def collect_revision_feedback(
+    state: TravelState,
+) -> list[str]:
+    """收集Validator与Reviewer提出的修改意见。"""
+
+    feedback = list(
+        state.get("revision_feedback", [])
+    )
+
+    validation = state.get(
+        "plan_validation_result"
+    )
+
+    if validation is not None:
+        feedback.extend(
+            validation.revision_feedback
+        )
+
+        for issue in validation.issues:
+            if (
+                issue.severity == "error"
+                and issue.message
+            ):
+                feedback.append(issue.message)
+
+    review = state.get("trip_review_result")
+
+    if review is not None:
+        feedback.extend(
+            review.revision_feedback
+        )
+
+        for issue in review.issues:
+            if (
+                issue.severity == "blocking"
+                and issue.message
+            ):
+                feedback.append(
+                    issue.suggestion
+                    or issue.message
+                )
+
+    return _unique_messages(feedback)
+
+
+def make_supervisor_node(
+    *,
+    max_planning_attempts: int = 3,
+):
+    """创建多角色工作流协调节点。
+
+    max_planning_attempts=3表示:
+    1次初始规划 + 最多2次修改。
+    """
+
+    if max_planning_attempts < 1:
+        raise ValueError(
+            "max_planning_attempts必须大于等于1。"
+        )
+
+    def supervisor_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """监督节点:基于校验与评审结果及当前尝试次数,决策工作流的下一步——继续重规划、定稿(含风险)或终止。"""
+
+        system_errors = state.get(
+            "errors",
+            [],
+        )
+
+        if system_errors:
+            return {
+                "supervisor_decision": "terminate",
+                "supervisor_reason": (
+                    "工作流存在系统级错误,"
+                    "不能继续重新规划。"
+                ),
+                "workflow_status": "failed",
+            }
+
+        validation = state.get(
+            "plan_validation_result"
+        )
+
+        review = state.get(
+            "trip_review_result"
+        )
+
+        if validation is None or review is None:
+            return {
+                "supervisor_decision": "terminate",
+                "supervisor_reason": (
+                    "Supervisor缺少Validator或"
+                    "Reviewer的结构化结果。"
+                ),
+                "workflow_status": "failed",
+                "errors": [
+                    "Supervisor输入状态不完整。"
+                ],
+            }
+
+        validation_passed = validation.is_valid
+
+        review_passed = bool(
+            state.get("review_passed", False)
+        )
+
+        planning_attempts = state.get(
+            "planning_attempts",
+            0,
+        )
+
+        if validation_passed and review_passed:
+            return {
+                "supervisor_decision": "finalize",
+                "supervisor_reason": (
+                    "确定性校验和独立审查均已通过。"
+                ),
+                "workflow_status": "approved",
+                "revision_feedback": [],
+                "final_answer": (
+                    "Supervisor批准当前方案,"
+                    "进入最终输出阶段。"
+                ),
+            }
+
+        feedback = collect_revision_feedback(
+            state
+        )
+
+        if not feedback:
+            feedback = [
+                "重新检查方案与用户需求、"
+                "确定性校验结果和审查意见,"
+                "修复所有阻塞问题。"
+            ]
+
+        if planning_attempts < max_planning_attempts:
+            next_attempt = planning_attempts + 1
+
+            return {
+                "supervisor_decision": "replan",
+                "supervisor_reason": (
+                    "当前方案未通过,且仍有"
+                    "剩余重新规划次数。"
+                ),
+                "workflow_status": "running",
+                "revision_feedback": feedback,
+                "validation_passed": False,
+                "review_passed": False,
+                "final_answer": (
+                    "Supervisor要求重新规划:"
+                    f"即将执行第{next_attempt}次规划。"
+                ),
+            }
+
+        return {
+            "supervisor_decision": (
+                "finalize_with_risks"
+            ),
+            "supervisor_reason": (
+                "方案仍存在问题,但已经达到"
+                f"最大规划次数"
+                f"{max_planning_attempts}次。"
+            ),
+            "workflow_status": (
+                "completed_with_risks"
+            ),
+            "revision_feedback": feedback,
+            "final_answer": (
+                "已达到最大重新规划次数,"
+                "系统将保留风险说明并输出"
+                "当前最佳方案。"
+            ),
+        }
+
+    return supervisor_node
+
+
+def route_after_supervisor(
+    state: TravelState,
+) -> SupervisorRoute:
+    """根据Supervisor决策选择后续路径。"""
+
+    decision = state.get(
+        "supervisor_decision",
+        "terminate",
+    )
+
+    if decision in {
+        "replan",
+        "finalize",
+        "finalize_with_risks",
+        "terminate",
+    }:
+        return decision
+
+    return "terminate"

+ 96 - 0
app/graph/travel_builder.py

@@ -0,0 +1,96 @@
+from __future__ import annotations
+"""资源查询阶段子图:在需求解析后调用航班/酒店MCP工具查询外部资源。"""
+
+from langgraph.graph import (
+    END,
+    START,
+    StateGraph,
+)
+
+from app.agents.requirement_agent import (
+    RequirementAgent,
+)
+from app.agents.resource_agent import (
+    ResourceSearchAgent,
+)
+from app.graph.nodes import (
+    clarification_node,
+    error_node,
+    make_parse_request_node,
+    route_after_parse,
+)
+from app.graph.resource_nodes import (
+    make_resource_search_node,
+)
+from app.graph.state import TravelState
+from app.mcp_client import load_mcp_tools
+
+
+async def build_resource_graph():
+    """构建需求解析与资源查询工作流。"""
+
+    tool_bundle = await load_mcp_tools()
+
+    requirement_agent = RequirementAgent()
+
+    resource_agent = ResourceSearchAgent(
+        tools=tool_bundle.travel_tools,
+    )
+
+    builder = StateGraph(TravelState)
+
+    builder.add_node(
+        "parse_request",
+        make_parse_request_node(
+            requirement_agent
+        ),
+    )
+
+    builder.add_node(
+        "clarify",
+        clarification_node,
+    )
+
+    builder.add_node(
+        "error",
+        error_node,
+    )
+
+    builder.add_node(
+        "search_resources",
+        make_resource_search_node(
+            resource_agent
+        ),
+    )
+
+    builder.add_edge(
+        START,
+        "parse_request",
+    )
+
+    builder.add_conditional_edges(
+        "parse_request",
+        route_after_parse,
+        {
+            "clarify": "clarify",
+            "error": "error",
+            "ready": "search_resources",
+        },
+    )
+
+    builder.add_edge(
+        "clarify",
+        END,
+    )
+
+    builder.add_edge(
+        "error",
+        END,
+    )
+
+    builder.add_edge(
+        "search_resources",
+        END,
+    )
+
+    return builder.compile()

+ 115 - 0
app/graph/validation_nodes.py

@@ -0,0 +1,115 @@
+from __future__ import annotations
+"""方案校验节点:调用PlanValidationService对行程做确定性规则校验,验证航班/酒店ID真实性与预算合理性。"""
+
+from typing import Any, Literal
+
+from app.graph.state import TravelState
+from app.services.plan_validation_service import (
+    PlanValidationService,
+)
+
+
+def route_after_planning(
+    state: TravelState,
+) -> Literal[
+    "validate_plan",
+    "end",
+]:
+    """Planner成功后进入确定性校验。"""
+
+    if state.get("errors"):
+        return "end"
+
+    if not state.get("itinerary_plan"):
+        return "end"
+
+    return "validate_plan"
+
+
+def make_plan_validation_node(
+    service: PlanValidationService
+    | None = None,
+):
+    """创建确定性行程校验节点。"""
+
+    validation_service = (
+        service
+        or PlanValidationService()
+    )
+
+    async def plan_validation_node(
+        state: TravelState,
+    ) -> dict[str, Any]:
+        """方案校验节点:用确定性规则校验行程方案,将校验结果和预算估算写入state。"""
+
+        try:
+            result = validation_service.validate(
+                request=state["travel_request"],
+                plan=state["itinerary_plan"],
+                selection=state[
+                    "candidate_selection_result"
+                ],
+                map_result=state[
+                    "map_research_result"
+                ],
+                route_result=state[
+                    "route_evaluation_result"
+                ],
+            )
+
+            warnings = [
+                issue.message
+                for issue in result.issues
+                if issue.severity == "warning"
+            ]
+
+            if result.is_valid:
+                message = (
+                    "确定性校验通过:"
+                    f"发现{result.warning_count}项"
+                    "非阻塞警告;"
+                    "等待Trip Reviewer审查。"
+                )
+            else:
+                message = (
+                    "确定性校验未通过:"
+                    f"发现{result.error_count}项错误、"
+                    f"{result.warning_count}项警告;"
+                    "需要Planner修改方案。"
+                )
+
+            # 注意:
+            # 业务校验失败不能写入全局errors,
+            # 否则LangGraph会立即终止,
+            # 后续无法进入重新规划循环。
+            return {
+                "plan_validation_result": result,
+                "validation_passed": (
+                    result.is_valid
+                ),
+                "validation_warnings": warnings,
+                "revision_feedback": (
+                    result.revision_feedback
+                ),
+                "final_answer": message,
+            }
+
+        except Exception as exc:
+            message = (
+                "确定性校验节点执行失败:"
+                f"{type(exc).__name__}: {exc}"
+            )
+
+            existing_errors = list(
+                state.get("errors", [])
+            )
+
+            existing_errors.append(message)
+
+            return {
+                "errors": existing_errors,
+                "validation_passed": False,
+                "final_answer": message,
+            }
+
+    return plan_validation_node

+ 40 - 0
app/llm.py

@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+"""大模型工厂:统一创建项目共用的ChatOpenAI实例,兼容OpenAI及其兼容服务(DeepSeek等),支持独立超时配置。"""
+
+from typing import Any
+
+from langchain_openai import ChatOpenAI
+
+from app.config import get_settings
+
+
+def get_chat_model(
+    timeout: float | None = None,
+) -> ChatOpenAI:
+    """创建项目统一使用的大模型。
+
+    支持OpenAI以及兼容OpenAI协议的模型服务。
+    timeout: 可选的自定义超时时间(秒)。
+    """
+
+    settings = get_settings()
+
+    model_kwargs: dict[str, Any] = {
+        "model": settings.require("model"),
+        "api_key": settings.require("openai_api_key"),
+        "temperature": 0,
+        "max_retries": 2,
+        "timeout": (
+            timeout
+            if timeout is not None
+            else settings.request_timeout_seconds
+        ),
+    }
+
+    base_url = settings.base_url.strip()
+
+    if base_url:
+        model_kwargs["base_url"] = base_url
+
+    return ChatOpenAI(**model_kwargs)

+ 95 - 0
app/logging_config.py

@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+"""统一日志配置与trace_id上下文管理:通过contextvars实现一次工作流调用的trace_id贯穿所有日志。"""
+
+import logging
+import sys
+import uuid
+from contextvars import ContextVar
+
+
+_trace_id_context: ContextVar[str] = ContextVar(
+    "trace_id",
+    default="-",
+)
+
+
+class TraceIdFilter(logging.Filter):
+    """向每条日志中注入当前请求trace_id。"""
+
+    def filter(
+        self,
+        record: logging.LogRecord,
+    ) -> bool:
+        """向日志记录注入当前上下文的trace_id,供日志格式串引用。"""
+
+        record.trace_id = get_trace_id()
+        return True
+
+
+def configure_logging(
+    level: int = logging.INFO,
+) -> None:
+    """配置TravelMind统一日志格式。"""
+
+    handler = logging.StreamHandler(
+        sys.stdout
+    )
+
+    handler.addFilter(TraceIdFilter())
+
+    formatter = logging.Formatter(
+        fmt=(
+            "%(asctime)s "
+            "| %(levelname)s "
+            "| trace_id=%(trace_id)s "
+            "| %(name)s "
+            "| %(message)s"
+        ),
+        datefmt="%Y-%m-%d %H:%M:%S",
+    )
+
+    handler.setFormatter(formatter)
+
+    root_logger = logging.getLogger()
+
+    root_logger.handlers.clear()
+    root_logger.addHandler(handler)
+    root_logger.setLevel(level)
+
+    # 降低第三方HTTP库的日志噪声。
+    logging.getLogger(
+        "httpcore"
+    ).setLevel(logging.WARNING)
+
+    logging.getLogger(
+        "httpx"
+    ).setLevel(logging.WARNING)
+
+
+def new_trace_id() -> str:
+    """生成并设置一次工作流调用的trace_id。"""
+
+    trace_id = uuid.uuid4().hex[:16]
+
+    _trace_id_context.set(trace_id)
+
+    return trace_id
+
+
+def set_trace_id(
+    trace_id: str,
+) -> None:
+    """设置外部传入的trace_id。"""
+
+    normalized = trace_id.strip()
+
+    _trace_id_context.set(
+        normalized or "-"
+    )
+
+
+def get_trace_id() -> str:
+    """读取当前上下文trace_id。"""
+
+    return _trace_id_context.get()

+ 102 - 0
app/mcp_client.py

@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+"""MCP工具加载器:连接自建旅行MCP与高德MCP两个服务器,校验必需工具存在后打包为MCPToolBundle供Agent使用。"""
+
+import sys
+from dataclasses import dataclass
+
+from langchain_core.tools import BaseTool
+from langchain_mcp_adapters.client import (
+    MultiServerMCPClient,
+)
+
+from app.config import get_settings
+
+
+TRAVEL_TOOL_NAMES = {
+    "search_airports",
+    "search_flights",
+    "search_return_flights",
+    "search_hotels",
+}
+
+
+@dataclass
+class MCPToolBundle:
+    """项目使用的全部MCP工具。"""
+
+    client: MultiServerMCPClient
+    all_tools: list[BaseTool]
+    travel_tools: list[BaseTool]
+    amap_tools: list[BaseTool]
+
+
+async def load_mcp_tools() -> MCPToolBundle:
+    """连接自建旅行MCP和高德MCP。"""
+
+    settings = get_settings()
+
+    client = MultiServerMCPClient(
+        {
+            "travel_search": {
+                # 使用当前虚拟环境的Python启动本地Server。
+                "transport": "stdio",
+                "command": sys.executable,
+                "args": [
+                    "-m",
+                    "mcp_servers.travel_search_server",
+                ],
+            },
+            "amap": {
+                # LangChain配置中写http,
+                # 实际协议为Streamable HTTP。
+                "transport": "http",
+                "url": (
+                    "https://mcp.amap.com/mcp"
+                    f"?key={settings.require('amap_api_key')}"
+                ),
+            },
+        }
+    )
+
+    all_tools = await client.get_tools()
+
+    travel_tools = [
+        tool
+        for tool in all_tools
+        if tool.name in TRAVEL_TOOL_NAMES
+    ]
+
+    amap_tools = [
+        tool
+        for tool in all_tools
+        if tool.name.startswith("maps_")
+    ]
+
+    loaded_travel_names = {
+        tool.name
+        for tool in travel_tools
+    }
+
+    missing_tools = (
+        TRAVEL_TOOL_NAMES
+        - loaded_travel_names
+    )
+
+    if missing_tools:
+        raise RuntimeError(
+            "旅行MCP缺少工具:"
+            + "、".join(sorted(missing_tools))
+        )
+
+    if not amap_tools:
+        raise RuntimeError(
+            "没有从高德MCP加载到任何地图工具。"
+        )
+
+    return MCPToolBundle(
+        client=client,
+        all_tools=all_tools,
+        travel_tools=travel_tools,
+        amap_tools=amap_tools,
+    )

+ 0 - 0
app/schemas/__init__.py


+ 30 - 0
app/schemas/common.py

@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+"""公共Pydantic模型:提供AppModel基类(统一配置)与SourceMetadata第三方数据来源元数据。"""
+
+from datetime import datetime,timezone
+from pydantic import BaseModel,Field,ConfigDict
+
+def utc_now()->datetime:
+    """返回带时区的 UTC时间。"""
+    return datetime.now(timezone.utc)
+
+class AppModel(BaseModel):
+    """项目中所有 Pydantic 模型的基础类。"""
+
+    model_config = ConfigDict(
+        extra="ignore",
+        str_strip_whitespace = True,
+        validate_assignment = True,
+    )
+
+class SourceMetadata(AppModel):
+    """第三方数据来源信息。"""
+    provider: str 
+    status: str | None = None
+    search_id: str | None = None
+    retrieved_at: datetime = Field(default_factory=utc_now)
+    currency:str | None = None
+    is_cached:bool | None = None
+
+

+ 176 - 0
app/schemas/flight.py

@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+"""航班查询与结果模型:FlightSearchQuery查询参数与FlightOption搜索结果。"""
+
+from datetime import datetime,date
+from typing import Literal
+from pydantic import field_validator,Field,model_validator
+from app.schemas.common import SourceMetadata,AppModel
+
+FlightType = Literal["round_trip","one_way"]
+
+TravelClass = Literal["economy","business","first","premium_economy"]
+
+FlightSort = Literal["top","price","departure_time","arrival_time","duration","emissions"]
+
+StopsFilter = Literal["any","nonstop","one_or_fewer","two_or_fewer"]
+
+class FlightSearchQuery(AppModel):
+    """
+    航班查询参数
+    当前所有筛选条件默认不启用。
+    后续由需求解析agent根据用户真实需求赋值
+    """
+    departure_airports:list[str] = Field(min_length=1)
+    arrival_airports:list[str] = Field(min_length=1)
+
+    outbound_date:date
+    return_date:date | None = None
+
+    flight_type:FlightType="one_way"
+
+    adults:int = Field(default=1, ge=1, le=9)
+    children:int = Field(default=0, ge=0, le=8)
+
+    travel_class:TravelClass="economy"
+    currency:str = "CNY"
+
+    # 以下全部是可选条件,不设置就不限制
+    sort_by:FlightSort | None=None
+    stops:StopsFilter | None=None
+    max_price:int | None = Field(default=None,gt=0)
+
+    # SerpApi 时间范围格式,例如:
+    # "9,20" 或 "9,20,10,22"
+    outbound_times:str | None=None
+    language:str = "zh-cn"
+    country:str = "cn"
+
+    no_cache:bool = False
+    # 是否加载 Google Flights 的“查看更多航班”
+    show_hidden: bool = False
+    # 是否启用更完整但响应更慢的深度查询
+    deep_search: bool = False
+
+    @field_validator("departure_airports","arrival_airports")
+    @classmethod
+    def normalize_airports(cls,value:list[str])->list[str]:
+        """机场代码统一转为大写并去重"""
+        normalized:list[str] = []
+
+        for item in value:
+            code = item.strip().upper()
+            if not code:
+                continue
+            if code not in normalized:
+                normalized.append(code)
+
+        if not normalized:
+            raise ValueError("机场代码不能为空")
+        return normalized
+    
+    @field_validator("currency")
+    @classmethod
+    def normalize_currency(cls,value:str)->str:
+        return value.upper()
+    
+    @model_validator(mode="after")
+    def validate_dates(self)->"FlightSearchQuery":
+        if self.flight_type == "round_trip":
+            if self.return_date is None:
+                raise ValueError("往返航班必须提供return_date。")
+            if self.return_date <= self.outbound_date:
+                raise ValueError("返程日期必须晚于出发日期。")
+        return self
+    
+
+class FlightSegment(AppModel):
+    """一个具体航段,中转航班会包含FlightSegment。"""
+    flight_number:str | None=None
+    airline:str | None=None
+    departure_airport_code:str
+    departure_airport_name:str | None=None
+    departure_time:datetime
+    arrival_airport_code:str
+    arrival_airport_name:str | None=None
+    arrival_time:datetime
+
+    duration_minutes:int | None=Field(default=None,ge=0)
+    airplane:str | None=None
+    travel_class:str | None=None
+    legroom:str | None=None
+
+    overnight:bool = False
+    often_delayed_by_over_30_min:bool = False
+    planed_and_crew_by:str | None=None
+    extensions:list[str] = Field(default_factory=list)
+    ticket_also_sold_by:list[str] = Field(default_factory=list)
+
+class Layover(AppModel):
+    """经停或中转信息。"""
+    airport_code:str
+    airport_name:str | None=None
+
+    duration_minutes:int | None=Field(default=None,ge=0)
+    overnight:bool = False
+
+class CarbonEmissions(AppModel):
+    """航班碳排放数据。"""
+    this_flight_grams:int | None=None
+    typical_for_route_grams:int | None=None
+    difference_percent:int | None=None
+
+# SerpApi 返回的航班分组:best_flights / other_flights 等
+FlightSourceGroup = Literal["best","other","best_flights","other_flights"]
+
+class FlightOption(AppModel):
+    """一个完整的去程或者返程候选方案。"""
+    option_id:str
+    source_group:FlightSourceGroup
+    provider_rank:int = Field(ge=0)
+
+    segments:list[FlightSegment] = Field(min_length=1)
+    layovers:list[Layover] = Field(default_factory=list)
+
+    departure_airport_code:str
+    final_arrival_airport_code:str
+
+    departure_time:datetime
+    arrival_time:datetime
+
+    total_duration_minutes:int | None=Field(default=None,ge=0)
+
+    stop_count:int = Field(default=0,ge=0)
+
+    price:int | None=Field(default=None,gt=0)
+    currency:str = "CNY"
+
+    flight_type:str | None=None
+
+    airlines:list[str] = Field(default_factory=list) 
+
+    is_overnight:bool = False
+
+    carbon_emissions:CarbonEmissions | None=None
+
+    # 往返查询选择去程后,用于继续查询返程
+    departure_token:str | None=None
+
+    # 获取预定选项是可能使用
+    booking_token:str | None=None
+
+class FlightSearchResult(AppModel):
+    """标准化后的航班查询结果。"""
+    
+    query:FlightSearchQuery
+    flights:list[FlightOption] = Field(default_factory=list)
+    total_count:int = Field(default=0,ge=0)
+
+    source:SourceMetadata
+
+    price_insights:dict | None=None
+    warnings:list[str] = Field(default_factory=list)
+
+
+        
+

+ 131 - 0
app/schemas/hotel.py

@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+"""酒店查询与结果模型:HotelSearchQuery查询参数与HotelOption搜索结果。"""
+
+from datetime import date
+from typing import Literal
+from pydantic import field_validator,Field,model_validator
+from app.schemas.common import SourceMetadata,AppModel
+
+HotelSort = Literal["lowest_price","highest_price","most_reviewed"]
+
+HotelRatingFilter = Literal["3.5+","4.0+","4.5+"]
+
+HotelClass = Literal[2,3,4,5]
+
+class HotelSearchQuery(AppModel):
+    """酒店查询参数"""
+    query:str
+    check_in_date:date
+    check_out_date:date
+
+    adults:int = Field(default=1, ge=1, le=9)
+
+    # SerpApi要求提供儿童年龄,而不只是儿童人数。
+    children_ages:list[int] = Field(default_factory=list)
+    
+    currency:str = "CNY"
+    language:str = "zh-cn"
+    country:str = "cn"
+
+    # 以下全部为可选条件
+    min_price:int | None = Field(default=None,gt=0)
+    max_price:int | None = Field(default=None,gt=0)
+
+    rating_filter:HotelRatingFilter | None=None
+    hotel_class:list[HotelClass] = Field(default_factory=list)
+
+    free_cancellation:bool | None=None
+    sort:HotelSort | None=None
+
+    no_cache:bool = False
+
+    @field_validator("query")
+    @classmethod
+    def validate_query(cls,value:str)->str:
+        if not value.strip():
+            raise ValueError("酒店搜索关键词不能为空")
+        return value.strip()
+    
+    @field_validator("currency")
+    @classmethod
+    def normalize_currency(cls,value:str)->str:
+        return value.upper()
+    
+
+    @field_validator("children_ages")
+    @classmethod
+    def validate_children_ages(cls,values:list[int])->list[int]:
+        for age in values:
+            if age < 1 or age > 17:
+                raise ValueError("儿童年龄必须在1-17岁")
+        return values
+    
+    @model_validator(mode="after")
+    def validate_dates_and_prices(self)->"HotelSearchQuery":
+        if self.check_out_date <= self.check_in_date:
+            raise ValueError("退房日期必须晚于入住日期")
+        if (
+            self.min_price is not None
+            and self.max_price is not None
+            and self.min_price > self.max_price
+        ):
+            raise ValueError("最低价格不能高于最高价格")
+        return self
+    
+
+class GeoPoint(AppModel):
+    """地理坐标"""
+    latitude:float
+    longitude:float
+
+
+class HotelPriceOffer(AppModel):
+    """某个酒店供应商给出的价格。"""
+    source:str | None=None
+    price_per_night:float | None=Field(default=None,gt=0)
+
+    total_price:float | None=Field(default=None,gt=0)
+
+    free_cancellation:bool | None=None
+
+class HotelOption(AppModel):
+    """一个标准化酒店候选。"""
+    hotel_id:str
+    provider_rank:int = Field(ge=0)
+    name:str
+    property_type:str | None=None
+    description:str | None=None
+    coordinates:GeoPoint | None=None
+    check_in_time:str | None=None
+    check_out_time:str | None=None
+    hotel_class:int | None=Field(default=None,ge=1,le=5)
+    overall_rating:float | None=Field(default=None,ge=0,le=5)
+    review_count:int | None=Field(default=None,ge=0)
+    location_rating:float | None=Field(default=None,ge=0)
+    price_per_night:float | None=Field(default=None,gt=0)
+    total_price:float | None=Field(default=None,gt=0)
+    currency:str = "CNY"
+
+    amenities:list[str] = Field(default_factory=list)
+    excluded_amenities:list[str] = Field(default_factory=list)
+    free_cancellation:bool | None=None
+    sponsored:bool = False
+    eco_certified:bool | None=None
+    property_token:str | None=None
+    thumbnail_url:str | None=None
+    price_offers:list[HotelPriceOffer] = Field(default_factory=list)
+
+    # 新增这两个字段,用于保存接口返回的优惠信息
+    deal: str | None = None
+    deal_description: str | None = None
+
+class HotelSearchResult(AppModel):
+    """酒店搜索结果"""
+    query:HotelSearchQuery
+    hotels:list[HotelOption] = Field(default_factory=list)
+    total_count:int = Field(default=0,ge=0)
+    source:SourceMetadata
+    next_page_token:str | None=None
+    warnings:list[str] = Field(default_factory=list)
+    

+ 241 - 0
app/schemas/itinerary.py

@@ -0,0 +1,241 @@
+from __future__ import annotations
+
+"""行程规划结果模型:ItineraryPlan多日行程、FlightSelection航班选择、HotelSelection酒店选择及每日活动。"""
+
+from datetime import date, time
+from typing import Literal
+
+from pydantic import Field, field_validator, model_validator
+
+from app.schemas.common import AppModel
+
+
+ActivityType = Literal[
+    "flight",
+    "hotel_check_in",
+    "hotel_check_out",
+    "attraction",
+    "restaurant",
+    "transport",
+    "free_time",
+    "other",
+]
+
+TransportMode = Literal[
+    "flight",
+    "walking",
+    "public_transit",
+    "driving",
+    "taxi",
+    "unknown",
+]
+
+
+class FlightSelection(AppModel):
+    """规划角色选择的航班方案。
+
+    这里保存候选ID,后续Validator将检查这些ID
+    是否真实存在于候选结果中。
+    """
+
+    flight_type: Literal[
+        "one_way",
+        "round_trip",
+    ]
+
+    combination_id: str | None = None
+
+    outbound_option_id: str
+    return_option_id: str | None = None
+
+    # 接口报价(不是去程+返程的加总)。
+    quoted_price: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    currency: str = "CNY"
+
+    outbound_flight_numbers: list[str] = Field(
+        default_factory=list
+    )
+
+    return_flight_numbers: list[str] = Field(
+        default_factory=list
+    )
+
+    outbound_departure_time: str | None = None
+    outbound_arrival_time: str | None = None
+
+    return_departure_time: str | None = None
+    return_arrival_time: str | None = None
+
+    selection_reason: str
+
+    @model_validator(mode="after")
+    def validate_round_trip(
+        self,
+    ) -> "FlightSelection":
+        if self.flight_type == "round_trip":
+            if not self.combination_id:
+                raise ValueError(
+                    "往返方案必须提供combination_id。"
+                )
+
+            if not self.return_option_id:
+                raise ValueError(
+                    "往返方案必须提供return_option_id。"
+                )
+
+        return self
+
+
+class HotelSelection(AppModel):
+    """规划角色选择的住宿方案。"""
+
+    hotel_id: str
+    name: str
+
+    check_in_date: date
+    check_out_date: date
+    nights: int = Field(ge=1)
+
+    price_per_night: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    estimated_total_price: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    currency: str = "CNY"
+
+    address: str | None = None
+    location: str | None = None
+
+    selection_reason: str
+
+    @field_validator("location", mode="before")
+    @classmethod
+    def _coerce_location_to_str(
+        cls,
+        value: object,
+    ) -> object:
+        """LLM 可能将 coordinates dict 误填到 location,
+        自动转换为 "longitude,latitude" 字符串。"""
+        if isinstance(value, dict):
+            lat = value.get("latitude")
+            lng = value.get("longitude")
+            if lat is not None and lng is not None:
+                return f"{lng},{lat}"
+        return value
+
+
+class ItineraryActivity(AppModel):
+    """一天中的一个具体活动。"""
+
+    sequence: int = Field(ge=1)
+
+    activity_type: ActivityType
+
+    # 景点或餐厅对应的真实POI ID。
+    # 航班、入住等活动允许为空。
+    reference_id: str | None = None
+
+    name: str
+
+    start_time: time
+    end_time: time
+
+    address: str | None = None
+    location: str | None = None
+
+    transport_mode: TransportMode | None = None
+
+    estimated_transport_minutes: int | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    estimated_cost: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    notes: str | None = None
+
+
+class DailyItinerary(AppModel):
+    """单日行程。"""
+
+    day_index: int = Field(
+        ge=1,
+        le=30,
+    )
+
+    date: date
+
+    theme: str
+
+    activities: list[ItineraryActivity] = Field(
+        min_length=1
+    )
+
+    daily_notes: list[str] = Field(
+        default_factory=list
+    )
+
+
+class ItineraryPlan(AppModel):
+    """行程规划角色生成的完整结构化方案。"""
+
+    title: str
+    overview: str
+
+    selected_flight: FlightSelection
+
+    selected_hotel: HotelSelection
+
+    days: list[DailyItinerary] = Field(
+        min_length=1
+    )
+
+    highlights: list[str] = Field(
+        default_factory=list
+    )
+
+    assumptions: list[str] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )
+
+    @model_validator(mode="after")
+    def validate_days(
+        self,
+    ) -> "ItineraryPlan":
+        day_indexes = [
+            item.day_index
+            for item in self.days
+        ]
+
+        dates = [
+            item.date
+            for item in self.days
+        ]
+
+        if len(day_indexes) != len(set(day_indexes)):
+            raise ValueError(
+                "每日行程的day_index不能重复。"
+            )
+
+        if len(dates) != len(set(dates)):
+            raise ValueError(
+                "每日行程日期不能重复。"
+            )
+
+        return self

+ 68 - 0
app/schemas/place.py

@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+"""地点研究结果模型:MapResearchResult包含候选景点、餐饮与天气信息。"""
+
+from pydantic import Field
+
+from app.schemas.common import AppModel
+
+
+class PlaceCandidate(AppModel):
+    """真实地点候选。"""
+
+    poi_id: str | None = None
+
+    name: str
+    category: str | None = None
+
+    address: str | None = None
+    district: str | None = None
+
+    # 高德通常使用 "经度,纬度" 字符串
+    location: str | None = None
+
+    telephone: str | None = None
+
+    source_query: str | None = None
+
+    # 这里只说明为什么匹配用户兴趣,
+    # 不涉及距离和交通便利性的判断。
+    match_reason: str | None = None
+
+
+class WeatherItem(AppModel):
+    """天气查询结果。"""
+
+    date: str | None = None
+    week: str | None = None
+
+    day_weather: str | None = None
+    night_weather: str | None = None
+
+    day_temperature: str | None = None
+    night_temperature: str | None = None
+
+    day_wind_direction: str | None = None
+    night_wind_direction: str | None = None
+
+
+class MapResearchResult(AppModel):
+    """地图研究 Agent 的结构化结果。"""
+
+    city: str
+
+    attractions: list[PlaceCandidate] = Field(
+        default_factory=list
+    )
+
+    restaurants: list[PlaceCandidate] = Field(
+        default_factory=list
+    )
+
+    weather: list[WeatherItem] = Field(
+        default_factory=list
+    )
+
+    notes: list[str] = Field(
+        default_factory=list
+    )

+ 107 - 0
app/schemas/review.py

@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+"""行程评审结果模型:TripReviewResult包含评审结论、问题列表与修改意见。"""
+
+from typing import Literal
+
+from pydantic import Field, model_validator
+
+from app.schemas.common import AppModel
+
+
+ReviewCategory = Literal[
+    "requirement_fit",
+    "pace",
+    "geography",
+    "flight",
+    "hotel",
+    "budget",
+    "completeness",
+    "grounding",
+    "user_experience",
+    "other",
+]
+
+ReviewSeverity = Literal[
+    "blocking",
+    "warning",
+]
+
+
+class ReviewIssue(AppModel):
+    """Reviewer发现的单个语义问题。"""
+
+    category: ReviewCategory
+    severity: ReviewSeverity
+
+    message: str
+
+    day_index: int | None = Field(
+        default=None,
+        ge=1,
+        le=30,
+    )
+
+    activity_name: str | None = None
+
+    evidence: str | None = None
+
+    suggestion: str | None = None
+
+
+class TripReviewResult(AppModel):
+    """Trip Reviewer的结构化审查结果。"""
+
+    approved: bool
+
+    summary: str
+
+    strengths: list[str] = Field(
+        default_factory=list
+    )
+
+    issues: list[ReviewIssue] = Field(
+        default_factory=list
+    )
+
+    # 非阻塞优化建议。
+    recommendations: list[str] = Field(
+        default_factory=list
+    )
+
+    # 仅在方案不通过时填写,
+    # 下一轮Planner必须执行这些修改。
+    revision_feedback: list[str] = Field(
+        default_factory=list
+    )
+
+    @model_validator(mode="after")
+    def validate_review_result(
+        self,
+    ) -> "TripReviewResult":
+        blocking_issues = [
+            issue
+            for issue in self.issues
+            if issue.severity == "blocking"
+        ]
+
+        if self.approved:
+            if blocking_issues:
+                raise ValueError(
+                    "审查通过时不能包含blocking问题。"
+                )
+
+            if self.revision_feedback:
+                raise ValueError(
+                    "审查通过时revision_feedback"
+                    "必须为空。"
+                )
+
+        else:
+            if not self.revision_feedback:
+                raise ValueError(
+                    "审查不通过时必须给出"
+                    "revision_feedback。"
+                )
+
+        return self

+ 107 - 0
app/schemas/route.py

@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+"""路线评估结果模型:RouteLeg路线段与RouteEvaluationResult酒店位置评估。"""
+
+from typing import Any, Literal
+
+from pydantic import Field
+
+from app.schemas.common import AppModel
+
+
+RouteDataSource = Literal[
+    "amap",
+    "haversine_fallback",
+]
+
+
+class RouteLeg(AppModel):
+    """酒店与一个景点之间的距离结果。"""
+
+    origin_name: str
+    origin_location: str
+
+    destination_name: str
+    destination_location: str
+
+    distance_meters: float = Field(ge=0)
+
+    duration_seconds: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    source: RouteDataSource = "amap"
+
+
+class HotelRouteEvaluation(AppModel):
+    """综合酒店自身质量和真实位置后的评分结果。"""
+
+    hotel_id: str
+    hotel_name: str
+
+    final_score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    base_hotel_score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    centrality_score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    subway_score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    average_distance_meters: float | None = None
+    maximum_distance_meters: float | None = None
+
+    average_duration_seconds: float | None = None
+
+    nearest_subway_distance_meters: (
+        float | None
+    ) = None
+
+    route_legs: list[RouteLeg] = Field(
+        default_factory=list
+    )
+
+    reasons: list[str] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )
+
+    hotel: dict[str, Any]
+
+
+class RouteEvaluationResult(AppModel):
+    """路线评估阶段的最终结果。"""
+
+    evaluated_hotels: list[
+        HotelRouteEvaluation
+    ] = Field(
+        default_factory=list
+    )
+
+    reference_attractions: list[str] = Field(
+        default_factory=list
+    )
+
+    selected_hotel_id: str | None = None
+
+    distance_call_count: int = 0
+    around_search_call_count: int = 0
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )

+ 118 - 0
app/schemas/selection.py

@@ -0,0 +1,118 @@
+from __future__ import annotations
+
+"""候选筛选结果模型:RankedFlightCandidate与RankedHotelCandidate等经排序的候选。"""
+
+from typing import Any, Literal
+
+from pydantic import Field
+
+from app.schemas.common import AppModel
+
+
+class RankedFlightCandidate(AppModel):
+    """经过确定性评分的单个航班候选。"""
+
+    option_id: str
+
+    score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    reasons: list[str] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )
+
+    flight: dict[str, Any]
+
+
+class RoundTripOption(AppModel):
+    """一个完整的往返航班组合。"""
+
+    combination_id: str
+
+    score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    # 返程查询返回的完整往返接口报价(不是去程+返程的加总)。
+    quoted_price: float | None = None
+
+    currency: str = "CNY"
+
+    outbound: dict[str, Any]
+    return_flight: dict[str, Any]
+
+    reasons: list[str] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )
+
+
+class RankedHotelCandidate(AppModel):
+    """经过确定性评分的酒店候选。"""
+
+    hotel_id: str
+
+    score: float = Field(
+        ge=0,
+        le=100,
+    )
+
+    reasons: list[str] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )
+
+    hotel: dict[str, Any]
+
+
+class CandidateSelectionResult(AppModel):
+    """航班和酒店候选筛选结果。"""
+
+    flight_type: Literal[
+        "one_way",
+        "round_trip",
+    ]
+
+    # 往返搜索时,用于发起返程查询的去程候选。
+    outbound_candidates: list[
+        RankedFlightCandidate
+    ] = Field(
+        default_factory=list
+    )
+
+    # 单程旅行使用。
+    one_way_options: list[
+        RankedFlightCandidate
+    ] = Field(
+        default_factory=list
+    )
+
+    # 往返旅行使用。
+    round_trip_options: list[
+        RoundTripOption
+    ] = Field(
+        default_factory=list
+    )
+
+    hotels: list[
+        RankedHotelCandidate
+    ] = Field(
+        default_factory=list
+    )
+
+    warnings: list[str] = Field(
+        default_factory=list
+    )

+ 221 - 0
app/schemas/travel_request.py

@@ -0,0 +1,221 @@
+from __future__ import annotations
+
+"""用户旅行需求模型:从自然语言解析出的结构化TravelRequest,包含偏好、日期、预算等所有维度。"""
+
+from datetime import date, time
+from typing import Literal
+
+from pydantic import Field, field_validator
+
+from app.schemas.common import AppModel
+
+
+TravelPace = Literal[
+    "relaxed",
+    "normal",
+    "intensive",
+]
+
+TravelPriority = Literal[
+    "price",
+    "convenience",
+    "balanced",
+]
+
+
+class FlightPreferences(AppModel):
+    """用户对航班的偏好。
+
+    这些字段全部是软偏好或可选限制。
+    用户没有明确表达时,不主动添加限制。
+    """
+
+    priority: TravelPriority = "balanced"
+
+    preferred_departure_airports: list[str] = Field(
+        default_factory=list
+    )
+    preferred_arrival_airports: list[str] = Field(
+        default_factory=list
+    )
+
+    earliest_departure_time: time | None = None
+    latest_departure_time: time | None = None
+
+    earliest_arrival_time: time | None = None
+    latest_arrival_time: time | None = None
+
+    # 0表示只接受直飞;None表示不限制。
+    max_stops: int | None = Field(
+        default=None,
+        ge=0,
+        le=3,
+    )
+
+    @field_validator(
+        "preferred_departure_airports",
+        "preferred_arrival_airports",
+    )
+    @classmethod
+    def normalize_airports(
+        cls,
+        value: list[str],
+    ) -> list[str]:
+        result: list[str] = []
+
+        for airport in value:
+            code = airport.strip().upper()
+
+            if code and code not in result:
+                result.append(code)
+
+        return result
+
+
+class HotelPreferences(AppModel):
+    """用户对酒店的偏好。"""
+
+    max_price_per_night: float | None = Field(
+        default=None,
+        gt=0,
+    )
+
+    minimum_rating: float | None = Field(
+        default=None,
+        ge=0,
+        le=5,
+    )
+
+    hotel_classes: list[int] = Field(
+        default_factory=list,
+    )
+
+    @field_validator("hotel_classes")
+    @classmethod
+    def validate_hotel_classes(
+        cls,
+        value: list[int],
+    ) -> list[int]:
+        for item in value:
+            if item not in (2, 3, 4, 5):
+                raise ValueError(
+                    f"酒店星级必须在 2-5 之间,实际: {item}"
+                )
+        return value
+
+    near_subway: bool | None = None
+
+    preferred_areas: list[str] = Field(
+        default_factory=list
+    )
+
+    amenities: list[str] = Field(
+        default_factory=list
+    )
+
+
+class TravelRequest(AppModel):
+    """从用户自然语言中提取的完整旅行需求。"""
+
+    origin_city: str | None = None
+    destination_city: str | None = None
+
+    departure_date: date | None = None
+    return_date: date | None = None
+
+    # “4天3晚”等信息先保留下来,
+    # 后续确定性代码可以据此推算返程日期。
+    trip_days: int | None = Field(
+        default=None,
+        ge=1,
+        le=30,
+    )
+
+    nights: int | None = Field(
+        default=None,
+        ge=0,
+        le=29,
+    )
+
+    adults: int = Field(
+        default=1,
+        ge=1,
+        le=9,
+    )
+
+    children: int = Field(
+        default=0,
+        ge=0,
+        le=8,
+    )
+
+    @field_validator(
+        "children",
+        mode="before",
+    )
+    @classmethod
+    def normalize_children(
+        cls,
+        value: object,
+    ) -> int:
+        """LLM 可能误将 children 输出为 [],防御性转换为 int。"""
+        if value is None:
+            return 0
+
+        if isinstance(value, bool):
+            return 1 if value else 0
+
+        if isinstance(value, (list, tuple)):
+            return len(value)
+
+        return int(value)
+
+    total_budget: float | None = Field(
+        default=None,
+        gt=0,
+    )
+
+    currency: str = "CNY"
+
+    interests: list[str] = Field(
+        default_factory=list
+    )
+
+    pace: TravelPace = "normal"
+
+    flight_preferences: FlightPreferences = Field(
+        default_factory=FlightPreferences
+    )
+
+    hotel_preferences: HotelPreferences = Field(
+        default_factory=HotelPreferences
+    )
+
+    special_requirements: list[str] = Field(
+        default_factory=list
+    )
+
+    @field_validator(
+        "origin_city",
+        "destination_city",
+        mode="before",
+    )
+    @classmethod
+    def normalize_city(
+        cls,
+        value: object,
+    ) -> str | None:
+        if value is None:
+            return None
+
+        text = str(value).strip()
+
+        return text or None
+
+    @field_validator("currency")
+    @classmethod
+    def normalize_currency(
+        cls,
+        value: str,
+    ) -> str:
+        return value.strip().upper()

+ 93 - 0
app/schemas/validation.py

@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+"""方案校验结果模型:PlanValidationResult包含问题清单、预算估算与修改意见。"""
+
+from typing import Literal
+
+from pydantic import Field
+
+from app.schemas.common import AppModel
+
+
+ValidationSeverity = Literal[
+    "error",
+    "warning",
+]
+
+BudgetStatus = Literal[
+    "within_known_budget",
+    "over_budget",
+    "unknown",
+]
+
+
+class ValidationIssue(AppModel):
+    """行程校验发现的单个问题。"""
+
+    code: str
+    severity: ValidationSeverity
+    message: str
+
+    day_index: int | None = None
+    activity_sequence: int | None = None
+
+
+class BudgetEstimate(AppModel):
+    """当前能够确定的预算估算。
+
+    航班仍标记为接口报价,不擅自解释为
+    单人价格或全部旅客总价。
+    """
+
+    currency: str = "CNY"
+
+    flight_quote: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    hotel_estimate: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    explicit_activity_costs: float = Field(
+        default=0,
+        ge=0,
+    )
+
+    known_total: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    user_budget: float | None = Field(
+        default=None,
+        ge=0,
+    )
+
+    remaining_known_budget: float | None = None
+
+    status: BudgetStatus = "unknown"
+
+    coverage_note: str
+
+
+class PlanValidationResult(AppModel):
+    """确定性校验阶段的完整结果。"""
+
+    is_valid: bool
+
+    error_count: int = Field(ge=0)
+    warning_count: int = Field(ge=0)
+
+    issues: list[ValidationIssue] = Field(
+        default_factory=list
+    )
+
+    budget: BudgetEstimate
+
+    # 后续Supervisor将这些信息交给Planner重写。
+    revision_feedback: list[str] = Field(
+        default_factory=list
+    )

+ 0 - 0
app/services/__init__.py


+ 1134 - 0
app/services/candidate_selection_service.py

@@ -0,0 +1,1134 @@
+from __future__ import annotations
+
+"""候选筛选服务:对航班/酒店/往返组合做确定性规则筛选与加权评分排序,不调用大模型或外部接口。"""
+
+import hashlib
+from datetime import datetime, time
+from typing import Any, Iterable
+
+from app.schemas.selection import (
+    RankedFlightCandidate,
+    RankedHotelCandidate,
+    RoundTripOption,
+)
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+
+
+class CandidateSelectionService:
+    """使用确定性规则筛选和排序旅行候选。
+
+    该服务不调用大模型,也不调用外部接口。
+    """
+
+    def rank_flights(
+        self,
+        flights: list[dict[str, Any]],
+        request: TravelRequest,
+        *,
+        limit: int = 10,
+        reverse_direction: bool = False,
+        apply_time_preferences: bool = True,
+    ) -> tuple[
+        list[RankedFlightCandidate],
+        list[str],
+    ]:
+        """对单程方向的航班候选进行排序。"""
+
+        valid_flights = [
+            flight
+            for flight in flights
+            if isinstance(flight, dict)
+            and flight.get("option_id")
+        ]
+
+        warnings: list[str] = []
+
+        if not valid_flights:
+            return [], ["没有有效航班候选。"]
+
+        filtered_flights = self._filter_flights(
+            valid_flights,
+            request,
+        )
+
+        if not filtered_flights:
+            filtered_flights = valid_flights
+
+            warnings.append(
+                "没有航班完全满足中转限制,"
+                "已回退到全部候选并进行排序。"
+            )
+
+        prices = [
+            price
+            for flight in filtered_flights
+            if (
+                price := self._to_float(
+                    flight.get("price")
+                )
+            )
+            is not None
+        ]
+
+        durations = [
+            duration
+            for flight in filtered_flights
+            if (
+                duration := self._to_float(
+                    flight.get(
+                        "duration_minutes"
+                    )
+                )
+            )
+            is not None
+        ]
+
+        priority = (
+            request.flight_preferences.priority
+        )
+
+        weights = self._flight_weights(priority)
+
+        ranked: list[RankedFlightCandidate] = []
+
+        for flight in filtered_flights:
+            price = self._to_float(
+                flight.get("price")
+            )
+
+            duration = self._to_float(
+                flight.get("duration_minutes")
+            )
+
+            stop_count = self._to_int(
+                flight.get("stop_count")
+            )
+
+            price_score = self._inverse_score(
+                price,
+                prices,
+            )
+
+            duration_score = self._inverse_score(
+                duration,
+                durations,
+            )
+
+            stop_score = self._stop_score(
+                stop_count
+            )
+
+            airport_score = (
+                self._airport_match_score(
+                    flight,
+                    request,
+                    reverse_direction=(
+                        reverse_direction
+                    ),
+                )
+            )
+
+            time_score = 100.0
+
+            candidate_warnings: list[str] = []
+
+            if apply_time_preferences:
+                (
+                    time_score,
+                    time_warnings,
+                ) = self._time_match_score(
+                    flight,
+                    request,
+                )
+
+                candidate_warnings.extend(
+                    time_warnings
+                )
+
+            score = (
+                price_score * weights["price"]
+                + duration_score
+                * weights["duration"]
+                + stop_score
+                * weights["stops"]
+                + airport_score
+                * weights["airport"]
+                + time_score
+                * weights["time"]
+            )
+
+            reasons = self._flight_reasons(
+                flight=flight,
+                price_score=price_score,
+                duration_score=duration_score,
+                airport_score=airport_score,
+                time_score=time_score,
+                apply_time_preferences=(
+                    apply_time_preferences
+                ),
+            )
+
+            ranked.append(
+                RankedFlightCandidate(
+                    option_id=str(
+                        flight["option_id"]
+                    ),
+                    score=round(score, 2),
+                    reasons=reasons,
+                    warnings=candidate_warnings,
+                    flight=flight,
+                )
+            )
+
+        ranked.sort(
+            key=lambda candidate: (
+                -candidate.score,
+                self._to_float(
+                    candidate.flight.get(
+                        "price"
+                    )
+                )
+                or float("inf"),
+            )
+        )
+
+        return ranked[:limit], warnings
+
+    def rank_hotels(
+        self,
+        hotels: list[dict[str, Any]],
+        request: TravelRequest,
+        *,
+        limit: int = 10,
+    ) -> tuple[
+        list[RankedHotelCandidate],
+        list[str],
+    ]:
+        """根据酒店预算、评分和客观属性排序。"""
+
+        valid_hotels = [
+            hotel
+            for hotel in hotels
+            if isinstance(hotel, dict)
+            and hotel.get("hotel_id")
+        ]
+
+        warnings: list[str] = []
+
+        if not valid_hotels:
+            return [], ["没有有效酒店候选。"]
+
+        filtered_hotels = self._filter_hotels(
+            valid_hotels,
+            request,
+        )
+
+        if not filtered_hotels:
+            filtered_hotels = valid_hotels
+
+            warnings.append(
+                "没有酒店同时满足全部预算、"
+                "评分或星级限制,"
+                "已回退到全部候选。"
+            )
+
+        prices = [
+            price
+            for hotel in filtered_hotels
+            if (
+                price := self._to_float(
+                    hotel.get("price_per_night")
+                )
+            )
+            is not None
+        ]
+
+        ranked: list[RankedHotelCandidate] = []
+
+        hotel_preferences = (
+            request.hotel_preferences
+        )
+
+        for hotel in filtered_hotels:
+            price = self._to_float(
+                hotel.get("price_per_night")
+            )
+
+            rating = self._to_float(
+                hotel.get("overall_rating")
+            )
+
+            location_rating = self._to_float(
+                hotel.get("location_rating")
+            )
+
+            price_score = self._inverse_score(
+                price,
+                prices,
+            )
+
+            rating_score = self._rating_score(
+                rating
+            )
+
+            location_score = self._rating_score(
+                location_rating
+            )
+
+            cancellation_bonus = (
+                5.0
+                if hotel.get(
+                    "free_cancellation"
+                )
+                is True
+                else 0.0
+            )
+
+            score = (
+                price_score * 0.45
+                + rating_score * 0.35
+                + location_score * 0.20
+                + cancellation_bonus
+            )
+
+            score = min(score, 100.0)
+
+            reasons: list[str] = []
+            candidate_warnings: list[str] = []
+
+            if price_score >= 70:
+                reasons.append(
+                    "每晚价格在候选中相对较低"
+                )
+
+            if rating is not None and rating >= 4.5:
+                reasons.append(
+                    f"用户评分较高({rating:.1f})"
+                )
+
+            if (
+                location_rating is not None
+                and location_rating >= 4.0
+            ):
+                reasons.append(
+                    "位置评分较高"
+                )
+
+            if hotel.get("free_cancellation"):
+                reasons.append(
+                    "支持免费取消"
+                )
+
+            if hotel.get("deal"):
+                reasons.append(
+                    "接口返回优惠信息"
+                )
+
+            requested_amenities = {
+                item.strip().lower()
+                for item in hotel_preferences.amenities
+                if item.strip()
+            }
+
+            available_amenities = {
+                str(item).strip().lower()
+                for item in hotel.get(
+                    "amenities",
+                    []
+                )
+                if str(item).strip()
+            }
+
+            if requested_amenities:
+                matched = (
+                    requested_amenities
+                    & available_amenities
+                )
+
+                if matched:
+                    reasons.append(
+                        "匹配部分指定设施"
+                    )
+
+                missing = (
+                    requested_amenities
+                    - available_amenities
+                )
+
+                if missing:
+                    candidate_warnings.append(
+                        "未确认全部指定设施:"
+                        + "、".join(sorted(missing))
+                    )
+
+            if not reasons:
+                reasons.append(
+                    "综合价格、评分和位置排序"
+                )
+
+            ranked.append(
+                RankedHotelCandidate(
+                    hotel_id=str(
+                        hotel["hotel_id"]
+                    ),
+                    score=round(score, 2),
+                    reasons=reasons,
+                    warnings=candidate_warnings,
+                    hotel=hotel,
+                )
+            )
+
+        ranked.sort(
+            key=lambda candidate: (
+                -candidate.score,
+                self._to_float(
+                    candidate.hotel.get(
+                        "price_per_night"
+                    )
+                )
+                or float("inf"),
+            )
+        )
+
+        if hotel_preferences.near_subway:
+            warnings.append(
+                "酒店靠近地铁的要求尚未完成"
+                "真实路线校验,将在高德路线阶段处理。"
+            )
+
+        if hotel_preferences.preferred_areas:
+            warnings.append(
+                "酒店商圈偏好将在地图距离阶段"
+                "进一步验证。"
+            )
+
+        return ranked[:limit], warnings
+
+    def rank_round_trip_combinations(
+        self,
+        combinations: list[
+            dict[str, dict[str, Any]]
+        ],
+        request: TravelRequest,
+        *,
+        limit: int = 10,
+    ) -> list[RoundTripOption]:
+        """对完整往返组合进行统一排序。"""
+
+        valid_combinations = [
+            combination
+            for combination in combinations
+            if isinstance(
+                combination.get("outbound"),
+                dict,
+            )
+            and isinstance(
+                combination.get(
+                    "return_flight"
+                ),
+                dict,
+            )
+        ]
+
+        prices: list[float] = []
+        durations: list[float] = []
+
+        for combination in valid_combinations:
+            outbound = combination["outbound"]
+            return_flight = combination[
+                "return_flight"
+            ]
+
+            # 返程查询中的price通常代表选择该去程后
+            # 得到的完整接口报价,因此不与去程相加。
+            price = self._to_float(
+                return_flight.get("price")
+            )
+
+            if price is None:
+                price = self._to_float(
+                    outbound.get("price")
+                )
+
+            if price is not None:
+                prices.append(price)
+
+            total_duration = (
+                (
+                    self._to_float(
+                        outbound.get(
+                            "duration_minutes"
+                        )
+                    )
+                    or 0.0
+                )
+                + (
+                    self._to_float(
+                        return_flight.get(
+                            "duration_minutes"
+                        )
+                    )
+                    or 0.0
+                )
+            )
+
+            durations.append(total_duration)
+
+        weights = self._flight_weights(
+            request.flight_preferences.priority
+        )
+
+        results: list[RoundTripOption] = []
+
+        seen_ids: set[str] = set()
+
+        for combination in valid_combinations:
+            outbound = combination["outbound"]
+            return_flight = combination[
+                "return_flight"
+            ]
+
+            outbound_id = str(
+                outbound.get("option_id", "")
+            )
+
+            return_id = str(
+                return_flight.get(
+                    "option_id",
+                    "",
+                )
+            )
+
+            combination_id = self._stable_id(
+                outbound_id,
+                return_id,
+            )
+
+            if combination_id in seen_ids:
+                continue
+
+            seen_ids.add(combination_id)
+
+            quoted_price = self._to_float(
+                return_flight.get("price")
+            )
+
+            if quoted_price is None:
+                quoted_price = self._to_float(
+                    outbound.get("price")
+                )
+
+            outbound_duration = (
+                self._to_float(
+                    outbound.get(
+                        "duration_minutes"
+                    )
+                )
+                or 0.0
+            )
+
+            return_duration = (
+                self._to_float(
+                    return_flight.get(
+                        "duration_minutes"
+                    )
+                )
+                or 0.0
+            )
+
+            total_duration = (
+                outbound_duration
+                + return_duration
+            )
+
+            outbound_stops = (
+                self._to_int(
+                    outbound.get("stop_count")
+                )
+                or 0
+            )
+
+            return_stops = (
+                self._to_int(
+                    return_flight.get(
+                        "stop_count"
+                    )
+                )
+                or 0
+            )
+
+            total_stops = (
+                outbound_stops
+                + return_stops
+            )
+
+            price_score = self._inverse_score(
+                quoted_price,
+                prices,
+            )
+
+            duration_score = (
+                self._inverse_score(
+                    total_duration,
+                    durations,
+                )
+            )
+
+            stop_score = max(
+                0.0,
+                100.0 - total_stops * 25.0,
+            )
+
+            outbound_airport_score = (
+                self._airport_match_score(
+                    outbound,
+                    request,
+                    reverse_direction=False,
+                )
+            )
+
+            return_airport_score = (
+                self._airport_match_score(
+                    return_flight,
+                    request,
+                    reverse_direction=True,
+                )
+            )
+
+            airport_score = (
+                outbound_airport_score
+                + return_airport_score
+            ) / 2.0
+
+            # 往返组合中不再次使用去程时间偏好
+            # 评价返程,因为当前模型没有独立返程时间字段。
+            score = (
+                price_score * weights["price"]
+                + duration_score
+                * weights["duration"]
+                + stop_score
+                * weights["stops"]
+                + airport_score
+                * (
+                    weights["airport"]
+                    + weights["time"]
+                )
+            )
+
+            reasons: list[str] = []
+
+            if price_score >= 70:
+                reasons.append(
+                    "往返接口报价在组合中相对较低"
+                )
+
+            if total_stops == 0:
+                reasons.append(
+                    "去程和返程均为直飞"
+                )
+
+            elif total_stops <= 2:
+                reasons.append(
+                    "往返中转次数较少"
+                )
+
+            if duration_score >= 70:
+                reasons.append(
+                    "往返总飞行时长相对较短"
+                )
+
+            if airport_score >= 90:
+                reasons.append(
+                    "匹配用户机场偏好"
+                )
+
+            if not reasons:
+                reasons.append(
+                    "综合价格、时长和中转次数排序"
+                )
+
+            currency = str(
+                return_flight.get("currency")
+                or outbound.get("currency")
+                or request.currency
+            )
+
+            results.append(
+                RoundTripOption(
+                    combination_id=combination_id,
+                    score=round(score, 2),
+                    quoted_price=quoted_price,
+                    currency=currency,
+                    outbound=outbound,
+                    return_flight=return_flight,
+                    reasons=reasons,
+                    warnings=[
+                        "组合价格采用返程查询返回的"
+                        "接口报价,未将两个方向报价相加。"
+                    ],
+                )
+            )
+
+        results.sort(
+            key=lambda option: (
+                -option.score,
+                option.quoted_price
+                if option.quoted_price is not None
+                else float("inf"),
+            )
+        )
+
+        return results[:limit]
+
+    def _filter_flights(
+        self,
+        flights: list[dict[str, Any]],
+        request: TravelRequest,
+    ) -> list[dict[str, Any]]:
+        """按中转次数上限过滤航班列表。"""
+        max_stops = (
+            request.flight_preferences.max_stops
+        )
+
+        if max_stops is None:
+            return flights
+
+        return [
+            flight
+            for flight in flights
+            if (
+                self._to_int(
+                    flight.get("stop_count")
+                )
+                is not None
+                and self._to_int(
+                    flight.get("stop_count")
+                )
+                <= max_stops
+            )
+        ]
+
+    def _filter_hotels(
+        self,
+        hotels: list[dict[str, Any]],
+        request: TravelRequest,
+    ) -> list[dict[str, Any]]:
+        """按价格上限、最低评分、星级过滤酒店列表。"""
+        preferences = request.hotel_preferences
+
+        result: list[dict[str, Any]] = []
+
+        for hotel in hotels:
+            price = self._to_float(
+                hotel.get("price_per_night")
+            )
+
+            rating = self._to_float(
+                hotel.get("overall_rating")
+            )
+
+            hotel_class = self._to_float(
+                hotel.get("hotel_class")
+            )
+
+            if (
+                preferences.max_price_per_night
+                is not None
+                and (
+                    price is None
+                    or price
+                    > preferences.max_price_per_night
+                )
+            ):
+                continue
+
+            if (
+                preferences.minimum_rating
+                is not None
+                and (
+                    rating is None
+                    or rating
+                    < preferences.minimum_rating
+                )
+            ):
+                continue
+
+            if preferences.hotel_classes:
+                allowed_classes = {
+                    float(value)
+                    for value
+                    in preferences.hotel_classes
+                }
+
+                if (
+                    hotel_class is None
+                    or hotel_class
+                    not in allowed_classes
+                ):
+                    continue
+
+            result.append(hotel)
+
+        return result
+
+    def _flight_weights(
+        self,
+        priority: str,
+    ) -> dict[str, float]:
+        """根据用户偏好优先级返回各评分维度的权重字典。"""
+        if priority == "price":
+            return {
+                "price": 0.60,
+                "duration": 0.15,
+                "stops": 0.15,
+                "airport": 0.05,
+                "time": 0.05,
+            }
+
+        if priority == "convenience":
+            return {
+                "price": 0.15,
+                "duration": 0.30,
+                "stops": 0.25,
+                "airport": 0.15,
+                "time": 0.15,
+            }
+
+        return {
+            "price": 0.35,
+            "duration": 0.25,
+            "stops": 0.20,
+            "airport": 0.10,
+            "time": 0.10,
+        }
+
+    def _flight_reasons(
+        self,
+        *,
+        flight: dict[str, Any],
+        price_score: float,
+        duration_score: float,
+        airport_score: float,
+        time_score: float,
+        apply_time_preferences: bool,
+    ) -> list[str]:
+        """根据各维度得分生成中文排序理由片段列表。"""
+        reasons: list[str] = []
+
+        stop_count = (
+            self._to_int(
+                flight.get("stop_count")
+            )
+            or 0
+        )
+
+        if stop_count == 0:
+            reasons.append("直飞")
+
+        elif stop_count == 1:
+            reasons.append("仅中转一次")
+
+        if price_score >= 70:
+            reasons.append(
+                "报价在候选中相对较低"
+            )
+
+        if duration_score >= 70:
+            reasons.append(
+                "飞行时长相对较短"
+            )
+
+        if airport_score >= 90:
+            reasons.append(
+                "匹配用户机场偏好"
+            )
+
+        if (
+            apply_time_preferences
+            and time_score >= 90
+        ):
+            reasons.append(
+                "起降时间符合用户偏好"
+            )
+
+        if not reasons:
+            reasons.append(
+                "综合价格、时长和中转次数排序"
+            )
+
+        return reasons
+
+    def _airport_match_score(
+        self,
+        flight: dict[str, Any],
+        request: TravelRequest,
+        *,
+        reverse_direction: bool,
+    ) -> float:
+        """计算机场偏好匹配得分:命中偏好机场满分,否则低分。"""
+        preferences = request.flight_preferences
+
+        if reverse_direction:
+            preferred_departure = (
+                preferences
+                .preferred_arrival_airports
+            )
+
+            preferred_arrival = (
+                preferences
+                .preferred_departure_airports
+            )
+        else:
+            preferred_departure = (
+                preferences
+                .preferred_departure_airports
+            )
+
+            preferred_arrival = (
+                preferences
+                .preferred_arrival_airports
+            )
+
+        scores: list[float] = []
+
+        departure_airport = str(
+            flight.get("departure_airport", "")
+        ).upper()
+
+        arrival_airport = str(
+            flight.get("arrival_airport", "")
+        ).upper()
+
+        if preferred_departure:
+            scores.append(
+                100.0
+                if departure_airport
+                in preferred_departure
+                else 20.0
+            )
+
+        if preferred_arrival:
+            scores.append(
+                100.0
+                if arrival_airport
+                in preferred_arrival
+                else 20.0
+            )
+
+        if not scores:
+            return 100.0
+
+        return sum(scores) / len(scores)
+
+    def _time_match_score(
+        self,
+        flight: dict[str, Any],
+        request: TravelRequest,
+    ) -> tuple[float, list[str]]:
+        """计算出发/到达时间窗匹配得分,返回得分与警告列表。"""
+        preferences = request.flight_preferences
+
+        departure_datetime = (
+            self._parse_datetime(
+                flight.get("departure_time")
+            )
+        )
+
+        arrival_datetime = (
+            self._parse_datetime(
+                flight.get("arrival_time")
+            )
+        )
+
+        scores: list[float] = []
+        warnings: list[str] = []
+
+        if (
+            preferences.earliest_departure_time
+            is not None
+            or preferences.latest_departure_time
+            is not None
+        ):
+            if departure_datetime is None:
+                scores.append(0.0)
+                warnings.append(
+                    "无法解析航班起飞时间。"
+                )
+
+            elif self._within_time_window(
+                departure_datetime.time(),
+                preferences
+                .earliest_departure_time,
+                preferences
+                .latest_departure_time,
+            ):
+                scores.append(100.0)
+
+            else:
+                scores.append(20.0)
+                warnings.append(
+                    "起飞时间不在用户偏好范围内。"
+                )
+
+        if (
+            preferences.earliest_arrival_time
+            is not None
+            or preferences.latest_arrival_time
+            is not None
+        ):
+            if arrival_datetime is None:
+                scores.append(0.0)
+                warnings.append(
+                    "无法解析航班到达时间。"
+                )
+
+            elif self._within_time_window(
+                arrival_datetime.time(),
+                preferences
+                .earliest_arrival_time,
+                preferences
+                .latest_arrival_time,
+            ):
+                scores.append(100.0)
+
+            else:
+                scores.append(20.0)
+                warnings.append(
+                    "到达时间不在用户偏好范围内。"
+                )
+
+        if not scores:
+            return 100.0, warnings
+
+        return sum(scores) / len(scores), warnings
+
+    @staticmethod
+    def _within_time_window(
+        value: time,
+        earliest: time | None,
+        latest: time | None,
+    ) -> bool:
+        """判断给定时间是否落在可选的起止时间窗口内。"""
+        if earliest is not None and value < earliest:
+            return False
+
+        if latest is not None and value > latest:
+            return False
+
+        return True
+
+    @staticmethod
+    def _stop_score(
+        stop_count: int | None,
+    ) -> float:
+        """经停次数得分:0经停=100分,每多1次减40分。"""
+        if stop_count is None:
+            return 0.0
+
+        return max(
+            0.0,
+            100.0 - stop_count * 40.0,
+        )
+
+    @staticmethod
+    def _rating_score(
+        value: float | None,
+    ) -> float:
+        """将5分制评分/星级线性映射为百分制得分。"""
+        if value is None:
+            return 0.0
+
+        return max(
+            0.0,
+            min(100.0, value / 5.0 * 100.0),
+        )
+
+    @staticmethod
+    def _inverse_score(
+        value: float | None,
+        values: Iterable[float],
+    ) -> float:
+        """将成本类数值反转为得分(值越小得分越高),在候选中做min-max归一化。"""
+        if value is None:
+            return 0.0
+
+        valid_values = list(values)
+
+        if not valid_values:
+            return 0.0
+
+        minimum = min(valid_values)
+        maximum = max(valid_values)
+
+        if maximum == minimum:
+            return 100.0
+
+        score = (
+            maximum - value
+        ) / (
+            maximum - minimum
+        ) * 100.0
+
+        return max(0.0, min(100.0, score))
+
+    @staticmethod
+    def _parse_datetime(
+        value: object,
+    ) -> datetime | None:
+        """安全解析ISO格式时间字符串,解析失败返回None。"""
+        if value is None:
+            return None
+
+        try:
+            return datetime.fromisoformat(
+                str(value)
+            )
+        except ValueError:
+            return None
+
+    @staticmethod
+    def _to_float(
+        value: object,
+    ) -> float | None:
+        """安全转换为float,bool或不可转换值返回None。"""
+        if value is None:
+            return None
+
+        if isinstance(value, bool):
+            return None
+
+        try:
+            return float(value)
+        except (TypeError, ValueError):
+            return None
+
+    @staticmethod
+    def _to_int(
+        value: object,
+    ) -> int | None:
+        """安全转换为int,bool或不可转换值返回None。"""
+        if value is None:
+            return None
+
+        if isinstance(value, bool):
+            return None
+
+        try:
+            return int(value)
+        except (TypeError, ValueError):
+            return None
+
+    @staticmethod
+    def _stable_id(
+        *parts: str,
+    ) -> str:
+        """基于不定个字符串片段生成稳定的SHA256派生ID。"""
+        raw_value = "|".join(parts)
+
+        digest = hashlib.sha256(
+            raw_value.encode("utf-8")
+        ).hexdigest()
+
+        return f"round-{digest[:16]}"

+ 533 - 0
app/services/finalization_service.py

@@ -0,0 +1,533 @@
+from __future__ import annotations
+
+"""定稿渲染服务:将审核通过的行程方案渲染为面向最终用户的Markdown格式旅行方案文本。"""
+
+from app.schemas.itinerary import (
+    ItineraryPlan,
+)
+from app.schemas.review import (
+    TripReviewResult,
+)
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+from app.schemas.validation import (
+    PlanValidationResult,
+)
+
+
+ACTIVITY_LABELS = {
+    "flight": "航班",
+    "hotel_check_in": "酒店入住",
+    "hotel_check_out": "酒店退房",
+    "attraction": "景点",
+    "restaurant": "餐饮",
+    "transport": "交通",
+    "free_time": "自由活动",
+    "other": "其他",
+}
+
+
+class FinalizationService:
+    """将已审核的结构化方案渲染为最终Markdown。"""
+
+    def render(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        validation: PlanValidationResult,
+        review: TripReviewResult,
+        approved: bool,
+        planning_attempts: int,
+        supervisor_reason: str,
+    ) -> str:
+        """渲染最终旅行方案:根据请求、方案、校验结果和评审结论,分段生成需求摘要、航班、酒店、每日行程、预算与风险说明。"""
+        lines: list[str] = []
+
+        if approved:
+            lines.append("# 个性化旅行方案")
+        else:
+            lines.append(
+                "# 个性化旅行方案(存在未解决风险)"
+            )
+
+            lines.extend(
+                [
+                    "",
+                    "> 当前方案已达到最大修改次数,"
+                    "但尚未完全通过校验或审查。"
+                    "请重点阅读文末风险说明。",
+                ]
+            )
+
+        lines.extend(
+            [
+                "",
+                f"## {plan.title}",
+                "",
+                plan.overview,
+                "",
+                "### 需求摘要",
+                "",
+                (
+                    f"- 行程:{request.origin_city}"
+                    f" → {request.destination_city}"
+                ),
+                (
+                    f"- 日期:{request.departure_date}"
+                    f" 至 {request.return_date}"
+                ),
+                (
+                    f"- 人数:{request.adults}位成人,"
+                    f"{request.children}位儿童"
+                ),
+                (
+                    f"- 节奏:{request.pace}"
+                ),
+                (
+                    "- 总预算:"
+                    + (
+                        f"{request.total_budget:.0f}"
+                        f" {request.currency}"
+                        if request.total_budget
+                        is not None
+                        else "未设置"
+                    )
+                ),
+            ]
+        )
+
+        self._append_flight(lines, plan)
+        self._append_hotel(lines, plan)
+        self._append_days(lines, plan)
+        self._append_budget(
+            lines,
+            validation,
+        )
+        self._append_review(
+            lines,
+            review,
+        )
+
+        combined_warnings = self._collect_warnings(
+            plan=plan,
+            validation=validation,
+            review=review,
+            approved=approved,
+        )
+
+        if combined_warnings:
+            lines.extend(
+                [
+                    "",
+                    "### 风险与注意事项",
+                    "",
+                ]
+            )
+
+            for warning in combined_warnings:
+                lines.append(f"- {warning}")
+
+        if plan.assumptions:
+            lines.extend(
+                [
+                    "",
+                    "### 方案假设",
+                    "",
+                ]
+            )
+
+            for assumption in plan.assumptions:
+                lines.append(f"- {assumption}")
+
+        lines.extend(
+            [
+                "",
+                "### 系统执行信息",
+                "",
+                (
+                    f"- Planner执行次数:"
+                    f"{planning_attempts}"
+                ),
+                (
+                    "- 最终状态:"
+                    + (
+                        "校验与审查通过"
+                        if approved
+                        else "达到最大修改次数后输出"
+                    )
+                ),
+                (
+                    f"- Supervisor说明:"
+                    f"{supervisor_reason}"
+                ),
+            ]
+        )
+
+        return "\n".join(lines)
+
+    def _append_flight(
+        self,
+        lines: list[str],
+        plan: ItineraryPlan,
+    ) -> None:
+        """向输出文本追加航班推荐段落。"""
+        flight = plan.selected_flight
+
+        outbound_numbers = "、".join(
+            flight.outbound_flight_numbers
+        ) or "未提供航班号"
+
+        return_numbers = "、".join(
+            flight.return_flight_numbers
+        ) or "未提供航班号"
+
+        lines.extend(
+            [
+                "",
+                "### 推荐航班",
+                "",
+                (
+                    f"- 方案ID:"
+                    f"{flight.combination_id or flight.outbound_option_id}"
+                ),
+                (
+                    f"- 去程:{outbound_numbers}"
+                ),
+                (
+                    "- 去程时间:"
+                    f"{flight.outbound_departure_time or '未知'}"
+                    " → "
+                    f"{flight.outbound_arrival_time or '未知'}"
+                ),
+            ]
+        )
+
+        if flight.flight_type == "round_trip":
+            lines.extend(
+                [
+                    (
+                        f"- 返程:{return_numbers}"
+                    ),
+                    (
+                        "- 返程时间:"
+                        f"{flight.return_departure_time or '未知'}"
+                        " → "
+                        f"{flight.return_arrival_time or '未知'}"
+                    ),
+                ]
+            )
+
+        lines.extend(
+            [
+                (
+                    "- 接口报价:"
+                    + (
+                        f"{flight.quoted_price:.0f}"
+                        f" {flight.currency}"
+                        if flight.quoted_price
+                        is not None
+                        else "未知"
+                    )
+                ),
+                (
+                    f"- 选择原因:"
+                    f"{flight.selection_reason}"
+                ),
+            ]
+        )
+
+    def _append_hotel(
+        self,
+        lines: list[str],
+        plan: ItineraryPlan,
+    ) -> None:
+        """向输出文本追加酒店推荐段落。"""
+        hotel = plan.selected_hotel
+
+        lines.extend(
+            [
+                "",
+                "### 推荐住宿",
+                "",
+                f"- 酒店:{hotel.name}",
+                f"- 酒店ID:{hotel.hotel_id}",
+                (
+                    f"- 入住:{hotel.check_in_date}"
+                    f" 至 {hotel.check_out_date}"
+                    f",共{hotel.nights}晚"
+                ),
+                (
+                    "- 每晚价格:"
+                    + (
+                        f"{hotel.price_per_night:.0f}"
+                        f" {hotel.currency}"
+                        if hotel.price_per_night
+                        is not None
+                        else "未知"
+                    )
+                ),
+                (
+                    "- 酒店估算:"
+                    + (
+                        f"{hotel.estimated_total_price:.0f}"
+                        f" {hotel.currency}"
+                        if hotel.estimated_total_price
+                        is not None
+                        else "未知"
+                    )
+                ),
+                (
+                    f"- 地址:"
+                    f"{hotel.address or '未提供'}"
+                ),
+                (
+                    f"- 选择原因:"
+                    f"{hotel.selection_reason}"
+                ),
+            ]
+        )
+
+    def _append_days(
+        self,
+        lines: list[str],
+        plan: ItineraryPlan,
+    ) -> None:
+        """向输出文本追加每日行程段落。"""
+        lines.extend(
+            [
+                "",
+                "### 每日行程",
+            ]
+        )
+
+        for day in sorted(
+            plan.days,
+            key=lambda item: item.day_index,
+        ):
+            lines.extend(
+                [
+                    "",
+                    (
+                        f"#### 第{day.day_index}天|"
+                        f"{day.date}|{day.theme}"
+                    ),
+                    "",
+                ]
+            )
+
+            activities = sorted(
+                day.activities,
+                key=lambda item: (
+                    item.start_time,
+                    item.sequence,
+                ),
+            )
+
+            for activity in activities:
+                activity_label = (
+                    ACTIVITY_LABELS.get(
+                        activity.activity_type,
+                        activity.activity_type,
+                    )
+                )
+
+                activity_line = (
+                    f"- {activity.start_time.strftime('%H:%M')}"
+                    f"–{activity.end_time.strftime('%H:%M')}"
+                    f"|{activity_label}|"
+                    f"{activity.name}"
+                )
+
+                lines.append(activity_line)
+
+                details: list[str] = []
+
+                if activity.address:
+                    details.append(
+                        f"地址:{activity.address}"
+                    )
+
+                if (
+                    activity.transport_mode
+                    and activity.transport_mode
+                    != "unknown"
+                ):
+                    details.append(
+                        "交通方式:"
+                        f"{activity.transport_mode}"
+                    )
+
+                if (
+                    activity
+                    .estimated_transport_minutes
+                    is not None
+                ):
+                    details.append(
+                        "预计交通:"
+                        f"{activity.estimated_transport_minutes}"
+                        "分钟"
+                    )
+
+                if activity.notes:
+                    details.append(
+                        f"说明:{activity.notes}"
+                    )
+
+                if details:
+                    lines.append(
+                        "  - " + ";".join(details)
+                    )
+
+            for note in day.daily_notes:
+                lines.append(f"- 当日提示:{note}")
+
+    def _append_budget(
+        self,
+        lines: list[str],
+        validation: PlanValidationResult,
+    ) -> None:
+        """向输出文本追加预算估算段落。"""
+        budget = validation.budget
+
+        lines.extend(
+            [
+                "",
+                "### 已知预算估算",
+                "",
+                (
+                    "- 航班接口报价:"
+                    + self._money(
+                        budget.flight_quote,
+                        budget.currency,
+                    )
+                ),
+                (
+                    "- 酒店估算:"
+                    + self._money(
+                        budget.hotel_estimate,
+                        budget.currency,
+                    )
+                ),
+                (
+                    "- 已明确的活动费用:"
+                    + self._money(
+                        budget.explicit_activity_costs,
+                        budget.currency,
+                    )
+                ),
+                (
+                    "- 当前已知费用合计:"
+                    + self._money(
+                        budget.known_total,
+                        budget.currency,
+                    )
+                ),
+                (
+                    "- 用户预算:"
+                    + self._money(
+                        budget.user_budget,
+                        budget.currency,
+                    )
+                ),
+                (
+                    "- 当前已知预算余额:"
+                    + self._money(
+                        budget.remaining_known_budget,
+                        budget.currency,
+                    )
+                ),
+                "",
+                f"> {budget.coverage_note}",
+            ]
+        )
+
+    def _append_review(
+        self,
+        lines: list[str],
+        review: TripReviewResult,
+    ) -> None:
+        """向输出文本追加独立审查结论段落。"""
+        lines.extend(
+            [
+                "",
+                "### 独立审查结论",
+                "",
+                f"- {review.summary}",
+            ]
+        )
+
+        if review.strengths:
+            lines.append("- 方案优点:")
+
+            for strength in review.strengths:
+                lines.append(
+                    f"  - {strength}"
+                )
+
+        if review.recommendations:
+            lines.append("- 出行前建议:")
+
+            for recommendation in (
+                review.recommendations
+            ):
+                lines.append(
+                    f"  - {recommendation}"
+                )
+
+    def _collect_warnings(
+        self,
+        *,
+        plan: ItineraryPlan,
+        validation: PlanValidationResult,
+        review: TripReviewResult,
+        approved: bool,
+    ) -> list[str]:
+        """汇总方案、校验和审查三个阶段的警告信息并去重。"""
+        warnings: list[str] = []
+
+        warnings.extend(plan.warnings)
+
+        for issue in validation.issues:
+            if (
+                issue.severity == "warning"
+                or not approved
+            ):
+                warnings.append(issue.message)
+
+        for issue in review.issues:
+            if (
+                issue.severity == "warning"
+                or not approved
+            ):
+                warnings.append(issue.message)
+
+        if not approved:
+            warnings.extend(
+                review.revision_feedback
+            )
+
+        result: list[str] = []
+
+        for warning in warnings:
+            normalized = warning.strip()
+
+            if (
+                normalized
+                and normalized not in result
+            ):
+                result.append(normalized)
+
+        return result
+
+    @staticmethod
+    def _money(
+        value: float | None,
+        currency: str,
+    ) -> str:
+        """把金额(float|None)和货币代码格式化为"数值 货币"的中文展示文本。"""
+        if value is None:
+            return "未知"
+
+        return f"{value:.0f} {currency}"

+ 1442 - 0
app/services/plan_validation_service.py

@@ -0,0 +1,1442 @@
+from __future__ import annotations
+
+"""方案校验服务:用确定性规则校验行程方案的航班/酒店/活动安排/预算合理性,产出结构化问题清单与预算估算。不调用大模型。"""
+
+from datetime import datetime, timedelta
+from typing import Any
+
+from app.schemas.itinerary import (
+    ItineraryActivity,
+    ItineraryPlan,
+)
+from app.schemas.place import (
+    MapResearchResult,
+)
+from app.schemas.route import (
+    RouteEvaluationResult,
+)
+from app.schemas.selection import (
+    CandidateSelectionResult,
+)
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+from app.schemas.validation import (
+    BudgetEstimate,
+    PlanValidationResult,
+    ValidationIssue,
+)
+
+
+class PlanValidationService:
+    """对Planner生成的行程进行确定性校验。
+
+    不调用大模型,不调用外部API。
+    """
+
+    def validate(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        selection: CandidateSelectionResult,
+        map_result: MapResearchResult,
+        route_result: RouteEvaluationResult,
+    ) -> PlanValidationResult:
+        """校验完整行程方案:验证航班与酒店ID是否来自候选列表、活动地点是否存在、预算是否合理,返回校验结果与问题清单。"""
+        issues: list[ValidationIssue] = []
+
+        flight_data = self._validate_flight(
+            plan=plan,
+            selection=selection,
+            issues=issues,
+        )
+
+        hotel_data = self._validate_hotel(
+            request=request,
+            plan=plan,
+            selection=selection,
+            route_result=route_result,
+            issues=issues,
+        )
+
+        self._validate_days(
+            request=request,
+            plan=plan,
+            issues=issues,
+        )
+
+        self._validate_activities(
+            request=request,
+            plan=plan,
+            map_result=map_result,
+            issues=issues,
+        )
+
+        self._validate_boundary_days(
+            request=request,
+            plan=plan,
+            flight_data=flight_data,
+            issues=issues,
+        )
+
+        budget = self._build_budget(
+            request=request,
+            plan=plan,
+            flight_quote=flight_data.get(
+                "price"
+            ),
+            hotel_estimate=hotel_data.get(
+                "hotel_estimate"
+            ),
+            issues=issues,
+        )
+
+        error_issues = [
+            issue
+            for issue in issues
+            if issue.severity == "error"
+        ]
+
+        warning_issues = [
+            issue
+            for issue in issues
+            if issue.severity == "warning"
+        ]
+
+        revision_feedback = [
+            issue.message
+            for issue in error_issues
+        ]
+
+        return PlanValidationResult(
+            is_valid=not error_issues,
+            error_count=len(error_issues),
+            warning_count=len(warning_issues),
+            issues=issues,
+            budget=budget,
+            revision_feedback=revision_feedback,
+        )
+
+    def _validate_flight(
+        self,
+        *,
+        plan: ItineraryPlan,
+        selection: CandidateSelectionResult,
+        issues: list[ValidationIssue],
+    ) -> dict[str, Any]:
+        """确认Planner选择的航班来自真实候选。"""
+
+        selected = plan.selected_flight
+
+        if (
+            selected.flight_type
+            != selection.flight_type
+        ):
+            self._add_issue(
+                issues,
+                code="FLIGHT_TYPE_MISMATCH",
+                severity="error",
+                message=(
+                    "Planner选择的航班类型与"
+                    "候选筛选结果不一致。"
+                ),
+            )
+
+        if selected.flight_type == "round_trip":
+            combination = next(
+                (
+                    option
+                    for option
+                    in selection.round_trip_options
+                    if option.combination_id
+                    == selected.combination_id
+                ),
+                None,
+            )
+
+            if combination is None:
+                self._add_issue(
+                    issues,
+                    code=(
+                        "FLIGHT_COMBINATION_NOT_FOUND"
+                    ),
+                    severity="error",
+                    message=(
+                        "Planner选择的往返航班组合"
+                        "不存在于真实候选中:"
+                        f"{selected.combination_id}"
+                    ),
+                )
+
+                return {
+                    "price": None,
+                    "outbound": {},
+                    "return_flight": {},
+                }
+
+            outbound_id = str(
+                combination.outbound.get(
+                    "option_id",
+                    "",
+                )
+            )
+
+            return_id = str(
+                combination.return_flight.get(
+                    "option_id",
+                    "",
+                )
+            )
+
+            if (
+                outbound_id
+                != selected.outbound_option_id
+            ):
+                self._add_issue(
+                    issues,
+                    code=(
+                        "OUTBOUND_FLIGHT_ID_MISMATCH"
+                    ),
+                    severity="error",
+                    message=(
+                        "Planner填写的去程航班ID"
+                        "与所选往返组合不一致。"
+                    ),
+                )
+
+            if (
+                return_id
+                != selected.return_option_id
+            ):
+                self._add_issue(
+                    issues,
+                    code=(
+                        "RETURN_FLIGHT_ID_MISMATCH"
+                    ),
+                    severity="error",
+                    message=(
+                        "Planner填写的返程航班ID"
+                        "与所选往返组合不一致。"
+                    ),
+                )
+
+            if (
+                selected.quoted_price is not None
+                and combination.quoted_price
+                is not None
+                and not self._numbers_close(
+                    selected.quoted_price,
+                    combination.quoted_price,
+                )
+            ):
+                self._add_issue(
+                    issues,
+                    code="FLIGHT_PRICE_MISMATCH",
+                    severity="warning",
+                    message=(
+                        "Planner填写的航班报价与"
+                        "真实候选接口报价不一致,"
+                        "预算将采用候选接口报价。"
+                    ),
+                )
+
+            return {
+                "price": combination.quoted_price,
+                "outbound": combination.outbound,
+                "return_flight": (
+                    combination.return_flight
+                ),
+            }
+
+        candidate = next(
+            (
+                item
+                for item
+                in selection.one_way_options
+                if item.option_id
+                == selected.outbound_option_id
+            ),
+            None,
+        )
+
+        if candidate is None:
+            self._add_issue(
+                issues,
+                code="ONE_WAY_FLIGHT_NOT_FOUND",
+                severity="error",
+                message=(
+                    "Planner选择的单程航班"
+                    "不存在于真实候选中。"
+                ),
+            )
+
+            return {
+                "price": None,
+                "outbound": {},
+                "return_flight": {},
+            }
+
+        candidate_price = self._to_float(
+            candidate.flight.get("price")
+        )
+
+        return {
+            "price": candidate_price,
+            "outbound": candidate.flight,
+            "return_flight": {},
+        }
+
+    def _validate_hotel(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        selection: CandidateSelectionResult,
+        route_result: RouteEvaluationResult,
+        issues: list[ValidationIssue],
+    ) -> dict[str, Any]:
+        """确认酒店真实存在,并重新计算住宿费用。"""
+
+        selected = plan.selected_hotel
+
+        evaluated_hotel = next(
+            (
+                item
+                for item
+                in route_result.evaluated_hotels
+                if item.hotel_id
+                == selected.hotel_id
+            ),
+            None,
+        )
+
+        ranked_hotel = next(
+            (
+                item
+                for item
+                in selection.hotels
+                if item.hotel_id
+                == selected.hotel_id
+            ),
+            None,
+        )
+
+        if (
+            evaluated_hotel is None
+            and ranked_hotel is None
+        ):
+            self._add_issue(
+                issues,
+                code="HOTEL_NOT_FOUND",
+                severity="error",
+                message=(
+                    "Planner选择的酒店不存在于"
+                    "真实酒店候选中:"
+                    f"{selected.hotel_id}"
+                ),
+            )
+
+            return {
+                "hotel_estimate": None,
+                "hotel": {},
+            }
+
+        source_hotel = (
+            evaluated_hotel.hotel
+            if evaluated_hotel is not None
+            else ranked_hotel.hotel
+        )
+
+        source_name = str(
+            source_hotel.get("name", "")
+        )
+
+        if (
+            source_name
+            and source_name != selected.name
+        ):
+            self._add_issue(
+                issues,
+                code="HOTEL_NAME_MISMATCH",
+                severity="error",
+                message=(
+                    "Planner填写的酒店名称与"
+                    "所选酒店ID对应名称不一致。"
+                ),
+            )
+
+        if (
+            request.departure_date is not None
+            and selected.check_in_date
+            != request.departure_date
+        ):
+            # 红眼航班(如20:50出发、次日00:05到达)
+            # 允许入住日期延后一天。
+            next_day = (
+                request.departure_date
+                + timedelta(days=1)
+            )
+
+            if selected.check_in_date != next_day:
+                self._add_issue(
+                    issues,
+                    code="HOTEL_CHECK_IN_MISMATCH",
+                    severity="error",
+                    message=(
+                        "酒店入住日期必须与"
+                        "旅行出发日期一致;"
+                        "红眼航班最多允许延后一天。"
+                    ),
+                )
+            else:
+                self._add_issue(
+                    issues,
+                    code="HOTEL_CHECK_IN_REDEYE",
+                    severity="warning",
+                    message=(
+                        "酒店入住日期比出发日期晚一天,"
+                        "疑似红眼航班。"
+                        "Validator已接受此日期,"
+                        "但请确认住宿晚数仍正确。"
+                    ),
+                )
+
+        if (
+            request.return_date is not None
+            and selected.check_out_date
+            != request.return_date
+        ):
+            self._add_issue(
+                issues,
+                code="HOTEL_CHECK_OUT_MISMATCH",
+                severity="error",
+                message=(
+                    "酒店退房日期必须与"
+                    "旅行返程日期一致。"
+                ),
+            )
+
+        expected_nights: int | None = None
+
+        if (
+            request.departure_date is not None
+            and request.return_date is not None
+        ):
+            expected_nights = (
+                request.return_date
+                - request.departure_date
+            ).days
+
+        elif request.nights is not None:
+            expected_nights = request.nights
+
+        if (
+            expected_nights is not None
+            and selected.nights
+            != expected_nights
+        ):
+            self._add_issue(
+                issues,
+                code="HOTEL_NIGHTS_MISMATCH",
+                severity="error",
+                message=(
+                    "酒店住宿晚数不正确:"
+                    f"应为{expected_nights}晚,"
+                    f"Planner填写为"
+                    f"{selected.nights}晚。"
+                ),
+            )
+
+        source_price_per_night = self._to_float(
+            source_hotel.get(
+                "price_per_night"
+            )
+        )
+
+        if source_price_per_night is None:
+            source_price_per_night = (
+                selected.price_per_night
+            )
+
+            self._add_issue(
+                issues,
+                code="HOTEL_PRICE_SOURCE_MISSING",
+                severity="warning",
+                message=(
+                    "酒店候选缺少可解析的"
+                    "每晚价格,预算暂时采用"
+                    "Planner填写的价格。"
+                ),
+            )
+
+        if (
+            source_price_per_night is not None
+            and selected.price_per_night
+            is not None
+            and not self._numbers_close(
+                source_price_per_night,
+                selected.price_per_night,
+            )
+        ):
+            self._add_issue(
+                issues,
+                code=(
+                    "HOTEL_PRICE_PER_NIGHT_MISMATCH"
+                ),
+                severity="warning",
+                message=(
+                    "Planner填写的每晚酒店价格"
+                    "与候选数据不一致,预算将采用"
+                    "候选价格重新计算。"
+                ),
+            )
+
+        hotel_estimate: float | None = None
+
+        if (
+            source_price_per_night is not None
+            and expected_nights is not None
+        ):
+            hotel_estimate = (
+                source_price_per_night
+                * expected_nights
+            )
+
+        if (
+            hotel_estimate is not None
+            and selected.estimated_total_price
+            is not None
+            and not self._numbers_close(
+                hotel_estimate,
+                selected.estimated_total_price,
+            )
+        ):
+            self._add_issue(
+                issues,
+                code="HOTEL_TOTAL_PRICE_MISMATCH",
+                severity="warning",
+                message=(
+                    "Planner填写的酒店总价估算"
+                    "不正确,已按每晚价格乘"
+                    "住宿晚数重新计算。"
+                ),
+            )
+
+        if (
+            route_result.selected_hotel_id
+            and selected.hotel_id
+            != route_result.selected_hotel_id
+        ):
+            self._add_issue(
+                issues,
+                code="NON_TOP_ROUTE_HOTEL",
+                severity="warning",
+                message=(
+                    "Planner未选择路线综合排名"
+                    "第一的酒店,需要Reviewer检查"
+                    "选择理由是否充分。"
+                ),
+            )
+
+        return {
+            "hotel_estimate": hotel_estimate,
+            "hotel": source_hotel,
+        }
+
+    def _validate_days(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        issues: list[ValidationIssue],
+    ) -> None:
+        """检查行程日期是否完整且连续。"""
+
+        if (
+            request.departure_date is None
+            or request.return_date is None
+        ):
+            return
+
+        expected_day_count = (
+            request.return_date
+            - request.departure_date
+        ).days + 1
+
+        expected_dates = [
+            request.departure_date
+            + timedelta(days=index)
+            for index
+            in range(expected_day_count)
+        ]
+
+        actual_dates = [
+            day.date
+            for day in plan.days
+        ]
+
+        actual_indexes = [
+            day.day_index
+            for day in plan.days
+        ]
+
+        if len(plan.days) != expected_day_count:
+            self._add_issue(
+                issues,
+                code="DAY_COUNT_MISMATCH",
+                severity="error",
+                message=(
+                    "行程天数不正确:"
+                    f"应生成{expected_day_count}天,"
+                    f"实际生成{len(plan.days)}天。"
+                ),
+            )
+
+        if actual_dates != expected_dates:
+            missing_dates = [
+                item.isoformat()
+                for item in expected_dates
+                if item not in actual_dates
+            ]
+
+            extra_dates = [
+                item.isoformat()
+                for item in actual_dates
+                if item not in expected_dates
+            ]
+
+            details: list[str] = []
+
+            if missing_dates:
+                details.append(
+                    "缺少日期:"
+                    + "、".join(missing_dates)
+                )
+
+            if extra_dates:
+                details.append(
+                    "多余日期:"
+                    + "、".join(extra_dates)
+                )
+
+            self._add_issue(
+                issues,
+                code="DAY_DATE_MISMATCH",
+                severity="error",
+                message=(
+                    "每日行程日期没有完整覆盖"
+                    "旅行期间。"
+                    + (
+                        " ".join(details)
+                        if details
+                        else ""
+                    )
+                ),
+            )
+
+        expected_indexes = list(
+            range(1, len(plan.days) + 1)
+        )
+
+        if actual_indexes != expected_indexes:
+            self._add_issue(
+                issues,
+                code="DAY_INDEX_MISMATCH",
+                severity="error",
+                message=(
+                    "每日行程的day_index必须"
+                    "从1开始连续递增。"
+                ),
+            )
+
+    def _validate_activities(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        map_result: MapResearchResult,
+        issues: list[ValidationIssue],
+    ) -> None:
+        """检查时间冲突、POI真实性和旅行节奏。"""
+
+        attractions_by_id = {
+            place.poi_id: place
+            for place in map_result.attractions
+            if place.poi_id
+        }
+
+        attractions_by_name = {
+            place.name: place
+            for place in map_result.attractions
+        }
+
+        restaurants_by_id = {
+            place.poi_id: place
+            for place in map_result.restaurants
+            if place.poi_id
+        }
+
+        restaurants_by_name = {
+            place.name: place
+            for place in map_result.restaurants
+        }
+
+        used_attractions: set[str] = set()
+
+        pace_limits = {
+            "relaxed": 2,
+            "normal": 3,
+            "intensive": 4,
+        }
+
+        attraction_limit = pace_limits.get(
+            request.pace,
+            3,
+        )
+
+        for day in plan.days:
+            sequences = [
+                activity.sequence
+                for activity in day.activities
+            ]
+
+            if len(sequences) != len(set(sequences)):
+                self._add_issue(
+                    issues,
+                    code=(
+                        "DUPLICATE_ACTIVITY_SEQUENCE"
+                    ),
+                    severity="error",
+                    message=(
+                        f"第{day.day_index}天存在"
+                        "重复的活动sequence。"
+                    ),
+                    day_index=day.day_index,
+                )
+
+            expected_sequences = list(
+                range(1, len(day.activities) + 1)
+            )
+
+            if sequences != expected_sequences:
+                self._add_issue(
+                    issues,
+                    code="ACTIVITY_SEQUENCE_GAP",
+                    severity="warning",
+                    message=(
+                        f"第{day.day_index}天的活动"
+                        "sequence没有从1连续递增。"
+                    ),
+                    day_index=day.day_index,
+                )
+
+            ordered_activities = sorted(
+                day.activities,
+                key=lambda item: item.start_time,
+            )
+
+            previous: ItineraryActivity | None = None
+
+            attraction_count = 0
+
+            for activity in ordered_activities:
+                # 航班、交通类活动允许跨午夜
+                # (如 20:50->次日00:05)。
+                if (
+                    activity.end_time
+                    <= activity.start_time
+                ):
+                    is_cross_midnight = (
+                        activity.activity_type
+                        in {"flight", "transport"}
+                    )
+
+                    self._add_issue(
+                        issues,
+                        code=(
+                            "INVALID_ACTIVITY_TIME"
+                        ),
+                        severity=(
+                            "warning"
+                            if is_cross_midnight
+                            else "error"
+                        ),
+                        message=(
+                            f"第{day.day_index}天活动"
+                            f"'{activity.name}'的"
+                            "结束时间早于或等于开始时间"
+                            "(跨午夜活动请确认时长正确)。"
+                            if is_cross_midnight
+                            else (
+                                f"第{day.day_index}天活动"
+                                f"'{activity.name}'的"
+                                "结束时间必须晚于开始时间。"
+                            )
+                        ),
+                        day_index=day.day_index,
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+                if (
+                    previous is not None
+                    and activity.start_time
+                    < previous.end_time
+                    # 跨午夜活动的时间比较
+                    # 无法反映实际先后关系,跳过。
+                    and previous.end_time
+                    > previous.start_time
+                    and activity.end_time
+                    > activity.start_time
+                ):
+                    self._add_issue(
+                        issues,
+                        code="ACTIVITY_OVERLAP",
+                        severity="error",
+                        message=(
+                            f"第{day.day_index}天活动"
+                            f"'{previous.name}'与"
+                            f"'{activity.name}'"
+                            "存在时间重叠。"
+                        ),
+                        day_index=day.day_index,
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+                previous = activity
+
+                if (
+                    activity.activity_type
+                    == "attraction"
+                ):
+                    attraction_count += 1
+
+                    attraction_key = (
+                        activity.reference_id
+                        or activity.name
+                    )
+
+                    if attraction_key in used_attractions:
+                        self._add_issue(
+                            issues,
+                            code=(
+                                "DUPLICATE_ATTRACTION"
+                            ),
+                            severity="error",
+                            message=(
+                                f"景点'{activity.name}'"
+                                "在行程中被重复安排。"
+                            ),
+                            day_index=day.day_index,
+                            activity_sequence=(
+                                activity.sequence
+                            ),
+                        )
+
+                    used_attractions.add(
+                        attraction_key
+                    )
+
+                    self._validate_place_reference(
+                        activity=activity,
+                        places_by_id=(
+                            attractions_by_id
+                        ),
+                        places_by_name=(
+                            attractions_by_name
+                        ),
+                        place_label="景点",
+                        issues=issues,
+                        day_index=day.day_index,
+                    )
+
+                elif (
+                    activity.activity_type
+                    == "restaurant"
+                ):
+                    self._validate_place_reference(
+                        activity=activity,
+                        places_by_id=(
+                            restaurants_by_id
+                        ),
+                        places_by_name=(
+                            restaurants_by_name
+                        ),
+                        place_label="餐厅",
+                        issues=issues,
+                        day_index=day.day_index,
+                    )
+
+            if attraction_count > attraction_limit:
+                self._add_issue(
+                    issues,
+                    code="PACE_LIMIT_EXCEEDED",
+                    severity="warning",
+                    message=(
+                        f"第{day.day_index}天安排了"
+                        f"{attraction_count}个主要景点,"
+                        f"超过{request.pace}节奏建议的"
+                        f"{attraction_limit}个。"
+                    ),
+                    day_index=day.day_index,
+                )
+
+    def _validate_place_reference(
+        self,
+        *,
+        activity: ItineraryActivity,
+        places_by_id: dict[str, Any],
+        places_by_name: dict[str, Any],
+        place_label: str,
+        issues: list[ValidationIssue],
+        day_index: int,
+    ) -> None:
+        """确认活动引用的POI来自高德查询结果。"""
+
+        if activity.reference_id:
+            place = places_by_id.get(
+                activity.reference_id
+            )
+
+            if place is None:
+                self._add_issue(
+                    issues,
+                    code=f"UNKNOWN_{place_label}_ID",
+                    severity="error",
+                    message=(
+                        f"{place_label}"
+                        f"'{activity.name}'的POI ID"
+                        "不存在于高德查询候选中。"
+                    ),
+                    day_index=day_index,
+                    activity_sequence=(
+                        activity.sequence
+                    ),
+                )
+
+                return
+
+            if place.name != activity.name:
+                self._add_issue(
+                    issues,
+                    code=f"{place_label}_NAME_MISMATCH",
+                    severity="error",
+                    message=(
+                        f"{place_label}POI ID对应名称"
+                        f"为'{place.name}',"
+                        f"Planner却填写为"
+                        f"'{activity.name}'。"
+                    ),
+                    day_index=day_index,
+                    activity_sequence=(
+                        activity.sequence
+                    ),
+                )
+
+            return
+
+        if activity.name not in places_by_name:
+            self._add_issue(
+                issues,
+                code=f"UNKNOWN_{place_label}_NAME",
+                severity="error",
+                message=(
+                    f"{place_label}"
+                    f"'{activity.name}'"
+                    "不存在于高德查询候选中。"
+                ),
+                day_index=day_index,
+                activity_sequence=(
+                    activity.sequence
+                ),
+            )
+
+    def _validate_boundary_days(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        flight_data: dict[str, Any],
+        issues: list[ValidationIssue],
+    ) -> None:
+        """检查到达日和返程日安排是否合理。"""
+
+        outbound = flight_data.get(
+            "outbound",
+            {},
+        )
+
+        return_flight = flight_data.get(
+            "return_flight",
+            {},
+        )
+
+        if not isinstance(outbound, dict):
+            outbound = {}
+
+        if not isinstance(return_flight, dict):
+            return_flight = {}
+
+        outbound_departure = self._parse_datetime(
+            outbound.get("departure_time")
+        )
+
+        outbound_arrival = self._parse_datetime(
+            outbound.get("arrival_time")
+        )
+
+        return_departure = self._parse_datetime(
+            return_flight.get(
+                "departure_time"
+            )
+        )
+
+        # 检查去程日期。
+        if (
+            outbound_departure is not None
+            and request.departure_date is not None
+            and outbound_departure.date()
+            != request.departure_date
+        ):
+            self._add_issue(
+                issues,
+                code="OUTBOUND_DATE_MISMATCH",
+                severity="error",
+                message=(
+                    "所选去程航班日期与"
+                    "用户出发日期不一致。"
+                ),
+            )
+
+        # 检查返程日期。
+        if (
+            return_departure is not None
+            and request.return_date is not None
+            and return_departure.date()
+            != request.return_date
+        ):
+            self._add_issue(
+                issues,
+                code="RETURN_DATE_MISMATCH",
+                severity="error",
+                message=(
+                    "所选返程航班日期与"
+                    "用户返程日期不一致。"
+                ),
+            )
+
+        departure_day = next(
+            (
+                day
+                for day in plan.days
+                if day.date
+                == request.departure_date
+            ),
+            None,
+        )
+
+        return_day = next(
+            (
+                day
+                for day in plan.days
+                if day.date
+                == request.return_date
+            ),
+            None,
+        )
+
+        if departure_day is not None:
+            has_outbound_flight = any(
+                activity.activity_type
+                == "flight"
+                for activity
+                in departure_day.activities
+            )
+
+            if not has_outbound_flight:
+                self._add_issue(
+                    issues,
+                    code=(
+                        "OUTBOUND_FLIGHT_ACTIVITY_MISSING"
+                    ),
+                    severity="warning",
+                    message=(
+                        "到达日行程中没有明确记录"
+                        "去程航班活动。"
+                    ),
+                    day_index=(
+                        departure_day.day_index
+                    ),
+                )
+
+        if (
+            departure_day is not None
+            and outbound_arrival is not None
+            and outbound_arrival.date()
+            == departure_day.date
+        ):
+            # 默认预留90分钟完成下机、
+            # 取行李和离开机场。
+            recommended_ready_time = (
+                outbound_arrival
+                + timedelta(minutes=90)
+            )
+
+            for activity in (
+                departure_day.activities
+            ):
+                if activity.activity_type in {
+                    "flight",
+                    "transport",
+                }:
+                    continue
+
+                activity_start = datetime.combine(
+                    departure_day.date,
+                    activity.start_time,
+                )
+
+                if activity_start < outbound_arrival:
+                    self._add_issue(
+                        issues,
+                        code=(
+                            "ACTIVITY_BEFORE_ARRIVAL"
+                        ),
+                        severity="error",
+                        message=(
+                            f"到达日活动"
+                            f"'{activity.name}'"
+                            "安排在航班到达之前。"
+                        ),
+                        day_index=(
+                            departure_day.day_index
+                        ),
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+                elif (
+                    activity_start
+                    < recommended_ready_time
+                ):
+                    self._add_issue(
+                        issues,
+                        code=(
+                            "ARRIVAL_BUFFER_TOO_SHORT"
+                        ),
+                        severity="warning",
+                        message=(
+                            f"到达日活动"
+                            f"'{activity.name}'"
+                            "距离航班到达时间过近,"
+                            "建议预留下机、行李和"
+                            "机场离场时间。"
+                        ),
+                        day_index=(
+                            departure_day.day_index
+                        ),
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+        if return_day is not None:
+            has_return_flight = any(
+                activity.activity_type
+                == "flight"
+                for activity
+                in return_day.activities
+            )
+
+            if (
+                return_flight
+                and not has_return_flight
+            ):
+                self._add_issue(
+                    issues,
+                    code=(
+                        "RETURN_FLIGHT_ACTIVITY_MISSING"
+                    ),
+                    severity="warning",
+                    message=(
+                        "返程日行程中没有明确记录"
+                        "返程航班活动。"
+                    ),
+                    day_index=return_day.day_index,
+                )
+
+        if (
+            return_day is not None
+            and return_departure is not None
+            and return_departure.date()
+            == return_day.date
+        ):
+            # 默认国内航班提前180分钟结束
+            # 景点、餐饮等非交通活动。
+            recommended_leave_time = (
+                return_departure
+                - timedelta(minutes=180)
+            )
+
+            for activity in return_day.activities:
+                if activity.activity_type in {
+                    "flight",
+                    "transport",
+                }:
+                    continue
+
+                activity_end = datetime.combine(
+                    return_day.date,
+                    activity.end_time,
+                )
+
+                if activity_end > return_departure:
+                    self._add_issue(
+                        issues,
+                        code=(
+                            "ACTIVITY_AFTER_FLIGHT"
+                        ),
+                        severity="error",
+                        message=(
+                            f"返程日活动"
+                            f"'{activity.name}'"
+                            "结束时间晚于返程航班"
+                            "起飞时间。"
+                        ),
+                        day_index=return_day.day_index,
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+                elif (
+                    activity_end
+                    > recommended_leave_time
+                ):
+                    self._add_issue(
+                        issues,
+                        code=(
+                            "RETURN_BUFFER_TOO_SHORT"
+                        ),
+                        severity="warning",
+                        message=(
+                            f"返程日活动"
+                            f"'{activity.name}'"
+                            "距离航班起飞时间较近,"
+                            "可能没有充分预留"
+                            "前往机场和办理登机的时间。"
+                        ),
+                        day_index=return_day.day_index,
+                        activity_sequence=(
+                            activity.sequence
+                        ),
+                    )
+
+    def _build_budget(
+        self,
+        *,
+        request: TravelRequest,
+        plan: ItineraryPlan,
+        flight_quote: float | None,
+        hotel_estimate: float | None,
+        issues: list[ValidationIssue],
+    ) -> BudgetEstimate:
+        """计算目前能够确定的旅行费用。"""
+
+        explicit_activity_costs = 0.0
+
+        for day in plan.days:
+            for activity in day.activities:
+                if activity.activity_type in {
+                    "flight",
+                    "hotel_check_in",
+                    "hotel_check_out",
+                }:
+                    # 航班和酒店已经单独统计,
+                    # 防止重复计费。
+                    continue
+
+                if activity.estimated_cost is not None:
+                    explicit_activity_costs += (
+                        activity.estimated_cost
+                    )
+
+        known_components: list[float] = []
+
+        if flight_quote is not None:
+            known_components.append(flight_quote)
+        else:
+            self._add_issue(
+                issues,
+                code="FLIGHT_PRICE_UNKNOWN",
+                severity="warning",
+                message=(
+                    "所选航班缺少可解析报价,"
+                    "无法完整估算预算。"
+                ),
+            )
+
+        if hotel_estimate is not None:
+            known_components.append(
+                hotel_estimate
+            )
+        else:
+            self._add_issue(
+                issues,
+                code="HOTEL_PRICE_UNKNOWN",
+                severity="warning",
+                message=(
+                    "所选酒店缺少可解析价格,"
+                    "无法完整估算预算。"
+                ),
+            )
+
+        known_components.append(
+            explicit_activity_costs
+        )
+
+        known_total = (
+            round(sum(known_components), 2)
+            if known_components
+            else None
+        )
+
+        user_budget = request.total_budget
+
+        remaining_known_budget: (
+            float | None
+        ) = None
+
+        status = "unknown"
+
+        if (
+            user_budget is not None
+            and known_total is not None
+        ):
+            remaining_known_budget = round(
+                user_budget - known_total,
+                2,
+            )
+
+            if known_total > user_budget:
+                status = "over_budget"
+
+                self._add_issue(
+                    issues,
+                    code="KNOWN_BUDGET_EXCEEDED",
+                    severity="error",
+                    message=(
+                        "当前可确定费用已超过"
+                        f"用户总预算:已知费用"
+                        f"{known_total:.0f}"
+                        f"{request.currency},"
+                        f"预算为"
+                        f"{user_budget:.0f}"
+                        f"{request.currency}。"
+                    ),
+                )
+
+            else:
+                status = "within_known_budget"
+
+        coverage_note = (
+            "当前预算仅包含航班接口报价、"
+            "酒店估算和行程中明确填写的活动费用;"
+            "未明确查询的餐饮、门票、市内交通、"
+            "保险及其他费用未计入。"
+            "航班接口报价的计价范围仍以"
+            "实际供应商页面为准。"
+        )
+
+        return BudgetEstimate(
+            currency=request.currency,
+            flight_quote=flight_quote,
+            hotel_estimate=hotel_estimate,
+            explicit_activity_costs=round(
+                explicit_activity_costs,
+                2,
+            ),
+            known_total=known_total,
+            user_budget=user_budget,
+            remaining_known_budget=(
+                remaining_known_budget
+            ),
+            status=status,
+            coverage_note=coverage_note,
+        )
+
+    @staticmethod
+    def _parse_datetime(
+        value: object,
+    ) -> datetime | None:
+        """安全解析时间字符串,解析失败返回None。"""
+        if value is None:
+            return None
+
+        if isinstance(value, datetime):
+            return value
+
+        try:
+            return datetime.fromisoformat(
+                str(value)
+            )
+        except ValueError:
+            return None
+
+    @staticmethod
+    def _to_float(
+        value: object,
+    ) -> float | None:
+        """安全转换为float,不可转换值返回None。"""
+        if value is None:
+            return None
+
+        if isinstance(value, bool):
+            return None
+
+        try:
+            return float(value)
+        except (TypeError, ValueError):
+            return None
+
+    @staticmethod
+    def _numbers_close(
+        first: float,
+        second: float,
+    ) -> bool:
+        """允许1元或1%的价格误差。"""
+
+        tolerance = max(
+            1.0,
+            abs(second) * 0.01,
+        )
+
+        return abs(first - second) <= tolerance
+
+    @staticmethod
+    def _add_issue(
+        issues: list[ValidationIssue],
+        *,
+        code: str,
+        severity: str,
+        message: str,
+        day_index: int | None = None,
+        activity_sequence: int | None = None,
+    ) -> None:
+        """按严重级别(error/warning)向问题列表追加一条校验问题。"""
+        issues.append(
+            ValidationIssue(
+                code=code,
+                severity=severity,
+                message=message,
+                day_index=day_index,
+                activity_sequence=(
+                    activity_sequence
+                ),
+            )
+        )

+ 434 - 0
app/services/route_evaluation_service.py

@@ -0,0 +1,434 @@
+from __future__ import annotations
+
+"""路线评估服务:对酒店按距离/地铁/评分做加权排序,使用高德距离或球面余弦公式计算路线衔接。不调用大模型。"""
+
+import math
+from statistics import mean
+from typing import Iterable
+
+from app.schemas.route import (
+    HotelRouteEvaluation,
+    RouteLeg,
+)
+from app.schemas.selection import (
+    RankedHotelCandidate,
+)
+
+
+class RouteEvaluationService:
+    """根据真实距离重新评价酒店候选。
+
+    本服务不调用大模型和外部接口。
+    """
+
+    def rank_hotels(
+        self,
+        hotels: list[RankedHotelCandidate],
+        legs_by_hotel: dict[
+            str,
+            list[RouteLeg],
+        ],
+        subway_distance_by_hotel: dict[
+            str,
+            float | None,
+        ],
+        *,
+        require_near_subway: bool,
+        limit: int = 3,
+    ) -> list[HotelRouteEvaluation]:
+        """综合原酒店得分、景点距离和地铁距离。"""
+
+        average_distances: list[float] = []
+
+        average_distance_by_hotel: dict[
+            str,
+            float | None,
+        ] = {}
+
+        for hotel_candidate in hotels:
+            hotel_id = hotel_candidate.hotel_id
+
+            legs = legs_by_hotel.get(
+                hotel_id,
+                [],
+            )
+
+            distances = [
+                leg.distance_meters
+                for leg in legs
+            ]
+
+            average_distance = (
+                mean(distances)
+                if distances
+                else None
+            )
+
+            average_distance_by_hotel[
+                hotel_id
+            ] = average_distance
+
+            if average_distance is not None:
+                average_distances.append(
+                    average_distance
+                )
+
+        results: list[HotelRouteEvaluation] = []
+
+        for hotel_candidate in hotels:
+            hotel_id = hotel_candidate.hotel_id
+            hotel = hotel_candidate.hotel
+
+            legs = legs_by_hotel.get(
+                hotel_id,
+                [],
+            )
+
+            distances = [
+                leg.distance_meters
+                for leg in legs
+            ]
+
+            durations = [
+                leg.duration_seconds
+                for leg in legs
+                if leg.duration_seconds is not None
+            ]
+
+            average_distance = (
+                mean(distances)
+                if distances
+                else None
+            )
+
+            maximum_distance = (
+                max(distances)
+                if distances
+                else None
+            )
+
+            average_duration = (
+                mean(durations)
+                if durations
+                else None
+            )
+
+            centrality_score = self._inverse_score(
+                average_distance,
+                average_distances,
+            )
+
+            subway_distance = (
+                subway_distance_by_hotel.get(
+                    hotel_id
+                )
+            )
+
+            subway_score = self._subway_score(
+                subway_distance
+            )
+
+            base_score = hotel_candidate.score
+
+            if require_near_subway:
+                final_score = (
+                    base_score * 0.45
+                    + centrality_score * 0.40
+                    + subway_score * 0.15
+                )
+            else:
+                final_score = (
+                    base_score * 0.55
+                    + centrality_score * 0.45
+                )
+
+            reasons = list(
+                hotel_candidate.reasons
+            )
+
+            warnings = list(
+                hotel_candidate.warnings
+            )
+
+            if (
+                average_distance is not None
+                and centrality_score >= 70
+            ):
+                reasons.append(
+                    "到主要景点的平均距离较短"
+                )
+
+            if (
+                average_duration is not None
+                and centrality_score >= 70
+            ):
+                reasons.append(
+                    "前往主要景点的预计耗时较低"
+                )
+
+            if require_near_subway:
+                if (
+                    subway_distance is not None
+                    and subway_distance <= 800
+                ):
+                    reasons.append(
+                        "周边800米内查询到地铁站"
+                    )
+
+                elif subway_distance is None:
+                    warnings.append(
+                        "未获得可解析的地铁站距离"
+                    )
+
+                else:
+                    warnings.append(
+                        "距离最近地铁站超过"
+                        f"{subway_distance:.0f}米"
+                    )
+
+            if not legs:
+                warnings.append(
+                    "酒店缺少有效路线距离结果"
+                )
+
+            hotel_name = str(
+                hotel.get("name")
+                or hotel_candidate.hotel_id
+            )
+
+            results.append(
+                HotelRouteEvaluation(
+                    hotel_id=hotel_id,
+                    hotel_name=hotel_name,
+                    final_score=round(
+                        min(final_score, 100.0),
+                        2,
+                    ),
+                    base_hotel_score=(
+                        hotel_candidate.score
+                    ),
+                    centrality_score=round(
+                        centrality_score,
+                        2,
+                    ),
+                    subway_score=round(
+                        subway_score,
+                        2,
+                    ),
+                    average_distance_meters=(
+                        round(average_distance, 2)
+                        if average_distance
+                        is not None
+                        else None
+                    ),
+                    maximum_distance_meters=(
+                        round(maximum_distance, 2)
+                        if maximum_distance
+                        is not None
+                        else None
+                    ),
+                    average_duration_seconds=(
+                        round(average_duration, 2)
+                        if average_duration
+                        is not None
+                        else None
+                    ),
+                    nearest_subway_distance_meters=(
+                        round(subway_distance, 2)
+                        if subway_distance
+                        is not None
+                        else None
+                    ),
+                    route_legs=legs,
+                    reasons=self._unique(reasons),
+                    warnings=self._unique(
+                        warnings
+                    ),
+                    hotel=hotel,
+                )
+            )
+
+        results.sort(
+            key=lambda item: (
+                -item.final_score,
+                (
+                    item.average_distance_meters
+                    if item.average_distance_meters
+                    is not None
+                    else float("inf")
+                ),
+            )
+        )
+
+        return results[:limit]
+
+    def build_haversine_leg(
+        self,
+        *,
+        origin_name: str,
+        origin_location: str,
+        destination_name: str,
+        destination_location: str,
+    ) -> RouteLeg:
+        """高德距离查询失败时的直线距离降级。"""
+
+        distance = self.haversine_distance_meters(
+            origin_location,
+            destination_location,
+        )
+
+        return RouteLeg(
+            origin_name=origin_name,
+            origin_location=origin_location,
+            destination_name=destination_name,
+            destination_location=(
+                destination_location
+            ),
+            distance_meters=distance,
+            duration_seconds=None,
+            source="haversine_fallback",
+        )
+
+    @classmethod
+    def haversine_distance_meters(
+        cls,
+        first_location: str,
+        second_location: str,
+    ) -> float:
+        """根据两组经纬度计算球面直线距离。"""
+
+        first_lon, first_lat = (
+            cls.parse_location(first_location)
+        )
+
+        second_lon, second_lat = (
+            cls.parse_location(second_location)
+        )
+
+        earth_radius = 6_371_000.0
+
+        lat1 = math.radians(first_lat)
+        lat2 = math.radians(second_lat)
+
+        delta_lat = math.radians(
+            second_lat - first_lat
+        )
+
+        delta_lon = math.radians(
+            second_lon - first_lon
+        )
+
+        value = (
+            math.sin(delta_lat / 2) ** 2
+            + math.cos(lat1)
+            * math.cos(lat2)
+            * math.sin(delta_lon / 2) ** 2
+        )
+
+        central_angle = 2 * math.atan2(
+            math.sqrt(value),
+            math.sqrt(1 - value),
+        )
+
+        return round(
+            earth_radius * central_angle,
+            2,
+        )
+
+    @staticmethod
+    def parse_location(
+        location: str,
+    ) -> tuple[float, float]:
+        """解析高德使用的 经度,纬度 格式。"""
+
+        parts = [
+            part.strip()
+            for part in location.split(",")
+        ]
+
+        if len(parts) != 2:
+            raise ValueError(
+                "坐标必须使用“经度,纬度”格式:"
+                f"{location}"
+            )
+
+        longitude = float(parts[0])
+        latitude = float(parts[1])
+
+        if not -180 <= longitude <= 180:
+            raise ValueError(
+                f"经度超出范围:{longitude}"
+            )
+
+        if not -90 <= latitude <= 90:
+            raise ValueError(
+                f"纬度超出范围:{latitude}"
+            )
+
+        return longitude, latitude
+
+    @staticmethod
+    def _inverse_score(
+        value: float | None,
+        values: Iterable[float],
+    ) -> float:
+        """将距离类数值反转为得分(越近得分越高)。"""
+        if value is None:
+            return 0.0
+
+        valid_values = list(values)
+
+        if not valid_values:
+            return 0.0
+
+        minimum = min(valid_values)
+        maximum = max(valid_values)
+
+        if maximum == minimum:
+            return 100.0
+
+        score = (
+            maximum - value
+        ) / (
+            maximum - minimum
+        ) * 100.0
+
+        return max(
+            0.0,
+            min(100.0, score),
+        )
+
+    @staticmethod
+    def _subway_score(
+        distance_meters: float | None,
+    ) -> float:
+        """根据酒店到最近地铁站的步行距离计算地铁便利得分。"""
+        if distance_meters is None:
+            return 0.0
+
+        if distance_meters <= 500:
+            return 100.0
+
+        if distance_meters <= 800:
+            return 85.0
+
+        if distance_meters <= 1200:
+            return 60.0
+
+        if distance_meters <= 2000:
+            return 30.0
+
+        return 10.0
+
+    @staticmethod
+    def _unique(
+        values: list[str],
+    ) -> list[str]:
+        """按元素值去重并保持首次出现顺序。"""
+        result: list[str] = []
+
+        for value in values:
+            if value and value not in result:
+                result.append(value)
+
+        return result

+ 920 - 0
app/services/travel_search_service.py

@@ -0,0 +1,920 @@
+from __future__ import annotations
+"""旅行搜索服务:封装SerpApiClient,把原始搜索响应规范化为Pydantic结果模型,提供统一的航班/酒店/机场搜索入口。"""
+import hashlib
+from datetime import datetime
+from typing import Any
+from app.clients.serpapi_client import SerpApiClient
+from app.schemas.common import SourceMetadata
+from app.schemas.flight import (
+    CarbonEmissions,
+    FlightOption,
+    FlightSearchQuery,
+    FlightSearchResult,
+    FlightSegment,
+    FlightSourceGroup,
+    Layover,
+)
+
+from app.schemas.hotel import (
+    GeoPoint,
+    HotelOption,
+    HotelPriceOffer,
+    HotelSearchQuery,
+    HotelSearchResult,
+)
+
+class TravelSearchService:
+    """
+    旅行资源查询服务
+    职责:
+    1.调用SerpApiClient 获取原始数据
+    2.合并不同结果分组
+    3.标准化字段
+    4.去除完成重复的候选
+    5.返回项目内部统一数据模型
+    不负责:
+    1.根据用户偏好筛选
+    2.对候选进行个性化排序
+    3.生成推荐理由
+    4.阶段候选数量。
+    """
+
+    def __init__(self,client:SerpApiClient)->None:
+        """初始化搜索服务,传入配置好的SerpApiClient实例。"""
+        self._client = client
+
+    async def search_flights(
+        self,
+        query: FlightSearchQuery,
+    ) -> FlightSearchResult:
+        """查询航班(单程或去程)。
+
+        单程搜索返回完整单程候选。
+        往返搜索首先返回去程候选。
+        """
+        raw_data = await self._client.search_flights_raw(query)
+
+        return self.normalize_flights_response(
+            query=query,
+            raw_data=raw_data,
+        )
+
+    async def search_return_flights(
+        self,
+        query: FlightSearchQuery,
+        departure_token: str,
+    ) -> FlightSearchResult:
+        """查询与选定去程匹配的返程候选。"""
+
+        raw_data = (
+            await self._client.search_return_flights_raw(
+                query=query,
+                departure_token=departure_token,
+            )
+        )
+
+        return self.normalize_flights_response(
+            query=query,
+            raw_data=raw_data,
+        )
+    
+    async def search_hotels(self, query: HotelSearchQuery) -> HotelSearchResult:
+        """按查询条件搜索酒店并对结果做规范化处理。"""
+        raw_data = await self._client.search_hotels_raw(query)
+
+        return self.normalize_hotels_response(query=query, raw_data=raw_data)
+    
+    @classmethod
+    def normalize_flights_response(cls,query:FlightSearchQuery,raw_data:dict[str,Any])->FlightSearchResult:
+        """
+        把SerpApi航班原始响应转换成统一结构。
+        best_flights和other_flights会全部进入标准化流程。
+        这里只去除完全重复的候选,不限制返回数量
+        """
+        normalized_flights:list[FlightOption]=[]
+        seen_option_ids:set[str]=set()
+        warnings:list[str]=[]
+
+        groups: tuple[
+            tuple[FlightSourceGroup,list[dict[str,Any]]],
+            ...,
+        ] = (
+            (
+                "best_flights",
+                raw_data.get("best_flights",[]),
+            ),
+            (
+                "other_flights",
+                raw_data.get("other_flights",[]),
+            )
+        )
+
+        for source_group, raw_options in groups:
+            if not isinstance(raw_options,list):
+                warnings.append(f"{source_group}不是列表,已忽略。")
+                continue
+
+            for provider_rank,raw_option in enumerate(
+                raw_options,
+                start=1
+            ):
+                if not isinstance(raw_option,dict):
+                    warnings.append(f"{source_group}第{provider_rank}条。不是有效对象,已忽略")
+                    continue
+
+                try:
+                    option = cls._normalize_flight_option(
+                        raw_option = raw_option,
+                        source_group = source_group,
+                        provider_rank = provider_rank,
+                        currency = query.currency,
+                        adults = query.adults,
+                    )
+                except (KeyError,TypeError,ValueError) as e:
+                    warnings.append(f"{source_group}第{provider_rank}条。航班解析失败:{e}")
+                    continue
+
+                if option.option_id in seen_option_ids:
+                    continue
+
+                seen_option_ids.add(option.option_id)
+                normalized_flights.append(option)
+
+        metadata = raw_data.get("search_metadata",{})
+
+        warnings.append(
+            "price 字段已按单人(每成人)票价标准化,原始 SerpApi 总价已除以 adults 人数"
+        )
+
+        return FlightSearchResult(
+            query=query,
+            flights=normalized_flights,
+            total_count=len(normalized_flights),
+            source=SourceMetadata(
+                provider="serpapi_google_flights",
+                search_id=metadata.get("id"),
+                status=metadata.get("status","Success"),
+                currency=query.currency,
+                is_cached=None
+            ),
+            price_insights=raw_data.get("price_insights"),
+            warnings=warnings
+        )
+    
+    @classmethod
+    def _normalize_flight_option(
+        cls,
+        raw_option:dict[str,Any],
+        source_group:FlightSourceGroup,
+        provider_rank:int,
+        currency:str,
+        adults:int = 1,
+        )->FlightOption:
+        """把单条航班原始dict规范化为Pydantic FlightOption。"""
+        raw_segments = raw_option.get("flights")
+
+        if not isinstance(raw_segments,list) or not raw_segments:
+            raise ValueError("缺少flights航段信息。")
+        
+        segments = [
+            cls._normalize_flight_segment(raw_segment)
+            for raw_segment in raw_segments
+        ]
+
+        raw_layovers = raw_option.get("layovers", [])
+
+        layovers = [
+            cls._normalize_layover(raw_layover)
+            for raw_layover in raw_layovers
+            if isinstance(raw_layover, dict)
+        ]
+
+        first_segment = segments[0]
+        last_segment = segments[-1]
+
+        total_duration = cls._to_int(
+            raw_option.get("total_duration")
+        )
+
+        if total_duration is None:
+            total_duration = sum(
+                segment.duration_minutes or 0
+                for segment in segments
+            ) + sum(
+                layover.duration_minutes or 0
+                for layover in layovers
+            )
+
+        airlines = cls._unique_strings(
+            segment.airline
+            for segment in segments
+            if segment.airline
+        )
+
+        price = cls._normalize_price_per_adult(
+            raw_option.get("price"),
+            adults=adults,
+        )
+
+        option_id = cls._build_flight_option_id(
+            segments=segments,
+            price=price,
+        )
+
+        raw_carbon = raw_option.get("carbon_emissions")
+
+        carbon_emissions = None
+
+        if isinstance(raw_carbon, dict):
+            carbon_emissions = CarbonEmissions(
+                this_flight_grams=cls._to_int(
+                    raw_carbon.get("this_flight")
+                ),
+                typical_for_route_grams=cls._to_int(
+                    raw_carbon.get(
+                        "typical_for_this_route"
+                    )
+                ),
+                difference_percent=cls._to_int(
+                    raw_carbon.get(
+                        "difference_percent"
+                    )
+                ),
+            )
+
+        return FlightOption(
+            option_id=option_id,
+            source_group=source_group,
+            provider_rank=provider_rank,
+            segments=segments,
+            layovers=layovers,
+            departure_airport_code=(
+                first_segment.departure_airport_code
+            ),
+            final_arrival_airport_code=(
+                last_segment.arrival_airport_code
+            ),
+            departure_time=first_segment.departure_time,
+            arrival_time=last_segment.arrival_time,
+            total_duration_minutes=total_duration,
+            stop_count=max(len(segments) - 1, 0),
+            price=price,
+            currency=currency,
+            flight_type=raw_option.get("type"),
+            airlines=airlines,
+            is_overnight=(
+                any(segment.overnight for segment in segments)
+                or any(layover.overnight for layover in layovers)
+            ),
+            carbon_emissions=carbon_emissions,
+            departure_token=raw_option.get(
+                "departure_token"
+            ),
+            booking_token=raw_option.get(
+                "booking_token"
+            ),
+        )
+
+    @classmethod
+    def _normalize_flight_segment(
+        cls,
+        raw_segment: dict[str, Any],
+    ) -> FlightSegment:
+        """规范化单个航段的起降信息。"""
+        if not isinstance(raw_segment, dict):
+            raise TypeError("航段不是有效对象。")
+
+        departure = raw_segment.get(
+            "departure_airport",
+            {},
+        )
+        arrival = raw_segment.get(
+            "arrival_airport",
+            {},
+        )
+
+        departure_code = str(
+            departure.get("id", "")
+        ).strip().upper()
+
+        arrival_code = str(
+            arrival.get("id", "")
+        ).strip().upper()
+
+        if not departure_code or not arrival_code:
+            raise ValueError("航段缺少机场代码。")
+
+        departure_time = cls._parse_local_datetime(
+            departure.get("time")
+        )
+        arrival_time = cls._parse_local_datetime(
+            arrival.get("time")
+        )
+
+        return FlightSegment(
+            flight_number=raw_segment.get(
+                "flight_number"
+            ),
+            airline=raw_segment.get("airline"),
+            departure_airport_code=departure_code,
+            departure_airport_name=departure.get("name"),
+            departure_time=departure_time,
+            arrival_airport_code=arrival_code,
+            arrival_airport_name=arrival.get("name"),
+            arrival_time=arrival_time,
+            duration_minutes=cls._to_int(
+                raw_segment.get("duration")
+            ),
+            airplane=raw_segment.get("airplane"),
+            travel_class=raw_segment.get(
+                "travel_class"
+            ),
+            legroom=raw_segment.get("legroom"),
+            overnight=bool(
+                raw_segment.get("overnight", False)
+            ),
+            often_delayed_by_over_30_min=bool(
+                raw_segment.get(
+                    "often_delayed_by_over_30_min",
+                    False,
+                )
+            ),
+            plane_and_crew_by=raw_segment.get(
+                "plane_and_crew_by"
+            ),
+            extensions=cls._string_list(
+                raw_segment.get("extensions")
+            ),
+            ticket_also_sold_by=cls._string_list(
+                raw_segment.get(
+                    "ticket_also_sold_by"
+                )
+            ),
+        )
+
+    @classmethod
+    def _normalize_layover(
+        cls,
+        raw_layover: dict[str, Any],
+    ) -> Layover:
+        """规范化中转停留信息。"""
+        airport_code = str(
+            raw_layover.get("id", "")
+        ).strip().upper()
+
+        if not airport_code:
+            raise ValueError("经停信息缺少机场代码。")
+
+        return Layover(
+            airport_code=airport_code,
+            airport_name=raw_layover.get("name"),
+            duration_minutes=cls._to_int(
+                raw_layover.get("duration")
+            ),
+            overnight=bool(
+                raw_layover.get("overnight", False)
+            ),
+        )
+
+    @classmethod
+    def normalize_hotels_response(
+        cls,
+        query: HotelSearchQuery,
+        raw_data: dict[str, Any],
+    ) -> HotelSearchResult:
+        """把酒店原始响应转换为统一结构。"""
+
+        raw_properties = raw_data.get("properties", [])
+
+        normalized_hotels: list[HotelOption] = []
+        seen_hotel_ids: set[str] = set()
+        warnings: list[str] = []
+
+        if not isinstance(raw_properties, list):
+            raw_properties = []
+            warnings.append(
+                "properties 不是列表,已按空结果处理。"
+            )
+
+        for provider_rank, raw_hotel in enumerate(
+            raw_properties,
+            start=1,
+        ):
+            if not isinstance(raw_hotel, dict):
+                warnings.append(
+                    f"第 {provider_rank} 条酒店不是有效对象,"
+                    "已忽略。"
+                )
+                continue
+
+            try:
+                hotel = cls._normalize_hotel_option(
+                    raw_hotel=raw_hotel,
+                    provider_rank=provider_rank,
+                    currency=query.currency,
+                )
+            except (KeyError, TypeError, ValueError) as exc:
+                warnings.append(
+                    f"第 {provider_rank} 条酒店解析失败:"
+                    f"{exc}"
+                )
+                continue
+
+            if hotel.hotel_id in seen_hotel_ids:
+                continue
+
+            seen_hotel_ids.add(hotel.hotel_id)
+            normalized_hotels.append(hotel)
+
+        metadata = raw_data.get("search_metadata", {})
+        pagination = raw_data.get(
+            "serpapi_pagination",
+            {},
+        )
+
+        return HotelSearchResult(
+            query=query,
+            hotels=normalized_hotels,
+            total_count=len(normalized_hotels),
+            source=SourceMetadata(
+                provider="serpapi_google_hotels",
+                search_id=metadata.get("id"),
+                status=metadata.get("status", "Success"),
+                currency=query.currency,
+                is_cached=None,
+            ),
+            next_page_token=(
+                pagination.get("next_page_token")
+                or raw_data.get("next_page_token")
+            ),
+            warnings=warnings,
+        )
+
+    @classmethod
+    def _normalize_hotel_option(
+        cls,
+        raw_hotel: dict[str, Any],
+        provider_rank: int,
+        currency: str,
+    ) -> HotelOption:
+        """把单条酒店原始dict规范化为Pydantic HotelOption。"""
+        name = str(raw_hotel.get("name", "")).strip()
+
+        if not name:
+            raise ValueError("酒店名称为空。")
+
+        raw_coordinates = raw_hotel.get(
+            "gps_coordinates",
+            {},
+        )
+
+        coordinates = None
+
+        if isinstance(raw_coordinates, dict):
+            latitude = cls._to_float(
+                raw_coordinates.get("latitude")
+            )
+            longitude = cls._to_float(
+                raw_coordinates.get("longitude")
+            )
+
+            if latitude is not None and longitude is not None:
+                coordinates = GeoPoint(
+                    latitude=latitude,
+                    longitude=longitude,
+                )
+
+        rate_per_night = raw_hotel.get(
+            "rate_per_night",
+            {},
+        )
+        total_rate = raw_hotel.get(
+            "total_rate",
+            {},
+        )
+
+        price_per_night = None
+        total_price = None
+
+        if isinstance(rate_per_night, dict):
+            price_per_night = cls._to_float(
+                rate_per_night.get(
+                    "extracted_lowest"
+                )
+            )
+
+        if isinstance(total_rate, dict):
+            total_price = cls._to_float(
+                total_rate.get(
+                    "extracted_lowest"
+                )
+            )
+
+        price_offers = cls._normalize_hotel_prices(
+            raw_hotel.get("prices", [])
+        )
+
+        property_token = raw_hotel.get(
+            "property_token"
+        )
+
+        hotel_id = (
+            str(property_token)
+            if property_token
+            else cls._stable_id(
+                prefix="hotel",
+                values=[
+                    name,
+                    coordinates.latitude
+                    if coordinates
+                    else "",
+                    coordinates.longitude
+                    if coordinates
+                    else "",
+                ],
+            )
+        )
+
+        images = raw_hotel.get("images", [])
+        thumbnail = raw_hotel.get("thumbnail")
+
+        if (
+            thumbnail is None
+            and isinstance(images, list)
+            and images
+            and isinstance(images[0], dict)
+        ):
+            thumbnail = images[0].get("thumbnail")
+
+        free_cancellation_raw = raw_hotel.get(
+            "free_cancellation"
+        )
+
+        free_cancellation = (
+            free_cancellation_raw
+            if isinstance(
+                free_cancellation_raw,
+                bool,
+            )
+            else None
+        )
+
+        return HotelOption(
+            hotel_id=hotel_id,
+            provider_rank=provider_rank,
+            name=name,
+            property_type=raw_hotel.get("type"),
+            description=raw_hotel.get("description"),
+            coordinates=coordinates,
+            check_in_time=raw_hotel.get(
+                "check_in_time"
+            ),
+            check_out_time=raw_hotel.get(
+                "check_out_time"
+            ),
+            hotel_class=cls._to_int(
+                raw_hotel.get(
+                    "extracted_hotel_class"
+                )
+            ),
+            overall_rating=cls._to_float(
+                raw_hotel.get("overall_rating")
+            ),
+            review_count=cls._to_int(
+                raw_hotel.get("reviews")
+            ),
+            location_rating=cls._to_float(
+                raw_hotel.get("location_rating")
+            ),
+            price_per_night=price_per_night,
+            total_price=total_price,
+            currency=currency,
+            amenities=cls._string_list(
+                raw_hotel.get("amenities")
+            ),
+            excluded_amenities=cls._string_list(
+                raw_hotel.get(
+                    "excluded_amenities"
+                )
+            ),
+            free_cancellation=free_cancellation,
+            sponsored=bool(
+                raw_hotel.get("sponsored", False)
+            ),
+            eco_certified=bool(
+                raw_hotel.get(
+                    "eco_certified",
+                    False,
+                )
+            ),
+            property_token=property_token,
+            thumbnail_url=thumbnail,
+            price_offers=price_offers,
+            deal=raw_hotel.get("deal"),
+            deal_description=raw_hotel.get(
+                "deal_description"
+            ),
+        )
+
+    @classmethod
+    def _normalize_hotel_prices(
+        cls,
+        raw_prices: Any,
+    ) -> list[HotelPriceOffer]:
+        """规范化酒店价格列表。"""
+        if not isinstance(raw_prices, list):
+            return []
+
+        offers: list[HotelPriceOffer] = []
+
+        for raw_price in raw_prices:
+            if not isinstance(raw_price, dict):
+                continue
+
+            raw_nightly = raw_price.get(
+                "rate_per_night",
+                {},
+            )
+            raw_total = raw_price.get(
+                "total_rate",
+                {},
+            )
+
+            nightly_price = None
+            total_price = None
+
+            if isinstance(raw_nightly, dict):
+                nightly_price = cls._to_float(
+                    raw_nightly.get(
+                        "extracted_lowest"
+                    )
+                )
+
+            if isinstance(raw_total, dict):
+                total_price = cls._to_float(
+                    raw_total.get(
+                        "extracted_lowest"
+                    )
+                )
+
+            free_cancellation_raw = raw_price.get(
+                "free_cancellation"
+            )
+
+            offers.append(
+                HotelPriceOffer(
+                    source=raw_price.get("source"),
+                    price_per_night=nightly_price,
+                    total_price=total_price,
+                    free_cancellation=(
+                        free_cancellation_raw
+                        if isinstance(
+                            free_cancellation_raw,
+                            bool,
+                        )
+                        else None
+                    ),
+                )
+            )
+
+        return offers
+
+    @staticmethod
+    def _build_flight_option_id(
+        segments: list[FlightSegment],
+        price: int | None,
+    ) -> str:
+        """根据关键字段生成稳定的航班选项ID。"""
+        values: list[Any] = [price]
+
+        for segment in segments:
+            values.extend(
+                [
+                    segment.flight_number,
+                    segment.departure_airport_code,
+                    segment.departure_time.isoformat(),
+                    segment.arrival_airport_code,
+                    segment.arrival_time.isoformat(),
+                ]
+            )
+
+        return TravelSearchService._stable_id(
+            prefix="flight",
+            values=values,
+        )
+
+    @staticmethod
+    def _stable_id(
+        prefix: str,
+        values: list[Any],
+    ) -> str:
+        """生成SHA256派生的稳定字符串ID。"""
+        raw_value = "|".join(
+            str(value)
+            for value in values
+        )
+
+        digest = hashlib.sha256(
+            raw_value.encode("utf-8")
+        ).hexdigest()[:20]
+
+        return f"{prefix}_{digest}"
+
+    @staticmethod
+    def _parse_local_datetime(
+        value: Any,
+    ) -> datetime:
+        """解析第三方接口返回的机场当地时间。
+
+        SerpApi 返回值没有明确时区偏移,
+        因此这里保留为无时区 datetime。
+        """
+
+        if not isinstance(value, str) or not value.strip():
+            raise ValueError("航班时间为空。")
+
+        try:
+            return datetime.fromisoformat(value.strip())
+        except ValueError as exc:
+            raise ValueError(
+                f"无法解析航班时间:{value}"
+            ) from exc
+
+    @staticmethod
+    def _to_int(value: Any) -> int | None:
+        """安全转换为int,不可转换返回None。"""
+        if value is None or isinstance(value, bool):
+            return None
+
+        try:
+            return int(value)
+        except (TypeError, ValueError):
+            return None
+
+    @staticmethod
+    def _normalize_price_per_adult(
+        raw_price: Any,
+        adults: int,
+    ) -> int | None:
+        """将 SerpApi 原始总价除以成人数,返回单人票价。
+
+        SerpApi / Google Flights 返回的 price 是所选乘客的
+        订单总价(含所有成人),这里统一折算为每成人单价,
+        避免前端误解为「一个人」或「全部人」的价格。
+        """
+        if raw_price is None or isinstance(raw_price, bool):
+            return None
+
+        if adults < 1:
+            adults = 1
+
+        try:
+            total = float(raw_price)
+        except (TypeError, ValueError):
+            return None
+
+        return round(total / adults)
+
+    @staticmethod
+    def _to_float(value: Any) -> float | None:
+        """安全转换为float,不可转换返回None。"""
+        if value is None or isinstance(value, bool):
+            return None
+
+        try:
+            return float(value)
+        except (TypeError, ValueError):
+            return None
+
+    @staticmethod
+    def _string_list(value: Any) -> list[str]:
+        """把字符串或列表统一转换为字符串列表。"""
+        if not isinstance(value, list):
+            return []
+
+        return [
+            str(item).strip()
+            for item in value
+            if str(item).strip()
+        ]
+
+    @staticmethod
+    def _unique_strings(
+        values: Any,
+    ) -> list[str]:
+        """字符串列表去重并保持顺序。"""
+        result: list[str] = []
+
+        for value in values:
+            text = str(value).strip()
+
+            if text and text not in result:
+                result.append(text)
+
+        return result
+    
+
+    async def search_airports(
+        self,
+        query: str,
+    ) -> dict[str, Any]:
+        """查询并标准化城市对应的机场信息。"""
+
+        raw_data = await self._client.search_airports_raw(
+            query=query,
+        )
+
+        raw_suggestions = raw_data.get(
+            "suggestions",
+            [],
+        )
+
+        suggestions: list[dict[str, Any]] = []
+
+        if not isinstance(raw_suggestions, list):
+            raw_suggestions = []
+
+        for raw_suggestion in raw_suggestions:
+            if not isinstance(raw_suggestion, dict):
+                continue
+
+            raw_airports = raw_suggestion.get(
+                "airports",
+                [],
+            )
+
+            airports: list[dict[str, Any]] = []
+
+            if isinstance(raw_airports, list):
+                for raw_airport in raw_airports:
+                    if not isinstance(
+                        raw_airport,
+                        dict,
+                    ):
+                        continue
+
+                    airport_code = str(
+                        raw_airport.get("id", "")
+                    ).strip().upper()
+
+                    if not airport_code:
+                        continue
+
+                    airports.append(
+                        {
+                            "code": airport_code,
+                            "name": raw_airport.get(
+                                "name"
+                            ),
+                            "city": raw_airport.get(
+                                "city"
+                            ),
+                            "city_id": raw_airport.get(
+                                "city_id"
+                            ),
+                            "distance": raw_airport.get(
+                                "distance"
+                            ),
+                        }
+                    )
+
+            suggestions.append(
+                {
+                    "position": raw_suggestion.get(
+                        "position"
+                    ),
+                    "name": raw_suggestion.get("name"),
+                    "type": raw_suggestion.get("type"),
+                    "description": (
+                        raw_suggestion.get(
+                            "description"
+                        )
+                    ),
+                    "location_id": raw_suggestion.get(
+                        "id"
+                    ),
+                    "airports": airports,
+                }
+            )
+
+        metadata = raw_data.get(
+            "search_metadata",
+            {},
+        )
+
+        return {
+            "query": query,
+            "total_count": len(suggestions),
+            "suggestions": suggestions,
+            "source": {
+                "provider": (
+                    "serpapi_google_flights_autocomplete"
+                ),
+                "search_id": metadata.get("id"),
+                "status": metadata.get(
+                    "status",
+                    "Success",
+                ),
+            },
+        }
+

+ 198 - 0
main.py

@@ -0,0 +1,198 @@
+from __future__ import annotations
+
+"""TravelMind CLI入口:读取用户旅行需求(支持多行输入),构建多角色规划图并执行完整工作流。"""
+
+import asyncio
+import logging
+import sys
+from time import perf_counter
+
+from rich.console import Console
+
+from app.graph.planner_builder import (
+    build_planner_graph,
+)
+from app.logging_config import (
+    configure_logging,
+    new_trace_id,
+)
+
+
+console = Console()
+
+logger = logging.getLogger(__name__)
+
+
+def get_user_query() -> str:
+    """从命令行参数或终端读取旅行需求(支持多行输入,空行结束)。"""
+
+    command_line_query = " ".join(
+        sys.argv[1:]
+    ).strip()
+
+    if command_line_query:
+        return command_line_query
+
+    console.print(
+        "[bold]请输入旅行需求"
+        "(支持多行,输入空行结束):[/bold]"
+    )
+
+    lines: list[str] = []
+    line_count = 0
+
+    while True:
+        prefix = "> " if line_count == 0 else "… "
+        line = input(prefix)
+
+        stripped = line.strip()
+
+        if not stripped:
+            if lines:
+                break
+            # 第一行就为空,继续等待。
+            continue
+
+        lines.append(stripped)
+        line_count += 1
+
+    return " ".join(lines)
+
+
+async def run_workflow(
+    user_query: str,
+) -> dict:
+    """执行完整TravelMind工作流。"""
+
+    graph = await build_planner_graph()
+
+    return await graph.ainvoke(
+        {
+            "user_query": user_query,
+            "errors": [],
+            "missing_fields": [],
+            "planning_attempts": 0,
+            "review_attempts": 0,
+            "revision_feedback": [],
+            "workflow_status": "running",
+        },
+        config={
+            # 图中存在重新规划循环。
+            "recursion_limit": 80,
+        },
+    )
+
+
+async def main() -> None:
+    """CLI主入口:配置日志、生成trace_id、读取用户需求并执行完整工作流。"""
+
+    configure_logging()
+
+    trace_id = new_trace_id()
+
+    user_query = get_user_query()
+
+    if not user_query:
+        console.print(
+            "[bold red]旅行需求不能为空。[/bold red]"
+        )
+        raise SystemExit(1)
+
+    console.rule(
+        "[bold blue]TravelMind 多角色旅行规划"
+    )
+
+    console.print(
+        f"[dim]trace_id:{trace_id}[/dim]"
+    )
+
+    started_at = perf_counter()
+
+    logger.info(
+        "TravelMind工作流开始,用户输入长度=%s",
+        len(user_query),
+    )
+
+    try:
+        result = await run_workflow(
+            user_query
+        )
+
+    except Exception as exc:
+        elapsed = perf_counter() - started_at
+
+        logger.exception(
+            "TravelMind工作流异常,"
+            "elapsed_seconds=%.2f",
+            elapsed,
+        )
+
+        console.print(
+            "[bold red]工作流执行失败:[/bold red]"
+            f"{type(exc).__name__}: {exc}"
+        )
+
+        raise SystemExit(1) from exc
+
+    elapsed = perf_counter() - started_at
+
+    workflow_status = result.get(
+        "workflow_status"
+    )
+
+    logger.info(
+        "TravelMind工作流结束,"
+        "status=%s,"
+        "planning_attempts=%s,"
+        "review_attempts=%s,"
+        "elapsed_seconds=%.2f",
+        workflow_status,
+        result.get("planning_attempts"),
+        result.get("review_attempts"),
+        elapsed,
+    )
+
+    final_answer = result.get(
+        "final_answer"
+    )
+
+    console.print()
+
+    if final_answer:
+        console.print(final_answer)
+    else:
+        console.print(
+            "[bold red]"
+            "系统没有生成最终回答。"
+            "[/bold red]"
+        )
+
+    console.rule("[bold]执行状态")
+
+    console.print(
+        {
+            "trace_id": trace_id,
+            "工作流状态": workflow_status,
+            "Planner执行次数": result.get(
+                "planning_attempts"
+            ),
+            "Reviewer执行次数": result.get(
+                "review_attempts"
+            ),
+            "Supervisor决策": result.get(
+                "supervisor_decision"
+            ),
+            "总耗时(秒)": round(
+                elapsed,
+                2,
+            ),
+            "系统错误": result.get(
+                "errors",
+                [],
+            ),
+        }
+    )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 0 - 0
mcp_servers/__init__.py


+ 328 - 0
mcp_servers/travel_search_server.py

@@ -0,0 +1,328 @@
+from __future__ import annotations
+
+"""自建旅行搜索MCP服务器:把TravelSearchService的航班/酒店搜索能力以MCP工具形式暴露,供LangChain Agent调用。"""
+
+from datetime import date, datetime
+from typing import Any
+
+from mcp.server.fastmcp import FastMCP
+
+from app.clients.serpapi_client import SerpApiClient, SerpApiError
+from app.config import get_settings
+from app.schemas.flight import FlightSearchQuery, FlightSearchResult
+from app.schemas.hotel import HotelSearchQuery, HotelSearchResult
+from app.services.travel_search_service import TravelSearchService
+
+mcp = FastMCP("travel-search")
+
+# ---------------------------------------------------------------------------
+# 辅助函数
+# ---------------------------------------------------------------------------
+
+
+def _parse_date(value: Any) -> date:
+    """将字符串或 date 对象统一转为 date。"""
+    if isinstance(value, date):
+        return value
+    if isinstance(value, str):
+        return date.fromisoformat(value.strip())
+    raise ValueError(f"无法解析日期: {value!r}")
+
+
+def _build_service() -> TravelSearchService:
+    """创建 Service(懒加载 API key,避免模块导入时崩溃)。"""
+    settings = get_settings()
+    client = SerpApiClient(
+        api_key=settings.require("serpapi_api_key"),
+        timeout_seconds=settings.request_timeout_seconds,
+    )
+    return TravelSearchService(client)
+
+
+# ---------------------------------------------------------------------------
+# MCP 工具
+# ---------------------------------------------------------------------------
+
+
+@mcp.tool()
+async def search_flights(
+    departure_airports: list[str],
+    arrival_airports: list[str],
+    outbound_date: str,
+    return_date: str | None = None,
+    flight_type: str = "round_trip",
+    adults: int = 1,
+    children: int = 0,
+    travel_class: str = "economy",
+    currency: str = "CNY",
+    sort_by: str | None = None,
+    stops: str | None = None,
+    max_price: int | None = None,
+    outbound_times: str | None = None,
+    language: str = "zh-cn",
+    country: str = "cn",
+    no_cache: bool = False,
+    show_hidden: bool = False,
+    deep_search: bool = False,
+) -> dict[str, Any]:
+    """查询航班(单程或往返去程)。
+
+    返回标准化的航班候选列表。
+    """
+    query = FlightSearchQuery(
+        departure_airports=departure_airports,
+        arrival_airports=arrival_airports,
+        outbound_date=_parse_date(outbound_date),
+        return_date=_parse_date(return_date) if return_date else None,
+        flight_type=flight_type,  # type: ignore[arg-type]
+        adults=adults,
+        children=children,
+        travel_class=travel_class,  # type: ignore[arg-type]
+        currency=currency,
+        sort_by=sort_by,  # type: ignore[arg-type]
+        stops=stops,  # type: ignore[arg-type]
+        max_price=max_price,
+        outbound_times=outbound_times,
+        language=language,
+        country=country,
+        no_cache=no_cache,
+        show_hidden=show_hidden,
+        deep_search=deep_search,
+    )
+
+    service = _build_service()
+    result: FlightSearchResult = await service.search_flights(query)
+
+    return _serialize_flight_result(result)
+
+
+@mcp.tool()
+async def search_return_flights(
+    departure_token: str,
+    departure_airports: list[str],
+    arrival_airports: list[str],
+    outbound_date: str,
+    return_date: str,
+    adults: int = 1,
+    children: int = 0,
+    travel_class: str = "economy",
+    currency: str = "CNY",
+    sort_by: str | None = None,
+    stops: str | None = None,
+    max_price: int | None = None,
+    language: str = "zh-cn",
+    country: str = "cn",
+    no_cache: bool = False,
+    show_hidden: bool = False,
+    deep_search: bool = False,
+) -> dict[str, Any]:
+    """查询与选定去程匹配的返程航班。
+
+    需要提供去程的 departure_token。
+    """
+    query = FlightSearchQuery(
+        departure_airports=departure_airports,
+        arrival_airports=arrival_airports,
+        outbound_date=_parse_date(outbound_date),
+        return_date=_parse_date(return_date),
+        flight_type="round_trip",
+        adults=adults,
+        children=children,
+        travel_class=travel_class,  # type: ignore[arg-type]
+        currency=currency,
+        sort_by=sort_by,  # type: ignore[arg-type]
+        stops=stops,  # type: ignore[arg-type]
+        max_price=max_price,
+        language=language,
+        country=country,
+        no_cache=no_cache,
+        show_hidden=show_hidden,
+        deep_search=deep_search,
+    )
+
+    service = _build_service()
+    result: FlightSearchResult = await service.search_return_flights(
+        query=query,
+        departure_token=departure_token,
+    )
+
+    return _serialize_flight_result(result)
+
+
+@mcp.tool()
+async def search_hotels(
+    query: str,
+    check_in_date: str,
+    check_out_date: str,
+    adults: int = 1,
+    children_ages: list[int] | None = None,
+    currency: str = "CNY",
+    language: str = "zh-cn",
+    country: str = "cn",
+    min_price: int | None = None,
+    max_price: int | None = None,
+    rating_filter: str | None = None,
+    hotel_class: list[int] | None = None,
+    free_cancellation: bool | None = None,
+    sort: str | None = None,
+    no_cache: bool = False,
+) -> dict[str, Any]:
+    """搜索酒店。
+
+    根据关键词和入住/退房日期返回标准化的酒店候选列表。
+    """
+    hotel_query = HotelSearchQuery(
+        query=query,
+        check_in_date=_parse_date(check_in_date),
+        check_out_date=_parse_date(check_out_date),
+        adults=adults,
+        children_ages=children_ages or [],
+        currency=currency,
+        language=language,
+        country=country,
+        min_price=min_price,
+        max_price=max_price,
+        rating_filter=rating_filter,  # type: ignore[arg-type]
+        hotel_class=hotel_class or [],  # type: ignore[arg-type]
+        free_cancellation=free_cancellation,
+        sort=sort,  # type: ignore[arg-type]
+        no_cache=no_cache,
+    )
+
+    service = _build_service()
+    result: HotelSearchResult = await service.search_hotels(hotel_query)
+
+    return _serialize_hotel_result(result)
+
+@mcp.tool()
+async def search_airports(
+    query: str,
+) -> dict[str, Any]:
+    """根据城市或机场名称查询真实机场代码。
+
+    返回城市候选以及该城市包含的机场名称和
+    IATA三字代码。查询航班前应先调用本工具。
+    """
+
+    service = _build_service()
+
+    try:
+        return await service.search_airports(query)
+    except SerpApiError as exc:
+        raise RuntimeError(
+            f"机场查询失败:{exc}"
+        ) from exc
+# ---------------------------------------------------------------------------
+# 序列化辅助:把 Pydantic 模型转成 JSON-safe dict
+# ---------------------------------------------------------------------------
+
+
+def _serialize_datetime(v: Any) -> str | None:
+    """把datetime对象序列化为ISO格式字符串,None则返回None。"""
+
+    if isinstance(v, datetime):
+        return v.isoformat()
+    return None
+
+
+def _serialize_flight_result(result: FlightSearchResult) -> dict[str, Any]:
+    """把Pydantic航班结果模型序列化为MCP工具返回的JSON结构。"""
+
+    flights: list[dict[str, Any]] = []
+    for opt in result.flights:
+        flights.append(
+            {
+                "option_id": opt.option_id,
+                "source_group": opt.source_group,
+                "provider_rank": opt.provider_rank,
+                "flight_numbers": [
+                    seg.flight_number for seg in opt.segments
+                ],
+                "departure_airport_code": opt.departure_airport_code,
+                "final_arrival_airport_code": opt.final_arrival_airport_code,
+                "departure_time": _serialize_datetime(opt.departure_time),
+                "arrival_time": _serialize_datetime(opt.arrival_time),
+                "total_duration_minutes": opt.total_duration_minutes,
+                "stop_count": opt.stop_count,
+                "price": opt.price,
+                "currency": opt.currency,
+                "airlines": opt.airlines,
+                "is_overnight": opt.is_overnight,
+                "departure_token": opt.departure_token,
+                "booking_token": opt.booking_token,
+                "segments": [
+                    {
+                        "flight_number": seg.flight_number,
+                        "airline": seg.airline,
+                        "departure_airport_code": (
+                            seg.departure_airport_code
+                        ),
+                        "arrival_airport_code": (
+                            seg.arrival_airport_code
+                        ),
+                        "departure_time": _serialize_datetime(
+                            seg.departure_time
+                        ),
+                        "arrival_time": _serialize_datetime(
+                            seg.arrival_time
+                        ),
+                        "duration_minutes": seg.duration_minutes,
+                    }
+                    for seg in opt.segments
+                ],
+                "layovers": [
+                    {
+                        "airport_code": lay.airport_code,
+                        "duration_minutes": lay.duration_minutes,
+                    }
+                    for lay in opt.layovers
+                ],
+            }
+        )
+
+    return {
+        "flights": flights,
+        "total_count": result.total_count,
+        "warnings": result.warnings,
+    }
+
+
+def _serialize_hotel_result(
+    result: HotelSearchResult,
+) -> dict[str, Any]:
+    """把Pydantic酒店结果模型序列化为MCP工具返回的JSON结构。"""
+
+    hotels: list[dict[str, Any]] = []
+    for h in result.hotels:
+        hotels.append(
+            {
+                "hotel_id": h.hotel_id,
+                "name": h.name,
+                "description": h.description,
+                "overall_rating": h.overall_rating,
+                "review_count": h.review_count,
+                "location_rating": h.location_rating,
+                "price_per_night": h.price_per_night,
+                "total_price": h.total_price,
+                "currency": h.currency,
+                "hotel_class": h.hotel_class,
+                "amenities": h.amenities,
+                "free_cancellation": h.free_cancellation,
+                "thumbnail_url": h.thumbnail_url,
+                "coordinates": (
+                    h.coordinates.model_dump()
+                    if h.coordinates is not None
+                    else None
+                ),
+            }
+        )
+
+    return {
+        "hotels": hotels,
+        "total_count": result.total_count,
+        "warnings": result.warnings,
+    }
+
+
+if __name__ == "__main__":
+    mcp.run()

+ 28 - 0
pyproject.toml

@@ -0,0 +1,28 @@
+[project]
+name = "travel-mind"
+version = "0.1.0"
+description = "Travel mind: SerpApi 航班/酒店查询与行程规划"
+requires-python = ">=3.10"
+dependencies = [
+    "httpx[socks]>=0.27",
+    "rich>=13.7",
+    "pydantic-settings>=2.2",
+    "langchain>=1.0.0",
+    "langchain-core>=1.5.2",
+    "langchain-mcp-adapters>=0.3.1",
+    "mcp[cli]>=1.9.2,<2.0.0",
+    "langgraph>=0.4.0",
+    "langchain-openai>=0.3.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["app", "scripts"]
+
+[dependency-groups]
+dev = [
+    "pytest>=9.1.1",
+]

+ 0 - 0
scripts/__init__.py


+ 182 - 0
scripts/run_regression.py

@@ -0,0 +1,182 @@
+from __future__ import annotations
+
+"""TravelMind自动回归测试入口:依次运行全部本地单元测试,可选运行调用真实模型和外部API的集成测试。"""
+
+import argparse
+import subprocess
+import sys
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True)
+class TestCommand:
+    name: str
+    args: list[str]
+
+
+UNIT_TESTS = [
+    TestCommand(
+        name="所有本地单元测试",
+        args=[
+            sys.executable,
+            "-m",
+            "pytest",
+            "tests",
+            "-v",
+        ],
+    ),
+]
+
+
+INTEGRATION_TESTS = [
+    TestCommand(
+        name="MCP工具注册",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_mcp_registry",
+        ],
+    ),
+    TestCommand(
+        name="需求解析Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_requirement_graph",
+        ],
+    ),
+    TestCommand(
+        name="资源查询Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_resource_graph",
+        ],
+    ),
+    TestCommand(
+        name="地图研究Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_research_graph",
+        ],
+    ),
+    TestCommand(
+        name="候选筛选Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_selection_graph",
+        ],
+    ),
+    TestCommand(
+        name="路线评估Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_route_graph",
+        ],
+    ),
+    TestCommand(
+        name="最终多角色Graph",
+        args=[
+            sys.executable,
+            "-m",
+            "scripts.test_final_graph",
+        ],
+    ),
+]
+
+
+def run_command(
+    command: TestCommand,
+) -> bool:
+    """以子进程方式执行单个TestCommand,输出分隔线与结果。"""
+    separator = "=" * 72
+
+    print()
+    print(separator)
+    print(f"开始:{command.name}")
+    print(separator)
+
+    completed = subprocess.run(
+        command.args,
+        check=False,
+    )
+
+    if completed.returncode == 0:
+        print(f"通过:{command.name}")
+        return True
+
+    print(
+        f"失败:{command.name},"
+        f"returncode={completed.returncode}"
+    )
+
+    return False
+
+
+def parse_args() -> argparse.Namespace:
+    """解析命令行参数:--integration启用集成测试,--continue-on-error在单项失败后继续。"""
+    parser = argparse.ArgumentParser(
+        description=(
+            "运行TravelMind自动回归测试。"
+        )
+    )
+
+    parser.add_argument(
+        "--integration",
+        action="store_true",
+        help=(
+            "同时运行会调用真实模型、SerpApi"
+            "和高德MCP的集成测试。"
+        ),
+    )
+
+    parser.add_argument(
+        "--continue-on-error",
+        action="store_true",
+        help="单项失败后继续执行后续测试。",
+    )
+
+    return parser.parse_args()
+
+
+def main() -> None:
+    """依次执行全部测试命令,汇总失败项并以非零退出码结束。"""
+    args = parse_args()
+
+    commands = list(UNIT_TESTS)
+
+    if args.integration:
+        commands.extend(INTEGRATION_TESTS)
+
+    failed_commands: list[str] = []
+
+    for command in commands:
+        passed = run_command(command)
+
+        if passed:
+            continue
+
+        failed_commands.append(command.name)
+
+        if not args.continue_on_error:
+            break
+
+    print()
+    print("=" * 72)
+
+    if failed_commands:
+        print("回归测试未通过:")
+
+        for name in failed_commands:
+            print(f"- {name}")
+
+        raise SystemExit(1)
+
+    print("全部回归测试通过。")
+
+
+if __name__ == "__main__":
+    main()

+ 149 - 0
scripts/test_amap_mcp.py

@@ -0,0 +1,149 @@
+from __future__ import annotations
+"""测试高德MCP的文本搜索与周边搜索等工具调用。"""
+import asyncio
+import json
+from typing import Any
+
+# 直接运行本文件时(如 python scripts/test_amap_mcp.py),Python 只把 scripts/ 加入 sys.path,
+# 找不到项目根的 app 包。此时需要把项目根目录注入 sys.path:
+# sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+# 使用 `python -m scripts.test_amap_mcp` 运行时则无需此处理,项目根会自动加入 sys.path。
+from rich.console import Console
+from rich.table import Table
+from langchain_core.tools import BaseTool
+from langchain_mcp_adapters.client import MultiServerMCPClient
+from app.config import get_settings
+
+console = Console()
+
+def get_tool_schema(tool:BaseTool)->dict[str, Any]:
+    """兼容不同 LangChain 工具 Schema 表示。"""
+
+    schema = tool.args_schema
+
+    if schema is None:
+        return {}
+    if isinstance(schema,dict):
+        return schema
+    if hasattr(schema,"model_json_schema"):
+        return schema.model_json_schema()
+    return {}
+
+async def invoke_text_search(tools:list[BaseTool])->None:
+    """如果发现高德关键词搜索工具,则执行一次真实POI查询。"""
+    candidate_names = {
+        "maps_text_search",
+        "map_text_search",
+        "text_search",
+    }
+
+    search_tool = next(
+        (
+            tool
+            for tool in tools
+            if tool.name in candidate_names
+            or "text_search" in tool.name.lower()
+        ),
+        None,
+    )
+
+    if search_tool is None:
+        console.print(
+            "[yellow]MCP 已连接成功,但没有匹配到预设的关键词搜索工具名。[/yellow]"
+        )
+        console.print(
+            "这是正常的版本差异。先保留工具列表,后续根据实际名称接入。"
+        )
+        return
+    
+    schema = get_tool_schema(search_tool)
+    properties = schema.get("properties",{})
+    # 注意:JSON Schema 中 required 和 properties 是平级字段,不是嵌套关系。
+    required = schema.get("required",[])
+
+    console.rule("[bold]关键词搜索工具 Schema")
+    console.print_json(json.dumps(schema,ensure_ascii=False))
+
+    args:dict[str, Any] = {}
+
+    keyword_names = {"keywords","keyword","query"}
+    city_names = {"city","region"}
+
+    
+    for name in properties:
+        if name in keyword_names:
+            args[name] = "宽窄巷子"
+        elif name in city_names:
+            args[name] = "成都"
+
+
+    missing_required = [
+        name
+        for name in required
+        if name not in args
+    ]
+
+    if missing_required:
+        console.print(
+            "[yellow]发现关键词搜索工具,但存在无法自动填充的必填参数:"
+            f"{missing_required}[/yellow]"
+        )
+        return
+    
+    console.rule("[bold]执行真实 POI 查询")
+    console.print(f"工具:{search_tool.name}")
+    console.print(f"参数:{args}")
+
+    result = await search_tool.ainvoke(args)
+
+    console.print("[green]POI 查询成功:[/green]")
+    console.print(result)
+
+async def main()->None:
+    """主函数,用于测试高德关键词搜索工具。"""
+    settings = get_settings()
+    # 高德 key 在 modelscope MCP 端点中通常通过 URL token 鉴权,这里读取备用。
+    settings.require("amap_api_key")
+
+    mcp_client = MultiServerMCPClient(
+        {
+            "amap": {
+                "transport": "streamable_http",
+                "url": "https://mcp.api-inference.modelscope.net/8ac378c1961447/mcp",
+            }
+        }
+    )
+
+    console.rule("[bold]连接 高德MCP")
+
+    tools = await mcp_client.get_tools()
+
+    if not tools:
+        raise RuntimeError("MCP 连接成功,但服务端未返回任何工具")
+    
+    table = Table(title=f"高德MCP 工具列表,共{len(tools)}个")
+    table.add_column("序号")
+    table.add_column("工具名")
+    table.add_column("说明")
+
+    for index,tool in enumerate(tools,start=1):
+        description = (tool.description or "").replace("\n"," ")
+
+        table.add_row(
+            str(index),
+            tool.name,
+            description[:100],
+        )
+    
+    console.print(table)
+
+    await invoke_text_search(tools)
+
+if __name__ == "__main__":
+    try:
+        asyncio.run(main())
+    except Exception as e:
+        console.print(f"[red]高德mcp测试失败:{type(e).__name__}:{e}[/red]")
+        raise SystemExit(1) from e
+
+

+ 24 - 0
scripts/test_connections.py

@@ -0,0 +1,24 @@
+from __future__ import annotations
+"""快速验证项目基础配置与外部服务连接是否可用。"""
+import asyncio
+from rich.console import Console
+from scripts.test_amap_mcp import main as test_amap_mcp
+from scripts.test_serpapi import main as test_serpapi
+
+console = Console()
+
+async def main()->None:
+    console.rule("[bold blue]TravelMind 外部服务连接性测试")
+
+    console.print("\n[bold]开始测试 SerpAPI[/bold]")
+    await test_serpapi()
+
+    console.print("\n[bold]开始测试 高德MCP[/bold]")
+    await test_amap_mcp()
+
+    console.rule("[bold green]全部外部服务测试通过")
+
+if __name__ == "__main__":
+    asyncio.run(main())
+
+

+ 86 - 0
scripts/test_final_graph.py

@@ -0,0 +1,86 @@
+from __future__ import annotations
+
+"""测试完整多角色工作流端到端流程:从需求输入到最终旅行方案输出。"""
+
+import asyncio
+
+from rich.console import Console
+
+from app.graph.planner_builder import (
+    build_planner_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_planner_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月14日返回,"
+                "总预算5000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "希望行程轻松一些。"
+                "酒店每晚不超过300元,"
+                "评分不低于3,"
+                "并且希望靠近地铁。"
+                "机票价格和便利性综合考虑。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+            "planning_attempts": 0,
+            "review_attempts": 0,
+            "revision_feedback": [],
+            "workflow_status": "running",
+        },
+        config={
+            "recursion_limit": 80,
+        },
+    )
+
+    console.rule(
+        "[bold]最终多角色工作流"
+    )
+
+    console.print(
+        {
+            "工作流状态": result.get(
+                "workflow_status"
+            ),
+            "Supervisor决策": result.get(
+                "supervisor_decision"
+            ),
+            "Planner次数": result.get(
+                "planning_attempts"
+            ),
+            "Reviewer次数": result.get(
+                "review_attempts"
+            ),
+            "校验通过": result.get(
+                "validation_passed"
+            ),
+            "审查通过": result.get(
+                "review_passed"
+            ),
+            "系统错误": result.get(
+                "errors"
+            ),
+        }
+    )
+
+    console.rule("[bold]最终旅行方案")
+
+    console.print(
+        result.get(
+            "final_answer",
+            "没有最终回答。",
+        )
+    )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 60 - 0
scripts/test_mcp_registry.py

@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+"""验证自建旅行MCP与高德MCP服务器的工具注册与加载是否正常。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.mcp_client import load_mcp_tools
+
+
+console = Console()
+
+
+async def main() -> None:
+    bundle = await load_mcp_tools()
+
+    console.print(
+        {
+            "全部MCP工具数": len(
+                bundle.all_tools
+            ),
+            "旅行查询工具数": len(
+                bundle.travel_tools
+            ),
+            "高德工具数": len(
+                bundle.amap_tools
+            ),
+        }
+    )
+
+    table = Table(title="已加载MCP工具")
+    table.add_column("工具类型")
+    table.add_column("工具名称")
+    table.add_column("说明")
+
+    travel_names = {
+        tool.name
+        for tool in bundle.travel_tools
+    }
+
+    for tool in bundle.all_tools:
+        tool_type = (
+            "旅行查询"
+            if tool.name in travel_names
+            else "高德地图"
+        )
+
+        table.add_row(
+            tool_type,
+            tool.name,
+            (tool.description or "")[:100],
+        )
+
+    console.print(table)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 125 - 0
scripts/test_planner_graph.py

@@ -0,0 +1,125 @@
+from __future__ import annotations
+
+"""测试行程规划阶段子图:验证ItineraryPlannerAgent生成多日行程的流程。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.planner_builder import (
+    build_planner_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_planner_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月14日返回,"
+                "总预算5000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "希望行程轻松一些。"
+                "酒店每晚不超过300元,"
+                "评分不低于3,"
+                "并且希望靠近地铁。"
+                "机票价格和便利性综合考虑。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+            "planning_attempts": 0,
+            "revision_feedback": [],
+        }
+    )
+
+    console.rule("[bold]Planner工作流结果")
+    console.print(result["final_answer"])
+
+    plan = result.get("itinerary_plan")
+
+    if plan is None:
+        console.print(
+            {
+                "错误": result.get("errors"),
+            }
+        )
+        return
+
+    console.rule("[bold]方案摘要")
+
+    console.print(
+        {
+            "标题": plan.title,
+            "概述": plan.overview,
+            "规划次数": result.get(
+                "planning_attempts"
+            ),
+            "航班组合": (
+                plan.selected_flight
+                .combination_id
+            ),
+            "航班报价": (
+                plan.selected_flight
+                .quoted_price
+            ),
+            "酒店": plan.selected_hotel.name,
+            "酒店ID": (
+                plan.selected_hotel.hotel_id
+            ),
+            "住宿晚数": (
+                plan.selected_hotel.nights
+            ),
+        }
+    )
+
+    for day in plan.days:
+        table = Table(
+            title=(
+                f"第{day.day_index}天 "
+                f"{day.date}|{day.theme}"
+            )
+        )
+
+        table.add_column("时间")
+        table.add_column("类型")
+        table.add_column("活动")
+        table.add_column("地址")
+        table.add_column("交通")
+        table.add_column("备注")
+
+        for activity in day.activities:
+            table.add_row(
+                (
+                    f"{activity.start_time}"
+                    f"—{activity.end_time}"
+                ),
+                activity.activity_type,
+                activity.name,
+                activity.address or "",
+                activity.transport_mode or "",
+                activity.notes or "",
+            )
+
+        console.print(table)
+
+    if plan.assumptions:
+        console.rule("[bold yellow]假设")
+
+        for item in plan.assumptions:
+            console.print(f"- {item}")
+
+    if plan.warnings:
+        console.rule("[bold yellow]警告")
+
+        for item in plan.warnings:
+            console.print(f"- {item}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 70 - 0
scripts/test_requirement_graph.py

@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+"""测试需求解析阶段子图:验证自然语言解析为TravelRequest的完整流程与澄清分支。"""
+
+import asyncio
+
+from rich.console import Console
+
+from app.graph.builder import (
+    build_requirement_graph,
+)
+
+
+console = Console()
+
+
+async def run_case(
+    graph,
+    user_query: str,
+) -> None:
+    console.rule("[bold]用户输入")
+    console.print(user_query)
+
+    result = await graph.ainvoke(
+        {
+            "user_query": user_query,
+            "errors": [],
+            "missing_fields": [],
+        }
+    )
+
+    console.rule("[bold]Graph输出")
+    console.print(result["final_answer"])
+
+    request = result.get("travel_request")
+
+    if request is not None:
+        console.rule("[bold]结构化需求")
+        console.print_json(
+            request.model_dump_json(
+                indent=2,
+            )
+        )
+
+
+async def main() -> None:
+    graph = build_requirement_graph()
+
+    await run_case(
+        graph,
+        (
+            "我和妻子计划2026年8月10日"
+            "从上海去成都,5天4晚,"
+            "总预算5000元。喜欢熊猫、"
+            "自然景观和川菜,不想安排太赶。"
+            "酒店希望靠近地铁,最好住在市中心,住宿每晚不超过"
+            "300元。机票价格相差不大的情况下,优先考虑落地双流机场"
+            "只优先考虑直达。"
+        ),
+    )
+
+    # 可选测试:信息不完整时是否追问。
+    await run_case(
+        graph,
+        "我想从上海去成都旅游,喜欢美食。",
+    )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 108 - 0
scripts/test_research_graph.py

@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+"""测试地图研究阶段子图:验证高德地图POI搜索与天气查询的完整流程。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.research_builder import (
+    build_research_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_research_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "不想安排太赶。"
+                "酒店希望靠近地铁,"
+                "每晚不超过600元。"
+                "机票不限制是否中转。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+        }
+    )
+
+    console.rule("[bold]工作流结果")
+
+    console.print(result["final_answer"])
+
+    map_result = result.get(
+        "map_research_result"
+    )
+
+    if map_result is None:
+        console.print(
+            {
+                "错误": result.get("errors"),
+            }
+        )
+        return
+
+    console.print(
+        {
+            "城市": map_result.city,
+            "景点数量": len(
+                map_result.attractions
+            ),
+            "餐饮数量": len(
+                map_result.restaurants
+            ),
+            "天气数量": len(
+                map_result.weather
+            ),
+            "备注": map_result.notes,
+        }
+    )
+
+    attraction_table = Table(
+        title="真实景点候选"
+    )
+
+    attraction_table.add_column("名称")
+    attraction_table.add_column("地址")
+    attraction_table.add_column("坐标")
+    attraction_table.add_column("匹配原因")
+
+    for place in map_result.attractions:
+        attraction_table.add_row(
+            place.name,
+            place.address or "未知",
+            place.location or "未知",
+            place.match_reason or "未说明",
+        )
+
+    console.print(attraction_table)
+
+    restaurant_table = Table(
+        title="真实餐饮候选"
+    )
+
+    restaurant_table.add_column("名称")
+    restaurant_table.add_column("地址")
+    restaurant_table.add_column("坐标")
+
+    for place in map_result.restaurants:
+        restaurant_table.add_row(
+            place.name,
+            place.address or "未知",
+            place.location or "未知",
+        )
+
+    console.print(restaurant_table)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 67 - 0
scripts/test_resource_graph.py

@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+"""测试资源查询阶段子图:验证航班和酒店搜索的完整流程(调用真实MCP服务)。"""
+
+import asyncio
+
+from rich.console import Console
+
+from app.graph.travel_builder import (
+    build_resource_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_resource_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "不想安排太赶。"
+                "酒店希望靠近地铁,"
+                "每晚不超过600元。"
+                "机票不限制是否中转。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+        }
+    )
+
+    console.rule("[bold]最终状态摘要")
+
+    console.print(result["final_answer"])
+
+    console.print(
+        {
+            "出发机场": result.get(
+                "origin_airports"
+            ),
+            "目的机场": result.get(
+                "destination_airports"
+            ),
+            "航班候选数": (
+                result.get(
+                    "flight_search_result",
+                    {},
+                ).get("total_count")
+            ),
+            "酒店候选数": (
+                result.get(
+                    "hotel_search_result",
+                    {},
+                ).get("total_count")
+            ),
+            "错误": result.get("errors"),
+        }
+    )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 149 - 0
scripts/test_reviewer_graph.py

@@ -0,0 +1,149 @@
+from __future__ import annotations
+
+"""测试Trip Reviewer阶段子图:验证行程独立评审与修改意见生成。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.planner_builder import (
+    build_planner_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_planner_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "希望行程轻松一些。"
+                "酒店每晚不超过600元,"
+                "评分不低于4.5,"
+                "并且希望靠近地铁。"
+                "机票价格和便利性综合考虑。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+            "planning_attempts": 0,
+            "review_attempts": 0,
+            "revision_feedback": [],
+        },
+        config={
+            "recursion_limit": 50,
+        },
+    )
+
+    console.rule(
+        "[bold]Trip Reviewer工作流结果"
+    )
+
+    console.print(result["final_answer"])
+
+    validation = result.get(
+        "plan_validation_result"
+    )
+
+    review = result.get(
+        "trip_review_result"
+    )
+
+    console.print(
+        {
+            "Planner执行次数": result.get(
+                "planning_attempts"
+            ),
+            "Reviewer执行次数": result.get(
+                "review_attempts"
+            ),
+            "确定性校验通过": (
+                validation.is_valid
+                if validation
+                else None
+            ),
+            "Reviewer原始结论": (
+                review.approved
+                if review
+                else None
+            ),
+            "系统最终审查结论": result.get(
+                "review_passed"
+            ),
+        }
+    )
+
+    if review is None:
+        console.print(
+            {
+                "系统错误": result.get("errors"),
+            }
+        )
+        return
+
+    console.rule("[bold]审查总结")
+    console.print(review.summary)
+
+    issue_table = Table(
+        title="Reviewer发现的问题"
+    )
+
+    issue_table.add_column("级别")
+    issue_table.add_column("分类")
+    issue_table.add_column("日期")
+    issue_table.add_column("活动")
+    issue_table.add_column("问题")
+    issue_table.add_column("建议")
+
+    for issue in review.issues:
+        issue_table.add_row(
+            issue.severity,
+            issue.category,
+            (
+                str(issue.day_index)
+                if issue.day_index
+                else ""
+            ),
+            issue.activity_name or "",
+            issue.message,
+            issue.suggestion or "",
+        )
+
+    console.print(issue_table)
+
+    if review.strengths:
+        console.rule("[bold green]方案优点")
+
+        for strength in review.strengths:
+            console.print(f"- {strength}")
+
+    if result.get("revision_feedback"):
+        console.rule(
+            "[bold yellow]下一轮修改要求"
+        )
+
+        for feedback in result[
+            "revision_feedback"
+        ]:
+            console.print(f"- {feedback}")
+
+    if review.recommendations:
+        console.rule("[bold]非阻塞建议")
+
+        for recommendation in (
+            review.recommendations
+        ):
+            console.print(
+                f"- {recommendation}"
+            )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 142 - 0
scripts/test_route_graph.py

@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+"""测试路线评估阶段子图:验证酒店位置评估与每日景点路线衔接。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.route_builder import (
+    build_route_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_route_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "行程安排轻松一些。"
+                "酒店每晚不超过600元,"
+                "评分不低于4.5,"
+                "并且希望靠近地铁。"
+                "机票价格和便利性综合考虑。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+        }
+    )
+
+    console.rule("[bold]路线评估结果")
+
+    console.print(result["final_answer"])
+
+    route_result = result.get(
+        "route_evaluation_result"
+    )
+
+    if route_result is None:
+        console.print(
+            {
+                "错误": result.get("errors"),
+                "警告": result.get(
+                    "route_warnings"
+                ),
+            }
+        )
+        return
+
+    console.print(
+        {
+            "参考景点": (
+                route_result
+                .reference_attractions
+            ),
+            "距离调用次数": (
+                route_result
+                .distance_call_count
+            ),
+            "周边搜索次数": (
+                route_result
+                .around_search_call_count
+            ),
+            "最终酒店ID": (
+                route_result
+                .selected_hotel_id
+            ),
+            "警告数": len(
+                route_result.warnings
+            ),
+        }
+    )
+
+    table = Table(
+        title="酒店综合路线排名"
+    )
+
+    table.add_column("排名")
+    table.add_column("酒店")
+    table.add_column("综合分")
+    table.add_column("原酒店分")
+    table.add_column("景点平均距离")
+    table.add_column("平均驾车时间")
+    table.add_column("最近地铁")
+    table.add_column("原因")
+
+    for index, hotel in enumerate(
+        route_result.evaluated_hotels,
+        start=1,
+    ):
+        average_distance = (
+            f"{hotel.average_distance_meters / 1000:.1f}km"
+            if hotel.average_distance_meters
+            is not None
+            else "未知"
+        )
+
+        average_duration = (
+            f"{hotel.average_duration_seconds / 60:.0f}分钟"
+            if hotel.average_duration_seconds
+            is not None
+            else "未知"
+        )
+
+        subway_distance = (
+            f"{hotel.nearest_subway_distance_meters:.0f}m"
+            if hotel
+            .nearest_subway_distance_meters
+            is not None
+            else "未知"
+        )
+
+        table.add_row(
+            str(index),
+            hotel.hotel_name,
+            f"{hotel.final_score:.2f}",
+            f"{hotel.base_hotel_score:.2f}",
+            average_distance,
+            average_duration,
+            subway_distance,
+            ";".join(hotel.reasons),
+        )
+
+    console.print(table)
+
+    if route_result.warnings:
+        console.rule("[bold yellow]警告")
+
+        for warning in route_result.warnings:
+            console.print(f"- {warning}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 158 - 0
scripts/test_selection_graph.py

@@ -0,0 +1,158 @@
+from __future__ import annotations
+
+"""测试候选筛选阶段子图:验证航班筛选、酒店过滤与往返组合排序。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.selection_builder import (
+    build_selection_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_selection_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "酒店每晚不超过600元,"
+                "评分希望不低于4.5分。"
+                "机票价格和便利性综合考虑,"
+                "不限制是否中转。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+        }
+    )
+
+    console.rule("[bold]候选筛选结果")
+
+    console.print(result["final_answer"])
+
+    selection = result.get(
+        "candidate_selection_result"
+    )
+
+    if selection is None:
+        console.print(
+            {
+                "错误": result.get("errors"),
+                "警告": result.get(
+                    "selection_warnings"
+                ),
+            }
+        )
+        return
+
+    console.print(
+        {
+            "旅行类型": selection.flight_type,
+            "去程查询候选数": len(
+                selection.outbound_candidates
+            ),
+            "往返组合数": len(
+                selection.round_trip_options
+            ),
+            "酒店候选数": len(
+                selection.hotels
+            ),
+            "警告": selection.warnings,
+        }
+    )
+
+    flight_table = Table(
+        title="排名靠前的往返组合"
+    )
+
+    flight_table.add_column("排名")
+    flight_table.add_column("去程航班")
+    flight_table.add_column("返程航班")
+    flight_table.add_column("接口报价")
+    flight_table.add_column("分数")
+    flight_table.add_column("排序原因")
+
+    for index, option in enumerate(
+        selection.round_trip_options[:5],
+        start=1,
+    ):
+        outbound_numbers = "、".join(
+            option.outbound.get(
+                "flight_numbers",
+                [],
+            )
+        )
+
+        return_numbers = "、".join(
+            option.return_flight.get(
+                "flight_numbers",
+                [],
+            )
+        )
+
+        flight_table.add_row(
+            str(index),
+            outbound_numbers or "未知",
+            return_numbers or "未知",
+            (
+                f"{option.quoted_price:.0f}"
+                f" {option.currency}"
+                if option.quoted_price is not None
+                else "未知"
+            ),
+            f"{option.score:.2f}",
+            ";".join(option.reasons),
+        )
+
+    console.print(flight_table)
+
+    hotel_table = Table(
+        title="排名靠前的酒店"
+    )
+
+    hotel_table.add_column("排名")
+    hotel_table.add_column("酒店")
+    hotel_table.add_column("每晚价格")
+    hotel_table.add_column("评分")
+    hotel_table.add_column("分数")
+    hotel_table.add_column("排序原因")
+
+    for index, candidate in enumerate(
+        selection.hotels[:5],
+        start=1,
+    ):
+        hotel = candidate.hotel
+
+        price = hotel.get("price_per_night")
+
+        hotel_table.add_row(
+            str(index),
+            str(hotel.get("name", "未知")),
+            (
+                f"{price} {hotel.get('currency', '')}"
+                if price is not None
+                else "未知"
+            ),
+            str(
+                hotel.get(
+                    "overall_rating",
+                    "未知",
+                )
+            ),
+            f"{candidate.score:.2f}",
+            ";".join(candidate.reasons),
+        )
+
+    console.print(hotel_table)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 246 - 0
scripts/test_serpapi.py

@@ -0,0 +1,246 @@
+from __future__ import annotations
+"""直接调用SerpApi REST接口,验证原始JSON返回结构。"""
+import asyncio
+import sys
+from pathlib import Path
+from typing import Any
+import httpx
+from rich.console import Console
+from rich.table import Table
+
+# 直接运行本文件时,Python 只把 scripts/ 加入 sys.path,找不到项目根的 app 包。
+# 这里把项目根目录(本文件的上级目录)注入 sys.path,保证直接运行文件也能 import app。
+# sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+# 使用 `python -m scripts.test_amap_mcp` 运行时则无需此处理,项目根会自动加入 sys.path。
+
+
+from app.config import get_settings
+
+SERPAPI_URL = "https://serpapi.com/search.json"
+
+console = Console()
+
+async def serpapi_request(
+        client:httpx.AsyncClient,
+        params:dict[str,Any],
+)->dict[str,Any]:
+    """调用SerpAPI,并进行统一错误处理。"""
+    response = await client.get(SERPAPI_URL,params=params)
+    response.raise_for_status()
+    data:dict[str,Any] = response.json()
+
+    if error := data.get("error"):
+        raise RuntimeError(f"SerpApi 查询失败:{error}")
+
+    status = data.get("search_metadata",{}).get("status")
+
+    # SerpApi 返回的 status 形如 "Success"(首字母大写),这里做大小写不敏感比较
+    if status is not None and str(status).lower() != "success":
+        raise RuntimeError(f"SerpApi 返回异常状态:{status}")
+    return data
+
+def format_flight_route(flight_option:dict[str,Any])->str:
+    """把航班列表转换成可读路线"""
+
+    segments = flight_option.get("flights",[])
+
+    if not segments:
+        return "未知路线"
+
+    airport_ids:list[str] = []
+
+    for index,segment in enumerate(segments):
+        departure = segment.get("departure_airport",{}).get("id","?")
+        arrival = segment.get("arrival_airport",{}).get("id","?")
+
+        if index == 0:
+            airport_ids.append(departure)
+        airport_ids.append(arrival)
+
+    return " -> ".join(airport_ids)
+
+
+def format_duration(minutes:Any)->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}分"
+
+
+async def test_flights(client:httpx.AsyncClient,api_key:str)->None:
+    """
+    测试上海到成都的真实航班查询。
+    第一版使用单程搜索。
+    正式项目中,去成和返程各调用一次,
+    比使用departure_token 的往返联动流程更容易维护。
+    """
+    console.rule("[bold]1.SerpApi 航班查询")
+
+    params = {
+        "engine":"google_flights",
+        "api_key":api_key,
+        "departure_id":"SHA,PVG",
+        "arrival_id":"CTU,TFU",
+        "outbound_date":"2026-08-10",
+        "type":2, # 单程
+        "travel_class":1,
+        "currency":"CNY",
+        "gl":"cn",
+        "hl":"zh-cn",
+    }
+
+    data = await serpapi_request(client,params)
+
+    options = [
+        *data.get("best_flights",[]),
+        *data.get("other_flights",[]),
+    ]
+
+    if not options:
+        console.print("[yellow]查询成功,但没有返回航班信息。[/yellow]")
+        console.print("返回字段:",list(data.keys()))
+        return
+
+    table = Table(title="上海->成都航班候选")
+    table.add_column("路线")
+    table.add_column("总时长")
+    table.add_column("价格")
+    table.add_column("航班号")
+    table.add_column("出发时间")
+    table.add_column("到达时间")
+
+    for option in options:
+        segments = option.get("flights",[])
+        first_segment = segments[0] if segments else {}
+        last_segment = segments[-1] if segments else {}
+
+        # 预先取值,避免 f-string 嵌套双引号(Python 3.9 不支持)
+        duration = option.get("total_duration","?")
+        price = option.get("price","?")
+        flight_numbers = ", ".join(
+            seg.get("flight_number","?") for seg in segments
+        ) or "?"
+
+        table.add_row(
+            format_flight_route(option),
+            format_duration(duration),
+            f"¥{price}",
+            flight_numbers,
+            str(first_segment.get("departure_airport",{}).get("time","?")),
+            str(last_segment.get("arrival_airport",{}).get("time","?")),
+        )
+
+    console.print(table)
+
+    metadata = data.get("search_metadata",{})
+    console.print(
+        {
+            "provider":"serpapi_google_flights",
+            "status":metadata.get("status"),
+            "search_id":metadata.get("id"),
+            "result_count":len(options),
+        }
+    )
+
+
+
+async def test_hotels(client:httpx.AsyncClient,api_key:str)->None:
+    """测试成都酒店真实查询。"""
+
+    params = {
+        "engine":"google_hotels",
+        "api_key":api_key,
+        "q":"成都 地铁附近 酒店",
+        "check_in_date":"2026-08-10",
+        "check_out_date":"2026-08-15",
+        "adults":2,
+        "children":0,
+        "currency":"CNY",
+        "gl":"cn",
+        "hl":"zh-cn",
+    }
+
+    data = await serpapi_request(client,params)
+    properties = data.get("properties",[])
+
+    if not properties:
+        console.print("[yellow]查询成功,但没有返回酒店候选。[/yellow]")
+        console.print("返回字段",list(data.keys()))
+        return
+
+    table = Table(title="成都酒店候选")
+    table.add_column("酒店")
+    table.add_column("星级")
+    table.add_column("评分")
+    table.add_column("每晚最低价")
+    table.add_column("附近地标")
+
+    for hotel in properties[:20]:
+        rate = hotel.get("rate_per_night",{})
+
+        # 价格:用 extracted_lowest(纯数字)统一加 ¥,避免和 SerpApi 自带的 ¥ 重复
+        extracted = rate.get("extracted_lowest")
+        price_text = f"¥{extracted}" if extracted is not None else "?"
+
+        # 附近地标:取 nearby_places 第一项的名称 + 交通耗时
+        nearby = hotel.get("nearby_places") or []
+        landmark_text = "?"
+        if nearby:
+            first = nearby[0]
+            landmark_name = first.get("name","?")
+            transports = first.get("transportations") or []
+            duration = transports[0].get("duration","") if transports else ""
+            landmark_text = f"{landmark_name}({duration})" if duration else str(landmark_name)
+
+        table.add_row(
+            str(hotel.get("name","?")),
+            str(hotel.get("hotel_class","?")),
+            str(hotel.get("overall_rating","?")),
+            price_text,
+            landmark_text,
+        )
+
+    console.print(table)
+
+    metadata = data.get("search_metadata",{})
+    console.print(
+        {
+            "provider":"serpapi_google_hotels",
+            "status":metadata.get("status"),
+            "search_id":metadata.get("id"),
+            "result_count":len(properties),
+        }
+    )
+
+async def main()->None:
+    settings = get_settings()
+    api_key = settings.require("serpapi_api_key")
+
+    time_out = httpx.Timeout(
+        timeout = settings.request_timeout_seconds,
+        connect = 10.0,
+    )
+
+    async with httpx.AsyncClient(timeout = time_out, follow_redirects = True) as client:
+        await test_flights(client,api_key)
+        await test_hotels(client,api_key)
+
+
+if __name__=="__main__":
+    try:
+        asyncio.run(main())
+    except httpx.HTTPStatusError as e:
+        console.print(
+            f"[red]HTTP错误:{e.response.status_code} {e.response.text[:500]}[/red]"
+        )
+        raise SystemExit(1) from e
+    except (httpx.RequestError, RuntimeError) as e:
+        console.print(f"[red]测试实效:{e}[/red]")
+        raise SystemExit(1) from e

+ 242 - 0
scripts/test_serpapi_client.py

@@ -0,0 +1,242 @@
+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

+ 259 - 0
scripts/test_travel_mcp.py

@@ -0,0 +1,259 @@
+from __future__ import annotations
+"""直接调用自建旅行MCP服务器工具,验证返回结构是否符合预期。"""
+import asyncio
+from typing import Any
+from mcp import ClientSession
+from mcp.shared.memory import create_client_server_memory_streams
+from rich.console import Console
+from rich.table import Table
+from mcp_servers.travel_search_server import mcp
+
+
+console = Console()
+
+def get_payload(result: Any) -> dict[str, Any]:
+    payload = result.structuredContent
+
+    if not isinstance(payload, dict):
+        raise RuntimeError(
+            "MCP工具没有返回结构化结果。"
+        )
+
+    return payload
+
+
+async def main() -> None:
+    console.rule(
+        "[bold blue]TravelMind MCP Server测试"
+    )
+
+    # MCP SDK 1.x 内存直连模式:
+    # 1. 创建双向内存流
+    # 2. 服务端通过 asyncio.create_task 在后台运行
+    # 3. 客户端通过 ClientSession 连接并初始化
+    async with create_client_server_memory_streams() as (
+        client_streams,
+        server_streams,
+    ):
+        client_read, client_write = client_streams
+        server_read, server_write = server_streams
+
+        init_opts = mcp._mcp_server.create_initialization_options()
+
+        async with ClientSession(client_read, client_write) as client:
+            server_task = asyncio.create_task(
+                mcp._mcp_server.run(
+                    server_read,
+                    server_write,
+                    init_opts,
+                )
+            )
+            try:
+                await client.initialize()
+
+                tools_result = await client.list_tools()
+
+                tool_table = Table(title="已发现MCP工具")
+                tool_table.add_column("工具名")
+                tool_table.add_column("说明")
+
+                for tool in tools_result.tools:
+                    tool_table.add_row(
+                        tool.name,
+                        (tool.description or "")[:100],
+                    )
+
+                console.print(tool_table)
+
+                console.rule("[bold]1. 查询往返去程")
+
+                outbound_result = await client.call_tool(
+                    "search_flights",
+                    {
+                        "departure_airports": [
+                            "SHA",
+                            "PVG",
+                        ],
+                        "arrival_airports": [
+                            "CTU",
+                            "TFU",
+                        ],
+                        "outbound_date": "2026-08-10",
+                        "return_date": "2026-08-13",
+                        "flight_type": "round_trip",
+                        "adults": 2,
+                        "currency": "CNY",
+                    },
+                )
+
+                outbound_payload = get_payload(
+                    outbound_result
+                )
+
+                outbound_flights = outbound_payload.get(
+                    "flights",
+                    [],
+                )
+
+                console.print(
+                    {
+                        "去程候选总数": (
+                            outbound_payload.get(
+                                "total_count"
+                            )
+                        ),
+                        "实际结构化数量": len(
+                            outbound_flights
+                        ),
+                    }
+                )
+
+                if not outbound_flights:
+                    raise RuntimeError(
+                        "没有查询到去程航班。"
+                    )
+
+                selected_outbound = next(
+                    (
+                        flight
+                        for flight in outbound_flights
+                        if flight.get("departure_token")
+                    ),
+                    None,
+                )
+
+                if selected_outbound is None:
+                    raise RuntimeError(
+                        "去程候选中没有 departure_token。"
+                    )
+
+                console.print(
+                    {
+                        "测试选中的去程": (
+                            selected_outbound.get(
+                                "flight_numbers"
+                            )
+                        ),
+                        "出发时间": (
+                            selected_outbound.get(
+                                "departure_time"
+                            )
+                        ),
+                        "到达时间": (
+                            selected_outbound.get(
+                                "arrival_time"
+                            )
+                        ),
+                        "价格": selected_outbound.get(
+                            "price"
+                        ),
+                    }
+                )
+
+                console.rule("[bold]2. 查询对应返程")
+
+                return_result = await client.call_tool(
+                    "search_return_flights",
+                    {
+                        "departure_token": (
+                            selected_outbound[
+                                "departure_token"
+                            ]
+                        ),
+                        "departure_airports": [
+                            "SHA",
+                            "PVG",
+                        ],
+                        "arrival_airports": [
+                            "CTU",
+                            "TFU",
+                        ],
+                        "outbound_date": "2026-08-10",
+                        "return_date": "2026-08-13",
+                        "adults": 2,
+                        "currency": "CNY",
+                    },
+                )
+
+                return_payload = get_payload(
+                    return_result
+                )
+
+                console.print(
+                    {
+                        "返程候选总数": (
+                            return_payload.get(
+                                "total_count"
+                            )
+                        ),
+                        "实际结构化数量": len(
+                            return_payload.get(
+                                "flights",
+                                [],
+                            )
+                        ),
+                    }
+                )
+
+                console.rule("[bold]3. 查询酒店")
+
+                hotel_result = await client.call_tool(
+                    "search_hotels",
+                    {
+                        "query": "成都酒店",
+                        "check_in_date": "2026-08-10",
+                        "check_out_date": "2026-08-13",
+                        "adults": 2,
+                        "currency": "CNY",
+                    },
+                )
+
+                hotel_payload = get_payload(
+                    hotel_result
+                )
+
+                hotels = hotel_payload.get("hotels", [])
+
+                console.print(
+                    {
+                        "酒店候选总数": (
+                            hotel_payload.get(
+                                "total_count"
+                            )
+                        ),
+                        "实际结构化数量": len(hotels),
+                    }
+                )
+
+                if hotels:
+                    console.print(
+                        {
+                            "首家酒店": hotels[0].get(
+                                "name"
+                            ),
+                            "评分": hotels[0].get(
+                                "overall_rating"
+                            ),
+                            "每晚价格": hotels[0].get(
+                                "price_per_night"
+                            ),
+                            "总价格": hotels[0].get(
+                                "total_price"
+                            ),
+                        }
+                    )
+
+            finally:
+                server_task.cancel()
+                try:
+                    await server_task
+                except asyncio.CancelledError:
+                    pass
+
+    console.rule(
+        "[bold green]MCP Server测试通过"
+    )
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 249 - 0
scripts/test_travel_service.py

@@ -0,0 +1,249 @@
+from __future__ import annotations
+
+"""测试TravelSearchService的航班/酒店搜索与规范化功能。"""
+
+import asyncio
+from collections import Counter
+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
+from app.services.travel_search_service import (
+    TravelSearchService,
+)
+
+
+console = Console()
+
+
+def format_duration(minutes: int | None) -> str:
+    if minutes is None:
+        return "未知"
+
+    hours, remaining_minutes = divmod(minutes, 60)
+
+    return f"{hours}小时{remaining_minutes}分"
+
+
+def format_flight_numbers(flight) -> str:
+    flight_numbers = [
+        segment.flight_number
+        for segment in flight.segments
+        if segment.flight_number
+    ]
+
+    return " / ".join(flight_numbers) or "未知"
+
+
+async def test_flight_service(
+    service: TravelSearchService,
+) -> None:
+    console.rule("[bold]TravelSearchService 航班测试")
+
+    query = FlightSearchQuery(
+        departure_airports=["SHA", "PVG"],
+        arrival_airports=["CTU", "TFU"],
+        outbound_date=date(2026, 8, 10),
+        flight_type="one_way",
+        adults=2,
+
+        # 当前不限制时间、机场、中转次数和价格。
+        show_hidden=False,
+        deep_search=False,
+    )
+
+    result = await service.search_flights(query)
+
+    airport_counts = Counter(
+        flight.final_arrival_airport_code
+        for flight in result.flights
+    )
+
+    console.print(
+        {
+            "标准化航班总数": result.total_count,
+            "抵达机场分布": dict(airport_counts),
+            "数据源": result.source.provider,
+            "查询ID": result.source.search_id,
+        }
+    )
+
+    table = Table(
+        title=(
+            f"航班结果预览:展示10条,"
+            f"实际保留{result.total_count}条"
+        )
+    )
+
+    table.add_column("来源")
+    table.add_column("航司/航班号")
+    table.add_column("出发")
+    table.add_column("到达")
+    table.add_column("耗时")
+    table.add_column("中转")
+    table.add_column("单张票价")
+
+    # 这里只限制终端预览,不修改 result.flights。
+    for flight in result.flights[:10]:
+        first_segment = flight.segments[0]
+        last_segment = flight.segments[-1]
+
+        airline_text = (
+            "/".join(flight.airlines)
+            if flight.airlines
+            else "未知"
+        )
+
+        table.add_row(
+            flight.source_group,
+            (
+                f"{airline_text}\n"
+                f"{format_flight_numbers(flight)}"
+            ),
+            (
+                f"{first_segment.departure_airport_code}\n"
+                f"{flight.departure_time:%m-%d %H:%M}"
+            ),
+            (
+                f"{last_segment.arrival_airport_code}\n"
+                f"{flight.arrival_time:%m-%d %H:%M}"
+            ),
+            format_duration(
+                flight.total_duration_minutes
+            ),
+            (
+                "直飞"
+                if flight.stop_count == 0
+                else f"{flight.stop_count}次"
+            ),
+            (
+                f"¥{flight.price}"
+                if flight.price is not None
+                else "未知"
+            ),
+        )
+
+    console.print(table)
+
+    for warning in result.warnings:
+        console.print(f"[yellow]提示:{warning}[/yellow]")
+
+
+async def test_hotel_service(
+    service: TravelSearchService,
+) -> None:
+    console.rule("[bold]TravelSearchService 酒店测试")
+
+    query = HotelSearchQuery(
+        query="成都酒店",
+        check_in_date=date(2026, 8, 10),
+        check_out_date=date(2026, 8, 13),
+        adults=2,
+    )
+
+    result = await service.search_hotels(query)
+
+    console.print(
+        {
+            "标准化酒店总数": result.total_count,
+            "数据源": result.source.provider,
+            "查询ID": result.source.search_id,
+        }
+    )
+
+    table = Table(
+        title=(
+            f"酒店结果预览:展示10家,"
+            f"实际保留{result.total_count}家"
+        )
+    )
+
+    table.add_column("酒店")
+    table.add_column("星级")
+    table.add_column("评分")
+    table.add_column("每晚")
+    table.add_column("整个入住期")
+    table.add_column("客观信息")
+
+    # 这里是客观信息摘要,不是个性化推荐理由。
+    for hotel in result.hotels[:10]:
+        highlights: list[str] = []
+
+        if hotel.review_count is not None:
+            highlights.append(
+                f"{hotel.review_count}条评价"
+            )
+
+        if hotel.location_rating is not None:
+            highlights.append(
+                f"位置评分{hotel.location_rating}"
+            )
+
+        highlights.extend(hotel.amenities[:2])
+
+        if hotel.deal_description:
+            highlights.append(
+                hotel.deal_description
+            )
+
+        table.add_row(
+            hotel.name,
+            (
+                f"{hotel.hotel_class}星"
+                if hotel.hotel_class
+                else "未知"
+            ),
+            (
+                str(hotel.overall_rating)
+                if hotel.overall_rating is not None
+                else "未知"
+            ),
+            (
+                f"¥{hotel.price_per_night:.0f}"
+                if hotel.price_per_night is not None
+                else "未知"
+            ),
+            (
+                f"¥{hotel.total_price:.0f}"
+                if hotel.total_price is not None
+                else "未知"
+            ),
+            ";".join(highlights) or "暂无",
+        )
+
+    console.print(table)
+
+
+async def main() -> None:
+    settings = get_settings()
+
+    async with SerpApiClient(
+        api_key=settings.require(
+            "serpapi_api_key"
+        ),
+        timeout_seconds=(
+            settings.request_timeout_seconds
+        ),
+    ) as client:
+        service = TravelSearchService(client)
+
+        await test_flight_service(service)
+        await test_hotel_service(service)
+
+
+if __name__ == "__main__":
+    try:
+        asyncio.run(main())
+    except SerpApiError as exc:
+        console.print(
+            f"[red]真实接口测试失败:{exc}[/red]"
+        )
+        raise SystemExit(1) from exc

+ 129 - 0
scripts/test_validation_graph.py

@@ -0,0 +1,129 @@
+from __future__ import annotations
+
+"""测试方案校验阶段子图:验证确定性规则校验与校验失败后的路由。"""
+
+import asyncio
+
+from rich.console import Console
+from rich.table import Table
+
+from app.graph.planner_builder import (
+    build_planner_graph,
+)
+
+
+console = Console()
+
+
+async def main() -> None:
+    graph = await build_planner_graph()
+
+    result = await graph.ainvoke(
+        {
+            "user_query": (
+                "我和妻子计划2026年8月10日"
+                "从上海去成都,8月13日返回,"
+                "总预算8000元。"
+                "喜欢熊猫、自然景观和川菜,"
+                "希望行程轻松一些。"
+                "酒店每晚不超过600元,"
+                "评分不低于4.5,"
+                "并且希望靠近地铁。"
+                "机票价格和便利性综合考虑。"
+            ),
+            "errors": [],
+            "missing_fields": [],
+            "planning_attempts": 0,
+            "revision_feedback": [],
+        }
+    )
+
+    console.rule(
+        "[bold]Validator工作流结果"
+    )
+
+    console.print(result["final_answer"])
+
+    validation = result.get(
+        "plan_validation_result"
+    )
+
+    if validation is None:
+        console.print(
+            {
+                "系统错误": result.get("errors"),
+            }
+        )
+        return
+
+    console.print(
+        {
+            "校验通过": validation.is_valid,
+            "错误数": validation.error_count,
+            "警告数": validation.warning_count,
+            "已知费用": (
+                validation.budget.known_total
+            ),
+            "用户预算": (
+                validation.budget.user_budget
+            ),
+            "预算状态": (
+                validation.budget.status
+            ),
+            "已知预算余额": (
+                validation
+                .budget
+                .remaining_known_budget
+            ),
+        }
+    )
+
+    issue_table = Table(
+        title="确定性校验问题"
+    )
+
+    issue_table.add_column("级别")
+    issue_table.add_column("代码")
+    issue_table.add_column("日期序号")
+    issue_table.add_column("活动序号")
+    issue_table.add_column("问题")
+
+    for issue in validation.issues:
+        issue_table.add_row(
+            issue.severity,
+            issue.code,
+            (
+                str(issue.day_index)
+                if issue.day_index is not None
+                else ""
+            ),
+            (
+                str(issue.activity_sequence)
+                if issue.activity_sequence
+                is not None
+                else ""
+            ),
+            issue.message,
+        )
+
+    console.print(issue_table)
+
+    console.rule("[bold]预算覆盖说明")
+
+    console.print(
+        validation.budget.coverage_note
+    )
+
+    if validation.revision_feedback:
+        console.rule(
+            "[bold yellow]需要Planner修改"
+        )
+
+        for feedback in (
+            validation.revision_feedback
+        ):
+            console.print(f"- {feedback}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 186 - 0
tests/test_candidate_selection_service.py

@@ -0,0 +1,186 @@
+from __future__ import annotations
+
+from datetime import date, timedelta
+
+from app.schemas.travel_request import (
+    FlightPreferences,
+    HotelPreferences,
+    TravelRequest,
+)
+from app.services.candidate_selection_service import (
+    CandidateSelectionService,
+)
+
+
+def build_request(
+    *,
+    priority: str = "balanced",
+    max_stops: int | None = None,
+) -> TravelRequest:
+    departure_date = (
+        date.today() + timedelta(days=10)
+    )
+
+    return TravelRequest(
+        origin_city="上海",
+        destination_city="成都",
+        departure_date=departure_date,
+        return_date=(
+            departure_date
+            + timedelta(days=3)
+        ),
+        adults=2,
+        flight_preferences=(
+            FlightPreferences(
+                priority=priority,
+                max_stops=max_stops,
+            )
+        ),
+        hotel_preferences=(
+            HotelPreferences(
+                max_price_per_night=600,
+                minimum_rating=4.5,
+            )
+        ),
+    )
+
+
+def test_price_priority_prefers_lower_price() -> None:
+    service = CandidateSelectionService()
+
+    flights = [
+        {
+            "option_id": "cheap",
+            "price": 700,
+            "duration_minutes": 300,
+            "stop_count": 1,
+            "departure_airport": "PVG",
+            "arrival_airport": "TFU",
+            "departure_time": (
+                "2030-01-01 08:00"
+            ),
+            "arrival_time": (
+                "2030-01-01 13:00"
+            ),
+        },
+        {
+            "option_id": "fast",
+            "price": 900,
+            "duration_minutes": 120,
+            "stop_count": 0,
+            "departure_airport": "PVG",
+            "arrival_airport": "TFU",
+            "departure_time": (
+                "2030-01-01 09:00"
+            ),
+            "arrival_time": (
+                "2030-01-01 11:00"
+            ),
+        },
+    ]
+
+    ranked, warnings = service.rank_flights(
+        flights,
+        build_request(priority="price"),
+    )
+
+    assert ranked[0].option_id == "cheap"
+    assert warnings == []
+
+
+def test_max_stops_filters_connections() -> None:
+    service = CandidateSelectionService()
+
+    flights = [
+        {
+            "option_id": "direct",
+            "price": 900,
+            "duration_minutes": 150,
+            "stop_count": 0,
+        },
+        {
+            "option_id": "transfer",
+            "price": 700,
+            "duration_minutes": 280,
+            "stop_count": 1,
+        },
+    ]
+
+    ranked, _ = service.rank_flights(
+        flights,
+        build_request(max_stops=0),
+    )
+
+    assert len(ranked) == 1
+    assert ranked[0].option_id == "direct"
+
+
+def test_hotel_hard_filters() -> None:
+    service = CandidateSelectionService()
+
+    hotels = [
+        {
+            "hotel_id": "valid",
+            "name": "酒店A",
+            "price_per_night": 500,
+            "overall_rating": 4.6,
+            "location_rating": 4.5,
+        },
+        {
+            "hotel_id": "too-expensive",
+            "name": "酒店B",
+            "price_per_night": 800,
+            "overall_rating": 4.8,
+            "location_rating": 4.7,
+        },
+        {
+            "hotel_id": "low-rating",
+            "name": "酒店C",
+            "price_per_night": 400,
+            "overall_rating": 4.0,
+            "location_rating": 4.2,
+        },
+    ]
+
+    ranked, _ = service.rank_hotels(
+        hotels,
+        build_request(),
+    )
+
+    assert len(ranked) == 1
+    assert ranked[0].hotel_id == "valid"
+
+
+def test_round_trip_uses_return_quote() -> None:
+    service = CandidateSelectionService()
+
+    combinations = [
+        {
+            "outbound": {
+                "option_id": "out-1",
+                "price": 1000,
+                "duration_minutes": 180,
+                "stop_count": 0,
+                "currency": "CNY",
+            },
+            "return_flight": {
+                "option_id": "ret-1",
+                "price": 1800,
+                "duration_minutes": 190,
+                "stop_count": 0,
+                "currency": "CNY",
+            },
+        }
+    ]
+
+    ranked = (
+        service.rank_round_trip_combinations(
+            combinations,
+            build_request(),
+        )
+    )
+
+    assert len(ranked) == 1
+
+    # 不是1000 + 1800。
+    assert ranked[0].quoted_price == 1800

+ 102 - 0
tests/test_itinerary_schema.py

@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+from datetime import date, time
+
+import pytest
+from pydantic import ValidationError
+
+from app.schemas.itinerary import (
+    DailyItinerary,
+    FlightSelection,
+    HotelSelection,
+    ItineraryActivity,
+    ItineraryPlan,
+)
+
+
+def build_plan() -> ItineraryPlan:
+    return ItineraryPlan(
+        title="上海至成都四日旅行",
+        overview="轻松游览成都代表性景点。",
+        selected_flight=FlightSelection(
+            flight_type="round_trip",
+            combination_id="round-001",
+            outbound_option_id="out-001",
+            return_option_id="ret-001",
+            quoted_price=1800,
+            currency="CNY",
+            selection_reason="综合价格与时间。",
+        ),
+        selected_hotel=HotelSelection(
+            hotel_id="hotel-001",
+            name="测试酒店",
+            check_in_date=date(2026, 8, 10),
+            check_out_date=date(2026, 8, 13),
+            nights=3,
+            price_per_night=500,
+            estimated_total_price=1500,
+            currency="CNY",
+            selection_reason="靠近主要景点。",
+        ),
+        days=[
+            DailyItinerary(
+                day_index=1,
+                date=date(2026, 8, 10),
+                theme="抵达成都",
+                activities=[
+                    ItineraryActivity(
+                        sequence=1,
+                        activity_type="flight",
+                        name="前往成都",
+                        start_time=time(9, 0),
+                        end_time=time(12, 0),
+                    )
+                ],
+            )
+        ],
+    )
+
+
+def test_valid_itinerary_plan() -> None:
+    plan = build_plan()
+
+    assert plan.selected_hotel.nights == 3
+    assert plan.selected_flight.quoted_price == 1800
+    assert len(plan.days) == 1
+
+
+def test_round_trip_requires_return_option() -> None:
+    with pytest.raises(ValidationError):
+        FlightSelection(
+            flight_type="round_trip",
+            combination_id="round-001",
+            outbound_option_id="out-001",
+            return_option_id=None,
+            selection_reason="测试",
+        )
+
+
+def test_duplicate_day_date_is_rejected() -> None:
+    plan = build_plan()
+
+    duplicate_day = plan.days[0].model_copy(
+        update={
+            "day_index": 2,
+        }
+    )
+
+    with pytest.raises(ValidationError):
+        ItineraryPlan(
+            title=plan.title,
+            overview=plan.overview,
+            selected_flight=(
+                plan.selected_flight
+            ),
+            selected_hotel=(
+                plan.selected_hotel
+            ),
+            days=[
+                plan.days[0],
+                duplicate_day,
+            ],
+        )

+ 433 - 0
tests/test_plan_validation_service.py

@@ -0,0 +1,433 @@
+from __future__ import annotations
+
+from datetime import date, time
+
+from app.schemas.itinerary import (
+    DailyItinerary,
+    FlightSelection,
+    HotelSelection,
+    ItineraryActivity,
+    ItineraryPlan,
+)
+from app.schemas.place import (
+    MapResearchResult,
+    PlaceCandidate,
+)
+from app.schemas.route import (
+    HotelRouteEvaluation,
+    RouteEvaluationResult,
+)
+from app.schemas.selection import (
+    CandidateSelectionResult,
+    RankedHotelCandidate,
+    RoundTripOption,
+)
+from app.schemas.travel_request import (
+    TravelRequest,
+)
+from app.services.plan_validation_service import (
+    PlanValidationService,
+)
+
+
+def build_context(
+    *,
+    total_budget: float = 8000,
+):
+    request = TravelRequest(
+        origin_city="上海",
+        destination_city="成都",
+        departure_date=date(2026, 8, 10),
+        return_date=date(2026, 8, 13),
+        trip_days=4,
+        nights=3,
+        adults=2,
+        total_budget=total_budget,
+        pace="relaxed",
+    )
+
+    outbound = {
+        "option_id": "out-001",
+        "flight_numbers": ["MU001"],
+        "departure_time": (
+            "2026-08-10 08:00"
+        ),
+        "arrival_time": (
+            "2026-08-10 11:00"
+        ),
+        "price": 2000,
+        "currency": "CNY",
+    }
+
+    return_flight = {
+        "option_id": "ret-001",
+        "flight_numbers": ["MU002"],
+        "departure_time": (
+            "2026-08-13 12:00"
+        ),
+        "arrival_time": (
+            "2026-08-13 15:00"
+        ),
+        "price": 2000,
+        "currency": "CNY",
+    }
+
+    hotel = {
+        "hotel_id": "hotel-001",
+        "name": "成都测试酒店",
+        "price_per_night": 500,
+        "currency": "CNY",
+    }
+
+    selection = CandidateSelectionResult(
+        flight_type="round_trip",
+        round_trip_options=[
+            RoundTripOption(
+                combination_id="round-001",
+                score=90,
+                quoted_price=2000,
+                currency="CNY",
+                outbound=outbound,
+                return_flight=return_flight,
+                reasons=[],
+                warnings=[],
+            )
+        ],
+        hotels=[
+            RankedHotelCandidate(
+                hotel_id="hotel-001",
+                score=90,
+                reasons=[],
+                warnings=[],
+                hotel=hotel,
+            )
+        ],
+    )
+
+    map_result = MapResearchResult(
+        city="成都",
+        attractions=[
+            PlaceCandidate(
+                poi_id="poi-001",
+                name="测试景点",
+                location="104.01,30.01",
+            )
+        ],
+        restaurants=[
+            PlaceCandidate(
+                poi_id="food-001",
+                name="测试餐厅",
+                location="104.02,30.02",
+            )
+        ],
+    )
+
+    route_result = RouteEvaluationResult(
+        evaluated_hotels=[
+            HotelRouteEvaluation(
+                hotel_id="hotel-001",
+                hotel_name="成都测试酒店",
+                final_score=90,
+                base_hotel_score=85,
+                centrality_score=95,
+                subway_score=90,
+                hotel=hotel,
+            )
+        ],
+        selected_hotel_id="hotel-001",
+    )
+
+    plan = ItineraryPlan(
+        title="成都四日旅行",
+        overview="轻松游览成都。",
+        selected_flight=FlightSelection(
+            flight_type="round_trip",
+            combination_id="round-001",
+            outbound_option_id="out-001",
+            return_option_id="ret-001",
+            quoted_price=2000,
+            currency="CNY",
+            selection_reason="综合价格和时间。",
+        ),
+        selected_hotel=HotelSelection(
+            hotel_id="hotel-001",
+            name="成都测试酒店",
+            check_in_date=date(2026, 8, 10),
+            check_out_date=date(2026, 8, 13),
+            nights=3,
+            price_per_night=500,
+            estimated_total_price=1500,
+            currency="CNY",
+            selection_reason="位置较方便。",
+        ),
+        days=[
+            DailyItinerary(
+                day_index=1,
+                date=date(2026, 8, 10),
+                theme="抵达成都",
+                activities=[
+                    ItineraryActivity(
+                        sequence=1,
+                        activity_type="flight",
+                        name="上海飞往成都",
+                        start_time=time(8, 0),
+                        end_time=time(11, 0),
+                    ),
+                    ItineraryActivity(
+                        sequence=2,
+                        activity_type="transport",
+                        name="机场前往酒店",
+                        start_time=time(11, 30),
+                        end_time=time(13, 0),
+                    ),
+                    ItineraryActivity(
+                        sequence=3,
+                        activity_type=(
+                            "hotel_check_in"
+                        ),
+                        name="办理入住",
+                        start_time=time(14, 0),
+                        end_time=time(14, 30),
+                    ),
+                ],
+            ),
+            DailyItinerary(
+                day_index=2,
+                date=date(2026, 8, 11),
+                theme="景点游览",
+                activities=[
+                    ItineraryActivity(
+                        sequence=1,
+                        activity_type="attraction",
+                        reference_id="poi-001",
+                        name="测试景点",
+                        start_time=time(9, 0),
+                        end_time=time(12, 0),
+                    ),
+                    ItineraryActivity(
+                        sequence=2,
+                        activity_type="restaurant",
+                        reference_id="food-001",
+                        name="测试餐厅",
+                        start_time=time(12, 30),
+                        end_time=time(14, 0),
+                    ),
+                ],
+            ),
+            DailyItinerary(
+                day_index=3,
+                date=date(2026, 8, 12),
+                theme="自由活动",
+                activities=[
+                    ItineraryActivity(
+                        sequence=1,
+                        activity_type="free_time",
+                        name="自由活动",
+                        start_time=time(10, 0),
+                        end_time=time(16, 0),
+                    )
+                ],
+            ),
+            DailyItinerary(
+                day_index=4,
+                date=date(2026, 8, 13),
+                theme="返程",
+                activities=[
+                    ItineraryActivity(
+                        sequence=1,
+                        activity_type=(
+                            "hotel_check_out"
+                        ),
+                        name="办理退房",
+                        start_time=time(8, 0),
+                        end_time=time(9, 0),
+                    ),
+                    ItineraryActivity(
+                        sequence=2,
+                        activity_type="transport",
+                        name="前往机场",
+                        start_time=time(9, 0),
+                        end_time=time(10, 0),
+                    ),
+                    ItineraryActivity(
+                        sequence=3,
+                        activity_type="flight",
+                        name="成都飞往上海",
+                        start_time=time(12, 0),
+                        end_time=time(15, 0),
+                    ),
+                ],
+            ),
+        ],
+    )
+
+    return (
+        request,
+        plan,
+        selection,
+        map_result,
+        route_result,
+    )
+
+
+def test_valid_plan_passes() -> None:
+    service = PlanValidationService()
+
+    (
+        request,
+        plan,
+        selection,
+        map_result,
+        route_result,
+    ) = build_context()
+
+    result = service.validate(
+        request=request,
+        plan=plan,
+        selection=selection,
+        map_result=map_result,
+        route_result=route_result,
+    )
+
+    assert result.is_valid is True
+    assert result.error_count == 0
+    assert result.budget.known_total == 3500
+    assert (
+        result.budget.status
+        == "within_known_budget"
+    )
+
+
+def test_unknown_flight_combination_fails() -> None:
+    service = PlanValidationService()
+
+    (
+        request,
+        plan,
+        selection,
+        map_result,
+        route_result,
+    ) = build_context()
+
+    plan = plan.model_copy(
+        update={
+            "selected_flight": (
+                plan.selected_flight.model_copy(
+                    update={
+                        "combination_id": (
+                            "round-not-found"
+                        )
+                    }
+                )
+            )
+        }
+    )
+
+    result = service.validate(
+        request=request,
+        plan=plan,
+        selection=selection,
+        map_result=map_result,
+        route_result=route_result,
+    )
+
+    assert result.is_valid is False
+
+    codes = {
+        issue.code
+        for issue in result.issues
+    }
+
+    assert (
+        "FLIGHT_COMBINATION_NOT_FOUND"
+        in codes
+    )
+
+
+def test_unknown_attraction_fails() -> None:
+    service = PlanValidationService()
+
+    (
+        request,
+        plan,
+        selection,
+        map_result,
+        route_result,
+    ) = build_context()
+
+    invalid_activity = (
+        plan.days[1]
+        .activities[0]
+        .model_copy(
+            update={
+                "reference_id": "unknown-poi",
+                "name": "不存在的景点",
+            }
+        )
+    )
+
+    invalid_day = plan.days[1].model_copy(
+        update={
+            "activities": [
+                invalid_activity,
+                plan.days[1].activities[1],
+            ]
+        }
+    )
+
+    invalid_plan = plan.model_copy(
+        update={
+            "days": [
+                plan.days[0],
+                invalid_day,
+                plan.days[2],
+                plan.days[3],
+            ]
+        }
+    )
+
+    result = service.validate(
+        request=request,
+        plan=invalid_plan,
+        selection=selection,
+        map_result=map_result,
+        route_result=route_result,
+    )
+
+    assert result.is_valid is False
+
+    assert any(
+        issue.code.startswith("UNKNOWN_")
+        for issue in result.issues
+    )
+
+
+def test_known_cost_over_budget_fails() -> None:
+    service = PlanValidationService()
+
+    (
+        request,
+        plan,
+        selection,
+        map_result,
+        route_result,
+    ) = build_context(
+        total_budget=3000
+    )
+
+    result = service.validate(
+        request=request,
+        plan=plan,
+        selection=selection,
+        map_result=map_result,
+        route_result=route_result,
+    )
+
+    assert result.is_valid is False
+    assert result.budget.status == "over_budget"
+
+    assert any(
+        issue.code
+        == "KNOWN_BUDGET_EXCEEDED"
+        for issue in result.issues
+    )

+ 0 - 0
tests/test_requirement_validation.py


+ 95 - 0
tests/test_review_schema.py

@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+import pytest
+from pydantic import ValidationError
+
+from app.schemas.review import (
+    ReviewIssue,
+    TripReviewResult,
+)
+
+
+def test_approved_review_with_warning() -> None:
+    result = TripReviewResult(
+        approved=True,
+        summary="方案整体合理。",
+        strengths=[
+            "符合用户轻松旅行偏好",
+        ],
+        issues=[
+            ReviewIssue(
+                category="budget",
+                severity="warning",
+                message=(
+                    "餐饮和门票费用尚未计入。"
+                ),
+            )
+        ],
+        recommendations=[
+            "出发前确认门票价格。",
+        ],
+        revision_feedback=[],
+    )
+
+    assert result.approved is True
+    assert len(result.issues) == 1
+
+
+def test_approved_review_cannot_have_blocking_issue(
+) -> None:
+    with pytest.raises(ValidationError):
+        TripReviewResult(
+            approved=True,
+            summary="错误的通过结果。",
+            issues=[
+                ReviewIssue(
+                    category="pace",
+                    severity="blocking",
+                    message="行程安排严重过满。",
+                )
+            ],
+            revision_feedback=[],
+        )
+
+
+def test_rejected_review_requires_feedback() -> None:
+    with pytest.raises(ValidationError):
+        TripReviewResult(
+            approved=False,
+            summary="方案需要修改。",
+            issues=[
+                ReviewIssue(
+                    category="requirement_fit",
+                    severity="blocking",
+                    message="没有满足用户需求。",
+                )
+            ],
+            revision_feedback=[],
+        )
+
+
+def test_rejected_review_with_feedback() -> None:
+    result = TripReviewResult(
+        approved=False,
+        summary="返程日安排不合理。",
+        issues=[
+            ReviewIssue(
+                category="flight",
+                severity="blocking",
+                message=(
+                    "返程日前往机场时间不足。"
+                ),
+                day_index=4,
+                suggestion=(
+                    "取消上午景点并提前前往机场。"
+                ),
+            )
+        ],
+        revision_feedback=[
+            "取消返程日上午的景点活动,"
+            "至少提前3小时前往机场。",
+        ],
+    )
+
+    assert result.approved is False
+    assert len(result.revision_feedback) == 1

+ 178 - 0
tests/test_route_evaluation_service.py

@@ -0,0 +1,178 @@
+from __future__ import annotations
+
+from app.schemas.route import RouteLeg
+from app.schemas.selection import (
+    RankedHotelCandidate,
+)
+from app.services.route_evaluation_service import (
+    RouteEvaluationService,
+)
+
+
+def build_hotel(
+    hotel_id: str,
+    name: str,
+    score: float,
+) -> RankedHotelCandidate:
+    return RankedHotelCandidate(
+        hotel_id=hotel_id,
+        score=score,
+        reasons=[],
+        warnings=[],
+        hotel={
+            "hotel_id": hotel_id,
+            "name": name,
+        },
+    )
+
+
+def test_central_hotel_ranks_first() -> None:
+    service = RouteEvaluationService()
+
+    hotels = [
+        build_hotel(
+            "central",
+            "中心酒店",
+            80,
+        ),
+        build_hotel(
+            "remote",
+            "远郊酒店",
+            82,
+        ),
+    ]
+
+    legs_by_hotel = {
+        "central": [
+            RouteLeg(
+                origin_name="中心酒店",
+                origin_location=(
+                    "104.0700,30.6700"
+                ),
+                destination_name="景点A",
+                destination_location=(
+                    "104.0800,30.6800"
+                ),
+                distance_meters=2000,
+                duration_seconds=600,
+            ),
+            RouteLeg(
+                origin_name="中心酒店",
+                origin_location=(
+                    "104.0700,30.6700"
+                ),
+                destination_name="景点B",
+                destination_location=(
+                    "104.0900,30.6900"
+                ),
+                distance_meters=3000,
+                duration_seconds=900,
+            ),
+        ],
+        "remote": [
+            RouteLeg(
+                origin_name="远郊酒店",
+                origin_location=(
+                    "104.2000,30.8000"
+                ),
+                destination_name="景点A",
+                destination_location=(
+                    "104.0800,30.6800"
+                ),
+                distance_meters=15000,
+                duration_seconds=2400,
+            ),
+            RouteLeg(
+                origin_name="远郊酒店",
+                origin_location=(
+                    "104.2000,30.8000"
+                ),
+                destination_name="景点B",
+                destination_location=(
+                    "104.0900,30.6900"
+                ),
+                distance_meters=18000,
+                duration_seconds=3000,
+            ),
+        ],
+    }
+
+    result = service.rank_hotels(
+        hotels,
+        legs_by_hotel,
+        {
+            "central": None,
+            "remote": None,
+        },
+        require_near_subway=False,
+    )
+
+    assert result[0].hotel_id == "central"
+    assert (
+        result[0].average_distance_meters
+        == 2500
+    )
+
+
+def test_subway_distance_affects_score() -> None:
+    service = RouteEvaluationService()
+
+    hotels = [
+        build_hotel(
+            "near-subway",
+            "地铁酒店",
+            80,
+        ),
+        build_hotel(
+            "far-subway",
+            "远离地铁酒店",
+            80,
+        ),
+    ]
+
+    same_legs = [
+        RouteLeg(
+            origin_name="酒店",
+            origin_location="104.0,30.0",
+            destination_name="景点",
+            destination_location=(
+                "104.1,30.1"
+            ),
+            distance_meters=5000,
+        )
+    ]
+
+    result = service.rank_hotels(
+        hotels,
+        {
+            "near-subway": same_legs,
+            "far-subway": same_legs,
+        },
+        {
+            "near-subway": 300,
+            "far-subway": 1800,
+        },
+        require_near_subway=True,
+    )
+
+    assert (
+        result[0].hotel_id
+        == "near-subway"
+    )
+
+
+def test_haversine_fallback() -> None:
+    service = RouteEvaluationService()
+
+    leg = service.build_haversine_leg(
+        origin_name="酒店",
+        origin_location="104.0600,30.6700",
+        destination_name="景点",
+        destination_location=(
+            "104.0700,30.6800"
+        ),
+    )
+
+    assert leg.distance_meters > 0
+    assert leg.source == "haversine_fallback"
+    assert leg.duration_seconds is None

+ 193 - 0
tests/test_supervisor_nodes.py

@@ -0,0 +1,193 @@
+from __future__ import annotations
+
+from app.graph.supervisor_nodes import (
+    make_supervisor_node,
+)
+from app.schemas.review import (
+    ReviewIssue,
+    TripReviewResult,
+)
+from app.schemas.validation import (
+    BudgetEstimate,
+    PlanValidationResult,
+    ValidationIssue,
+)
+
+
+def build_validation(
+    *,
+    valid: bool,
+) -> PlanValidationResult:
+    issues = []
+
+    if not valid:
+        issues = [
+            ValidationIssue(
+                code="TEST_ERROR",
+                severity="error",
+                message="测试校验错误。",
+            )
+        ]
+
+    return PlanValidationResult(
+        is_valid=valid,
+        error_count=0 if valid else 1,
+        warning_count=0,
+        issues=issues,
+        budget=BudgetEstimate(
+            currency="CNY",
+            known_total=3000,
+            user_budget=8000,
+            remaining_known_budget=5000,
+            status="within_known_budget",
+            coverage_note="测试预算。",
+        ),
+        revision_feedback=(
+            []
+            if valid
+            else ["修复测试校验错误。"]
+        ),
+    )
+
+
+def build_review(
+    *,
+    approved: bool,
+) -> TripReviewResult:
+    if approved:
+        return TripReviewResult(
+            approved=True,
+            summary="方案通过。",
+        )
+
+    return TripReviewResult(
+        approved=False,
+        summary="方案未通过。",
+        issues=[
+            ReviewIssue(
+                category="pace",
+                severity="blocking",
+                message="行程过于紧凑。",
+                suggestion="减少每日景点数量。",
+            )
+        ],
+        revision_feedback=[
+            "减少每日景点数量。",
+        ],
+    )
+
+
+def test_supervisor_approves_valid_plan() -> None:
+    node = make_supervisor_node(
+        max_planning_attempts=3
+    )
+
+    result = node(
+        {
+            "planning_attempts": 1,
+            "plan_validation_result": (
+                build_validation(valid=True)
+            ),
+            "trip_review_result": (
+                build_review(approved=True)
+            ),
+            "review_passed": True,
+            "errors": [],
+        }
+    )
+
+    assert (
+        result["supervisor_decision"]
+        == "finalize"
+    )
+
+    assert (
+        result["workflow_status"]
+        == "approved"
+    )
+
+
+def test_supervisor_requests_replan() -> None:
+    node = make_supervisor_node(
+        max_planning_attempts=3
+    )
+
+    result = node(
+        {
+            "planning_attempts": 1,
+            "plan_validation_result": (
+                build_validation(valid=False)
+            ),
+            "trip_review_result": (
+                build_review(approved=False)
+            ),
+            "review_passed": False,
+            "revision_feedback": [],
+            "errors": [],
+        }
+    )
+
+    assert (
+        result["supervisor_decision"]
+        == "replan"
+    )
+
+    assert result["revision_feedback"]
+
+
+def test_supervisor_stops_after_max_attempts(
+) -> None:
+    node = make_supervisor_node(
+        max_planning_attempts=3
+    )
+
+    result = node(
+        {
+            "planning_attempts": 3,
+            "plan_validation_result": (
+                build_validation(valid=False)
+            ),
+            "trip_review_result": (
+                build_review(approved=False)
+            ),
+            "review_passed": False,
+            "revision_feedback": [],
+            "errors": [],
+        }
+    )
+
+    assert (
+        result["supervisor_decision"]
+        == "finalize_with_risks"
+    )
+
+    assert (
+        result["workflow_status"]
+        == "completed_with_risks"
+    )
+
+
+def test_supervisor_terminates_system_error(
+) -> None:
+    node = make_supervisor_node(
+        max_planning_attempts=3
+    )
+
+    result = node(
+        {
+            "planning_attempts": 1,
+            "errors": [
+                "MCP连接失败。",
+            ],
+        }
+    )
+
+    assert (
+        result["supervisor_decision"]
+        == "terminate"
+    )
+
+    assert (
+        result["workflow_status"]
+        == "failed"
+    )

+ 2380 - 0
uv.lock

@@ -0,0 +1,2380 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+resolution-markers = [
+    "python_full_version >= '3.14'",
+    "python_full_version >= '3.11' and python_full_version < '3.14'",
+    "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "idna" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" },
+    { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" },
+    { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" },
+    { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" },
+    { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" },
+    { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" },
+    { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" },
+    { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" },
+    { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" },
+    { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" },
+    { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" },
+    { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" },
+    { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" },
+    { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" },
+    { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" },
+    { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" },
+    { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" },
+    { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" },
+    { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" },
+    { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" },
+    { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" },
+    { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" },
+    { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" },
+    { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" },
+    { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" },
+    { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" },
+    { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" },
+    { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" },
+    { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" },
+    { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" },
+    { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" },
+    { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" },
+    { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" },
+    { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" },
+    { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" },
+    { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" },
+    { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" },
+    { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" },
+    { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" },
+    { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" },
+    { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" },
+    { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" },
+    { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
+    { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
+    { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
+    { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
+    { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
+    { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
+    { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
+    { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
+    { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
+    { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
+    { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
+    { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
+    { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
+    { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
+    { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
+    { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
+    { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
+    { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
+    { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
+    { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
+    { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
+]
+
+[[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, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "49.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
+    { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
+    { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
+    { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
+    { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
+    { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
+    { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
+    { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
+    { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
+    { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
+    { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
+    { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
+    { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
+    { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
+    { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
+    { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" },
+    { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "certifi" },
+    { name = "httpcore" },
+    { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[package.optional-dependencies]
+socks = [
+    { name = "socksio" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" },
+    { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" },
+    { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" },
+    { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" },
+    { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" },
+    { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" },
+    { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" },
+    { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" },
+    { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" },
+    { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" },
+    { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" },
+    { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" },
+    { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" },
+    { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" },
+    { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" },
+    { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
+    { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
+    { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
+    { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
+    { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
+    { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
+    { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
+    { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
+    { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
+    { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
+    { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
+    { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
+    { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
+    { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
+    { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
+    { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
+    { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
+    { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
+    { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
+    { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
+    { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
+    { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" },
+    { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" },
+    { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" },
+    { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" },
+    { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "jsonschema-specifications" },
+    { name = "referencing" },
+    { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+    { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "langchain"
+version = "1.3.14"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "langgraph" },
+    { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a9/ec/0f942e78a621f8e3162ff1ed24284f469aaf51fb4607ee5831c626f2b2bc/langchain-1.3.14-py3-none-any.whl", hash = "sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782", size = 139560, upload-time = "2026-07-16T13:28:16.498Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsonpatch" },
+    { name = "langchain-protocol" },
+    { name = "langsmith" },
+    { name = "packaging" },
+    { name = "pydantic" },
+    { name = "pyyaml" },
+    { name = "tenacity" },
+    { name = "typing-extensions" },
+    { name = "uuid-utils" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
+]
+
+[[package]]
+name = "langchain-mcp-adapters"
+version = "0.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "mcp" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/5d/1f43b6a14cf448ea47bceb2860e9a6e3b0a911de0719aba43ed3eb927195/langchain_mcp_adapters-0.3.1.tar.gz", hash = "sha256:e08d4a74a0934d558ddb807ecbb8f89466c93a0d06c1fb58974b93b4703de01c", size = 47140, upload-time = "2026-07-27T18:36:45.792Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/22/8d/934946caf18559158678099ac00e6eed67d78e68c4e73c7afd01e19d31ec/langchain_mcp_adapters-0.3.1-py3-none-any.whl", hash = "sha256:1e354d9466db9fab9e30f24419adffbd39a84229e7f452927f99ea799e1937f9", size = 28877, upload-time = "2026-07-27T18:36:44.762Z" },
+]
+
+[[package]]
+name = "langchain-openai"
+version = "1.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "openai" },
+    { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/1b/a83bf6cae4632363cef0b6f2ee1b4f62c8a5ebcf22cd8ef24430a736c2a8/langchain_openai-1.4.1.tar.gz", hash = "sha256:6d16be615d997db80294731b8e768783f1fb8e0313668e64acd50cd68acbad20", size = 3262416, upload-time = "2026-07-23T20:31:13.053Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fe/1c/8b604dc8be2735c8ae5c655e520066231057d1301958c20c776e62bd00fb/langchain_openai-1.4.1-py3-none-any.whl", hash = "sha256:8528bb34cc78fdfd2d895573c7917f9441cbb82db5f18ae0e6b3b75d95bdefb3", size = 122067, upload-time = "2026-07-23T20:31:11.809Z" },
+]
+
+[[package]]
+name = "langchain-protocol"
+version = "0.0.18"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" },
+]
+
+[[package]]
+name = "langgraph"
+version = "1.2.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "langgraph-checkpoint" },
+    { name = "langgraph-prebuilt" },
+    { name = "langgraph-sdk" },
+    { name = "pydantic" },
+    { name = "xxhash" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/1d/a32f3caf4b3d60651656c0d64976b48d168653e81c71bb7512e9a31541aa/langgraph-1.2.10.tar.gz", hash = "sha256:05a183a746ed570a06c7c1b879920163509a75df9e44e92dd2238218d677fd37", size = 723404, upload-time = "2026-07-28T18:33:51.441Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e2/4d/3fc3e2535ee2c731130d71371848ebc6d4a9d2e8ae6060b11987ba134951/langgraph-1.2.10-py3-none-any.whl", hash = "sha256:52c48bd42fa31a1de0e1c0f0ebfe342e11ca2957b8b3563f83dbd60d8e30f921", size = 247753, upload-time = "2026-07-28T18:33:50.028Z" },
+]
+
+[[package]]
+name = "langgraph-checkpoint"
+version = "4.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "ormsgpack" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" },
+]
+
+[[package]]
+name = "langgraph-prebuilt"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "langchain-core" },
+    { name = "langgraph-checkpoint" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" },
+]
+
+[[package]]
+name = "langgraph-sdk"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "httpx" },
+    { name = "langchain-core" },
+    { name = "langchain-protocol" },
+    { name = "orjson" },
+    { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.10.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "distro" },
+    { name = "httpx" },
+    { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+    { name = "packaging" },
+    { name = "pydantic" },
+    { name = "requests" },
+    { name = "requests-toolbelt" },
+    { name = "sniffio" },
+    { name = "typing-extensions" },
+    { name = "uuid-utils" },
+    { name = "websockets" },
+    { name = "xxhash" },
+    { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/c3/7203f18c6ca0ff4c65087fe8694511fadc8a0037c37010e43d25b9ecacd8/langsmith-0.10.12.tar.gz", hash = "sha256:e27cace529a5c54546ecdc2d3fa7af6d8a84a5b9333e99b497f47cafffa16ef5", size = 4749918, upload-time = "2026-07-29T14:22:52.335Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/f8/de3b66a9b9212ed41541814d8e062909c009837e650a4c2e43425ee1b43c/langsmith-0.10.12-py3-none-any.whl", hash = "sha256:d280fff7a03ecf612c3fdb172bf716467cfb9c0b9d90661b3523c2d8ee1b9a6e", size = 681040, upload-time = "2026-07-29T14:22:50.372Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.29.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "httpx" },
+    { name = "httpx-sse" },
+    { name = "jsonschema" },
+    { name = "pydantic" },
+    { name = "pydantic-settings" },
+    { name = "pyjwt", extra = ["crypto"] },
+    { name = "python-multipart" },
+    { name = "pywin32", marker = "sys_platform == 'win32'" },
+    { name = "sse-starlette" },
+    { name = "starlette" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+    { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+    { name = "python-dotenv" },
+    { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "openai"
+version = "2.51.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "distro" },
+    { name = "httpx" },
+    { name = "jiter" },
+    { name = "pydantic" },
+    { name = "sniffio" },
+    { name = "tqdm" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/75/d8/06fda9685e47d9a8fc177ef57f8af75207938fad49a45ce23bfa7b6a2a5c/openai-2.51.0.tar.gz", hash = "sha256:4d61287c42eba54086d09346e709cbf7f8cec51822efce9cc399450b9385fba5", size = 1083410, upload-time = "2026-07-30T17:43:26.507Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/57/6f/c49e21245ad4865e886f6885e95e998bc024335b392c202bd571639e9d50/openai-2.51.0-py3-none-any.whl", hash = "sha256:91db13ce59a670fddc820a6983989650095e43e3acac09288bceb69356a8904e", size = 1652344, upload-time = "2026-07-30T17:43:24.225Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/10/5d/b95ca542a001135cc250a49370f282f578c8f4e46cc8617d73775297eea8/orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce", size = 228986, upload-time = "2026-05-06T15:09:14.765Z" },
+    { url = "https://files.pythonhosted.org/packages/80/01/be33fbff646e22f93398429ea645f20d2097aea1a6cdc1e6628e70125f83/orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd", size = 132558, upload-time = "2026-05-06T15:09:17.431Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/61/73d49333bba660a075daccca10970dc6409ce1cf42ae4046646a19468aad/orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4", size = 128213, upload-time = "2026-05-06T15:09:18.719Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/7d/30e844b3dac3f74aed66b1f984daf9db3c98c0328c03d965a9e8dc06449e/orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4", size = 135430, upload-time = "2026-05-06T15:09:20.257Z" },
+    { url = "https://files.pythonhosted.org/packages/16/64/bd815f5c610b3facc204f26ba94e87a9eb49b0d83de3d5fc1eee2402d91b/orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e", size = 146178, upload-time = "2026-05-06T15:09:21.616Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/35/e744fd36c79b339d27beb06068b5a08a8882ef5418804d0ce545a31f718d/orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb", size = 133068, upload-time = "2026-05-06T15:09:23.228Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/56/d54152b67b63a0b3e556cfc549d6ce84f74d7f425ddeadc6c8a74d913da7/orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47", size = 134217, upload-time = "2026-05-06T15:09:24.847Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/ee/66154baf69f71c7164a268a5e888908aec5a0819d13c81d5e2755a257758/orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d", size = 141917, upload-time = "2026-05-06T15:09:26.647Z" },
+    { url = "https://files.pythonhosted.org/packages/09/d3/c5824260ca8b9d7ba82648d042a3f8f4815d18c15bb98a1f30edd1bb2d83/orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13", size = 415356, upload-time = "2026-05-06T15:09:28.252Z" },
+    { url = "https://files.pythonhosted.org/packages/64/cb/509c2e816fe4df641d93dc92f6a89adc8df3ada8ebdee2bd44aba3264c3c/orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92", size = 148112, upload-time = "2026-05-06T15:09:29.783Z" },
+    { url = "https://files.pythonhosted.org/packages/db/b5/3ceae56d2e4962979eedb023ba6a46a4bb65f333960379be0ca470686220/orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48", size = 137112, upload-time = "2026-05-06T15:09:31.432Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/7a/81fa3f2c7bef79b04cf2ab7838e5ac74b1f12511ceab979759b0275d6bb4/orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94", size = 131706, upload-time = "2026-05-06T15:09:32.707Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/d8/b64600f9083c7f151ad39717a5877fccbeb0ef6d7efcb55f971ce00b6bee/orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244", size = 127282, upload-time = "2026-05-06T15:09:33.955Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" },
+    { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" },
+    { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" },
+    { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" },
+    { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" },
+    { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" },
+    { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" },
+    { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" },
+    { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" },
+    { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" },
+    { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" },
+    { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" },
+    { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
+    { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
+    { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
+    { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
+    { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
+    { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
+    { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
+    { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
+    { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
+    { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
+    { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
+    { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
+    { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
+]
+
+[[package]]
+name = "ormsgpack"
+version = "1.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" },
+    { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" },
+    { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" },
+    { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" },
+    { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" },
+    { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" },
+    { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" },
+    { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" },
+    { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" },
+    { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" },
+    { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" },
+    { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" },
+    { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" },
+    { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" },
+    { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[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, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
+    { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
+    { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
+    { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
+    { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
+    { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+    { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+    { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+    { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+    { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+    { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+    { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+    { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+    { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+    { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+    { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+    { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+    { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+    { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+    { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+    { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+    { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+    { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+    { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+    { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+    { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+    { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+    { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+    { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+    { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+    { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+    { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+    { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+    { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+    { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+    { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+    { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+    { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+    { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+    { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+    { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+    { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+    { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+    { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pydantic" },
+    { name = "python-dotenv" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+    { name = "cryptography" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "pygments" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.32"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" },
+    { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" },
+    { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" },
+    { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
+    { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
+    { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
+    { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
+    { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+    { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+    { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+    { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+    { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+    { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+    { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+    { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+    { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+    { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+    { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+    { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+    { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+    { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+    { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+    { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+    { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+    { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+    { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+    { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+    { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+    { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+    { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+    { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+    { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+    { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2026.7.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/24/13/bbf7d9d1887fe4a3693527c6caa232c197ea9da91f1212e9672eff60329d/regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b", size = 494009, upload-time = "2026-07-19T00:16:13.602Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/19/783688e75a2bec15d50aec0d5e7e317d363808bc82a6eb6750b897bfcd7b/regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52", size = 295287, upload-time = "2026-07-19T00:16:15.702Z" },
+    { url = "https://files.pythonhosted.org/packages/16/01/cefe4f051302ca298d3f3e79ed6dbd933ac84485b9515acdd6a52d70cef7/regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665", size = 290633, upload-time = "2026-07-19T00:16:17.182Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/1e/1045ca2cabb12e8ec41ad0d138e9f3ff1eb079d30a6f51ccf7d709b44aad/regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0", size = 785300, upload-time = "2026-07-19T00:16:18.505Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/4a/20e5bca184e90bf1bd187efdb53363f4a7b7b34f01d54ced5740caf104bd/regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951", size = 854079, upload-time = "2026-07-19T00:16:19.909Z" },
+    { url = "https://files.pythonhosted.org/packages/09/9b/5a2e59678be3b24aa6a42b2c6d66a48daa212593e9f4096fe7ba577fa9b1/regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3", size = 899496, upload-time = "2026-07-19T00:16:21.453Z" },
+    { url = "https://files.pythonhosted.org/packages/48/9a/7317f14ed8ed9fd998d1978b4802b07bc4d79216353c435dbcc1ddd1301f/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c", size = 793541, upload-time = "2026-07-19T00:16:22.991Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/53/833c2db3e274d3c191f4c42fe5bfa358e4c8b617d5d7312d31334965fc46/regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6", size = 785515, upload-time = "2026-07-19T00:16:24.654Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/b9/efb2f9fa151d71db09d4015e1fb92fee47416f01c12164836bbd23e2f3c2/regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175", size = 769556, upload-time = "2026-07-19T00:16:26.207Z" },
+    { url = "https://files.pythonhosted.org/packages/81/4d/45610c263f8eadb84e4a1fabd904d81d5176226faa9104ef498bf8a8b285/regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6", size = 774130, upload-time = "2026-07-19T00:16:27.786Z" },
+    { url = "https://files.pythonhosted.org/packages/40/95/1b40d87c7a9e5480bec7a87bce9fd67fc3f14b5f106c8ee66d660249072f/regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095", size = 848694, upload-time = "2026-07-19T00:16:29.412Z" },
+    { url = "https://files.pythonhosted.org/packages/17/8b/bb45968addd5b394ef9cd9184bd9c65ade1a819dbb2b92b71ad52a0c7907/regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0", size = 758505, upload-time = "2026-07-19T00:16:31.006Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/6f/33386c672fbf43e21602135a0f29a97ee251a483f007fe51d10e9b2dbc93/regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a", size = 836985, upload-time = "2026-07-19T00:16:32.459Z" },
+    { url = "https://files.pythonhosted.org/packages/22/f1/9112b86e9bb075619862e8e42b604794389f1958faa68fb69bde505dd90e/regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902", size = 782610, upload-time = "2026-07-19T00:16:33.857Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/f9/13d460d8a385ca0b0be9e6be80a90968b9293b3e30895543ad2d1d1653e4/regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e", size = 266772, upload-time = "2026-07-19T00:16:35.194Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/90/29addd7a03e1aea402c1f31467e25c80caabc8c3735b88a23cf73b0aa9c2/regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db", size = 277967, upload-time = "2026-07-19T00:16:36.847Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/ba/ecfce06fe66c122bc6f77ae284887a9282e4411fe1e6268c5266611ca054/regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6", size = 276963, upload-time = "2026-07-19T00:16:38.315Z" },
+    { url = "https://files.pythonhosted.org/packages/05/e5/cef4de2bac939280b68d32adc659478845238a8274f2f79c465063f590ad/regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c", size = 494012, upload-time = "2026-07-19T00:16:39.927Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/87/e86f51eb117457bb7803132ffe5cb6e2841e2b5bea4cc85d397f3c6e257d/regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae", size = 295281, upload-time = "2026-07-19T00:16:41.433Z" },
+    { url = "https://files.pythonhosted.org/packages/41/2e/2360c41d8080a3d9ec7e5c90fad6eab3b50192869d10e9a5609e48c8177b/regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5", size = 290615, upload-time = "2026-07-19T00:16:43.058Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/69/b65ba4344efbc771b28fe5dde84cbbb6c8f9551165952fe78def5b9dde6a/regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20", size = 791804, upload-time = "2026-07-19T00:16:44.662Z" },
+    { url = "https://files.pythonhosted.org/packages/81/b6/a40dfa0dc6224b36f620c00296eacc830489cbf8c2837b6750dfe6170375/regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a", size = 861723, upload-time = "2026-07-19T00:16:46.412Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/02/735991dee71abd83196a7962f7ed8bf5aa05720ff06e2d3ff896a85e2bbb/regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7", size = 905932, upload-time = "2026-07-19T00:16:47.956Z" },
+    { url = "https://files.pythonhosted.org/packages/45/6c/e7098d8b846ccdbf431d8c081b61e496526a27a28094ed09e0dce21b3f54/regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc", size = 801407, upload-time = "2026-07-19T00:16:49.43Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/18/34b69274e2649bcc7d9b089c2b2983fb2632d8ecf667e359593be9072e79/regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864", size = 774448, upload-time = "2026-07-19T00:16:51.352Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/e6/0a72247d025585fd3800b98e040b84d562a88af6303347100484849f4f01/regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af", size = 783297, upload-time = "2026-07-19T00:16:53.071Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/aa/c4f65ae7dd02a36b323a70c4cff326e1f3442361aaebc9311100a130d54f/regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5", size = 854736, upload-time = "2026-07-19T00:16:54.607Z" },
+    { url = "https://files.pythonhosted.org/packages/62/c3/668082bcc817b9e694189b84997aeba7385b7779faa6711788679c482e35/regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82", size = 763298, upload-time = "2026-07-19T00:16:56.289Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/fb/2d07ad555e7af88aa5f867fdafa47a8d945ee237c20af3ebceb46a820835/regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78", size = 844430, upload-time = "2026-07-19T00:16:57.933Z" },
+    { url = "https://files.pythonhosted.org/packages/51/15/c82a471fe3dce56f03745635b43aa456c40dc0db089e07ef148b331507d1/regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327", size = 789683, upload-time = "2026-07-19T00:16:59.583Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/f4/7532a2c59d56f5398902c20de60f0c9a5d1cd364e42a051b48e1b210be7b/regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d", size = 266778, upload-time = "2026-07-19T00:17:01.032Z" },
+    { url = "https://files.pythonhosted.org/packages/83/2b/cf1bc631db154eb95520d9d5dbc2371ff77a0f014bbf7d748fed8496aa63/regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965", size = 277983, upload-time = "2026-07-19T00:17:02.571Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/bd/56ceaf170e875d5a6761bf2bfd0d040f1cacc896850d5e40cb29b11bbd06/regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e", size = 276961, upload-time = "2026-07-19T00:17:04.135Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" },
+    { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" },
+    { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" },
+    { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" },
+    { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" },
+    { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" },
+    { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" },
+    { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" },
+    { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" },
+    { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" },
+    { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" },
+    { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" },
+    { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" },
+    { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" },
+    { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" },
+    { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" },
+    { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" },
+    { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" },
+    { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" },
+    { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" },
+    { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" },
+    { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" },
+    { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" },
+    { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" },
+    { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" },
+    { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" },
+    { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" },
+    { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" },
+    { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" },
+    { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" },
+    { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" },
+    { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" },
+    { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" },
+    { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" },
+    { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" },
+    { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" },
+    { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" },
+    { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "charset-normalizer" },
+    { name = "idna" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py" },
+    { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+    "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+    { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+    { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+    { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+    { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+    { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+    { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+    { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+    { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+    { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+    { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+    { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+    { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+    { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+    { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+    { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+    { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+    { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+    { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+    { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+    { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+    { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+    { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+    { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+    { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+    { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+    { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+    { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+    { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+    { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+    { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+    { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+    { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+    { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+    { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+    { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+    { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+    { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+    { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+    { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+    { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+    { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+    { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+    { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+    { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+    { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+    { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+    { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+    { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+    { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+    { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+    { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+    "python_full_version >= '3.14'",
+    "python_full_version >= '3.11' and python_full_version < '3.14'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" },
+    { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" },
+    { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" },
+    { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" },
+    { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
+    { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
+    { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
+    { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
+    { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
+    { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+    { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+    { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+    { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+    { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+    { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
+    { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
+    { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
+    { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
+    { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
+    { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
+    { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
+    { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
+    { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
+    { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
+    { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
+    { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
+    { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
+    { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
+    { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
+    { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
+    { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
+    { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
+    { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
+    { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" },
+    { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" },
+    { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" },
+    { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "socksio"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.4.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/10/a34c656829ffc1c4b22ef36d70d9ebb6b99c020e2aeb17cee5485099f028/sse_starlette-3.4.6.tar.gz", hash = "sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627", size = 32542, upload-time = "2026-07-20T14:16:32.201Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/49/36/e10c1d1b7ca881d2625db2ec28508578499187bb1c389952c398474e1834/sse_starlette-3.4.6-py3-none-any.whl", hash = "sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6", size = 16516, upload-time = "2026-07-20T14:16:30.978Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "regex" },
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" },
+    { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" },
+    { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" },
+    { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" },
+    { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" },
+    { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" },
+    { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" },
+    { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" },
+    { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" },
+    { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" },
+    { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" },
+    { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" },
+    { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" },
+    { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" },
+    { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" },
+    { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" },
+    { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" },
+    { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" },
+    { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" },
+    { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" },
+    { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" },
+    { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+    { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+    { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+    { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+    { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+    { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+    { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+    { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+    { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+    { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+    { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+    { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+    { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+    { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+    { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+    { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+    { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+    { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+    { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+    { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+    { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+    { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+    { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+    { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+    { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+    { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.70.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
+]
+
+[[package]]
+name = "travel-mind"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+    { name = "httpx", extra = ["socks"] },
+    { name = "langchain" },
+    { name = "langchain-core" },
+    { name = "langchain-mcp-adapters" },
+    { name = "langchain-openai" },
+    { name = "langgraph" },
+    { name = "mcp", extra = ["cli"] },
+    { name = "pydantic-settings" },
+    { name = "rich" },
+]
+
+[package.dev-dependencies]
+dev = [
+    { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "httpx", extras = ["socks"], specifier = ">=0.27" },
+    { name = "langchain", specifier = ">=1.0.0" },
+    { name = "langchain-core", specifier = ">=1.5.2" },
+    { name = "langchain-mcp-adapters", specifier = ">=0.3.1" },
+    { name = "langchain-openai", specifier = ">=0.3.0" },
+    { name = "langgraph", specifier = ">=0.4.0" },
+    { name = "mcp", extras = ["cli"], specifier = ">=1.9.2,<2.0.0" },
+    { name = "pydantic-settings", specifier = ">=2.2" },
+    { name = "rich", specifier = ">=13.7" },
+]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pytest", specifier = ">=9.1.1" }]
+
+[[package]]
+name = "typer"
+version = "0.27.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "annotated-doc" },
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "rich" },
+    { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
+[[package]]
+name = "uuid-utils"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7c/60/659104207938f2ac62508b9aa595fc0515ac7452dd515c8e1d47d0b91169/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d2d9a63a9e6f2416ace8c109043a9280d6b34f34bb2e5421903e149403db40a6", size = 564038, upload-time = "2026-07-09T13:47:51.731Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/e7/e0d048a268b4163058bdd2f07a45bbe13c29e3cc6b7b88f8f00b001617ce/uuid_utils-0.17.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b776c7fc8755c7de06dd5a22b47c40ae84f67d13277ebb233cc84933ba4dcbcd", size = 286680, upload-time = "2026-07-09T13:47:53.141Z" },
+    { url = "https://files.pythonhosted.org/packages/84/83/e3606dc9b4224d0c9a6675d9347e7e0da7e67fa30e061bfdb686138844d0/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1edf2f8732e4ed95bd7b65f2658f4aa072efaaff321144f4e0d4bf6a22709263", size = 323533, upload-time = "2026-07-09T13:47:54.433Z" },
+    { url = "https://files.pythonhosted.org/packages/22/f8/aec5c34fa80c9fef09a506a098015e728080076494b72b9e8e5cfc9669c4/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84ed3a2d5cd3ae6db87af20bfed3331116195ba4757ad7177fc8f12c1bbce2a9", size = 330691, upload-time = "2026-07-09T13:47:55.677Z" },
+    { url = "https://files.pythonhosted.org/packages/08/73/85776566863514f37b0a761648368e96b07d64981a9b6c391220aa2563a9/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bf4d9cd1e80e73922073b9b27c143bedeb109d65f94cd12712e2c87118f2b7d", size = 444094, upload-time = "2026-07-09T13:47:56.936Z" },
+    { url = "https://files.pythonhosted.org/packages/68/06/e0424b4268c0932e0ff8257303d70de4053f05958843268fac4cb0f79b57/uuid_utils-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52db0e471d3d2632d35445af352591f40a8f32959a412981d9f51e068bb9514b", size = 324548, upload-time = "2026-07-09T13:47:58.217Z" },
+    { url = "https://files.pythonhosted.org/packages/db/d2/a0cb3a69ef6d9becc30a6a0594ddf6f798f6204953dfa85073cbec875b94/uuid_utils-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:344f7c755e280ea0ba6aeb08022190d867a80000b1715cacded54fc4b5633607", size = 350307, upload-time = "2026-07-09T13:47:59.418Z" },
+    { url = "https://files.pythonhosted.org/packages/82/81/d82766af7db541e4a78b920bc1c4303d44995f841805d1498934088cd12c/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:589d9da7de8fa7f739bb970ac4632c9a268213117d634e1c4a58c1c1e821ca05", size = 500661, upload-time = "2026-07-09T13:48:00.726Z" },
+    { url = "https://files.pythonhosted.org/packages/10/71/b261cd0d38497ed8c2cce0263c5607ec9cd2bbace0f73cb19a6fc2060b6e/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cee808b405e9095506f4e4e89924bec7ea77eac3129b6fe36eda04364b3b343b", size = 606577, upload-time = "2026-07-09T13:48:02.539Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/63/9e48512bb235e9533adbb25c30fd0c9cef09f6ecefe131ba392b98572b40/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:53ce348ef4c6e98c02c19c522af01334fe94476ce9af0db8c4482f9f142ae9c1", size = 567054, upload-time = "2026-07-09T13:48:03.833Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/cc/d7bad8799a37ec33fc21b29fcb459d63d9f88aa09056d0c3e58903ba2fb0/uuid_utils-0.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e753e81457241e2200c56a898e268e8fa25796271af0489c608f24d8e631eed", size = 529682, upload-time = "2026-07-09T13:48:05.097Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/3b/59b1e07ada8aadd3c046c97fe9814d85e770abb7e8cf68d5d86538bf62e9/uuid_utils-0.17.0-cp310-cp310-win32.whl", hash = "sha256:c589f5023d471ce75dd2cce61acb25ed6347e562041588a1a366808f22d7176c", size = 170595, upload-time = "2026-07-09T13:48:06.411Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/5c/23a2d0253ada2ee8c497d541d4ef0dd5576c3d2454ec2f9d0b8a06af9304/uuid_utils-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:981cc10163988defea96e8d6c507df151eab8f483e7df9ae543d5a41a4be073b", size = 177225, upload-time = "2026-07-09T13:48:07.561Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/b2/8f03b61f0aa4afc687855c4f00db35f4d3e58c480cd885abc46f6e41308f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371", size = 563901, upload-time = "2026-07-09T13:48:08.961Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/cb/88b909ffb9ac11f88d2e6ceabc592ccc660b5830b06dbcbd290ab8981f1f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2", size = 286383, upload-time = "2026-07-09T13:48:10.2Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/b8/bc5b64e9898867227c535cd0366c571c580a736748e81329437c1773e442/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479", size = 323244, upload-time = "2026-07-09T13:48:11.477Z" },
+    { url = "https://files.pythonhosted.org/packages/13/d9/8a17462ce066fbf89670fb737a3f0c93a77816736d2a4d134787e759d8ea/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1", size = 330466, upload-time = "2026-07-09T13:48:13.092Z" },
+    { url = "https://files.pythonhosted.org/packages/43/37/0c65d0db3bae45183419756d938f1791a82c835fd92bf234eb4f008d2e02/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f", size = 443806, upload-time = "2026-07-09T13:48:14.372Z" },
+    { url = "https://files.pythonhosted.org/packages/32/d5/7e698466d1f5254620b5ee0d711fdd20a0e9c2acd7040740c37193a8f673/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46", size = 324261, upload-time = "2026-07-09T13:48:15.642Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/48/3a5b242d7f0b8e3ca77dcd7177f3cf73e0280cee32e2349d9796ca27f183/uuid_utils-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0", size = 350657, upload-time = "2026-07-09T13:48:17.273Z" },
+    { url = "https://files.pythonhosted.org/packages/95/f4/f32ea82a89efed2eafee2f1d925d64687a81e550a9951933fb1b75c95ca6/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7", size = 500613, upload-time = "2026-07-09T13:48:18.459Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/5c/c7b73ec4bbe28db162a4841d352c6eda582801e0dd9fe72f6ad5cc584ee4/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803", size = 606306, upload-time = "2026-07-09T13:48:19.726Z" },
+    { url = "https://files.pythonhosted.org/packages/63/95/8a2777204e8691b4961e6aa619001c3e5175aa430ab43da3079142e8d310/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb", size = 567231, upload-time = "2026-07-09T13:48:21.024Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/6f/1d778ca3ed6d2cf35f22088e2de714675416747ab41be510f22c141043a7/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5", size = 529373, upload-time = "2026-07-09T13:48:22.312Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/d3/9ad1ab64b3bed0a0237d1db89dc6f5001d6116a82766753da4ac4496f979/uuid_utils-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13", size = 169930, upload-time = "2026-07-09T13:48:23.504Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/1a/e01417f52eae6e2cb412260bb332b4ee4b37af2982d9c38cff4b68b2e899/uuid_utils-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70", size = 177242, upload-time = "2026-07-09T13:48:24.723Z" },
+    { url = "https://files.pythonhosted.org/packages/35/20/396c27f996add19f8ac31e49cc4570824e51a97719087dabf94694d25bc4/uuid_utils-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2", size = 177023, upload-time = "2026-07-09T13:48:25.834Z" },
+    { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" },
+    { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" },
+    { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" },
+    { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" },
+    { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" },
+    { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" },
+    { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" },
+    { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" },
+    { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" },
+    { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" },
+    { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" },
+    { url = "https://files.pythonhosted.org/packages/56/44/e2fd3fdf356e1b55d2acf1b956b4f3f29ffb215a99c387eba04b1c5fba66/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd", size = 562232, upload-time = "2026-07-09T13:49:06.201Z" },
+    { url = "https://files.pythonhosted.org/packages/19/28/65e0980d668a6d44e699f59d1acf43d6b5d4893592c115ce7c680bb4dfa1/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a", size = 287858, upload-time = "2026-07-09T13:49:07.45Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/8d/5e97bcebc90fb6a10f98af3dc1ba552e04183aba59e2edc0b9cf486dd998/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc", size = 321587, upload-time = "2026-07-09T13:49:09.489Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/d7/88b2a2370cc3d455ba0515fb6f5c8f7ac0c0f55a86801b6e56a432f22c17/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d", size = 328964, upload-time = "2026-07-09T13:49:11.292Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/0f/181c5da673953dfc0958cb4fb3a4984a9098673ddb05cac68e994bc8511b/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7", size = 442909, upload-time = "2026-07-09T13:49:12.644Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/38/5c5e665af542884a8fd3c61725c38453239e13940326b5b70f3ef8881a97/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4", size = 323076, upload-time = "2026-07-09T13:49:13.897Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/35/7de97de18cbf226c2a4f2104ad15e56ca4491717c81c0b71795c0c585b4e/uuid_utils-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099", size = 347360, upload-time = "2026-07-09T13:49:15.237Z" },
+    { url = "https://files.pythonhosted.org/packages/26/a1/9915d5dd59fdd1957ded5d188c0ea0b9db5a1d84d42c8d8828a7b83b366e/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354", size = 499267, upload-time = "2026-07-09T13:49:16.774Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/05/88108405262ec850cea0f95733445d6873e5772af3292baabd9ef8457740/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330", size = 604940, upload-time = "2026-07-09T13:49:18.147Z" },
+    { url = "https://files.pythonhosted.org/packages/89/d5/6dbcd300de47cc443cff2656cd5327a385751213dcb2101cfee7388170b2/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0", size = 564172, upload-time = "2026-07-09T13:49:19.593Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/94/e8057f2288a415fba8a978bca4b589f5cb6b91a028a5dc07a1775938b33f/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5", size = 528533, upload-time = "2026-07-09T13:49:21.075Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/6b/31713148c77e48e62f51aa042a98a54a8be0396912ea5130f83f52ae722d/uuid_utils-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0", size = 99197, upload-time = "2026-07-09T13:49:22.351Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/f3/ca6f6ac5428312df8ed632f6dd9f9e6aba23090471fcdeae53eab027e8b3/uuid_utils-0.17.0-cp314-cp314-win32.whl", hash = "sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a", size = 169540, upload-time = "2026-07-09T13:49:23.563Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/cd/7ede0db66411fa09817d79b680f7454ea9bee2d374e1922e4efd065760a3/uuid_utils-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0", size = 175984, upload-time = "2026-07-09T13:49:24.703Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/81/533b5f80cd4918c0693f4e1b7b90ceb1caa45f4266ae8b528135d7ecca5d/uuid_utils-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae", size = 174749, upload-time = "2026-07-09T13:49:25.886Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/13/f400ac39d06fd8be5b099c09e41bb975205926722a3e8d53348817cb7ff9/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0", size = 562610, upload-time = "2026-07-09T13:49:27.374Z" },
+    { url = "https://files.pythonhosted.org/packages/03/8c/c71c8312304c56f6d0bcba87cd402fa79bec35d18ffc8c41954196ca68e5/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b", size = 289473, upload-time = "2026-07-09T13:49:28.989Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/cd/522117e2e5184ca1d4f0f85ee833e9e21bd8c6b99eff8a4d1a8e5a194e33/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750", size = 321600, upload-time = "2026-07-09T13:49:30.4Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/f4/0d81f9bd346fc717bc561c08fa6457e0328966eb76e536b938fe77d56459/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912", size = 329569, upload-time = "2026-07-09T13:49:31.732Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/41/26e1363f36a94c9e8ec2dd21d5f63088d3e7c723adbb12dcc8fdc77be417/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa", size = 442051, upload-time = "2026-07-09T13:49:33.024Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/a7/2c1ed1b34d7df7fdcc11c28fd26d94d44843b37d9af2435ff9fd8abdbc08/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2", size = 324372, upload-time = "2026-07-09T13:49:34.554Z" },
+    { url = "https://files.pythonhosted.org/packages/78/bf/328d3c6bb22c496944a1b3b732207d71aa6964eb604e5e3b9dcb91ed0a00/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354", size = 348548, upload-time = "2026-07-09T13:49:35.898Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/76/a07de5cb7b90582fdbbc830fd19be129cbbb9897cfe239fef469d7bd2d09/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6", size = 498985, upload-time = "2026-07-09T13:49:37.142Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/62/9966e46ae34fcec6b06119631fb3c09705ea78835035ce3a82d3348eb61a/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68", size = 605183, upload-time = "2026-07-09T13:49:38.648Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/4e/bb962ba0fe31e903b199f22cf4c1a6cba35a8987aef526d287277ab8ca8b/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3", size = 565412, upload-time = "2026-07-09T13:49:40.115Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/9e/122adfeeeae8a84ccfd43bce627b104d12a2180a93bffd2c0e1b54dad7a6/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd", size = 529885, upload-time = "2026-07-09T13:49:41.513Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/4f/257304dded339dc35fc9bf35722ac68fd4fdb930f255b8f7bccdf74ebba9/uuid_utils-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91", size = 169472, upload-time = "2026-07-09T13:49:42.871Z" },
+    { url = "https://files.pythonhosted.org/packages/35/c8/e78c06db7e9ce317ce7b8759ff2058333eac75caa8c22b75f0059589c9be/uuid_utils-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab", size = 176271, upload-time = "2026-07-09T13:49:44.105Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/11/bd1c70e1ad3301163cebe66c8d26de26e6814d52f642a849448bd2833626/uuid_utils-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9", size = 175004, upload-time = "2026-07-09T13:49:45.591Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/14/4ae708968b15cac7b68d5b854bfce724b21faa1c7a5147fb96d87f468a45/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf", size = 567823, upload-time = "2026-07-09T13:49:46.902Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/e2/d3af9c3d1dc6efb9ee1cffab30f3f2aacacc3892b21b495d78d34c6696bc/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb", size = 288763, upload-time = "2026-07-09T13:49:48.491Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/c2/f1b183e412387529893015a94a8447633c665f6d0392de20e245680e636a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343", size = 324919, upload-time = "2026-07-09T13:49:49.972Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/3c/d32c799bdd51f3b08b6ee95f9de921b59c69075a96767f937fab55014813/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1", size = 332689, upload-time = "2026-07-09T13:49:51.402Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/90/b4cd455619ff276dc3c3262a7420ead63aa1e531362f00df4cdb07d90e0a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec", size = 445726, upload-time = "2026-07-09T13:49:52.757Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/f1/5cc042a37932aa9a66eb8ab4a9a5b31d80261ae4565ff0193d8cc1fb9392/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391", size = 325610, upload-time = "2026-07-09T13:49:54.191Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/72/9e800c41d766484484e97845a7a7f677ba94462df86c97183e0290229d16/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a", size = 352672, upload-time = "2026-07-09T13:49:55.748Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.52.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "h11" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/c8/2d307868453a4bca6e64fa3581d122ae0748a0869c53f159339def179c7c/uvicorn-0.52.0.tar.gz", hash = "sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae", size = 97504, upload-time = "2026-07-29T08:45:34.065Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/39/e6/b5c0630ace9757232aec07112be8146b812787db52141ff9d50674aa7634/uvicorn-0.52.0-py3-none-any.whl", hash = "sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08", size = 79058, upload-time = "2026-07-29T08:45:32.492Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
+    { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
+    { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
+    { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+    { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+    { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+    { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+    { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+    { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+    { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+    { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+    { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+    { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+    { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+    { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+    { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+    { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+    { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+    { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+    { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
+    { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
+    { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
+[[package]]
+name = "xxhash"
+version = "3.8.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/55/97/1a8cebf0a6650417f08a18231590e2515aacd5ce39c3ad8b9e013ebd437d/xxhash-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937", size = 34695, upload-time = "2026-07-06T10:43:40.248Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/cf/745b9bc0dd9c341bc074b5fc700db7bbef0f3b69ab21446492296ab37e50/xxhash-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c", size = 32376, upload-time = "2026-07-06T10:43:41.97Z" },
+    { url = "https://files.pythonhosted.org/packages/65/a4/8512a901b1d6ad4a9838d1b40385907a879d7e005a5afbec5d39526b69f6/xxhash-3.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780", size = 217470, upload-time = "2026-07-06T10:43:43.572Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/ad/0ffd8094ea29579bb2dc42fa74d08570e9ea3d95db561e6b1105e69b9ca6/xxhash-3.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f", size = 237799, upload-time = "2026-07-06T10:43:45.248Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/90/783c6b3f9336bd07449fe672be32cef6833633936bbfda8d3b23ee18d202/xxhash-3.8.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce", size = 262587, upload-time = "2026-07-06T10:43:46.733Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/77/ba0316a7c3e661b86830a47ae4987798616ce1b15af8d2a6358e2d89ef60/xxhash-3.8.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8", size = 238484, upload-time = "2026-07-06T10:43:48.453Z" },
+    { url = "https://files.pythonhosted.org/packages/09/79/33001037c1cba90f4ced38b257161c13452024c0db44208f883e2e47f3fc/xxhash-3.8.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656", size = 469909, upload-time = "2026-07-06T10:43:50.188Z" },
+    { url = "https://files.pythonhosted.org/packages/45/90/237eded9dd6ae638083294e5a9f77b317aaebd480a330806b39c192a0de1/xxhash-3.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb", size = 217166, upload-time = "2026-07-06T10:43:51.816Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/6a/8cb439dc9920e1468e1c2d69ef77cbeb4be3b1ae9f4b5344c07a2b59af18/xxhash-3.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea", size = 307593, upload-time = "2026-07-06T10:43:53.436Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/c6/c0607d373c8affea92101a3926c4fc8b026bcf8983e05fd58f3a0380ebf8/xxhash-3.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b", size = 234702, upload-time = "2026-07-06T10:43:55.042Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/cb/f4cfd456624c1f017858168b7ba9443dad810da8aac779a612658450e827/xxhash-3.8.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f", size = 265749, upload-time = "2026-07-06T10:43:56.749Z" },
+    { url = "https://files.pythonhosted.org/packages/33/f3/9006669c04b01206e21b2177425c649461ba188930a052c2f1728d6ec6a8/xxhash-3.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef", size = 221992, upload-time = "2026-07-06T10:43:58.12Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/0b/7e6f3eaa05df5e0b6c94aa452b0672801f7031e602081f07fd441aaaaed5/xxhash-3.8.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8", size = 236899, upload-time = "2026-07-06T10:43:59.562Z" },
+    { url = "https://files.pythonhosted.org/packages/da/cc/bbaee4987f3aab1d7b33bb430bb49e940646160af448b9167431c931126d/xxhash-3.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5", size = 297934, upload-time = "2026-07-06T10:44:01.132Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/97/6bee358660eb8b4f73c00b00b00bc616ebde00e1ab4b67c63486ce360648/xxhash-3.8.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4", size = 439315, upload-time = "2026-07-06T10:44:02.878Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/50/7e35275f39256bedace0c3cd5be3c72d4ac9d5aecf5e5fdc3530337cd263/xxhash-3.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb", size = 214038, upload-time = "2026-07-06T10:44:04.504Z" },
+    { url = "https://files.pythonhosted.org/packages/59/2d/69d02d096ee50bdf3ef0d208d874f52c71b1aa6906066bce3c52fedb8bc6/xxhash-3.8.1-cp310-cp310-win32.whl", hash = "sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626", size = 31939, upload-time = "2026-07-06T10:44:06.213Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/1d/e06fca9844919ca91c6587d530cfa1e745830ec73ad38f44f04b25d1bfb7/xxhash-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf", size = 32729, upload-time = "2026-07-06T10:44:07.621Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/c2/800648d99039927b5a86d8ae02cd86a556a5ee1678d388216f6b44c8966c/xxhash-3.8.1-cp310-cp310-win_arm64.whl", hash = "sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3", size = 29215, upload-time = "2026-07-06T10:44:08.916Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/5a/05eaa129555f85476a3e16ff869e95f81a78bbe4647eef9d0229f515a317/xxhash-3.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147", size = 34699, upload-time = "2026-07-06T10:44:10.14Z" },
+    { url = "https://files.pythonhosted.org/packages/80/59/0df1133958b2228929355e022aab1e958c7b2c43e27bf7f59bc9edfa8a54/xxhash-3.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10", size = 32373, upload-time = "2026-07-06T10:44:11.353Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/bf/1cfda5b5e6bf26617812b4a31662ef2220d2ad04e0a55b8ff9eb36e56a5c/xxhash-3.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670", size = 220284, upload-time = "2026-07-06T10:44:12.683Z" },
+    { url = "https://files.pythonhosted.org/packages/70/93/45dc0ad7913b69e5b08bd039236cf628380e4c9cc76a8a4c6625a328e058/xxhash-3.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05", size = 240980, upload-time = "2026-07-06T10:44:14.297Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/02/f28ba7d17f2c1410ee397982c817ab1bd5b2701070c2d2c373539aad000a/xxhash-3.8.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae", size = 264526, upload-time = "2026-07-06T10:44:16.017Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/d0/f10651cec2c7981b20d693deae6bdfc438427d92be2db4ccabb6181f0021/xxhash-3.8.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a", size = 241369, upload-time = "2026-07-06T10:44:17.698Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/40/136e0cbaf5db51e191423b1c98643593189f02b6cd90837bf64b19113d70/xxhash-3.8.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c", size = 473186, upload-time = "2026-07-06T10:44:19.354Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/3f/6aa808a96bdc43dba9a740dec56c744526ee3c0019e32c75e810fa90ae4d/xxhash-3.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60", size = 220092, upload-time = "2026-07-06T10:44:20.956Z" },
+    { url = "https://files.pythonhosted.org/packages/47/28/a8675e78a9ced96dab853416162268e10e05b452e95db7888cf69f58ac5f/xxhash-3.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342", size = 309846, upload-time = "2026-07-06T10:44:22.543Z" },
+    { url = "https://files.pythonhosted.org/packages/89/0f/7fe4d4ef4e69f0033e012396ee2a115886bca7b10b7e45ce398626436bfc/xxhash-3.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723", size = 237659, upload-time = "2026-07-06T10:44:24.135Z" },
+    { url = "https://files.pythonhosted.org/packages/38/8f/83e9e31d4ed57fe963b99cb5b13a23e3e0f0dad1885aa0ebd2a7819dd423/xxhash-3.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a", size = 268737, upload-time = "2026-07-06T10:44:25.875Z" },
+    { url = "https://files.pythonhosted.org/packages/57/79/7e7de46dbe5d1f49afc96a0bc42e6b8df24eae3d6bad6007b99e42f48430/xxhash-3.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937", size = 224955, upload-time = "2026-07-06T10:44:27.777Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/34/b8540839e958d5ef5c6101af6f16032109e7099698ae8edbc8dcefe4d8f4/xxhash-3.8.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661", size = 239653, upload-time = "2026-07-06T10:44:29.422Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/87/a735d05f7f859354acadabe470ff40e2c46672275f96dcf096a761904def/xxhash-3.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673", size = 300213, upload-time = "2026-07-06T10:44:31.401Z" },
+    { url = "https://files.pythonhosted.org/packages/98/31/3e1cb020237b68117fc212dc5f9753b87f865b4dfee7c1ce62d0836955b5/xxhash-3.8.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872", size = 442508, upload-time = "2026-07-06T10:44:33.023Z" },
+    { url = "https://files.pythonhosted.org/packages/23/bf/f80090622141cc734b039ce1d15ce3ff6dced375e9680249bf5b9b8c6bf9/xxhash-3.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef", size = 216853, upload-time = "2026-07-06T10:44:34.983Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/a3/60157acecc307b238d3651c2483168e224b48b23a36ae6d6903588341d80/xxhash-3.8.1-cp311-cp311-win32.whl", hash = "sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792", size = 31936, upload-time = "2026-07-06T10:44:36.542Z" },
+    { url = "https://files.pythonhosted.org/packages/59/5c/ef70c418d878d187b8da56d4cdc06aea6cf5e456b301e96e51e1d2cc8625/xxhash-3.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f", size = 32724, upload-time = "2026-07-06T10:44:38.177Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/25/f008db952cec6b2a26445b456eeed2ebebd65e08e848ebe09ed6ac0634e6/xxhash-3.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d", size = 29212, upload-time = "2026-07-06T10:44:39.577Z" },
+    { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680, upload-time = "2026-07-06T10:44:40.932Z" },
+    { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397, upload-time = "2026-07-06T10:44:42.196Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549, upload-time = "2026-07-06T10:44:43.603Z" },
+    { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186, upload-time = "2026-07-06T10:44:45.534Z" },
+    { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852, upload-time = "2026-07-06T10:44:47.199Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663, upload-time = "2026-07-06T10:44:48.846Z" },
+    { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510, upload-time = "2026-07-06T10:44:50.695Z" },
+    { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469, upload-time = "2026-07-06T10:44:52.422Z" },
+    { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290, upload-time = "2026-07-06T10:44:54.142Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173, upload-time = "2026-07-06T10:44:55.693Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026, upload-time = "2026-07-06T10:44:57.424Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970, upload-time = "2026-07-06T10:44:59.085Z" },
+    { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820, upload-time = "2026-07-06T10:45:00.746Z" },
+    { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619, upload-time = "2026-07-06T10:45:02.716Z" },
+    { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267, upload-time = "2026-07-06T10:45:04.653Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338, upload-time = "2026-07-06T10:45:06.304Z" },
+    { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970, upload-time = "2026-07-06T10:45:07.823Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741, upload-time = "2026-07-06T10:45:09.42Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234, upload-time = "2026-07-06T10:45:10.809Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562, upload-time = "2026-07-06T10:45:12.425Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656, upload-time = "2026-07-06T10:45:13.932Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169, upload-time = "2026-07-06T10:45:15.687Z" },
+    { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177, upload-time = "2026-07-06T10:45:17.035Z" },
+    { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642, upload-time = "2026-07-06T10:45:18.39Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684, upload-time = "2026-07-06T10:45:20.033Z" },
+    { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401, upload-time = "2026-07-06T10:45:21.414Z" },
+    { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617, upload-time = "2026-07-06T10:45:23.234Z" },
+    { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295, upload-time = "2026-07-06T10:45:25.364Z" },
+    { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688, upload-time = "2026-07-06T10:45:27.09Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740, upload-time = "2026-07-06T10:45:28.753Z" },
+    { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599, upload-time = "2026-07-06T10:45:30.524Z" },
+    { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559, upload-time = "2026-07-06T10:45:32.979Z" },
+    { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383, upload-time = "2026-07-06T10:45:35.875Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228, upload-time = "2026-07-06T10:45:38.02Z" },
+    { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137, upload-time = "2026-07-06T10:45:39.954Z" },
+    { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068, upload-time = "2026-07-06T10:45:41.718Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874, upload-time = "2026-07-06T10:45:43.414Z" },
+    { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702, upload-time = "2026-07-06T10:45:45.135Z" },
+    { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351, upload-time = "2026-07-06T10:45:47.188Z" },
+    { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396, upload-time = "2026-07-06T10:45:48.925Z" },
+    { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974, upload-time = "2026-07-06T10:45:50.591Z" },
+    { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739, upload-time = "2026-07-06T10:45:52.221Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239, upload-time = "2026-07-06T10:45:53.714Z" },
+    { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964, upload-time = "2026-07-06T10:45:55.535Z" },
+    { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697, upload-time = "2026-07-06T10:45:57.288Z" },
+    { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954, upload-time = "2026-07-06T10:45:58.897Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776, upload-time = "2026-07-06T10:46:00.977Z" },
+    { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776, upload-time = "2026-07-06T10:46:02.869Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056, upload-time = "2026-07-06T10:46:04.688Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108, upload-time = "2026-07-06T10:46:06.498Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739, upload-time = "2026-07-06T10:46:08.598Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658, upload-time = "2026-07-06T10:46:10.504Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059, upload-time = "2026-07-06T10:46:12.634Z" },
+    { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535, upload-time = "2026-07-06T10:46:14.614Z" },
+    { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292, upload-time = "2026-07-06T10:46:16.452Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490, upload-time = "2026-07-06T10:46:18.318Z" },
+    { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861, upload-time = "2026-07-06T10:46:20.503Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739, upload-time = "2026-07-06T10:46:22.79Z" },
+    { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657, upload-time = "2026-07-06T10:46:24.831Z" },
+    { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377, upload-time = "2026-07-06T10:46:26.86Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274, upload-time = "2026-07-06T10:46:28.39Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375, upload-time = "2026-07-06T10:46:29.962Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/8b/df2ba04f22a6cd6b39f96a6577329a8471a55c90ef8d8e2f7c102363613f/xxhash-3.8.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba", size = 38430, upload-time = "2026-07-06T10:46:31.496Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/4f/6a059e8ad3ca8deedc91dfe335b211204900895152212c03ebbe721de68b/xxhash-3.8.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17", size = 36558, upload-time = "2026-07-06T10:46:33.078Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/95/40be178205acce092ae418feb20ac737b32a02c7b864926ed0717354c9f8/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd", size = 31181, upload-time = "2026-07-06T10:46:34.793Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/89/2da4dbf051bafa156c0e3f12012db2b0ac3b84ff37ca1f021f6bfffcdfbb/xxhash-3.8.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5", size = 32192, upload-time = "2026-07-06T10:46:36.393Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/4e/e000bbae3566bc8e0be771a8a0f294aa99075e3f0bc4ef43922ebffdebc8/xxhash-3.8.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf", size = 34691, upload-time = "2026-07-06T10:46:38.1Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/4a/ea954aacc7d1c8711880ac2b55da94429a9b4296b151c4fc0966549ca1ee/xxhash-3.8.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9", size = 34807, upload-time = "2026-07-06T10:46:39.647Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/29/df598e738ff37558ac627264deb2e560902d9bf7f46d3bd5175c9eee593e/xxhash-3.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711", size = 32410, upload-time = "2026-07-06T10:46:41.359Z" },
+    { url = "https://files.pythonhosted.org/packages/59/9c/81ab40e7d33ada0b3df5d1bc884894d15dbf4f805cd645b685e4606bb8e0/xxhash-3.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920", size = 220564, upload-time = "2026-07-06T10:46:43.463Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/6f/62ae6f5c8606320a0e2a41c2dc8c6d91cc5d63d0f84dd9582e9543779dd8/xxhash-3.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc", size = 241462, upload-time = "2026-07-06T10:46:45.509Z" },
+    { url = "https://files.pythonhosted.org/packages/15/a1/9c3a0ec6cb524396f551eddd102a76690a795494eb9784fc67542b0daa37/xxhash-3.8.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62", size = 264491, upload-time = "2026-07-06T10:46:47.655Z" },
+    { url = "https://files.pythonhosted.org/packages/64/f2/700a4674e4308eb59d2fdb973977e82eae231bea5044753fee5c9eec0e0c/xxhash-3.8.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f", size = 242905, upload-time = "2026-07-06T10:46:49.857Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/8a/72d9874375c8d4cbc64a8cd1d659d5695a8765c3db82efa82dc5bd9f14d0/xxhash-3.8.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e", size = 473873, upload-time = "2026-07-06T10:46:51.953Z" },
+    { url = "https://files.pythonhosted.org/packages/03/f0/6db07590ed7e0a77f186ef0bcea8d52553bf1ba57833e09467a2411f0f2d/xxhash-3.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b", size = 220765, upload-time = "2026-07-06T10:46:55.41Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/10/00d12d8b8beabbf49a8bbc626fb9f40445145a8887eb41a6acfb69149ac4/xxhash-3.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d", size = 310478, upload-time = "2026-07-06T10:46:57.729Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/f9/12a82394eefb0f185d15a7f7b9f627c61c475a72dd83718436a5b84b42ac/xxhash-3.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b", size = 238393, upload-time = "2026-07-06T10:46:59.87Z" },
+    { url = "https://files.pythonhosted.org/packages/20/f3/53f963e320b9ce678337aa7273f39ce692ded8b99e3d22a866ec722159ab/xxhash-3.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc", size = 268704, upload-time = "2026-07-06T10:47:01.806Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/50/5b5badbd87c82d9f9b5f58ac74a3f29ef08f6fc387b324b8fd482450b862/xxhash-3.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f", size = 225015, upload-time = "2026-07-06T10:47:03.784Z" },
+    { url = "https://files.pythonhosted.org/packages/30/93/3ca68265afe7b4e69435e08a7b6a1d9d0f2a071e889da1f8041ed00fe878/xxhash-3.8.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1", size = 240951, upload-time = "2026-07-06T10:47:05.816Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/a6/27e19670c40f46b5e76e11f2f4713d21054804568425d870670e757172ad/xxhash-3.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e", size = 300751, upload-time = "2026-07-06T10:47:07.95Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/fb/b33e27689959fe7ed2ae0b830af41560d65213943983afa9db3a8d481bce/xxhash-3.8.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231", size = 443480, upload-time = "2026-07-06T10:47:10Z" },
+    { url = "https://files.pythonhosted.org/packages/26/60/0e0d973be5fe280753ef02fbc89349492ad6e903bf1dcb870b668f94b662/xxhash-3.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe", size = 217657, upload-time = "2026-07-06T10:47:12.196Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/68/c9e3ecef4a9a417d464cb5bd200aa12f73192dee677901b9e08e0ad0d1bb/xxhash-3.8.1-cp314-cp314-win32.whl", hash = "sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab", size = 32690, upload-time = "2026-07-06T10:47:14.204Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/99/e9e44588c0b62837bbec5ba7927816de0afa03406b1a0b6c7a7e1d1a30a0/xxhash-3.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f", size = 33460, upload-time = "2026-07-06T10:47:16.021Z" },
+    { url = "https://files.pythonhosted.org/packages/45/2b/64f36d86380b3657ad9031967ab814f3ef31307174650853f69c18932ebc/xxhash-3.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5", size = 30092, upload-time = "2026-07-06T10:47:17.81Z" },
+    { url = "https://files.pythonhosted.org/packages/92/cb/18b64bff88c58a0ca209dc533e63cf02d7ae5aa6b1b9a9fd14e81b5dbd60/xxhash-3.8.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c", size = 35024, upload-time = "2026-07-06T10:47:19.821Z" },
+    { url = "https://files.pythonhosted.org/packages/af/1d/72d8a70520e5dcddb472ea0486d299da3240745a10658290cd7b5690ede2/xxhash-3.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230", size = 32697, upload-time = "2026-07-06T10:47:21.649Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/b8/e041f555903c56db3d0a731b3d72a6575d75e0ed868b1bd2e5176111ca44/xxhash-3.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489", size = 226044, upload-time = "2026-07-06T10:47:23.612Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/7e/5cdcf06bf6ec4b5d2ac073feb23432ec1d603fd438864cbd2c09c7cb45e1/xxhash-3.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5", size = 249899, upload-time = "2026-07-06T10:47:25.812Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/c0/eb7e059cb5e1dba11fd30d2fdf882f56e5a417a3eaa43669d43623767f45/xxhash-3.8.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e", size = 274892, upload-time = "2026-07-06T10:47:27.931Z" },
+    { url = "https://files.pythonhosted.org/packages/66/74/a600aaf7cd39957fd1510adeedb1749c1e7eb82bd632a1153d9c664c3135/xxhash-3.8.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6", size = 252243, upload-time = "2026-07-06T10:47:30.288Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/04/78d88fa75a6763e5d09bf1b947a392a27988903381b219006f92f3c68fc8/xxhash-3.8.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339", size = 482191, upload-time = "2026-07-06T10:47:32.45Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/06/07a8aea1108d682de8791ce608cdf367d75ff4e7e57cd3c154bdc6f47b23/xxhash-3.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542", size = 226877, upload-time = "2026-07-06T10:47:34.705Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/b5/86bade5618a524d2c06c4041aa2fe8e5749ce16e88afba60d67c1684a21f/xxhash-3.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08", size = 319794, upload-time = "2026-07-06T10:47:37.08Z" },
+    { url = "https://files.pythonhosted.org/packages/23/69/9b1a2b89b1621bb740fbcb7beb512f60f99480c1bdc680c0c90e1f56ff75/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775", size = 246202, upload-time = "2026-07-06T10:47:39.676Z" },
+    { url = "https://files.pythonhosted.org/packages/08/ea/662ed6cb49f1d34078b6a3a3e0f3d29ff93fd7b5a03c0bc9ecfd9b2159c3/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8", size = 275628, upload-time = "2026-07-06T10:47:41.99Z" },
+    { url = "https://files.pythonhosted.org/packages/13/f5/49fc9e4c6728a5a3bd8fe639199d2fa67609b3a84f938aff6e8568dd3e4f/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e", size = 231390, upload-time = "2026-07-06T10:47:44.233Z" },
+    { url = "https://files.pythonhosted.org/packages/64/9d/3acaf8f599c0e0b30e910a3a11ba32929da53c86dc73c7c55fe6a010b4e9/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99", size = 250600, upload-time = "2026-07-06T10:47:47.611Z" },
+    { url = "https://files.pythonhosted.org/packages/23/64/8acab4c5ec60dbe664b5b9858fd44c2413b07e535b09556a0a5022e78aa6/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0", size = 310032, upload-time = "2026-07-06T10:47:49.88Z" },
+    { url = "https://files.pythonhosted.org/packages/56/47/a0288d7329b1fe63e2734a32d19d444a96ae2b4810f545bc61e561224917/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07", size = 448882, upload-time = "2026-07-06T10:47:52.631Z" },
+    { url = "https://files.pythonhosted.org/packages/01/e7/3071dfd3beb5c38204ce1cf56bf7749fce08de900fa92714b81d1d8ca1f2/xxhash-3.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799", size = 223728, upload-time = "2026-07-06T10:47:55.093Z" },
+    { url = "https://files.pythonhosted.org/packages/12/11/b99949f0ba2b07e9f9ffe83b9c86faa685f9080725dc21a916a607313be5/xxhash-3.8.1-cp314-cp314t-win32.whl", hash = "sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497", size = 33150, upload-time = "2026-07-06T10:47:57.274Z" },
+    { url = "https://files.pythonhosted.org/packages/54/1c/09703eb341f8416e74e58d6c6732d4b5c46de59c942363203cb237cc95b0/xxhash-3.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684", size = 34005, upload-time = "2026-07-06T10:47:59.434Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/f9/6ed7251bb6a8af10ac73b1821c60583d2826e5b2064e45a979c935287c98/xxhash-3.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20", size = 30239, upload-time = "2026-07-06T10:48:01.874Z" },
+    { url = "https://files.pythonhosted.org/packages/99/e4/4d8040435aeac814fc69ba63621565fbeb19229a138e2568324a26b2a45c/xxhash-3.8.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12", size = 32687, upload-time = "2026-07-06T10:49:42.803Z" },
+    { url = "https://files.pythonhosted.org/packages/da/6a/975f1f2318c760e5bcec109ed379713ae645d8d856c2a3b9ec5d26857087/xxhash-3.8.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9", size = 29879, upload-time = "2026-07-06T10:49:45.172Z" },
+    { url = "https://files.pythonhosted.org/packages/08/0b/40a2a55ff52cf635bfdc5eae67a772bec85b4f44c6c737f73f6f528d51d1/xxhash-3.8.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd", size = 43246, upload-time = "2026-07-06T10:49:47.905Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/6d/56ed2b6b200f26fb474f3fd387d95d0601efcd5bb33430c90c68924bdd77/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02", size = 38202, upload-time = "2026-07-06T10:49:50.59Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/a3/56864d895d1161a9f17502088e9c1fb7c06bde2c2efdde620d22bb7a9c43/xxhash-3.8.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20", size = 34448, upload-time = "2026-07-06T10:49:53.242Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/57/5c6e0908a47f61dca96d01c8ee6fce01ed1050611eb779083ba8758fed81/xxhash-3.8.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42", size = 32869, upload-time = "2026-07-06T10:49:55.972Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" },
+    { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" },
+    { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" },
+    { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" },
+    { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" },
+    { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" },
+    { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" },
+    { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" },
+    { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" },
+    { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" },
+    { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" },
+    { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" },
+    { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" },
+    { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" },
+    { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" },
+    { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" },
+    { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" },
+    { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" },
+    { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" },
+    { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
+    { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
+    { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
+    { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
+    { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
+    { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
+    { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
+    { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
+]