python-openpublictransport 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/PKG-INFO +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/pyproject.toml +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/const.py +23 -2
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/parsers.py +25 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/__init__.py +66 -3
- python_openpublictransport-0.3.1/src/openpublictransport/providers/cta.py +400 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/db.py +69 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/digitransit.py +102 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/efa_base.py +8 -7
- python_openpublictransport-0.3.1/src/openpublictransport/providers/golemio.py +251 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/gvh.py +55 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/hafas_mgate_base.py +143 -10
- python_openpublictransport-0.3.1/src/openpublictransport/providers/hongkong.py +364 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/insa.py +29 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/irail.py +210 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/nahsh.py +29 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/nextbus_base.py +260 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/nvv.py +30 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/otp.py +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/otp_base.py +26 -4
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/otp_custom.py +2 -2
- python_openpublictransport-0.3.1/src/openpublictransport/providers/saarvv.py +29 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/sbb.py +22 -11
- python_openpublictransport-0.3.1/src/openpublictransport/providers/sl.py +219 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/tfl.py +256 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/tfnsw.py +205 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/transitous.py +8 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/trimet.py +22 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/ttc.py +41 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vao.py +34 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vbn_hafas.py +31 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vmt.py +29 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vmv.py +55 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vos.py +29 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vrr.py +2 -2
- python_openpublictransport-0.3.1/src/openpublictransport/providers/vsn.py +30 -0
- python_openpublictransport-0.3.1/src/openpublictransport/providers/wienerlinien.py +253 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/python_openpublictransport.egg-info/SOURCES.txt +38 -2
- python_openpublictransport-0.3.1/tests/test_cta.py +349 -0
- python_openpublictransport-0.3.1/tests/test_digitransit.py +73 -0
- python_openpublictransport-0.3.1/tests/test_german_regional_providers.py +204 -0
- python_openpublictransport-0.3.1/tests/test_golemio.py +128 -0
- python_openpublictransport-0.3.1/tests/test_hongkong.py +148 -0
- python_openpublictransport-0.3.1/tests/test_irail.py +131 -0
- python_openpublictransport-0.3.1/tests/test_issue_fixes_0_3_1.py +84 -0
- python_openpublictransport-0.3.1/tests/test_provider_expansion.py +154 -0
- python_openpublictransport-0.3.1/tests/test_sbb.py +54 -0
- python_openpublictransport-0.3.1/tests/test_sl.py +150 -0
- python_openpublictransport-0.3.1/tests/test_tfl.py +114 -0
- python_openpublictransport-0.3.1/tests/test_tfnsw.py +137 -0
- python_openpublictransport-0.3.1/tests/test_ttc.py +167 -0
- python_openpublictransport-0.3.1/tests/test_wienerlinien.py +160 -0
- python_openpublictransport-0.2.0/src/openpublictransport/providers/db.py +0 -39
- python_openpublictransport-0.2.0/src/openpublictransport/providers/gtfsde.py +0 -21
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/setup.cfg +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/__init__.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/data/uk_stations.json +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/exceptions.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/models.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/avv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/bart.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/beg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/bsvg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/bvg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/dart.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/ding.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/entur.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/fptf_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/hafas_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/hvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/hvv_gti.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/irishrail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/kvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/mobiliteit_lu.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/mvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/national_rail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/ns.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/nta.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/nvbw.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/nwl.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/oebb.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/rejseplanen.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/rmv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/rvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/tpg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/trafiklab.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/trias_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vagfr.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vbn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vgn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vrn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vvo.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/providers/vvs.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/openpublictransport/py.typed +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/python_openpublictransport.egg-info/requires.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_efa_mimetype.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_efa_platform.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_entur.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_hafas_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_hafas_mgate.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_http_errors.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_hvv_gti.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_national_rail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_national_rail_parse.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.1}/tests/test_national_rail_static.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-openpublictransport"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Python library for public transport APIs (EFA, OTP2, TRIAS, FPTF, HAFAS, GTFS-RT)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -28,7 +28,6 @@ PROVIDER_TRANSITOUS = "transitous"
|
|
|
28
28
|
PROVIDER_DB = "db"
|
|
29
29
|
PROVIDER_VBN_OTP = "vbn_otp"
|
|
30
30
|
PROVIDER_VBN_TRIAS = "vbn_trias"
|
|
31
|
-
PROVIDER_OPT = "openpublictransport"
|
|
32
31
|
PROVIDER_OTP_CUSTOM = "otp_custom"
|
|
33
32
|
PROVIDER_NATIONAL_RAIL = "national_rail"
|
|
34
33
|
PROVIDER_REJSEPLANEN = "rejseplanen"
|
|
@@ -39,9 +38,31 @@ PROVIDER_BART_US = "bart_us"
|
|
|
39
38
|
PROVIDER_DART_US = "dart_us"
|
|
40
39
|
PROVIDER_IRISHRAIL_IE = "irishrail_ie"
|
|
41
40
|
PROVIDER_TPG_CH = "tpg_ch"
|
|
41
|
+
PROVIDER_VMV = "vmv"
|
|
42
|
+
PROVIDER_GVH = "gvh"
|
|
43
|
+
PROVIDER_NAHSH = "nahsh"
|
|
44
|
+
PROVIDER_INSA = "insa"
|
|
45
|
+
PROVIDER_VMT = "vmt"
|
|
46
|
+
PROVIDER_SAARVV = "saarvv"
|
|
47
|
+
PROVIDER_VAO_AT = "vao_at"
|
|
48
|
+
PROVIDER_VSN = "vsn"
|
|
49
|
+
PROVIDER_VOS = "vos"
|
|
50
|
+
PROVIDER_NVV = "nvv"
|
|
51
|
+
PROVIDER_VBN_HAFAS = "vbn_hafas"
|
|
52
|
+
PROVIDER_TRIMET_US = "trimet_us"
|
|
53
|
+
PROVIDER_IRAIL_BE = "irail_be"
|
|
54
|
+
PROVIDER_SL_SE = "sl_se"
|
|
55
|
+
PROVIDER_TTC_CA = "ttc_ca"
|
|
56
|
+
PROVIDER_HONGKONG = "hongkong"
|
|
57
|
+
PROVIDER_WIENERLINIEN = "wienerlinien_at"
|
|
58
|
+
PROVIDER_TFL = "tfl_uk"
|
|
59
|
+
PROVIDER_TFNSW = "tfnsw_au"
|
|
60
|
+
PROVIDER_DIGITRANSIT = "digitransit_fi"
|
|
61
|
+
PROVIDER_GOLEMIO = "golemio_cz"
|
|
62
|
+
PROVIDER_CTA_US = "cta_us"
|
|
42
63
|
|
|
43
64
|
# API base URLs
|
|
44
|
-
API_BASE_URL_VRR = "https://openservice
|
|
65
|
+
API_BASE_URL_VRR = "https://openservice.vrr.de/static03/XML_DM_REQUEST"
|
|
45
66
|
API_BASE_URL_KVV = "https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST"
|
|
46
67
|
API_BASE_URL_HVV = "https://hvv.efa.de/efa/XML_DM_REQUEST"
|
|
47
68
|
API_BASE_URL_TRAFIKLAB = "https://realtime-api.trafiklab.se/v1"
|
|
@@ -10,6 +10,30 @@ from .models import UnifiedDeparture
|
|
|
10
10
|
|
|
11
11
|
_LOGGER = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
|
+
# Train categories that are shown in front of a bare train number ("ICE 620").
|
|
14
|
+
# Buses and trams are deliberately left out: "11" reads better than "Bus 11".
|
|
15
|
+
RAIL_CATEGORIES = frozenset({
|
|
16
|
+
"ICE", "IC", "EC", "ECE", "EN", "NJ", "RJ", "RJX", "FLX", "TGV", "THA", "EST", "WB",
|
|
17
|
+
"IR", "IRE", "D", "RE", "RB", "MEX", "S",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rail_line_label(number: str, name: str) -> str:
|
|
22
|
+
"""Prefix a bare train number with its category taken from the long name.
|
|
23
|
+
|
|
24
|
+
EFA reports long-distance trains as ``number="620"`` and
|
|
25
|
+
``name="ICE 620 InterCityExpress"``, so the board showed just "620".
|
|
26
|
+
Returns ``number`` unchanged unless it is purely numeric and ``name``
|
|
27
|
+
starts with a known rail category followed by that number.
|
|
28
|
+
"""
|
|
29
|
+
if not number.isdigit() or not name:
|
|
30
|
+
return number
|
|
31
|
+
parts = name.split()
|
|
32
|
+
if len(parts) >= 2 and parts[1] == number and parts[0].upper() in RAIL_CATEGORIES:
|
|
33
|
+
return f"{parts[0]} {number}"
|
|
34
|
+
return number
|
|
35
|
+
|
|
36
|
+
|
|
13
37
|
# Human-readable platform labels APIs prefix onto the bare track identifier:
|
|
14
38
|
# "Gleis 3", "Bstg. 12", "Steig B", "Platform 4", "Pl. 2", "Track 1".
|
|
15
39
|
_PLATFORM_LABEL_RE = re.compile(
|
|
@@ -102,7 +126,7 @@ def parse_departure_generic(
|
|
|
102
126
|
destination_obj = {}
|
|
103
127
|
destination = destination_obj.get("name", "Unknown")
|
|
104
128
|
|
|
105
|
-
line_number = str(transportation.get("number", ""))
|
|
129
|
+
line_number = rail_line_label(str(transportation.get("number", "")), str(transportation.get("name", "")))
|
|
106
130
|
description = str(transportation.get("description", ""))
|
|
107
131
|
agency = stop.get("agency")
|
|
108
132
|
|
|
@@ -18,7 +18,6 @@ from ..const import (
|
|
|
18
18
|
PROVIDER_NVBW,
|
|
19
19
|
PROVIDER_NWL,
|
|
20
20
|
PROVIDER_OEBB,
|
|
21
|
-
PROVIDER_OPT,
|
|
22
21
|
PROVIDER_OTP_CUSTOM,
|
|
23
22
|
PROVIDER_RMV,
|
|
24
23
|
PROVIDER_RVV,
|
|
@@ -42,6 +41,28 @@ from ..const import (
|
|
|
42
41
|
PROVIDER_DART_US,
|
|
43
42
|
PROVIDER_IRISHRAIL_IE,
|
|
44
43
|
PROVIDER_TPG_CH,
|
|
44
|
+
PROVIDER_VMV,
|
|
45
|
+
PROVIDER_GVH,
|
|
46
|
+
PROVIDER_NAHSH,
|
|
47
|
+
PROVIDER_INSA,
|
|
48
|
+
PROVIDER_VMT,
|
|
49
|
+
PROVIDER_SAARVV,
|
|
50
|
+
PROVIDER_VBN_HAFAS,
|
|
51
|
+
PROVIDER_NVV,
|
|
52
|
+
PROVIDER_VOS,
|
|
53
|
+
PROVIDER_VSN,
|
|
54
|
+
PROVIDER_VAO_AT,
|
|
55
|
+
PROVIDER_TRIMET_US,
|
|
56
|
+
PROVIDER_IRAIL_BE,
|
|
57
|
+
PROVIDER_SL_SE,
|
|
58
|
+
PROVIDER_TTC_CA,
|
|
59
|
+
PROVIDER_HONGKONG,
|
|
60
|
+
PROVIDER_WIENERLINIEN,
|
|
61
|
+
PROVIDER_TFL,
|
|
62
|
+
PROVIDER_TFNSW,
|
|
63
|
+
PROVIDER_DIGITRANSIT,
|
|
64
|
+
PROVIDER_GOLEMIO,
|
|
65
|
+
PROVIDER_CTA_US,
|
|
45
66
|
)
|
|
46
67
|
from .avv import AVVProvider
|
|
47
68
|
from .base import BaseProvider
|
|
@@ -50,7 +71,6 @@ from .bsvg import BSVGProvider
|
|
|
50
71
|
from .bvg import BVGProvider
|
|
51
72
|
from .db import DBProvider
|
|
52
73
|
from .ding import DINGProvider
|
|
53
|
-
from .gtfsde import OPTProvider
|
|
54
74
|
from .hvv import HVVProvider
|
|
55
75
|
from .hvv_gti import HVVGTIProvider
|
|
56
76
|
from .kvv import KVVProvider
|
|
@@ -81,6 +101,28 @@ from .bart import BARTProvider
|
|
|
81
101
|
from .dart import DARTProvider
|
|
82
102
|
from .irishrail import IrishRailProvider
|
|
83
103
|
from .tpg import TPGProvider
|
|
104
|
+
from .vmv import VMVProvider
|
|
105
|
+
from .gvh import GVHProvider
|
|
106
|
+
from .nahsh import NAHSHProvider
|
|
107
|
+
from .insa import INSAProvider
|
|
108
|
+
from .vmt import VMTProvider
|
|
109
|
+
from .saarvv import SaarVVProvider
|
|
110
|
+
from .vbn_hafas import VBNHafasProvider
|
|
111
|
+
from .nvv import NVVProvider
|
|
112
|
+
from .vos import VOSProvider
|
|
113
|
+
from .vsn import VSNProvider
|
|
114
|
+
from .vao import VAOProvider
|
|
115
|
+
from .trimet import TriMetProvider
|
|
116
|
+
from .irail import IRailProvider
|
|
117
|
+
from .sl import SLProvider
|
|
118
|
+
from .ttc import TTCProvider
|
|
119
|
+
from .hongkong import HongKongProvider
|
|
120
|
+
from .wienerlinien import WienerLinienProvider
|
|
121
|
+
from .tfl import TfLProvider
|
|
122
|
+
from .tfnsw import TfNSWProvider
|
|
123
|
+
from .digitransit import DigitransitProvider
|
|
124
|
+
from .golemio import GolemioProvider
|
|
125
|
+
from .cta import CTAProvider
|
|
84
126
|
|
|
85
127
|
_PROVIDER_REGISTRY: Dict[str, Type[BaseProvider]] = {}
|
|
86
128
|
|
|
@@ -152,7 +194,6 @@ register_provider(PROVIDER_TRANSITOUS, TransitousProvider)
|
|
|
152
194
|
register_provider(PROVIDER_DB, DBProvider)
|
|
153
195
|
register_provider(PROVIDER_VBN_OTP, VBNOTPProvider)
|
|
154
196
|
register_provider(PROVIDER_VBN_TRIAS, VBNTriasProvider)
|
|
155
|
-
register_provider(PROVIDER_OPT, OPTProvider)
|
|
156
197
|
register_provider(PROVIDER_OTP_CUSTOM, OTPCustomProvider)
|
|
157
198
|
register_provider(PROVIDER_NATIONAL_RAIL, NationalRailProvider)
|
|
158
199
|
register_provider(PROVIDER_REJSEPLANEN, RejseplanenProvider)
|
|
@@ -163,4 +204,26 @@ register_provider(PROVIDER_BART_US, BARTProvider)
|
|
|
163
204
|
register_provider(PROVIDER_DART_US, DARTProvider)
|
|
164
205
|
register_provider(PROVIDER_IRISHRAIL_IE, IrishRailProvider)
|
|
165
206
|
register_provider(PROVIDER_TPG_CH, TPGProvider)
|
|
207
|
+
register_provider(PROVIDER_VMV, VMVProvider)
|
|
208
|
+
register_provider(PROVIDER_GVH, GVHProvider)
|
|
209
|
+
register_provider(PROVIDER_NAHSH, NAHSHProvider)
|
|
210
|
+
register_provider(PROVIDER_INSA, INSAProvider)
|
|
211
|
+
register_provider(PROVIDER_VMT, VMTProvider)
|
|
212
|
+
register_provider(PROVIDER_SAARVV, SaarVVProvider)
|
|
213
|
+
register_provider(PROVIDER_VBN_HAFAS, VBNHafasProvider)
|
|
214
|
+
register_provider(PROVIDER_NVV, NVVProvider)
|
|
215
|
+
register_provider(PROVIDER_VOS, VOSProvider)
|
|
216
|
+
register_provider(PROVIDER_VSN, VSNProvider)
|
|
217
|
+
register_provider(PROVIDER_VAO_AT, VAOProvider)
|
|
218
|
+
register_provider(PROVIDER_TRIMET_US, TriMetProvider)
|
|
219
|
+
register_provider(PROVIDER_IRAIL_BE, IRailProvider)
|
|
220
|
+
register_provider(PROVIDER_SL_SE, SLProvider)
|
|
221
|
+
register_provider(PROVIDER_TTC_CA, TTCProvider)
|
|
222
|
+
register_provider(PROVIDER_HONGKONG, HongKongProvider)
|
|
223
|
+
register_provider(PROVIDER_WIENERLINIEN, WienerLinienProvider)
|
|
224
|
+
register_provider(PROVIDER_TFL, TfLProvider)
|
|
225
|
+
register_provider(PROVIDER_TFNSW, TfNSWProvider)
|
|
226
|
+
register_provider(PROVIDER_DIGITRANSIT, DigitransitProvider)
|
|
227
|
+
register_provider(PROVIDER_GOLEMIO, GolemioProvider)
|
|
228
|
+
register_provider(PROVIDER_CTA_US, CTAProvider)
|
|
166
229
|
register_provider(PROVIDER_HVV_GTI, HVVGTIProvider)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""CTA provider (Chicago Transit Authority) — real-time 'L' trains and buses.
|
|
2
|
+
|
|
3
|
+
Departures come from CTA's own real-time APIs, which use separate keys:
|
|
4
|
+
|
|
5
|
+
* Train Tracker Arrivals API (``api_key_secondary``):
|
|
6
|
+
https://www.transitchicago.com/developers/ttdocs/
|
|
7
|
+
* Bus Tracker API v3 (``api_key``):
|
|
8
|
+
https://www.transitchicago.com/developers/bustracker/
|
|
9
|
+
|
|
10
|
+
Stop search needs no key: it uses the Transitous geocoder (which carries
|
|
11
|
+
CTA's GTFS stop ids) and keeps only CTA stops. Per CTA's docs, stop ids
|
|
12
|
+
0–29999 are bus stops, 30000–39999 'L' platforms and 40000–49999 'L'
|
|
13
|
+
parent stations. Ids are encoded as ``train:<id>`` or ``bus:<id>``.
|
|
14
|
+
|
|
15
|
+
Both CTA APIs report errors (including an invalid key) with HTTP 200 and an
|
|
16
|
+
error object in the body, so the payload is inspected here.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
22
|
+
from urllib.parse import quote
|
|
23
|
+
from zoneinfo import ZoneInfo
|
|
24
|
+
|
|
25
|
+
from ..exceptions import ApiError, ApiResponseError, AuthenticationError
|
|
26
|
+
from ..models import UnifiedDeparture
|
|
27
|
+
from .base import BaseProvider
|
|
28
|
+
from .transitous import API_BASE as TRANSITOUS_API_BASE
|
|
29
|
+
from .transitous import USER_AGENT
|
|
30
|
+
from ..const import PROVIDER_CTA_US
|
|
31
|
+
|
|
32
|
+
_LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
PROVIDER_ID = PROVIDER_CTA_US
|
|
35
|
+
PROVIDER_NAME = "CTA (Chicago)"
|
|
36
|
+
TIMEZONE = "America/Chicago"
|
|
37
|
+
|
|
38
|
+
TRAIN_API_URL = "https://lapi.transitchicago.com/api/1.0/ttarrivals.aspx"
|
|
39
|
+
BUS_API_URL = "https://www.ctabustracker.com/bustime/api/v3/getpredictions"
|
|
40
|
+
|
|
41
|
+
# Transitous feed prefix for CTA GTFS stop ids.
|
|
42
|
+
TRANSITOUS_PREFIX = "us-il-CTA_"
|
|
43
|
+
# Downtown Chicago, used to bias the geocoder towards CTA stops.
|
|
44
|
+
CHICAGO_PLACE = "41.8781,-87.6298"
|
|
45
|
+
|
|
46
|
+
KIND_TRAIN = "train"
|
|
47
|
+
KIND_BUS = "bus"
|
|
48
|
+
|
|
49
|
+
# Train Tracker route codes → (display name, background colour, text colour).
|
|
50
|
+
TRAIN_ROUTES: Dict[str, Tuple[str, str, str]] = {
|
|
51
|
+
"Red": ("Red Line", "#C60C30", "#FFFFFF"),
|
|
52
|
+
"Blue": ("Blue Line", "#00A1DE", "#FFFFFF"),
|
|
53
|
+
"Brn": ("Brown Line", "#62361B", "#FFFFFF"),
|
|
54
|
+
"G": ("Green Line", "#009B3A", "#FFFFFF"),
|
|
55
|
+
"Org": ("Orange Line", "#F9461C", "#FFFFFF"),
|
|
56
|
+
"P": ("Purple Line", "#522398", "#FFFFFF"),
|
|
57
|
+
"Pink": ("Pink Line", "#E27EA6", "#FFFFFF"),
|
|
58
|
+
"Y": ("Yellow Line", "#F9E300", "#000000"),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
TRANSPORT_TYPE_MAPPING = {KIND_TRAIN: "subway", KIND_BUS: "bus"}
|
|
62
|
+
|
|
63
|
+
# Train Tracker errCd values (docs: "Error Codes").
|
|
64
|
+
TRAIN_AUTH_ERRORS = {"101"}
|
|
65
|
+
TRAIN_QUOTA_ERRORS = {"102"}
|
|
66
|
+
|
|
67
|
+
# Bus Tracker dyn values (docs: "Dynamic Action Types").
|
|
68
|
+
BUS_DYN_CANCELLED = {1, 18}
|
|
69
|
+
BUS_DYN_EXPRESSED = {4}
|
|
70
|
+
BUS_DYN_HIDDEN = {16, 17} # invalidated trips: "should not be shown to the public"
|
|
71
|
+
|
|
72
|
+
# Bus Tracker error messages (docs: "Error codes"), matched case-insensitively.
|
|
73
|
+
BUS_AUTH_ERRORS = ("invalid api access key", "no api access key")
|
|
74
|
+
BUS_QUOTA_ERRORS = ("transaction limit",)
|
|
75
|
+
BUS_EMPTY_ERRORS = ("no arrival times", "no service scheduled", "no data found")
|
|
76
|
+
|
|
77
|
+
MAX_TRAIN_RESULTS = 50
|
|
78
|
+
MAX_BUS_RESULTS = 30
|
|
79
|
+
|
|
80
|
+
_SOURCE_TZ = ZoneInfo(TIMEZONE)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def stop_kind(stop_id: str) -> str:
|
|
84
|
+
"""Return ``train`` or ``bus`` for a bare CTA stop id, using CTA's id ranges."""
|
|
85
|
+
try:
|
|
86
|
+
number = int(stop_id)
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
return KIND_BUS
|
|
89
|
+
return KIND_TRAIN if 30000 <= number <= 49999 else KIND_BUS
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def encode_stop_id(stop_id: str) -> str:
|
|
93
|
+
"""Return the ``train:<id>`` / ``bus:<id>`` form used as station_id."""
|
|
94
|
+
return f"{stop_kind(stop_id)}:{stop_id}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def decode_stop_id(station_id: str) -> Tuple[str, str]:
|
|
98
|
+
"""Split a station_id into (kind, bare id). Bare and Transitous ids are accepted."""
|
|
99
|
+
value = str(station_id).strip()
|
|
100
|
+
if value.startswith(TRANSITOUS_PREFIX):
|
|
101
|
+
value = value[len(TRANSITOUS_PREFIX):]
|
|
102
|
+
kind, sep, rest = value.partition(":")
|
|
103
|
+
if sep and kind in (KIND_TRAIN, KIND_BUS) and rest:
|
|
104
|
+
return kind, rest
|
|
105
|
+
return stop_kind(value), value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _flag(value: Any) -> bool:
|
|
109
|
+
"""CTA encodes booleans as "0"/"1", "true"/"false" or real booleans."""
|
|
110
|
+
if isinstance(value, bool):
|
|
111
|
+
return value
|
|
112
|
+
return str(value).strip().lower() in ("1", "true")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _parse_train_time(value: Any) -> Optional[datetime]:
|
|
116
|
+
"""Parse arrT: the JSON feed uses ISO, the docs table "yyyyMMdd HH:mm:ss"."""
|
|
117
|
+
if not value or not isinstance(value, str):
|
|
118
|
+
return None
|
|
119
|
+
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y%m%d %H:%M:%S", "%Y%m%d %H:%M"):
|
|
120
|
+
try:
|
|
121
|
+
return datetime.strptime(value.strip(), fmt).replace(tzinfo=_SOURCE_TZ)
|
|
122
|
+
except ValueError:
|
|
123
|
+
continue
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_bus_time(value: Any) -> Optional[datetime]:
|
|
128
|
+
"""Parse prdtm ("yyyyMMdd HH:mm", optionally with seconds), Chicago local time."""
|
|
129
|
+
if not value or not isinstance(value, str):
|
|
130
|
+
return None
|
|
131
|
+
for fmt in ("%Y%m%d %H:%M", "%Y%m%d %H:%M:%S"):
|
|
132
|
+
try:
|
|
133
|
+
return datetime.strptime(value.strip(), fmt).replace(tzinfo=_SOURCE_TZ)
|
|
134
|
+
except ValueError:
|
|
135
|
+
continue
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class CTAProvider(BaseProvider):
|
|
140
|
+
"""Chicago Transit Authority: Train Tracker ('L') and Bus Tracker predictions.
|
|
141
|
+
|
|
142
|
+
``api_key`` is the Bus Tracker key, ``api_key_secondary`` the Train
|
|
143
|
+
Tracker key. Each is only needed for the matching stop type.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def provider_id(self) -> str:
|
|
148
|
+
return PROVIDER_ID
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def provider_name(self) -> str:
|
|
152
|
+
return PROVIDER_NAME
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def requires_api_key(self) -> bool:
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def get_timezone(self) -> str:
|
|
159
|
+
return TIMEZONE
|
|
160
|
+
|
|
161
|
+
def get_transport_type_mapping(self) -> Dict[Any, str]:
|
|
162
|
+
return dict(TRANSPORT_TYPE_MAPPING)
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------ search
|
|
165
|
+
|
|
166
|
+
async def _geocode(self, text: str) -> List[Dict[str, Any]]:
|
|
167
|
+
url = (
|
|
168
|
+
f"{TRANSITOUS_API_BASE}/v1/geocode?text={quote(text, safe='')}"
|
|
169
|
+
f"&type=STOP&place={CHICAGO_PLACE}&placeBias=5"
|
|
170
|
+
)
|
|
171
|
+
data = await self._request("get", url, headers={"User-Agent": USER_AGENT}, timeout=10)
|
|
172
|
+
if not isinstance(data, list):
|
|
173
|
+
raise ApiResponseError(f"{PROVIDER_NAME}: geocoder returned {type(data).__name__} instead of a list")
|
|
174
|
+
return [loc for loc in data if isinstance(loc, dict)]
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _cta_stops(locations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
178
|
+
results: List[Dict[str, Any]] = []
|
|
179
|
+
seen = set()
|
|
180
|
+
for loc in locations:
|
|
181
|
+
raw_id = str(loc.get("id") or "")
|
|
182
|
+
if not raw_id.startswith(TRANSITOUS_PREFIX):
|
|
183
|
+
continue
|
|
184
|
+
bare = raw_id[len(TRANSITOUS_PREFIX):]
|
|
185
|
+
if not bare or bare in seen:
|
|
186
|
+
continue
|
|
187
|
+
seen.add(bare)
|
|
188
|
+
kind = stop_kind(bare)
|
|
189
|
+
results.append(
|
|
190
|
+
{
|
|
191
|
+
"id": f"{kind}:{bare}",
|
|
192
|
+
"name": loc.get("name") or bare,
|
|
193
|
+
"place": "Chicago",
|
|
194
|
+
"area_type": "stop",
|
|
195
|
+
"transport_modes": [TRANSPORT_TYPE_MAPPING[kind]],
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
return results
|
|
199
|
+
|
|
200
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
201
|
+
term = (search_term or "").strip()
|
|
202
|
+
if not term:
|
|
203
|
+
return []
|
|
204
|
+
results = self._cta_stops(await self._geocode(term))
|
|
205
|
+
if not results and "chicago" not in term.lower():
|
|
206
|
+
# A short or ambiguous term can be crowded out by other cities.
|
|
207
|
+
results = self._cta_stops(await self._geocode(f"{term}, Chicago"))
|
|
208
|
+
return results
|
|
209
|
+
|
|
210
|
+
# -------------------------------------------------------------- departures
|
|
211
|
+
|
|
212
|
+
def _require_key(self, kind: str) -> str:
|
|
213
|
+
key = self.api_key_secondary if kind == KIND_TRAIN else self.api_key
|
|
214
|
+
if not key:
|
|
215
|
+
if kind == KIND_TRAIN:
|
|
216
|
+
message = f"{PROVIDER_NAME}: a Train Tracker key is required for 'L' stations"
|
|
217
|
+
else:
|
|
218
|
+
message = f"{PROVIDER_NAME}: a Bus Tracker key is required for bus stops"
|
|
219
|
+
raise AuthenticationError(PROVIDER_NAME, 401, message=message)
|
|
220
|
+
return key
|
|
221
|
+
|
|
222
|
+
async def fetch_departures(
|
|
223
|
+
self,
|
|
224
|
+
station_id: Optional[str],
|
|
225
|
+
place_dm: str,
|
|
226
|
+
name_dm: str,
|
|
227
|
+
departures_limit: int,
|
|
228
|
+
) -> Optional[Dict[str, Any]]:
|
|
229
|
+
if not station_id:
|
|
230
|
+
matches = await self.search_stops(name_dm or place_dm)
|
|
231
|
+
if not matches:
|
|
232
|
+
return {"stopEvents": []}
|
|
233
|
+
station_id = matches[0]["id"]
|
|
234
|
+
|
|
235
|
+
kind, stop_id = decode_stop_id(station_id)
|
|
236
|
+
key = self._require_key(kind)
|
|
237
|
+
if kind == KIND_TRAIN:
|
|
238
|
+
events = await self._fetch_train(key, stop_id, departures_limit)
|
|
239
|
+
else:
|
|
240
|
+
events = await self._fetch_bus(key, stop_id, departures_limit)
|
|
241
|
+
return {"stopEvents": events[: max(0, departures_limit)]}
|
|
242
|
+
|
|
243
|
+
async def _fetch_train(self, key: str, stop_id: str, limit: int) -> List[Dict[str, Any]]:
|
|
244
|
+
# Parent stations (4xxxx) are queried by mapid, platforms (3xxxx) by stpid.
|
|
245
|
+
id_param = "mapid" if stop_id.startswith("4") else "stpid"
|
|
246
|
+
params = {
|
|
247
|
+
"key": key,
|
|
248
|
+
id_param: stop_id,
|
|
249
|
+
"max": max(1, min(limit, MAX_TRAIN_RESULTS)),
|
|
250
|
+
"outputType": "JSON",
|
|
251
|
+
}
|
|
252
|
+
data = await self._request("get", TRAIN_API_URL, params=params, timeout=15)
|
|
253
|
+
root = data.get("ctatt") if isinstance(data, dict) else None
|
|
254
|
+
if not isinstance(root, dict):
|
|
255
|
+
raise ApiResponseError(f"{PROVIDER_NAME}: unexpected Train Tracker payload")
|
|
256
|
+
|
|
257
|
+
code = str(root.get("errCd") or "0").strip()
|
|
258
|
+
if code != "0":
|
|
259
|
+
message = f"{PROVIDER_NAME}: Train Tracker error {code} ({root.get('errNm') or 'unknown'})"
|
|
260
|
+
if code in TRAIN_AUTH_ERRORS:
|
|
261
|
+
raise AuthenticationError(PROVIDER_NAME, 401, message=message)
|
|
262
|
+
if code in TRAIN_QUOTA_ERRORS:
|
|
263
|
+
raise ApiError(PROVIDER_NAME, 429, message=message)
|
|
264
|
+
raise ApiResponseError(message)
|
|
265
|
+
|
|
266
|
+
etas = root.get("eta") or []
|
|
267
|
+
if isinstance(etas, dict):
|
|
268
|
+
etas = [etas]
|
|
269
|
+
return [{**eta, "_cta": KIND_TRAIN} for eta in etas if isinstance(eta, dict)]
|
|
270
|
+
|
|
271
|
+
async def _fetch_bus(self, key: str, stop_id: str, limit: int) -> List[Dict[str, Any]]:
|
|
272
|
+
params = {
|
|
273
|
+
"key": key,
|
|
274
|
+
"stpid": stop_id,
|
|
275
|
+
"format": "json",
|
|
276
|
+
"top": max(1, min(limit, MAX_BUS_RESULTS)),
|
|
277
|
+
}
|
|
278
|
+
data = await self._request("get", BUS_API_URL, params=params, timeout=15)
|
|
279
|
+
root = data.get("bustime-response") if isinstance(data, dict) else None
|
|
280
|
+
if not isinstance(root, dict):
|
|
281
|
+
raise ApiResponseError(f"{PROVIDER_NAME}: unexpected Bus Tracker payload")
|
|
282
|
+
|
|
283
|
+
predictions = root.get("prd") or []
|
|
284
|
+
if isinstance(predictions, dict):
|
|
285
|
+
predictions = [predictions]
|
|
286
|
+
errors = root.get("error") or []
|
|
287
|
+
if isinstance(errors, dict):
|
|
288
|
+
errors = [errors]
|
|
289
|
+
|
|
290
|
+
if errors and not predictions:
|
|
291
|
+
messages = [str(e.get("msg") or "") for e in errors if isinstance(e, dict)]
|
|
292
|
+
lowered = " | ".join(messages).lower()
|
|
293
|
+
message = f"{PROVIDER_NAME}: Bus Tracker error ({'; '.join(m for m in messages if m) or 'unknown'})"
|
|
294
|
+
if any(token in lowered for token in BUS_AUTH_ERRORS):
|
|
295
|
+
raise AuthenticationError(PROVIDER_NAME, 401, message=message)
|
|
296
|
+
if any(token in lowered for token in BUS_QUOTA_ERRORS):
|
|
297
|
+
raise ApiError(PROVIDER_NAME, 429, message=message)
|
|
298
|
+
if any(token in lowered for token in BUS_EMPTY_ERRORS):
|
|
299
|
+
return []
|
|
300
|
+
raise ApiResponseError(message)
|
|
301
|
+
|
|
302
|
+
events = []
|
|
303
|
+
for prd in predictions:
|
|
304
|
+
if not isinstance(prd, dict):
|
|
305
|
+
continue
|
|
306
|
+
try:
|
|
307
|
+
if int(prd.get("dyn") or 0) in BUS_DYN_HIDDEN:
|
|
308
|
+
continue
|
|
309
|
+
except (TypeError, ValueError):
|
|
310
|
+
pass
|
|
311
|
+
events.append({**prd, "_cta": KIND_BUS})
|
|
312
|
+
return events
|
|
313
|
+
|
|
314
|
+
# ----------------------------------------------------------------- parsing
|
|
315
|
+
|
|
316
|
+
def parse_departure(
|
|
317
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
318
|
+
) -> Optional[UnifiedDeparture]:
|
|
319
|
+
try:
|
|
320
|
+
kind = stop.get("_cta") or (KIND_TRAIN if "arrT" in stop else KIND_BUS)
|
|
321
|
+
if kind == KIND_TRAIN:
|
|
322
|
+
return self._parse_train(stop, tz, now)
|
|
323
|
+
return self._parse_bus(stop, tz, now)
|
|
324
|
+
except Exception as e: # noqa: BLE001
|
|
325
|
+
_LOGGER.debug("Error parsing CTA departure: %s", e)
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def _parse_train(self, eta: Dict[str, Any], tz: Any, now: datetime) -> Optional[UnifiedDeparture]:
|
|
329
|
+
arrival = _parse_train_time(eta.get("arrT"))
|
|
330
|
+
if arrival is None:
|
|
331
|
+
return None
|
|
332
|
+
dep_local = arrival.astimezone(tz)
|
|
333
|
+
|
|
334
|
+
route = str(eta.get("rt") or "")
|
|
335
|
+
line_name, color, text_color = TRAIN_ROUTES.get(route, (route, None, None))
|
|
336
|
+
|
|
337
|
+
notices: List[str] = []
|
|
338
|
+
if _flag(eta.get("isDly")):
|
|
339
|
+
notices.append("Delayed")
|
|
340
|
+
|
|
341
|
+
return UnifiedDeparture(
|
|
342
|
+
line=line_name,
|
|
343
|
+
destination=eta.get("destNm") or "Unknown",
|
|
344
|
+
departure_time=dep_local.strftime("%H:%M"),
|
|
345
|
+
planned_time=dep_local.strftime("%H:%M"),
|
|
346
|
+
delay=0,
|
|
347
|
+
platform=None,
|
|
348
|
+
transportation_type=TRANSPORT_TYPE_MAPPING[KIND_TRAIN],
|
|
349
|
+
is_realtime=not _flag(eta.get("isSch")),
|
|
350
|
+
minutes_until_departure=max(0, int((dep_local - now).total_seconds() / 60)),
|
|
351
|
+
departure_time_obj=dep_local,
|
|
352
|
+
description=eta.get("stpDe") or None,
|
|
353
|
+
agency="CTA",
|
|
354
|
+
notices=notices or None,
|
|
355
|
+
line_color=color,
|
|
356
|
+
line_text_color=text_color,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _parse_bus(self, prd: Dict[str, Any], tz: Any, now: datetime) -> Optional[UnifiedDeparture]:
|
|
360
|
+
predicted = _parse_bus_time(prd.get("prdtm"))
|
|
361
|
+
if predicted is None:
|
|
362
|
+
return None
|
|
363
|
+
dep_local = predicted.astimezone(tz)
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
dyn = int(prd.get("dyn") or 0)
|
|
367
|
+
except (TypeError, ValueError):
|
|
368
|
+
dyn = 0
|
|
369
|
+
|
|
370
|
+
notices: List[str] = []
|
|
371
|
+
if dyn in BUS_DYN_CANCELLED:
|
|
372
|
+
notices.append("Cancelled")
|
|
373
|
+
if _flag(prd.get("dly")) or str(prd.get("prdctdn") or "").upper() == "DLY":
|
|
374
|
+
notices.append("Delayed")
|
|
375
|
+
if dyn in BUS_DYN_EXPRESSED:
|
|
376
|
+
notices.append("Drop-off only")
|
|
377
|
+
|
|
378
|
+
countdown = str(prd.get("prdctdn") or "").strip()
|
|
379
|
+
if countdown.upper() == "DUE":
|
|
380
|
+
minutes_until = 0
|
|
381
|
+
elif countdown.isdigit():
|
|
382
|
+
minutes_until = int(countdown)
|
|
383
|
+
else:
|
|
384
|
+
minutes_until = max(0, int((dep_local - now).total_seconds() / 60))
|
|
385
|
+
|
|
386
|
+
return UnifiedDeparture(
|
|
387
|
+
line=str(prd.get("rtdd") or prd.get("rt") or ""),
|
|
388
|
+
destination=prd.get("des") or "Unknown",
|
|
389
|
+
departure_time=dep_local.strftime("%H:%M"),
|
|
390
|
+
planned_time=dep_local.strftime("%H:%M"),
|
|
391
|
+
delay=0,
|
|
392
|
+
platform=None,
|
|
393
|
+
transportation_type=TRANSPORT_TYPE_MAPPING[KIND_BUS],
|
|
394
|
+
is_realtime=True,
|
|
395
|
+
minutes_until_departure=minutes_until,
|
|
396
|
+
departure_time_obj=dep_local,
|
|
397
|
+
description=prd.get("rtdir") or None,
|
|
398
|
+
agency="CTA",
|
|
399
|
+
notices=notices or None,
|
|
400
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Deutsche Bahn provider implementation.
|
|
2
|
+
|
|
3
|
+
Served through Transitous (MOTIS 2, api.transitous.org), which carries the
|
|
4
|
+
nationwide DELFI timetable with DB real-time data. No API key required.
|
|
5
|
+
|
|
6
|
+
The provider used to talk to the community proxy v6.db.transport.rest, which
|
|
7
|
+
has been unavailable since autumn 2026. Entries created back then store a
|
|
8
|
+
numeric station number (IBNR) as stop id; those are resolved once by name to
|
|
9
|
+
the matching Transitous stop.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from ..const import PROVIDER_DB
|
|
16
|
+
from .transitous import TransitousProvider
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DBProvider(TransitousProvider):
|
|
22
|
+
"""Deutsche Bahn (Germany-wide) via Transitous."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self._legacy_ids: Dict[str, str] = {}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def provider_id(self) -> str:
|
|
30
|
+
return PROVIDER_DB
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def provider_name(self) -> str:
|
|
34
|
+
return "DB (Deutsche Bahn)"
|
|
35
|
+
|
|
36
|
+
def get_timezone(self) -> str:
|
|
37
|
+
return "Europe/Berlin"
|
|
38
|
+
|
|
39
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
40
|
+
"""Transitous searches worldwide; keep the German stops."""
|
|
41
|
+
stops = await super().search_stops(search_term)
|
|
42
|
+
german = [stop for stop in stops if stop.get("country") in (None, "", "DE")]
|
|
43
|
+
return german or stops
|
|
44
|
+
|
|
45
|
+
async def fetch_departures(
|
|
46
|
+
self,
|
|
47
|
+
station_id: Optional[str],
|
|
48
|
+
place_dm: str,
|
|
49
|
+
name_dm: str,
|
|
50
|
+
departures_limit: int,
|
|
51
|
+
) -> Optional[Dict[str, Any]]:
|
|
52
|
+
if station_id and station_id.isdigit():
|
|
53
|
+
station_id = await self._resolve_legacy_id(station_id, name_dm or place_dm)
|
|
54
|
+
return await super().fetch_departures(station_id, place_dm, name_dm, departures_limit)
|
|
55
|
+
|
|
56
|
+
async def _resolve_legacy_id(self, ibnr: str, name: str) -> Optional[str]:
|
|
57
|
+
"""Map a v6.db.transport.rest station number to a Transitous stop id."""
|
|
58
|
+
if ibnr in self._legacy_ids:
|
|
59
|
+
return self._legacy_ids[ibnr]
|
|
60
|
+
if not name:
|
|
61
|
+
_LOGGER.warning("DB: cannot resolve legacy station %s without a stop name", ibnr)
|
|
62
|
+
return None
|
|
63
|
+
stops = await self.search_stops(name)
|
|
64
|
+
if not stops:
|
|
65
|
+
_LOGGER.warning("DB: no Transitous stop found for legacy station %s (%s)", ibnr, name)
|
|
66
|
+
return None
|
|
67
|
+
self._legacy_ids[ibnr] = stops[0]["id"]
|
|
68
|
+
_LOGGER.debug("DB: legacy station %s (%s) resolved to %s", ibnr, name, stops[0]["id"])
|
|
69
|
+
return stops[0]["id"]
|