Add car journeys to trips
This commit is contained in:
parent
bc7a2e89d0
commit
c6cb06b71e
9 changed files with 430 additions and 10 deletions
|
|
@ -94,7 +94,7 @@ def _require_date_value(value: typing.Any, field_name: str) -> DateOrDateTime:
|
|||
|
||||
def _require_date_only(value: typing.Any, field_name: str) -> date:
|
||||
"""Return field value as a date or raise ValueError."""
|
||||
return typing.cast(date, utils.as_date(_require_date_value(value, field_name)))
|
||||
return utils.as_date(_require_date_value(value, field_name))
|
||||
|
||||
|
||||
def conference_date_fields(item: StrDict) -> StrDict:
|
||||
|
|
|
|||
185
agenda/trip.py
185
agenda/trip.py
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import decimal
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import typing
|
||||
import unicodedata
|
||||
|
|
@ -10,6 +11,7 @@ from datetime import date, datetime, timedelta, timezone
|
|||
import flask
|
||||
import pycountry
|
||||
import yaml
|
||||
from geopy.distance import geodesic # type: ignore
|
||||
|
||||
from agenda import conference, ical, travel, trip_schengen
|
||||
from agenda.types import StrDict, Trip, TripElement
|
||||
|
|
@ -281,6 +283,156 @@ def load_buses(
|
|||
)
|
||||
|
||||
|
||||
def route_filename_without_extension(route: str) -> str:
|
||||
"""Return route filename without a GeoJSON extension."""
|
||||
return route.removesuffix(".geojson")
|
||||
|
||||
|
||||
def line_strings_from_geojson(geojson_data: StrDict) -> list[list[list[float]]]:
|
||||
"""Extract LineString coordinate arrays from GeoJSON."""
|
||||
if geojson_data["type"] == "FeatureCollection":
|
||||
features = typing.cast(list[StrDict], geojson_data["features"])
|
||||
return [
|
||||
line for feature in features for line in line_strings_from_geojson(feature)
|
||||
]
|
||||
|
||||
if geojson_data["type"] == "Feature":
|
||||
return line_strings_from_geojson(typing.cast(StrDict, geojson_data["geometry"]))
|
||||
|
||||
if geojson_data["type"] == "LineString":
|
||||
return [typing.cast(list[list[float]], geojson_data["coordinates"])]
|
||||
|
||||
if geojson_data["type"] == "MultiLineString":
|
||||
return typing.cast(list[list[list[float]]], geojson_data["coordinates"])
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def geojson_lonlat_to_latlon(coord: list[float]) -> tuple[float, float]:
|
||||
"""Convert a GeoJSON lon/lat coordinate to a Leaflet lat/lon tuple."""
|
||||
return (coord[1], coord[0])
|
||||
|
||||
|
||||
def geojson_route_endpoints(
|
||||
geojson_data: StrDict,
|
||||
) -> tuple[tuple[float, float], tuple[float, float]] | None:
|
||||
"""Return the first and last points of a GeoJSON route."""
|
||||
lines = line_strings_from_geojson(geojson_data)
|
||||
populated_lines = [line for line in lines if line]
|
||||
if not populated_lines:
|
||||
return None
|
||||
return (
|
||||
geojson_lonlat_to_latlon(populated_lines[0][0]),
|
||||
geojson_lonlat_to_latlon(populated_lines[-1][-1]),
|
||||
)
|
||||
|
||||
|
||||
def geojson_route_distance_km(geojson_data: StrDict) -> float:
|
||||
"""Calculate the length of a GeoJSON route in kilometres."""
|
||||
total = 0.0
|
||||
for line in line_strings_from_geojson(geojson_data):
|
||||
for i in range(len(line) - 1):
|
||||
total += float(
|
||||
geodesic(
|
||||
geojson_lonlat_to_latlon(line[i]),
|
||||
geojson_lonlat_to_latlon(line[i + 1]),
|
||||
).km
|
||||
)
|
||||
return total
|
||||
|
||||
|
||||
def car_endpoint_type(name: str) -> str:
|
||||
"""Choose a marker type for a car route endpoint."""
|
||||
return (
|
||||
"home"
|
||||
if name.casefold() in {"home", "pch", "picture house court"}
|
||||
else "car_stop"
|
||||
)
|
||||
|
||||
|
||||
def car_route_labels(item: StrDict) -> tuple[str | None, str | None]:
|
||||
"""Return car route from/to labels from YAML or the route filename."""
|
||||
from_label = item.get("from")
|
||||
to_label = item.get("to")
|
||||
if isinstance(from_label, str) and isinstance(to_label, str):
|
||||
return (from_label, to_label)
|
||||
|
||||
route = item.get("route")
|
||||
if not isinstance(route, str):
|
||||
return (
|
||||
from_label if isinstance(from_label, str) else None,
|
||||
to_label if isinstance(to_label, str) else None,
|
||||
)
|
||||
|
||||
route_stem = os.path.basename(route_filename_without_extension(route))
|
||||
if "_to_" not in route_stem:
|
||||
return (
|
||||
from_label if isinstance(from_label, str) else None,
|
||||
to_label if isinstance(to_label, str) else None,
|
||||
)
|
||||
|
||||
start, end = route_stem.split("_to_", 1)
|
||||
return (
|
||||
from_label if isinstance(from_label, str) else start.replace("_", " "),
|
||||
to_label if isinstance(to_label, str) else end.replace("_", " "),
|
||||
)
|
||||
|
||||
|
||||
def car_endpoint_marker_type(item: StrDict, direction: str, label: str) -> str:
|
||||
"""Return the marker type for a car route endpoint."""
|
||||
marker_type = item.get(direction + "_type")
|
||||
return marker_type if isinstance(marker_type, str) else car_endpoint_type(label)
|
||||
|
||||
|
||||
def load_cars(data_dir: str) -> list[StrDict]:
|
||||
"""Load car journeys."""
|
||||
filename = os.path.join(data_dir, "car_journeys.yaml")
|
||||
if not os.path.exists(filename):
|
||||
return []
|
||||
|
||||
items = load_travel("car", "car_journeys", data_dir)
|
||||
for item in items:
|
||||
route = item.get("route")
|
||||
if not isinstance(route, str):
|
||||
continue
|
||||
|
||||
route_filename = route_filename_without_extension(route)
|
||||
item["geojson_filename"] = route_filename
|
||||
|
||||
with open(
|
||||
os.path.join(data_dir, "car_routes", route_filename + ".geojson")
|
||||
) as f:
|
||||
geojson_data = typing.cast(StrDict, json.load(f))
|
||||
|
||||
if "distance" not in item:
|
||||
item["distance"] = geojson_route_distance_km(geojson_data)
|
||||
|
||||
endpoints = geojson_route_endpoints(geojson_data)
|
||||
from_label, to_label = car_route_labels(item)
|
||||
if endpoints is None:
|
||||
continue
|
||||
|
||||
for direction, field, label, endpoint in (
|
||||
("from", "from_location", from_label, endpoints[0]),
|
||||
("to", "to_location", to_label, endpoints[1]),
|
||||
):
|
||||
if not label:
|
||||
continue
|
||||
item[field] = {
|
||||
"name": label,
|
||||
"type": car_endpoint_marker_type(item, direction, label),
|
||||
"latitude": endpoint[0],
|
||||
"longitude": endpoint[1],
|
||||
}
|
||||
|
||||
if from_label:
|
||||
item["from"] = from_label
|
||||
if to_label:
|
||||
item["to"] = to_label
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def process_flight(
|
||||
flight: StrDict, by_iata: dict[str, Airline], airports: list[StrDict]
|
||||
) -> None:
|
||||
|
|
@ -337,7 +489,8 @@ def collect_travel_items(
|
|||
+ load_trains(data_dir, route_distances=route_distances)
|
||||
+ load_ferries(data_dir, route_distances=route_distances)
|
||||
+ load_coaches(data_dir, route_distances=route_distances)
|
||||
+ load_buses(data_dir, route_distances=route_distances),
|
||||
+ load_buses(data_dir, route_distances=route_distances)
|
||||
+ load_cars(data_dir),
|
||||
key=depart_datetime,
|
||||
)
|
||||
|
||||
|
|
@ -465,6 +618,8 @@ def get_locations(trip: Trip) -> dict[str, StrDict]:
|
|||
"ferry_terminal": {},
|
||||
"coach_station": {},
|
||||
"bus_stop": {},
|
||||
"home": {},
|
||||
"car_stop": {},
|
||||
}
|
||||
|
||||
station_list = []
|
||||
|
|
@ -488,6 +643,13 @@ def get_locations(trip: Trip) -> dict[str, StrDict]:
|
|||
for field in "from_terminal", "to_terminal":
|
||||
terminal = t[field]
|
||||
locations["ferry_terminal"][terminal["name"]] = terminal
|
||||
case "car":
|
||||
for field in ("from_location", "to_location"):
|
||||
location = t.get(field)
|
||||
if not location:
|
||||
continue
|
||||
location_type = location.get("type", "car_stop")
|
||||
locations[location_type][location["name"]] = location
|
||||
|
||||
locations["station"] = process_station_list(station_list)
|
||||
return locations
|
||||
|
|
@ -635,6 +797,27 @@ def get_trip_routes(trip: Trip, data_dir: str) -> list[StrDict]:
|
|||
}
|
||||
)
|
||||
continue
|
||||
if t["type"] == "car":
|
||||
route = t.get("geojson_filename")
|
||||
if not isinstance(route, str):
|
||||
continue
|
||||
route_filename = route_filename_without_extension(route)
|
||||
key = "_".join(
|
||||
[
|
||||
"car",
|
||||
typing.cast(str, t.get("from", "")),
|
||||
typing.cast(str, t.get("to", "")),
|
||||
route_filename,
|
||||
]
|
||||
)
|
||||
routes.append(
|
||||
{
|
||||
"type": "car",
|
||||
"key": key,
|
||||
"geojson_filename": os.path.join("car_routes", route_filename),
|
||||
}
|
||||
)
|
||||
continue
|
||||
if t["type"] == "train":
|
||||
for leg in t["legs"]:
|
||||
train_from, train_to = leg["from_station"], leg["to_station"]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from agenda import format_list_with_ampersand
|
|||
|
||||
from . import utils
|
||||
|
||||
|
||||
StrDict = dict[str, typing.Any]
|
||||
DateOrDateTime = datetime.datetime | datetime.date
|
||||
|
||||
|
|
@ -46,6 +45,7 @@ class TripElement:
|
|||
"ferry": ":ferry:",
|
||||
"coach": ":bus:",
|
||||
"bus": ":bus:",
|
||||
"car": ":automobile:",
|
||||
}
|
||||
|
||||
alias = emoji_map.get(self.element_type)
|
||||
|
|
@ -414,6 +414,25 @@ class Trip:
|
|||
end_country=to_country,
|
||||
)
|
||||
)
|
||||
if item["type"] == "car":
|
||||
start_location = item.get("from_location", {})
|
||||
end_location = item.get("to_location", {})
|
||||
from_country = agenda.get_country(start_location.get("country"))
|
||||
to_country = agenda.get_country(end_location.get("country"))
|
||||
name = f"{item.get('from', 'Car journey')} → {item.get('to', 'destination')}"
|
||||
elements.append(
|
||||
TripElement(
|
||||
start_time=item["depart"],
|
||||
end_time=item.get("arrive"),
|
||||
title=name,
|
||||
detail=item,
|
||||
element_type="car",
|
||||
start_loc=item.get("from"),
|
||||
end_loc=item.get("to"),
|
||||
start_country=from_country,
|
||||
end_country=to_country,
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(elements, key=lambda e: utils.as_datetime(e.start_time))
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ This document describes the YAML files read from `../personal-data/`. It is inte
|
|||
- `ferries.yaml` `from` and `to` values reference `ferry_terminals.yaml` `name`.
|
||||
- `buses.yaml` `from` and `to` values reference `bus_stops.yaml` `name`.
|
||||
- `coaches.yaml` `from` and `to` values reference `coach_stations.yaml` `name`.
|
||||
- `car_journeys.yaml` `route` values name files in `car_routes/`. The `.geojson` extension is optional.
|
||||
- Station, stop, and terminal `routes` values name GeoJSON files without the `.geojson` extension.
|
||||
|
||||
## `accommodation.yaml`
|
||||
|
|
@ -250,6 +251,41 @@ Example:
|
|||
base_fare: '55.00'
|
||||
```
|
||||
|
||||
## `car_journeys.yaml`
|
||||
|
||||
Top-level shape: list of car journeys.
|
||||
|
||||
Used by: trip loading, maps, trip timeline. Car journeys are for driving your own car or a rental car.
|
||||
|
||||
Required fields:
|
||||
|
||||
- `trip`: trip start date.
|
||||
- `depart`: departure date or datetime.
|
||||
- `arrive`: arrival date or datetime.
|
||||
- `route`: GeoJSON filename in `car_routes/`, with or without the `.geojson` extension.
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `from`, `to`: endpoint labels. If omitted and the route filename uses `A_to_B`, labels are inferred from the filename.
|
||||
- `from_type`, `to_type`: marker type override. Use `home` to render a house icon. Otherwise car endpoints render as car markers.
|
||||
- `operator`, `vehicle`, `price`, `currency`, `distance`.
|
||||
|
||||
The route distance is calculated from the GeoJSON when `distance` is not present. If an inferred or explicit endpoint label is `home`, `PCH`, or `Picture House Court`, the marker is rendered as a house.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
- trip: 2026-07-16
|
||||
depart: 2026-07-16
|
||||
arrive: 2026-07-16
|
||||
route: PCH_to_EMF.geojson
|
||||
|
||||
- trip: 2026-07-16
|
||||
depart: 2026-07-19
|
||||
arrive: 2026-07-19
|
||||
route: EMF_to_PCH.geojson
|
||||
```
|
||||
|
||||
## `conferences.yaml`
|
||||
|
||||
Top-level shape: list of conferences and conference-like events.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ var emojiByType = {
|
|||
"ferry_terminal": "🚢",
|
||||
"coach_station": "🚌",
|
||||
"bus_stop": "🚏",
|
||||
"home": "🏠",
|
||||
"car_stop": "🚗",
|
||||
"accommodation": "🏨",
|
||||
"conference": "🖥️",
|
||||
"event": "🍷"
|
||||
|
|
@ -197,7 +199,7 @@ function build_map(map_id, coordinates, routes) {
|
|||
|
||||
// Draw routes
|
||||
routes.forEach(function(route) {
|
||||
var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange", "coach": "green", "bus": "purple"}[route.type];
|
||||
var color = {"train": "blue", "flight": "red", "unbooked_flight": "orange", "coach": "green", "bus": "purple", "car": "#555"}[route.type];
|
||||
var style = { weight: 3, opacity: 0.5, color: color };
|
||||
if (route.geojson) {
|
||||
L.geoJSON(JSON.parse(route.geojson), {
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ https://www.flightradar24.com/data/flights/{{ flight.airline_detail.iata | lower
|
|||
{% if item.operator and item.operator != item.name %}<span class="text-muted small">{{ item.operator }}</span>{% endif %}
|
||||
</div>
|
||||
{% elif e.element_type != "conference" %}
|
||||
{# Transport: flight, train, ferry, coach, bus #}
|
||||
{# Transport: flight, train, ferry, coach, bus, car #}
|
||||
{% set has_arrive = item.arrive is defined and item.arrive %}
|
||||
{# item.depart may be a date (no .date() method) or a datetime (has .date()) #}
|
||||
{% set has_time = item.depart is defined and item.depart and item.depart.hour is defined %}
|
||||
|
|
|
|||
|
|
@ -367,20 +367,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% elif e.element_type in ("coach", "bus") %}
|
||||
{% elif e.element_type in ("coach", "bus", "car") %}
|
||||
{% set item = e.detail %}
|
||||
<div class="trip-transport-card my-1">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
🚌 {{ item.from }} → {{ item.to }}
|
||||
{% if e.element_type == "car" %}🚗{% else %}🚌{% endif %}
|
||||
{{ item.from }} → {{ item.to }}
|
||||
{% if item.operator %}<small class="text-muted fw-normal">{{ item.operator }}</small>{% endif %}
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
{% if item.depart.hour is defined and item.arrive.hour is defined %}
|
||||
{{ item.depart.strftime("%H:%M") }} → {{ item.arrive.strftime("%H:%M") }}
|
||||
{% endif %}
|
||||
{% if item.class %}
|
||||
<span class="badge bg-info text-nowrap">{{ item.class }}</span>
|
||||
{% endif %}
|
||||
{% if item.depart.hour is defined and item.arrive.hour is defined %}
|
||||
<span class="text-muted">🕒{{ trip_duration(item.depart, item.arrive) }}</span>
|
||||
{% endif %}
|
||||
{% if item.distance %}
|
||||
<span class="text-muted">🛤️ {{ "{:,.0f} km".format(item.distance) }}</span>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Tests for trip map coordinate assembly."""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from datetime import date
|
||||
|
||||
import agenda.trip
|
||||
|
|
@ -167,3 +169,72 @@ def test_get_trip_routes_assumes_unbooked_paris_trip_is_by_train(
|
|||
"geojson_filename": "train_routes/London_St_Pancras_to_Paris_Gare_du_Nord",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_load_cars_infers_route_labels_and_home_marker(tmp_path: pathlib.Path) -> None:
|
||||
"""Car journeys should load direct GeoJSON routes and endpoint markers."""
|
||||
(tmp_path / "car_routes").mkdir()
|
||||
(tmp_path / "car_journeys.yaml").write_text("""---
|
||||
- trip: 2026-07-16
|
||||
depart: 2026-07-16
|
||||
arrive: 2026-07-16
|
||||
route: PCH_to_EMF.geojson
|
||||
""")
|
||||
(tmp_path / "car_routes" / "PCH_to_EMF.geojson").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": [[-2.60283, 51.44083], [-1.2, 52.0]],
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
cars = agenda.trip.load_cars(str(tmp_path))
|
||||
|
||||
assert len(cars) == 1
|
||||
assert cars[0]["type"] == "car"
|
||||
assert cars[0]["from"] == "PCH"
|
||||
assert cars[0]["to"] == "EMF"
|
||||
assert cars[0]["geojson_filename"] == "PCH_to_EMF"
|
||||
assert cars[0]["distance"] > 0
|
||||
assert cars[0]["from_location"] == {
|
||||
"name": "PCH",
|
||||
"type": "home",
|
||||
"latitude": 51.44083,
|
||||
"longitude": -2.60283,
|
||||
}
|
||||
assert cars[0]["to_location"] == {
|
||||
"name": "EMF",
|
||||
"type": "car_stop",
|
||||
"latitude": 52.0,
|
||||
"longitude": -1.2,
|
||||
}
|
||||
|
||||
|
||||
def test_get_trip_routes_includes_car_geojson() -> None:
|
||||
"""Car routes should render from the car_routes directory."""
|
||||
trip = Trip(
|
||||
start=date(2026, 7, 16),
|
||||
travel=[
|
||||
{
|
||||
"type": "car",
|
||||
"from": "PCH",
|
||||
"to": "EMF",
|
||||
"geojson_filename": "PCH_to_EMF",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
routes = agenda.trip.get_trip_routes(trip, "/tmp/personal-data")
|
||||
|
||||
assert routes == [
|
||||
{
|
||||
"type": "car",
|
||||
"key": "car_PCH_EMF_PCH_to_EMF",
|
||||
"geojson_filename": "car_routes/PCH_to_EMF",
|
||||
}
|
||||
]
|
||||
|
|
|
|||
106
validate_yaml.py
106
validate_yaml.py
|
|
@ -38,7 +38,7 @@ def check_currency(item: agenda.types.StrDict) -> None:
|
|||
|
||||
|
||||
def check_country_code(
|
||||
item: agenda.types.StrDict, source: str, required: bool = True
|
||||
item: typing.Mapping[str, typing.Any], source: str, required: bool = True
|
||||
) -> None:
|
||||
"""Throw error if country code is missing or invalid."""
|
||||
country = item.get("country")
|
||||
|
|
@ -494,6 +494,109 @@ def check_buses() -> None:
|
|||
print(len(buses), "buses")
|
||||
|
||||
|
||||
def car_route_path(route: str) -> str:
|
||||
"""Return the absolute path for a car route filename."""
|
||||
route_filename = agenda.trip.route_filename_without_extension(route)
|
||||
if os.path.isabs(route_filename) or ".." in route_filename.split(os.path.sep):
|
||||
raise ValueError(f"invalid car route filename {route!r}")
|
||||
return os.path.join(data_dir, "car_routes", route_filename + ".geojson")
|
||||
|
||||
|
||||
def validate_car_journey_routes(cars: list[agenda.types.StrDict]) -> None:
|
||||
"""Check every car journey route references an existing GeoJSON file."""
|
||||
for car in cars:
|
||||
route = car.get("route")
|
||||
if not isinstance(route, str) or not route:
|
||||
pprint(car)
|
||||
print("car journey missing route")
|
||||
sys.exit(-1)
|
||||
try:
|
||||
route_path = car_route_path(route)
|
||||
except ValueError as exc:
|
||||
pprint(car)
|
||||
print(exc)
|
||||
sys.exit(-1)
|
||||
if not os.path.exists(route_path):
|
||||
pprint(car)
|
||||
print(f"car route does not exist: {route_path}")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def validate_car_journey_dates(
|
||||
cars: list[agenda.types.StrDict], trip_list: list[agenda.types.Trip]
|
||||
) -> None:
|
||||
"""Check car journey dates fit the referenced trip."""
|
||||
trip_lookup = {trip.start: trip for trip in trip_list}
|
||||
prev_depart = None
|
||||
prev_car = None
|
||||
|
||||
for car in cars:
|
||||
for field in ("trip", "depart", "arrive"):
|
||||
if field not in car:
|
||||
pprint(car)
|
||||
print(f"car journey missing {field}")
|
||||
sys.exit(-1)
|
||||
|
||||
current_depart = normalize_datetime(car["depart"])
|
||||
arrive = normalize_datetime(car["arrive"])
|
||||
|
||||
if prev_depart and current_depart < prev_depart:
|
||||
assert prev_car is not None
|
||||
print("Out of order car journey found:")
|
||||
print(
|
||||
f" Previous: {prev_car.get('depart')} {prev_car.get('from', '')} -> {prev_car.get('to', '')}"
|
||||
)
|
||||
print(
|
||||
f" Current: {car.get('depart')} {car.get('from', '')} -> {car.get('to', '')}"
|
||||
)
|
||||
assert (
|
||||
False
|
||||
), "Car journeys are not in chronological order by departure time."
|
||||
prev_depart = current_depart
|
||||
prev_car = car
|
||||
|
||||
if arrive < current_depart:
|
||||
print("Invalid car journey found (arrive before depart):")
|
||||
print(f" {car.get('depart')} {car.get('from', '')} -> {car.get('to', '')}")
|
||||
print(f" arrive: {car.get('arrive')}")
|
||||
assert False, "Car journey arrival time is earlier than departure time."
|
||||
|
||||
trip_start = agenda.utils.as_date(car["trip"])
|
||||
trip = trip_lookup.get(trip_start)
|
||||
if trip is None:
|
||||
pprint(car)
|
||||
print(f"car journey references unknown trip {trip_start}")
|
||||
sys.exit(-1)
|
||||
|
||||
trip_end = trip.end or trip.start
|
||||
depart_date = agenda.utils.as_date(car["depart"])
|
||||
arrive_date = agenda.utils.as_date(car["arrive"])
|
||||
|
||||
if depart_date < trip.start or arrive_date > trip_end:
|
||||
pprint(car)
|
||||
print(
|
||||
"car journey dates outside trip range: "
|
||||
f"{depart_date} to {arrive_date}; trip {trip.start} to {trip_end}"
|
||||
)
|
||||
sys.exit(-1)
|
||||
|
||||
check_currency(car)
|
||||
|
||||
|
||||
def check_car_journeys() -> None:
|
||||
"""Check car journeys, route files and trip date consistency."""
|
||||
filepath = os.path.join(data_dir, "car_journeys.yaml")
|
||||
if not os.path.exists(filepath):
|
||||
print("0 car journeys")
|
||||
return
|
||||
|
||||
cars = agenda.travel.parse_yaml("car_journeys", data_dir)
|
||||
validate_car_journey_routes(cars)
|
||||
trip_list = agenda.trip.build_trip_list(data_dir)
|
||||
validate_car_journey_dates(cars, trip_list)
|
||||
print(len(cars), "car journeys")
|
||||
|
||||
|
||||
def check_airlines() -> list[agenda.types.StrDict]:
|
||||
"""Check airlines."""
|
||||
airlines: list[agenda.types.StrDict] = agenda.travel.parse_yaml(
|
||||
|
|
@ -521,6 +624,7 @@ def check_airlines() -> list[agenda.types.StrDict]:
|
|||
def check() -> None:
|
||||
"""Validate personal data YAML files."""
|
||||
airlines = check_airlines()
|
||||
check_car_journeys()
|
||||
check_trips()
|
||||
check_flights({airline["iata"] for airline in airlines})
|
||||
check_trains()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue