Add full type annotations and black formatting across all modules

Annotated all functions with mypy --strict-compatible types (-> None, dict[str,
Any], Generator types, etc.), added # type: ignore for untyped third-party libs
(lxml), and reformatted with black. All 18 source files now pass mypy --strict
with zero errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-25 21:48:53 +01:00
parent 453d6244ec
commit 13c4341f3a
14 changed files with 1802 additions and 974 deletions

View file

@ -3,6 +3,7 @@ Combine GWR station→Paddington trains with Eurostar St Pancras→destination t
"""
from datetime import datetime, timedelta
from typing import Any
import circle_line
from tfl_fare import circle_line_fare
@ -15,14 +16,16 @@ DATE_FMT = "%Y-%m-%d"
TIME_FMT = "%H:%M"
PAD_WALK_TO_UNDERGROUND_MINUTES = 8 # GWR platform → Paddington (H&C Line) platform
KX_WALK_TO_UNDERGROUND_MINUTES = 10 # St Pancras arrivals → King's Cross St Pancras Underground
KX_WALK_TO_UNDERGROUND_MINUTES = (
10 # St Pancras arrivals → King's Cross St Pancras Underground
)
def _parse_dt(date: str, time: str) -> datetime:
return datetime.strptime(f"{date} {time}", f"{DATE_FMT} {TIME_FMT}")
def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
def _circle_line_services(arrive_paddington: datetime) -> list[dict[str, Any]]:
"""
Given GWR arrival at Paddington, return up to 2 upcoming Circle line services
as [{'depart': 'HH:MM', 'arrive_kx': 'HH:MM'}, ...].
@ -33,7 +36,9 @@ def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
earliest_board = arrive_paddington + timedelta(
minutes=PAD_WALK_TO_UNDERGROUND_MINUTES
)
services = circle_line.upcoming_services(earliest_board, count=2, direction='pad_to_kx')
services = circle_line.upcoming_services(
earliest_board, count=2, direction="pad_to_kx"
)
return [
{
"depart": dep.strftime(TIME_FMT),
@ -44,24 +49,32 @@ def _circle_line_services(arrive_paddington: datetime) -> list[dict]:
]
PAD_WALK_FROM_UNDERGROUND_MINUTES = 5 # Circle line platform → GWR platform at Paddington
INBOUND_COMFORTABLE_MIN_CONN = 40 # threshold above which we apply the platform walk buffer
PAD_WALK_FROM_UNDERGROUND_MINUTES = (
5 # Circle line platform → GWR platform at Paddington
)
INBOUND_COMFORTABLE_MIN_CONN = (
40 # threshold above which we apply the platform walk buffer
)
def _circle_line_services_to_paddington(
arrive_st_pancras: datetime,
dep_paddington: datetime | None = None,
min_conn_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
) -> list[dict]:
) -> list[dict[str, Any]]:
earliest_board = arrive_st_pancras + timedelta(
minutes=KX_WALK_TO_UNDERGROUND_MINUTES
)
if min_conn_minutes >= INBOUND_COMFORTABLE_MIN_CONN and dep_paddington is not None:
cutoff = dep_paddington - timedelta(minutes=PAD_WALK_FROM_UNDERGROUND_MINUTES)
candidates = circle_line.upcoming_services(earliest_board, count=4, direction='kx_to_pad')
candidates = circle_line.upcoming_services(
earliest_board, count=4, direction="kx_to_pad"
)
services = [(dep, arr) for dep, arr in candidates if arr <= cutoff][:2]
else:
services = circle_line.upcoming_services(earliest_board, count=1, direction='kx_to_pad', preceding=1)
services = circle_line.upcoming_services(
earliest_board, count=1, direction="kx_to_pad", preceding=1
)
return [
{
"depart": dep.strftime(TIME_FMT),
@ -82,8 +95,8 @@ def _fmt_duration(minutes: int) -> str:
def _is_viable_connection(
gwr: dict,
eurostar: dict,
gwr: dict[str, Any],
eurostar: dict[str, Any],
travel_date: str,
min_connection_minutes: int,
max_connection_minutes: int,
@ -112,8 +125,8 @@ def _is_viable_connection(
def _is_viable_inbound_connection(
eurostar: dict,
gwr: dict,
eurostar: dict[str, Any],
gwr: dict[str, Any],
travel_date: str,
min_connection_minutes: int,
max_connection_minutes: int,
@ -143,13 +156,13 @@ def _is_viable_inbound_connection(
def combine_trips(
gwr_trains: list[dict],
eurostar_trains: list[dict],
gwr_trains: list[dict[str, Any]],
eurostar_trains: list[dict[str, Any]],
travel_date: str,
min_connection_minutes: int = MIN_CONNECTION_MINUTES,
max_connection_minutes: int = MAX_CONNECTION_MINUTES,
gwr_fares: dict | None = None,
) -> list[dict]:
gwr_fares: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""
Return a list of valid combined trips, sorted by Bristol departure time.
@ -217,13 +230,13 @@ def combine_trips(
def combine_inbound_trips(
eurostar_trains: list[dict],
gwr_trains: list[dict],
eurostar_trains: list[dict[str, Any]],
gwr_trains: list[dict[str, Any]],
travel_date: str,
min_connection_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
max_connection_minutes: int = INBOUND_MAX_CONNECTION_MINUTES,
gwr_fares: dict | None = None,
) -> list[dict]:
gwr_fares: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Return valid continent→UK combined trips."""
trips = []
@ -243,12 +256,16 @@ def combine_inbound_trips(
total_mins = int((arr_station - dep_dest).total_seconds() / 60) + 60
eurostar_mins = int((arr_stp - dep_dest).total_seconds() / 60) + 60
fare = (gwr_fares or {}).get(gwr["depart_paddington"])
circle_svcs = _circle_line_services_to_paddington(arr_stp, dep_pad, min_connection_minutes)
circle_svcs = _circle_line_services_to_paddington(
arr_stp, dep_pad, min_connection_minutes
)
trips.append(
{
"direction": "inbound",
"depart_destination": es["depart_destination"],
"check_in_by": (dep_dest - timedelta(minutes=30)).strftime(TIME_FMT),
"check_in_by": (dep_dest - timedelta(minutes=30)).strftime(
TIME_FMT
),
"arrive_st_pancras": es["arrive_st_pancras"],
"depart_paddington": gwr["depart_paddington"],
"arrive_uk_station": gwr["arrive_destination"],
@ -279,12 +296,12 @@ def combine_inbound_trips(
def find_unreachable_morning_eurostars(
gwr_trains: list[dict],
eurostar_trains: list[dict],
gwr_trains: list[dict[str, Any]],
eurostar_trains: list[dict[str, Any]],
travel_date: str,
min_connection_minutes: int = MIN_CONNECTION_MINUTES,
max_connection_minutes: int = MAX_CONNECTION_MINUTES,
) -> list[dict]:
) -> list[dict[str, Any]]:
unreachable = []
for es in eurostar_trains:
@ -311,12 +328,12 @@ def find_unreachable_morning_eurostars(
def find_unreachable_inbound_eurostars(
eurostar_trains: list[dict],
gwr_trains: list[dict],
eurostar_trains: list[dict[str, Any]],
gwr_trains: list[dict[str, Any]],
travel_date: str,
min_connection_minutes: int = INBOUND_MIN_CONNECTION_MINUTES,
max_connection_minutes: int = INBOUND_MAX_CONNECTION_MINUTES,
) -> list[dict]:
) -> list[dict[str, Any]]:
unreachable = []
for es in eurostar_trains: