Skip to content

Commit 61825d9

Browse files
🩹 fix(.bin/stib.fetch): Update API usage and drop arrow.
1 parent f1d55ee commit 61825d9

File tree

1 file changed

+123
-52
lines changed

1 file changed

+123
-52
lines changed

.bin/stib.fetch

Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import os
44
import sys
55
import json
6-
import arrow
6+
from typing import Optional
7+
from dataclasses import dataclass
8+
from itertools import count, chain
79
import shutil
10+
from datetime import datetime
11+
import urllib.parse
812
import urllib.request
9-
10-
from xml.etree import ElementTree
13+
import urllib.error
1114

1215
TZ = 'Europe/Brussels'
1316

@@ -18,73 +21,141 @@ HALT = CACHE + '/{}'
1821
LINE = HALT + '/{}'
1922
TIME = 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

2327
log = 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

5072
with 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

Comments
 (0)