Skip to content

Commit 24838e4

Browse files
♻️ refactor(commuting.fetch): Update data usage and drop arrow.
1 parent 785db2f commit 24838e4

File tree

1 file changed

+95
-85
lines changed

1 file changed

+95
-85
lines changed

.bin/commuting.fetch

Lines changed: 95 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import os
44
import sys
55
import json
6-
import arrow
76
import 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
811
from collections import defaultdict
912

1013
CONFIG = os.path.expanduser('~/.config/commuting/config')
@@ -38,19 +41,48 @@ with open(RTMCONFIG) as _config:
3841
geolocation = 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+
4476
pt = (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

181189
PT = 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

186194
for 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

317327
def 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

Comments
 (0)