From a5195cba1a76cf5906bf1565fa39a6a2af4e4cfb Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 16 Apr 2023 19:56:26 +0100 Subject: [PATCH] Update --- ferry/api.py | 15 +- main.py | 271 +++++++++++++++++++++++++------- templates/all_routes.html | 8 +- templates/cabins.html | 19 ++- templates/index.html | 7 +- templates/individual_route.html | 6 + templates/show_error.html | 8 +- 7 files changed, 263 insertions(+), 71 deletions(-) diff --git a/ferry/api.py b/ferry/api.py index 40221aa..7eaf2ca 100644 --- a/ferry/api.py +++ b/ferry/api.py @@ -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 diff --git a/main.py b/main.py index 571ef45..f602180 100755 --- a/main.py +++ b/main.py @@ -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////") +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/////" +) 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, ) diff --git a/templates/all_routes.html b/templates/all_routes.html index 667cdc5..48d5821 100644 --- a/templates/all_routes.html +++ b/templates/all_routes.html @@ -30,12 +30,18 @@ a:link {
-

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

+

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

{#

{{ other }}

#} + + {% if extra_routes %}
    {% for dep, arr in extra_routes %} diff --git a/templates/cabins.html b/templates/cabins.html index 67b3ce9..38bb78f 100644 --- a/templates/cabins.html +++ b/templates/cabins.html @@ -34,13 +34,15 @@ a:link {

    {{ departure_date.strftime("%A, %d %B %Y %H:%M UTC") }} {{ ticket_tier }}

    +

    back to sailings

    + - + {% for a in accommodations if a.quantityAvailable > 0 %} @@ -54,7 +56,7 @@ a:link { {{ a.quantityAvailable }} {% endif %} - + {% endfor %}
    code description births quantity
    available
    priceprice
    £{{ a.unitCost.amount }}£{{ a.unitCost.amount }}
    @@ -69,6 +71,19 @@ a:link { {% endfor %}
+

Sailing

+ +
    +
  • sailing ID: {{ crossing.sailingId }}
  • +
  • depart: {{ crossing.departureDateTime.time }}
  • +
  • arrive: {{ crossing.arrivalDateTime.time }}
  • +
  • duration: {{ get_duration(crossing.departureDateTime.time, crossing.arrivalDateTime.time, time_delta) }}
  • +
  • ship: {{ crossing.shipName }}
  • +
  • economy price: £{{ crossing.economyPrice.amount }}
  • +
  • standard price: £{{ crossing.standardPrice.amount }}
  • +
  • flexi price: £{{ crossing.flexiPrice.amount }}
  • +
+
diff --git a/templates/index.html b/templates/index.html index 93ab256..ed28d5c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -11,9 +11,10 @@ diff --git a/templates/individual_route.html b/templates/individual_route.html index 13248eb..8c7aec6 100644 --- a/templates/individual_route.html +++ b/templates/individual_route.html @@ -39,9 +39,13 @@ {{ crossing.shipName }} + {% if crossing.economyPrice %} £{{ crossing.economyPrice.amount }} + {% else %} + n/a + {% endif %} @@ -53,9 +57,11 @@ £{{ crossing.flexiPrice.amount }} + {% if crossing.petAvailabilities %} {{ format_pet_options(crossing.petAvailabilities) | join(", ") }} + {% endif %} {% if crossing.full %}full |{% endif %} {% if crossing.isCabinSpaceFull %}no cabin space |{% endif %} diff --git a/templates/show_error.html b/templates/show_error.html index 8d97299..5d0cf27 100644 --- a/templates/show_error.html +++ b/templates/show_error.html @@ -7,9 +7,9 @@ {% block content %}
-

Software error: {{ tb.exception_type }}

+

Software error: {{ exception_type }}

-
{{ tb.exception }}
+
{{ exception }}
{% set body %} @@ -19,10 +19,12 @@ URL: {{ request.url }} {% endset %}

Traceback (most recent call last)

-{{ tb.render_summary(include_title=False) | safe }} +{{ summary | safe }} +{#

Error in function "{{ last_frame.function_name }}": {{ last_frame_args | pprint }}

{{ last_frame.locals | pprint }}
+#}
{% endblock %}