Improvements

This commit is contained in:
Edward Betts 2023-09-11 07:00:20 +01:00
parent 7599f655ad
commit b6f0c88320
7 changed files with 139 additions and 27 deletions

View file

@ -19,6 +19,7 @@ ports = {
"CHERBOURG": "FRCER", "CHERBOURG": "FRCER",
"ST MALO": "FRSML", "ST MALO": "FRSML",
"LE HAVRE": "FRLEH", "LE HAVRE": "FRLEH",
"ROSCOFF": "FRROS",
} }
port_lookup = {code: name for name, code in ports.items()} port_lookup = {code: name for name, code in ports.items()}

View file

@ -3,6 +3,7 @@
from typing import Any, TypedDict from typing import Any, TypedDict
import requests import requests
import json
from . import Vehicle from . import Vehicle
@ -19,17 +20,18 @@ class VehicleDict(TypedDict):
registrations: list[str] registrations: list[str]
height: int height: int
length: 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.""" """Return vehicle detail in the format for the Brittany Ferries API."""
rmbc = True if rear_mounted_bike_carrier else None
return { return {
"type": v.type, "type": v.type,
"registrations": [v.registration], "registrations": [v.registration],
"height": v.height, "height": v.height,
"length": v.length, "length": v.length,
"extras": {"rearMountedBikeCarrier": None}, "extras": {"rearMountedBikeCarrier": rmbc},
} }
@ -41,6 +43,7 @@ def get_prices(
vehicle: Vehicle, vehicle: Vehicle,
adults: int = 2, adults: int = 2,
small_dogs: int = 1, small_dogs: int = 1,
rear_mounted_bike_carrier: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Call Brittany Ferries API to get details of crossings.""" """Call Brittany Ferries API to get details of crossings."""
url = api_root_url + "crossing/prices" url = api_root_url + "crossing/prices"
@ -49,7 +52,7 @@ def get_prices(
"bookingReference": None, "bookingReference": None,
"pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0}, "pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0},
"passengers": {"adults": adults, "children": 0, "infants": 0}, "passengers": {"adults": adults, "children": 0, "infants": 0},
"vehicle": vehicle_dict(vehicle), "vehicle": vehicle_dict(vehicle, rear_mounted_bike_carrier),
"departurePort": departure_port, "departurePort": departure_port,
"arrivalPort": arrival_port, "arrivalPort": arrival_port,
"disability": None, "disability": None,
@ -60,6 +63,8 @@ def get_prices(
r = requests.post(url, json=post_data, headers=headers) r = requests.post(url, json=post_data, headers=headers)
data: dict[str, Any] = r.json() data: dict[str, Any] = r.json()
if "crossings" not in data:
print(json.dumps(data, indent=2))
return data return data
@ -71,6 +76,7 @@ def get_accommodations(
vehicle: Vehicle, vehicle: Vehicle,
adults: int, adults: int,
small_dogs: int, small_dogs: int,
rear_mounted_bike_carrier: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Grab cabin details.""" """Grab cabin details."""
url = api_root_url + "crossing/accommodations" url = api_root_url + "crossing/accommodations"
@ -81,7 +87,7 @@ def get_accommodations(
"departureDate": departure_date, "departureDate": departure_date,
"passengers": {"adults": adults, "children": 0, "infants": 0}, "passengers": {"adults": adults, "children": 0, "infants": 0},
"disability": None, "disability": None,
"vehicle": vehicle_dict(vehicle), "vehicle": vehicle_dict(vehicle, rear_mounted_bike_carrier),
"petCabinsNeeded": True, "petCabinsNeeded": True,
"ticketTier": ticket_tier, "ticketTier": ticket_tier,
"pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0}, "pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0},
@ -92,4 +98,9 @@ def get_accommodations(
json_data: dict[str, Any] = requests.post( json_data: dict[str, Any] = requests.post(
url, json=post_data, headers=headers url, json=post_data, headers=headers
).json() ).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 return json_data

96
main.py
View file

@ -8,6 +8,7 @@ import os.path
import re import re
import sys import sys
import traceback import traceback
import collections
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Any from typing import Any
@ -37,6 +38,7 @@ routes = {
("PORTSMOUTH", "ST MALO"), ("PORTSMOUTH", "ST MALO"),
("PORTSMOUTH", "LE HAVRE"), ("PORTSMOUTH", "LE HAVRE"),
("POOLE", "CHERBOURG"), ("POOLE", "CHERBOURG"),
("PLYMOUTH", "ROSCOFF"),
], ],
"return": [ "return": [
("CAEN", "PORTSMOUTH"), ("CAEN", "PORTSMOUTH"),
@ -44,6 +46,7 @@ routes = {
("ST MALO", "PORTSMOUTH"), ("ST MALO", "PORTSMOUTH"),
("LE HAVRE", "PORTSMOUTH"), ("LE HAVRE", "PORTSMOUTH"),
("CHERBOURG", "POOLE"), ("CHERBOURG", "POOLE"),
("ROSCOFF", "PLYMOUTH"),
], ],
} }
@ -139,7 +142,13 @@ def start() -> Response | str:
return flask.render_template("index.html") 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.""" """Generate a URL for the cabins on a given crossing."""
dt = datetime.fromisoformat(crossing["departureDateTime"]["iso"]) dt = datetime.fromisoformat(crossing["departureDateTime"]["iso"])
utc_dt = dt.astimezone(pytz.utc) 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, ticket_tier=ticket_tier,
adults=adults_str, adults=adults_str,
small_dogs=small_dogs_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, adults: int,
small_dogs: int, small_dogs: int,
refresh: bool = False, refresh: bool = False,
rear_mounted_bike_carrier: bool = False,
) -> PriceData: ) -> PriceData:
"""Get price data using cache.""" """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: if not refresh:
data = check_cache_for_prices(params) data = check_cache_for_prices(params)
if data: if data:
@ -217,6 +230,7 @@ def get_prices_with_cache(
vehicle, vehicle,
adults, adults,
small_dogs, small_dogs,
rear_mounted_bike_carrier,
)["crossings"], )["crossings"],
) )
for dep, arr in selection for dep, arr in selection
@ -228,7 +242,10 @@ def get_prices_with_cache(
return all_data 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.""" """Get the number of adults and dogs that are travelling."""
config_adults = int(ferry_config.get("pax", "adults")) config_adults = int(ferry_config.get("pax", "adults"))
config_dogs = int(ferry_config.get("pax", "dogs")) 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_str = flask.request.args.get("small_dogs")
small_dogs = int(small_dogs_str) if small_dogs_str else config_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: def build_outbound(section: str) -> str:
@ -247,6 +271,9 @@ def build_outbound(section: str) -> str:
start = ferry_config.get(section, "from") start = ferry_config.get(section, "from")
end = ferry_config.get(section, "to") end = ferry_config.get(section, "to")
refresh = bool(flask.request.args.get("refresh")) refresh = bool(flask.request.args.get("refresh"))
rear_mounted_bike_carrier = (
flask.request.args.get("rear_mounted_bike_carrier") == "true"
)
pax = read_pax() pax = read_pax()
@ -257,9 +284,10 @@ def build_outbound(section: str) -> str:
start, start,
end, end,
routes["outbound"], routes["outbound"],
pax["adults"], pax.adults,
pax["small_dogs"], pax.small_dogs,
refresh, refresh,
rear_mounted_bike_carrier,
) )
return flask.render_template( return flask.render_template(
@ -274,6 +302,9 @@ def build_outbound(section: str) -> str:
get_duration=get_duration, get_duration=get_duration,
time_delta=-60, time_delta=-60,
format_pet_options=format_pet_options, 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") end = ferry_config.get(section, "to")
refresh = bool(flask.request.args.get("refresh")) refresh = bool(flask.request.args.get("refresh"))
pax = read_pax() 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 direction = section[:-1] if section[-1].isdigit() else section
@ -291,9 +325,10 @@ def build_return(section: str) -> str:
start, start,
end, end,
routes["return"], routes["return"],
pax["adults"], pax.adults,
pax["small_dogs"], pax.small_dogs,
refresh, refresh,
rear_mounted_bike_carrier,
) )
return flask.render_template( return flask.render_template(
@ -308,6 +343,9 @@ def build_return(section: str) -> str:
get_duration=get_duration, get_duration=get_duration,
time_delta=60, time_delta=60,
format_pet_options=format_pet_options, 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( 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]]]: ) -> dict[str, list[dict[str, Any]]]:
pax = read_pax() 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_files = os.listdir(cache_location())
existing = [f for f in existing_files if f.endswith(params + ".json")] existing = [f for f in existing_files if f.endswith(params + ".json")]
if not refresh and existing: if not refresh and existing:
@ -372,7 +415,14 @@ def get_accommodations_with_cache(
vehicle = vehicle_from_config(ferry_config) vehicle = vehicle_from_config(ferry_config)
filename = cache_filename(params) filename = cache_filename(params)
data = get_accommodations( 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: with open(filename, "w") as out:
@ -451,14 +501,29 @@ def cabins(
time_delta = -60 time_delta = -60
pax = read_pax() pax = read_pax()
rear_mounted_bike_carrier = (
flask.request.args.get("rear_mounted_bike_carrier") == "true"
)
prices = get_prices_with_cache( 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) crossing = lookup_sailing_id(prices, sailing_id)
cabin_data = get_accommodations_with_cache( 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 = [ accommodations = [
a a
@ -467,6 +532,8 @@ def cabins(
# and "Inside" not in a["description"] # and "Inside" not in a["description"]
] ]
pax_labels = {i["pax"]: i["label"] for i in pax_options}
dep = dateutil.parser.isoparse(departure_date) dep = dateutil.parser.isoparse(departure_date)
return flask.render_template( return flask.render_template(
@ -483,6 +550,9 @@ def cabins(
time_delta=time_delta, time_delta=time_delta,
format_pet_options=format_pet_options, format_pet_options=format_pet_options,
section=section, section=section,
pax=pax,
pax_label=pax_labels[pax],
rear_mounted_bike_carrier=rear_mounted_bike_carrier,
) )

View file

@ -36,11 +36,27 @@ a:link {
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p> <p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
#} #}
<ul> <p>
<li><a href="?adults=2&small_dogs=1">2 adults and a dog</a></li> Passengers:
<li><a href="?adults=2&small_dogs=0">2 adults</a></li> {% for o in pax_options %}
<li><a href="?adults=1&small_dogs=0">1 adult</a></li> {% if o.pax == pax %}
</ul> <strong>{{ o.label }}</strong>
{% else %}
<a href="?adults={{ o.pax.adults}}&small_dogs={{ o.pax.small_dogs }}">{{ o.label }}</a>
{% endif %}
{% if not loop.last %}|{% endif %}
{% endfor %}
</p>
{% if rear_mounted_bike_carrier %}
<p>Bike carrier: <strong>yes</strong> | <a href="{{ url_for(request.endpoint) }}">no</a></p>
{% else %}
<p>Bike carrier:
<a href="{{ url_for(request.endpoint, rear_mounted_bike_carrier='true') }}">yes</a>
|
<strong>no</strong>
</p>
{% endif %}
{% if extra_routes %} {% if extra_routes %}
<ul> <ul>

View file

@ -34,7 +34,19 @@ a:link {
<p>{{ departure_date.strftime("%A, %d %B %Y %H:%M UTC") }} {{ ticket_tier }}</p> <p>{{ departure_date.strftime("%A, %d %B %Y %H:%M UTC") }} {{ ticket_tier }}</p>
<p><a href="{{ url_for(section + "_page") }}">back to sailings</a></p> <p><a href="{{ url_for(section + "_page", rear_mounted_bike_carrier=request.args.get("rear_mounted_bike_carrier")) }}">back to sailings</a></p>
<p>Passengers: {{ pax_label }}</p>
{% if rear_mounted_bike_carrier %}
<p>Bike carrier: <strong>yes</strong> | <a href="{{ url_for(request.endpoint, rear_mounted_bike_carrier=None, **request.view_args) }}">no</a></p>
{% else %}
<p>Bike carrier:
<a href="{{ url_for(request.endpoint, rear_mounted_bike_carrier='true', **request.view_args) }}">yes</a>
|
<strong>no</strong>
</p>
{% endif %}
<table class="table w-auto"> <table class="table w-auto">
<tr> <tr>

View file

@ -12,7 +12,9 @@
<ul> <ul>
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 29 September</a> <li><a href="{{ url_for("outbound1_page") }}">Outbound: 29 September</a>
<li><a href="{{ url_for("outbound2_page") }}">Outbound 9 October</a>
<li><a href="{{ url_for("return1_page") }}">Return: 6 October</a> <li><a href="{{ url_for("return1_page") }}">Return: 6 October</a>
<li><a href="{{ url_for("return2_page") }}">Return: 13 October</a>
{# {#
<li><a href="{{ url_for("outbound3_page") }}">Outbound: 29 September</a> <li><a href="{{ url_for("outbound3_page") }}">Outbound: 29 September</a>
<li><a href="{{ url_for("return2_page") }}">Return: </a> <li><a href="{{ url_for("return2_page") }}">Return: </a>

View file

@ -44,7 +44,7 @@
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
{% if crossing.economyPrice %} {% if crossing.economyPrice %}
<a href="{{ cabins_url(dep, arr, crossing, "ECONOMY") }}"> <a href="{{ cabins_url(dep, arr, crossing, "ECONOMY", rear_mounted_bike_carrier) }}">
£{{ crossing.economyPrice.amount }} £{{ crossing.economyPrice.amount }}
</a> </a>
{% else %} {% else %}
@ -52,12 +52,12 @@
{% endif %} {% endif %}
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
<a href="{{ cabins_url(dep, arr, crossing, "STANDARD") }}"> <a href="{{ cabins_url(dep, arr, crossing, "STANDARD", rear_mounted_bike_carrier) }}">
£{{ crossing.standardPrice.amount }} £{{ crossing.standardPrice.amount }}
</a> </a>
</td> </td>
<td class="text-nowrap"> <td class="text-nowrap">
<a href="{{ cabins_url(dep, arr, crossing, "FLEXI") }}"> <a href="{{ cabins_url(dep, arr, crossing, "FLEXI", rear_mounted_bike_carrier) }}">
£{{ crossing.flexiPrice.amount }} £{{ crossing.flexiPrice.amount }}
</a> </a>
</td> </td>