python-openpublictransport 0.1.17__tar.gz → 0.3.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 (110) hide show
  1. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/PKG-INFO +1 -1
  2. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/pyproject.toml +1 -1
  3. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/__init__.py +13 -1
  4. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/const.py +22 -2
  5. python_openpublictransport-0.3.0/src/openpublictransport/exceptions.py +81 -0
  6. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/__init__.py +63 -3
  7. python_openpublictransport-0.3.0/src/openpublictransport/providers/base.py +199 -0
  8. python_openpublictransport-0.3.0/src/openpublictransport/providers/db.py +69 -0
  9. python_openpublictransport-0.3.0/src/openpublictransport/providers/digitransit.py +102 -0
  10. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/efa_base.py +46 -92
  11. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/entur.py +8 -31
  12. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/fptf_base.py +33 -51
  13. python_openpublictransport-0.3.0/src/openpublictransport/providers/golemio.py +251 -0
  14. python_openpublictransport-0.3.0/src/openpublictransport/providers/gvh.py +55 -0
  15. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_base.py +2 -28
  16. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_mgate_base.py +166 -38
  17. python_openpublictransport-0.3.0/src/openpublictransport/providers/hongkong.py +364 -0
  18. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv_gti.py +21 -37
  19. python_openpublictransport-0.3.0/src/openpublictransport/providers/insa.py +29 -0
  20. python_openpublictransport-0.3.0/src/openpublictransport/providers/irail.py +210 -0
  21. python_openpublictransport-0.3.0/src/openpublictransport/providers/nahsh.py +29 -0
  22. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/national_rail.py +35 -41
  23. python_openpublictransport-0.3.0/src/openpublictransport/providers/nextbus_base.py +260 -0
  24. python_openpublictransport-0.3.0/src/openpublictransport/providers/nta.py +243 -0
  25. python_openpublictransport-0.3.0/src/openpublictransport/providers/nvv.py +30 -0
  26. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp.py +8 -16
  27. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_base.py +50 -41
  28. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_custom.py +2 -2
  29. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rejseplanen.py +10 -31
  30. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rmv.py +42 -78
  31. python_openpublictransport-0.3.0/src/openpublictransport/providers/saarvv.py +29 -0
  32. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/sbb.py +46 -53
  33. python_openpublictransport-0.3.0/src/openpublictransport/providers/sl.py +219 -0
  34. python_openpublictransport-0.3.0/src/openpublictransport/providers/tfl.py +256 -0
  35. python_openpublictransport-0.3.0/src/openpublictransport/providers/tfnsw.py +205 -0
  36. python_openpublictransport-0.3.0/src/openpublictransport/providers/trafiklab.py +170 -0
  37. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/transitous.py +25 -47
  38. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/trias_base.py +12 -26
  39. python_openpublictransport-0.3.0/src/openpublictransport/providers/trimet.py +22 -0
  40. python_openpublictransport-0.3.0/src/openpublictransport/providers/ttc.py +41 -0
  41. python_openpublictransport-0.3.0/src/openpublictransport/providers/vao.py +34 -0
  42. python_openpublictransport-0.3.0/src/openpublictransport/providers/vbn_hafas.py +31 -0
  43. python_openpublictransport-0.3.0/src/openpublictransport/providers/vmt.py +29 -0
  44. python_openpublictransport-0.3.0/src/openpublictransport/providers/vmv.py +55 -0
  45. python_openpublictransport-0.3.0/src/openpublictransport/providers/vos.py +29 -0
  46. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrr.py +2 -2
  47. python_openpublictransport-0.3.0/src/openpublictransport/providers/vsn.py +30 -0
  48. python_openpublictransport-0.3.0/src/openpublictransport/providers/wienerlinien.py +253 -0
  49. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
  50. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/SOURCES.txt +36 -2
  51. python_openpublictransport-0.3.0/tests/test_digitransit.py +73 -0
  52. python_openpublictransport-0.3.0/tests/test_german_regional_providers.py +204 -0
  53. python_openpublictransport-0.3.0/tests/test_golemio.py +128 -0
  54. python_openpublictransport-0.3.0/tests/test_hongkong.py +148 -0
  55. python_openpublictransport-0.3.0/tests/test_http_errors.py +222 -0
  56. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_hvv_gti.py +7 -3
  57. python_openpublictransport-0.3.0/tests/test_irail.py +131 -0
  58. python_openpublictransport-0.3.0/tests/test_provider_expansion.py +154 -0
  59. python_openpublictransport-0.3.0/tests/test_sbb.py +54 -0
  60. python_openpublictransport-0.3.0/tests/test_sl.py +150 -0
  61. python_openpublictransport-0.3.0/tests/test_tfl.py +114 -0
  62. python_openpublictransport-0.3.0/tests/test_tfnsw.py +137 -0
  63. python_openpublictransport-0.3.0/tests/test_ttc.py +167 -0
  64. python_openpublictransport-0.3.0/tests/test_wienerlinien.py +160 -0
  65. python_openpublictransport-0.1.17/src/openpublictransport/exceptions.py +0 -9
  66. python_openpublictransport-0.1.17/src/openpublictransport/providers/base.py +0 -77
  67. python_openpublictransport-0.1.17/src/openpublictransport/providers/db.py +0 -39
  68. python_openpublictransport-0.1.17/src/openpublictransport/providers/gtfsde.py +0 -21
  69. python_openpublictransport-0.1.17/src/openpublictransport/providers/nta.py +0 -291
  70. python_openpublictransport-0.1.17/src/openpublictransport/providers/trafiklab.py +0 -284
  71. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/setup.cfg +0 -0
  72. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/data/uk_stations.json +0 -0
  73. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/models.py +0 -0
  74. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/parsers.py +0 -0
  75. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/avv.py +0 -0
  76. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bart.py +0 -0
  77. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/beg.py +0 -0
  78. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bsvg.py +0 -0
  79. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bvg.py +0 -0
  80. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/dart.py +0 -0
  81. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ding.py +0 -0
  82. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv.py +0 -0
  83. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/irishrail.py +0 -0
  84. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/kvv.py +0 -0
  85. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mobiliteit_lu.py +0 -0
  86. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mvv.py +0 -0
  87. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ns.py +0 -0
  88. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nvbw.py +0 -0
  89. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nwl.py +0 -0
  90. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/oebb.py +0 -0
  91. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rvv.py +0 -0
  92. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/tpg.py +0 -0
  93. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vagfr.py +0 -0
  94. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vbn.py +0 -0
  95. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vgn.py +0 -0
  96. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrn.py +0 -0
  97. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvo.py +0 -0
  98. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvs.py +0 -0
  99. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/openpublictransport/py.typed +0 -0
  100. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
  101. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/requires.txt +0 -0
  102. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
  103. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_efa_mimetype.py +0 -0
  104. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_efa_platform.py +0 -0
  105. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_entur.py +0 -0
  106. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_hafas_base.py +0 -0
  107. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_hafas_mgate.py +0 -0
  108. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_national_rail.py +0 -0
  109. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_national_rail_parse.py +0 -0
  110. {python_openpublictransport-0.1.17 → python_openpublictransport-0.3.0}/tests/test_national_rail_static.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.1.17
