Skip to content

Commit 29d44dd

Browse files
🩹 fix(.bin/rtm.fetch): Update API usage and drop arrow.
1 parent 2c7c393 commit 29d44dd

File tree

1 file changed

+95
-51
lines changed

1 file changed

+95
-51
lines changed

.bin/rtm.fetch

Lines changed: 95 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
import os
44
import sys
55
import 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
811
import 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

1714
CACHE = os.path.expanduser('~/.cache/rtm')
1815
CONFIG = os.path.expanduser('~/.config/rtm/config')
@@ -22,76 +19,123 @@ LINE = HALT + '/{}'
2219
DEST = LINE + '/{}'
2320
TIME = DEST + '/{}'
2421

25-
REQUEST = 'http://www.rtm.fr/node/2091832?codesigep={}'
22+
REQUEST = 'https://api.rtm.fr/front/getReelTime?{}'
2623

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

Comments
 (0)