33import os
44import sys
55import json
6- import arrow
7- import shutil
6+ from typing import Literal , Union
7+ from dataclasses import dataclass
8+ from datetime import datetime , timedelta
9+ import urllib .error
10+ import urllib .parse
811import urllib .request
9-
10- import lxml .html as html
11- from lxml .cssselect import CSSSelector
12-
13- TZ = 'Europe/Paris'
14-
15- sel = lambda h , s : CSSSelector (s )(h )
12+ import shutil
1613
1714CACHE = os .path .expanduser ('~/.cache/rtm' )
1815CONFIG = os .path .expanduser ('~/.config/rtm/config' )
@@ -22,76 +19,123 @@ LINE = HALT + '/{}'
2219DEST = LINE + '/{}'
2320TIME = DEST + '/{}'
2421
25- REQUEST = 'http ://www .rtm.fr/node/2091832?codesigep= {}'
22+ REQUEST = 'https ://api .rtm.fr/front/getReelTime? {}'
2623
2724log = lambda * x , ** y : print (* x , ** y , file = sys .stderr )
2825
26+ def _is_valid_id_char (c : str ):
27+ return (c >= "A" and c <= "Z" ) or (c >= "0" and c <= "9" ) or (c == ":" )
2928
30- def init (halt ):
29+ def _is_valid_id (x : str ):
30+ assert (isinstance (x , str ))
31+ return all (map (_is_valid_id_char , x ))
3132
32- # dangerous
33- assert (isinstance (halt , str ))
34- assert (all (c >= "A" and c <= "Z" for c in halt ))
33+ @dataclass (frozen = True )
34+ class Line :
35+ ref : str
36+ direction : Union [Literal [1 ], Literal [2 ]]
3537
36- path = HALT .format (halt )
38+ @dataclass (frozen = True )
39+ class DepartureTime :
40+ Hour : str
41+ Monitored : bool
3742
38- shutil .rmtree (path , True ) # True to ignore errors
39- os .makedirs (path )
43+ @dataclass (frozen = True )
44+ class Event :
45+ DepartureTime : dict
46+ ReelHour : bool
47+ PublishedLineName : str
48+ DestinationName : str
4049
50+ def _event (kwargs : dict ):
51+ return Event (** kwargs )
4152
42- def save (halt , line , dest , timestamp ):
53+ @dataclass (frozen = True )
54+ class Data :
55+ temps_reel : list [dict ]
4356
44- log ('x' , halt , line , dest , timestamp )
57+ @dataclass (frozen = True )
58+ class Response :
59+ data : dict
60+ returnCode : Literal [200 ]
61+ dateReturn : str
62+ meta : dict
4563
46- try :
47- os .makedirs (DEST .format (halt , line , dest ))
48- except FileExistsError :
49- pass
64+ def _line (kwargs : dict ):
65+ return Line (** kwargs )
5066
51- with open (TIME .format (halt , line , dest , timestamp ), 'w' ) as fd :
52- pass
67+ def _queries (config : dict ):
68+ for halt , lines in config .items ():
69+ for line in map (_line , lines ):
70+ yield halt , line
5371
54- with open (CONFIG ) as _config :
55- config = json .load (_config )
72+ def init (halt : str , line : Line ):
73+
74+ # NOTE: Dangerous.
75+ assert (_is_valid_id (halt ))
76+ assert (_is_valid_id (line .ref ))
77+ assert (line .direction in (1 , 2 ))
78+
79+ path = DEST .format (halt , line .ref , line .direction )
5680
57- for halt in config :
81+ shutil .rmtree (path , True ) # NOTE: True to ignore errors.
82+ os .makedirs (path )
5883
59- url = REQUEST .format (halt )
6084
61- now = arrow .now (TZ )
85+ def save (halt : str , line : Line , timestamp : datetime ):
86+ log ('x' , halt , line .ref , line .direction , timestamp .isoformat ())
87+ with open (TIME .format (halt , line .ref , line .direction , timestamp .isoformat ()), 'w' ):
88+ pass
89+
90+ with open (CONFIG ) as _config :
91+ config = json .load (_config )
6292
63- log (url )
93+ for halt , line in _queries (config ):
94+ params = urllib .parse .urlencode ({
95+ 'lineRef' : line .ref ,
96+ 'direction' : line .direction ,
97+ 'pointRef' : halt
98+ })
6499
65- try :
100+ url = REQUEST . format ( params )
66101
67- tree = html . parse (url )
102+ log (url )
68103
69- except urllib . error . HTTPError :
70- log ( 'failed to download' , url )
71- continue
104+ try :
105+ with urllib . request . urlopen ( url ) as fp :
106+ response = Response ( ** json . load ( fp ))
72107
73- waitingtimes = sel (tree , '.sticky-enabled > tbody:nth-child(2) > tr' )
108+ except urllib .error .HTTPError :
109+ log ('failed to download' , url )
110+ continue
74111
75- waitingtimes = [ sel ( tr , 'td' ) for tr in waitingtimes ]
112+ # NOTE: In case of a bug do not erase previously known information.
76113
77- waitingtimes = [[td .text for td in tr ] for tr in waitingtimes ]
114+ if response .returnCode != 200 : continue
115+ data = Data (** response .data )
78116
79- # in case of a bug do not erase previously known information
117+ if not data .temps_reel :
118+ continue
80119
81- if not waitingtimes :
82- continue
120+ events = map ( _event , data . temps_reel )
121+ now = datetime . fromisoformat ( response . dateReturn )
83122
84- init (halt )
123+ init (halt , line )
85124
86- for line , time , dest in waitingtimes :
125+ for event in events :
126+ departure = DepartureTime (** event .DepartureTime )
87127
88- hour , minute = map (int , time .split (':' ))
128+ hour , minute , second = map (int , departure . Hour .split (':' ))
89129
90- timestamp = now .replace (hour = hour % 24 , minute = minute ,
91- second = 0 , microsecond = 0 )
130+ timestamp = now .replace (
131+ hour = hour % 24 ,
132+ minute = minute ,
133+ second = second ,
134+ microsecond = 0
135+ )
92136
93- # let 's say it is 23:57 and time == '00:05'
94- if timestamp < now . shift (minutes = - 1 ):
95- timestamp = timestamp . shift (days = + 1 )
137+ # NOTE: Let 's say it is 23:57 and time == '00:05'.
138+ if timestamp < now - timedelta (minutes = 1 ):
139+ timestamp = timestamp + timedelta (days = 1 )
96140
97- save (halt , line , dest , timestamp )
141+ save (halt , line , timestamp )
0 commit comments