🎨 Reorganize back-end code
This commit is contained in:
67
backend/api/idfm_interface/__init__.py
Normal file
67
backend/api/idfm_interface/__init__.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from .idfm_types import (
|
||||
Coordinate,
|
||||
Destinations,
|
||||
FramedVehicleJourney,
|
||||
IdfmLineState,
|
||||
IdfmOperator,
|
||||
IdfmResponse,
|
||||
IdfmState,
|
||||
LinePicto,
|
||||
LineFields,
|
||||
Line,
|
||||
MonitoredCall,
|
||||
MonitoredVehicleJourney,
|
||||
Point,
|
||||
Siri,
|
||||
ServiceDelivery,
|
||||
Stop,
|
||||
StopArea,
|
||||
StopAreaFields,
|
||||
StopAreaStopAssociation,
|
||||
StopAreaStopAssociationFields,
|
||||
StopAreaType,
|
||||
StopDelivery,
|
||||
StopFields,
|
||||
StopLineAsso,
|
||||
StopLineAssoFields,
|
||||
StopMonitoringDelivery,
|
||||
TrainNumber,
|
||||
TrainStatus,
|
||||
TransportMode,
|
||||
TransportSubMode,
|
||||
Value,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Coordinate",
|
||||
"Destinations",
|
||||
"FramedVehicleJourney",
|
||||
"IdfmLineState",
|
||||
"IdfmOperator",
|
||||
"IdfmResponse",
|
||||
"IdfmState",
|
||||
"LinePicto",
|
||||
"LineFields",
|
||||
"Line",
|
||||
"MonitoredCall",
|
||||
"MonitoredVehicleJourney",
|
||||
"Point",
|
||||
"Siri",
|
||||
"ServiceDelivery",
|
||||
"Stop",
|
||||
"StopArea",
|
||||
"StopAreaFields",
|
||||
"StopAreaStopAssociation",
|
||||
"StopAreaStopAssociationFields",
|
||||
"StopAreaType",
|
||||
"StopDelivery",
|
||||
"StopFields",
|
||||
"StopLineAsso",
|
||||
"StopLineAssoFields",
|
||||
"StopMonitoringDelivery",
|
||||
"TrainNumber",
|
||||
"TrainStatus",
|
||||
"TransportMode",
|
||||
"TransportSubMode",
|
||||
"Value",
|
||||
]
|
115
backend/api/idfm_interface/idfm_interface.py
Normal file
115
backend/api/idfm_interface/idfm_interface.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from collections import defaultdict
|
||||
from re import compile as re_compile
|
||||
from typing import ByteString
|
||||
|
||||
from aiofiles import open as async_open
|
||||
from aiohttp import ClientSession
|
||||
from msgspec import ValidationError
|
||||
from msgspec.json import Decoder
|
||||
|
||||
from .idfm_types import Destinations as IdfmDestinations, IdfmResponse, IdfmState
|
||||
from db import Database
|
||||
from models import Line, Stop, StopArea
|
||||
|
||||
|
||||
class IdfmInterface:
|
||||
|
||||
IDFM_ROOT_URL = "https://prim.iledefrance-mobilites.fr/marketplace"
|
||||
IDFM_STOP_MON_URL = f"{IDFM_ROOT_URL}/stop-monitoring"
|
||||
|
||||
OPERATOR_RE = re_compile(r"[^:]+:Operator::([^:]+):")
|
||||
LINE_RE = re_compile(r"[^:]+:Line::C([^:]+):")
|
||||
|
||||
def __init__(self, api_key: str, database: Database) -> None:
|
||||
self._api_key = api_key
|
||||
self._database = database
|
||||
|
||||
self._http_headers = {"Accept": "application/json", "apikey": self._api_key}
|
||||
|
||||
self._response_json_decoder = Decoder(type=IdfmResponse)
|
||||
|
||||
async def startup(self) -> None:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _format_line_id(line_id: str) -> int:
|
||||
return int(line_id[1:] if line_id[0] == "C" else line_id)
|
||||
|
||||
async def render_line_picto(self, line: Line) -> tuple[None | str, None | str]:
|
||||
line_picto_path = line_picto_format = None
|
||||
target = f"/tmp/{line.id}_repr"
|
||||
|
||||
picto = line.picto
|
||||
if picto is not None:
|
||||
if (picto_data := await self._get_line_picto(line)) is not None:
|
||||
async with async_open(target, "wb") as fd:
|
||||
await fd.write(bytes(picto_data))
|
||||
line_picto_path = target
|
||||
line_picto_format = picto.mime_type
|
||||
|
||||
return (line_picto_path, line_picto_format)
|
||||
|
||||
async def _get_line_picto(self, line: Line) -> ByteString | None:
|
||||
data = None
|
||||
|
||||
picto = line.picto
|
||||
if picto is not None and picto.url is not None:
|
||||
headers = (
|
||||
self._http_headers if picto.url.startswith(self.IDFM_ROOT_URL) else None
|
||||
)
|
||||
|
||||
async with ClientSession(headers=headers) as session:
|
||||
async with session.get(picto.url) as response:
|
||||
data = await response.read()
|
||||
|
||||
return data
|
||||
|
||||
async def get_next_passages(self, stop_point_id: int) -> IdfmResponse | None:
|
||||
ret = None
|
||||
params = {"MonitoringRef": f"STIF:StopPoint:Q:{stop_point_id}:"}
|
||||
|
||||
async with ClientSession(headers=self._http_headers) as session:
|
||||
async with session.get(self.IDFM_STOP_MON_URL, params=params) as response:
|
||||
|
||||
if response.status == 200:
|
||||
data = await response.read()
|
||||
try:
|
||||
ret = self._response_json_decoder.decode(data)
|
||||
except ValidationError as err:
|
||||
print(err)
|
||||
|
||||
return ret
|
||||
|
||||
async def get_destinations(self, stop_id: int) -> IdfmDestinations | None:
|
||||
destinations: IdfmDestinations = defaultdict(set)
|
||||
|
||||
if (stop := await Stop.get_by_id(stop_id)) is not None:
|
||||
expected_stop_ids = {stop.id}
|
||||
elif (stop_area := await StopArea.get_by_id(stop_id)) is not None:
|
||||
expected_stop_ids = {stop.id for stop in stop_area.stops}
|
||||
else:
|
||||
return None
|
||||
|
||||
if (res := await self.get_next_passages(stop_id)) is not None:
|
||||
|
||||
for delivery in res.Siri.ServiceDelivery.StopMonitoringDelivery:
|
||||
if delivery.Status == IdfmState.true:
|
||||
for stop_visit in delivery.MonitoredStopVisit:
|
||||
|
||||
monitoring_ref = stop_visit.MonitoringRef.value
|
||||
|
||||
try:
|
||||
monitored_stop_id = int(monitoring_ref.split(":")[-2])
|
||||
except (IndexError, ValueError):
|
||||
print(f"Unable to get stop id from {monitoring_ref}")
|
||||
continue
|
||||
|
||||
journey = stop_visit.MonitoredVehicleJourney
|
||||
if (
|
||||
dst_names := journey.DestinationName
|
||||
) and monitored_stop_id in expected_stop_ids:
|
||||
raw_line_id = journey.LineRef.value.split(":")[-2]
|
||||
line_id = IdfmInterface._format_line_id(raw_line_id)
|
||||
destinations[line_id].add(dst_names[0].value)
|
||||
|
||||
return destinations
|
300
backend/api/idfm_interface/idfm_types.py
Normal file
300
backend/api/idfm_interface/idfm_types.py
Normal file
@@ -0,0 +1,300 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from msgspec import Struct
|
||||
|
||||
|
||||
class Coordinate(NamedTuple):
|
||||
lat: float
|
||||
lon: float
|
||||
|
||||
|
||||
class IdfmState(Enum):
|
||||
unknown = "unknown"
|
||||
false = "false"
|
||||
partial = "partial"
|
||||
true = "true"
|
||||
|
||||
|
||||
class TrainStatus(Enum):
|
||||
unknown = ""
|
||||
arrived = "arrived"
|
||||
onTime = "onTime"
|
||||
delayed = "delayed"
|
||||
noReport = "noReport"
|
||||
early = "early"
|
||||
cancelled = "cancelled"
|
||||
undefined = "undefined"
|
||||
|
||||
|
||||
class TransportMode(StrEnum):
|
||||
bus = "bus"
|
||||
tram = "tram"
|
||||
metro = "metro"
|
||||
rail = "rail"
|
||||
funicular = "funicular"
|
||||
|
||||
|
||||
class TransportSubMode(Enum):
|
||||
unknown = "unknown"
|
||||
|
||||
localBus = "localBus"
|
||||
regionalBus = "regionalBus"
|
||||
highFrequencyBus = "highFrequencyBus"
|
||||
expressBus = "expressBus"
|
||||
nightBus = "nightBus"
|
||||
demandAndResponseBus = "demandAndResponseBus"
|
||||
airportLinkBus = "airportLinkBus"
|
||||
|
||||
regionalRail = "regionalRail"
|
||||
railShuttle = "railShuttle"
|
||||
suburbanRailway = "suburbanRailway"
|
||||
|
||||
local = "local"
|
||||
|
||||
|
||||
class StopFields(Struct, kw_only=True):
|
||||
arrgeopoint: Coordinate
|
||||
arrtown: str
|
||||
arrcreated: None | datetime = None
|
||||
arryepsg2154: int
|
||||
arrpostalregion: str
|
||||
arrid: str
|
||||
arrxepsg2154: int
|
||||
arraccessibility: IdfmState
|
||||
arrvisualsigns: IdfmState
|
||||
arrtype: TransportMode
|
||||
arrname: str
|
||||
arrversion: str
|
||||
arrchanged: datetime
|
||||
arraudiblesignals: IdfmState
|
||||
|
||||
|
||||
class Point(Struct):
|
||||
coordinates: Coordinate
|
||||
|
||||
|
||||
class Stop(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: StopFields
|
||||
record_timestamp: datetime
|
||||
# geometry: Union[Point]
|
||||
|
||||
|
||||
Stops = dict[str, Stop]
|
||||
|
||||
|
||||
class StopAreaType(StrEnum):
|
||||
metroStation = "metroStation"
|
||||
onstreetBus = "onstreetBus"
|
||||
onstreetTram = "onstreetTram"
|
||||
railStation = "railStation"
|
||||
|
||||
|
||||
class StopAreaFields(Struct, kw_only=True):
|
||||
zdaname: str
|
||||
zdcid: str
|
||||
zdatown: str
|
||||
zdaversion: str
|
||||
zdaid: str
|
||||
zdacreated: datetime | None = None
|
||||
zdatype: StopAreaType
|
||||
zdayepsg2154: int
|
||||
zdapostalregion: str
|
||||
zdachanged: datetime
|
||||
zdaxepsg2154: int
|
||||
|
||||
|
||||
class StopArea(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: StopAreaFields
|
||||
record_timestamp: datetime
|
||||
|
||||
|
||||
class ConnectionAreaFields(Struct, kw_only=True):
|
||||
zdcid: str
|
||||
zdcversion: str
|
||||
zdccreated: datetime
|
||||
zdcchanged: datetime
|
||||
zdcname: str
|
||||
zdcxepsg2154: int | None = None
|
||||
zdcyepsg2154: int | None = None
|
||||
zdctown: str
|
||||
zdcpostalregion: str
|
||||
zdctype: StopAreaType
|
||||
|
||||
|
||||
class ConnectionArea(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: ConnectionAreaFields
|
||||
record_timestamp: datetime
|
||||
|
||||
|
||||
class StopAreaStopAssociationFields(Struct, kw_only=True):
|
||||
arrid: str # TODO: use int ?
|
||||
artid: str | None = None
|
||||
arrversion: str
|
||||
zdcid: str
|
||||
version: int
|
||||
zdaid: str
|
||||
zdaversion: str
|
||||
artversion: str | None = None
|
||||
|
||||
|
||||
class StopAreaStopAssociation(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: StopAreaStopAssociationFields
|
||||
record_timestamp: datetime
|
||||
|
||||
|
||||
class IdfmLineState(Enum):
|
||||
active = "active"
|
||||
available_soon = "prochainement active"
|
||||
|
||||
|
||||
class LinePicto(Struct, rename={"id_": "id"}):
|
||||
id_: str
|
||||
mimetype: str
|
||||
height: int
|
||||
width: int
|
||||
filename: str
|
||||
thumbnail: bool
|
||||
format: str
|
||||
# color_summary: list[str]
|
||||
|
||||
|
||||
class LineFields(Struct, kw_only=True):
|
||||
name_line: str
|
||||
status: IdfmLineState
|
||||
accessibility: IdfmState
|
||||
shortname_groupoflines: str | None = None
|
||||
transportmode: TransportMode
|
||||
colourweb_hexa: str
|
||||
textcolourprint_hexa: str
|
||||
transportsubmode: TransportSubMode | None = TransportSubMode.unknown
|
||||
operatorref: str | None = None
|
||||
visualsigns_available: IdfmState
|
||||
networkname: str | None = None
|
||||
id_line: str
|
||||
id_groupoflines: str | None = None
|
||||
operatorname: str | None = None
|
||||
audiblesigns_available: IdfmState
|
||||
shortname_line: str
|
||||
picto: LinePicto | None = None
|
||||
|
||||
|
||||
class Line(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: LineFields
|
||||
record_timestamp: datetime
|
||||
|
||||
@staticmethod
|
||||
def optional_value(value: Any) -> Any:
|
||||
if value:
|
||||
return value.value if isinstance(value, Enum) else value
|
||||
return "NULL"
|
||||
|
||||
|
||||
Lines = dict[str, Line]
|
||||
|
||||
Destinations = dict[str, set[str]]
|
||||
|
||||
|
||||
# TODO: Set structs frozen
|
||||
class StopLineAssoFields(Struct):
|
||||
pointgeo: Coordinate
|
||||
stop_id: str
|
||||
stop_name: str
|
||||
operatorname: str
|
||||
nom_commune: str
|
||||
route_long_name: str
|
||||
id: str
|
||||
stop_lat: str
|
||||
stop_lon: str
|
||||
code_insee: str
|
||||
|
||||
|
||||
class StopLineAsso(Struct):
|
||||
datasetid: str
|
||||
recordid: str
|
||||
fields: StopLineAssoFields
|
||||
# geometry: Union[Point]
|
||||
|
||||
|
||||
class Value(Struct):
|
||||
value: str
|
||||
|
||||
|
||||
class FramedVehicleJourney(Struct):
|
||||
DataFrameRef: Value
|
||||
DatedVehicleJourneyRef: str
|
||||
|
||||
|
||||
class TrainNumber(Struct):
|
||||
TrainNumberRef: list[Value]
|
||||
|
||||
|
||||
class MonitoredCall(Struct, kw_only=True):
|
||||
Order: int | None = None
|
||||
StopPointName: list[Value]
|
||||
VehicleAtStop: bool
|
||||
DestinationDisplay: list[Value]
|
||||
AimedArrivalTime: datetime | None = None
|
||||
ExpectedArrivalTime: datetime | None = None
|
||||
ArrivalPlatformName: Value | None = None
|
||||
AimedDepartureTime: datetime | None = None
|
||||
ExpectedDepartureTime: datetime | None = None
|
||||
ArrivalStatus: TrainStatus | None = None
|
||||
DepartureStatus: TrainStatus | None = None
|
||||
|
||||
|
||||
class MonitoredVehicleJourney(Struct, kw_only=True):
|
||||
LineRef: Value
|
||||
OperatorRef: Value
|
||||
FramedVehicleJourneyRef: FramedVehicleJourney
|
||||
DestinationRef: Value
|
||||
DestinationName: list[Value] | None = None
|
||||
JourneyNote: list[Value] | None = None
|
||||
TrainNumbers: TrainNumber | None = None
|
||||
MonitoredCall: MonitoredCall
|
||||
|
||||
|
||||
class StopDelivery(Struct):
|
||||
RecordedAtTime: datetime
|
||||
ItemIdentifier: str
|
||||
MonitoringRef: Value
|
||||
MonitoredVehicleJourney: MonitoredVehicleJourney
|
||||
|
||||
|
||||
class StopMonitoringDelivery(Struct):
|
||||
ResponseTimestamp: datetime
|
||||
Version: str
|
||||
Status: IdfmState
|
||||
MonitoredStopVisit: list[StopDelivery]
|
||||
|
||||
|
||||
class ServiceDelivery(Struct):
|
||||
ResponseTimestamp: datetime
|
||||
ProducerRef: str
|
||||
ResponseMessageIdentifier: str
|
||||
StopMonitoringDelivery: list[StopMonitoringDelivery]
|
||||
|
||||
|
||||
class Siri(Struct):
|
||||
ServiceDelivery: ServiceDelivery
|
||||
|
||||
|
||||
class IdfmOperator(Enum):
|
||||
SNCF = "SNCF"
|
||||
|
||||
|
||||
class IdfmResponse(Struct):
|
||||
Siri: Siri
|
15
backend/api/idfm_interface/ratp_types.py
Normal file
15
backend/api/idfm_interface/ratp_types.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from msgspec import Struct
|
||||
|
||||
|
||||
class PictoFieldsFile(Struct, rename={"id_": "id"}):
|
||||
id_: str
|
||||
height: int
|
||||
width: int
|
||||
filename: str
|
||||
thumbnail: bool
|
||||
format: str
|
||||
|
||||
|
||||
class Picto(Struct):
|
||||
indices_commerciaux: str
|
||||
noms_des_fichiers: PictoFieldsFile | None = None
|
Reference in New Issue
Block a user