Compare commits
3 commits
2b822e28a0
...
afa2a2e934
Author | SHA1 | Date | |
---|---|---|---|
Edward Betts | afa2a2e934 | ||
Edward Betts | b9b849802d | ||
Edward Betts | a5d1290491 |
28
agenda/fx.py
28
agenda/fx.py
|
@ -44,6 +44,17 @@ async def get_gbpusd(config: flask.config.Config) -> Decimal:
|
||||||
return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"])
|
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]:
|
def get_rates(config: flask.config.Config) -> dict[str, Decimal]:
|
||||||
"""Get current values of exchange rates for a list of currencies against GBP."""
|
"""Get current values of exchange rates for a list of currencies against GBP."""
|
||||||
currencies = config["CURRENCIES"]
|
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")
|
recent = datetime.strptime(recent_filename[:16], "%Y-%m-%d_%H:%M")
|
||||||
delta = now - recent
|
delta = now - recent
|
||||||
|
|
||||||
|
full_path = os.path.join(fx_dir, recent_filename)
|
||||||
if delta < timedelta(hours=12):
|
if delta < timedelta(hours=12):
|
||||||
full_path = os.path.join(fx_dir, recent_filename)
|
return read_cached_rates(full_path, currencies)
|
||||||
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"]
|
|
||||||
}
|
|
||||||
|
|
||||||
url = "http://api.exchangerate.host/live"
|
url = "http://api.exchangerate.host/live"
|
||||||
params = {"currencies": currency_string, "source": "GBP", "access_key": access_key}
|
params = {"currencies": currency_string, "source": "GBP", "access_key": access_key}
|
||||||
|
|
||||||
filename = f"{now_str}_{file_suffix}"
|
filename = f"{now_str}_{file_suffix}"
|
||||||
with httpx.Client() as client:
|
try:
|
||||||
response = client.get(url, params=params)
|
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:
|
with open(os.path.join(fx_dir, filename), "w") as file:
|
||||||
file.write(response.text)
|
file.write(response.text)
|
||||||
|
|
|
@ -19,9 +19,9 @@ class UnknownStation(Exception):
|
||||||
pass
|
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."""
|
"""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:
|
for item in items:
|
||||||
item["type"] = travel_type
|
item["type"] = travel_type
|
||||||
return items
|
return items
|
||||||
|
@ -31,7 +31,7 @@ def load_trains(
|
||||||
data_dir: str, route_distances: travel.RouteDistances | None = None
|
data_dir: str, route_distances: travel.RouteDistances | None = None
|
||||||
) -> list[StrDict]:
|
) -> list[StrDict]:
|
||||||
"""Load trains."""
|
"""Load trains."""
|
||||||
trains = load_travel("train", data_dir)
|
trains = load_travel("train", "trains", data_dir)
|
||||||
stations = travel.parse_yaml("stations", data_dir)
|
stations = travel.parse_yaml("stations", data_dir)
|
||||||
by_name = {station["name"]: station for station in stations}
|
by_name = {station["name"]: station for station in stations}
|
||||||
|
|
||||||
|
@ -58,6 +58,25 @@ def load_trains(
|
||||||
return 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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def depart_datetime(item: StrDict) -> datetime:
|
def depart_datetime(item: StrDict) -> datetime:
|
||||||
depart = item["depart"]
|
depart = item["depart"]
|
||||||
if isinstance(depart, datetime):
|
if isinstance(depart, datetime):
|
||||||
|
@ -67,7 +86,7 @@ def depart_datetime(item: StrDict) -> datetime:
|
||||||
|
|
||||||
def load_flight_bookings(data_dir: str) -> list[StrDict]:
|
def load_flight_bookings(data_dir: str) -> list[StrDict]:
|
||||||
"""Load flight bookings."""
|
"""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")))
|
airlines = yaml.safe_load(open(os.path.join(data_dir, "airlines.yaml")))
|
||||||
airports = travel.parse_yaml("airports", data_dir)
|
airports = travel.parse_yaml("airports", data_dir)
|
||||||
for booking in bookings:
|
for booking in bookings:
|
||||||
|
@ -110,7 +129,9 @@ def build_trip_list(
|
||||||
yaml_trip_lookup = {item["trip"]: item for item in yaml_trip_list}
|
yaml_trip_lookup = {item["trip"]: item for item in yaml_trip_list}
|
||||||
|
|
||||||
travel_items = sorted(
|
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,
|
key=depart_datetime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -169,17 +190,22 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]:
|
||||||
stations = {}
|
stations = {}
|
||||||
station_list = []
|
station_list = []
|
||||||
airports = {}
|
airports = {}
|
||||||
|
ferry_terminals = {}
|
||||||
for t in trip.travel:
|
for t in trip.travel:
|
||||||
if t["type"] == "train":
|
if t["type"] == "train":
|
||||||
station_list += [t["from_station"], t["to_station"]]
|
station_list += [t["from_station"], t["to_station"]]
|
||||||
for leg in t["legs"]:
|
for leg in t["legs"]:
|
||||||
station_list.append(leg["from_station"])
|
station_list.append(leg["from_station"])
|
||||||
station_list.append(leg["to_station"])
|
station_list.append(leg["to_station"])
|
||||||
else:
|
elif t["type"] == "flight":
|
||||||
assert t["type"] == "flight"
|
|
||||||
for field in "from_airport", "to_airport":
|
for field in "from_airport", "to_airport":
|
||||||
if field in t:
|
if field in t:
|
||||||
airports[t[field]["iata"]] = t[field]
|
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:
|
for s in station_list:
|
||||||
if s["uic"] in stations:
|
if s["uic"] in stations:
|
||||||
|
@ -205,7 +231,12 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]:
|
||||||
if "latitude" in item and "longitude" in item
|
if "latitude" in item and "longitude" in item
|
||||||
]
|
]
|
||||||
|
|
||||||
for coord_type, coord_dict in ("station", stations), ("airport", airports):
|
locations = [
|
||||||
|
("station", stations),
|
||||||
|
("airport", airports),
|
||||||
|
("ferry_terminal", ferry_terminals),
|
||||||
|
]
|
||||||
|
for coord_type, coord_dict in locations:
|
||||||
coords += [
|
coords += [
|
||||||
{
|
{
|
||||||
"name": s["name"],
|
"name": s["name"],
|
||||||
|
@ -226,7 +257,7 @@ def latlon_tuple(stop: StrDict) -> tuple[float, float]:
|
||||||
|
|
||||||
def read_geojson(data_dir: str, filename: str) -> str:
|
def read_geojson(data_dir: str, filename: str) -> str:
|
||||||
"""Read GeoJSON from file."""
|
"""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]:
|
def get_trip_routes(trip: Trip) -> list[StrDict]:
|
||||||
|
@ -234,6 +265,20 @@ def get_trip_routes(trip: Trip) -> list[StrDict]:
|
||||||
routes = []
|
routes = []
|
||||||
seen_geojson = set()
|
seen_geojson = set()
|
||||||
for t in trip.travel:
|
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 t["type"] == "flight":
|
||||||
if "from_airport" not in t or "to_airport" not in t:
|
if "from_airport" not in t or "to_airport" not in t:
|
||||||
continue
|
continue
|
||||||
|
@ -272,7 +317,7 @@ def get_trip_routes(trip: Trip) -> list[StrDict]:
|
||||||
{
|
{
|
||||||
"type": "train",
|
"type": "train",
|
||||||
"key": key,
|
"key": key,
|
||||||
"geojson_filename": geojson_filename,
|
"geojson_filename": os.path.join("train_routes", geojson_filename),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ function emoji_icon(emoji) {
|
||||||
var icons = {
|
var icons = {
|
||||||
"station": emoji_icon("🚉"),
|
"station": emoji_icon("🚉"),
|
||||||
"airport": emoji_icon("✈️"),
|
"airport": emoji_icon("✈️"),
|
||||||
|
"ferry_terminal": emoji_icon("✈️"),
|
||||||
"accommodation": emoji_icon("🏨"),
|
"accommodation": emoji_icon("🏨"),
|
||||||
"conference": emoji_icon("🎤"),
|
"conference": emoji_icon("🎤"),
|
||||||
"event": emoji_icon("🍷"),
|
"event": emoji_icon("🍷"),
|
||||||
|
|
|
@ -226,3 +226,36 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro ferry_row(item) %}
|
||||||
|
<div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
|
||||||
|
<div class="grid-item">
|
||||||
|
{{ item.from }} → {{ item.to }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">{{ item.depart.strftime("%H:%M") }}</div>
|
||||||
|
<div class="grid-item">
|
||||||
|
{% if item.arrive %}
|
||||||
|
{{ item.arrive.strftime("%H:%M") }}
|
||||||
|
{% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item"></div>
|
||||||
|
<div class="grid-item">{{ item.operator }}</div>
|
||||||
|
<div class="grid-item"></div>
|
||||||
|
<div class="grid-item"></div>
|
||||||
|
<div class="grid-item"></div>
|
||||||
|
|
||||||
|
<div class="grid-item text-end">
|
||||||
|
{% if g.user.is_authenticated and item.price and item.currency %}
|
||||||
|
<span class="badge bg-info text-nowrap">{{ "{:,f}".format(item.price) }} {{ item.currency }}</span>
|
||||||
|
{% if item.currency != "GBP" %}
|
||||||
|
<span class="badge bg-info text-nowrap">{{ "{:,.2f}".format(item.price / fx_rate[item.currency]) }} GBP</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{# <div class="grid-item">{{ item | pprint }}</div> #}
|
||||||
|
{% endmacro %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block title %}Trips - Edward Betts{% endblock %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue