python-openpublictransport 0.3.1__tar.gz → 0.3.3__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 (108) hide show
  1. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/PKG-INFO +1 -1
  2. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/pyproject.toml +1 -1
  3. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/models.py +7 -0
  4. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/parsers.py +12 -0
  5. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/hafas_mgate_base.py +7 -3
  6. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
  7. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/python_openpublictransport.egg-info/SOURCES.txt +1 -0
  8. python_openpublictransport-0.3.3/tests/test_disruptions.py +101 -0
  9. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_german_regional_providers.py +19 -0
  10. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/setup.cfg +0 -0
  11. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/__init__.py +0 -0
  12. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/const.py +0 -0
  13. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/data/uk_stations.json +0 -0
  14. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/exceptions.py +0 -0
  15. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/__init__.py +0 -0
  16. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/avv.py +0 -0
  17. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/bart.py +0 -0
  18. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/base.py +0 -0
  19. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/beg.py +0 -0
  20. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/bsvg.py +0 -0
  21. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/bvg.py +0 -0
  22. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/cta.py +0 -0
  23. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/dart.py +0 -0
  24. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/db.py +0 -0
  25. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/digitransit.py +0 -0
  26. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/ding.py +0 -0
  27. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/efa_base.py +0 -0
  28. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/entur.py +0 -0
  29. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/fptf_base.py +0 -0
  30. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/golemio.py +0 -0
  31. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/gvh.py +0 -0
  32. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/hafas_base.py +0 -0
  33. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/hongkong.py +0 -0
  34. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/hvv.py +0 -0
  35. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/hvv_gti.py +0 -0
  36. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/insa.py +0 -0
  37. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/irail.py +0 -0
  38. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/irishrail.py +0 -0
  39. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/kvv.py +0 -0
  40. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/mobiliteit_lu.py +0 -0
  41. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/mvv.py +0 -0
  42. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nahsh.py +0 -0
  43. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/national_rail.py +0 -0
  44. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nextbus_base.py +0 -0
  45. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/ns.py +0 -0
  46. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nta.py +0 -0
  47. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nvbw.py +0 -0
  48. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nvv.py +0 -0
  49. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/nwl.py +0 -0
  50. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/oebb.py +0 -0
  51. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/otp.py +0 -0
  52. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/otp_base.py +0 -0
  53. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/otp_custom.py +0 -0
  54. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/rejseplanen.py +0 -0
  55. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/rmv.py +0 -0
  56. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/rvv.py +0 -0
  57. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/saarvv.py +0 -0
  58. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/sbb.py +0 -0
  59. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/sl.py +0 -0
  60. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/tfl.py +0 -0
  61. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/tfnsw.py +0 -0
  62. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/tpg.py +0 -0
  63. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/trafiklab.py +0 -0
  64. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/transitous.py +0 -0
  65. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/trias_base.py +0 -0
  66. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/trimet.py +0 -0
  67. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/ttc.py +0 -0
  68. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vagfr.py +0 -0
  69. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vao.py +0 -0
  70. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vbn.py +0 -0
  71. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vbn_hafas.py +0 -0
  72. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vgn.py +0 -0
  73. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vmt.py +0 -0
  74. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vmv.py +0 -0
  75. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vos.py +0 -0
  76. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vrn.py +0 -0
  77. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vrr.py +0 -0
  78. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vsn.py +0 -0
  79. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vvo.py +0 -0
  80. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/vvs.py +0 -0
  81. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/providers/wienerlinien.py +0 -0
  82. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/openpublictransport/py.typed +0 -0
  83. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
  84. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/python_openpublictransport.egg-info/requires.txt +0 -0
  85. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
  86. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_cta.py +0 -0
  87. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_digitransit.py +0 -0
  88. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_efa_mimetype.py +0 -0
  89. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_efa_platform.py +0 -0
  90. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_entur.py +0 -0
  91. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_golemio.py +0 -0
  92. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_hafas_base.py +0 -0
  93. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_hafas_mgate.py +0 -0
  94. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_hongkong.py +0 -0
  95. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_http_errors.py +0 -0
  96. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_hvv_gti.py +0 -0
  97. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_irail.py +0 -0
  98. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_issue_fixes_0_3_1.py +0 -0
  99. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_national_rail.py +0 -0
  100. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_national_rail_parse.py +0 -0
  101. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_national_rail_static.py +0 -0
  102. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_provider_expansion.py +0 -0
  103. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_sbb.py +0 -0
  104. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_sl.py +0 -0
  105. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_tfl.py +0 -0
  106. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_tfnsw.py +0 -0
  107. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_ttc.py +0 -0
  108. {python_openpublictransport-0.3.1 → python_openpublictransport-0.3.3}/tests/test_wienerlinien.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.3.1
