diff --git a/ferry/__init__.py b/ferry/__init__.py index 58a329f..4d24354 100644 --- a/ferry/__init__.py +++ b/ferry/__init__.py @@ -19,6 +19,7 @@ ports = { "CHERBOURG": "FRCER", "ST MALO": "FRSML", "LE HAVRE": "FRLEH", + "ROSCOFF": "FRROS", } port_lookup = {code: name for name, code in ports.items()} diff --git a/ferry/api.py b/ferry/api.py index dc57c1d..15dffac 100644 --- a/ferry/api.py +++ b/ferry/api.py @@ -3,6 +3,7 @@ from typing import Any, TypedDict import requests +import json from . import Vehicle @@ -19,17 +20,18 @@ class VehicleDict(TypedDict): registrations: list[str] height: int length: int - extras: dict[str, None] + extras: dict[str, None | bool] -def vehicle_dict(v: Vehicle) -> VehicleDict: +def vehicle_dict(v: Vehicle, rear_mounted_bike_carrier: bool = False) -> VehicleDict: """Return vehicle detail in the format for the Brittany Ferries API.""" + rmbc = True if rear_mounted_bike_carrier else None return { "type": v.type, "registrations": [v.registration], "height": v.height, "length": v.length, - "extras": {"rearMountedBikeCarrier": None}, + "extras": {"rearMountedBikeCarrier": rmbc}, } @@ -41,6 +43,7 @@ def get_prices( vehicle: Vehicle, adults: int = 2, small_dogs: int = 1, + rear_mounted_bike_carrier: bool = False, ) -> dict[str, Any]: """Call Brittany Ferries API to get details of crossings.""" url = api_root_url + "crossing/prices" @@ -49,7 +52,7 @@ def get_prices( "bookingReference": None, "pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0}, "passengers": {"adults": adults, "children": 0, "infants": 0}, - "vehicle": vehicle_dict(vehicle), + "vehicle": vehicle_dict(vehicle, rear_mounted_bike_carrier), "departurePort": departure_port, "arrivalPort": arrival_port, "disability": None, @@ -60,6 +63,8 @@ def get_prices( r = requests.post(url, json=post_data, headers=headers) data: dict[str, Any] = r.json() + if "crossings" not in data: + print(json.dumps(data, indent=2)) return data @@ -71,6 +76,7 @@ def get_accommodations( vehicle: Vehicle, adults: int, small_dogs: int, + rear_mounted_bike_carrier: bool = False, ) -> dict[str, Any]: """Grab cabin details.""" url = api_root_url + "crossing/accommodations" @@ -81,7 +87,7 @@ def get_accommodations( "departureDate": departure_date, "passengers": {"adults": adults, "children": 0, "infants": 0}, "disability": None, - "vehicle": vehicle_dict(vehicle), + "vehicle": vehicle_dict(vehicle, rear_mounted_bike_carrier), "petCabinsNeeded": True, "ticketTier": ticket_tier, "pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0}, @@ -92,4 +98,9 @@ def get_accommodations( json_data: dict[str, Any] = requests.post( url, json=post_data, headers=headers ).json() + if "crossings" not in json_data: + print() + print(json.dumps(post_data, indent=2)) + print() + print(json.dumps(json_data, indent=2)) return json_data diff --git a/main.py b/main.py index ba46759..a997b28 100755 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import os.path import re import sys import traceback +import collections from datetime import date, datetime, timedelta from typing import Any @@ -37,6 +38,7 @@ routes = { ("PORTSMOUTH", "ST MALO"), ("PORTSMOUTH", "LE HAVRE"), ("POOLE", "CHERBOURG"), + ("PLYMOUTH", "ROSCOFF"), ], "return": [ ("CAEN", "PORTSMOUTH"), @@ -44,6 +46,7 @@ routes = { ("ST MALO", "PORTSMOUTH"), ("LE HAVRE", "PORTSMOUTH"), ("CHERBOURG", "POOLE"), + ("ROSCOFF", "PLYMOUTH"), ], } @@ -139,7 +142,13 @@ def start() -> Response | str: return flask.render_template("index.html") -def cabins_url(dep: str, arr: str, crossing: dict[str, Any], ticket_tier: str) -> str: +def cabins_url( + dep: str, + arr: str, + crossing: dict[str, Any], + ticket_tier: str, + rear_mounted_bike_carrier: bool, +) -> str: """Generate a URL for the cabins on a given crossing.""" dt = datetime.fromisoformat(crossing["departureDateTime"]["iso"]) utc_dt = dt.astimezone(pytz.utc) @@ -155,6 +164,7 @@ def cabins_url(dep: str, arr: str, crossing: dict[str, Any], ticket_tier: str) - ticket_tier=ticket_tier, adults=adults_str, small_dogs=small_dogs_str, + rear_mounted_bike_carrier=("true" if rear_mounted_bike_carrier else None), ) @@ -195,9 +205,12 @@ def get_prices_with_cache( adults: int, small_dogs: int, refresh: bool = False, + rear_mounted_bike_carrier: bool = False, ) -> PriceData: """Get price data using cache.""" - params = f"{direction}_{start}_{end}_{adults}_{small_dogs}" + params = ( + f"{direction}_{start}_{end}_{adults}_{small_dogs}_{rear_mounted_bike_carrier}" + ) if not refresh: data = check_cache_for_prices(params) if data: @@ -217,6 +230,7 @@ def get_prices_with_cache( vehicle, adults, small_dogs, + rear_mounted_bike_carrier, )["crossings"], ) for dep, arr in selection @@ -228,7 +242,10 @@ def get_prices_with_cache( return all_data -def read_pax() -> dict[str, int]: +Pax = collections.namedtuple("Pax", ["adults", "small_dogs"]) + + +def read_pax() -> Pax: """Get the number of adults and dogs that are travelling.""" config_adults = int(ferry_config.get("pax", "adults")) config_dogs = int(ferry_config.get("pax", "dogs")) @@ -239,7 +256,14 @@ def read_pax() -> dict[str, int]: small_dogs_str = flask.request.args.get("small_dogs") small_dogs = int(small_dogs_str) if small_dogs_str else config_dogs - return {"adults": adults, "small_dogs": small_dogs} + return Pax(adults=adults, small_dogs=small_dogs) + + +pax_options = [ + {"pax": Pax(adults=1, small_dogs=0), "label": "1 adult"}, + {"pax": Pax(adults=2, small_dogs=0), "label": "2 adults"}, + {"pax": Pax(adults=2, small_dogs=1), "label": "2 adults and a dog"}, +] def build_outbound(section: str) -> str: @@ -247,6 +271,9 @@ def build_outbound(section: str) -> str: start = ferry_config.get(section, "from") end = ferry_config.get(section, "to") refresh = bool(flask.request.args.get("refresh")) + rear_mounted_bike_carrier = ( + flask.request.args.get("rear_mounted_bike_carrier") == "true" + ) pax = read_pax() @@ -257,9 +284,10 @@ def build_outbound(section: str) -> str: start, end, routes["outbound"], - pax["adults"], - pax["small_dogs"], + pax.adults, + pax.small_dogs, refresh, + rear_mounted_bike_carrier, ) return flask.render_template( @@ -274,6 +302,9 @@ def build_outbound(section: str) -> str: get_duration=get_duration, time_delta=-60, format_pet_options=format_pet_options, + pax=pax, + pax_options=pax_options, + rear_mounted_bike_carrier=rear_mounted_bike_carrier, ) @@ -283,6 +314,9 @@ def build_return(section: str) -> str: end = ferry_config.get(section, "to") refresh = bool(flask.request.args.get("refresh")) pax = read_pax() + rear_mounted_bike_carrier = ( + flask.request.args.get("rear_mounted_bike_carrier") == "true" + ) direction = section[:-1] if section[-1].isdigit() else section @@ -291,9 +325,10 @@ def build_return(section: str) -> str: start, end, routes["return"], - pax["adults"], - pax["small_dogs"], + pax.adults, + pax.small_dogs, refresh, + rear_mounted_bike_carrier, ) return flask.render_template( @@ -308,6 +343,9 @@ def build_return(section: str) -> str: get_duration=get_duration, time_delta=60, format_pet_options=format_pet_options, + pax=pax, + pax_options=pax_options, + rear_mounted_bike_carrier=rear_mounted_bike_carrier, ) @@ -350,11 +388,16 @@ def format_pet_options(o: dict[str, bool]) -> list[str]: def get_accommodations_with_cache( - dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False + dep: str, + arr: str, + d: str, + ticket_tier: str, + refresh: bool = False, + rear_mounted_bike_carrier: bool = False, ) -> dict[str, list[dict[str, Any]]]: pax = read_pax() - params = f"{dep}_{arr}_{d}_{ticket_tier}_{pax['adults']}_{pax['small_dogs']}" + params = f"{dep}_{arr}_{d}_{ticket_tier}_{pax.adults}_{pax.small_dogs}_{rear_mounted_bike_carrier}" existing_files = os.listdir(cache_location()) existing = [f for f in existing_files if f.endswith(params + ".json")] if not refresh and existing: @@ -372,7 +415,14 @@ def get_accommodations_with_cache( vehicle = vehicle_from_config(ferry_config) filename = cache_filename(params) data = get_accommodations( - dep, arr, d, ticket_tier, vehicle, pax["adults"], pax["small_dogs"] + dep, + arr, + d, + ticket_tier, + vehicle, + pax.adults, + pax.small_dogs, + rear_mounted_bike_carrier, ) with open(filename, "w") as out: @@ -451,14 +501,29 @@ def cabins( time_delta = -60 pax = read_pax() + rear_mounted_bike_carrier = ( + flask.request.args.get("rear_mounted_bike_carrier") == "true" + ) prices = get_prices_with_cache( - direction, start, end, routes[direction], pax["adults"], pax["small_dogs"] + direction, + start, + end, + routes[direction], + pax.adults, + pax.small_dogs, + False, + rear_mounted_bike_carrier, ) crossing = lookup_sailing_id(prices, sailing_id) cabin_data = get_accommodations_with_cache( - departure_port, arrival_port, departure_date, ticket_tier + departure_port, + arrival_port, + departure_date, + ticket_tier, + False, + rear_mounted_bike_carrier, ) accommodations = [ a @@ -467,6 +532,8 @@ def cabins( # and "Inside" not in a["description"] ] + pax_labels = {i["pax"]: i["label"] for i in pax_options} + dep = dateutil.parser.isoparse(departure_date) return flask.render_template( @@ -483,6 +550,9 @@ def cabins( time_delta=time_delta, format_pet_options=format_pet_options, section=section, + pax=pax, + pax_label=pax_labels[pax], + rear_mounted_bike_carrier=rear_mounted_bike_carrier, ) diff --git a/templates/all_routes.html b/templates/all_routes.html index bdee066..d5146e3 100644 --- a/templates/all_routes.html +++ b/templates/all_routes.html @@ -36,11 +36,27 @@ a:link {

{{ other }}

#} - +

+ Passengers: + {% for o in pax_options %} + {% if o.pax == pax %} + {{ o.label }} + {% else %} + {{ o.label }} + {% endif %} + {% if not loop.last %}|{% endif %} + {% endfor %} +

+ + {% if rear_mounted_bike_carrier %} +

Bike carrier: yes | no

+ {% else %} +

Bike carrier: + yes + | + no +

+ {% endif %} {% if extra_routes %}