warn if hotel/accommodation coords to far apart
This commit is contained in:
parent
4364c95f25
commit
a7ec33c500
130
validate_yaml.py
130
validate_yaml.py
|
|
@ -5,10 +5,10 @@ import os
|
|||
import sys
|
||||
import typing
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Tuple, TypeVar
|
||||
from typing import Tuple, TypeVar, cast
|
||||
|
||||
import yaml
|
||||
from geopy.distance import distance
|
||||
from geopy.distance import distance # type: ignore[import-untyped]
|
||||
from rich.pretty import pprint
|
||||
|
||||
import agenda
|
||||
|
|
@ -23,6 +23,8 @@ data_dir = config.PERSONAL_DATA
|
|||
|
||||
currencies = set(config.CURRENCIES + ["GBP"])
|
||||
|
||||
LatLon = Tuple[float, float]
|
||||
|
||||
|
||||
def check_currency(item: agenda.types.StrDict) -> None:
|
||||
"""Throw error if currency is not in config."""
|
||||
|
|
@ -34,11 +36,15 @@ def check_currency(item: agenda.types.StrDict) -> None:
|
|||
sys.exit(-1)
|
||||
|
||||
|
||||
def get_coords(item):
|
||||
def get_coords(item: agenda.types.StrDict) -> LatLon | None:
|
||||
"""Return latitude/longitude tuple when present."""
|
||||
if "latitude" in item and "longitude" in item:
|
||||
return (item["latitude"], item["longitude"])
|
||||
else:
|
||||
return None
|
||||
latitude = item["latitude"]
|
||||
longitude = item["longitude"]
|
||||
assert isinstance(latitude, (int, float))
|
||||
assert isinstance(longitude, (int, float))
|
||||
return (float(latitude), float(longitude))
|
||||
return None
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
|
@ -49,12 +55,33 @@ def remove_nones(items: list[T | None]) -> list[T]:
|
|||
return [item for item in items if item is not None]
|
||||
|
||||
|
||||
LatLon = Tuple[float, float]
|
||||
|
||||
|
||||
def distance_km(a: LatLon, b: LatLon) -> float:
|
||||
"""Return the great-circle distance between two (lat, lon) points in km."""
|
||||
return distance(a, b).km
|
||||
return cast(float, distance(a, b).km)
|
||||
|
||||
|
||||
def parse_datetime_value(value: typing.Any) -> datetime | None:
|
||||
"""Return naive datetime for supported input types."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid ISO datetime string: {value}") from exc
|
||||
return parsed.replace(tzinfo=None)
|
||||
if isinstance(value, datetime):
|
||||
return value.replace(tzinfo=None)
|
||||
if isinstance(value, date):
|
||||
return datetime.combine(value, datetime.min.time())
|
||||
raise TypeError(f"Unsupported datetime value type: {type(value)}")
|
||||
|
||||
|
||||
def ranges_overlap(
|
||||
start_a: datetime, end_a: datetime, start_b: datetime, end_b: datetime
|
||||
) -> bool:
|
||||
"""Return True when two datetime ranges overlap."""
|
||||
return start_a < end_b and start_b < end_a
|
||||
|
||||
|
||||
def check_trips() -> None:
|
||||
|
|
@ -67,6 +94,7 @@ def check_trips() -> None:
|
|||
for trip_data in trips_data:
|
||||
current_trip = normalize_datetime(trip_data["trip"])
|
||||
if prev_trip and current_trip < prev_trip:
|
||||
assert prev_trip_data is not None
|
||||
print("Out of order trip found:")
|
||||
print(
|
||||
f" Previous: {prev_trip_data.get('trip')} - "
|
||||
|
|
@ -86,12 +114,69 @@ def check_trips() -> None:
|
|||
for trip in trip_list:
|
||||
if not trip.accommodation or not trip.conferences:
|
||||
continue
|
||||
accommodation_coords = remove_nones([get_coords(a) for a in trip.accommodation])
|
||||
conference_coords = remove_nones([get_coords(c) for c in trip.conferences])
|
||||
if len(accommodation_coords) != 1 or len(conference_coords) != 1:
|
||||
accommodation_entries: list[
|
||||
tuple[agenda.types.StrDict, LatLon, datetime, datetime]
|
||||
] = []
|
||||
for accommodation in trip.accommodation:
|
||||
accommodation_coords = get_coords(accommodation)
|
||||
if accommodation_coords is None:
|
||||
continue
|
||||
start_dt = parse_datetime_value(accommodation.get("from"))
|
||||
end_dt = parse_datetime_value(accommodation.get("to"))
|
||||
if start_dt is None or end_dt is None:
|
||||
continue
|
||||
accommodation_entries.append(
|
||||
(accommodation, accommodation_coords, start_dt, end_dt)
|
||||
)
|
||||
|
||||
if not accommodation_entries:
|
||||
continue
|
||||
dist = distance_km(conference_coords[0], accommodation_coords[0])
|
||||
assert dist < config.ACCOMODATION_MAX_DISTANCE_KM
|
||||
|
||||
for conference in trip.conferences:
|
||||
if conference.get("online"):
|
||||
continue
|
||||
conference_coords = get_coords(conference)
|
||||
if conference_coords is None:
|
||||
continue
|
||||
start_dt = parse_datetime_value(conference.get("start"))
|
||||
end_value = conference.get("end") or conference.get("start")
|
||||
end_dt = parse_datetime_value(end_value)
|
||||
if start_dt is None or end_dt is None:
|
||||
continue
|
||||
|
||||
conference_country = (
|
||||
str(conference.get("country")).lower()
|
||||
if conference.get("country")
|
||||
else None
|
||||
)
|
||||
overlapping_distances = []
|
||||
for (
|
||||
accommodation_item,
|
||||
accommodation_coords,
|
||||
accommodation_start,
|
||||
accommodation_end,
|
||||
) in accommodation_entries:
|
||||
accommodation_country = (
|
||||
str(accommodation_item.get("country")).lower()
|
||||
if accommodation_item.get("country")
|
||||
else None
|
||||
)
|
||||
if (
|
||||
conference_country
|
||||
and accommodation_country
|
||||
and accommodation_country != conference_country
|
||||
):
|
||||
continue
|
||||
if not ranges_overlap(
|
||||
accommodation_start, accommodation_end, start_dt, end_dt
|
||||
):
|
||||
continue
|
||||
overlapping_distances.append(
|
||||
distance_km(conference_coords, accommodation_coords)
|
||||
)
|
||||
if not overlapping_distances:
|
||||
continue
|
||||
assert min(overlapping_distances) < config.ACCOMODATION_MAX_DISTANCE_KM
|
||||
|
||||
coords, routes = agenda.trip.get_coordinates_and_routes(trip_list, data_dir)
|
||||
print(len(coords), "coords")
|
||||
|
|
@ -132,14 +217,13 @@ def check_flights(airlines: set[str]) -> None:
|
|||
)
|
||||
|
||||
|
||||
def normalize_datetime(dt_value):
|
||||
def normalize_datetime(dt_value: date | datetime) -> datetime:
|
||||
"""Convert date or datetime to datetime for comparison, removing timezone info."""
|
||||
if isinstance(dt_value, date) and not isinstance(dt_value, datetime):
|
||||
return datetime.combine(dt_value, datetime.min.time())
|
||||
elif isinstance(dt_value, datetime):
|
||||
# Remove timezone info to allow comparison between naive and aware datetimes
|
||||
if isinstance(dt_value, datetime):
|
||||
return dt_value.replace(tzinfo=None)
|
||||
return dt_value
|
||||
if isinstance(dt_value, date):
|
||||
return datetime.combine(dt_value, datetime.min.time())
|
||||
raise TypeError(f"Unsupported datetime value type: {type(dt_value)}")
|
||||
|
||||
|
||||
def check_trains() -> None:
|
||||
|
|
@ -151,6 +235,7 @@ def check_trains() -> None:
|
|||
for train in trains:
|
||||
current_depart = normalize_datetime(train["depart"])
|
||||
if prev_depart and current_depart < prev_depart:
|
||||
assert prev_train is not None
|
||||
print(f"Out of order train found:")
|
||||
print(
|
||||
f" Previous: {prev_train.get('depart')} {prev_train.get('from', '')} -> {prev_train.get('to', '')}"
|
||||
|
|
@ -184,6 +269,7 @@ def check_conferences() -> None:
|
|||
|
||||
current_start = normalize_datetime(conf_data["start"])
|
||||
if prev_start and current_start < prev_start:
|
||||
assert prev_conf_data is not None
|
||||
print(f"Out of order conference found:")
|
||||
print(
|
||||
f" Previous: {prev_conf_data.get('start')} - {prev_conf_data.get('name', 'No name')}"
|
||||
|
|
@ -237,6 +323,7 @@ def check_accommodation() -> None:
|
|||
|
||||
current_from = normalize_datetime(stay["from"])
|
||||
if prev_from and current_from < prev_from:
|
||||
assert prev_stay is not None
|
||||
print(f"Out of order accommodation found:")
|
||||
print(
|
||||
f" Previous: {prev_stay.get('from')} - {prev_stay.get('name', 'No name')} ({prev_stay.get('location', '')})"
|
||||
|
|
@ -282,6 +369,7 @@ def check_ferries() -> None:
|
|||
for ferry in ferries:
|
||||
current_depart = normalize_datetime(ferry["depart"])
|
||||
if prev_depart and current_depart < prev_depart:
|
||||
assert prev_ferry is not None
|
||||
print(f"Out of order ferry found:")
|
||||
print(
|
||||
f" Previous: {prev_ferry.get('depart')} {prev_ferry.get('from', '')} -> {prev_ferry.get('to', '')}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue