python-openpublictransport 0.1.5__tar.gz → 0.1.7__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 (50) hide show
  1. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/PKG-INFO +1 -1
  2. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/pyproject.toml +1 -1
  3. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/const.py +2 -0
  4. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/efa_base.py +27 -1
  5. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/rejseplanen.py +53 -12
  6. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/python_openpublictransport.egg-info/PKG-INFO +1 -1
  7. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/setup.cfg +0 -0
  8. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/__init__.py +0 -0
  9. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/exceptions.py +0 -0
  10. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/models.py +0 -0
  11. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/parsers.py +0 -0
  12. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/__init__.py +0 -0
  13. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/avv.py +0 -0
  14. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/base.py +0 -0
  15. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/beg.py +0 -0
  16. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/bsvg.py +0 -0
  17. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/bvg.py +0 -0
  18. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/db.py +0 -0
  19. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/ding.py +0 -0
  20. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/fptf_base.py +0 -0
  21. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/gtfsde.py +0 -0
  22. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/hvv.py +0 -0
  23. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/kvv.py +0 -0
  24. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/mvv.py +0 -0
  25. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/national_rail.py +0 -0
  26. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/nta.py +0 -0
  27. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/nvbw.py +0 -0
  28. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/nwl.py +0 -0
  29. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/oebb.py +0 -0
  30. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/otp.py +0 -0
  31. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/otp_base.py +0 -0
  32. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/otp_custom.py +0 -0
  33. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/rmv.py +0 -0
  34. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/rvv.py +0 -0
  35. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/sbb.py +0 -0
  36. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/trafiklab.py +0 -0
  37. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/transitous.py +0 -0
  38. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/trias_base.py +0 -0
  39. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vagfr.py +0 -0
  40. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vbn.py +0 -0
  41. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vgn.py +0 -0
  42. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vrn.py +0 -0
  43. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vrr.py +0 -0
  44. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vvo.py +0 -0
  45. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/providers/vvs.py +0 -0
  46. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/openpublictransport/py.typed +0 -0
  47. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/python_openpublictransport.egg-info/SOURCES.txt +0 -0
  48. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/python_openpublictransport.egg-info/dependency_links.txt +0 -0
  49. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/python_openpublictransport.egg-info/requires.txt +0 -0
  50. {python_openpublictransport-0.1.5 → python_openpublictransport-0.1.7}/src/python_openpublictransport.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.1.5
3
+ Version: 0.1.7
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.5"
7
+ version = "0.1.7"
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" }
@@ -44,6 +44,8 @@ KVV_TRANSPORTATION_TYPES = {
44
44
  1: "train",
45
45
  4: "tram",
46
46
  5: "bus",
47
+ 6: "bus", # Regionalbus (e.g. KVV line 106)
48
+ 7: "bus", # Schnellbus
47
49
  }
48
50
 
