warn if hotel/accommodation coords to far apart
This commit is contained in:
parent
4364c95f25
commit
a7ec33c500
1 changed files with 109 additions and 21 deletions
130
validate_yaml.py
130
validate_yaml.py
|
|
@ -5,10 +5,10 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Tuple, TypeVar
|
from typing import Tuple, TypeVar, cast
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from geopy.distance import distance
|
from geopy.distance import distance # type: ignore[import-untyped]
|
||||||
from rich.pretty import pprint
|
from rich.pretty import pprint
|
||||||
|
|
||||||
import agenda
|
import agenda
|
||||||
|
|
@ -23,6 +23,8 @@ data_dir = config.PERSONAL_DATA
|
||||||
|
|
||||||
currencies = set(config.CURRENCIES + ["GBP"])
|
currencies = set(config.CURRENCIES + ["GBP"])
|
||||||
|
|
||||||
|
LatLon = Tuple[float, float]
|
||||||
|
|
||||||
|
|
||||||
def check_currency(item: agenda.types.StrDict) -> None:
|
def check_currency(item: agenda.types.StrDict) -> None:
|
||||||
"""Throw error if currency is not in config."""
|
"""Throw error if currency is not in config."""
|
||||||
|
|
@ -34,11 +36,15 @@ def check_currency(item: agenda.types.StrDict) -> None:
|
||||||
sys.exit(-1)
|
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:
|
if "latitude" in item and "longitude" in item:
|
||||||
return (item["latitude"], item["longitude"])
|
latitude = item["latitude"]
|
||||||
else:
|
longitude = item["longitude"]
|
||||||
return None
|
assert isinstance(latitude, (int, float))
|
||||||
|
assert isinstance(longitude, (int, float))
|
||||||
|
return (float(latitude), float(longitude))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
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]
|
return [item for item in items if item is not None]
|
||||||
|
|
||||||
|
|
||||||
LatLon = Tuple[float, float]
|
|
||||||
|
|
||||||
|
|
||||||
def distance_km(a: LatLon, b: LatLon) -> float:
|
def distance_km(a: LatLon, b: LatLon) -> float:
|
||||||
"""Return the great-circle distance between two (lat, lon) points in km."""
|
"""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:
|
def check_trips() -> None:
|
||||||
|
|
@ -67,6 +94,7 @@ def check_trips() -> None:
|
||||||
for trip_data in trips_data:
|
for trip_data in trips_data:
|
||||||
current_trip = normalize_datetime(trip_data["trip"])
|
current_trip = normalize_datetime(trip_data["trip"])
|
||||||
if prev_trip and current_trip < prev_trip:
|
if prev_trip and current_trip < prev_trip:
|
||||||
|
assert prev_trip_data is not None
|
||||||
print("Out of order trip found:")
|
print("Out of order trip found:")
|
||||||
print(
|
print(
|
||||||
f" Previous: {prev_trip_data.get('trip')} - "
|
f" Previous: {prev_trip_data.get('trip')} - "
|
||||||
|
|
@ -86,12 +114,69 @@ def check_trips() -> None:
|
||||||
for trip in trip_list:
|
for trip in trip_list:
|
||||||
if not trip.accommodation or not trip.conferences:
|
if not trip.accommodation or not trip.conferences:
|
||||||
continue
|
continue
|
||||||
accommodation_coords = remove_nones([get_coords(a) for a in trip.accommodation])
|
accommodation_entries: list[
|
||||||
conference_coords = remove_nones([get_coords(c) for c in trip.conferences])
|
tuple[agenda.types.StrDict, LatLon, datetime, datetime]
|
||||||
if len(accommodation_coords) != 1 or len(conference_coords) != 1:
|
] = []
|
||||||
|
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
|
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)
|
coords, routes = agenda.trip.get_coordinates_and_routes(trip_list, data_dir)
|
||||||
print(len(coords), "coords")
|
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."""
|
"""Convert date or datetime to datetime for comparison, removing timezone info."""
|
||||||
if isinstance(dt_value, date) and not isinstance(dt_value, datetime):
|
if 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
|
|
||||||
return dt_value.replace(tzinfo=None)
|
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:
|
def check_trains() -> None:
|
||||||
|
|
@ -151,6 +235,7 @@ def check_trains() -> None:
|
||||||
for train in trains:
|
for train in trains:
|
||||||
current_depart = normalize_datetime(train["depart"])
|
current_depart = normalize_datetime(train["depart"])
|
||||||
if prev_depart and current_depart < prev_depart:
|
if prev_depart and current_depart < prev_depart:
|
||||||
|
assert prev_train is not None
|
||||||
print(f"Out of order train found:")
|
print(f"Out of order train found:")
|
||||||
print(
|
print(
|
||||||
f" Previous: {prev_train.get('depart')} {prev_train.get('from', '')} -> {prev_train.get('to', '')}"
|
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"])
|
current_start = normalize_datetime(conf_data["start"])
|
||||||
if prev_start and current_start < prev_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"Out of order conference found:")
|
||||||
print(
|
print(
|
||||||
f" Previous: {prev_conf_data.get('start')} - {prev_conf_data.get('name', 'No name')}"
|
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"])
|
current_from = normalize_datetime(stay["from"])
|
||||||
if prev_from and current_from < prev_from:
|
if prev_from and current_from < prev_from:
|
||||||
|
assert prev_stay is not None
|
||||||
print(f"Out of order accommodation found:")
|
print(f"Out of order accommodation found:")
|
||||||
print(
|
print(
|
||||||
f" Previous: {prev_stay.get('from')} - {prev_stay.get('name', 'No name')} ({prev_stay.get('location', '')})"
|
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:
|
for ferry in ferries:
|
||||||
current_depart = normalize_datetime(ferry["depart"])
|
current_depart = normalize_datetime(ferry["depart"])
|
||||||
if prev_depart and current_depart < prev_depart:
|
if prev_depart and current_depart < prev_depart:
|
||||||
|
assert prev_ferry is not None
|
||||||
print(f"Out of order ferry found:")
|
print(f"Out of order ferry found:")
|
||||||
print(
|
print(
|
||||||
f" Previous: {prev_ferry.get('depart')} {prev_ferry.get('from', '')} -> {prev_ferry.get('to', '')}"
|
f" Previous: {prev_ferry.get('depart')} {prev_ferry.get('from', '')} -> {prev_ferry.get('to', '')}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue