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.
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/PKG-INFO +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/pyproject.toml +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/const.py +22 -2
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/__init__.py +63 -3
- python_openpublictransport-0.3.0/src/openpublictransport/providers/db.py +69 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/digitransit.py +102 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/efa_base.py +8 -7
- python_openpublictransport-0.3.0/src/openpublictransport/providers/golemio.py +251 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/gvh.py +55 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_mgate_base.py +143 -10
- python_openpublictransport-0.3.0/src/openpublictransport/providers/hongkong.py +364 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/insa.py +29 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/irail.py +210 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/nahsh.py +29 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/nextbus_base.py +260 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/nvv.py +30 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp.py +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_base.py +20 -3
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/otp_custom.py +2 -2
- python_openpublictransport-0.3.0/src/openpublictransport/providers/saarvv.py +29 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/sbb.py +22 -11
- python_openpublictransport-0.3.0/src/openpublictransport/providers/sl.py +219 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/tfl.py +256 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/tfnsw.py +205 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/transitous.py +1 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/trimet.py +22 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/ttc.py +41 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vao.py +34 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vbn_hafas.py +31 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vmt.py +29 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vmv.py +55 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vos.py +29 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrr.py +2 -2
- python_openpublictransport-0.3.0/src/openpublictransport/providers/vsn.py +30 -0
- python_openpublictransport-0.3.0/src/openpublictransport/providers/wienerlinien.py +253 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/SOURCES.txt +35 -2
- python_openpublictransport-0.3.0/tests/test_digitransit.py +73 -0
- python_openpublictransport-0.3.0/tests/test_german_regional_providers.py +204 -0
- python_openpublictransport-0.3.0/tests/test_golemio.py +128 -0
- python_openpublictransport-0.3.0/tests/test_hongkong.py +148 -0
- python_openpublictransport-0.3.0/tests/test_irail.py +131 -0
- python_openpublictransport-0.3.0/tests/test_provider_expansion.py +154 -0
- python_openpublictransport-0.3.0/tests/test_sbb.py +54 -0
- python_openpublictransport-0.3.0/tests/test_sl.py +150 -0
- python_openpublictransport-0.3.0/tests/test_tfl.py +114 -0
- python_openpublictransport-0.3.0/tests/test_tfnsw.py +137 -0
- python_openpublictransport-0.3.0/tests/test_ttc.py +167 -0
- python_openpublictransport-0.3.0/tests/test_wienerlinien.py +160 -0
- python_openpublictransport-0.2.0/src/openpublictransport/providers/db.py +0 -39
- python_openpublictransport-0.2.0/src/openpublictransport/providers/gtfsde.py +0 -21
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/setup.cfg +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/__init__.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/data/uk_stations.json +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/exceptions.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/models.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/parsers.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/avv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bart.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/beg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bsvg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/bvg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/dart.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ding.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/entur.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/fptf_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hafas_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/hvv_gti.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/irishrail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/kvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mobiliteit_lu.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/mvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/national_rail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/ns.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nta.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nvbw.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/nwl.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/oebb.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rejseplanen.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rmv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/rvv.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/tpg.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/trafiklab.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/trias_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vagfr.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vbn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vgn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vrn.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvo.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/providers/vvs.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/openpublictransport/py.typed +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/requires.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_efa_mimetype.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_efa_platform.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_entur.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hafas_base.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hafas_mgate.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_http_errors.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_hvv_gti.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_national_rail.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_national_rail_parse.py +0 -0
- {python_openpublictransport-0.2.0 → python_openpublictransport-0.3.0}/tests/test_national_rail_static.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-openpublictransport"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
#
|
|
222
|
-
#
|
|
223
|
-
|
|
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)
|