python-openpublictransport 0.1.0__py3-none-any.whl
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.
- openpublictransport/__init__.py +5 -0
- openpublictransport/const.py +75 -0
- openpublictransport/models.py +95 -0
- openpublictransport/parsers.py +122 -0
- openpublictransport/providers/__init__.py +127 -0
- openpublictransport/providers/avv.py +44 -0
- openpublictransport/providers/base.py +77 -0
- openpublictransport/providers/beg.py +46 -0
- openpublictransport/providers/bsvg.py +44 -0
- openpublictransport/providers/bvg.py +32 -0
- openpublictransport/providers/db.py +39 -0
- openpublictransport/providers/ding.py +44 -0
- openpublictransport/providers/efa_base.py +209 -0
- openpublictransport/providers/fptf_base.py +169 -0
- openpublictransport/providers/gtfsde.py +21 -0
- openpublictransport/providers/hvv.py +40 -0
- openpublictransport/providers/kvv.py +38 -0
- openpublictransport/providers/mvv.py +48 -0
- openpublictransport/providers/nta.py +288 -0
- openpublictransport/providers/nvbw.py +44 -0
- openpublictransport/providers/nwl.py +46 -0
- openpublictransport/providers/oebb.py +182 -0
- openpublictransport/providers/otp.py +378 -0
- openpublictransport/providers/otp_base.py +268 -0
- openpublictransport/providers/otp_custom.py +22 -0
- openpublictransport/providers/rmv.py +272 -0
- openpublictransport/providers/rvv.py +43 -0
- openpublictransport/providers/sbb.py +174 -0
- openpublictransport/providers/trafiklab.py +280 -0
- openpublictransport/providers/transitous.py +198 -0
- openpublictransport/providers/trias_base.py +323 -0
- openpublictransport/providers/vagfr.py +44 -0
- openpublictransport/providers/vbn.py +73 -0
- openpublictransport/providers/vgn.py +46 -0
- openpublictransport/providers/vrn.py +51 -0
- openpublictransport/providers/vrr.py +51 -0
- openpublictransport/providers/vvo.py +45 -0
- openpublictransport/providers/vvs.py +48 -0
- python_openpublictransport-0.1.0.dist-info/METADATA +14 -0
- python_openpublictransport-0.1.0.dist-info/RECORD +42 -0
- python_openpublictransport-0.1.0.dist-info/WHEEL +5 -0
- python_openpublictransport-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Provider IDs, transport type mappings and API base URLs."""
|
|
2
|
+
|
|
3
|
+
# Provider IDs
|
|
4
|
+
PROVIDER_VRR = "vrr"
|
|
5
|
+
PROVIDER_KVV = "kvv"
|
|
6
|
+
PROVIDER_HVV = "hvv"
|
|
7
|
+
PROVIDER_BVG = "bvg"
|
|
8
|
+
PROVIDER_MVV = "mvv"
|
|
9
|
+
PROVIDER_VVS = "vvs"
|
|
10
|
+
PROVIDER_VGN = "vgn"
|
|
11
|
+
PROVIDER_VAGFR = "vagfr"
|
|
12
|
+
PROVIDER_RMV = "rmv"
|
|
13
|
+
PROVIDER_TRAFIKLAB_SE = "trafiklab_se"
|
|
14
|
+
PROVIDER_NTA_IE = "nta_ie"
|
|
15
|
+
PROVIDER_VRN = "vrn"
|
|
16
|
+
PROVIDER_VVO = "vvo"
|
|
17
|
+
PROVIDER_DING = "ding"
|
|
18
|
+
PROVIDER_AVV_AUGSBURG = "avv_augsburg"
|
|
19
|
+
PROVIDER_RVV = "rvv"
|
|
20
|
+
PROVIDER_BSVG = "bsvg"
|
|
21
|
+
PROVIDER_NWL = "nwl"
|
|
22
|
+
PROVIDER_NVBW = "nvbw"
|
|
23
|
+
PROVIDER_BEG = "beg"
|
|
24
|
+
PROVIDER_SBB = "sbb"
|
|
25
|
+
PROVIDER_OEBB = "oebb"
|
|
26
|
+
PROVIDER_TRANSITOUS = "transitous"
|
|
27
|
+
PROVIDER_DB = "db"
|
|
28
|
+
PROVIDER_VBN_OTP = "vbn_otp"
|
|
29
|
+
PROVIDER_VBN_TRIAS = "vbn_trias"
|
|
30
|
+
PROVIDER_OPT = "openpublictransport"
|
|
31
|
+
PROVIDER_OTP_CUSTOM = "otp_custom"
|
|
32
|
+
|
|
33
|
+
# API base URLs
|
|
34
|
+
API_BASE_URL_VRR = "https://openservice-test.vrr.de/static03/XML_DM_REQUEST"
|
|
35
|
+
API_BASE_URL_KVV = "https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST"
|
|
36
|
+
API_BASE_URL_HVV = "https://hvv.efa.de/efa/XML_DM_REQUEST"
|
|
37
|
+
API_BASE_URL_TRAFIKLAB = "https://realtime-api.trafiklab.se/v1"
|
|
38
|
+
API_BASE_URL_NTA_GTFSR = "https://api.nationaltransport.ie/gtfsr"
|
|
39
|
+
|
|
40
|
+
# Transport type mappings
|
|
41
|
+
KVV_TRANSPORTATION_TYPES = {
|
|
42
|
+
1: "train",
|
|
43
|
+
4: "tram",
|
|
44
|
+
5: "bus",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
HVV_TRANSPORTATION_TYPES = {
|
|
48
|
+
0: "train",
|
|
49
|
+
1: "train",
|
|
50
|
+
2: "subway",
|
|
51
|
+
3: "bus",
|
|
52
|
+
4: "tram",
|
|
53
|
+
5: "bus",
|
|
54
|
+
6: "ferry",
|
|
55
|
+
7: "on_demand",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
TRAFIKLAB_TRANSPORTATION_TYPES = {
|
|
59
|
+
"BUS": "bus",
|
|
60
|
+
"TRAIN": "train",
|
|
61
|
+
"TRAM": "tram",
|
|
62
|
+
"METRO": "subway",
|
|
63
|
+
"FERRY": "ferry",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
NTA_TRANSPORTATION_TYPES = {
|
|
67
|
+
0: "tram",
|
|
68
|
+
1: "subway",
|
|
69
|
+
2: "train",
|
|
70
|
+
3: "bus",
|
|
71
|
+
4: "ferry",
|
|
72
|
+
5: "tram",
|
|
73
|
+
6: "tram",
|
|
74
|
+
7: "train",
|
|
75
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Unified data models for all public transport providers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnifiedTransportType(str, Enum):
|
|
10
|
+
"""Unified transportation types across all providers."""
|
|
11
|
+
|
|
12
|
+
BUS = "bus"
|
|
13
|
+
TRAM = "tram"
|
|
14
|
+
SUBWAY = "subway"
|
|
15
|
+
TRAIN = "train"
|
|
16
|
+
FERRY = "ferry"
|
|
17
|
+
TAXI = "taxi"
|
|
18
|
+
ON_DEMAND = "on_demand"
|
|
19
|
+
UNKNOWN = "unknown"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class UnifiedDeparture:
|
|
24
|
+
"""Unified departure data structure."""
|
|
25
|
+
|
|
26
|
+
line: str
|
|
27
|
+
destination: str
|
|
28
|
+
departure_time: str # HH:MM format
|
|
29
|
+
planned_time: str # HH:MM format
|
|
30
|
+
delay: int # minutes
|
|
31
|
+
platform: Optional[str]
|
|
32
|
+
transportation_type: str
|
|
33
|
+
is_realtime: bool
|
|
34
|
+
minutes_until_departure: int
|
|
35
|
+
departure_time_obj: datetime # For internal sorting
|
|
36
|
+
description: Optional[str] = None
|
|
37
|
+
agency: Optional[str] = None
|
|
38
|
+
notices: Optional[list[str]] = None
|
|
39
|
+
planned_platform: Optional[str] = None
|
|
40
|
+
platform_changed: bool = False
|
|
41
|
+
line_color: Optional[str] = None
|
|
42
|
+
line_text_color: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
"""Convert to dictionary."""
|
|
46
|
+
result = {
|
|
47
|
+
"line": self.line,
|
|
48
|
+
"destination": self.destination,
|
|
49
|
+
"departure_time": self.departure_time,
|
|
50
|
+
"planned_time": self.planned_time,
|
|
51
|
+
"delay": self.delay,
|
|
52
|
+
"platform": self.platform,
|
|
53
|
+
"transportation_type": self.transportation_type,
|
|
54
|
+
"is_realtime": self.is_realtime,
|
|
55
|
+
"minutes_until_departure": self.minutes_until_departure,
|
|
56
|
+
}
|
|
57
|
+
if self.description:
|
|
58
|
+
result["description"] = self.description
|
|
59
|
+
if self.agency:
|
|
60
|
+
result["agency"] = self.agency
|
|
61
|
+
if self.notices:
|
|
62
|
+
result["notices"] = self.notices
|
|
63
|
+
if self.platform_changed:
|
|
64
|
+
result["planned_platform"] = self.planned_platform
|
|
65
|
+
result["platform_changed"] = True
|
|
66
|
+
if self.line_color:
|
|
67
|
+
result["line_color"] = self.line_color
|
|
68
|
+
if self.line_text_color:
|
|
69
|
+
result["line_text_color"] = self.line_text_color
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class UnifiedStop:
|
|
75
|
+
"""Unified stop data structure for stop search results."""
|
|
76
|
+
|
|
77
|
+
id: str
|
|
78
|
+
name: str
|
|
79
|
+
place: Optional[str] = None
|
|
80
|
+
area_type: Optional[str] = None
|
|
81
|
+
transport_modes: Optional[list[str]] = None
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict[str, Any]:
|
|
84
|
+
"""Convert to dictionary."""
|
|
85
|
+
result: dict[str, Any] = {
|
|
86
|
+
"id": self.id,
|
|
87
|
+
"name": self.name,
|
|
88
|
+
}
|
|
89
|
+
if self.place:
|
|
90
|
+
result["place"] = self.place
|
|
91
|
+
if self.area_type:
|
|
92
|
+
result["area_type"] = self.area_type
|
|
93
|
+
if self.transport_modes is not None:
|
|
94
|
+
result["transport_modes"] = self.transport_modes
|
|
95
|
+
return result
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Common parsing utilities for all providers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
from .models import UnifiedDeparture
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_dt(s: str) -> Optional[datetime]:
|
|
14
|
+
"""Parse an ISO datetime string, returning None on failure."""
|
|
15
|
+
try:
|
|
16
|
+
return datetime.fromisoformat(s)
|
|
17
|
+
except (ValueError, TypeError):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_departure_generic(
|
|
22
|
+
stop: Dict[str, Any],
|
|
23
|
+
tz: Union[ZoneInfo, Any],
|
|
24
|
+
now: datetime,
|
|
25
|
+
get_transport_type_fn: Callable[[Dict[str, Any]], str],
|
|
26
|
+
get_platform_fn: Callable[[Dict[str, Any]], str],
|
|
27
|
+
get_realtime_fn: Callable[[Dict[str, Any], Optional[str], Optional[str]], bool],
|
|
28
|
+
) -> Optional[UnifiedDeparture]:
|
|
29
|
+
"""Generic parser for departure data — shared logic across all providers."""
|
|
30
|
+
try:
|
|
31
|
+
if not isinstance(stop, dict):
|
|
32
|
+
_LOGGER.debug("Invalid stop data: expected dict, got %s", type(stop))
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
planned_time_str = stop.get("departureTimePlanned")
|
|
36
|
+
estimated_time_str = stop.get("departureTimeEstimated")
|
|
37
|
+
|
|
38
|
+
if not planned_time_str:
|
|
39
|
+
_LOGGER.debug("Missing departureTimePlanned in stop data")
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
if not isinstance(planned_time_str, str):
|
|
43
|
+
_LOGGER.debug("Invalid departureTimePlanned: expected str, got %s", type(planned_time_str))
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
planned_time = _parse_dt(planned_time_str)
|
|
47
|
+
estimated_time = _parse_dt(estimated_time_str) if estimated_time_str else planned_time
|
|
48
|
+
|
|
49
|
+
if not planned_time:
|
|
50
|
+
_LOGGER.debug("Failed to parse departureTimePlanned: %s", planned_time_str)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
planned_local = planned_time.astimezone(tz)
|
|
55
|
+
estimated_local = estimated_time.astimezone(tz) if estimated_time else planned_local
|
|
56
|
+
except (ValueError, TypeError) as e:
|
|
57
|
+
_LOGGER.debug("Failed to convert timezone: %s", e)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
delay_minutes = int((estimated_local - planned_local).total_seconds() / 60)
|
|
61
|
+
|
|
62
|
+
transportation = stop.get("transportation", {})
|
|
63
|
+
if not isinstance(transportation, dict):
|
|
64
|
+
_LOGGER.debug("Invalid transportation data: expected dict, got %s", type(transportation))
|
|
65
|
+
transportation = {}
|
|
66
|
+
|
|
67
|
+
destination_obj = transportation.get("destination", {})
|
|
68
|
+
if not isinstance(destination_obj, dict):
|
|
69
|
+
destination_obj = {}
|
|
70
|
+
destination = destination_obj.get("name", "Unknown")
|
|
71
|
+
|
|
72
|
+
line_number = str(transportation.get("number", ""))
|
|
73
|
+
description = str(transportation.get("description", ""))
|
|
74
|
+
agency = stop.get("agency")
|
|
75
|
+
|
|
76
|
+
transport_type = get_transport_type_fn(transportation)
|
|
77
|
+
platform = get_platform_fn(stop)
|
|
78
|
+
|
|
79
|
+
time_diff = estimated_local - now
|
|
80
|
+
minutes_until = max(0, int(time_diff.total_seconds() / 60))
|
|
81
|
+
|
|
82
|
+
is_realtime = get_realtime_fn(stop, estimated_time_str, planned_time_str)
|
|
83
|
+
|
|
84
|
+
notices = []
|
|
85
|
+
for info in stop.get("infos", []):
|
|
86
|
+
if isinstance(info, dict):
|
|
87
|
+
text = info.get("subtitle") or info.get("title") or info.get("content", "")
|
|
88
|
+
if text and isinstance(text, str):
|
|
89
|
+
notices.append(text.strip())
|
|
90
|
+
for hint in stop.get("hints", []):
|
|
91
|
+
if isinstance(hint, dict):
|
|
92
|
+
text = hint.get("content") or hint.get("text", "")
|
|
93
|
+
if text and isinstance(text, str):
|
|
94
|
+
notices.append(text.strip())
|
|
95
|
+
|
|
96
|
+
planned_platform = stop.get("plannedPlatformName") or stop.get("platform", {}).get("plannedName")
|
|
97
|
+
actual_platform = platform
|
|
98
|
+
platform_changed = bool(
|
|
99
|
+
planned_platform and actual_platform and str(planned_platform).strip() != str(actual_platform).strip()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return UnifiedDeparture(
|
|
103
|
+
line=line_number,
|
|
104
|
+
destination=destination,
|
|
105
|
+
departure_time=estimated_local.strftime("%H:%M"),
|
|
106
|
+
planned_time=planned_local.strftime("%H:%M"),
|
|
107
|
+
delay=delay_minutes,
|
|
108
|
+
platform=platform,
|
|
109
|
+
transportation_type=transport_type,
|
|
110
|
+
is_realtime=is_realtime,
|
|
111
|
+
minutes_until_departure=minutes_until,
|
|
112
|
+
departure_time_obj=estimated_local,
|
|
113
|
+
description=description if description else None,
|
|
114
|
+
agency=agency if agency else None,
|
|
115
|
+
notices=notices if notices else None,
|
|
116
|
+
planned_platform=str(planned_platform).strip() if planned_platform and platform_changed else None,
|
|
117
|
+
platform_changed=platform_changed,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
_LOGGER.debug("Error parsing departure: %s", e)
|
|
122
|
+
return None
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Provider registry and factory."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
from typing import Dict, Optional, Type
|
|
5
|
+
|
|
6
|
+
from ..const import (
|
|
7
|
+
PROVIDER_AVV_AUGSBURG,
|
|
8
|
+
PROVIDER_BEG,
|
|
9
|
+
PROVIDER_BSVG,
|
|
10
|
+
PROVIDER_BVG,
|
|
11
|
+
PROVIDER_DB,
|
|
12
|
+
PROVIDER_DING,
|
|
13
|
+
PROVIDER_HVV,
|
|
14
|
+
PROVIDER_KVV,
|
|
15
|
+
PROVIDER_MVV,
|
|
16
|
+
PROVIDER_NTA_IE,
|
|
17
|
+
PROVIDER_NVBW,
|
|
18
|
+
PROVIDER_NWL,
|
|
19
|
+
PROVIDER_OEBB,
|
|
20
|
+
PROVIDER_OPT,
|
|
21
|
+
PROVIDER_OTP_CUSTOM,
|
|
22
|
+
PROVIDER_RMV,
|
|
23
|
+
PROVIDER_RVV,
|
|
24
|
+
PROVIDER_SBB,
|
|
25
|
+
PROVIDER_TRAFIKLAB_SE,
|
|
26
|
+
PROVIDER_TRANSITOUS,
|
|
27
|
+
PROVIDER_VAGFR,
|
|
28
|
+
PROVIDER_VBN_OTP,
|
|
29
|
+
PROVIDER_VBN_TRIAS,
|
|
30
|
+
PROVIDER_VGN,
|
|
31
|
+
PROVIDER_VRN,
|
|
32
|
+
PROVIDER_VRR,
|
|
33
|
+
PROVIDER_VVO,
|
|
34
|
+
PROVIDER_VVS,
|
|
35
|
+
)
|
|
36
|
+
from .avv import AVVProvider
|
|
37
|
+
from .base import BaseProvider
|
|
38
|
+
from .beg import BEGProvider
|
|
39
|
+
from .bsvg import BSVGProvider
|
|
40
|
+
from .bvg import BVGProvider
|
|
41
|
+
from .db import DBProvider
|
|
42
|
+
from .ding import DINGProvider
|
|
43
|
+
from .gtfsde import OPTProvider
|
|
44
|
+
from .hvv import HVVProvider
|
|
45
|
+
from .kvv import KVVProvider
|
|
46
|
+
from .mvv import MVVProvider
|
|
47
|
+
from .nta import NTAProvider
|
|
48
|
+
from .nvbw import NVBWProvider
|
|
49
|
+
from .nwl import NWLProvider
|
|
50
|
+
from .oebb import OeBBProvider
|
|
51
|
+
from .otp_custom import OTPCustomProvider
|
|
52
|
+
from .rmv import RMVProvider
|
|
53
|
+
from .rvv import RVVProvider
|
|
54
|
+
from .sbb import SBBProvider
|
|
55
|
+
from .trafiklab import TrafiklabProvider
|
|
56
|
+
from .transitous import TransitousProvider
|
|
57
|
+
from .vagfr import VAGFRProvider
|
|
58
|
+
from .vbn import VBNOTPProvider, VBNTriasProvider
|
|
59
|
+
from .vgn import VGNProvider
|
|
60
|
+
from .vrn import VRNProvider
|
|
61
|
+
from .vrr import VRRProvider
|
|
62
|
+
from .vvo import VVOProvider
|
|
63
|
+
from .vvs import VVSProvider
|
|
64
|
+
|
|
65
|
+
_PROVIDER_REGISTRY: Dict[str, Type[BaseProvider]] = {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def register_provider(provider_id: str, provider_class: Type[BaseProvider]) -> None:
|
|
69
|
+
"""Register a provider class."""
|
|
70
|
+
_PROVIDER_REGISTRY[provider_id] = provider_class
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_provider(
|
|
74
|
+
provider_id: Optional[str],
|
|
75
|
+
session: aiohttp.ClientSession,
|
|
76
|
+
api_key: Optional[str] = None,
|
|
77
|
+
api_key_secondary: Optional[str] = None,
|
|
78
|
+
custom_url: Optional[str] = None,
|
|
79
|
+
) -> Optional[BaseProvider]:
|
|
80
|
+
"""Get a provider instance by ID."""
|
|
81
|
+
if provider_id is None:
|
|
82
|
+
return None
|
|
83
|
+
provider_class = _PROVIDER_REGISTRY.get(provider_id)
|
|
84
|
+
if provider_class:
|
|
85
|
+
return provider_class(
|
|
86
|
+
session,
|
|
87
|
+
api_key=api_key,
|
|
88
|
+
api_key_secondary=api_key_secondary,
|
|
89
|
+
custom_url=custom_url,
|
|
90
|
+
)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_all_provider_ids() -> list[str]:
|
|
95
|
+
"""Get all registered provider IDs."""
|
|
96
|
+
return list(_PROVIDER_REGISTRY.keys())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Register all providers
|
|
100
|
+
register_provider(PROVIDER_VRR, VRRProvider)
|
|
101
|
+
register_provider(PROVIDER_KVV, KVVProvider)
|
|
102
|
+
register_provider(PROVIDER_HVV, HVVProvider)
|
|
103
|
+
register_provider(PROVIDER_BVG, BVGProvider)
|
|
104
|
+
register_provider(PROVIDER_MVV, MVVProvider)
|
|
105
|
+
register_provider(PROVIDER_VVS, VVSProvider)
|
|
106
|
+
register_provider(PROVIDER_VGN, VGNProvider)
|
|
107
|
+
register_provider(PROVIDER_VAGFR, VAGFRProvider)
|
|
108
|
+
register_provider(PROVIDER_RMV, RMVProvider)
|
|
109
|
+
register_provider(PROVIDER_TRAFIKLAB_SE, TrafiklabProvider)
|
|
110
|
+
register_provider(PROVIDER_NTA_IE, NTAProvider)
|
|
111
|
+
register_provider(PROVIDER_VRN, VRNProvider)
|
|
112
|
+
register_provider(PROVIDER_VVO, VVOProvider)
|
|
113
|
+
register_provider(PROVIDER_DING, DINGProvider)
|
|
114
|
+
register_provider(PROVIDER_AVV_AUGSBURG, AVVProvider)
|
|
115
|
+
register_provider(PROVIDER_RVV, RVVProvider)
|
|
116
|
+
register_provider(PROVIDER_BSVG, BSVGProvider)
|
|
117
|
+
register_provider(PROVIDER_NWL, NWLProvider)
|
|
118
|
+
register_provider(PROVIDER_NVBW, NVBWProvider)
|
|
119
|
+
register_provider(PROVIDER_BEG, BEGProvider)
|
|
120
|
+
register_provider(PROVIDER_SBB, SBBProvider)
|
|
121
|
+
register_provider(PROVIDER_OEBB, OeBBProvider)
|
|
122
|
+
register_provider(PROVIDER_TRANSITOUS, TransitousProvider)
|
|
123
|
+
register_provider(PROVIDER_DB, DBProvider)
|
|
124
|
+
register_provider(PROVIDER_VBN_OTP, VBNOTPProvider)
|
|
125
|
+
register_provider(PROVIDER_VBN_TRIAS, VBNTriasProvider)
|
|
126
|
+
register_provider(PROVIDER_OPT, OPTProvider)
|
|
127
|
+
register_provider(PROVIDER_OTP_CUSTOM, OTPCustomProvider)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""AVV (Augsburger Verkehrs- & Tarifverbund) provider implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ..const import PROVIDER_AVV_AUGSBURG
|
|
6
|
+
from .efa_base import EFABaseProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AVVProvider(EFABaseProvider):
|
|
10
|
+
"""AVV (Augsburg) provider."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def provider_id(self) -> str:
|
|
14
|
+
return PROVIDER_AVV_AUGSBURG
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def provider_name(self) -> str:
|
|
18
|
+
return "AVV (Augsburg)"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def dm_base_url(self) -> str:
|
|
22
|
+
return "https://fahrtauskunft.avv-augsburg.de/efa/XML_DM_REQUEST"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def sf_base_url(self) -> str:
|
|
26
|
+
return "https://fahrtauskunft.avv-augsburg.de/efa/XML_STOPFINDER_REQUEST"
|
|
27
|
+
|
|
28
|
+
def get_timezone(self) -> str:
|
|
29
|
+
return "Europe/Berlin"
|
|
30
|
+
|
|
31
|
+
def get_transport_type_mapping(self) -> Dict[Any, str]:
|
|
32
|
+
return {
|
|
33
|
+
0: "train", # High-speed trains (ICE, IC, EC)
|
|
34
|
+
1: "train", # Regional trains (RE, RB, S-Bahn)
|
|
35
|
+
4: "tram", # Straßenbahn
|
|
36
|
+
5: "bus", # City bus
|
|
37
|
+
6: "bus", # Regional bus
|
|
38
|
+
7: "bus", # Express bus
|
|
39
|
+
8: "bus", # Night bus
|
|
40
|
+
13: "train", # Regionalzug (RE)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
|
|
44
|
+
return lambda s, est, plan: est != plan if est and plan else False
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base class for all public transport providers."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
|
|
9
|
+
from ..models import UnifiedDeparture
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseProvider(ABC):
|
|
13
|
+
"""Abstract base class for all public transport providers."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
session: aiohttp.ClientSession,
|
|
18
|
+
api_key: Optional[str] = None,
|
|
19
|
+
api_key_secondary: Optional[str] = None,
|
|
20
|
+
custom_url: Optional[str] = None,
|
|
21
|
+
):
|
|
22
|
+
self.session = session
|
|
23
|
+
self.api_key = api_key
|
|
24
|
+
self.api_key_secondary = api_key_secondary
|
|
25
|
+
self.custom_url = custom_url
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def provider_id(self) -> str:
|
|
30
|
+
"""Return the provider identifier (e.g., 'vrr', 'kvv')."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def provider_name(self) -> str:
|
|
36
|
+
"""Return the human-readable provider name."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def requires_api_key(self) -> bool:
|
|
41
|
+
"""Return True if this provider requires an API key."""
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
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
|
+
"""Fetch departure data from the provider's API."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def parse_departure(
|
|
57
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
58
|
+
) -> Optional[UnifiedDeparture]:
|
|
59
|
+
"""Parse a single departure from the provider's API response."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
64
|
+
"""Search for stops/stations."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def get_timezone(self) -> str:
|
|
68
|
+
"""Return the timezone for this provider (e.g., 'Europe/Berlin')."""
|
|
69
|
+
return "Europe/Berlin"
|
|
70
|
+
|
|
71
|
+
def get_transport_type_mapping(self) -> Dict[Any, str]:
|
|
72
|
+
"""Return the transportation type mapping for this provider."""
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
async def cleanup(self) -> None:
|
|
76
|
+
"""Cleanup provider resources."""
|
|
77
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""BEG (Bayerische Eisenbahngesellschaft) provider implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ..const import PROVIDER_BEG
|
|
6
|
+
from .efa_base import EFABaseProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BEGProvider(EFABaseProvider):
|
|
10
|
+
"""BEG (Bayern) provider."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def provider_id(self) -> str:
|
|
14
|
+
return PROVIDER_BEG
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def provider_name(self) -> str:
|
|
18
|
+
return "BEG (Bayern)"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def dm_base_url(self) -> str:
|
|
22
|
+
return "https://bahnland-bayern.de/efa/XML_DM_REQUEST"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def sf_base_url(self) -> str:
|
|
26
|
+
return "https://bahnland-bayern.de/efa/XML_STOPFINDER_REQUEST"
|
|
27
|
+
|
|
28
|
+
def get_timezone(self) -> str:
|
|
29
|
+
return "Europe/Berlin"
|
|
30
|
+
|
|
31
|
+
def get_transport_type_mapping(self) -> Dict[Any, str]:
|
|
32
|
+
return {
|
|
33
|
+
0: "train", # High-speed trains (ICE, IC, EC)
|
|
34
|
+
1: "train", # Regional trains (RE, RB)
|
|
35
|
+
2: "subway", # U-Bahn
|
|
36
|
+
3: "subway", # U-Bahn variant
|
|
37
|
+
4: "tram", # Tram/Streetcar
|
|
38
|
+
5: "bus", # City bus
|
|
39
|
+
6: "bus", # Regional bus
|
|
40
|
+
7: "bus", # Express bus
|
|
41
|
+
8: "bus", # Night bus
|
|
42
|
+
13: "train", # Regionalzug (RE)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
|
|
46
|
+
return lambda s, est, plan: est != plan if est and plan else False
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""BSVG (Braunschweiger Verkehrs-GmbH) provider implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ..const import PROVIDER_BSVG
|
|
6
|
+
from .efa_base import EFABaseProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BSVGProvider(EFABaseProvider):
|
|
10
|
+
"""BSVG (Braunschweig) provider."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def provider_id(self) -> str:
|
|
14
|
+
return PROVIDER_BSVG
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def provider_name(self) -> str:
|
|
18
|
+
return "BSVG (Braunschweig)"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def dm_base_url(self) -> str:
|
|
22
|
+
return "https://bsvg.efa.de/bsvagstd/XML_DM_REQUEST"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def sf_base_url(self) -> str:
|
|
26
|
+
return "https://bsvg.efa.de/bsvagstd/XML_STOPFINDER_REQUEST"
|
|
27
|
+
|
|
28
|
+
def get_timezone(self) -> str:
|
|
29
|
+
return "Europe/Berlin"
|
|
30
|
+
|
|
31
|
+
def get_transport_type_mapping(self) -> Dict[Any, str]:
|
|
32
|
+
return {
|
|
33
|
+
0: "train", # High-speed trains (ICE, IC, EC)
|
|
34
|
+
1: "train", # Regional trains (RE, RB)
|
|
35
|
+
4: "tram", # Tram/Streetcar
|
|
36
|
+
5: "bus", # City bus
|
|
37
|
+
6: "bus", # Regional bus
|
|
38
|
+
7: "bus", # Express bus
|
|
39
|
+
8: "bus", # Night bus
|
|
40
|
+
13: "train", # Regionalzug (RE)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
|
|
44
|
+
return lambda s, est, plan: est != plan if est and plan else False
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""BVG (Berliner Verkehrsbetriebe) provider implementation.
|
|
2
|
+
|
|
3
|
+
Uses the v6.vbb.transport.rest API (FPTF format).
|
|
4
|
+
No API key required.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..const import PROVIDER_BVG
|
|
8
|
+
from .fptf_base import FPTFBaseProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BVGProvider(FPTFBaseProvider):
|
|
12
|
+
"""BVG (Berlin) provider using VBB REST API."""
|
|
13
|
+
|
|
14
|
+
API_BASE = "https://v6.vbb.transport.rest"
|
|
15
|
+
|
|
16
|
+
PRODUCT_MAPPING = {
|
|
17
|
+
"subway": "subway",
|
|
18
|
+
"suburban": "train",
|
|
19
|
+
"tram": "tram",
|
|
20
|
+
"bus": "bus",
|
|
21
|
+
"ferry": "ferry",
|
|
22
|
+
"express": "train",
|
|
23
|
+
"regional": "train",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def provider_id(self) -> str:
|
|
28
|
+
return PROVIDER_BVG
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def provider_name(self) -> str:
|
|
32
|
+
return "BVG (Berlin)"
|