33import os
44import sys
55import json
6- import arrow
76import subprocess
7+ from typing import Callable , Iterable , Literal , Optional , Union
8+ from dataclasses import dataclass , asdict
9+ import human
10+ from datetime import datetime , timedelta
811from collections import defaultdict
912
1013CONFIG = os .path .expanduser ('~/.config/commuting/config' )
@@ -38,19 +41,48 @@ with open(RTMCONFIG) as _config:
3841geolocation = json .loads (subprocess .check_output (
3942 ['memoize-get' , '1200' , 'geolocation.fetch' ]).decode ())
4043
41- def istransit (method ):
44+ def istransit (method : str ):
4245 return method in [ 'train' , 'tram' , 'metro' , 'bus' ]
4346
47+ Method = Union [
48+ Literal ['train' ],
49+ Literal ['tram' ],
50+ Literal ['metro' ],
51+ Literal ['bus' ],
52+ ]
53+
54+ Vendor = Union [
55+ Literal ['sncb-nmbs' ],
56+ Literal ['stib-mivb' ],
57+ Literal ['tec' ],
58+ Literal ['rtm' ],
59+ ]
60+
61+ @dataclass (frozen = True )
62+ class Part :
63+ method : Method
64+ seconds : Optional [float ] = None
65+ vendor : Optional [Vendor ] = None
66+ halt : Optional [str ] = None
67+ line : Optional [str ] = None
68+ dest : Optional [str ] = None
69+ origin : Optional [str ] = None
70+ destination : Optional [str ] = None
71+
72+ def _part (kwargs : dict ):
73+ return Part (** kwargs )
74+
75+
4476pt = (part for road in config .values () for path in
4577 road ['paths' ] for part in
46- path ['path' ] if istransit (part [ ' method' ] ))
78+ map ( _part , path ['path' ]) if istransit (part . method ))
4779
4880
49- def stibgrab (halt , line ):
81+ def stibgrab (halt : str , line : str ):
5082
5183 try :
5284
53- return sorted (map (arrow . get , os .listdir (STIBLINE .format (halt , line ))))
85+ return sorted (map (datetime . fromisoformat , os .listdir (STIBLINE .format (halt , line ))))
5486
5587 except FileNotFoundError :
5688
@@ -59,11 +91,11 @@ def stibgrab(halt, line):
5991 return []
6092
6193
62- def rtmgrab (halt , line , dest ):
94+ def rtmgrab (halt : str , line : str , dest : str ):
6395
6496 try :
6597
66- return sorted (map (arrow . get , os .listdir (RTMDEST .format (halt , line , dest ))))
98+ return sorted (map (datetime . fromisoformat , os .listdir (RTMDEST .format (halt , line , dest ))))
6799
68100 except FileNotFoundError :
69101
@@ -73,56 +105,32 @@ def rtmgrab(halt, line, dest):
73105
74106
75107
76- def _humanize (time , ref ):
77-
78- x = time .humanize (ref )
79-
80- if x == "in seconds" :
81-
82- return "in {} seconds" .format ((time - ref ).seconds )
83-
84- if x == "just now" :
85-
86- return "now"
87-
88- return x
89-
90-
91- def _duration (A , B ):
92-
93- x = _humanize (B , A )
108+ def _humanize (time : datetime , ref : datetime ):
94109
95- if x == 'now' :
110+ return human . datetime ( time , ref )
96111
97- return '{} seconds' .format ((B - A ).seconds )
98112
99- if x == 'in a minute' :
113+ def _duration ( delta : timedelta ) :
100114
101- return '1 minute'
115+ return human . timedelta ( delta )
102116
103- return x [3 :]
104117
105118
106- def _shortduration (A , B ):
119+ def _shortduration (delta : timedelta ):
107120
108- return _shorten (_duration (A , B ))
121+ return _shorten (_duration (delta ))
109122
110123
111- def _shorthumanize (time , ref ):
124+ def _shorthumanize (time : datetime , ref : datetime ):
112125
113126 return _shorten (_humanize (time , ref ))
114127
115- def _tinyduration (A , B ):
116-
117- return _tinier (_duration (A , B ))
118-
128+ def _tinyduration (delta : timedelta ):
119129
120- def _tinyhumanize ( time , ref ):
130+ return _tinier ( _duration ( delta ))
121131
122- return _tinier (_humanize (time , ref ))
123132
124-
125- def _shorten (x ):
133+ def _shorten (x : str ):
126134
127135 if 'minutes' in x or 'seconds' in x :
128136
@@ -134,7 +142,7 @@ def _shorten(x):
134142
135143 return x
136144
137- def _tinier (x ):
145+ def _tinier (x : str ):
138146
139147 if 'minutes' in x or 'seconds' in x :
140148
@@ -147,11 +155,11 @@ def _tinier(x):
147155 return x
148156
149157
150- def _repr (path , fduration = _shortduration ):
158+ def _repr (path : Iterable [ Part ], fduration : Callable [[ timedelta ], str ] = _shortduration ):
151159
152160 _map = {
153- "wait" : lambda part : ' ({})' .format (fduration (part [ ' seconds' ] )),
154- "walk" : lambda part : ' ' ,
161+ "wait" : lambda part : ' ({})' .format (fduration (timedelta ( seconds = part . seconds ) )),
162+ "walk" : lambda _ : ' ' ,
155163 "tram" : lambda part : {
156164 "stib-mivb" : lambda part : ' {line}' .format (** part ),
157165 "rtm" : lambda part : ' {line}' .format (** part ),
@@ -170,27 +178,29 @@ def _repr(path,fduration=_shortduration):
170178 }[part ['vendor' ]](part ),
171179 }
172180
173- return ' ' .join (_map [part [ ' method' ] ](part ) for part in path )
181+ return ' ' .join (_map [part . method ](part ) for part in path )
174182
175183
176184
177- def _diff (a , b ):
185+ def _diff (a : datetime , b : datetime ):
178186
179- return a .datetime . timestamp () - b . datetime .timestamp ()
187+ return a .timestamp () - b .timestamp ()
180188
181189PT = defaultdict (list )
182190
183- def _key ( x ) :
184- return frozenset ( ( k , x [ k ] ) for k in x . keys () if k != 'seconds' )
191+ def _key ( x : Part ) :
192+ return frozenset ( ( k , v ) for k , v in asdict ( x ). items () if k != 'seconds' )
185193
186194for part in pt :
187195
188- vendor = part [ ' vendor' ]
196+ vendor = part . vendor
189197
190198 if vendor == 'stib-mivb' :
191199
192- halt = part ['halt' ]
193- line = part ['line' ]
200+ halt = part .halt
201+ assert (halt is not None )
202+ line = part .line
203+ assert (line is not None )
194204
195205 if halt not in stibconfig or line not in stibconfig [halt ]:
196206
@@ -202,13 +212,16 @@ for part in pt:
202212
203213 elif vendor == 'rtm' :
204214
205- halt = part ['halt' ]
206- line = part ['line' ]
207- dest = part ['dest' ]
215+ halt = part .halt
216+ assert (halt is not None )
217+ line = part .line
218+ assert (line is not None )
219+ dest = part .dest
220+ assert (dest is not None )
208221
209- if halt not in rtmconfig :
222+ if halt not in rtmconfig or line not in set ( x [ 'ref' ] for x in rtmconfig [ halt ]) :
210223
211- log ('warning: rtm is not configured to fetch' , halt )
224+ log ('warning: rtm is not configured to fetch' , halt , line )
212225
213226 k = _key (part )
214227 if k not in PT :
@@ -225,36 +238,37 @@ def allroutes():
225238
226239 for path in road ['paths' ]:
227240
228- for route in routes (path ['path' ]):
241+ for route in routes (tuple ( map ( _part , path ['path' ])) ):
229242
230243 yield name , route
231244
232245
233- def routes (path , leave = None , total = 0 , prev = ()):
246+ def routes (path : tuple [ Part , ...], leave = None , total : float = 0 , prev = ()):
234247
235248 if not path :
236249
237250 yield leave , total , prev
238251
239252 return
240253
241- if path [0 ][ ' method' ] == 'walk' :
254+ if path [0 ]. method == 'walk' :
242255
243- duration = path [0 ][ ' seconds' ]
256+ duration = path [0 ]. seconds
244257
245258 yield from routes (path [1 :], leave , total + duration , prev + ( path [0 ], ))
246259
247260 elif _key (path [0 ]) in PT :
248261
249- duration = path [0 ]['seconds' ]
262+ duration = path [0 ].seconds
263+ assert (duration is not None )
250264 times = PT [_key (path [0 ])]
251265
252266 if leave is None :
253267
254268 for timestamp in times :
255269
256270 _path = path [1 :]
257- _leave = timestamp . shift (seconds = - total )
271+ _leave = timestamp - timedelta (seconds = total )
258272 _total = total + duration
259273 _prev = prev + (path [0 ], )
260274
@@ -279,9 +293,6 @@ def routes(path, leave=None, total=0, prev=()):
279293
280294 else :
281295
282- A = leave .shift (seconds = + total )
283- B = A .shift (seconds = + waiting )
284-
285296 _path = path [1 :]
286297 _leave = leave
287298 _total = total + waiting + duration
@@ -293,25 +304,24 @@ def routes(path, leave=None, total=0, prev=()):
293304
294305 yield from routes (_path , _leave , _total , _prev )
295306
296- NOW = arrow .now ()
307+ NOW = datetime .now (). astimezone ()
297308
298- myroutes = []
309+ def _myroutes ():
310+ for name , route in allroutes ():
299311
300- for name , route in allroutes ():
312+ _leave , _total , _path = route
301313
302- _leave , _total , _path = route
314+ if _leave is None :
315+ _leave = NOW
303316
304- if _leave is None :
305- _leave = NOW
306-
307- elif _leave < NOW :
308- continue
317+ elif _leave < NOW :
318+ continue
309319
310- _arrival = _leave . shift (seconds = + _total )
320+ _arrival = _leave + timedelta (seconds = _total )
311321
312- myroutes . append (( name , _path , _leave , _arrival ) )
322+ yield ( name , _path , _leave , _arrival )
313323
314- myroutes = sorted (myroutes , key = lambda x : (x [0 ], x [3 ], x [2 ]))
324+ myroutes = sorted (_myroutes () , key = lambda x : (x [0 ], x [3 ], x [2 ]))
315325
316326
317327def myfilter (name ):
@@ -322,7 +332,7 @@ def myfilter(name):
322332
323333 return False
324334
325- if 'days' in config [name ] and not NOW .format ( 'dddd ' ) in config [name ]['days' ]:
335+ if 'days' in config [name ] and not NOW .isoformat ( '%A ' ) in config [name ]['days' ]:
326336
327337 return False
328338
@@ -336,17 +346,17 @@ for name, _path, _leave, _arrival in myroutes:
336346 title = config [name ]['title' ]
337347 leave = _shorthumanize (_leave , NOW )
338348 arrival = _shorthumanize (_arrival , NOW )
339- total = _shortduration (_leave , _arrival ) # remove "in " prefix
349+ total = _shortduration (_arrival - _leave ) # remove "in " prefix
340350 path = _repr (_path )
341- eta = _arrival .format ( 'HH:mm:ss ' )
351+ eta = _arrival .strftime ( '%H:%M:%S ' )
342352
343353 full_text = ' [{}] {}, {}, {} :{} : ETA {}' .format (
344354 title , leave , total , arrival , path , eta
345355 )
346356
347- sleave = _tinyduration (NOW , _leave )
348- sarrival = _tinyduration (NOW , _arrival )
349- stotal = _tinyduration (_leave , _arrival )
357+ sleave = _tinyduration (_leave - NOW )
358+ sarrival = _tinyduration (_arrival - NOW )
359+ stotal = _tinyduration (_arrival - _leave )
350360 spath = _repr (_path ,fduration = _tinyduration )
351361
352362 short_text = ' [{}] {}|{}|{} :{} : {}' .format (
0 commit comments