3
+ Version: 0.3.0
4
4
  Summary: Python library for public transport APIs (EFA, OTP2, TRIAS, FPTF, HAFAS, GTFS-RT)
5
5
  License: MIT
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-openpublictransport"
7
- version = "0.1.17"
7
+ version = "0.3.0"
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" }
@@ -1,6 +1,13 @@
1
1
  """python-openpublictransport — public transport API library."""
2
2
 
3
- from .exceptions import AuthenticationError
3
+ from .exceptions import (
4
+ ApiConnectionError,
5
+ ApiError,
6
+ ApiResponseError,
7
+ ApiTimeoutError,
8
+ AuthenticationError,
9
+ OpenPublicTransportError,
10
+ )
4
11
  from .providers import get_all_provider_ids, get_provider, get_provider_class, register_provider
5
12
 
6
13
  __all__ = [
@@ -8,5 +15,10 @@ __all__ = [
8
15
  "get_provider_class",
9
16
  "get_all_provider_ids",
10
17
  "register_provider",
18
+ "OpenPublicTransportError",
19
+ "ApiError",
20
+ "ApiConnectionError",
21
+ "ApiTimeoutError",
22
+ "ApiResponseError",
11
23
  "AuthenticationError",
12
24
  ]
@@ -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,30 @@ 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"
42
62
 
43
63
  # API base URLs
44
- API_BASE_URL_VRR = "https://openservice-test.vrr.de/static03/XML_DM_REQUEST"
64
+ API_BASE_URL_VRR = "https://openservice.vrr.de/static03/XML_DM_REQUEST"
45
65
  API_BASE_URL_KVV = "https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST"
46
66
  API_BASE_URL_HVV = "https://hvv.efa.de/efa/XML_DM_REQUEST"
47
67
  API_BASE_URL_TRAFIKLAB = "https://realtime-api.trafiklab.se/v1"
@@ -0,0 +1,81 @@
1
+ """Exceptions for python-openpublictransport.
2
+
3
+ Every failure raised by this library derives from :class:`OpenPublicTransportError`.
4
+ Callers can therefore catch the whole family with a single ``except`` clause, or
5
+ discriminate on the concrete type to tell a server-side outage apart from a
6
+ network problem or an empty result.
7
+
8
+ Empty results are *not* errors: ``search_stops()`` returns ``[]`` and
9
+ ``fetch_departures()`` returns ``None`` only when the provider genuinely had no
10
+ data to give.
11
+ """
12
+
13
+ from typing import Optional
14
+
15
+
16
+ class OpenPublicTransportError(Exception):
17
+ """Base class for every error raised by this library."""
18
+
19
+
20
+ class ApiError(OpenPublicTransportError):
21
+ """Raised when a provider API answers with a non-200 HTTP status.
22
+
23
+ The HTTP status is available as :attr:`status` so callers can report it to
24
+ the user instead of silently treating the failure as "no results".
25
+ :attr:`body` holds a truncated copy of the error response, which is often
26
+ the only place a provider explains what it disliked.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ provider: str,
32
+ status: int,
33
+ message: Optional[str] = None,
34
+ body: Optional[str] = None,
35
+ ) -> None:
36
+ self.provider = provider
37
+ self.status = status
38
+ self.body = body
39
+ if message is None:
40
+ message = f"{provider}: API returned HTTP {status}"
41
+ if body:
42
+ message = f"{message} — {body}"
43
+ super().__init__(message)
44
+
45
+
46
+ class ApiConnectionError(OpenPublicTransportError):
47
+ """Raised when the provider API cannot be reached (DNS, TCP or TLS failure)."""
48
+
49
+
50
+ class ApiTimeoutError(ApiConnectionError):
51
+ """Raised when the provider API does not answer within the timeout."""
52
+
53
+
54
+ class ApiResponseError(OpenPublicTransportError):
55
+ """Raised when a 200 response carries a payload that cannot be used.
56
+
57
+ Covers undecodable JSON/XML as well as a well-formed body whose shape does
58
+ not match what the provider protocol requires.
59
+ """
60
+
61
+
62
+ class AuthenticationError(ApiError):
63
+ """Raised when API authentication fails (HTTP 401 or 403).
64
+
65
+ Signals that the configured API key is invalid or expired.
66
+ Callers should prompt the user to re-enter credentials rather than retrying.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ provider: str,
72
+ status: int = 401,
73
+ message: Optional[str] = None,
74
+ body: Optional[str] = None,
75
+ ) -> None:
76
+ super().__init__(
77
+ provider,
78
+ status,
79
+ message or f"{provider}: authentication failed (HTTP {status}) — check the API key",
80
+ body=body,
81
+ )
@@ -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,27 @@ 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,
45
65
  )
46
66
  from .avv import AVVProvider
47
67
  from .base import BaseProvider
@@ -50,7 +70,6 @@ from .bsvg import BSVGProvider
50
70
  from .bvg import BVGProvider
51
71
  from .db import DBProvider
52
72
  from .ding import DINGProvider
53
- from .gtfsde import OPTProvider
54
73
  from .hvv import HVVProvider
55
74
  from .hvv_gti import HVVGTIProvider
56
75
  from .kvv import KVVProvider
@@ -81,6 +100,27 @@ from .bart import BARTProvider
81
100
  from .dart import DARTProvider
82
101
  from .irishrail import IrishRailProvider
83
102
  from .tpg import TPGProvider
103
+ from .vmv import VMVProvider
104
+ from .gvh import GVHProvider
105
+ from .nahsh import NAHSHProvider
106
+ from .insa import INSAProvider
107
+ from .vmt import VMTProvider
108
+ from .saarvv import SaarVVProvider
109
+ from .vbn_hafas import VBNHafasProvider
110
+ from .nvv import NVVProvider
111
+ from .vos import VOSProvider
112
+ from .vsn import VSNProvider
113
+ from .vao import VAOProvider
114
+ from .trimet import TriMetProvider
115
+ from .irail import IRailProvider
116
+ from .sl import SLProvider
117
+ from .ttc import TTCProvider
118
+ from .hongkong import HongKongProvider
119
+ from .wienerlinien import WienerLinienProvider
120
+ from .tfl import TfLProvider
121
+ from .tfnsw import TfNSWProvider
122
+ from .digitransit import DigitransitProvider
123
+ from .golemio import GolemioProvider
84
124
 
85
125
  _PROVIDER_REGISTRY: Dict[str, Type[BaseProvider]] = {}
86
126
 
@@ -152,7 +192,6 @@ register_provider(PROVIDER_TRANSITOUS, TransitousProvider)
152
192
  register_provider(PROVIDER_DB, DBProvider)
153
193
  register_provider(PROVIDER_VBN_OTP, VBNOTPProvider)
154
194
  register_provider(PROVIDER_VBN_TRIAS, VBNTriasProvider)
155
- register_provider(PROVIDER_OPT, OPTProvider)
156
195
  register_provider(PROVIDER_OTP_CUSTOM, OTPCustomProvider)
157
196
  register_provider(PROVIDER_NATIONAL_RAIL, NationalRailProvider)
158
197
  register_provider(PROVIDER_REJSEPLANEN, RejseplanenProvider)
@@ -163,4 +202,25 @@ register_provider(PROVIDER_BART_US, BARTProvider)
163
202
  register_provider(PROVIDER_DART_US, DARTProvider)
164
203
  register_provider(PROVIDER_IRISHRAIL_IE, IrishRailProvider)
165
204
  register_provider(PROVIDER_TPG_CH, TPGProvider)
205
+ register_provider(PROVIDER_VMV, VMVProvider)
206
+ register_provider(PROVIDER_GVH, GVHProvider)
207
+ register_provider(PROVIDER_NAHSH, NAHSHProvider)
208
+ register_provider(PROVIDER_INSA, INSAProvider)
209
+ register_provider(PROVIDER_VMT, VMTProvider)
210
+ register_provider(PROVIDER_SAARVV, SaarVVProvider)
211
+ register_provider(PROVIDER_VBN_HAFAS, VBNHafasProvider)
212
+ register_provider(PROVIDER_NVV, NVVProvider)
213
+ register_provider(PROVIDER_VOS, VOSProvider)
214
+ register_provider(PROVIDER_VSN, VSNProvider)
215
+ register_provider(PROVIDER_VAO_AT, VAOProvider)
216
+ register_provider(PROVIDER_TRIMET_US, TriMetProvider)
217
+ register_provider(PROVIDER_IRAIL_BE, IRailProvider)
218
+ register_provider(PROVIDER_SL_SE, SLProvider)
219
+ register_provider(PROVIDER_TTC_CA, TTCProvider)
220
+ register_provider(PROVIDER_HONGKONG, HongKongProvider)
221
+ register_provider(PROVIDER_WIENERLINIEN, WienerLinienProvider)
222
+ register_provider(PROVIDER_TFL, TfLProvider)
223
+ register_provider(PROVIDER_TFNSW, TfNSWProvider)
224
+ register_provider(PROVIDER_DIGITRANSIT, DigitransitProvider)
225
+ register_provider(PROVIDER_GOLEMIO, GolemioProvider)
166
226
  register_provider(PROVIDER_HVV_GTI, HVVGTIProvider)
