python-openpublictransport 0.1.0__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.
Files changed (46) hide show
  1. python_openpublictransport-0.1.0/PKG-INFO +14 -0
  2. python_openpublictransport-0.1.0/pyproject.toml +29 -0
  3. python_openpublictransport-0.1.0/setup.cfg +4 -0
  4. python_openpublictransport-0.1.0/src/openpublictransport/__init__.py +5 -0
  5. python_openpublictransport-0.1.0/src/openpublictransport/const.py +75 -0
  6. python_openpublictransport-0.1.0/src/openpublictransport/models.py +95 -0
  7. python_openpublictransport-0.1.0/src/openpublictransport/parsers.py +122 -0
  8. python_openpublictransport-0.1.0/src/openpublictransport/providers/__init__.py +127 -0
  9. python_openpublictransport-0.1.0/src/openpublictransport/providers/avv.py +44 -0
  10. python_openpublictransport-0.1.0/src/openpublictransport/providers/base.py +77 -0
  11. python_openpublictransport-0.1.0/src/openpublictransport/providers/beg.py +46 -0
  12. python_openpublictransport-0.1.0/src/openpublictransport/providers/bsvg.py +44 -0
  13. python_openpublictransport-0.1.0/src/openpublictransport/providers/bvg.py +32 -0
  14. python_openpublictransport-0.1.0/src/openpublictransport/providers/db.py +39 -0
  15. python_openpublictransport-0.1.0/src/openpublictransport/providers/ding.py +44 -0
  16. python_openpublictransport-0.1.0/src/openpublictransport/providers/efa_base.py +209 -0
  17. python_openpublictransport-0.1.0/src/openpublictransport/providers/fptf_base.py +169 -0
  18. python_openpublictransport-0.1.0/src/openpublictransport/providers/gtfsde.py +21 -0
  19. python_openpublictransport-0.1.0/src/openpublictransport/providers/hvv.py +40 -0
  20. python_openpublictransport-0.1.0/src/openpublictransport/providers/kvv.py +38 -0
  21. python_openpublictransport-0.1.0/src/openpublictransport/providers/mvv.py +48 -0
  22. python_openpublictransport-0.1.0/src/openpublictransport/providers/nta.py +288 -0
  23. python_openpublictransport-0.1.0/src/openpublictransport/providers/nvbw.py +44 -0
  24. python_openpublictransport-0.1.0/src/openpublictransport/providers/nwl.py +46 -0
  25. python_openpublictransport-0.1.0/src/openpublictransport/providers/oebb.py +182 -0
  26. python_openpublictransport-0.1.0/src/openpublictransport/providers/otp.py +378 -0
  27. python_openpublictransport-0.1.0/src/openpublictransport/providers/otp_base.py +268 -0
  28. python_openpublictransport-0.1.0/src/openpublictransport/providers/otp_custom.py +22 -0
  29. python_openpublictransport-0.1.0/src/openpublictransport/providers/rmv.py +272 -0
  30. python_openpublictransport-0.1.0/src/openpublictransport/providers/rvv.py +43 -0
  31. python_openpublictransport-0.1.0/src/openpublictransport/providers/sbb.py +174 -0
  32. python_openpublictransport-0.1.0/src/openpublictransport/providers/trafiklab.py +280 -0
  33. python_openpublictransport-0.1.0/src/openpublictransport/providers/transitous.py +198 -0
  34. python_openpublictransport-0.1.0/src/openpublictransport/providers/trias_base.py +323 -0
  35. python_openpublictransport-0.1.0/src/openpublictransport/providers/vagfr.py +44 -0
  36. python_openpublictransport-0.1.0/src/openpublictransport/providers/vbn.py +73 -0
  37. python_openpublictransport-0.1.0/src/openpublictransport/providers/vgn.py +46 -0
  38. python_openpublictransport-0.1.0/src/openpublictransport/providers/vrn.py +51 -0
  39. python_openpublictransport-0.1.0/src/openpublictransport/providers/vrr.py +51 -0
  40. python_openpublictransport-0.1.0/src/openpublictransport/providers/vvo.py +45 -0
  41. python_openpublictransport-0.1.0/src/openpublictransport/providers/vvs.py +48 -0
  42. python_openpublictransport-0.1.0/src/python_openpublictransport.egg-info/PKG-INFO +14 -0
  43. python_openpublictransport-0.1.0/src/python_openpublictransport.egg-info/SOURCES.txt +44 -0
  44. python_openpublictransport-0.1.0/src/python_openpublictransport.egg-info/dependency_links.txt +1 -0
  45. python_openpublictransport-0.1.0/src/python_openpublictransport.egg-info/requires.txt +8 -0
  46. python_openpublictransport-0.1.0/src/python_openpublictransport.egg-info/top_level.txt +1 -0
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-openpublictransport
3
+ Version: 0.1.0
4
+ Summary: Python library for public transport APIs (EFA, OTP2, TRIAS, FPTF, HAFAS, GTFS-RT)
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: aiohttp>=3.9
9
+ Requires-Dist: aiofiles>=23.0
10
+ Requires-Dist: gtfs-realtime-bindings>=1.0.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
+ Requires-Dist: aioresponses>=0.7; extra == "dev"
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-openpublictransport"
7
+ version = "0.1.0"
8
+ description = "Python library for public transport APIs (EFA, OTP2, TRIAS, FPTF, HAFAS, GTFS-RT)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "aiohttp>=3.9",
14
+ "aiofiles>=23.0",
15
+ "gtfs-realtime-bindings>=1.0.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.0",
21
+ "pytest-asyncio>=0.23",
22
+ "aioresponses>=0.7",
23
+ ]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.pytest.ini_options]
29
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """python-openpublictransport — public transport API library."""
2
+
3
+ from .providers import get_all_provider_ids, get_provider, register_provider
4
+
5
+ __all__ = ["get_provider", "get_all_provider_ids", "register_provider"]
@@ -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