python-openpublictransport 0.1.11__tar.gz → 0.1.12__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 (54) hide show
  1. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/PKG-INFO +1 -1
  2. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/pyproject.toml +1 -1
  3. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/national_rail.py +46 -8
  4. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
  5. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/tests/test_national_rail_parse.py +7 -0
  6. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/tests/test_national_rail_static.py +4 -4
  7. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/setup.cfg +0 -0
  8. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/__init__.py +0 -0
  9. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/const.py +0 -0
  10. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/data/uk_stations.json +0 -0
  11. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/exceptions.py +0 -0
  12. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/models.py +0 -0
  13. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/parsers.py +0 -0
  14. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/__init__.py +0 -0
  15. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/avv.py +0 -0
  16. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/base.py +0 -0
  17. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/beg.py +0 -0
  18. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/bsvg.py +0 -0
  19. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/bvg.py +0 -0
  20. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/db.py +0 -0
  21. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/ding.py +0 -0
  22. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/efa_base.py +0 -0
  23. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/fptf_base.py +0 -0
  24. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/gtfsde.py +0 -0
  25. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/hvv.py +0 -0
  26. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/kvv.py +0 -0
  27. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/mvv.py +0 -0
  28. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/nta.py +0 -0
  29. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/nvbw.py +0 -0
  30. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/nwl.py +0 -0
  31. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/oebb.py +0 -0
  32. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/otp.py +0 -0
  33. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/otp_base.py +0 -0
  34. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/otp_custom.py +0 -0
  35. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/rejseplanen.py +0 -0
  36. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/rmv.py +0 -0
  37. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/rvv.py +0 -0
  38. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/sbb.py +0 -0
  39. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/trafiklab.py +0 -0
  40. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/transitous.py +0 -0
  41. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/trias_base.py +0 -0
  42. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vagfr.py +0 -0
  43. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vbn.py +0 -0
  44. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vgn.py +0 -0
  45. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vrn.py +0 -0
  46. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vrr.py +0 -0
  47. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vvo.py +0 -0
  48. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/providers/vvs.py +0 -0
  49. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/openpublictransport/py.typed +0 -0
  50. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/python_openpublictransport.egg-info/SOURCES.txt +0 -0
  51. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
  52. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/python_openpublictransport.egg-info/requires.txt +0 -0
  53. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
  54. {python_openpublictransport-0.1.11 → python_openpublictransport-0.1.12}/tests/test_national_rail.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.1.11
3
+ Version: 0.1.12
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.11"
7
+ version = "0.1.12"
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" }
@@ -4,6 +4,7 @@ Stop search uses the Overpass API (OSM) to find UK railway stations with
4
4
  ref:crs tags, returning the 3-letter CRS code used by OpenLDBWS.
5
5
  """
6
6
 
7
+ import asyncio
7
8
  import json
8
9
  import logging
9
10
  import re
@@ -22,8 +23,16 @@ from .base import BaseProvider
22
23
 
23
24
  _LOGGER = logging.getLogger(__name__)
24
25
 
25
- _ENDPOINT = "https://lite.realtime.nationalrail.co.uk/OpenLDBWS/ldb11.asmx"
26
- _SOAP_ACTION = "http://thalesgroup.com/RTTI/2017-10-01/ldb/GetDepartureBoard"
26
+ # Use the same LDBWS version triple as the reference OpenLDBWS clients:
27
+ # endpoint = ldb12.asmx (WSDL ver=2021-11-01)
28
+ # body ns = 2021-11-01/ldb/
29
+ # SOAPAction= 2012-01-13/ldb/GetDepartureBoard
30
+ # The SOAPAction is version-pinned to 2012-01-13 (it does NOT track the body's
31
+ # ldb version); sending a mismatched SOAPAction makes the ASMX service reject the
32
+ # request with an HTTP 500 SOAP fault ("did not recognize the value of the
33
+ # SOAPAction header").
34
+ _ENDPOINT = "https://lite.realtime.nationalrail.co.uk/OpenLDBWS/ldb12.asmx"
35
+ _SOAP_ACTION = "http://thalesgroup.com/RTTI/2012-01-13/ldb/GetDepartureBoard"
27
36
  _OVERPASS_URL = "https://overpass-api.de/api/interpreter"
28
37
 
29
38
  # UK bounding box (lat_min, lon_min, lat_max, lon_max)
@@ -31,7 +40,7 @@ _UK_BBOX = "49,-11,62,2"
31
40
 
32
41
  _SOAP_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
33
42
  <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
34
- xmlns:ldb="http://thalesgroup.com/RTTI/2017-10-01/ldb/"
43
+ xmlns:ldb="http://thalesgroup.com/RTTI/2021-11-01/ldb/"
35
44
  xmlns:tok="http://thalesgroup.com/RTTI/2013-11-28/Token/types">
36
45
  <soap:Header>
37
46
  <tok:AccessToken>
@@ -81,6 +90,16 @@ def _static_result(station: Dict[str, Any]) -> Dict[str, Any]:
81
90
  }
82
91
 
83
92
 
93
+ def _soap_fault_string(body: str) -> str:
94
+ """Best-effort extraction of a SOAP fault reason from an error response."""
95
+ if not body:
96
+ return ""
97
+ m = re.search(r"<(?:\w+:)?(?:faultstring|Text|Reason)[^>]*>(.*?)</", body, re.DOTALL | re.IGNORECASE)
98
+ if m:
99
+ return m.group(1).strip()[:200]
100
+ return body.strip()[:200]
101
+
102
+
84
103
  def _strip_namespaces(xml_string: str) -> str:
85
104
  """Remove XML namespace prefixes and declarations for simpler parsing.
