From a5d1290491f2aecc6402fb8beae4ad8c9296d352 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 1 May 2024 08:31:14 +0200 Subject: [PATCH 1/3] Use cached FX rate if fresh rate not available --- agenda/fx.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/agenda/fx.py b/agenda/fx.py index f0ec161..90cbc5f 100644 --- a/agenda/fx.py +++ b/agenda/fx.py @@ -44,6 +44,17 @@ async def get_gbpusd(config: flask.config.Config) -> Decimal: return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"]) +def read_cached_rates(filename: str, currencies: list[str]) -> dict[str, Decimal]: + """Read FX rates from cache.""" + with open(filename) as file: + data = json.load(file, parse_float=Decimal) + return { + cur: Decimal(data["quotes"][f"GBP{cur}"]) + for cur in currencies + if f"GBP{cur}" in data["quotes"] + } + + def get_rates(config: flask.config.Config) -> dict[str, Decimal]: """Get current values of exchange rates for a list of currencies against GBP.""" currencies = config["CURRENCIES"] @@ -65,22 +76,19 @@ def get_rates(config: flask.config.Config) -> dict[str, Decimal]: recent = datetime.strptime(recent_filename[:16], "%Y-%m-%d_%H:%M") delta = now - recent + full_path = os.path.join(fx_dir, recent_filename) if delta < timedelta(hours=12): - full_path = os.path.join(fx_dir, recent_filename) - with open(full_path) as file: - data = json.load(file, parse_float=Decimal) - return { - cur: Decimal(data["quotes"][f"GBP{cur}"]) - for cur in currencies - if f"GBP{cur}" in data["quotes"] - } + return read_cached_rates(full_path, currencies) url = "http://api.exchangerate.host/live" params = {"currencies": currency_string, "source": "GBP", "access_key": access_key} filename = f"{now_str}_{file_suffix}" - with httpx.Client() as client: - response = client.get(url, params=params) + try: + with httpx.Client() as client: + response = client.get(url, params=params) + except httpx.ConnectError: + return read_cached_rates(full_path, currencies) with open(os.path.join(fx_dir, filename), "w") as file: file.write(response.text) From b9b849802dd85c31fcd7a1ffda5860e6dabeebbb Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 1 May 2024 08:32:15 +0200 Subject: [PATCH 2/3] Show ferry bookings in trip list --- agenda/trip.py | 44 ++++++++++++++++++++++++++++++++-------- templates/macros.html | 33 ++++++++++++++++++++++++++++++ templates/trip_list.html | 4 ++-- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index f2c4a47..4651ec3 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -19,9 +19,9 @@ class UnknownStation(Exception): pass -def load_travel(travel_type: str, data_dir: str) -> list[StrDict]: +def load_travel(travel_type: str, plural: str, data_dir: str) -> list[StrDict]: """Read flight and train journeys.""" - items = travel.parse_yaml(travel_type + "s", data_dir) + items = travel.parse_yaml(plural, data_dir) for item in items: item["type"] = travel_type return items @@ -31,7 +31,7 @@ def load_trains( data_dir: str, route_distances: travel.RouteDistances | None = None ) -> list[StrDict]: """Load trains.""" - trains = load_travel("train", data_dir) + trains = load_travel("train", "trains", data_dir) stations = travel.parse_yaml("stations", data_dir) by_name = {station["name"]: station for station in stations} @@ -58,6 +58,20 @@ def load_trains( return trains +def load_ferries(data_dir: str) -> list[StrDict]: + """Load ferries.""" + ferries = load_travel("ferry", "ferries", data_dir) + terminals = travel.parse_yaml("ferry_terminals", data_dir) + by_name = {terminal["name"]: terminal for terminal in terminals} + + for item in ferries: + assert item["from"] in by_name and item["to"] in by_name + item["from_terminal"] = by_name[item["from"]] + item["to_terminal"] = by_name[item["to"]] + + return ferries + + def depart_datetime(item: StrDict) -> datetime: depart = item["depart"] if isinstance(depart, datetime): @@ -67,7 +81,7 @@ def depart_datetime(item: StrDict) -> datetime: def load_flight_bookings(data_dir: str) -> list[StrDict]: """Load flight bookings.""" - bookings = load_travel("flight", data_dir) + bookings = load_travel("flight", "flights", data_dir) airlines = yaml.safe_load(open(os.path.join(data_dir, "airlines.yaml"))) airports = travel.parse_yaml("airports", data_dir) for booking in bookings: @@ -110,7 +124,9 @@ def build_trip_list( yaml_trip_lookup = {item["trip"]: item for item in yaml_trip_list} travel_items = sorted( - load_flights(data_dir) + load_trains(data_dir, route_distances=route_distances), + load_flights(data_dir) + + load_trains(data_dir, route_distances=route_distances) + + load_ferries(data_dir), key=depart_datetime, ) @@ -169,17 +185,22 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]: stations = {} station_list = [] airports = {} + ferry_terminals = {} for t in trip.travel: if t["type"] == "train": station_list += [t["from_station"], t["to_station"]] for leg in t["legs"]: station_list.append(leg["from_station"]) station_list.append(leg["to_station"]) - else: - assert t["type"] == "flight" + elif t["type"] == "flight": for field in "from_airport", "to_airport": if field in t: airports[t[field]["iata"]] = t[field] + else: + assert t["type"] == "ferry" + for field in "from_terminal", "to_terminal": + terminal = t[field] + ferry_terminals[terminal["name"]] = terminal for s in station_list: if s["uic"] in stations: @@ -205,7 +226,12 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]: if "latitude" in item and "longitude" in item ] - for coord_type, coord_dict in ("station", stations), ("airport", airports): + locations = [ + ("station", stations), + ("airport", airports), + ("ferry_terminals", ferry_terminals), + ] + for coord_type, coord_dict in locations: coords += [ { "name": s["name"], @@ -234,6 +260,8 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: routes = [] seen_geojson = set() for t in trip.travel: + if t["type"] == "ferry": + continue if t["type"] == "flight": if "from_airport" not in t or "to_airport" not in t: continue diff --git a/templates/macros.html b/templates/macros.html index 6db2d87..fec5da9 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -226,3 +226,36 @@ {% endif %} {% endmacro %} + +{% macro ferry_row(item) %} +
{{ item.depart.strftime("%a, %d %b %Y") }}
+
+ {{ item.from }} → {{ item.to }} +
+ +
{{ item.depart.strftime("%H:%M") }}
+
+ {% if item.arrive %} + {{ item.arrive.strftime("%H:%M") }} + {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} + {% endif %} +
+ +
+
{{ item.operator }}
+
+
+
+ +
+ {% if g.user.is_authenticated and item.price and item.currency %} + {{ "{:,f}".format(item.price) }} {{ item.currency }} + {% if item.currency != "GBP" %} + {{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP + {% endif %} + {% endif %} +
+ + + {#
{{ item | pprint }}
#} +{% endmacro %} diff --git a/templates/trip_list.html b/templates/trip_list.html index 1304c51..8a497f9 100644 --- a/templates/trip_list.html +++ b/templates/trip_list.html @@ -1,8 +1,8 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} +{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row, ferry_row with context %} -{% set row = { "flight": flight_row, "train": train_row } %} +{% set row = { "flight": flight_row, "train": train_row, "ferry": ferry_row } %} {% block title %}Trips - Edward Betts{% endblock %} From afa2a2e93431346e2a2f000583c055e4ce29ebdf Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 1 May 2024 11:59:21 +0300 Subject: [PATCH 3/3] Show ferry routes and terminals on the map --- agenda/trip.py | 27 ++++++++++++++++++++++----- static/js/map.js | 1 + 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index 4651ec3..e8f542f 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -66,8 +66,13 @@ def load_ferries(data_dir: str) -> list[StrDict]: for item in ferries: assert item["from"] in by_name and item["to"] in by_name - item["from_terminal"] = by_name[item["from"]] - item["to_terminal"] = by_name[item["to"]] + from_terminal, to_terminal = by_name[item["from"]], by_name[item["to"]] + item["from_terminal"] = from_terminal + item["to_terminal"] = to_terminal + + geojson = from_terminal["routes"].get(item["to"]) + if geojson: + item["geojson_filename"] = geojson return ferries @@ -229,7 +234,7 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]: locations = [ ("station", stations), ("airport", airports), - ("ferry_terminals", ferry_terminals), + ("ferry_terminal", ferry_terminals), ] for coord_type, coord_dict in locations: coords += [ @@ -252,7 +257,7 @@ def latlon_tuple(stop: StrDict) -> tuple[float, float]: def read_geojson(data_dir: str, filename: str) -> str: """Read GeoJSON from file.""" - return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read() + return open(os.path.join(data_dir, filename + ".geojson")).read() def get_trip_routes(trip: Trip) -> list[StrDict]: @@ -261,6 +266,18 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: seen_geojson = set() for t in trip.travel: if t["type"] == "ferry": + ferry_from, ferry_to = t["from_terminal"], t["to_terminal"] + + key = "_".join(["ferry"] + sorted([ferry_from["name"], ferry_to["name"]])) + filename = os.path.join("ferry_routes", t["geojson_filename"]) + + routes.append( + { + "type": "train", + "key": key, + "geojson_filename": filename, + } + ) continue if t["type"] == "flight": if "from_airport" not in t or "to_airport" not in t: @@ -300,7 +317,7 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: { "type": "train", "key": key, - "geojson_filename": geojson_filename, + "geojson_filename": os.path.join("train_routes", geojson_filename), } ) diff --git a/static/js/map.js b/static/js/map.js index 21872c9..cd29dc7 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -16,6 +16,7 @@ function emoji_icon(emoji) { var icons = { "station": emoji_icon("🚉"), "airport": emoji_icon("✈️"), + "ferry_terminal": emoji_icon("✈️"), "accommodation": emoji_icon("🏨"), "conference": emoji_icon("🎤"), "event": emoji_icon("🍷"),