#!/usr/bin/python3 """Check prices of ferries to France.""" import inspect from datetime import date, datetime from typing import Any import configparser import flask import requests import werkzeug.exceptions from werkzeug.debug.tbtools import get_current_traceback import pytz app = flask.Flask(__name__) app.debug = True ports = { "PORTSMOUTH": "GBPME", "PLYMOUTH": "GBPLY", "POOLE": "GBPOO", "CAEN": "FROUI", "CHERBOURG": "FRCER", "ST MALO": "FRSML", } ferry_config = configparser.ConfigParser() ferry_config.read("/home/edward/.config/brittany-ferries/config") def get_vehicle() -> dict[str, str | int | list[str] | dict[str, None]]: """Return vehicle detail in the format for the Brittany Ferries API.""" return { "type": ferry_config.get("vehicle", "type"), "registrations": [ferry_config.get("vehicle", "registration")], "height": ferry_config.getint("vehicle", "height"), "length": ferry_config.getint("vehicle", "length"), "extras": {"rearMountedBikeCarrier": None}, } @app.errorhandler(werkzeug.exceptions.InternalServerError) def exception_handler(e): tb = get_current_traceback() last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library) last_frame_args = inspect.getargs(last_frame.code) return ( flask.render_template( "show_error.html", tb=tb, last_frame=last_frame, last_frame_args=last_frame_args, ), 500, ) def parse_date(d: str) -> date: """Parse an ISO date.""" return datetime.strptime(d, "%Y-%m-%d").date() def get_accommodations( departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str ): url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/accommodations" post_data = { "bookingReference": None, "departurePort": departure_port, "arrivalPort": arrival_port, "departureDate": departure_date, "passengers": {"adults": 2, "children": 0, "infants": 0}, "disability": None, "vehicle": get_vehicle(), "petCabinsNeeded": False, "ticketTier": ticket_tier, "pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0}, "sponsor": None, } r = requests.post(url, json=post_data) return r.json() def get_prices( departure_port: str, arrival_port: str, from_date: str, to_date: str ) -> dict[str, Any]: """Call Brittany Ferries API to get details of crossings.""" url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/prices" post_data = { "bookingReference": None, "pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0}, "passengers": {"adults": 2, "children": 0, "infants": 0}, "vehicle": get_vehicle(), "departurePort": departure_port, "arrivalPort": arrival_port, "disability": None, "sponsor": None, "fromDate": f"{from_date}T00:00:00", "toDate": f"{to_date}T23:59:59", } r = requests.post(url, json=post_data) data: dict[str, Any] = r.json() return data @app.route("/route////") def show_route( departure_port: str, arrival_port: str, from_date: str, to_date: str ) -> str: """Page showing list of prices.""" prices = get_prices(departure_port, arrival_port) port_lookup = {code: name for name, code in ports.items()} return flask.render_template( "route.html", departure_port=port_lookup[departure_port], arrival_port=port_lookup[arrival_port], days=prices["crossings"], parse_date=parse_date, ) @app.route("/") def start(): """Start page.""" return flask.redirect(flask.url_for("outbound_page")) def cabins_url(dep, arr, crossing, ticket_tier): dt = datetime.fromisoformat(crossing["departureDateTime"]["iso"]) utc_dt = dt.astimezone(pytz.utc) return flask.url_for( "cabins", departure_port=ports[dep], arrival_port=ports[arr], departure_date=utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z"), ticket_tier=ticket_tier, ) def get_days_until_start(): start = date.fromisoformat(ferry_config.get("dates", "start")) return (start - date.today()).days @app.route("/outbound") def outbound_page() -> str: """Show all routes on one page.""" selection = [ ["PORTSMOUTH", "CAEN"], ["PORTSMOUTH", "CHERBOURG"], ["PORTSMOUTH", "ST MALO"], ["POOLE", "CHERBOURG"], ] from_date = ferry_config.get("outbound", "from") to_date = ferry_config.get("outbound", "to") all_data = [ (dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"]) for dep, arr in selection ] return flask.render_template( "all_routes.html", data=all_data, days_until_start=get_days_until_start(), ports=ports, parse_date=parse_date, from_date=from_date, to_date=to_date, other="return", cabins_url=cabins_url, ) @app.route("/return") def return_page() -> str: """Show all routes on one page.""" selection = [ ["CAEN", "PORTSMOUTH"], ["CHERBOURG", "PORTSMOUTH"], ["ST MALO", "PORTSMOUTH"], ["CHERBOURG", "POOLE"], ] from_date = ferry_config.get("return", "from") to_date = ferry_config.get("return", "to") all_data = [ (dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"]) for dep, arr in selection ] return flask.render_template( "all_routes.html", data=all_data, ports=ports, days_until_start=get_days_until_start(), parse_date=parse_date, from_date=from_date, to_date=to_date, other="outbound", cabins_url=cabins_url, ) @app.route("/cabins////") def cabins(departure_port, arrival_port, departure_date, ticket_tier): data = get_accommodations(departure_port, arrival_port, departure_date, ticket_tier) return flask.render_template( "cabins.html", departure_port=departure_port, arrival_port=arrival_port, departure_date=departure_date, ticket_tier=ticket_tier, accommodations=data["accommodations"], ) @app.route("/routes") def route_list() -> str: """List of routes.""" return flask.render_template("index.html", routes=routes, ports=ports) if __name__ == "__main__": app.run(host="0.0.0.0", port=5001)