diff --git a/chsdi/lib/opentransapi/opentransapi.py b/chsdi/lib/opentransapi/opentransapi.py index c18be6549d..ef81328ba1 100644 --- a/chsdi/lib/opentransapi/opentransapi.py +++ b/chsdi/lib/opentransapi/opentransapi.py @@ -4,29 +4,23 @@ import xml.etree.ElementTree as et from pytz import timezone from datetime import datetime -from dateutil import tz +from dateutil.parser import isoparse import re -def format_time(str_date_time, fmt="%Y-%m-%dT%H:%M:%SZ"): - from_zone = tz.tzutc() - to_zone = tz.gettz('Europe/Zurich') +def format_time(str_date_time): + # Though the documentation of the OJP 2.0 API is not too verbose on this point (see: + # https://opentransportdata.swiss/de/cookbook/ojpstopeventrequest/), it seems, the + # timestamps are always handeled in some form of ISO 8601 datetime format. + # Using isoparse() should be able to handle all the needed cases, e.g. + # - "Z" as timezone designator + # - timezone offsets, e.g. "+01:00" + # - sometimes the returned timestamps have an unexpected number of + # fractional seconds, e.g. 7 instead of 6, should be handled, too + date_time = isoparse(str_date_time) - try: - date_time = datetime.strptime(str_date_time, fmt) - except ValueError: - # sometimes the timestamp of the OJP 2.0 API's response has 7 digits for the - # milliseconds. 6 are expected and only 6 can be handled by Python. - # Hence we need to safely truncate everything between the last . and - # the +01:00 part of the timestamp, e.g.: - # 2024-11-01T15:39:45.5348804+01:00 - # Use regex to capture and truncate everything between the last '.' and - # the first '+' to 6 digits - truncated_date_time = re.sub(r'(\.\d{6})\d*(?=\+)', r'\1', str_date_time) - date_time = datetime.strptime(truncated_date_time, '%Y-%m-%dT%H:%M:%S.%f%z') - date_time_utc = date_time.replace(tzinfo=from_zone) - date_time_zurich = date_time_utc.astimezone(to_zone) - return date_time_zurich.strftime('%d/%m/%Y %H:%M') + # Return time in local time, as needed. + return date_time.strftime('%d/%m/%Y %H:%M') class OpenTrans: @@ -80,7 +74,7 @@ def xml_to_array(self, xml_data): results.append({ 'id': el_id, 'label': el_service_name, - 'currentDate': format_time(el_current_date, fmt="%Y-%m-%dT%H:%M:%S.%f%z"), + 'currentDate': format_time(el_current_date), 'departureDate': format_time(el_departure_date), 'estimatedDate': self._convert_estimated_date(el.find('.//ojp:ServiceDeparture/ojp:EstimatedTime', ns)), 'destinationName': el_destination_name, diff --git a/tests/integration/test_opentransapi.py b/tests/integration/test_opentransapi.py index e73d62f459..23479be70c 100644 --- a/tests/integration/test_opentransapi.py +++ b/tests/integration/test_opentransapi.py @@ -54,6 +54,12 @@ def test_stationboard(self, mock_requests): self.assertEqual(results[0]["destinationName"], "Hogwarts") self.assertEqual(results[0]["destinationId"], "ch:1:sloid:91178::3") + # assert, that several timestamp formats are correctly handled and transformed into the + # correct local time + self.assertEqual(format_time("2024-11-19T08:52:00Z"), "19/11/2024 08:52") + self.assertEqual(format_time("2024-11-19T08:52:00.1234567"), "19/11/2024 08:52") + self.assertEqual(format_time("2024-11-19T08:52:00+01:00"), "19/11/2024 08:52") + @requests_mock.Mocker() def test_stationboard_nonexisting_station(self, mock_requests): now = datetime.now(timezone('Europe/Zurich')).isoformat(timespec="microseconds")