"""Pydantic models for US Navy Astronomical Data API responses.
All API responses are properly typed using Pydantic models for type safety,
validation, and better IDE support. No dictionary goop - everything is strongly typed.
"""
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
# ============================================================================
# Enums - No Magic Strings
# ============================================================================
class MoonPhase(str, Enum):
"""Moon phase enumeration."""
NEW_MOON = "New Moon"
FIRST_QUARTER = "First Quarter"
FULL_MOON = "Full Moon"
LAST_QUARTER = "Last Quarter"
class CelestialPhenomenon(str, Enum):
"""Rise/Set/Transit phenomenon types."""
RISE = "Rise"
SET = "Set"
UPPER_TRANSIT = "Upper Transit"
BEGIN_CIVIL_TWILIGHT = "Begin Civil Twilight"
END_CIVIL_TWILIGHT = "End Civil Twilight"
class EclipsePhenomenon(str, Enum):
"""Solar eclipse phenomenon types."""
ECLIPSE_BEGINS = "Eclipse Begins"
MAXIMUM_ECLIPSE = "Maximum Eclipse"
ECLIPSE_ENDS = "Eclipse Ends"
class SeasonPhenomenon(str, Enum):
"""Earth's seasonal phenomena."""
EQUINOX = "Equinox"
SOLSTICE = "Solstice"
PERIHELION = "Perihelion"
APHELION = "Aphelion"
class MoonCurPhase(str, Enum):
"""Current moon phase descriptions."""
NEW_MOON = "New Moon"
WAXING_CRESCENT = "Waxing Crescent"
FIRST_QUARTER = "First Quarter"
WAXING_GIBBOUS = "Waxing Gibbous"
FULL_MOON = "Full Moon"
WANING_GIBBOUS = "Waning Gibbous"
LAST_QUARTER = "Last Quarter"
WANING_CRESCENT = "Waning Crescent"
class DayOfWeek(str, Enum):
"""Days of the week."""
MONDAY = "Monday"
TUESDAY = "Tuesday"
WEDNESDAY = "Wednesday"
THURSDAY = "Thursday"
FRIDAY = "Friday"
SATURDAY = "Saturday"
SUNDAY = "Sunday"
# ============================================================================
# Moon Phase Models
# ============================================================================
class MoonPhaseData(BaseModel):
"""Single moon phase occurrence.
Represents one specific phase of the moon with exact timing.
"""
phase: MoonPhase = Field(
..., description="The moon phase (New Moon, First Quarter, Full Moon, Last Quarter)"
)
year: int = Field(..., description="Year of the phase", ge=1700, le=2100)
month: int = Field(..., description="Month of the phase (1-12)", ge=1, le=12)
day: int = Field(..., description="Day of the month (1-31)", ge=1, le=31)
time: str = Field(
..., description="Time in HH:MM format (24-hour). All times are in Universal Time (UT1)"
)
class MoonPhasesResponse(BaseModel):
"""Moon phases API response.
Contains a list of upcoming moon phases starting from a given date.
"""
apiversion: str = Field(..., description="API version string")
year: int = Field(..., description="Query year", ge=1700, le=2100)
month: int = Field(..., description="Query month", ge=1, le=12)
day: int = Field(..., description="Query day", ge=1, le=31)
numphases: int = Field(..., description="Number of phases returned", ge=1, le=99)
phasedata: list[MoonPhaseData] = Field(..., description="List of moon phase occurrences")
# ============================================================================
# Rise/Set/Transit Models
# ============================================================================
class CelestialEventData(BaseModel):
"""Single celestial rise/set/transit event."""
phen: CelestialPhenomenon = Field(..., description="Type of phenomenon (Rise, Set, Transit)")
time: str = Field(
..., description="Time in HH:MM format (24-hour). Timezone depends on query parameters"
)
class ClosestPhaseData(BaseModel):
"""Closest moon phase to the queried date."""
phase: MoonPhase = Field(..., description="The moon phase")
year: int = Field(..., description="Year of the phase")
month: int = Field(..., description="Month of the phase", ge=1, le=12)
day: int = Field(..., description="Day of the phase", ge=1, le=31)
time: str = Field(..., description="Time in HH:MM format (UT1)")
class OneDayData(BaseModel):
"""Complete sun and moon data for one day."""
year: int = Field(..., description="Year")
month: int = Field(..., description="Month", ge=1, le=12)
day: int = Field(..., description="Day", ge=1, le=31)
day_of_week: DayOfWeek = Field(..., description="Day of the week")
tz: float = Field(..., description="Timezone offset from UTC (hours, east positive)")
isdst: bool = Field(..., description="Whether daylight saving time is in effect")
sundata: list[CelestialEventData] = Field(
...,
description="Sun events (rise, set, transit, twilight). "
"Events are ordered chronologically. May be empty for polar regions during extreme seasons",
)
moondata: list[CelestialEventData] = Field(
...,
description="Moon events (rise, set, transit). "
"May be empty if moon doesn't rise/set on this day (polar regions)",
)
closestphase: ClosestPhaseData = Field(..., description="Closest moon phase to this date")
curphase: MoonCurPhase = Field(..., description="Current phase of the moon")
fracillum: str = Field(
..., description="Fraction of moon illuminated as percentage (e.g., '92%')"
)
label: Optional[str] = Field(
None, description="Optional user-provided label from query parameter"
)
class GeoJSONPoint(BaseModel):
"""GeoJSON Point geometry."""
type: str = Field(..., description="Geometry type (always 'Point')")
coordinates: list[float] = Field(
...,
description="Coordinates as [longitude, latitude] (note: lon, lat order per GeoJSON spec)",
)
class OneDayProperties(BaseModel):
"""Properties for OneDay GeoJSON Feature."""
data: OneDayData = Field(..., description="The complete sun/moon data for the day")
class OneDayResponse(BaseModel):
"""Rise/Set/Transit API response in GeoJSON Feature format."""
apiversion: str = Field(..., description="API version string")
type: str = Field(..., description="GeoJSON type (always 'Feature')")
geometry: GeoJSONPoint = Field(..., description="Location geometry")
properties: OneDayProperties = Field(..., description="Sun and moon data")
# ============================================================================
# Solar Eclipse Models
# ============================================================================
class EclipseLocalData(BaseModel):
"""Local circumstances of a solar eclipse event.
Contains positional data for the sun during different eclipse phases.
"""
day: str = Field(..., description="Day of the month")
phenomenon: EclipsePhenomenon = Field(..., description="Eclipse phase")
time: str = Field(..., description="Local time in HH:MM:SS.S format")
altitude: str = Field(..., description="Sun altitude in degrees above horizon")
azimuth: str = Field(..., description="Sun azimuth in degrees (0=N, 90=E, 180=S, 270=W)")
position_angle: Optional[str] = Field(
None,
description="Position angle of the eclipse in degrees (where on the sun's disk the eclipse occurs)",
)
vertex_angle: Optional[str] = Field(
None, description="Vertex angle in degrees (orientation of the eclipse path)"
)
class EclipseProperties(BaseModel):
"""Properties of a solar eclipse at a specific location."""
year: int = Field(..., description="Year of the eclipse")
month: int = Field(..., description="Month of the eclipse")
day: int = Field(..., description="Day of the eclipse")
event: str = Field(..., description="Full description of the eclipse event")
description: str = Field(
...,
description="Type of eclipse at this location (e.g., 'Sun in Partial Eclipse at this Location', "
"'Sun in Total Eclipse at this Location', 'No Eclipse at this Location')",
)
magnitude: Optional[str] = Field(
None,
description="Eclipse magnitude (fraction of sun's diameter covered). "
"1.0+ indicates total eclipse, <1.0 indicates partial",
)
obscuration: Optional[str] = Field(
None, description="Percentage of sun's area covered (e.g., '95.4%')"
)
duration: Optional[str] = Field(
None, description="Duration of the eclipse at this location (e.g., '2h 31m 01.9s')"
)
delta_t: str = Field(
..., description="Delta T value used for calculations (difference between TT and UT1)"
)
local_data: list[EclipseLocalData] = Field(
..., description="List of local eclipse events (begins, maximum, ends) with sun positions"
)
class SolarEclipseByDateResponse(BaseModel):
"""Solar eclipse data for a specific location and date (GeoJSON Feature)."""
apiversion: str = Field(..., description="API version string")
type: str = Field(..., description="GeoJSON type (always 'Feature')")
geometry: GeoJSONPoint = Field(..., description="Location geometry")
properties: EclipseProperties = Field(
..., description="Eclipse properties and local circumstances"
)
class SolarEclipseEvent(BaseModel):
"""A single solar eclipse in a year list."""
year: int = Field(..., description="Year of the eclipse")
month: int = Field(..., description="Month of the eclipse", ge=1, le=12)
day: int = Field(..., description="Day of the eclipse", ge=1, le=31)
event: str = Field(..., description="Full description of the eclipse")
class SolarEclipseByYearResponse(BaseModel):
"""List of all solar eclipses in a given year."""
apiversion: str = Field(..., description="API version string")
year: int = Field(..., description="Query year", ge=1800, le=2050)
eclipses_in_year: list[SolarEclipseEvent] = Field(
..., description="List of solar eclipses occurring in this year"
)
# ============================================================================
# Earth's Seasons Models
# ============================================================================
class SeasonEvent(BaseModel):
"""A seasonal event (equinox, solstice, perihelion, aphelion)."""
year: int = Field(..., description="Year")
month: int = Field(..., description="Month", ge=1, le=12)
day: int = Field(..., description="Day", ge=1, le=31)
time: str = Field(
...,
description="Time in HH:MM format. Timezone depends on query parameters (default UTC)",
)
phenom: SeasonPhenomenon = Field(
...,
description="Type of phenomenon. "
"Equinox occurs at vernal (spring) and autumnal (fall) equinoxes. "
"Solstice occurs at summer and winter solstices. "
"Perihelion is Earth's closest approach to sun. "
"Aphelion is Earth's farthest point from sun.",
)
class SeasonsResponse(BaseModel):
"""Earth's seasons and orbital events for a year."""
apiversion: str = Field(..., description="API version string")
year: int = Field(..., description="Query year", ge=1700, le=2100)
tz: float = Field(..., description="Timezone offset used (hours, east positive)")
dst: bool = Field(..., description="Whether daylight saving time adjustment was applied")
data: list[SeasonEvent] = Field(
...,
description="List of seasonal events for the year. "
"Typically contains: 2 equinoxes, 2 solstices, 1 perihelion, 1 aphelion",
)