This commit is contained in:
Edward Betts 2023-04-16 19:56:26 +01:00
parent c26383c9c0
commit a5195cba1a
7 changed files with 263 additions and 71 deletions

View file

@ -7,6 +7,9 @@ import requests
from . import Vehicle
api_root_url = "https://www.brittany-ferries.co.uk/api/ferry/v1/"
ua = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
headers = {"User-Agent": ua}
class VehicleDict(TypedDict):
@ -36,14 +39,16 @@ def get_prices(
from_date: str,
to_date: str,
vehicle: Vehicle,
adults: int = 2,
small_dogs: int = 1,
) -> dict[str, Any]:
"""Call Brittany Ferries API to get details of crossings."""
url = api_root_url + "crossing/prices"
post_data = {
"bookingReference": None,
"pets": {"smallDogs": 1, "largeDogs": 0, "cats": 0},
"passengers": {"adults": 2, "children": 0, "infants": 0},
"pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0},
"passengers": {"adults": adults, "children": 0, "infants": 0},
"vehicle": vehicle_dict(vehicle),
"departurePort": departure_port,
"arrivalPort": arrival_port,
@ -53,7 +58,7 @@ def get_prices(
"toDate": f"{to_date}T23:59:59",
}
r = requests.post(url, json=post_data)
r = requests.post(url, json=post_data, headers=headers)
data: dict[str, Any] = r.json()
return data
@ -82,5 +87,7 @@ def get_accommodations(
"offerType": "NONE",
}
json_data: dict[str, Any] = requests.post(url, json=post_data).json()
json_data: dict[str, Any] = requests.post(
url, json=post_data, headers=headers
).json()
return json_data

271
main.py
View file

@ -6,22 +6,46 @@ import json
import os
import os.path
import re
import sys
import traceback
from datetime import date, datetime, timedelta
from typing import Any
import dateutil.parser
import flask
import pytz
import werkzeug.exceptions
from werkzeug.debug.tbtools import get_current_traceback
import werkzeug
from werkzeug.debug.tbtools import DebugTraceback
from werkzeug.wrappers import Response
import ferry
from ferry.api import get_accommodations, get_prices
from ferry.read_config import ferry_config, vehicle_from_config
# import werkzeug.exceptions
# from werkzeug.debug.tbtools import DebugTraceback
# from werkzeug.debug.tbtools import get_current_traceback
app = flask.Flask(__name__)
app.debug = False
app.debug = True
routes = {
"outbound": [
("PORTSMOUTH", "CAEN"),
("PORTSMOUTH", "CHERBOURG"),
("PORTSMOUTH", "ST MALO"),
("PORTSMOUTH", "LE HAVRE"),
("POOLE", "CHERBOURG"),
],
"return": [
("CAEN", "PORTSMOUTH"),
("CHERBOURG", "PORTSMOUTH"),
("ST MALO", "PORTSMOUTH"),
("LE HAVRE", "PORTSMOUTH"),
("CHERBOURG", "POOLE"),
],
}
def cache_location() -> str:
@ -60,14 +84,26 @@ def get_duration(depart: str, arrive: str, time_delta: int) -> str:
@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)
def exception_handler(e: Exception) -> tuple[str, int]:
"""Show error page."""
exec_type, exc_value, current_traceback = sys.exc_info()
assert exc_value
tb = DebugTraceback(exc_value)
summary = tb.render_traceback_html(include_title=False)
exc_lines = list(tb._te.format_exception_only())
frames = [f for f, lineno in traceback.walk_tb(current_traceback)]
last_frame = frames[-1]
last_frame_args = inspect.getargs(last_frame.f_code)
return (
flask.render_template(
"show_error.html",
tb=tb,
plaintext=tb.render_traceback_text(),
exception="".join(exc_lines),
exception_type=tb._te.exc_type.__name__,
summary=summary,
last_frame=last_frame,
last_frame_args=last_frame_args,
),
@ -75,6 +111,23 @@ def exception_handler(e):
)
# @app.errorhandler(werkzeug.exceptions.InternalServerError)
# def exception_handler(e):
# # tb = get_current_traceback()
# tb = DebugTraceback()
# 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 a date from a string in ISO format."""
return datetime.strptime(d, "%Y-%m-%d").date()
@ -93,6 +146,7 @@ def cabins_url(dep: str, arr: str, crossing: dict[str, Any], ticket_tier: str) -
return flask.url_for(
"cabins",
sailing_id=int(crossing["sailingId"]),
departure_port=ferry.ports[dep],
arrival_port=ferry.ports[arr],
departure_date=utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
@ -106,7 +160,7 @@ def get_days_until_start() -> int:
return (start - date.today()).days
PriceData = list[tuple[str, str, dict[str, Any]]]
PriceData = list[tuple[str, str, list[dict[str, Any]]]]
def check_cache_for_prices(params: str) -> PriceData | None:
@ -130,14 +184,16 @@ def check_cache_for_prices(params: str) -> PriceData | None:
def get_prices_with_cache(
name: str,
direction: str,
start: str,
end: str,
selection: list[tuple[str, str]],
refresh: bool = False,
adults: int = 2,
small_dogs: int = 1,
) -> PriceData:
"""Get price data using cache."""
params = f"{name}_{start}_{end}"
params = f"{direction}_{start}_{end}_{adults}_{small_dogs}"
if not refresh:
data = check_cache_for_prices(params)
if data:
@ -149,9 +205,15 @@ def get_prices_with_cache(
(
dep,
arr,
get_prices(ferry.ports[dep], ferry.ports[arr], start, end, vehicle)[
"crossings"
],
get_prices(
ferry.ports[dep],
ferry.ports[arr],
start,
end,
vehicle,
adults,
small_dogs,
)["crossings"],
)
for dep, arr in selection
]
@ -164,18 +226,21 @@ def get_prices_with_cache(
def build_outbound(section: str) -> str:
"""Show all routes on one page."""
selection = [
("PORTSMOUTH", "CAEN"),
("PORTSMOUTH", "CHERBOURG"),
("PORTSMOUTH", "ST MALO"),
# ("POOLE", "CHERBOURG"),
]
start = ferry_config.get(section, "from")
end = ferry_config.get(section, "to")
refresh = bool(flask.request.args.get("refresh"))
all_data = get_prices_with_cache(section, start, end, selection, refresh)
adults_str = flask.request.args.get("adults")
adults = int(adults_str) if adults_str else 2
small_dogs_str = flask.request.args.get("small_dogs")
small_dogs = int(small_dogs_str) if small_dogs_str else 2
direction = section[:-1] if section[-1].isdigit() else section
all_data = get_prices_with_cache(
direction, start, end, routes["outbound"], refresh, adults, small_dogs
)
return flask.render_template(
"all_routes.html",
@ -192,43 +257,23 @@ def build_outbound(section: str) -> str:
)
@app.route("/outbound1")
def outbound1_page() -> str:
return build_outbound("outbound1")
@app.route("/outbound2")
def outbound2_page() -> str:
return build_outbound("outbound2")
def format_pet_options(o: dict[str, bool]) -> list[str]:
ret = []
if o.get("petCabinAvailable"):
ret.append("pet cabins")
if o.get("smallKennelAvailable"):
ret.append("small kennel")
if o.get("stayInCarAvailable"):
ret.append("pets stay in car")
return ret
@app.route("/return")
def return_page() -> str:
def build_return(section: str) -> str:
"""Show all routes on one page."""
selection = [
("CAEN", "PORTSMOUTH"),
("CHERBOURG", "PORTSMOUTH"),
("ST MALO", "PORTSMOUTH"),
# ("CHERBOURG", "POOLE"),
]
start = ferry_config.get("return", "from")
end = ferry_config.get("return", "to")
start = ferry_config.get(section, "from")
end = ferry_config.get(section, "to")
refresh = bool(flask.request.args.get("refresh"))
all_data = get_prices_with_cache("return", start, end, selection, refresh)
direction = section[:-1] if section[-1].isdigit() else section
adults_str = flask.request.args.get("adults")
adults = int(adults_str) if adults_str else 2
small_dogs_str = flask.request.args.get("small_dogs")
small_dogs = int(small_dogs_str) if small_dogs_str else 2
all_data = get_prices_with_cache(
direction, start, end, routes["return"], refresh, adults, small_dogs
)
return flask.render_template(
"all_routes.html",
@ -245,6 +290,44 @@ def return_page() -> str:
)
@app.route("/outbound1")
def outbound1_page() -> str:
return build_outbound("outbound1")
@app.route("/outbound2")
def outbound2_page() -> str:
return build_outbound("outbound2")
@app.route("/outbound3")
def outbound3_page() -> str:
return build_outbound("outbound3")
@app.route("/return1")
def return1_page() -> str:
return build_return("return1")
@app.route("/return2")
def return2_page() -> str:
return build_return("return2")
def format_pet_options(o: dict[str, bool]) -> list[str]:
"""Read pet options from API and format for display."""
ret = []
if o.get("petCabinAvailable"):
ret.append("pet cabins")
if o.get("smallKennelAvailable"):
ret.append("small kennel")
if o.get("stayInCarAvailable"):
ret.append("pets stay in car")
return ret
def get_accommodations_with_cache(
dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False
) -> dict[str, list[dict[str, Any]]]:
@ -274,10 +357,77 @@ def get_accommodations_with_cache(
return data
@app.route("/cabins/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>")
def get_start_end(section: str) -> tuple[str, str]:
start = ferry_config.get(section, "from")
end = ferry_config.get(section, "to")
return start, end
def get_return_section(departure_date: str) -> tuple[str, str, str]:
section = "return1"
start, end = get_start_end(section)
dep = departure_date[:10]
if start <= dep <= end:
return section, start, end
section = "return2"
start, end = get_start_end(section)
if start <= dep <= end:
return section, start, end
def get_outbound_section(departure_date: str) -> tuple[str, str, str]:
section = "outbound1"
start, end = get_start_end(section)
dep = departure_date[:10]
if start <= dep <= end:
return section, start, end
section = "outbound2"
start, end = get_start_end(section)
if start <= dep <= end:
return section, start, end
section = "outbound3"
start, end = get_start_end(section)
if start <= dep <= end:
return section, start, end
def lookup_sailing_id(prices: PriceData, sailing_id: int) -> dict[str, Any] | None:
for dep, arr, price_list in prices:
for i in price_list:
for j in i["prices"]:
crossing: dict[str, Any] = j["crossingPrices"]
if int(crossing["sailingId"]) == sailing_id:
return crossing
return None
@app.route(
"/cabins/<int:sailing_id>/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>"
)
def cabins(
departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
sailing_id: int,
departure_port: str,
arrival_port: str,
departure_date: str,
ticket_tier: str,
) -> str:
"""Get details of cabins."""
direction = {"GB": "outbound", "FR": "return"}[departure_port[:2]]
if direction == "return":
section, start, end = get_return_section(departure_date[:10])
start, end = get_start_end(section)
time_delta = 60
else:
section, start, end = get_outbound_section(departure_date[:10])
time_delta = -60
prices = get_prices_with_cache(direction, start, end, routes[direction])
crossing = lookup_sailing_id(prices, sailing_id)
cabin_data = get_accommodations_with_cache(
departure_port, arrival_port, departure_date, ticket_tier
)
@ -299,6 +449,11 @@ def cabins(
ticket_tier=ticket_tier,
accommodations=accommodations,
pet_accommodations=cabin_data["petAccommodations"],
crossing=crossing,
get_duration=get_duration,
time_delta=time_delta,
format_pet_options=format_pet_options,
section=section,
)

View file

@ -30,12 +30,18 @@ a:link {
<body>
<div class="m-3">
<p>{{ days_until_start }} days / {{ (days_until_start / 7) | int }} weeks / {{ "{:.1f}".format(days_until_start / 30.5) }} months until start of Dodainville week: Friday 17 March 2022</p>
<p>{{ days_until_start }} days / {{ (days_until_start / 7) | int }} weeks / {{ "{:.1f}".format(days_until_start / 30.5) }} months until start of Dodainville week: Friday 5 May 2022</p>
{#
<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>
{% if extra_routes %}
<ul>
{% for dep, arr in extra_routes %}

View file

@ -34,13 +34,15 @@ 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>
<table class="table w-auto">
<tr>
<th>code</th>
<th>description</th>
<th>births</th>
<th>quantity<br/>available</th>
<th>price</th>
<th class="text-end">price</th>
</tr>
{% for a in accommodations if a.quantityAvailable > 0 %}
<tr>
@ -54,7 +56,7 @@ a:link {
{{ a.quantityAvailable }}
{% endif %}
</td>
<td>£{{ a.unitCost.amount }}</td>
<td class="text-end">£{{ a.unitCost.amount }}</td>
</tr>
{% endfor %}
</table>
@ -69,6 +71,19 @@ a:link {
{% endfor %}
</ul>
<h4>Sailing</h4>
<ul>
<li>sailing ID: {{ crossing.sailingId }}</li>
<li>depart: {{ crossing.departureDateTime.time }}</li>
<li>arrive: {{ crossing.arrivalDateTime.time }}</li>
<li>duration: {{ get_duration(crossing.departureDateTime.time, crossing.arrivalDateTime.time, time_delta) }}</li>
<li>ship: {{ crossing.shipName }}</li>
<li>economy price: £{{ crossing.economyPrice.amount }}</li>
<li>standard price: £{{ crossing.standardPrice.amount }}</li>
<li>flexi price: £{{ crossing.flexiPrice.amount }}</li>
</ul>
</div>
</body>
</html>

View file

@ -11,9 +11,10 @@
<div class="m-3">
<ul>
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 10 March</a>
<li><a href="{{ url_for("outbound2_page") }}">Outbound: 17 March</a>
<li><a href="{{ url_for("return_page") }}">Return: 24 March</a>
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 5 May</a>
<li><a href="{{ url_for("return1_page") }}">Return: 12 May</a>
<li><a href="{{ url_for("outbound3_page") }}">Outbound: 29 September</a>
<li><a href="{{ url_for("return2_page") }}">Return: 6 October</a>
</ul>
</div>

View file

@ -39,9 +39,13 @@
{{ crossing.shipName }}
</td>
<td class="text-nowrap">
{% if crossing.economyPrice %}
<a href="{{ cabins_url(dep, arr, crossing, "ECONOMY") }}">
£{{ crossing.economyPrice.amount }}
</a>
{% else %}
n/a
{% endif %}
</td>
<td class="text-nowrap">
<a href="{{ cabins_url(dep, arr, crossing, "STANDARD") }}">
@ -53,9 +57,11 @@
£{{ crossing.flexiPrice.amount }}
</a>
</td>
{% if crossing.petAvailabilities %}
<td>
{{ format_pet_options(crossing.petAvailabilities) | join(", ") }}
</td>
{% endif %}
<td class="text-nowrap">
{% if crossing.full %}full |{% endif %}
{% if crossing.isCabinSpaceFull %}no cabin space |{% endif %}

View file

@ -7,9 +7,9 @@
{% block content %}
<div class="p-2">
<h1>Software error: {{ tb.exception_type }}</h1>
<h1>Software error: {{ exception_type }}</h1>
<div>
<pre>{{ tb.exception }}</pre>
<pre>{{ exception }}</pre>
</div>
{% set body %}
@ -19,10 +19,12 @@ URL: {{ request.url }}
{% endset %}
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
{{ tb.render_summary(include_title=False) | safe }}
{{ summary | safe }}
{#
<p>Error in function "{{ last_frame.function_name }}": {{ last_frame_args | pprint }}</p>
<pre>{{ last_frame.locals | pprint }}</pre>
#}
</div>
{% endblock %}