agenda/web_view.py

382 lines
11 KiB
Python
Executable file

#!/usr/bin/python3
"""Web page to show upcoming events."""
import inspect
import operator
import os.path
import sys
import traceback
from datetime import date, datetime
import flask
import werkzeug
import werkzeug.debug.tbtools
import yaml
import agenda.data
import agenda.error_mail
import agenda.thespacedevs
from agenda import format_list_with_ampersand, travel
from agenda.types import StrDict, Trip
app = flask.Flask(__name__)
app.debug = False
app.config.from_object("config.default")
agenda.error_mail.setup_error_mail(app)
@app.errorhandler(werkzeug.exceptions.InternalServerError)
def exception_handler(e: werkzeug.exceptions.InternalServerError) -> tuple[str, int]:
"""Handle exception."""
exec_type, exc_value, current_traceback = sys.exc_info()
assert exc_value
tb = werkzeug.debug.tbtools.DebugTraceback(exc_value)
summary = tb.render_traceback_html(include_title=False)
exc_lines = "".join(tb._te.format_exception_only())
last_frame = list(traceback.walk_tb(current_traceback))[-1][0]
last_frame_args = inspect.getargs(last_frame.f_code)
return (
flask.render_template(
"show_error.html",
plaintext=tb.render_traceback_text(),
exception=exc_lines,
exception_type=tb._te.exc_type.__name__,
summary=summary,
last_frame=last_frame,
last_frame_args=last_frame_args,
),
500,
)
@app.route("/")
async def index() -> str:
"""Index page."""
now = datetime.now()
data = await agenda.data.get_data(now, app.config)
return flask.render_template("index.html", today=now.date(), **data)
@app.route("/launches")
async def launch_list() -> str:
"""Web page showing List of space launches."""
now = datetime.now()
data_dir = app.config["DATA_DIR"]
rocket_dir = os.path.join(data_dir, "thespacedevs")
rockets = await agenda.thespacedevs.get_launches(rocket_dir, limit=100)
return flask.render_template("launches.html", rockets=rockets, now=now)
@app.route("/gaps")
async def gaps_page() -> str:
"""List of available gaps."""
now = datetime.now()
data = await agenda.data.get_data(now, app.config)
return flask.render_template("gaps.html", today=now.date(), gaps=data["gaps"])
@app.route("/travel")
def travel_list() -> str:
"""Page showing a list of upcoming travel."""
data_dir = app.config["PERSONAL_DATA"]
flights = travel.parse_yaml("flights", data_dir)
trains = travel.parse_yaml("trains", data_dir)
return flask.render_template("travel.html", flights=flights, trains=trains)
def as_date(d: date | datetime) -> date:
"""Date of event."""
return d.date() if isinstance(d, datetime) else d
@app.route("/conference")
def conference_list() -> str:
"""Page showing a list of conferences."""
data_dir = app.config["PERSONAL_DATA"]
filepath = os.path.join(data_dir, "conferences.yaml")
item_list = yaml.safe_load(open(filepath))
today = date.today()
for conf in item_list:
conf["start_date"] = as_date(conf["start"])
conf["end_date"] = as_date(conf["end"])
item_list.sort(key=operator.itemgetter("start_date"))
current = [
conf
for conf in item_list
if conf["start_date"] <= today and conf["end_date"] >= today
]
past = [conf for conf in item_list if conf["end_date"] < today]
future = [conf for conf in item_list if conf["start_date"] > today]
return flask.render_template(
"conference_list.html",
current=current,
past=past,
future=future,
today=today,
get_country=agenda.get_country,
)
@app.route("/accommodation")
def accommodation_list() -> str:
"""Page showing a list of past, present and future accommodation."""
data_dir = app.config["PERSONAL_DATA"]
items = travel.parse_yaml("accommodation", data_dir)
stays_in_2024 = [item for item in items if item["from"].year == 2024]
total_nights_2024 = sum(
(stay["to"].date() - stay["from"].date()).days for stay in stays_in_2024
)
nights_abroad_2024 = sum(
(stay["to"].date() - stay["from"].date()).days
for stay in stays_in_2024
if stay["country"] != "gb"
)
return flask.render_template(
"accommodation.html",
items=items,
total_nights_2024=total_nights_2024,
nights_abroad_2024=nights_abroad_2024,
get_country=agenda.get_country,
)
def load_travel(travel_type: str) -> list[StrDict]:
"""Read flight and train journeys."""
data_dir = app.config["PERSONAL_DATA"]
items = travel.parse_yaml(travel_type + "s", data_dir)
for item in items:
item["type"] = travel_type
return items
def load_trains() -> list[StrDict]:
"""Load trains."""
data_dir = app.config["PERSONAL_DATA"]
trains = load_travel("train")
stations = travel.parse_yaml("stations", data_dir)
by_name = {station["name"]: station for station in stations}
for train in trains:
assert train["from"] in by_name
assert train["to"] in by_name
train["from_station"] = by_name[train["from"]]
train["to_station"] = by_name[train["to"]]
for leg in train["legs"]:
assert leg["from"] in by_name
assert leg["to"] in by_name
leg["from_station"] = by_name[leg["from"]]
leg["to_station"] = by_name[leg["to"]]
return trains
def load_flights() -> list[StrDict]:
"""Load flights."""
data_dir = app.config["PERSONAL_DATA"]
flights = load_travel("flight")
airports = travel.parse_yaml("airports", data_dir)
for flight in flights:
if flight["from"] in airports:
flight["from_airport"] = airports[flight["from"]]
if flight["to"] in airports:
flight["to_airport"] = airports[flight["to"]]
return flights
def build_trip_list() -> list[Trip]:
"""Generate list of trips."""
trips: dict[date, Trip] = {}
data_dir = app.config["PERSONAL_DATA"]
travel_items = sorted(
load_flights() + load_trains(), key=operator.itemgetter("depart")
)
data = {
"travel": travel_items,
"accommodation": travel.parse_yaml("accommodation", data_dir),
"conferences": travel.parse_yaml("conferences", data_dir),
"events": travel.parse_yaml("events", data_dir),
}
for key, item_list in data.items():
assert isinstance(item_list, list)
for item in item_list:
if not (start := item.get("trip")):
continue
if start not in trips:
trips[start] = Trip(start=start)
getattr(trips[start], key).append(item)
return [trip for _, trip in sorted(trips.items())]
@app.route("/trip")
def trip_list() -> str:
"""Page showing a list of trips."""
trip_list = build_trip_list()
today = date.today()
current = [
item
for item in trip_list
if item.start <= today and (item.end or item.start) >= today
]
past = [item for item in trip_list if (item.end or item.start) < today]
future = [item for item in trip_list if item.start > today]
return flask.render_template(
"trips.html",
current=current,
past=past,
future=future,
today=today,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
)
def collect_trip_coordinates(trip: Trip) -> list[StrDict]:
"""Extract and deduplicate airport and station coordinates from trip."""
stations = {}
station_list = []
airports = {}
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"
for field in "from_airport", "to_airport":
if field in t:
airports[t[field]["iata"]] = t[field]
for s in station_list:
if s["uic"] in stations:
continue
stations[s["uic"]] = s
return [
{
"name": s["name"],
"type": "station",
"latitude": s["latitude"],
"longitude": s["longitude"],
}
for s in stations.values()
] + [
{
"name": s["name"],
"type": "airport",
"latitude": s["latitude"],
"longitude": s["longitude"],
}
for s in airports.values()
]
def latlon_tuple(stop: StrDict) -> tuple[float, float]:
"""Given a transport stop return the lat/lon as a tuple."""
return (stop["latitude"], stop["longitude"])
def read_geojson(filename: str) -> str:
data_dir = app.config["PERSONAL_DATA"]
return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read()
def get_trip_routes(trip: Trip) -> list[StrDict]:
routes = []
seen_geojson = set()
for t in trip.travel:
if t["type"] == "flight":
if "from_airport" not in t or "to_airport" not in t:
continue
fly_from, fly_to = t["from_airport"], t["to_airport"]
routes.append(
{
"type": "flight",
"from": latlon_tuple(fly_from),
"to": latlon_tuple(fly_to),
}
)
else:
assert t["type"] == "train"
for leg in t["legs"]:
train_from, train_to = leg["from_station"], leg["to_station"]
geojson_filename = train_from.get("routes", {}).get(train_to["uic"])
print(train_from["name"], train_to["name"], geojson_filename)
if not geojson_filename:
routes.append(
{
"type": "train",
"from": latlon_tuple(train_from),
"to": latlon_tuple(train_to),
}
)
continue
if geojson_filename in seen_geojson:
continue
seen_geojson.add(geojson_filename)
routes.append(
{
"type": "train",
"geojson": read_geojson(geojson_filename),
}
)
return routes
@app.route("/trip/<start>")
def trip_page(start: str) -> str:
"""Individual trip page."""
trip_list = build_trip_list()
today = date.today()
trip = next((trip for trip in trip_list if trip.start.isoformat() == start), None)
if not trip:
flask.abort(404)
coordinates = collect_trip_coordinates(trip)
routes = get_trip_routes(trip)
return flask.render_template(
"trip_page.html",
trip=trip,
today=today,
coordinates=coordinates,
routes=routes,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
)
if __name__ == "__main__":
app.run(host="0.0.0.0")