3
+ Version: 0.3.3
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.3.1"
7
+ version = "0.3.3"
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" }
@@ -36,6 +36,10 @@ class UnifiedDeparture:
36
36
  description: Optional[str] = None
37
37
  agency: Optional[str] = None
38
38
  notices: Optional[list[str]] = None
39
+ # The subset of ``notices`` that reports an actual disruption, as opposed
40
+ # to routine information such as bicycle rules or on-board facilities.
41
+ # None means the provider does not tell them apart: every notice counts.
42
+ disruptions: Optional[list[str]] = None
39
43
  planned_platform: Optional[str] = None
40
44
  platform_changed: bool = False
41
45
  # Human-readable form of `platform` when the provider offers one
@@ -63,6 +67,9 @@ class UnifiedDeparture:
63
67
  result["agency"] = self.agency
64
68
  if self.notices:
65
69
  result["notices"] = self.notices
70
+ # Always present next to the notices, even when empty, so consumers
71
+ # can tell "no disruption" apart from an older payload without it.
72
+ result["disruptions"] = self.notices if self.disruptions is None else self.disruptions
66
73
  if self.platform_name and self.platform_name != self.platform:
67
74
  result["platform_name"] = self.platform_name
68
75
  if self.platform_changed:
@@ -34,6 +34,13 @@ def rail_line_label(number: str, name: str) -> str:
34
34
  return number
35
35
 
36
36
 
37
+ # EFA hint types that carry routine timetable or vehicle information ("bicycles
38
+ # limited", "wheelchair accessible", "on-board restaurant") rather than a
39
+ # disruption. Infos (lineInfo, stopBlocking, …) and real-time hints such as
40
+ # RTIncidentCall ("delay of a preceding train") are disruptions.
41
+ EFA_ROUTINE_HINT_TYPES = frozenset({"Timetable", "VehicleType"})
42
+
43
+
37
44
  # Human-readable platform labels APIs prefix onto the bare track identifier:
38
45
  # "Gleis 3", "Bstg. 12", "Steig B", "Platform 4", "Pl. 2", "Track 1".
