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