Improvements
This commit is contained in:
parent
7599f655ad
commit
b6f0c88320
|
@ -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()}
|
||||
|
|
21
ferry/api.py
21
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
|
||||
|
|
96
main.py
96
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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue