Compare commits

..

No commits in common. "8a9126b72926cee691b25e18722cf2eb615c6d66" and "a8652d881c08e296647eb80df0c91a7584bebe4a" have entirely different histories.

4 changed files with 45 additions and 111 deletions

View file

@ -4,7 +4,7 @@ from datetime import timedelta, timezone
import dateutil.tz
import exchange_calendars # type: ignore
import pandas
import pandas # type: ignore
from . import utils

View file

@ -224,6 +224,28 @@ def export_schengen_data_for_external_calculator(
return export_data
def add_schengen_info_to_events(events: list, data_dir: str | None = None) -> list:
"""Add Schengen compliance info to event list."""
if data_dir is None:
data_dir = flask.current_app.config["PERSONAL_DATA"]
# Load all trips to get travel context
trip_list = trip.build_trip_list(data_dir)
all_travel_items = []
for trip_obj in trip_list:
all_travel_items.extend(trip_obj.travel)
# Add Schengen status to each event
for event in events:
if hasattr(event, "date"):
calculation = calculate_schengen_time(all_travel_items, event.date)
event.schengen_days_used = calculation.total_days_used
event.schengen_compliant = calculation.is_compliant
event.schengen_days_remaining = calculation.days_remaining
return events
# Integration with existing trip.py functions
def enhanced_build_trip_list(
data_dir: str | None = None,

View file

@ -83,7 +83,7 @@ class SchengenCalculation:
days_remaining: int
is_compliant: bool
current_180_day_period: tuple[date, date] # (start, end)
stays_in_period: list[SchengenStay]
stays_in_period: SchengenStay
next_reset_date: typing.Optional[date] # When the 180-day window resets
@property

View file

@ -5,10 +5,10 @@ import os
import sys
import typing
from datetime import date, datetime, timedelta
from typing import Tuple, TypeVar, cast
from typing import Tuple, TypeVar
import yaml
from geopy.distance import distance # type: ignore[import-untyped]
from geopy.distance import distance
from rich.pretty import pprint
import agenda
@ -23,8 +23,6 @@ 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."""
@ -36,15 +34,11 @@ def check_currency(item: agenda.types.StrDict) -> None:
sys.exit(-1)
def get_coords(item: agenda.types.StrDict) -> LatLon | None:
"""Return latitude/longitude tuple when present."""
def get_coords(item):
if "latitude" in item and "longitude" in item:
latitude = item["latitude"]
longitude = item["longitude"]
assert isinstance(latitude, (int, float))
assert isinstance(longitude, (int, float))
return (float(latitude), float(longitude))
return None
return (item["latitude"], item["longitude"])
else:
return None
T = TypeVar("T")
@ -55,33 +49,12 @@ 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 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
return distance(a, b).km
def check_trips() -> None:
@ -94,7 +67,6 @@ 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')} - "
@ -114,69 +86,12 @@ def check_trips() -> None:
for trip in trip_list:
if not trip.accommodation or not trip.conferences:
continue
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:
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:
continue
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
dist = distance_km(conference_coords[0], accommodation_coords[0])
assert dist < 5.0
coords, routes = agenda.trip.get_coordinates_and_routes(trip_list, data_dir)
print(len(coords), "coords")
@ -217,13 +132,14 @@ def check_flights(airlines: set[str]) -> None:
)
def normalize_datetime(dt_value: date | datetime) -> datetime:
def normalize_datetime(dt_value):
"""Convert date or datetime to datetime for comparison, removing timezone info."""
if isinstance(dt_value, datetime):
return dt_value.replace(tzinfo=None)
if isinstance(dt_value, date):
if isinstance(dt_value, date) and not isinstance(dt_value, datetime):
return datetime.combine(dt_value, datetime.min.time())
raise TypeError(f"Unsupported datetime value type: {type(dt_value)}")
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
def check_trains() -> None:
@ -235,7 +151,6 @@ 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', '')}"
@ -269,7 +184,6 @@ 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')}"
@ -323,7 +237,6 @@ 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', '')})"
@ -369,7 +282,6 @@ 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', '')}"