99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
1
2
|
#!/usr/bin/env python3
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
3
4
|
import sys
import argparse
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
5
|
import datetime
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
6
|
import urllib.request
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
7
|
from icalendar import Calendar
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
8
|
from html.parser import HTMLParser
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
9
|
from icalendar import vDatetime, Calendar, Event as CalEvent
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
10
|
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
11
|
# Parse command line arguments
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
12
13
|
parser = argparse.ArgumentParser(description='Convertit l\'emploi du temps IMA en ICS')
parser.add_argument('annee', metavar='ANNEE', type=int, help='année (3 ou 4)')
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
14
15
16
17
|
parser.add_argument('edt', metavar='EDT', type=str, help='la page pointant vers l\'emploi du temps concerné')
parser.add_argument('-o', '--output', dest='file', type=str, default='-', help='fichier de sortie, - pour stdout')
args = parser.parse_args()
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
if args.annee == 3:
url = 'http://dptima3.polytech-lille.net/' + args.edt + '.html'
SLOTS = [(( 8, 0), (10, 0)),
((10, 20), (12, 20)),
((13, 50), (15, 50)),
((16, 10), (18, 10))]
DATE_FORMAT = '%d/%m/%Y'
elif args.annee == 4:
url = 'http://www.lifl.fr/~forget/EDT/' + args.edt + '.html'
SLOTS = [(( 8, 0), ( 9, 0)),
(( 9, 10), (10, 10)),
((10, 20), (11, 20)),
((11, 30), (12, 30)),
((13, 50), (14, 50)),
((15, 00), (16, 00)),
((16, 10), (17, 10)),
((17, 20), (18, 20))]
DATE_FORMAT = '%d/%m/%y'
else:
raise ValueError('Année inconnue : ' + annee)
DAYS_PER_WEEK = 6
TABLE_1_DATE_X = 1
TABLE_1_FIRST_SLOT_X = 2
TABLE_2_DATE_X = 0
TABLE_2_FIRST_SLOT_X = 1
class TableHTMLParser(HTMLParser):
tables = [] # Tables
table = False # Current table content
line = False # Current line content
cell = False # Current cell content
cellx = 1
celly = 1
# Logic
def iscell(self):
"""
Return if we are currently in a cell
"""
return isinstance(self.cell, str)
def isline(self):
"""
Return if we are currently in a line
"""
return isinstance(self.line, list)
def istable(self):
"""
Return if we are currently in a table
"""
return isinstance(self.table, list)
# Actions
def endcell(self):
if self.iscell():
self.line.append((self.cell.strip(), self.cellx, self.celly))
self.cell = False
def endline(self):
self.endcell()
if self.isline():
self.table.append(self.line.copy())
self.line = False
def endtable(self):
self.endline()
if self.istable():
self.tables.append(self.table.copy())
self.table = False
# Inheritance
def handle_starttag(self, tag, attrs):
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
if tag == 'table':
self.table = []
elif tag == 'tr':
self.endline()
self.line = []
elif tag == 'td':
self.endcell()
self.cell = ''
self.cellx = 1
self.celly = 1
for attr in attrs:
if attr[0] == 'colspan':
self.cellx = int(attr[1])
elif attr[0] == 'rowspan':
self.celly = int(attr[1])
def handle_endtag(self, tag):
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
111
112
113
114
115
116
117
118
|
if tag == 'table':
self.endtable()
elif tag == 'tr':
self.endline()
elif tag == 'td':
self.endcell()
def handle_data(self, data):
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
119
120
121
|
if self.iscell():
self.cell += data
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
122
123
124
|
# TODO Use HTTP header date
UPDATE_TIME = datetime.datetime.now()
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
125
126
127
128
129
130
131
|
# TODO Do something that really is OOP or do not...
class Event:
# Mined data
shortText = ''
longText = ''
date = False
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
132
133
|
begSlot = 0
endSlot = 0
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
# Generated data
shortName = ''
longName = ''
location = ''
startTime = False
endTime = False
active = False
def feedShortText(self, shortText):
self.shortText = shortText
def feedLongText(self, longText):
self.longText = longText
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
149
150
151
152
153
|
def feedBegSlot(self, slot):
self.begSlot = slot
def feedEndSlot(self, slot):
self.endSlot = slot
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
154
155
156
157
158
159
160
161
162
163
164
|
def feedDate(self, date):
self.date = date
def endFeed(self):
self.shortName = self.shortText
self.longName = self.longText
if self.longName:
self.active = True
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
165
166
|
if self.date and isinstance(self.begSlot, int) and isinstance(self.endSlot, int):
h, m = SLOTS[self.begSlot][0]
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
167
|
self.startTime = self.date + datetime.timedelta(hours=h, minutes=m)
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
168
|
h, m = SLOTS[self.endSlot][1]
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
169
170
171
172
173
174
175
176
177
178
179
180
|
self.endTime = self.date + datetime.timedelta(hours=h, minutes=m)
if self.longName:
e = self.longName.split('(')
if len(e) >= 2:
f = e[1].split(')')
self.longName = e[0].strip()
self.location = f[0].strip()
def __str__(self):
if self.active:
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
181
|
return self.shortName + ' [' + self.longName + '] ' + (str(self.startTime) + ' - ' + (str(self.endTime) + ' ') if self.startTime else '') + (('@ ' + self.location) if self.location else '')
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
182
183
184
185
186
|
else:
return 'Inactive event'
def getEvent(self):
e = CalEvent()
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
187
188
189
190
191
192
193
194
|
e.add('uid', '-'.join([
'polytech',
'ima' + str(args.annee),
args.edt,
vDatetime(self.startTime).to_ical().decode(),
vDatetime(self.endTime).to_ical().decode(),
self.shortName
]))
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
195
196
197
198
199
|
e.add('summary', self.shortName)
e.add('description', self.longName)
e.add('dtstart', self.startTime)
e.add('dtend', self.endTime)
e.add('location', self.location)
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
200
201
|
e.add('last-modified', UPDATE_TIME)
e.add('dtstamp', UPDATE_TIME)
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
202
203
204
|
return e
with urllib.request.urlopen(url) as handle:
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
205
|
htmlStr = handle.read().decode('iso-8859-15')
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
# Read HTML tables
parser = TableHTMLParser()
parser.feed(htmlStr)
# Dupplicates cells with colspan & rowspan
tables = []
for parserTable in parser.tables:
# Figuring out dimensions
X, Y = 0, 0
for cell in parserTable[0]:
X += cell[1]
for line in parserTable:
Y += line[0][2]
# Constructing table with reals dimensions
table = []
for y in range(Y):
line = []
for x in range(X):
line.append(False)
table.append(line)
# Filling table with parsed table
x, y = 0, 0
for line in parserTable:
for cell in line:
# Offsetting to the right if cell is not empty
while isinstance(table[y][x], str):
x += 1
# Copying values
for y2 in range(y, y + cell[2]):
for x2 in range(x, x + cell[1]):
table[y2][x2] = cell[0]
x = 0
y += 1
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
245
246
247
248
249
250
251
252
|
tables.append(table)
# Creating events
days = dict()
# Parsing table 1
for line in tables[0]:
try:
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
253
|
day1date = datetime.datetime.strptime(line[TABLE_1_DATE_X], DATE_FORMAT)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
254
255
256
257
|
except (ValueError, TypeError):
# This is not a date, no data to grab here
continue
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
258
259
260
261
262
263
264
|
for day in range(DAYS_PER_WEEK):
date = day1date + datetime.timedelta(days=day)
if date not in days:
days[date] = [Event() for s in range(len(SLOTS))]
for slot in range(len(SLOTS)):
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
265
266
267
268
269
270
|
try:
cell = line[day * len(SLOTS) + slot + TABLE_1_FIRST_SLOT_X]
except IndexError:
# Out of the table: saturday afternoon
break
days[date][slot].feedShortText(cell)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
271
272
273
274
275
|
continue
# Parsing table 2
for line in tables[1]:
try:
|
3e3f7292
Geoffrey PREUD'HOMME
IMA 4 support
|
276
|
date = datetime.datetime.strptime(line[TABLE_2_DATE_X], DATE_FORMAT)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
277
278
279
280
281
282
283
284
285
286
287
288
289
|
except ValueError:
# This is not a date, no data to grab here
continue
if date not in days:
days[date] = [Event() for s in range(len(SLOTS))]
for slot in range(len(SLOTS)):
days[date][slot].feedLongText(line[slot + TABLE_2_FIRST_SLOT_X])
# Feeding back time and slot to events
events = []
for day in days:
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
290
|
prevEvent = False
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
291
292
|
for slot in range(len(SLOTS)):
event = days[day][slot]
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
293
|
if prevEvent:
|
7a72c101
Geoffrey PREUD'HOMME
Stricter verifica...
|
294
|
if prevEvent.longText == event.longText:
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
295
296
297
298
|
prevEvent.feedEndSlot(slot)
else:
prevEvent.endFeed()
events.append(prevEvent)
|
7a72c101
Geoffrey PREUD'HOMME
Stricter verifica...
|
299
|
if not prevEvent or (prevEvent and prevEvent.longText != event.longText):
|
ab9e873d
Geoffrey PREUD'HOMME
No longer cut les...
|
300
301
302
303
304
305
|
event.feedDate(day)
event.feedBegSlot(slot)
event.feedEndSlot(slot)
prevEvent = event
prevEvent.endFeed()
events.append(prevEvent)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
306
307
308
|
# Creating calendar
cal = Calendar()
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
309
|
cal.add('proid', '-//geoffrey.frogeye.fr//NONSGML Icalendar Calendar//EN')
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
310
|
cal.add('version', '2.0')
|
964ae38e
Geoffrey PREUD'HOMME
Meilleur respect ...
|
311
312
|
cal.add('calscale', 'GREGORIAN')
cal.add('x-wr-calname', 'Polytech IMA ' + str(args.annee) + ' ' + args.edt)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
313
314
315
|
for event in events:
if event.active:
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
316
|
print(event, file=sys.stderr)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
317
318
319
|
cal.add_component(event.getEvent())
# Writing calendar to file
|
82e73c44
Geoffrey PREUD'HOMME
Interface en lign...
|
320
321
322
323
324
325
|
data = cal.to_ical()
if args.file == '-':
sys.stdout.write(data.decode('utf-8'))
else:
with open(args.file, 'wb') as f:
f.write(data)
|
99b1d135
Geoffrey PREUD'HOMME
Initial commit
|
|
|