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
136
circle_line.py
136
circle_line.py
|
|
@ -3,16 +3,21 @@ Circle Line timetable between Paddington (H&C Line) and King's Cross St Pancras.
|
|||
|
||||
Parses the TransXChange XML file on first use and caches the result in memory.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
_PAD_STOP = '9400ZZLUPAH1' # Paddington (H&C Line)
|
||||
_KXP_STOP = '9400ZZLUKSX3' # King's Cross St Pancras
|
||||
_PAD_STOP = "9400ZZLUPAH1" # Paddington (H&C Line)
|
||||
_KXP_STOP = "9400ZZLUKSX3" # King's Cross St Pancras
|
||||
|
||||
from config.default import CIRCLE_LINE_XML as _TXC_XML # overridden by app config after import
|
||||
_NS = {'t': 'http://www.transxchange.org.uk/'}
|
||||
from config.default import (
|
||||
CIRCLE_LINE_XML as _TXC_XML,
|
||||
) # overridden by app config after import
|
||||
|
||||
_NS = {"t": "http://www.transxchange.org.uk/"}
|
||||
|
||||
# Populated on first call to next_service(); maps direction -> day-type -> sorted
|
||||
# list of (origin_depart_seconds, destination_arrive_seconds) measured from midnight.
|
||||
|
|
@ -22,8 +27,11 @@ _timetable: dict[str, dict[str, list[tuple[int, int]]]] | None = None
|
|||
def _parse_duration(s: str | None) -> int:
|
||||
if not s:
|
||||
return 0
|
||||
m = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?', s)
|
||||
return int(m.group(1) or 0) * 3600 + int(m.group(2) or 0) * 60 + int(m.group(3) or 0)
|
||||
m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", s)
|
||||
assert m is not None
|
||||
return (
|
||||
int(m.group(1) or 0) * 3600 + int(m.group(2) or 0) * 60 + int(m.group(3) or 0)
|
||||
)
|
||||
|
||||
|
||||
def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
||||
|
|
@ -31,23 +39,31 @@ def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
root = tree.getroot()
|
||||
|
||||
# Build JPS id -> [(from_stop, to_stop, runtime_secs, wait_secs)]
|
||||
jps_map: dict[str, list[tuple]] = {}
|
||||
for jps_el in root.find('t:JourneyPatternSections', _NS):
|
||||
jps_map: dict[str, list[tuple[str | None, str | None, int, int]]] = {}
|
||||
jps_sections = root.find("t:JourneyPatternSections", _NS)
|
||||
assert jps_sections is not None
|
||||
for jps_el in jps_sections:
|
||||
links = []
|
||||
for link in jps_el.findall('t:JourneyPatternTimingLink', _NS):
|
||||
fr = link.find('t:From/t:StopPointRef', _NS)
|
||||
to = link.find('t:To/t:StopPointRef', _NS)
|
||||
rt = link.find('t:RunTime', _NS)
|
||||
wait = link.find('t:From/t:WaitTime', _NS)
|
||||
links.append((
|
||||
fr.text if fr is not None else None,
|
||||
to.text if to is not None else None,
|
||||
_parse_duration(rt.text if rt is not None else None),
|
||||
_parse_duration(wait.text if wait is not None else None),
|
||||
))
|
||||
jps_map[jps_el.get('id')] = links
|
||||
for link in jps_el.findall("t:JourneyPatternTimingLink", _NS):
|
||||
fr = link.find("t:From/t:StopPointRef", _NS)
|
||||
to = link.find("t:To/t:StopPointRef", _NS)
|
||||
rt = link.find("t:RunTime", _NS)
|
||||
wait = link.find("t:From/t:WaitTime", _NS)
|
||||
links.append(
|
||||
(
|
||||
fr.text if fr is not None else None,
|
||||
to.text if to is not None else None,
|
||||
_parse_duration(rt.text if rt is not None else None),
|
||||
_parse_duration(wait.text if wait is not None else None),
|
||||
)
|
||||
)
|
||||
jps_id = jps_el.get("id")
|
||||
assert jps_id is not None
|
||||
jps_map[jps_id] = links
|
||||
|
||||
def _seconds_to_depart(links, stop):
|
||||
def _seconds_to_depart(
|
||||
links: list[tuple[str | None, str | None, int, int]], stop: str | None
|
||||
) -> int | None:
|
||||
"""Seconds from journey start until departure from *stop*."""
|
||||
elapsed = 0
|
||||
for fr, to, rt, wait in links:
|
||||
|
|
@ -57,7 +73,9 @@ def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
elapsed += rt
|
||||
return None
|
||||
|
||||
def _seconds_to_arrive(links, stop):
|
||||
def _seconds_to_arrive(
|
||||
links: list[tuple[str | None, str | None, int, int]], stop: str | None
|
||||
) -> int | None:
|
||||
"""Seconds from journey start until arrival at *stop*."""
|
||||
elapsed = 0
|
||||
for fr, to, rt, wait in links:
|
||||
|
|
@ -68,12 +86,14 @@ def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
|
||||
# Map JP id -> [(direction, origin_depart_offset_secs, destination_arrive_offset_secs)].
|
||||
jp_offsets: dict[str, list[tuple[str, int, int]]] = {}
|
||||
for svc in root.find('t:Services', _NS):
|
||||
for jp in svc.findall('.//t:JourneyPattern', _NS):
|
||||
jps_ref = jp.find('t:JourneyPatternSectionRefs', _NS)
|
||||
services_el = root.find("t:Services", _NS)
|
||||
assert services_el is not None
|
||||
for svc in services_el:
|
||||
for jp in svc.findall(".//t:JourneyPattern", _NS):
|
||||
jps_ref = jp.find("t:JourneyPatternSectionRefs", _NS)
|
||||
if jps_ref is None:
|
||||
continue
|
||||
links = jps_map.get(jps_ref.text, [])
|
||||
links = jps_map.get(jps_ref.text or "", [])
|
||||
stops = [l[0] for l in links] + ([links[-1][1]] if links else [])
|
||||
offsets = []
|
||||
if (
|
||||
|
|
@ -84,7 +104,7 @@ def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
pad_off = _seconds_to_depart(links, _PAD_STOP)
|
||||
kxp_off = _seconds_to_arrive(links, _KXP_STOP)
|
||||
if pad_off is not None and kxp_off is not None:
|
||||
offsets.append(('pad_to_kx', pad_off, kxp_off))
|
||||
offsets.append(("pad_to_kx", pad_off, kxp_off))
|
||||
if (
|
||||
_PAD_STOP in stops
|
||||
and _KXP_STOP in stops
|
||||
|
|
@ -93,42 +113,50 @@ def _load_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
kxp_off = _seconds_to_depart(links, _KXP_STOP)
|
||||
pad_off = _seconds_to_arrive(links, _PAD_STOP)
|
||||
if kxp_off is not None and pad_off is not None:
|
||||
offsets.append(('kx_to_pad', kxp_off, pad_off))
|
||||
offsets.append(("kx_to_pad", kxp_off, pad_off))
|
||||
if offsets:
|
||||
jp_offsets[jp.get('id')] = offsets
|
||||
jp_id = jp.get("id")
|
||||
assert jp_id is not None
|
||||
jp_offsets[jp_id] = offsets
|
||||
|
||||
result: dict[str, dict[str, list[tuple[int, int]]]] = {
|
||||
'pad_to_kx': {
|
||||
'MondayToFriday': [],
|
||||
'Saturday': [],
|
||||
'Sunday': [],
|
||||
"pad_to_kx": {
|
||||
"MondayToFriday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
},
|
||||
'kx_to_pad': {
|
||||
'MondayToFriday': [],
|
||||
'Saturday': [],
|
||||
'Sunday': [],
|
||||
"kx_to_pad": {
|
||||
"MondayToFriday": [],
|
||||
"Saturday": [],
|
||||
"Sunday": [],
|
||||
},
|
||||
}
|
||||
|
||||
for vj in root.find('t:VehicleJourneys', _NS):
|
||||
jp_ref = vj.find('t:JourneyPatternRef', _NS)
|
||||
dep_time = vj.find('t:DepartureTime', _NS)
|
||||
op = vj.find('t:OperatingProfile', _NS)
|
||||
vehicle_journeys = root.find("t:VehicleJourneys", _NS)
|
||||
assert vehicle_journeys is not None
|
||||
for vj in vehicle_journeys:
|
||||
jp_ref = vj.find("t:JourneyPatternRef", _NS)
|
||||
dep_time = vj.find("t:DepartureTime", _NS)
|
||||
op = vj.find("t:OperatingProfile", _NS)
|
||||
if jp_ref is None or dep_time is None or jp_ref.text not in jp_offsets:
|
||||
continue
|
||||
h, m, s = map(int, dep_time.text.split(':'))
|
||||
if dep_time.text is None:
|
||||
continue
|
||||
h, m, s = map(int, dep_time.text.split(":"))
|
||||
dep_secs = h * 3600 + m * 60 + s
|
||||
rdt = op.find('.//t:DaysOfWeek', _NS) if op is not None else None
|
||||
rdt = op.find(".//t:DaysOfWeek", _NS) if op is not None else None
|
||||
if rdt is None:
|
||||
continue
|
||||
for day_el in rdt:
|
||||
day_type = day_el.tag.split('}')[-1]
|
||||
day_type = day_el.tag.split("}")[-1]
|
||||
for direction, origin_off, dest_off in jp_offsets[jp_ref.text]:
|
||||
if day_type in result[direction]:
|
||||
result[direction][day_type].append((
|
||||
dep_secs + origin_off,
|
||||
dep_secs + dest_off,
|
||||
))
|
||||
result[direction][day_type].append(
|
||||
(
|
||||
dep_secs + origin_off,
|
||||
dep_secs + dest_off,
|
||||
)
|
||||
)
|
||||
|
||||
for direction in result:
|
||||
for key in result[direction]:
|
||||
|
|
@ -145,12 +173,12 @@ def _get_timetable() -> dict[str, dict[str, list[tuple[int, int]]]]:
|
|||
|
||||
def _day_type(weekday: int) -> str:
|
||||
if weekday < 5:
|
||||
return 'MondayToFriday'
|
||||
return 'Saturday' if weekday == 5 else 'Sunday'
|
||||
return "MondayToFriday"
|
||||
return "Saturday" if weekday == 5 else "Sunday"
|
||||
|
||||
|
||||
def next_service(
|
||||
earliest_board: datetime, direction: str = 'pad_to_kx'
|
||||
earliest_board: datetime, direction: str = "pad_to_kx"
|
||||
) -> tuple[datetime, datetime] | None:
|
||||
"""
|
||||
Given the earliest time a passenger can board at Paddington (H&C Line),
|
||||
|
|
@ -167,7 +195,7 @@ def next_service(
|
|||
def upcoming_services(
|
||||
earliest_board: datetime,
|
||||
count: int = 2,
|
||||
direction: str = 'pad_to_kx',
|
||||
direction: str = "pad_to_kx",
|
||||
preceding: int = 0,
|
||||
) -> list[tuple[datetime, datetime]]:
|
||||
"""
|
||||
|
|
@ -179,9 +207,7 @@ def upcoming_services(
|
|||
"""
|
||||
timetable = _get_timetable().get(direction, {})[_day_type(earliest_board.weekday())]
|
||||
board_secs = (
|
||||
earliest_board.hour * 3600
|
||||
+ earliest_board.minute * 60
|
||||
+ earliest_board.second
|
||||
earliest_board.hour * 3600 + earliest_board.minute * 60 + earliest_board.second
|
||||
)
|
||||
midnight = earliest_board.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
pre_results = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue