python-openpublictransport 0.2.0__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 (106) hide show
  1. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/PKG-INFO +1 -1
  2. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/pyproject.toml +1 -1
  3. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/const.py +22 -2
  4. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/__init__.py +63 -3
  5. python_openpublictransport-0.3.0/src/openpublictransport/providers/db.py +69 -0
  6. python_openpublictransport-0.3.0/src/openpublictransport/providers/digitransit.py +102 -0
  7. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/efa_base.py +8 -7
  8. python_openpublictransport-0.3.0/src/openpublictransport/providers/golemio.py +251 -0
  9. python_openpublictransport-0.3.0/src/openpublictransport/providers/gvh.py +55 -0
  10. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_mgate_base.py +143 -10
  11. python_openpublictransport-0.3.0/src/openpublictransport/providers/hongkong.py +364 -0
  12. python_openpublictransport-0.3.0/src/openpublictransport/providers/insa.py +29 -0
  13. python_openpublictransport-0.3.0/src/openpublictransport/providers/irail.py +210 -0
  14. python_openpublictransport-0.3.0/src/openpublictransport/providers/nahsh.py +29 -0
  15. python_openpublictransport-0.3.0/src/openpublictransport/providers/nextbus_base.py +260 -0
  16. python_openpublictransport-0.3.0/src/openpublictransport/providers/nvv.py +30 -0
  17. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp.py +1 -1
  18. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_base.py +20 -3
  19. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_custom.py +2 -2
  20. python_openpublictransport-0.3.0/src/openpublictransport/providers/saarvv.py +29 -0
  21. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/sbb.py +22 -11
  22. python_openpublictransport-0.3.0/src/openpublictransport/providers/sl.py +219 -0
  23. python_openpublictransport-0.3.0/src/openpublictransport/providers/tfl.py +256 -0
  24. python_openpublictransport-0.3.0/src/openpublictransport/providers/tfnsw.py +205 -0
  25. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/transitous.py +1 -0
  26. python_openpublictransport-0.3.0/src/openpublictransport/providers/trimet.py +22 -0
  27. python_openpublictransport-0.3.0/src/openpublictransport/providers/ttc.py +41 -0
  28. python_openpublictransport-0.3.0/src/openpublictransport/providers/vao.py +34 -0
  29. python_openpublictransport-0.3.0/src/openpublictransport/providers/vbn_hafas.py +31 -0
  30. python_openpublictransport-0.3.0/src/openpublictransport/providers/vmt.py +29 -0
  31. python_openpublictransport-0.3.0/src/openpublictransport/providers/vmv.py +55 -0
  32. python_openpublictransport-0.3.0/src/openpublictransport/providers/vos.py +29 -0
  33. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrr.py +2 -2
  34. python_openpublictransport-0.3.0/src/openpublictransport/providers/vsn.py +30 -0
  35. python_openpublictransport-0.3.0/src/openpublictransport/providers/wienerlinien.py +253 -0
  36. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
  37. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/SOURCES.txt +35 -2
  38. python_openpublictransport-0.3.0/tests/test_digitransit.py +73 -0
  39. python_openpublictransport-0.3.0/tests/test_german_regional_providers.py +204 -0
  40. python_openpublictransport-0.3.0/tests/test_golemio.py +128 -0
  41. python_openpublictransport-0.3.0/tests/test_hongkong.py +148 -0
  42. python_openpublictransport-0.3.0/tests/test_irail.py +131 -0
  43. python_openpublictransport-0.3.0/tests/test_provider_expansion.py +154 -0
  44. python_openpublictransport-0.3.0/tests/test_sbb.py +54 -0
  45. python_openpublictransport-0.3.0/tests/test_sl.py +150 -0
  46. python_openpublictransport-0.3.0/tests/test_tfl.py +114 -0
  47. python_openpublictransport-0.3.0/tests/test_tfnsw.py +137 -0
  48. python_openpublictransport-0.3.0/tests/test_ttc.py +167 -0
  49. python_openpublictransport-0.3.0/tests/test_wienerlinien.py +160 -0
  50. python_openpublictransport-0.2.0/src/openpublictransport/providers/db.py +0 -39
  51. python_openpublictransport-0.2.0/src/openpublictransport/providers/gtfsde.py +0 -21
  52. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/setup.cfg +0 -0
  53. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/__init__.py +0 -0
  54. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/data/uk_stations.json +0 -0
  55. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/exceptions.py +0 -0
  56. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/models.py +0 -0
  57. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/parsers.py +0 -0
  58. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/avv.py +0 -0
  59. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bart.py +0 -0
  60. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/base.py +0 -0
  61. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/beg.py +0 -0
  62. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bsvg.py +0 -0
  63. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bvg.py +0 -0
  64. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/dart.py +0 -0
  65. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ding.py +0 -0
  66. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/entur.py +0 -0
  67. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/fptf_base.py +0 -0
  68. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_base.py +0 -0
  69. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv.py +0 -0
  70. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv_gti.py +0 -0
  71. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/irishrail.py +0 -0
  72. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/kvv.py +0 -0
  73. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mobiliteit_lu.py +0 -0
  74. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mvv.py +0 -0
  75. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/national_rail.py +0 -0
  76. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ns.py +0 -0
  77. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nta.py +0 -0
  78. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nvbw.py +0 -0
  79. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nwl.py +0 -0
  80. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/oebb.py +0 -0
  81. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rejseplanen.py +0 -0
  82. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rmv.py +0 -0
  83. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rvv.py +0 -0
  84. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/tpg.py +0 -0
  85. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/trafiklab.py +0 -0
  86. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/trias_base.py +0 -0
  87. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vagfr.py +0 -0
  88. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vbn.py +0 -0
  89. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vgn.py +0 -0
  90. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrn.py +0 -0
  91. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvo.py +0 -0
  92. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvs.py +0 -0
  93. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/py.typed +0 -0
  94. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
  95. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/requires.txt +0 -0
  96. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
  97. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_efa_mimetype.py +0 -0
  98. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_efa_platform.py +0 -0
  99. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_entur.py +0 -0
  100. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hafas_base.py +0 -0
  101. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hafas_mgate.py +0 -0
  102. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_http_errors.py +0 -0
  103. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hvv_gti.py +0 -0
  104. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_national_rail.py +0 -0
  105. {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_national_rail_parse.py +0 -0
  106. {python_openpublictransport-0.2.0 → 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.2.0
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.2.0"
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" }
@@ -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"
@@ -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,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 []
@@ -122,7 +122,7 @@ class EFABaseProvider(BaseProvider):
122
122
  """Fetch departure data from EFA API."""
123
123
  if station_id:
124
124
  params = (
125
- f"outputFormat=RapidJSON&"
125
+ f"outputFormat=rapidJSON&"
126
126
  f"stateless=1&"
127
127
  f"type_dm=any&"
128
128
  f"name_dm={station_id}&"
@@ -132,7 +132,7 @@ class EFABaseProvider(BaseProvider):
132
132
  )
133
133
  else:
134
134
  params = (
135
- f"outputFormat=RapidJSON&"
135
+ f"outputFormat=rapidJSON&"
136
136
  f"place_dm={place_dm}&"
137
137
  f"type_dm=stop&"
138
138
  f"name_dm={name_dm}&"
@@ -199,7 +199,7 @@ class EFABaseProvider(BaseProvider):
199
199
  stop_name = parts[0].strip()
200
200
  place_name = parts[1].strip()
201
201
  params = (
202
- f"outputFormat=RapidJSON&"
202
+ f"outputFormat=rapidJSON&"
203
203
  f"locationServerActive=1&"
204
204
  f"type_sf=any&"
205
205
  f"name_sf={quote(stop_name, safe='')}&"
@@ -208,7 +208,7 @@ class EFABaseProvider(BaseProvider):
208
208
  )
209
209
  else:
210
210
  params = (
211
- f"outputFormat=RapidJSON&"
211
+ f"outputFormat=rapidJSON&"
212
212
  f"locationServerActive=1&"
213
213
  f"type_sf=stop&"
214
214
  f"name_sf={quote(search_term, safe='')}&"
@@ -218,9 +218,10 @@ class EFABaseProvider(BaseProvider):
218
218
  url = f"{self.sf_base_url}?{params}"
219
219
  name = self.provider_name
220
220
 
221
- # content_type=None: some deployments (VGN) send RapidJSON payloads with
222
- # a "text/xml;;charset=utf-8" header (issue #79).
223
- data = await self._request("get", url, timeout=10)
221
+ # Same User-Agent as the departure request: some EFA hosts throttle the
222
+ # bare aiohttp default (VRR answered it with HTTP 429).
223
+ headers = {"User-Agent": f"Mozilla/5.0 (compatible; OpenPublicTransport {self.provider_id.upper()})"}
224
+ data = await self._request("get", url, headers=headers, retries=2, timeout=10)
224
225
 
225
226
  if not isinstance(data, dict):
226
227
  raise ApiResponseError(f"{name}: API returned {type(data).__name__} instead of an object")
@@ -0,0 +1,251 @@
1
+ """Golemio PID (Prague Integrated Transport) provider.
2
+
3
+ Departures come from the Golemio API ``/v2/pid/departureboards`` and require a
4
+ free key (https://api.golemio.cz/api-keys/), sent as the ``x-access-token``
5
+ header.
6
+
7
+ Golemio has no fuzzy stop search, so stop search uses the public PID stop list
8
+ (https://data.pid.cz/stops/json/stops.json, no key, ~1.6 MB gzipped),
9
+ downloaded once per provider instance and filtered locally.
10
+
11
+ Station ids are ASW node numbers (``1072`` = Můstek): a node groups all
12
+ platforms of a stop incl. metro and train stations. Also accepted: a single
13
+ ASW stop ``1072/101`` / ``1072_101`` or a GTFS stop id ``U1072Z101P``; several
14
+ ids may be combined with ``|``.
15
+ """
16
+
17
+ import logging
18
+ import re
19
+ import unicodedata
20
+ from datetime import datetime
21
+ from typing import Any, Dict, List, Optional, Tuple, Union
22
+ from zoneinfo import ZoneInfo
23
+
24
+ from ..exceptions import ApiResponseError, AuthenticationError
25
+ from ..models import UnifiedDeparture
26
+ from .base import BaseProvider
27
+ from ..const import PROVIDER_GOLEMIO
28
+
29
+ _LOGGER = logging.getLogger(__name__)
30
+
31
+
32
+ DEPARTURES_URL = "https://api.golemio.cz/v2/pid/departureboards"
33
+ STOPS_URL = "https://data.pid.cz/stops/json/stops.json"
34
+
35
+ # GTFS route types as used by PID.
36
+ ROUTE_TYPES: Dict[Any, str] = {
37
+ 0: "tram",
38
+ 1: "subway",
39
+ 2: "train",
40
+ 3: "bus",
41
+ 4: "ferry",
42
+ 7: "train", # funicular (Petřín)
43
+ 11: "bus", # trolleybus
44
+ }
45
+
46
+ _GTFS_ID_RE = re.compile(r"^U\d+[ZN]\d+", re.IGNORECASE)
47
+
48
+
49
+ def _normalize(text: str) -> str:
50
+ text = (text or "").casefold()
51
+ return "".join(c for c in unicodedata.normalize("NFKD", text) if not unicodedata.combining(c))
52
+
53
+
54
+ def _parse_dt(value: Any) -> Optional[datetime]:
55
+ if not isinstance(value, str) or not value:
56
+ return None
57
+ try:
58
+ return datetime.fromisoformat(value)
59
+ except ValueError:
60
+ return None
61
+
62
+
63
+ def departure_params(station_id: str, limit: int) -> List[Tuple[str, str]]:
64
+ """Build the departureboards query for one or more ``|``-separated ids."""
65
+ params: List[Tuple[str, str]] = []
66
+ for part in station_id.split("|"):
67
+ part = part.strip()
68
+ if not part:
69
+ continue
70
+ if _GTFS_ID_RE.match(part):
71
+ params.append(("ids[]", part))
72
+ else:
73
+ params.append(("aswIds[]", part.replace("/", "_")))
74
+ params += [
75
+ ("limit", str(limit)),
76
+ ("minutesBefore", "0"),
77
+ ("minutesAfter", "180"),
78
+ ("mode", "departures"),
79
+ ("order", "real"),
80
+ ("skip[]", "canceled"),
81
+ ]
82
+ return params
83
+
84
+
85
+ class GolemioProvider(BaseProvider):
86
+ """PID Prague via Golemio."""
87
+
88
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
89
+ super().__init__(*args, **kwargs)
90
+ self._stops: Optional[List[Dict[str, str]]] = None
91
+
92
+ @property
93
+ def provider_id(self) -> str:
94
+ return PROVIDER_GOLEMIO
95
+
96
+ @property
97
+ def provider_name(self) -> str:
98
+ return "PID (Praha)"
99
+
100
+ @property
101
+ def requires_api_key(self) -> bool:
102
+ return True
103
+
104
+ def get_timezone(self) -> str:
105
+ return "Europe/Prague"
106
+
107
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
108
+ return ROUTE_TYPES
109
+
110
+ def _headers(self) -> Dict[str, str]:
111
+ if not self.api_key:
112
+ raise AuthenticationError(self.provider_name, 401, message=f"{self.provider_name}: API key required")
113
+ return {"x-access-token": self.api_key, "Accept": "application/json"}
114
+
115
+ async def fetch_departures(
116
+ self,
117
+ station_id: Optional[str],
118
+ place_dm: str,
119
+ name_dm: str,
120
+ departures_limit: int,
121
+ ) -> Optional[Dict[str, Any]]:
122
+ if station_id:
123
+ params = departure_params(station_id, departures_limit)
124
+ elif name_dm:
125
+ # Exact (case-sensitive) stop name, as Golemio's names[] requires.
126
+ params = [("names[]", name_dm), ("limit", str(departures_limit)), ("skip[]", "canceled")]
127
+ else:
128
+ return None
129
+
130
+ data = await self._request(
131
+ "get", DEPARTURES_URL, params=params, headers=self._headers(), timeout=15, retries=2
132
+ )
133
+ if not isinstance(data, dict):
134
+ raise ApiResponseError(f"{self.provider_name}: API returned {type(data).__name__} instead of an object")
135
+ return {"stopEvents": self.events_from_board(data)[:departures_limit]}
136
+
137
+ @staticmethod
138
+ def events_from_board(data: Dict[str, Any]) -> List[Dict[str, Any]]:
139
+ """Attach the board's infotexts to every departure."""
140
+ notices: List[str] = []
141
+ for info in data.get("infotexts") or []:
142
+ if isinstance(info, dict):
143
+ text = " ".join((info.get("text") or info.get("text_en") or "").split())
144
+ if text and text not in notices:
145
+ notices.append(text)
146
+ events = []
147
+ for dep in data.get("departures") or []:
148
+ if isinstance(dep, dict):
149
+ events.append({**dep, "_notices": notices or None})
150
+ return events
151
+
152
+ def parse_departure(
153
+ self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
154
+ ) -> Optional[UnifiedDeparture]:
155
+ try:
156
+ times = stop.get("departure_timestamp") or {}
157
+ scheduled = _parse_dt(times.get("scheduled"))
158
+ if scheduled is None:
159
+ return None
160
+ predicted = _parse_dt(times.get("predicted"))
161
+ delay_info = stop.get("delay") or {}
162
+ is_realtime = bool(delay_info.get("is_available"))
163
+
164
+ planned_local = scheduled.astimezone(tz)
165
+ dep_local = predicted.astimezone(tz) if predicted else planned_local
166
+
167
+ if is_realtime and delay_info.get("minutes") is not None:
168
+ delay = int(delay_info["minutes"])
169
+ else:
170
+ delay = round((dep_local - planned_local).total_seconds() / 60)
171
+ minutes_until = max(0, int((dep_local - now).total_seconds() / 60))
172
+
173
+ route = stop.get("route") or {}
174
+ trip = stop.get("trip") or {}
175
+ stop_ref = stop.get("stop") or {}
176
+
177
+ transport_type = ROUTE_TYPES.get(route.get("type"), "unknown")
178
+ notices = list(stop.get("_notices") or [])
179
+ if trip.get("is_canceled"):
180
+ notices.insert(0, "Spoj odřeknut / Trip cancelled")
181
+
182
+ return UnifiedDeparture(
183
+ line=str(route.get("short_name") or ""),
184
+ destination=trip.get("headsign") or "Unknown",
185
+ departure_time=dep_local.strftime("%H:%M"),
186
+ planned_time=planned_local.strftime("%H:%M"),
187
+ delay=delay,
188
+ platform=stop_ref.get("platform_code") or None,
189
+ transportation_type=transport_type,
190
+ is_realtime=is_realtime,
191
+ minutes_until_departure=minutes_until,
192
+ departure_time_obj=dep_local,
193
+ description=trip.get("short_name") or None,
194
+ agency="PID",
195
+ notices=notices or None,
196
+ )
197
+ except Exception as exc: # noqa: BLE001
198
+ _LOGGER.debug("Error parsing Golemio departure: %s", exc)
199
+ return None
200
+
201
+ @staticmethod
202
+ def parse_stop_groups(data: Dict[str, Any]) -> List[Dict[str, str]]:
203
+ """Reduce stops.json to one entry per ASW node."""
204
+ result = []
205
+ seen: set = set()
206
+ for group in data.get("stopGroups") or []:
207
+ if not isinstance(group, dict) or group.get("node") is None:
208
+ continue
209
+ node = str(group["node"])
210
+ if node in seen:
211
+ continue
212
+ seen.add(node)
213
+ result.append(
214
+ {
215
+ "id": node,
216
+ "name": group.get("uniqueName") or group.get("name") or "",
217
+ "place": group.get("municipality") or "",
218
+ }
219
+ )
220
+ return result
221
+
222
+ async def _load_stops(self) -> List[Dict[str, str]]:
223
+ if self._stops is None:
224
+ data = await self._request("get", STOPS_URL, timeout=30, retries=2)
225
+ if not isinstance(data, dict):
226
+ raise ApiResponseError(f"{self.provider_name}: stop list is {type(data).__name__}, expected an object")
227
+ self._stops = self.parse_stop_groups(data)
228
+ return self._stops
229
+
230
+ @staticmethod
231
+ def filter_stops(stops: List[Dict[str, str]], search_term: str, limit: int = 20) -> List[Dict[str, Any]]:
232
+ tokens = _normalize(search_term).replace(",", " ").split()
233
+ if not tokens:
234
+ return []
235
+ term = " ".join(tokens)
236
+ scored = []
237
+ for st in stops:
238
+ if not all(t in _normalize(f"{st['name']} {st['place']}") for t in tokens):
239
+ continue
240
+ name = _normalize(st["name"])
241
+ score = 0 if name == term else 1 if name.startswith(term) else 2
242
+ prague = 0 if st["place"] == "Praha" else 1
243
+ scored.append((score, prague, len(name), st["name"], st))
244
+ scored.sort(key=lambda x: x[:4])
245
+ return [
246
+ {"id": st["id"], "name": st["name"], "place": st["place"], "area_type": "stop"}
247
+ for *_, st in scored[:limit]
248
+ ]
249
+
250
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
251
+ return self.filter_stops(await self._load_stops(), search_term)