39
46
  _PLATFORM_LABEL_RE = re.compile(
@@ -140,16 +147,20 @@ def parse_departure_generic(
140
147
  is_realtime = get_realtime_fn(stop, estimated_time_str, planned_time_str)
141
148
 
142
149
  notices = []
150
+ disruptions = []
143
151
  for info in stop.get("infos", []):
144
152
  if isinstance(info, dict):
145
153
  text = info.get("subtitle") or info.get("title") or info.get("content", "")
146
154
  if text and isinstance(text, str):
147
155
  notices.append(text.strip())
156
+ disruptions.append(text.strip())
148
157
  for hint in stop.get("hints", []):
149
158
  if isinstance(hint, dict):
150
159
  text = hint.get("content") or hint.get("text", "")
151
160
  if text and isinstance(text, str):
152
161
  notices.append(text.strip())
162
+ if hint.get("type") not in EFA_ROUTINE_HINT_TYPES:
163
+ disruptions.append(text.strip())
153
164
 
154
165
  planned_fn = get_planned_platform_fn or _default_planned_platform_fn
155
166
  planned_platform = planned_fn(stop)
@@ -174,6 +185,7 @@ def parse_departure_generic(
174
185
  description=description if description else None,
175
186
  agency=agency if agency else None,
176
187
  notices=notices if notices else None,
188
+ disruptions=disruptions if notices else None,
177
189
  planned_platform=str(planned_platform).strip() if planned_platform and platform_changed else None,
178
190
  platform_changed=platform_changed,
179
191
  platform_name=str(platform_name).strip() or None,
@@ -327,11 +327,15 @@ class HafasMgateBaseProvider(BaseProvider):
327
327
  destination_id: str,
328
328
  departure_time: Optional[datetime] = None,
329
329
  results: int = 4,
330
+ arrival_time: Optional[datetime] = None,
330
331
  ) -> List[Dict[str, Any]]:
331
332
  """Plan journeys between two stops with the mgate ``TripSearch`` service.
332
333
 
333
334
  ``departure_time`` is converted to the network's time zone; without it
334
- the search starts now. Returns one dict per connection::
335
+ the search starts now. ``arrival_time`` switches to an "arrive by"
336
+ search instead (like hafas-client's ``arrival`` option, ``outFrwd``
337
+ false): the connections that arrive by then. It wins over
338
+ ``departure_time``. Returns one dict per connection::
335
339
 
336
340
  {"duration_minutes": int, "transfers": int, "legs": [leg, ...]}
337
341
 
@@ -342,7 +346,7 @@ class HafasMgateBaseProvider(BaseProvider):
342
346
  (the unprefixed ones are real-time when the network reports it).
343
347
  """
344
348
  tz = ZoneInfo(self.timezone)
345
- when = (departure_time or datetime.now(tz)).astimezone(tz)
349
+ when = (arrival_time or departure_time or datetime.now(tz)).astimezone(tz)
346
350
  svc = {
347
351
  "meth": "TripSearch",
348
352
  "req": {
@@ -350,7 +354,7 @@ class HafasMgateBaseProvider(BaseProvider):
350
354
  "arrLocL": [self._location(destination_id)],
351
355
  "outDate": when.strftime("%Y%m%d"),
352
356
  "outTime": when.strftime("%H%M%S"),
353
- "outFrwd": True,
357
+ "outFrwd": arrival_time is None,
354
358
  "numF": results,
355
359
  "getPasslist": False,
356
360
  "getPolyline": False,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -81,6 +81,7 @@ src/python_openpublictransport.egg-info/requires.txt
81
81
  src/python_openpublictransport.egg-info/top_level.txt
82
82
  tests/test_cta.py
83
83
  tests/test_digitransit.py
84
+ tests/test_disruptions.py
84
85
  tests/test_efa_mimetype.py
85
86
  tests/test_efa_platform.py
86
87
  tests/test_entur.py
@@ -0,0 +1,101 @@
1
+ """Tests for telling disruptions apart from routine notices.
2
+
3
+ EFA mixes routine timetable and vehicle information ("bicycles limited",
4
+ "wheelchair accessible vehicle", "on-board restaurant") with real disruptions
5
+ in the same ``hints`` list, so everything ended up in a red warning banner.
6
+ See https://github.com/NerdySoftPaw/openpublictransport-card/issues/15.
7
+ """
8
+
9
+ from datetime import datetime
10
+ from zoneinfo import ZoneInfo
11
+
12
+ from openpublictransport.models import UnifiedDeparture
13
+ from openpublictransport.providers.gvh import GVHProvider
14
+
15
+ TZ = ZoneInfo("Europe/Berlin")
16
+ NOW = datetime(2026, 10, 16, 20, 0, tzinfo=TZ)
17
+
18
+
19
+ def _stop_event(hints=None, infos=None) -> dict:
20
+ """A GVH departure from Hannover Hbf as the live API returns it."""
21
+ event = {
22
+ "location": {"id": "de:03241:31", "name": "Hauptbahnhof", "properties": {"platform": "8"}},
23
+ "departureTimePlanned": "2026-10-16T18:10:00Z",
24
+ "departureTimeEstimated": "2026-10-16T18:12:00Z",
25
+ "transportation": {
26
+ "name": "ICE 550 InterCityExpress",
27
+ "number": "550",
28
+ "destination": {"name": "Köln Hbf"},
29
+ "product": {"class": 0, "name": "ICE"},
30
+ },
31
+ }
32
+ if hints is not None:
33
+ event["hints"] = hints
34
+ if infos is not None:
35
+ event["infos"] = infos
36
+ return event
37
+
38
+
39
+ def test_routine_hints_are_notices_but_not_disruptions():
40
+ """Timetable and VehicleType hints stay notices without becoming disruptions."""
41
+ event = _stop_event(hints=[
42
+ {"content": "behindertengerechtes Fahrzeug", "providerCode": "13", "type": "VehicleType"},
43
+ {"content": "Bordrestaurant", "providerCode": "BR", "type": "Timetable"},
44
+ ])
45
+ dep = GVHProvider(None).parse_departure(event, TZ, NOW)
46
+
47
+ assert dep.notices == ["behindertengerechtes Fahrzeug", "Bordrestaurant"]
48
+ assert dep.disruptions == []
49
+ result = dep.to_dict()
50
+ assert result["notices"] == ["behindertengerechtes Fahrzeug", "Bordrestaurant"]
51
+ assert result["disruptions"] == []
52
+
53
+
54
+ def test_realtime_hints_and_infos_are_disruptions():
55
+ """Real-time incident hints and infos are disruptions; routine hints are not."""
56
+ event = _stop_event(
57
+ hints=[
58
+ {"content": "Bordrestaurant", "providerCode": "BR", "type": "Timetable"},
59
+ {"content": "Technische Störung am Zug", "providerCode": "x", "type": "RTIncidentCall"},
60
+ ],
61
+ infos=[{"type": "lineInfo", "priority": "normal", "title": "Bauarbeiten S1, S2, S5"}],
62
+ )
63
+ dep = GVHProvider(None).parse_departure(event, TZ, NOW)
64
+
65
+ assert dep.notices == ["Bauarbeiten S1, S2, S5", "Bordrestaurant", "Technische Störung am Zug"]
66
+ assert dep.disruptions == ["Bauarbeiten S1, S2, S5", "Technische Störung am Zug"]
67
+
68
+
69
+ def test_hint_without_type_is_a_disruption():
70
+ """An unknown or missing hint type is kept as a disruption, never hidden."""
71
+ dep = GVHProvider(None).parse_departure(_stop_event(hints=[{"content": "Ersatzverkehr"}]), TZ, NOW)
72
+
73
+ assert dep.disruptions == ["Ersatzverkehr"]
74
+
75
+
76
+ def test_no_notices_no_keys():
77
+ """Without notices, neither key appears in the attributes."""
78
+ dep = GVHProvider(None).parse_departure(_stop_event(), TZ, NOW)
79
+
80
+ assert dep.notices is None and dep.disruptions is None
81
+ assert "notices" not in dep.to_dict()
82
+ assert "disruptions" not in dep.to_dict()
83
+
84
+
85
+ def test_unclassified_notices_all_count_as_disruptions():
86
+ """Providers that do not classify their notices report all of them as disruptions."""
87
+ dep = UnifiedDeparture(
88
+ line="U1",
89
+ destination="Somewhere",
90
+ departure_time="20:10",
91
+ planned_time="20:10",
92
+ delay=0,
93
+ platform=None,
94
+ transportation_type="subway",
95
+ is_realtime=True,
96
+ minutes_until_departure=10,
97
+ departure_time_obj=NOW,
98
+ notices=["Cancelled"],
99
+ )
100
+
101
+ assert dep.to_dict()["disruptions"] == ["Cancelled"]
@@ -202,3 +202,22 @@ async def test_search_trips_with_extid_and_empty_result():
202
202
  def test_german_category_labels(cat_out, cat_out_l, expected):
203
203
  prod = {"prodCtx": {"catOut": cat_out, "catOutL": cat_out_l}}
204
204
  assert VMTProvider(session=None)._transport_type(prod) == expected
205
+
206
+
207
+ async def test_search_trips_arrive_by():
208
+ provider = NAHSHProvider(session=None)
209
+ provider._request = AsyncMock(return_value={"svcResL": [{"res": {}}]})
210
+
211
+ await provider.search_trips("A=1@L=1@", "A=1@L=2@", arrival_time=datetime(2026, 10, 17, 6, 0, tzinfo=ZoneInfo("UTC")))
212
+
213
+ req = json.loads(provider._request.call_args.kwargs["data"])["svcReqL"][0]["req"]
214
+ assert req["outFrwd"] is False
215
+ # 06:00 UTC is 08:00 in Berlin
216
+ assert (req["outDate"], req["outTime"]) == ("20261017", "080000")
217
+
218
+
219
+ async def test_search_trips_departure_stays_forward():
220
+ provider = NAHSHProvider(session=None)
221
+ provider._request = AsyncMock(return_value={"svcResL": [{"res": {}}]})
222
+ await provider.search_trips("A=1@L=1@", "A=1@L=2@")
223
+ assert json.loads(provider._request.call_args.kwargs["data"])["svcReqL"][0]["req"]["outFrwd"] is True