Files
geo-setp/schemas.py
2026-04-15 14:05:33 +08:00

231 lines
6.6 KiB
Python

from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Literal
# ---------------------------------------------------------------------------
# Input
# ---------------------------------------------------------------------------
class RawStop(BaseModel):
name: str | None = None
address: str
city: str | None = None
contact: str | None = None
@field_validator("name", "address", "city", "contact", mode="before")
@classmethod
def strip_optional_strings(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
@field_validator("address")
@classmethod
def validate_address(cls, value: str) -> str:
if not value:
raise ValueError("stop address cannot be empty")
return value
class RoutePlanRequest(BaseModel):
task_name: str = Field(default="multi-destination-route-planning")
origin_mode: Literal["fixed", "current_location"]
origin_name: str | None = None
origin_address: str | None = None
origin_city: str | None = None
destination_name: str | None = None
destination_address: str
destination_city: str | None = None
stops: list[RawStop]
route_strategy: Literal["shortest_distance", "fastest_time", "balanced"] = "shortest_distance"
transport_mode: Literal["driving"] = "driving"
need_deep_link: bool = True
deep_link_mode: Literal["personal_map", "route_plan", "auto"] = "auto"
need_html: bool = False
max_permutations: int | None = None
@field_validator(
"task_name",
"origin_name",
"origin_address",
"origin_city",
"destination_name",
"destination_address",
"destination_city",
mode="before",
)
@classmethod
def strip_request_strings(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
@field_validator("destination_address")
@classmethod
def validate_destination_address(cls, value: str) -> str:
if not value:
raise ValueError("destination_address cannot be empty")
return value
@field_validator("max_permutations")
@classmethod
def validate_max_permutations(cls, value: int | None) -> int | None:
if value is not None and value <= 0:
raise ValueError("max_permutations must be greater than 0")
return value
@model_validator(mode="after")
def validate_route_request(self) -> "RoutePlanRequest":
if not self.stops:
raise ValueError("stops must contain at least one stop")
if self.origin_mode == "fixed" and not self.origin_address:
raise ValueError("origin_address is required when origin_mode='fixed'")
destination_key = self.destination_address.casefold()
duplicate_stop = next(
(stop.address for stop in self.stops if stop.address.casefold() == destination_key),
None,
)
if duplicate_stop is not None:
raise ValueError("destination_address cannot also appear in stops")
return self
# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------
class ResolvedPoint(BaseModel):
role: Literal["origin", "stop", "destination"]
input_name: str | None = None
input_address: str
resolved_name: str
city: str | None = None
district: str | None = None
location: str # "lon,lat" format as returned by Amap
lon: float
lat: float
poi_id: str | None = None
source: Literal["geo", "text_search", "search_detail", "manual_fallback"]
confidence_note: str | None = None
class RouteLeg(BaseModel):
from_label: str
to_label: str
origin_location: str
destination_location: str
distance_m: int
duration_s: int
class CandidateRoute(BaseModel):
stop_order_labels: list[str]
full_order_labels: list[str]
legs: list[RouteLeg]
total_distance_m: int
total_duration_s: int
ranking_reason: str | None = None
class DeepLinks(BaseModel):
personal_map: str | None = None
android_route_plan: str | None = None
ios_route_plan: str | None = None
class RoutePlanResult(BaseModel):
success: bool
origin_mode: Literal["fixed", "current_location"]
resolved_origin: ResolvedPoint | None = None
resolved_destination: ResolvedPoint
resolved_stops: list[ResolvedPoint]
candidates: list[CandidateRoute]
best_route: CandidateRoute
deep_links: DeepLinks | None = None
summary: str
warnings: list[str]
class LoadPlanRequest(BaseModel):
merchant_id: int
area: str
license_plate: str
class LoadPlanResult(BaseModel):
success: bool
license_plate: str
selected_shipment_ids: list[int] = Field(default_factory=list)
summary: str
warnings: list[str] = Field(default_factory=list)
# ---------------------------------------------------------------------------
# Load planning API integration
# ---------------------------------------------------------------------------
class TransportVehicleMaterialCapacity(BaseModel):
id: int
material_name: str
capacity: int
class TransportVehicle(BaseModel):
id: int
merchant_id: int
name: str
license_plate: str
material_capacities: list[TransportVehicleMaterialCapacity] = Field(default_factory=list)
created_at: str
updated_at: str
class ShipmentGeoCoordinates(BaseModel):
lat: float
lng: float
class ShipmentSalesItem(BaseModel):
id: int
name: str
quantity: str
unit: int | None = None
unit_display: str | None = None
position: str | None = None
remark: str | None = None
printing_job_id: int | None = None
printing_job_width: str | None = None
class Shipment(BaseModel):
id: int
merchant_id: int
customer: int | None = None
customer_name: str | None = None
shipment_date: str
address: str
contact_name: str | None = None
contact_phone: str | None = None
area: str | None = None
remark: str | None = None
status: int | str
status_display: str | None = None
external_id: str | None = None
geo_coordinates: ShipmentGeoCoordinates | None = None
delivery: int | None = None
sales_items: list[ShipmentSalesItem] = Field(default_factory=list)
created_at: str
updated_at: str
class ShipmentPage(BaseModel):
count: int | None = None
next: str | None = None
previous: str | None = None
results: list[Shipment] = Field(default_factory=list)