86
105
 
@@ -167,7 +186,21 @@ class NationalRailProvider(BaseProvider):
167
186
  f"{self.provider_name}: authentication failed (HTTP {resp.status}) — check API key"
168
187
  )
169
188
  if resp.status != 200:
170
- _LOGGER.warning("%s: HTTP %s for CRS %s", self.provider_name, resp.status, crs)
189
+ # Surface the SOAP fault reason (HTTP 500) instead of hiding it,
190
+ # so genuine request/token faults are diagnosable.
191
+ body = ""
192
+ try:
193
+ body = await resp.text()
194
+ except Exception: # pragma: no cover - defensive
195
+ pass
196
+ fault = _soap_fault_string(body)
197
+ _LOGGER.warning(
198
+ "%s: HTTP %s for CRS %s%s",
199
+ self.provider_name,
200
+ resp.status,
201
+ crs,
202
+ f" — {fault}" if fault else "",
203
+ )
171
204
  return None
172
205
  text = await resp.text()
173
206
  except Exception as exc:
@@ -340,7 +373,7 @@ class NationalRailProvider(BaseProvider):
340
373
  return results
341
374
 
342
375
  # Overpass failed (unreachable / throttled) → use the offline snapshot.
343
- fallback = self._search_static(term)
376
+ fallback = await self._search_static(term)
344
377
  if fallback:
345
378
  _LOGGER.info(
346
379
  "%s: Overpass unavailable for %r — served %d result(s) from the "
@@ -351,9 +384,14 @@ class NationalRailProvider(BaseProvider):
351
384
  )
352
385
  return fallback
353
386
 
354
- def _search_static(self, term: str) -> List[Dict[str, Any]]:
355
- """Match ``term`` against the bundled offline station snapshot."""
356
- stations = _load_static_stations()
387
+ async def _search_static(self, term: str) -> List[Dict[str, Any]]:
388
+ """Match ``term`` against the bundled offline station snapshot.
389
+
390
+ The snapshot is read from disk on first use; that read is dispatched to
391
+ an executor so it never blocks the event loop (results are cached, so
392
+ subsequent calls do no I/O).
393
+ """
394
+ stations = await asyncio.get_running_loop().run_in_executor(None, _load_static_stations)
357
395
  if not stations:
358
396
  return []
359
397
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.1.11
3
+ Version: 0.1.12
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
@@ -55,6 +55,13 @@ SAMPLE = """<?xml version="1.0" encoding="utf-8"?>
55
55
  </soap:Envelope>"""
56
56
 
57
57
 
58
+ def test_ldbws_request_uses_consistent_version_triple():
59
+ """SOAPAction must be the 2012-01-13 value; a mismatch yields HTTP 500."""
60
+ assert nr._ENDPOINT.endswith("ldb12.asmx")
61
+ assert nr._SOAP_ACTION == "http://thalesgroup.com/RTTI/2012-01-13/ldb/GetDepartureBoard"
62
+ assert 'xmlns:ldb="http://thalesgroup.com/RTTI/2021-11-01/ldb/"' in nr._SOAP_TEMPLATE
63
+
64
+
58
65
  def test_strip_namespaces_handles_digit_prefixes():
59
66
  """Digit-bearing prefixes must be stripped so the XML parses."""
60
67
  stripped = nr._strip_namespaces(SAMPLE)
@@ -53,17 +53,17 @@ def test_snapshot_loads_and_has_known_stations():
53
53
  assert by_crs["RDG"]["name"] == "Reading"
54
54
 
55
55
 
56
- def test_static_search_by_crs_code():
56
+ async def test_static_search_by_crs_code():
57
57
  provider = NationalRailProvider(_Session("down"))
58
- results = provider._search_static("rdg")
58
+ results = await provider._search_static("rdg")
59
59
  assert results == [
60
60
  {"id": "RDG", "name": "Reading", "place": "Network Rail", "area_type": "stop"}
61
61
  ]
62
62
 
63
63
 
64
- def test_static_search_by_name():
64
+ async def test_static_search_by_name():
65
65
  provider = NationalRailProvider(_Session("down"))
66
- results = provider._search_static("winchester")
66
+ results = await provider._search_static("winchester")
67
67
  assert any(r["id"] == "WIN" and r["name"] == "Winchester" for r in results)
68
68
 
69
69