Fix HTML entities in GWR advance ticket dates

This commit is contained in:
Edward Betts 2026-09-25 15:56:16 +01:00
parent e75acc3364
commit 69992bebb7
2 changed files with 25 additions and 5 deletions

View file

@ -3,6 +3,7 @@
import os import os
import re import re
from datetime import date, datetime from datetime import date, datetime
from html import unescape
from time import time from time import time
import httpx import httpx
@ -12,6 +13,7 @@ url = "https://www.gwr.com/your-tickets/choosing-your-ticket/advance-tickets"
def parse_date_string(date_str: str) -> date: def parse_date_string(date_str: str) -> date:
"""Parse date string from HTML.""" """Parse date string from HTML."""
date_str = " ".join(unescape(date_str).split())
if not date_str[-1].isdigit(): # If the year is missing, use the current year if not date_str[-1].isdigit(): # If the year is missing, use the current year
date_str += f" {date.today().year}" date_str += f" {date.today().year}"

View file

@ -2,8 +2,8 @@
import os import os
import tempfile import tempfile
import typing
from datetime import date from datetime import date
from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@ -20,6 +20,16 @@ from agenda.gwr import (
class TestParseDateString: class TestParseDateString:
"""Tests for parse_date_string function.""" """Tests for parse_date_string function."""
@pytest.mark.parametrize("space", [" ", " ", " ", "\u00a0"])
@pytest.mark.parametrize("include_year", [True, False])
def test_parse_html_whitespace(self, space: str, include_year: bool) -> None:
"""Decode HTML spaces before parsing dates or inferring the year."""
date_str = f"Thursday 25{space}March"
if include_year:
date_str += " 2027"
result = parse_date_string(f" {space}{date_str}{space} ")
assert result == date(2027 if include_year else date.today().year, 3, 25)
def test_parse_date_with_year(self) -> None: def test_parse_date_with_year(self) -> None:
"""Test parsing date string with year included.""" """Test parsing date string with year included."""
result = parse_date_string("Monday 25 December 2023") result = parse_date_string("Monday 25 December 2023")
@ -152,7 +162,7 @@ class TestAdvanceTicketsPageHtml:
@pytest.mark.asyncio @pytest.mark.asyncio
@patch("httpx.AsyncClient") @patch("httpx.AsyncClient")
async def test_fetch_from_web(self, mock_client: Any) -> None: async def test_fetch_from_web(self, mock_client: typing.Any) -> None:
"""Test fetching from web when cache is stale.""" """Test fetching from web when cache is stale."""
mock_response = AsyncMock() mock_response = AsyncMock()
mock_response.text = "<html>fresh content</html>" mock_response.text = "<html>fresh content</html>"
@ -176,7 +186,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio @pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html") @patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_success(self, mock_html: Any) -> None: async def test_advance_ticket_date_success(self, mock_html: typing.Any) -> None:
"""Test successfully extracting advance ticket date.""" """Test successfully extracting advance ticket date."""
mock_html.return_value = """ mock_html.return_value = """
<tr><td>Weekdays</td><td>Monday 25 December 2023</td></tr> <tr><td>Weekdays</td><td>Monday 25 December 2023</td></tr>
@ -188,7 +198,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio @pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html") @patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_no_match(self, mock_html: Any) -> None: async def test_advance_ticket_date_no_match(self, mock_html: typing.Any) -> None:
"""Test when no weekday date can be extracted.""" """Test when no weekday date can be extracted."""
mock_html.return_value = "<p>No relevant data</p>" mock_html.return_value = "<p>No relevant data</p>"
@ -197,7 +207,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio @pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html") @patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_force_cache(self, mock_html: Any) -> None: async def test_advance_ticket_date_force_cache(self, mock_html: typing.Any) -> None:
"""Test advance_ticket_date with force_cache parameter.""" """Test advance_ticket_date with force_cache parameter."""
mock_html.return_value = """ mock_html.return_value = """
<tr><td>Weekdays</td><td>Tuesday 26 December 2023</td></tr> <tr><td>Weekdays</td><td>Tuesday 26 December 2023</td></tr>
@ -206,3 +216,11 @@ class TestAdvanceTicketDate:
result = await advance_ticket_date("/fake/dir", force_cache=True) result = await advance_ticket_date("/fake/dir", force_cache=True)
assert result == date(2023, 12, 26) assert result == date(2023, 12, 26)
mock_html.assert_called_once_with("/fake/dir", force_cache=True) mock_html.assert_called_once_with("/fake/dir", force_cache=True)
@pytest.mark.parametrize("suffix", ["", "**"])
def test_extract_html_entity_date(suffix: str) -> None:
"""Both extraction paths accept the date that caused the update crash."""
html = f"<tr><td>Weekdays</td><td>Thursday 25&nbsp;March 2027{suffix}</td></tr>"
assert extract_dates(html) == {"Weekdays": date(2027, 3, 25)}
assert extract_weekday_date(html) == date(2027, 3, 25)