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:
parent
453d6244ec
commit
13c4341f3a
14 changed files with 1802 additions and 974 deletions
|
|
@ -6,64 +6,80 @@ from trip_planner import (
|
|||
_fmt_duration,
|
||||
)
|
||||
|
||||
DATE = '2026-03-30'
|
||||
DATE = "2026-03-30"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _fmt_duration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_fmt_duration_hours_and_minutes():
|
||||
assert _fmt_duration(95) == '1h 35m'
|
||||
|
||||
def test_fmt_duration_exact_hours():
|
||||
assert _fmt_duration(120) == '2h'
|
||||
def test_fmt_duration_hours_and_minutes() -> None:
|
||||
assert _fmt_duration(95) == "1h 35m"
|
||||
|
||||
def test_fmt_duration_minutes_only():
|
||||
assert _fmt_duration(45) == '45m'
|
||||
|
||||
def test_fmt_duration_exact_hours() -> None:
|
||||
assert _fmt_duration(120) == "2h"
|
||||
|
||||
|
||||
def test_fmt_duration_minutes_only() -> None:
|
||||
assert _fmt_duration(45) == "45m"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# combine_trips — basic pairing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
GWR_FAST = {'depart_bristol': '07:00', 'arrive_paddington': '08:45'} # 1h 45m
|
||||
GWR_SLOW = {'depart_bristol': '07:00', 'arrive_paddington': '09:26'} # 2h 26m — connection too short for ES_PARIS
|
||||
GWR_FAST = {"depart_bristol": "07:00", "arrive_paddington": "08:45"} # 1h 45m
|
||||
GWR_SLOW = {
|
||||
"depart_bristol": "07:00",
|
||||
"arrive_paddington": "09:26",
|
||||
} # 2h 26m — connection too short for ES_PARIS
|
||||
|
||||
ES_PARIS = {'depart_st_pancras': '10:01', 'arrive_destination': '13:34', 'destination': 'Paris Gare du Nord'}
|
||||
ES_EARLY = {'depart_st_pancras': '09:00', 'arrive_destination': '12:00', 'destination': 'Paris Gare du Nord'}
|
||||
ES_PARIS = {
|
||||
"depart_st_pancras": "10:01",
|
||||
"arrive_destination": "13:34",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
ES_EARLY = {
|
||||
"depart_st_pancras": "09:00",
|
||||
"arrive_destination": "12:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
|
||||
|
||||
def test_valid_trip_is_returned():
|
||||
def test_valid_trip_is_returned() -> None:
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert len(trips) == 1
|
||||
t = trips[0]
|
||||
assert t['depart_bristol'] == '07:00'
|
||||
assert t['arrive_paddington'] == '08:45'
|
||||
assert t['depart_st_pancras'] == '10:01'
|
||||
assert t['arrive_destination'] == '13:34'
|
||||
assert t['destination'] == 'Paris Gare du Nord'
|
||||
assert t["depart_bristol"] == "07:00"
|
||||
assert t["arrive_paddington"] == "08:45"
|
||||
assert t["depart_st_pancras"] == "10:01"
|
||||
assert t["arrive_destination"] == "13:34"
|
||||
assert t["destination"] == "Paris Gare du Nord"
|
||||
|
||||
|
||||
def test_gwr_too_slow_excluded():
|
||||
def test_gwr_too_slow_excluded() -> None:
|
||||
# arrive 09:26, Eurostar 10:01 → 35 min connection < 50 min minimum
|
||||
trips = combine_trips([GWR_SLOW], [ES_PARIS], DATE)
|
||||
assert trips == []
|
||||
|
||||
|
||||
def test_eurostar_too_early_excluded():
|
||||
def test_eurostar_too_early_excluded() -> None:
|
||||
# Eurostar departs before min connection time has elapsed
|
||||
trips = combine_trips([GWR_FAST], [ES_EARLY], DATE)
|
||||
assert trips == []
|
||||
|
||||
|
||||
def test_no_trains_returns_empty():
|
||||
def test_no_trains_returns_empty() -> None:
|
||||
assert combine_trips([], [], DATE) == []
|
||||
|
||||
def test_no_gwr_returns_empty():
|
||||
|
||||
def test_no_gwr_returns_empty() -> None:
|
||||
assert combine_trips([], [ES_PARIS], DATE) == []
|
||||
|
||||
def test_no_eurostar_returns_empty():
|
||||
|
||||
def test_no_eurostar_returns_empty() -> None:
|
||||
assert combine_trips([GWR_FAST], [], DATE) == []
|
||||
|
||||
|
||||
|
|
@ -71,140 +87,211 @@ def test_no_eurostar_returns_empty():
|
|||
# Connection window constraints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_min_connection_enforced():
|
||||
|
||||
def test_min_connection_enforced() -> None:
|
||||
# Arrive Paddington 08:45, need 75 min → earliest St Pancras 10:00
|
||||
# ES at 09:59 should be excluded, 10:00 should be included
|
||||
es_too_close = {'depart_st_pancras': '09:59', 'arrive_destination': '13:00', 'destination': 'Paris Gare du Nord'}
|
||||
es_ok = {'depart_st_pancras': '10:00', 'arrive_destination': '13:00', 'destination': 'Paris Gare du Nord'}
|
||||
assert combine_trips([GWR_FAST], [es_too_close], DATE, min_connection_minutes=75) == []
|
||||
es_too_close = {
|
||||
"depart_st_pancras": "09:59",
|
||||
"arrive_destination": "13:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
es_ok = {
|
||||
"depart_st_pancras": "10:00",
|
||||
"arrive_destination": "13:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
assert (
|
||||
combine_trips([GWR_FAST], [es_too_close], DATE, min_connection_minutes=75) == []
|
||||
)
|
||||
trips = combine_trips([GWR_FAST], [es_ok], DATE, min_connection_minutes=75)
|
||||
assert len(trips) == 1
|
||||
|
||||
|
||||
def test_max_connection_enforced():
|
||||
def test_max_connection_enforced() -> None:
|
||||
# Arrive Paddington 08:45, max 140 min → latest St Pancras 11:05
|
||||
es_ok = {'depart_st_pancras': '11:05', 'arrive_destination': '14:00', 'destination': 'Paris Gare du Nord'}
|
||||
es_too_late = {'depart_st_pancras': '11:06', 'arrive_destination': '14:00', 'destination': 'Paris Gare du Nord'}
|
||||
es_ok = {
|
||||
"depart_st_pancras": "11:05",
|
||||
"arrive_destination": "14:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
es_too_late = {
|
||||
"depart_st_pancras": "11:06",
|
||||
"arrive_destination": "14:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
trips = combine_trips([GWR_FAST], [es_ok], DATE, max_connection_minutes=140)
|
||||
assert len(trips) == 1
|
||||
assert combine_trips([GWR_FAST], [es_too_late], DATE, max_connection_minutes=140) == []
|
||||
assert (
|
||||
combine_trips([GWR_FAST], [es_too_late], DATE, max_connection_minutes=140) == []
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Only earliest valid Eurostar per GWR departure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_only_earliest_eurostar_per_gwr():
|
||||
es1 = {'depart_st_pancras': '10:01', 'arrive_destination': '13:34', 'destination': 'Paris Gare du Nord'}
|
||||
es2 = {'depart_st_pancras': '11:01', 'arrive_destination': '14:34', 'destination': 'Paris Gare du Nord'}
|
||||
|
||||
def test_only_earliest_eurostar_per_gwr() -> None:
|
||||
es1 = {
|
||||
"depart_st_pancras": "10:01",
|
||||
"arrive_destination": "13:34",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
es2 = {
|
||||
"depart_st_pancras": "11:01",
|
||||
"arrive_destination": "14:34",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
trips = combine_trips([GWR_FAST], [es1, es2], DATE)
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['depart_st_pancras'] == '10:01'
|
||||
assert trips[0]["depart_st_pancras"] == "10:01"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multiple GWR trains → multiple trips
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_multiple_gwr_trains():
|
||||
gwr2 = {'depart_bristol': '08:00', 'arrive_paddington': '09:45'}
|
||||
es = {'depart_st_pancras': '11:01', 'arrive_destination': '14:34', 'destination': 'Paris Gare du Nord'}
|
||||
|
||||
def test_multiple_gwr_trains() -> None:
|
||||
gwr2 = {"depart_bristol": "08:00", "arrive_paddington": "09:45"}
|
||||
es = {
|
||||
"depart_st_pancras": "11:01",
|
||||
"arrive_destination": "14:34",
|
||||
"destination": "Paris Gare du Nord",
|
||||
}
|
||||
trips = combine_trips([GWR_FAST, gwr2], [es], DATE, max_connection_minutes=140)
|
||||
assert len(trips) == 2
|
||||
assert trips[0]['depart_bristol'] == '07:00'
|
||||
assert trips[1]['depart_bristol'] == '08:00'
|
||||
assert trips[0]["depart_bristol"] == "07:00"
|
||||
assert trips[1]["depart_bristol"] == "08:00"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Duration fields
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_gwr_duration_in_trip():
|
||||
|
||||
def test_gwr_duration_in_trip() -> None:
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert trips[0]['gwr_duration'] == '1h 45m'
|
||||
assert trips[0]["gwr_duration"] == "1h 45m"
|
||||
|
||||
|
||||
def test_total_duration_in_trip():
|
||||
def test_total_duration_in_trip() -> None:
|
||||
# depart 07:00, arrive 13:34 → 6h 34m
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert trips[0]['total_duration'] == '6h 34m'
|
||||
assert trips[0]["total_duration"] == "6h 34m"
|
||||
|
||||
|
||||
def test_connection_duration_in_trip():
|
||||
def test_connection_duration_in_trip() -> None:
|
||||
# arrive Paddington 08:45, depart St Pancras 10:01 → 1h 16m
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert trips[0]['connection_duration'] == '1h 16m'
|
||||
assert trips[0]["connection_duration"] == "1h 16m"
|
||||
|
||||
|
||||
def test_find_unreachable_eurostars_excludes_connectable_services():
|
||||
def test_find_unreachable_eurostars_excludes_connectable_services() -> None:
|
||||
# GWR arrives 08:45; default min=50/max=110 → viable window 09:35–10:35.
|
||||
# 09:30 too early, 10:15 connectable, 12:30 beyond max connection.
|
||||
gwr = [
|
||||
{'depart_bristol': '07:00', 'arrive_paddington': '08:45'},
|
||||
{"depart_bristol": "07:00", "arrive_paddington": "08:45"},
|
||||
]
|
||||
eurostar = [
|
||||
{'depart_st_pancras': '09:30', 'arrive_destination': '12:00', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9001'},
|
||||
{'depart_st_pancras': '10:15', 'arrive_destination': '13:40', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9002'},
|
||||
{'depart_st_pancras': '12:30', 'arrive_destination': '15:55', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9003'},
|
||||
{
|
||||
"depart_st_pancras": "09:30",
|
||||
"arrive_destination": "12:00",
|
||||
"destination": "Paris Gare du Nord",
|
||||
"train_number": "ES 9001",
|
||||
},
|
||||
{
|
||||
"depart_st_pancras": "10:15",
|
||||
"arrive_destination": "13:40",
|
||||
"destination": "Paris Gare du Nord",
|
||||
"train_number": "ES 9002",
|
||||
},
|
||||
{
|
||||
"depart_st_pancras": "12:30",
|
||||
"arrive_destination": "15:55",
|
||||
"destination": "Paris Gare du Nord",
|
||||
"train_number": "ES 9003",
|
||||
},
|
||||
]
|
||||
|
||||
unreachable = find_unreachable_morning_eurostars(gwr, eurostar, DATE)
|
||||
|
||||
assert [s['depart_st_pancras'] for s in unreachable] == ['09:30', '12:30']
|
||||
assert [s["depart_st_pancras"] for s in unreachable] == ["09:30", "12:30"]
|
||||
|
||||
|
||||
def test_combine_trips_includes_ticket_fields():
|
||||
def test_combine_trips_includes_ticket_fields() -> None:
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert len(trips) == 1
|
||||
t = trips[0]
|
||||
assert 'ticket_name' in t
|
||||
assert 'ticket_price' in t
|
||||
assert 'ticket_code' in t
|
||||
assert "ticket_name" in t
|
||||
assert "ticket_price" in t
|
||||
assert "ticket_code" in t
|
||||
|
||||
def test_combine_trips_uses_gwr_fares_when_provided():
|
||||
fares = {'07:00': {'ticket': 'Super Off-Peak Single', 'price': 49.30, 'code': 'SSS'}}
|
||||
|
||||
def test_combine_trips_uses_gwr_fares_when_provided() -> None:
|
||||
fares = {
|
||||
"07:00": {"ticket": "Super Off-Peak Single", "price": 49.30, "code": "SSS"}
|
||||
}
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares=fares)
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['ticket_price'] == 49.30
|
||||
assert trips[0]['ticket_code'] == 'SSS'
|
||||
assert trips[0]["ticket_price"] == 49.30
|
||||
assert trips[0]["ticket_code"] == "SSS"
|
||||
|
||||
def test_combine_trips_ticket_price_none_when_no_fares():
|
||||
|
||||
def test_combine_trips_ticket_price_none_when_no_fares() -> None:
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares={})
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['ticket_price'] is None
|
||||
assert trips[0]["ticket_price"] is None
|
||||
|
||||
|
||||
def test_find_unreachable_eurostars_returns_empty_when_all_connectable():
|
||||
def test_find_unreachable_eurostars_returns_empty_when_all_connectable() -> None:
|
||||
gwr = [
|
||||
{'depart_bristol': '07:00', 'arrive_paddington': '08:45'},
|
||||
{"depart_bristol": "07:00", "arrive_paddington": "08:45"},
|
||||
]
|
||||
eurostar = [
|
||||
{'depart_st_pancras': '10:15', 'arrive_destination': '13:40', 'destination': 'Paris Gare du Nord', 'train_number': 'ES 9002'},
|
||||
{
|
||||
"depart_st_pancras": "10:15",
|
||||
"arrive_destination": "13:40",
|
||||
"destination": "Paris Gare du Nord",
|
||||
"train_number": "ES 9002",
|
||||
},
|
||||
]
|
||||
|
||||
assert find_unreachable_morning_eurostars(gwr, eurostar, DATE) == []
|
||||
|
||||
|
||||
def test_combine_inbound_trips_pairs_eurostar_to_paddington_departure():
|
||||
eurostar = [{
|
||||
'depart_destination': '15:12',
|
||||
'arrive_st_pancras': '16:30',
|
||||
'destination': 'Paris Gare du Nord',
|
||||
'train_number': 'ES 9035',
|
||||
}]
|
||||
gwr = [{
|
||||
'depart_paddington': '17:15',
|
||||
'arrive_destination': '18:55',
|
||||
'headcode': '1B99',
|
||||
}]
|
||||
fares = {'17:15': {'ticket': 'Off-Peak Single', 'price': 63.60, 'code': 'SVS'}}
|
||||
def test_combine_inbound_trips_pairs_eurostar_to_paddington_departure() -> None:
|
||||
eurostar = [
|
||||
{
|
||||
"depart_destination": "15:12",
|
||||
"arrive_st_pancras": "16:30",
|
||||
"destination": "Paris Gare du Nord",
|
||||
"train_number": "ES 9035",
|
||||
}
|
||||
]
|
||||
gwr = [
|
||||
{
|
||||
"depart_paddington": "17:15",
|
||||
"arrive_destination": "18:55",
|
||||
"headcode": "1B99",
|
||||
}
|
||||
]
|
||||
fares = {"17:15": {"ticket": "Off-Peak Single", "price": 63.60, "code": "SVS"}}
|
||||
|
||||
trips = combine_inbound_trips(eurostar, gwr, DATE, min_connection_minutes=30, max_connection_minutes=120, gwr_fares=fares)
|
||||
trips = combine_inbound_trips(
|
||||
eurostar,
|
||||
gwr,
|
||||
DATE,
|
||||
min_connection_minutes=30,
|
||||
max_connection_minutes=120,
|
||||
gwr_fares=fares,
|
||||
)
|
||||
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['depart_destination'] == '15:12'
|
||||
assert trips[0]['arrive_st_pancras'] == '16:30'
|
||||
assert trips[0]['depart_paddington'] == '17:15'
|
||||
assert trips[0]['arrive_uk_station'] == '18:55'
|
||||
assert trips[0]['ticket_price'] == 63.60
|
||||
assert trips[0]['check_in_by'] == '14:42'
|
||||
assert trips[0]["depart_destination"] == "15:12"
|
||||
assert trips[0]["arrive_st_pancras"] == "16:30"
|
||||
assert trips[0]["depart_paddington"] == "17:15"
|
||||
assert trips[0]["arrive_uk_station"] == "18:55"
|
||||
assert trips[0]["ticket_price"] == 63.60
|
||||
assert trips[0]["check_in_by"] == "14:42"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue