33import os
44import sys
55import json
6- import arrow
6+ from typing import Optional
7+ from dataclasses import dataclass
8+ from itertools import count , chain
79import shutil
10+ from datetime import datetime
11+ import urllib .parse
812import urllib .request
9-
10- from xml .etree import ElementTree
13+ import urllib .error
1114
1215TZ = 'Europe/Brussels'
1316
@@ -18,73 +21,141 @@ HALT = CACHE + '/{}'
1821LINE = HALT + '/{}'
1922TIME = LINE + '/{}'
2023
21- REQUEST = 'http://m.stib.be/api/getwaitingtimes.php?halt={}'
24+ REQUEST = 'https://stibmivb.opendatasoft.com/api/explore/v2.1/catalog/datasets/waiting-time-rt-production/records?{}'
25+ PAGE_SIZE = 100
2226
2327log = lambda * x , ** y : print (* x , ** y , file = sys .stderr )
2428
2529
26- def init (halt ):
30+ def init (halt : str ):
31+ # NOTE: Dangerous.
32+ assert (isinstance (halt , str ))
33+ assert (all (c >= "0" and c <= "9" for c in halt ))
2734
28- # dangerous
29- assert (isinstance (halt , str ))
30- assert (all (c >= "0" and c <= "9" for c in halt ))
35+ path = HALT .format (halt )
3136
32- path = HALT .format (halt )
37+ shutil .rmtree (path , True ) # True to ignore errors
38+ os .makedirs (path )
3339
34- shutil .rmtree (path , True ) # True to ignore errors
35- os .makedirs (path )
40+ @dataclass (frozen = True )
41+ class Page :
42+ total_count : str
43+ results : list
3644
45+ @dataclass (frozen = True )
46+ class Result :
47+ pointid : str
48+ lineid : str
49+ passingtimes : str
3750
38- def save (halt , line , timestamp ):
51+ @dataclass (frozen = True )
52+ class Destination :
53+ fr : str
54+ nl : str
3955
40- log ('x' , halt , line , timestamp )
56+ @dataclass (frozen = True )
57+ class Event :
58+ halt : str
59+ line : str
60+ destination : Optional [Destination ]
61+ eta : datetime
4162
42- try :
43- os .mkdir (LINE .format (halt , line ))
44- except FileExistsError :
45- pass
63+ def save (event : Event ):
64+ try :
65+ os .mkdir (LINE .format (event .halt , event .line ))
66+ except FileExistsError :
67+ pass
4668
47- with open (TIME .format (halt , line , timestamp ) , 'w' ) as fd :
48- pass
69+ with open (TIME .format (event . halt , event . line , event . eta . isoformat ()) , 'w' ):
70+ pass
4971
5072with open (CONFIG ) as _config :
51- config = json .load (_config )
52-
53- for halt , lines in config .items ():
54-
55- url = REQUEST .format (halt )
56-
57- log (url )
58-
59- try :
60-
61- W = ElementTree .parse (urllib .request .urlopen (url )).getroot ()
73+ config = json .load (_config )
74+
75+ def _fetch (halt , lines ):
76+ results = _fetch_halt (halt )
77+ events = chain .from_iterable (map (_events , results ))
78+ return filter (_line_filter (lines ), events )
79+
80+ def _fetch_halt (halt : str ):
81+ expected_count = None
82+ yielded = 0
83+ for page in count (1 ):
84+ total_count , results = _fetch_halt_page (halt , page )
85+ if expected_count is None :
86+ expected_count = total_count
87+ else :
88+ assert (total_count == expected_count )
89+
90+ yield from results
91+
92+ yielded += len (results )
93+ if yielded >= expected_count :
94+ assert (yielded == expected_count )
95+ break
96+
97+ def _result_for (halt : str ):
98+ def _f (kwargs : dict ):
99+ assert (kwargs ['pointid' ] == halt )
100+ return Result (** kwargs )
101+
102+ return _f
103+
104+ def _fetch_halt_page (halt : str , page : int ):
105+ offset = (page - 1 ) * PAGE_SIZE
106+ limit = PAGE_SIZE
107+
108+ params = urllib .parse .urlencode ({
109+ 'where' : 'pointid={}' .format (halt ),
110+ 'limit' : limit ,
111+ 'offset' : offset
112+ })
113+
114+ url = REQUEST .format (params )
115+
116+ log (url )
117+
118+ with urllib .request .urlopen (url ) as fp :
119+ data = Page (** json .load (fp ))
120+ return int (data .total_count ), list (map (_result_for (halt ), data .results ))
121+
122+ def _events (result : Result ):
123+ halt = result .pointid
124+ line = result .lineid
125+ events = json .loads (result .passingtimes )
126+
127+ for event in events :
128+ assert (event ['lineId' ] == line )
129+ yield Event (
130+ halt = halt ,
131+ line = line ,
132+ destination = Destination (** event ['destination' ]) if 'destination' in event else None ,
133+ eta = datetime .fromisoformat (event ['expectedArrivalTime' ])
134+ )
135+
136+
137+ def _line_filter (lines : set [str ]):
138+ def predicate (event : Event ):
139+ keep = event .line in lines
140+ log ('x' if keep else ' ' , event .halt , event .line , event .eta .isoformat ())
141+ return keep
142+ return predicate
62143
63- except urllib .error .HTTPError :
64- log ('failed to download' , url )
65- continue
66144
67- now = arrow .now (TZ )
68-
69- waitingtimes = list (W .iter ('waitingtime' ))
70-
71- # in case of a bug do not erase previously known information
72-
73- if not waitingtimes :
74- continue
75-
76- init (halt )
77-
78- for waitingtime in waitingtimes :
79-
80- w = {tag .tag : tag .text for tag in waitingtime }
145+ for halt , lines in config .items ():
81146
82- timestamp = now .shift (minutes = + int (w ['minutes' ]))
147+ try :
148+ events = list (_fetch (halt , set (lines )))
149+ except urllib .error .HTTPError as error :
150+ log ('failed to download' , error .geturl ())
151+ continue
83152
84- if w [ 'line' ] in lines :
153+ # NOTE: In case of a bug do not erase previously known information.
85154
86- save (halt , w ['line' ], timestamp )
155+ if not events :
156+ continue
87157
88- else :
158+ init ( halt )
89159
90- log (' ' , halt , w ['line' ], timestamp )
160+ for event in events :
161+ save (event )
0 commit comments