from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator class GeoAntenna(BaseModel): lat: float lng: float antenna_height_m: float = Field(default=2.0, ge=0) @field_validator("lat") @classmethod def validate_lat(cls, value: float) -> float: if not -90 <= value <= 90: raise ValueError("lat must be between -90 and 90") return value @field_validator("lng") @classmethod def validate_lng(cls, value: float) -> float: if not -180 <= value <= 180: raise ValueError("lng must be between -180 and 180") return value class LinkRequest(BaseModel): model_config = ConfigDict(populate_by_name=True) from_: GeoAntenna = Field(alias="from") to: GeoAntenna frequency_mhz: float = Field(gt=0) fresnel_clearance: float = Field(default=0.6, ge=0, le=1) tx_power_dbm: float = 20.0 rx_sensitivity_dbm: float = -137.0 sample_step_m: float = Field(default=30.0, gt=0) class LinkBudget(BaseModel): tx_power_dbm: float rx_sensitivity_dbm: float margin_db: float status: Literal["good", "marginal", "poor"] class Obstacle(BaseModel): lat: float lng: float distance_from_start_m: float terrain_height_m: float canopy_height_m: float building_height_m: float total_height_m: float fresnel_radius_m: float clearance_m: float class PathProfilePoint(BaseModel): distance_m: float lat: float lng: float terrain_m: float canopy_m: float building_m: float total_m: float los_height_m: float fresnel_radius_m: float clearance_m: float class LinkResponse(BaseModel): distance_m: float los: bool fresnel_clear: bool free_space_loss_db: float link_budget: LinkBudget obstacles: list[Obstacle] path_profile: list[PathProfilePoint] class CoverageRequest(BaseModel): origin: GeoAntenna frequency_mhz: float = Field(gt=0) radius_m: float = Field(gt=0) resolution_m: float = Field(gt=0) fresnel_clearance: float = Field(default=0.6, ge=0, le=1) tx_power_dbm: float = 20.0 rx_sensitivity_dbm: float = -137.0 sample_step_m: float = Field(default=30.0, gt=0) class CoverageResponse(BaseModel): origin: GeoAntenna coverage_geojson: dict[str, Any] class ElevationProfilePoint(BaseModel): distance_m: float lat: float lng: float terrain_m: float class ElevationProfileResponse(BaseModel): distance_m: float path_profile: list[ElevationProfilePoint] class DataLayersHealth(BaseModel): srtm: bool canopy: bool buildings: bool class HealthResponse(BaseModel): status: Literal["ok", "degraded"] data_layers: DataLayersHealth