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",
"ST MALO": "FRSML",
"LE HAVRE": "FRLEH",
"ROSCOFF": "FRROS",
}
port_lookup = {code: name for name, code in ports.items()}

View file

@ -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

96
main.py
View file

@ -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,
)

View file

@ -36,11 +36,27 @@ a:link {
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
#}
<ul>
<li><a href="?adults=2&small_dogs=1">2 adults and a dog</a></li>
<li><a href="?adults=2&small_dogs=0">2 adults</a></li>
<li><a href="?adults=1&small_dogs=0">1 adult</a></li>
</ul>
<p>
Passengers:
{% for o in pax_options %}
{% if o.pax == pax %}
<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 %}
<ul>

View file

@ -34,7 +34,19 @@ a:link {
<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">
<tr>

View file

@ -12,7 +12,9 @@
<ul>
<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("return2_page") }}">Return: 13 October</a>
{#
<li><a href="{{ url_for("outbound3_page") }}">Outbound: 29 September</a>
<li><a href="{{ url_for("return2_page") }}">Return: </a>

View file

@ -44,7 +44,7 @@
</td>
<td class="text-nowrap">
{% 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 }}
</a>
{% else %}
@ -52,12 +52,12 @@
{% endif %}
</td>
<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 }}
</a>
</td>
<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 }}
</a>
</td>