49
51
  HVV_TRANSPORTATION_TYPES = {
@@ -17,6 +17,28 @@ from .base import BaseProvider
17
17
  _LOGGER = logging.getLogger(__name__)
18
18
 
19
19
 
20
+ def _transport_type_from_name(name: str) -> str:
21
+ """Best-effort transport type from an EFA product name.
22
+
23
+ Used as a fallback when the numeric product class is not in a provider's
24
+ mapping (e.g. KVV Regionalbus class 6), so unmapped classes are not dropped
25
+ as "unknown". Checks subway/tram before the generic "bahn" → train.
26
+ """
27
+ n = (name or "").lower()
28
+ if "u-bahn" in n or "u_bahn" in n or "ubahn" in n or "subway" in n or "metro" in n:
29
+ return "subway"
30
+ if "straßenbahn" in n or "strassenbahn" in n or "stadtbahn" in n or "tram" in n:
31
+ return "tram"
32
+ if "fähre" in n or "faehre" in n or "schiff" in n or "ferry" in n:
33
+ return "ferry"
34
+ if "bus" in n or "ast" in n or "ruf" in n or "ersatz" in n:
35
+ # Stadt-/Regional-/Schnell-/Nachtbus, AST, Rufbus, (Schienen-)Ersatzverkehr
36
+ return "bus"
37
+ if "bahn" in n or "zug" in n or "train" in n: # S-Bahn, Regionalbahn, Zug, …
38
+ return "train"
39
+ return "unknown"
40
+
41
+
20
42
  class EFABaseProvider(BaseProvider):
21
43
  """Base class for all EFA-based providers (VRR, KVV, HVV, MVV, etc.)."""
22
44
 
@@ -122,10 +144,14 @@ class EFABaseProvider(BaseProvider):
122
144
  product = transportation.get("product", {})
123
145
  product_class = product.get("class", 0)
124
146
  transport_type = type_mapping.get(product_class, "unknown")
147
+ if transport_type == "unknown":
148
+ # Fall back to the product name so unmapped classes aren't dropped.
149
+ transport_type = _transport_type_from_name(product.get("name", ""))
125
150
  if transport_type == "unknown":
126
151
  _LOGGER.debug(
127
- "Unknown transport class %s for line %s",
152
+ "Unknown transport class %s / name %r for line %s",
128
153
  product_class,
154
+ product.get("name"),
129
155
  transportation.get("number", "unknown"),
130
156
  )
131
157
  return transport_type
@@ -38,17 +38,49 @@ _PRODUCT_MAPPING: Dict[str, str] = {
38
38
 
39
39
 
40
40
  def _parse_hafas_time(date_str: str, time_str: str, tz: Any) -> Optional[datetime]:
41
- """Parse Rejseplanen date (DD.MM.YY) and time (HH:MM) into a timezone-aware datetime."""
41
+ """Parse a Rejseplanen date + time into a timezone-aware datetime.
42
+
43
+ The HAFAS REST API at www.rejseplanen.dk returns ISO dates (YYYY-MM-DD) and
44
+ times as HH:MM:SS; older HAFAS deployments used DD.MM.YY / HH:MM. Handle both.
45
+ """
42
46
  if not date_str or not time_str:
43
47
  return None
48
+ time_fmt = "%H:%M:%S" if time_str.count(":") == 2 else "%H:%M"
49
+ date_fmt = "%Y-%m-%d" if "-" in date_str else "%d.%m.%y"
44
50
  try:
45
- time_fmt = "%H:%M:%S" if time_str.count(":") == 2 else "%H:%M"
46
- dt = datetime.strptime(f"{date_str} {time_str}", f"%d.%m.%y {time_fmt}")
51
+ dt = datetime.strptime(f"{date_str} {time_str}", f"{date_fmt} {time_fmt}")
47
52
  return dt.replace(tzinfo=tz)
48
53
  except ValueError:
49
54
  return None
50
55
 
51
56
 
57
+ def _resolve_transport_type(stop: Dict[str, Any]) -> str:
58
+ """Derive the transport type from the HAFAS product category.
59
+
60
+ The departure's top-level ``type`` is a stop type (e.g. "ST"), not the
61
+ product, so read the category from ``Product[].catOut`` and fall back to the
62
+ line name (e.g. "Bus 250S", "Metro M4", "Re 1277").
63
+ """
64
+ products = stop.get("Product", [])
65
+ if isinstance(products, dict):
66
+ products = [products]
67
+ cat = ""
68
+ if products and isinstance(products[0], dict):
69
+ p = products[0]
70
+ cat = (p.get("catOut") or p.get("catOutL") or p.get("catIn") or "").strip()
71
+ text = f"{cat} {stop.get('name', '')}".lower()
72
+ if "bus" in text: # Bus, Togbus (rail-replacement), Natbus, Expresbus
73
+ return "bus"
74
+ if "metro" in text:
75
+ return "subway"
76
+ if "letbane" in text or "tram" in text:
77
+ return "tram"
78
+ if "færge" in text or "ferry" in text or "havnebus" in text:
79
+ return "ferry"
80
+ # Everything else (IC, ICL, Lyn, Re, Reg, Tog, S-tog, RJ, EC/ECE, …) is rail.
81
+ return "train"
82
+
83
+
52
84
  class RejseplanenProvider(BaseProvider):
53
85
  """Rejseplanen (Denmark) provider via HAFAS REST API."""
54
86
 
@@ -122,8 +154,11 @@ class RejseplanenProvider(BaseProvider):
122
154
  _LOGGER.warning("%s: API error %s: %s", self.provider_name, data.get("errorCode"), data.get("errorText"))
123
155
  return None
124
156
 
125
- board = data.get("DepartureBoard", {})
126
- departures = board.get("Departure", [])
157
+ # www.rejseplanen.dk returns "Departure" at the top level; older HAFAS
158
+ # deployments nested it under "DepartureBoard". Support both.
159
+ departures = data.get("Departure")
160
+ if departures is None:
161
+ departures = data.get("DepartureBoard", {}).get("Departure", [])
127
162
  if isinstance(departures, dict):
128
163
  departures = [departures]
129
164
 
@@ -156,8 +191,7 @@ class RejseplanenProvider(BaseProvider):
156
191
  delay = max(0, int((actual_dt - planned_dt).total_seconds() / 60))
157
192
  minutes_until = max(0, int((actual_dt - now).total_seconds() / 60))
158
193
 
159
- type_str = stop.get("type", "").upper()
160
- transport_type = _PRODUCT_MAPPING.get(type_str, "train")
194
+ transport_type = _resolve_transport_type(stop)
161
195
 
162
196
  line = stop.get("name", "")
163
197
  destination = stop.get("direction", stop.get("finalStop", ""))
@@ -223,13 +257,20 @@ class RejseplanenProvider(BaseProvider):
223
257
  if not isinstance(data, dict):
224
258
  return []
225
259
 
226
- location_list = data.get("LocationList", {})
227
- stops = location_list.get("StopLocation", [])
228
- if isinstance(stops, dict):
229
- stops = [stops]
260
+ # www.rejseplanen.dk returns "stopLocationOrCoordLocation" (a list whose
261
+ # entries wrap a StopLocation or CoordLocation); older HAFAS deployments
262
+ # used LocationList.StopLocation. Support both.
263
+ entries = data.get("stopLocationOrCoordLocation")
264
+ if entries is None:
265
+ entries = data.get("LocationList", {}).get("StopLocation", [])
266
+ if isinstance(entries, dict):
267
+ entries = [entries]
230
268
 
231
269
  results = []
232
- for loc in stops:
270
+ for entry in entries:
271
+ if not isinstance(entry, dict):
272
+ continue
273
+ loc = entry.get("StopLocation", entry)
233
274
  if not isinstance(loc, dict):
234
275
  continue
235
276
  station_id = loc.get("extId") or loc.get("id", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openpublictransport
3
- Version: 0.1.5
3
+ Version: 0.1.7
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