@@ -0,0 +1,199 @@
1
+ """Base class for all public transport providers."""
2
+
3
+ import asyncio
4
+ import logging
5
+
6
+ import aiohttp
7
+ from abc import ABC, abstractmethod
8
+ from datetime import datetime
9
+ from typing import Any, Dict, List, Optional, Union
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from ..exceptions import (
13
+ ApiConnectionError,
14
+ ApiError,
15
+ ApiResponseError,
16
+ ApiTimeoutError,
17
+ AuthenticationError,
18
+ )
19
+ from ..models import UnifiedDeparture
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+ class BaseProvider(ABC):
25
+ """Abstract base class for all public transport providers."""
26
+
27
+ def __init__(
28
+ self,
29
+ session: aiohttp.ClientSession,
30
+ api_key: Optional[str] = None,
31
+ api_key_secondary: Optional[str] = None,
32
+ custom_url: Optional[str] = None,
33
+ ):
34
+ self.session = session
35
+ self.api_key = api_key
36
+ self.api_key_secondary = api_key_secondary
37
+ self.custom_url = custom_url
38
+
39
+ async def _request(
40
+ self,
41
+ method: str,
42
+ url: str,
43
+ *,
44
+ retries: int = 1,
45
+ timeout: int = 10,
46
+ response_format: str = "json",
47
+ json_content_type: Optional[str] = None,
48
+ **kwargs: Any,
49
+ ) -> Any:
50
+ """Perform an HTTP request, returning the decoded payload or raising.
51
+
52
+ This is the single place where HTTP status codes are turned into
53
+ exceptions. Providers must not swallow the result: an empty list or
54
+ ``None`` returned by a provider means "no data", never "request failed".
55
+
56
+ Args:
57
+ method: ``"get"`` or ``"post"``.
58
+ url: Absolute request URL.
59
+ retries: Total number of attempts. Only 5xx responses, timeouts and
60
+ connection errors are retried, with ``2 ** attempt`` backoff.
61
+ 4xx responses fail immediately — retrying a bad key or a wrong
62
+ endpoint only delays the error the user needs to see.
63
+ timeout: Total request timeout in seconds.
64
+ response_format: ``"json"``, ``"text"`` or ``"bytes"``.
65
+ json_content_type: Passed to ``response.json()``. The default
66
+ ``None`` disables aiohttp's strict content-type check, because
67
+ some EFA deployments send RapidJSON with an XML header (#79).
68
+ **kwargs: Forwarded to the aiohttp session method (``params``,
69
+ ``headers``, ``data``, ``json``, ...).
70
+
71
+ Returns:
72
+ The decoded payload, or ``None`` for an empty 204 response.
73
+
74
+ Raises:
75
+ AuthenticationError: HTTP 401 or 403. Never retried.
76
+ ApiError: Any other non-2xx status, carrying ``.status``.
77
+ ApiTimeoutError: The request timed out on every attempt.
78
+ ApiConnectionError: The provider could not be reached.
79
+ ApiResponseError: A 2xx response whose body could not be decoded.
80
+ """
81
+ request = getattr(self.session, method.lower())
82
+ name = self.provider_name
83
+ last_error: Optional[Exception] = None
84
+
85
+ for attempt in range(1, retries + 1):
86
+ try:
87
+ async with request(url, timeout=aiohttp.ClientTimeout(total=timeout), **kwargs) as response:
88
+ status = response.status
89
+
90
+ if status in (401, 403):
91
+ raise AuthenticationError(name, status, body=await self._error_body(response))
92
+
93
+ if status >= 500:
94
+ last_error = ApiError(name, status, body=await self._error_body(response))
95
+ elif status >= 400:
96
+ raise ApiError(name, status, body=await self._error_body(response))
97
+ elif status == 204:
98
+ return None
99
+ else:
100
+ return await self._decode_response(response, response_format, json_content_type)
101
+
102
+ except asyncio.TimeoutError as exc:
103
+ last_error = ApiTimeoutError(f"{name}: request timed out after {timeout}s")
104
+ last_error.__cause__ = exc
105
+ except aiohttp.ClientError as exc:
106
+ last_error = ApiConnectionError(f"{name}: connection failed ({exc})")
107
+ last_error.__cause__ = exc
108
+
109
+ if attempt < retries:
110
+ _LOGGER.debug("%s: attempt %s/%s failed (%s), retrying", name, attempt, retries, last_error)
111
+ await asyncio.sleep(2**attempt)
112
+
113
+ raise last_error if last_error else ApiConnectionError(f"{name}: request failed")
114
+
115
+ @staticmethod
116
+ async def _error_body(response: Any, limit: int = 300) -> Optional[str]:
117
+ """Return a truncated error body, or None if it cannot be read.
118
+
119
+ Providers often explain a rejection only in the body (a SOAP fault, a
120
+ JSON ``errorText``), so it is worth carrying into the exception.
121
+ """
122
+ try:
123
+ text = (await response.text() or "").strip()
124
+ except Exception: # noqa: BLE001 — diagnostics must never mask the real error
125
+ return None
126
+ if not text:
127
+ return None
128
+ return text[:limit] + ("…" if len(text) > limit else "")
129
+
130
+ async def _decode_response(
131
+ self,
132
+ response: Any,
133
+ response_format: str,
134
+ json_content_type: Optional[str],
135
+ ) -> Any:
136
+ """Decode a successful response, raising ApiResponseError on bad payloads."""
137
+ try:
138
+ if response_format == "json":
139
+ return await response.json(content_type=json_content_type)
140
+ if response_format == "text":
141
+ return await response.text()
142
+ if response_format == "bytes":
143
+ return await response.read()
144
+ except (ValueError, aiohttp.ClientError) as exc:
145
+ raise ApiResponseError(f"{self.provider_name}: could not decode response ({exc})") from exc
146
+
147
+ raise ValueError(f"Unknown response_format: {response_format}")
148
+
149
+ @property
150
+ @abstractmethod
151
+ def provider_id(self) -> str:
152
+ """Return the provider identifier (e.g., 'vrr', 'kvv')."""
153
+ pass
154
+
155
+ @property
156
+ @abstractmethod
157
+ def provider_name(self) -> str:
158
+ """Return the human-readable provider name."""
159
+ pass
160
+
161
+ @property
162
+ def requires_api_key(self) -> bool:
163
+ """Return True if this provider requires an API key."""
164
+ return False
165
+
166
+ @abstractmethod
167
+ async def fetch_departures(
168
+ self,
169
+ station_id: Optional[str],
170
+ place_dm: str,
171
+ name_dm: str,
172
+ departures_limit: int,
173
+ ) -> Optional[Dict[str, Any]]:
174
+ """Fetch departure data from the provider's API."""
175
+ pass
176
+
177
+ @abstractmethod
178
+ def parse_departure(
179
+ self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
180
+ ) -> Optional[UnifiedDeparture]:
181
+ """Parse a single departure from the provider's API response."""
182
+ pass
183
+
184
+ @abstractmethod
185
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
186
+ """Search for stops/stations."""
187
+ pass
188
+
189
+ def get_timezone(self) -> str:
190
+ """Return the timezone for this provider (e.g., 'Europe/Berlin')."""
191
+ return "Europe/Berlin"
192
+
193
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
194
+ """Return the transportation type mapping for this provider."""
195
+ return {}
196
+
197
+ async def cleanup(self) -> None:
198
+ """Cleanup provider resources."""
199
+ pass
@@ -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"]
@@ -0,0 +1,102 @@
1
+ """Digitransit (Finland) provider — OpenTripPlanner 2 GraphQL.
2
+
3
+ Requires a free subscription key from https://portal-api.digitransit.fi/,
4
+ sent as the ``digitransit-subscription-key`` header.
5
+
6
+ Default router is ``finland`` (all of Finland, incl. HSL Helsinki, Tampere,
7
+ Turku, Oulu, VR long-distance trains). Regional routers can be selected via
8
+ ``custom_url``: either the full GraphQL URL or one of the shorthands
9
+ ``"finland"``, ``"hsl"`` (Helsinki region) and ``"waltti"`` (Waltti cities).
10
+
11
+ Stop ids are GTFS ids (``HSL:1020453``); stops that share a parent station or
12
+ lie close together with the same name are combined with ``|`` like the other
13
+ OTP2 providers.
14
+ """
15
+
16
+ import logging
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from .otp import OTPProvider, _smart_title
20
+ from ..const import PROVIDER_DIGITRANSIT
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+
25
+ ROUTER_URLS: Dict[str, str] = {
26
+ "finland": "https://api.digitransit.fi/routing/v2/finland/gtfs/v1",
27
+ "hsl": "https://api.digitransit.fi/routing/v2/hsl/gtfs/v1",
28
+ "waltti": "https://api.digitransit.fi/routing/v2/waltti/gtfs/v1",
29
+ }
30
+
31
+ MODE_MAPPING: Dict[str, str] = {
32
+ "BUS": "bus",
33
+ "COACH": "bus",
34
+ "TROLLEYBUS": "bus",
35
+ "RAIL": "train",
36
+ "TRAM": "tram",
37
+ "SUBWAY": "subway",
38
+ "FERRY": "ferry",
39
+ "FUNICULAR": "train",
40
+ "GONDOLA": "tram",
41
+ "CABLE_CAR": "tram",
42
+ }
43
+
44
+
45
+ class DigitransitProvider(OTPProvider):
46
+ """Digitransit Finland (HSL, Waltti, VR …)."""
47
+
48
+ otp_base_url: str = ROUTER_URLS["finland"]
49
+ geocode_countries: str = "fi"
50
+
51
+ @property
52
+ def provider_id(self) -> str:
53
+ return PROVIDER_DIGITRANSIT
54
+
55
+ @property
56
+ def provider_name(self) -> str:
57
+ return "Digitransit (Finnland)"
58
+
59
+ @property
60
+ def requires_api_key(self) -> bool:
61
+ return True
62
+
63
+ def get_timezone(self) -> str:
64
+ return "Europe/Helsinki"
65
+
66
+ def get_mode_mapping(self) -> Dict[str, str]:
67
+ return MODE_MAPPING
68
+
69
+ @property
70
+ def graphql_url(self) -> str:
71
+ custom = (self.custom_url or "").strip()
72
+ if custom:
73
+ return ROUTER_URLS.get(custom.lower(), custom.rstrip("/"))
74
+ return self.otp_base_url
75
+
76
+ def _auth_headers(self) -> Dict[str, str]:
77
+ headers = {"Accept": "application/json", "Content-Type": "application/json"}
78
+ if self.api_key:
79
+ headers["digitransit-subscription-key"] = self.api_key
80
+ return headers
81
+
82
+ async def _graphql(self, query: str) -> Optional[Dict[str, Any]]:
83
+ # Digitransit's endpoint *is* the GraphQL URL (no /index/graphql suffix).
84
+ return await self._request(
85
+ "post",
86
+ self.graphql_url,
87
+ json={"query": query},
88
+ headers=self._auth_headers(),
89
+ timeout=15,
90
+ )
91
+
92
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
93
+ """Search by stop name; no German city-prefix / Nominatim fallbacks."""
94
+ term = search_term.strip()
95
+ if not term:
96
+ return []
97
+ smart = _smart_title(term)
98
+ for candidate in dict.fromkeys([term, smart]):
99
+ raw = await self._search_one(candidate)
100
+ if raw:
101
+ return self._group_by_name(raw)[:20]
102
+ return []