aims-convert 2.1__tar.gz → 2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aims_convert-2.1 → aims_convert-2.2}/PKG-INFO +3 -1
- {aims_convert-2.1 → aims_convert-2.2}/aims/cli.py +9 -3
- {aims_convert-2.1 → aims_convert-2.2}/aims/data_structures.py +6 -2
- {aims_convert-2.1 → aims_convert-2.2}/aims/gui.py +7 -5
- {aims_convert-2.1 → aims_convert-2.2}/aims/logbook_report.py +7 -5
- {aims_convert-2.1 → aims_convert-2.2}/aims/output.py +67 -44
- aims_convert-2.2/aims/parse.py +16 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims/roster.py +38 -25
- aims_convert-2.2/aims/version.py +4 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/PKG-INFO +3 -1
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/SOURCES.txt +3 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/requires.txt +3 -0
- {aims_convert-2.1 → aims_convert-2.2}/setup.py +4 -1
- aims_convert-2.2/tests/test_compound.py +34 -0
- {aims_convert-2.1 → aims_convert-2.2}/tests/test_logbook.py +0 -1
- aims_convert-2.2/tests/test_output.py +231 -0
- {aims_convert-2.1 → aims_convert-2.2}/tests/test_roster.py +26 -32
- aims_convert-2.1/aims/parse.py +0 -24
- {aims_convert-2.1 → aims_convert-2.2}/README.md +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims/__init__.py +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims/py.typed +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/dependency_links.txt +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/entry_points.txt +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/aims_convert.egg-info/top_level.txt +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/pyproject.toml +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/setup.cfg +0 -0
- {aims_convert-2.1 → aims_convert-2.2}/tests/test_parse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aims-convert
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: Extract useful information from AIMS
|
|
5
5
|
Home-page: https://github.com/JonHurst/aims-convert
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -15,6 +15,8 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
Requires-Dist: nightflight
|
|
16
16
|
Requires-Dist: bs4
|
|
17
17
|
Requires-Dist: html5lib
|
|
18
|
+
Provides-Extra: unit-tests
|
|
19
|
+
Requires-Dist: freezegun; extra == "unit-tests"
|
|
18
20
|
|
|
19
21
|
# AIMS Roster Data Extraction #
|
|
20
22
|
|
|
@@ -5,6 +5,7 @@ import argparse
|
|
|
5
5
|
|
|
6
6
|
from aims.parse import parse
|
|
7
7
|
import aims.output as output
|
|
8
|
+
from aims.version import VERSION
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _args():
|
|
@@ -12,14 +13,17 @@ def _args():
|
|
|
12
13
|
description=(
|
|
13
14
|
'Process an AIMS detailed roster into various useful formats.'))
|
|
14
15
|
parser.add_argument('format',
|
|
15
|
-
choices=['roster', 'efj', 'csv', 'ical'])
|
|
16
|
+
choices=['roster', 'efj', 'csv', 'ical', 'version'])
|
|
16
17
|
parser.add_argument('--ade', action="store_true")
|
|
17
18
|
return parser.parse_args()
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def main() -> int:
|
|
21
22
|
args = _args()
|
|
22
|
-
|
|
23
|
+
if args.format == "version":
|
|
24
|
+
print(f"Version: {VERSION}")
|
|
25
|
+
return 0
|
|
26
|
+
duties, ade = parse(sys.stdin.read())
|
|
23
27
|
if args.format == "roster":
|
|
24
28
|
print(output.roster(duties))
|
|
25
29
|
elif args.format == "efj":
|
|
@@ -27,7 +31,9 @@ def main() -> int:
|
|
|
27
31
|
elif args.format == "csv":
|
|
28
32
|
print(output.csv(duties))
|
|
29
33
|
elif args.format == "ical":
|
|
30
|
-
|
|
34
|
+
if not args.ade:
|
|
35
|
+
ade = ()
|
|
36
|
+
print(output.ical(duties, ade))
|
|
31
37
|
return 0
|
|
32
38
|
|
|
33
39
|
|
|
@@ -2,6 +2,11 @@ from typing import NamedTuple, Optional
|
|
|
2
2
|
import datetime as dt
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
class AllDayEvent(NamedTuple):
|
|
6
|
+
date: dt.date
|
|
7
|
+
code: str
|
|
8
|
+
|
|
9
|
+
|
|
5
10
|
class CrewMember(NamedTuple):
|
|
6
11
|
name: str
|
|
7
12
|
role: str
|
|
@@ -21,9 +26,8 @@ class Sector(NamedTuple):
|
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class Duty(NamedTuple):
|
|
24
|
-
code: Optional[str] # only all day event has code
|
|
25
29
|
start: dt.datetime
|
|
26
|
-
finish:
|
|
30
|
+
finish: dt.datetime
|
|
27
31
|
sectors: tuple[Sector, ...]
|
|
28
32
|
|
|
29
33
|
|
|
@@ -9,8 +9,8 @@ import ctypes
|
|
|
9
9
|
import aims.parse
|
|
10
10
|
from aims.data_structures import RosterException, InputFileException
|
|
11
11
|
from aims.output import csv, ical, efj
|
|
12
|
+
from aims.version import VERSION
|
|
12
13
|
|
|
13
|
-
VERSION = "2.1"
|
|
14
14
|
|
|
15
15
|
SETTINGS_FILE = os.path.expanduser("~/.aimsgui")
|
|
16
16
|
|
|
@@ -328,7 +328,7 @@ class MainWindow(ttk.Frame):
|
|
|
328
328
|
self.txt.insert(tk.END, "Getting registration and type info...")
|
|
329
329
|
self.txt.update()
|
|
330
330
|
|
|
331
|
-
duties = aims.parse.parse(html)
|
|
331
|
+
duties, _ = aims.parse.parse(html)
|
|
332
332
|
# note: normalise newlines for Text widget - will restore on output
|
|
333
333
|
txt = csv(duties).replace("\r\n", "\n")
|
|
334
334
|
self.txt.delete('1.0', tk.END)
|
|
@@ -338,9 +338,11 @@ class MainWindow(ttk.Frame):
|
|
|
338
338
|
html = self.__roster_html()
|
|
339
339
|
if not html:
|
|
340
340
|
return
|
|
341
|
-
duties = aims.parse.parse(html)
|
|
341
|
+
duties, ade = aims.parse.parse(html)
|
|
342
342
|
# note: normalise newlines for Text widget - will restore on output
|
|
343
|
-
|
|
343
|
+
if not self.ms.with_ade.get():
|
|
344
|
+
ade = ()
|
|
345
|
+
txt = ical(duties, ade).replace("\r\n", "\n")
|
|
344
346
|
self.txt.delete('1.0', tk.END)
|
|
345
347
|
self.txt.insert(tk.END, txt, 'ical')
|
|
346
348
|
|
|
@@ -351,7 +353,7 @@ class MainWindow(ttk.Frame):
|
|
|
351
353
|
self.txt.delete('1.0', tk.END)
|
|
352
354
|
self.txt.insert(tk.END, "Working…", 'efj')
|
|
353
355
|
self.txt.update()
|
|
354
|
-
duties = aims.parse.parse(html)
|
|
356
|
+
duties, _ = aims.parse.parse(html)
|
|
355
357
|
txt = efj(duties)
|
|
356
358
|
self.txt.delete('1.0', tk.END)
|
|
357
359
|
self.txt.insert(tk.END, txt, 'efj')
|
|
@@ -2,7 +2,9 @@ import datetime as dt
|
|
|
2
2
|
import re
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from bs4 import BeautifulSoup # type: ignore
|
|
6
|
+
|
|
7
|
+
from aims.data_structures import Duty, Sector, CrewMember, AllDayEvent
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
DATE, FLTNUM, FROM, OFF, TO, ON, TYPE, REG, BLOCK, CP = range(1, 11)
|
|
@@ -30,13 +32,13 @@ def _sector(row: tuple[str, ...]) -> Optional[Sector]:
|
|
|
30
32
|
def _duty(sectors: tuple[Sector, ...]) -> Duty:
|
|
31
33
|
lowest_off = min([X.off for X in sectors])
|
|
32
34
|
highest_on = max([X.on for X in sectors])
|
|
33
|
-
return Duty(
|
|
34
|
-
lowest_off - dt.timedelta(hours=1),
|
|
35
|
+
return Duty(lowest_off - dt.timedelta(hours=1),
|
|
35
36
|
highest_on + dt.timedelta(minutes=30),
|
|
36
37
|
sectors)
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
def duties(
|
|
40
|
+
def duties(html) -> tuple[tuple[Duty, ...], tuple[AllDayEvent, ...]]:
|
|
41
|
+
soup = BeautifulSoup(html, "html5lib")
|
|
40
42
|
sectors: list[Sector] = []
|
|
41
43
|
re_date = re.compile(r"^\d{2}/\d{2}/\d{2}$")
|
|
42
44
|
for row in soup.find_all("tr"):
|
|
@@ -54,4 +56,4 @@ def duties(soup) -> tuple[Duty, ...]:
|
|
|
54
56
|
else:
|
|
55
57
|
groups[-1].append(sector)
|
|
56
58
|
last_on = sector.on
|
|
57
|
-
return tuple(_duty(tuple(X)) for X in groups)
|
|
59
|
+
return (tuple(_duty(tuple(X)) for X in groups), ())
|
|
@@ -5,12 +5,15 @@ import io
|
|
|
5
5
|
import csv as libcsv
|
|
6
6
|
import datetime as dt
|
|
7
7
|
import re
|
|
8
|
-
|
|
8
|
+
import itertools
|
|
9
9
|
|
|
10
|
-
from aims.data_structures import Duty, Sector, CrewMember
|
|
10
|
+
from aims.data_structures import Duty, Sector, CrewMember, AllDayEvent
|
|
11
11
|
import nightflight.night as nightcalc # type: ignore
|
|
12
12
|
from nightflight.airport_nvecs import airfields as nvecs # type: ignore
|
|
13
13
|
|
|
14
|
+
UTC = Z("UTC")
|
|
15
|
+
LT = Z("Europe/London")
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
def clean_name(name: str) -> str:
|
|
16
19
|
parts = [X.strip().capitalize() for X in name.split()]
|
|
@@ -52,46 +55,68 @@ def _night(sector: Sector) -> tuple[int, bool]:
|
|
|
52
55
|
return (round(night_duration), night_landing)
|
|
53
56
|
|
|
54
57
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
def _build_roster_string(start, end, block, string):
|
|
59
|
+
start, end = [X.replace(tzinfo=UTC).astimezone(LT)
|
|
60
|
+
for X in (start, end)]
|
|
61
|
+
duration = int((end - start).total_seconds()) // 60
|
|
62
|
+
duration_str = f"{duration // 60}:{duration % 60:02d}"
|
|
63
|
+
block_str = f"{block // 60}:{block % 60:02d}"
|
|
64
|
+
return (f"{start:%d/%m/%Y %H:%M}-{end:%H:%M} "
|
|
65
|
+
f"{string} {block_str}/{duration_str}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _roster_quasi(
|
|
69
|
+
qsectors: tuple[Sector, ...],
|
|
70
|
+
cursor: dt.datetime
|
|
71
|
+
) -> tuple[tuple[str, ...], dt.datetime]:
|
|
72
|
+
retval: list[str] = []
|
|
73
|
+
name = "Brief"
|
|
74
|
+
for s in qsectors:
|
|
75
|
+
if cursor < s.off:
|
|
76
|
+
retval.append(_build_roster_string(cursor, s.off, 0, name))
|
|
77
|
+
name = "Debrief"
|
|
78
|
+
retval.append(_build_roster_string(s.off, s.on, 0, s.name))
|
|
79
|
+
cursor = s.on
|
|
80
|
+
return (tuple(retval), cursor)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _roster_real(
|
|
84
|
+
sectors: tuple[Sector, ...],
|
|
85
|
+
cursor: dt.datetime
|
|
86
|
+
) -> tuple[str, dt.datetime]:
|
|
87
|
+
assert sectors[0].from_
|
|
88
|
+
airports: list[str] = [sectors[0].from_]
|
|
59
89
|
block = 0
|
|
60
|
-
|
|
90
|
+
start = cursor
|
|
61
91
|
for sector in sectors:
|
|
62
|
-
if not from_ and sector.from_:
|
|
63
|
-
from_ = sector.from_
|
|
64
92
|
if sector.position:
|
|
65
93
|
airports.append("[psn]")
|
|
66
|
-
|
|
67
|
-
if sector.from_:
|
|
68
|
-
airports.append(f"[{sector.name}]")
|
|
69
|
-
else:
|
|
70
|
-
airports.append(sector.name)
|
|
71
|
-
if sector.to:
|
|
72
|
-
airports.append(sector.to)
|
|
73
|
-
if not sector.quasi and not sector.position:
|
|
94
|
+
else:
|
|
74
95
|
block += int((sector.on - sector.off).total_seconds()) // 60
|
|
75
|
-
|
|
76
|
-
airports
|
|
77
|
-
|
|
96
|
+
assert sector.to
|
|
97
|
+
airports.append(sector.to)
|
|
98
|
+
cursor = sectors[-1].on + dt.timedelta(minutes=30)
|
|
99
|
+
return (
|
|
100
|
+
_build_roster_string(start, cursor, block, '-'.join(airports)),
|
|
101
|
+
cursor)
|
|
78
102
|
|
|
79
103
|
|
|
80
104
|
def roster(duties: tuple[Duty, ...]) -> str:
|
|
81
|
-
|
|
82
|
-
LT = Z("Europe/London")
|
|
83
|
-
output = []
|
|
105
|
+
output: list[str] = []
|
|
84
106
|
for duty in duties:
|
|
85
|
-
if not duty.finish:
|
|
107
|
+
if not duty.finish: # an all day duty
|
|
86
108
|
continue
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
cursor = duty.start
|
|
110
|
+
for k, g in itertools.groupby(duty.sectors, key=lambda X: X.quasi):
|
|
111
|
+
if k: # group of quasi sectors
|
|
112
|
+
new_output, cursor = _roster_quasi(tuple(g), cursor)
|
|
113
|
+
output += new_output
|
|
114
|
+
else: # group of real sectors
|
|
115
|
+
new, cursor = _roster_real(tuple(g), cursor)
|
|
116
|
+
output.append(new)
|
|
117
|
+
if cursor < duty.finish:
|
|
118
|
+
output.append(
|
|
119
|
+
_build_roster_string(cursor, duty.finish, 0, "Debrief"))
|
|
95
120
|
return "\n".join(output)
|
|
96
121
|
|
|
97
122
|
|
|
@@ -246,20 +271,18 @@ def _build_dict(duty: Duty) -> dict[str, str]:
|
|
|
246
271
|
return event
|
|
247
272
|
|
|
248
273
|
|
|
249
|
-
def ical(duties: tuple[Duty, ...],
|
|
274
|
+
def ical(duties: tuple[Duty, ...], ade: tuple[AllDayEvent, ...]) -> str:
|
|
250
275
|
events = []
|
|
251
276
|
for duty in duties:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
date
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
day=date,
|
|
262
|
-
ev=duty.code,
|
|
277
|
+
d = _build_dict(duty)
|
|
278
|
+
events.append(vevent.format(**d))
|
|
279
|
+
for e in ade:
|
|
280
|
+
uid = "{}{}@HURSTS.ORG.UK".format(
|
|
281
|
+
e.date.isoformat(), e.code)
|
|
282
|
+
modified = ical_datetime.format(dt.datetime.utcnow())
|
|
283
|
+
events.append(advevent.format(
|
|
284
|
+
day=e.date,
|
|
285
|
+
ev=e.code,
|
|
263
286
|
modified=modified,
|
|
264
287
|
uid=uid))
|
|
265
288
|
return vcalendar.format("\r\n".join(events))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from aims.data_structures import Duty, AllDayEvent, InputFileException
|
|
2
|
+
import aims.roster
|
|
3
|
+
import aims.logbook_report
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse(html: str) -> tuple[tuple[Duty, ...], tuple[AllDayEvent, ...]]:
|
|
7
|
+
# check it's an html5 file
|
|
8
|
+
html5_header = "<!DOCTYPE html><html>"
|
|
9
|
+
if html[:len(html5_header)] != html5_header:
|
|
10
|
+
raise InputFileException("HTML5 header not found.")
|
|
11
|
+
if html.find("Personal Crew Schedule Report") != -1:
|
|
12
|
+
return aims.roster.duties(html)
|
|
13
|
+
elif html.find("Pilot Logbook") != -1:
|
|
14
|
+
return aims.logbook_report.duties(html)
|
|
15
|
+
else:
|
|
16
|
+
raise InputFileException("Report type marker not found")
|
|
@@ -6,9 +6,11 @@ produced by processing the roster with BeautifulSoup.
|
|
|
6
6
|
|
|
7
7
|
import datetime as dt
|
|
8
8
|
import re
|
|
9
|
-
from typing import Optional
|
|
10
9
|
|
|
11
|
-
from
|
|
10
|
+
from bs4 import BeautifulSoup # type: ignore
|
|
11
|
+
|
|
12
|
+
from aims.data_structures import (
|
|
13
|
+
Duty, Sector, CrewMember, AllDayEvent, InputFileException)
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
Row = tuple[tuple[str, ...], ...]
|
|
@@ -78,7 +80,10 @@ def _sectors(data: Row, date: dt.date) -> tuple[Sector, ...]:
|
|
|
78
80
|
if airports[0][0] == "*": # Either ground or air positioning
|
|
79
81
|
airports[0] = airports[0][1:]
|
|
80
82
|
position = True
|
|
81
|
-
quasi =
|
|
83
|
+
quasi = False
|
|
84
|
+
if not type_: # If no type in code, assume quasi sector
|
|
85
|
+
quasi = True
|
|
86
|
+
crew = ()
|
|
82
87
|
retval.append(
|
|
83
88
|
Sector(name, None, type_, airports[0], airports[1],
|
|
84
89
|
_convert_timestring(times[0], date),
|
|
@@ -93,7 +98,7 @@ def _sectors(data: Row, date: dt.date) -> tuple[Sector, ...]:
|
|
|
93
98
|
return tuple(retval)
|
|
94
99
|
|
|
95
100
|
|
|
96
|
-
def _duty(row: Row) ->
|
|
101
|
+
def _duty(row: Row) -> Duty:
|
|
97
102
|
"""Creates a Duty object from a structured representation of an HTML row.
|
|
98
103
|
|
|
99
104
|
The input object is a 12 cell tuple with each cell containing the contents
|
|
@@ -158,15 +163,10 @@ def _duty(row: Row) -> Optional[Duty]:
|
|
|
158
163
|
|
|
159
164
|
"""
|
|
160
165
|
try:
|
|
161
|
-
|
|
162
|
-
return None
|
|
166
|
+
assert row[CODES] and row[TIMES]
|
|
163
167
|
date = _convert_datestring(row[DATE][0])
|
|
164
|
-
# if there are no times, it is an all day event
|
|
165
|
-
if not row[TIMES]:
|
|
166
|
-
return Duty(row[CODES][0],
|
|
167
|
-
dt.datetime.combine(date, dt.time()),
|
|
168
|
-
None, ())
|
|
169
168
|
if row[DSTART]:
|
|
169
|
+
assert row[DEND]
|
|
170
170
|
start = _convert_timestring(row[DSTART][0], date)
|
|
171
171
|
end = _convert_timestring(row[DEND][0], date)
|
|
172
172
|
else:
|
|
@@ -183,18 +183,26 @@ def _duty(row: Row) -> Optional[Duty]:
|
|
|
183
183
|
break
|
|
184
184
|
start = min(start, sector.off)
|
|
185
185
|
end = max(end, sector.on)
|
|
186
|
-
return Duty(
|
|
186
|
+
return Duty(start, end, sectors)
|
|
187
|
+
except (IndexError, ValueError):
|
|
188
|
+
raise InputFileException(f"Bad Duty Record: {str(row)}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _ade(row: Row) -> AllDayEvent:
|
|
192
|
+
try:
|
|
193
|
+
return AllDayEvent(_convert_datestring(row[DATE][0]), row[CODES][0])
|
|
187
194
|
except (IndexError, ValueError):
|
|
188
|
-
raise InputFileException(f"Bad Record: {str(row)}")
|
|
195
|
+
raise InputFileException(f"Bad All Day Duty Record: {str(row)}")
|
|
189
196
|
|
|
190
197
|
|
|
191
|
-
def duties(
|
|
192
|
-
"""
|
|
198
|
+
def duties(html: str) -> tuple[tuple[Duty, ...], tuple[AllDayEvent, ...]]:
|
|
199
|
+
"""Extract the data from an AIMS vertical roster.
|
|
193
200
|
|
|
194
201
|
The entire document is a single table (how retro!). The interesting part
|
|
195
202
|
starts with the row two rows below the row with a cell containing the
|
|
196
203
|
phrase "Schedule Details" and ends with the row above the first row with a
|
|
197
|
-
blank date field. Each row between these represents a single duty
|
|
204
|
+
blank date field. Each row between these represents a single duty or an all
|
|
205
|
+
dat event.
|
|
198
206
|
|
|
199
207
|
The date to the left of each row is the date that the duty started on. If
|
|
200
208
|
duties continue past midnight, relevant times are marked with a superscript
|
|
@@ -202,25 +210,30 @@ def duties(soup) -> tuple[Duty, ...]:
|
|
|
202
210
|
vertical roster much saner to parse than the alternatives since there are
|
|
203
211
|
no concerns about missing data at the start and end of the roster period.
|
|
204
212
|
|
|
205
|
-
:param
|
|
206
|
-
|
|
207
|
-
:return: A tuple of Duty objects encoding the roster in a standard form.
|
|
213
|
+
:param html: The html of a 'vertical' HTML AIMS roster.
|
|
214
|
+
:return: A tuple of Duty objects and a tuple of AllDayEvent objects
|
|
208
215
|
|
|
209
216
|
"""
|
|
217
|
+
soup = BeautifulSoup(html, "html5lib")
|
|
210
218
|
rows = iter(soup.find_all("tr"))
|
|
211
219
|
try:
|
|
212
220
|
while "Schedule Details" not in next(rows).stripped_strings:
|
|
213
221
|
pass
|
|
214
222
|
next(rows)
|
|
215
|
-
|
|
223
|
+
duty_list: list[Duty] = []
|
|
224
|
+
ade_list: list[AllDayEvent] = []
|
|
216
225
|
while True:
|
|
217
|
-
|
|
226
|
+
row: Row = tuple(
|
|
218
227
|
tuple(Y.replace("\xa0", " ") for Y in X.stripped_strings)
|
|
219
228
|
for X in next(rows)("td"))
|
|
220
|
-
if not
|
|
229
|
+
if not row[DATE]: # line without date ends table
|
|
221
230
|
break
|
|
222
|
-
if
|
|
223
|
-
|
|
231
|
+
if not row[CODES]: # unpublished duty
|
|
232
|
+
continue
|
|
233
|
+
if not row[TIMES]: # an all day event
|
|
234
|
+
ade_list.append(_ade(row))
|
|
235
|
+
elif duty := _duty(row): # a normal duty
|
|
236
|
+
duty_list.append(duty)
|
|
237
|
+
return (tuple(duty_list), tuple(ade_list))
|
|
224
238
|
except (StopIteration, IndexError):
|
|
225
239
|
raise InputFileException("Duty table ended unexpectedly")
|
|
226
|
-
return tuple(retval)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aims-convert
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2
|
|
4
4
|
Summary: Extract useful information from AIMS
|
|
5
5
|
Home-page: https://github.com/JonHurst/aims-convert
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -15,6 +15,8 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
Requires-Dist: nightflight
|
|
16
16
|
Requires-Dist: bs4
|
|
17
17
|
Requires-Dist: html5lib
|
|
18
|
+
Provides-Extra: unit-tests
|
|
19
|
+
Requires-Dist: freezegun; extra == "unit-tests"
|
|
18
20
|
|
|
19
21
|
# AIMS Roster Data Extraction #
|
|
20
22
|
|
|
@@ -10,12 +10,15 @@ aims/output.py
|
|
|
10
10
|
aims/parse.py
|
|
11
11
|
aims/py.typed
|
|
12
12
|
aims/roster.py
|
|
13
|
+
aims/version.py
|
|
13
14
|
aims_convert.egg-info/PKG-INFO
|
|
14
15
|
aims_convert.egg-info/SOURCES.txt
|
|
15
16
|
aims_convert.egg-info/dependency_links.txt
|
|
16
17
|
aims_convert.egg-info/entry_points.txt
|
|
17
18
|
aims_convert.egg-info/requires.txt
|
|
18
19
|
aims_convert.egg-info/top_level.txt
|
|
20
|
+
tests/test_compound.py
|
|
19
21
|
tests/test_logbook.py
|
|
22
|
+
tests/test_output.py
|
|
20
23
|
tests/test_parse.py
|
|
21
24
|
tests/test_roster.py
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="aims-convert",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.2",
|
|
9
9
|
author="Jon Hurst",
|
|
10
10
|
author_email="jon.a@hursts.org.uk",
|
|
11
11
|
description="Extract useful information from AIMS",
|
|
@@ -34,4 +34,7 @@ setuptools.setup(
|
|
|
34
34
|
"console_scripts": ["aims = aims.cli:main"],
|
|
35
35
|
"gui_scripts" : ["aimsgui = aims.gui:main"]
|
|
36
36
|
},
|
|
37
|
+
extras_require={
|
|
38
|
+
"unit_tests": ["freezegun"]
|
|
39
|
+
},
|
|
37
40
|
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from freezegun import freeze_time
|
|
3
|
+
import glob
|
|
4
|
+
|
|
5
|
+
import aims.parse
|
|
6
|
+
import aims.output
|
|
7
|
+
from aims.data_structures import InputFileException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@freeze_time("2024-01-01")
|
|
11
|
+
class Test_compound(unittest.TestCase):
|
|
12
|
+
|
|
13
|
+
def test_compound(self):
|
|
14
|
+
for f in glob.glob("files/*.htm"):
|
|
15
|
+
with open(f) as input_:
|
|
16
|
+
duties, ade = aims.parse.parse(input_.read())
|
|
17
|
+
with open(f + ".txt") as roster_file:
|
|
18
|
+
self.assertEqual(roster_file.read(),
|
|
19
|
+
aims.output.roster(duties) + "\n")
|
|
20
|
+
with open(f + ".ical", "rb") as ical_file:
|
|
21
|
+
self.assertEqual(
|
|
22
|
+
ical_file.read(),
|
|
23
|
+
(aims.output.ical(duties, ade) + "\n").encode())
|
|
24
|
+
with open(f + ".csv", "rb") as csv_file:
|
|
25
|
+
self.assertEqual(csv_file.read(),
|
|
26
|
+
(aims.output.csv(duties) + "\n").encode())
|
|
27
|
+
with open(f + ".efj") as efj_file:
|
|
28
|
+
self.assertEqual(efj_file.read(),
|
|
29
|
+
aims.output.efj(duties) + "\n")
|
|
30
|
+
|
|
31
|
+
def test_bad_file(self):
|
|
32
|
+
with open("files/README") as f:
|
|
33
|
+
with self.assertRaises(InputFileException):
|
|
34
|
+
aims.parse.parse(f.read())
|
|
@@ -22,7 +22,6 @@ class TestDuty(unittest.TestCase):
|
|
|
22
22
|
quasi=False, position=False,
|
|
23
23
|
crew=(CrewMember("TEST-2", "CP"), )))
|
|
24
24
|
expected = Duty(
|
|
25
|
-
code=None,
|
|
26
25
|
start=datetime.datetime(2022, 7, 23, 10, 44),
|
|
27
26
|
finish=datetime.datetime(2022, 7, 23, 18, 11),
|
|
28
27
|
sectors=(Sector(name='6053', reg='G-EZRY', type_='320',
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import datetime
|
|
3
|
+
from freezegun import freeze_time
|
|
4
|
+
|
|
5
|
+
from aims.output import roster, efj, ical
|
|
6
|
+
from aims.data_structures import Duty, Sector, CrewMember, AllDayEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
crew = (
|
|
10
|
+
CrewMember(name='CAPTAIN THE', role='CP'),
|
|
11
|
+
CrewMember(name='FO THE', role='FO'),
|
|
12
|
+
CrewMember(name='PU THE', role='PU'),
|
|
13
|
+
CrewMember(name='FA A', role='FA'))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
standard_duty = Duty(
|
|
17
|
+
start=datetime.datetime(2023, 6, 2, 5, 0),
|
|
18
|
+
finish=datetime.datetime(2023, 6, 2, 16, 1),
|
|
19
|
+
sectors=(
|
|
20
|
+
Sector(name='2867', reg=None, type_='320',
|
|
21
|
+
from_='BRS', to='LIS',
|
|
22
|
+
off=datetime.datetime(2023, 6, 2, 6, 0),
|
|
23
|
+
on=datetime.datetime(2023, 6, 2, 8, 36),
|
|
24
|
+
quasi=False, position=False, crew=crew),
|
|
25
|
+
Sector(name='2868', reg=None, type_='320',
|
|
26
|
+
from_='LIS', to='BRS',
|
|
27
|
+
off=datetime.datetime(2023, 6, 2, 9, 43),
|
|
28
|
+
on=datetime.datetime(2023, 6, 2, 12, 4),
|
|
29
|
+
quasi=False, position=False, crew=crew),
|
|
30
|
+
Sector(name='239', reg=None, type_='320',
|
|
31
|
+
from_='BRS', to='NCL',
|
|
32
|
+
off=datetime.datetime(2023, 6, 2, 12, 52),
|
|
33
|
+
on=datetime.datetime(2023, 6, 2, 13, 58),
|
|
34
|
+
quasi=False, position=False, crew=crew),
|
|
35
|
+
Sector(name='240', reg=None, type_='320',
|
|
36
|
+
from_='NCL', to='BRS',
|
|
37
|
+
off=datetime.datetime(2023, 6, 2, 14, 32),
|
|
38
|
+
on=datetime.datetime(2023, 6, 2, 15, 31),
|
|
39
|
+
quasi=False, position=False, crew=crew)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
standby_duty = Duty(
|
|
43
|
+
start=datetime.datetime(2024, 7, 25, 5, 15),
|
|
44
|
+
finish=datetime.datetime(2024, 7, 25, 13, 15),
|
|
45
|
+
sectors=(
|
|
46
|
+
Sector(name='ESBY', reg=None, type_=None,
|
|
47
|
+
from_=None, to=None,
|
|
48
|
+
off=datetime.datetime(2024, 7, 25, 5, 15),
|
|
49
|
+
on=datetime.datetime(2024, 7, 25, 13, 15),
|
|
50
|
+
quasi=True, position=False, crew=()), ))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
standby_before_flight = Duty(
|
|
54
|
+
start=datetime.datetime(2023, 7, 2, 11, 45),
|
|
55
|
+
finish=datetime.datetime(2023, 7, 2, 19, 26),
|
|
56
|
+
sectors=(
|
|
57
|
+
Sector(name='LSBY', reg=None, type_=None,
|
|
58
|
+
from_='BRS', to='BRS',
|
|
59
|
+
off=datetime.datetime(2023, 7, 2, 11, 45),
|
|
60
|
+
on=datetime.datetime(2023, 7, 2, 13, 20),
|
|
61
|
+
quasi=True, position=False,
|
|
62
|
+
crew=()),
|
|
63
|
+
Sector(name='ADTY', reg=None, type_=None,
|
|
64
|
+
from_='BRS', to='BRS',
|
|
65
|
+
off=datetime.datetime(2023, 7, 2, 13, 20),
|
|
66
|
+
on=datetime.datetime(2023, 7, 2, 14, 0),
|
|
67
|
+
quasi=True, position=False,
|
|
68
|
+
crew=()),
|
|
69
|
+
Sector(name='227', reg=None, type_='319',
|
|
70
|
+
from_='BRS', to='BFS',
|
|
71
|
+
off=datetime.datetime(2023, 7, 2, 16, 14),
|
|
72
|
+
on=datetime.datetime(2023, 7, 2, 17, 23),
|
|
73
|
+
quasi=False, position=False,
|
|
74
|
+
crew=crew),
|
|
75
|
+
Sector(name='228', reg=None, type_='319',
|
|
76
|
+
from_='BFS', to='BRS',
|
|
77
|
+
off=datetime.datetime(2023, 7, 2, 17, 56),
|
|
78
|
+
on=datetime.datetime(2023, 7, 2, 18, 56),
|
|
79
|
+
quasi=False, position=False,
|
|
80
|
+
crew=crew)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
loe_day1 = Duty(
|
|
84
|
+
start=datetime.datetime(2023, 6, 19, 14, 30),
|
|
85
|
+
finish=datetime.datetime(2023, 6, 19, 18, 0),
|
|
86
|
+
sectors=(
|
|
87
|
+
Sector(name='TAXI227', reg=None, type_=None,
|
|
88
|
+
from_='BRS', to='LGW',
|
|
89
|
+
off=datetime.datetime(2023, 6, 19, 14, 30),
|
|
90
|
+
on=datetime.datetime(2023, 6, 19, 18, 0),
|
|
91
|
+
quasi=True, position=True, crew=()),))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
loe_day2 = Duty(
|
|
95
|
+
start=datetime.datetime(2023, 6, 20, 12, 0),
|
|
96
|
+
finish=datetime.datetime(2023, 6, 20, 18, 30),
|
|
97
|
+
sectors=(
|
|
98
|
+
Sector(name='LOE', reg=None, type_=None,
|
|
99
|
+
from_=None, to=None,
|
|
100
|
+
off=datetime.datetime(2023, 6, 20, 13, 30),
|
|
101
|
+
on=datetime.datetime(2023, 6, 20, 17, 30),
|
|
102
|
+
quasi=True, position=False, crew=()),))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
loe_day3 = Duty(
|
|
106
|
+
start=datetime.datetime(2023, 6, 21, 8, 0),
|
|
107
|
+
finish=datetime.datetime(2023, 6, 21, 18, 30),
|
|
108
|
+
sectors=(
|
|
109
|
+
Sector(name='FFST', reg=None, type_=None,
|
|
110
|
+
from_='LGW', to='LGW',
|
|
111
|
+
off=datetime.datetime(2023, 6, 21, 9, 30),
|
|
112
|
+
on=datetime.datetime(2023, 6, 21, 13, 30),
|
|
113
|
+
quasi=True, position=False, crew=()),
|
|
114
|
+
Sector(name='TAXI72', reg=None, type_=None,
|
|
115
|
+
from_='LGW', to='BRS',
|
|
116
|
+
off=datetime.datetime(2023, 6, 21, 15, 0),
|
|
117
|
+
on=datetime.datetime(2023, 6, 21, 18, 30),
|
|
118
|
+
quasi=True, position=True, crew=())))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestRoster(unittest.TestCase):
|
|
122
|
+
|
|
123
|
+
def test_standard(self):
|
|
124
|
+
self.assertEqual(
|
|
125
|
+
roster((standard_duty, standby_duty)),
|
|
126
|
+
"02/06/2023 06:00-17:01 BRS-LIS-BRS-NCL-BRS 7:02/11:01\n"
|
|
127
|
+
"25/07/2024 06:15-14:15 ESBY 0:00/8:00")
|
|
128
|
+
|
|
129
|
+
def test_standby_before_flight(self):
|
|
130
|
+
self.assertEqual(
|
|
131
|
+
roster((standby_before_flight, )),
|
|
132
|
+
"02/07/2023 12:45-14:20 LSBY 0:00/1:35\n"
|
|
133
|
+
"02/07/2023 14:20-15:00 ADTY 0:00/0:40\n"
|
|
134
|
+
"02/07/2023 15:00-20:26 BRS-BFS-BRS 2:09/5:26")
|
|
135
|
+
|
|
136
|
+
def test_loe(self):
|
|
137
|
+
self.assertEqual(
|
|
138
|
+
roster((loe_day1, loe_day2, loe_day3)),
|
|
139
|
+
"19/06/2023 15:30-19:00 TAXI227 0:00/3:30\n"
|
|
140
|
+
"20/06/2023 13:00-14:30 Brief 0:00/1:30\n"
|
|
141
|
+
"20/06/2023 14:30-18:30 LOE 0:00/4:00\n"
|
|
142
|
+
"20/06/2023 18:30-19:30 Debrief 0:00/1:00\n"
|
|
143
|
+
"21/06/2023 09:00-10:30 Brief 0:00/1:30\n"
|
|
144
|
+
"21/06/2023 10:30-14:30 FFST 0:00/4:00\n"
|
|
145
|
+
"21/06/2023 14:30-16:00 Debrief 0:00/1:30\n"
|
|
146
|
+
"21/06/2023 16:00-19:30 TAXI72 0:00/3:30")
|
|
147
|
+
|
|
148
|
+
def test_empty(self):
|
|
149
|
+
self.assertEqual(roster(()), "")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestEFJ(unittest.TestCase):
|
|
153
|
+
|
|
154
|
+
def test_standard(self):
|
|
155
|
+
self.assertEqual(
|
|
156
|
+
efj((standard_duty, standby_duty)),
|
|
157
|
+
"2023-06-02\n"
|
|
158
|
+
"0500/1601\n"
|
|
159
|
+
"{ CP:Captain The, FO:Fo The, PU:Pu The, FA:Fa A }\n"
|
|
160
|
+
"?-????:320\n"
|
|
161
|
+
"BRS/LIS 0600/0836\n"
|
|
162
|
+
"LIS/BRS 0943/1204\n"
|
|
163
|
+
"BRS/NCL 1252/1358\n"
|
|
164
|
+
"NCL/BRS 1432/1531\n\n"
|
|
165
|
+
"2024-07-25\n"
|
|
166
|
+
"0515/1315 #ESBY\n")
|
|
167
|
+
|
|
168
|
+
def test_standby_before_flight(self):
|
|
169
|
+
self.assertEqual(
|
|
170
|
+
efj((standby_before_flight, )),
|
|
171
|
+
"2023-07-02\n"
|
|
172
|
+
"1145/1926\n"
|
|
173
|
+
"{ CP:Captain The, FO:Fo The, PU:Pu The, FA:Fa A }\n"
|
|
174
|
+
"?-????:319\n"
|
|
175
|
+
"BRS/BFS 1614/1723\n"
|
|
176
|
+
"BFS/BRS 1756/1856\n")
|
|
177
|
+
|
|
178
|
+
def test_loe(self):
|
|
179
|
+
self.assertEqual(
|
|
180
|
+
efj((loe_day1, loe_day2, loe_day3)),
|
|
181
|
+
"2023-06-19\n"
|
|
182
|
+
"1430/1800 #TAXI227\n"
|
|
183
|
+
"\n"
|
|
184
|
+
"2023-06-20\n"
|
|
185
|
+
"1200/1830 #LOE\n"
|
|
186
|
+
"\n"
|
|
187
|
+
"2023-06-21\n"
|
|
188
|
+
"0800/1830\n")
|
|
189
|
+
|
|
190
|
+
def test_empty(self):
|
|
191
|
+
self.assertEqual(efj(()), "")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@freeze_time("2024-01-01")
|
|
195
|
+
class Test_ical(unittest.TestCase):
|
|
196
|
+
|
|
197
|
+
def test_standard(self):
|
|
198
|
+
expected = ("BEGIN:VCALENDAR\r\n"
|
|
199
|
+
"VERSION:2.0\r\n"
|
|
200
|
+
"PRODID:hursts.org.uk\r\n"
|
|
201
|
+
"BEGIN:VEVENT\r\n"
|
|
202
|
+
"UID:2023-06-02T05:00:00BRSLISBRSNCLBRS@HURSTS.ORG.UK\r\n"
|
|
203
|
+
"DTSTAMP:20240101T000000Z\r\n"
|
|
204
|
+
"DTSTART:20230602T050000Z\r\n"
|
|
205
|
+
"DTEND:20230602T160100Z\r\n"
|
|
206
|
+
"SUMMARY:BRS-LIS-BRS-NCL-BRS\r\n"
|
|
207
|
+
"DESCRIPTION:06:00z-08:36z 2867 BRS/LIS \\n\r\n"
|
|
208
|
+
" 09:43z-12:04z 2868 LIS/BRS \\n\r\n"
|
|
209
|
+
" 12:52z-13:58z 239 BRS/NCL \\n\r\n"
|
|
210
|
+
" 14:32z-15:31z 240 NCL/BRS \r\n"
|
|
211
|
+
"LAST-MODIFIED:20240101T000000Z\r\n"
|
|
212
|
+
"END:VEVENT\r\n"
|
|
213
|
+
"END:VCALENDAR\r\n")
|
|
214
|
+
self.assertEqual(ical((standard_duty, ), ()), expected)
|
|
215
|
+
|
|
216
|
+
def test_all_day_event(self):
|
|
217
|
+
expected = ("BEGIN:VCALENDAR\r\n"
|
|
218
|
+
"VERSION:2.0\r\n"
|
|
219
|
+
"PRODID:hursts.org.uk\r\n"
|
|
220
|
+
"BEGIN:VEVENT\r\n"
|
|
221
|
+
"UID:2024-01-02P/T@HURSTS.ORG.UK\r\n"
|
|
222
|
+
"DTSTAMP:20240101T000000Z\r\n"
|
|
223
|
+
"DTSTART;VALUE=DATE:20240102\r\n"
|
|
224
|
+
"SUMMARY:P/T\r\n"
|
|
225
|
+
"TRANSP:TRANSPARENT\r\n"
|
|
226
|
+
"LAST-MODIFIED:20240101T000000Z\r\n"
|
|
227
|
+
"END:VEVENT\r\n"
|
|
228
|
+
"END:VCALENDAR\r\n")
|
|
229
|
+
self.assertEqual(
|
|
230
|
+
ical((), (AllDayEvent(datetime.date(2024, 1, 2), "P/T"), )),
|
|
231
|
+
expected)
|
|
@@ -3,7 +3,7 @@ import datetime
|
|
|
3
3
|
|
|
4
4
|
import aims.roster as roster
|
|
5
5
|
from aims.data_structures import (
|
|
6
|
-
Duty, Sector, CrewMember, InputFileException
|
|
6
|
+
Duty, Sector, CrewMember, AllDayEvent, InputFileException
|
|
7
7
|
)
|
|
8
8
|
|
|
9
9
|
crew = ('CP - 0000 - CAPTAIN THE',
|
|
@@ -34,7 +34,6 @@ class Test_duty(unittest.TestCase):
|
|
|
34
34
|
('11:01',),
|
|
35
35
|
(), crew, ())
|
|
36
36
|
expected = Duty(
|
|
37
|
-
code=None,
|
|
38
37
|
start=datetime.datetime(2023, 6, 2, 5, 0),
|
|
39
38
|
finish=datetime.datetime(2023, 6, 2, 16, 1),
|
|
40
39
|
sectors=(
|
|
@@ -61,17 +60,6 @@ class Test_duty(unittest.TestCase):
|
|
|
61
60
|
)
|
|
62
61
|
self.assertEqual(roster._duty(src), expected)
|
|
63
62
|
|
|
64
|
-
def test_all_day_event(self):
|
|
65
|
-
src = ((),
|
|
66
|
-
('04/06/2023 Sun',),
|
|
67
|
-
('D/O',),
|
|
68
|
-
('Day off',),
|
|
69
|
-
(), (), (), (), (), (), (), ())
|
|
70
|
-
expected = Duty(code='D/O',
|
|
71
|
-
start=datetime.datetime(2023, 6, 4, 0, 0),
|
|
72
|
-
finish=None, sectors=())
|
|
73
|
-
self.assertEqual(roster._duty(src), expected)
|
|
74
|
-
|
|
75
63
|
def test_loe(self):
|
|
76
64
|
src = (
|
|
77
65
|
((), ('19/06/2023', 'Mon'),
|
|
@@ -89,8 +77,7 @@ class Test_duty(unittest.TestCase):
|
|
|
89
77
|
(), ('10:30',), (), ('CP - PAX - 0000 - CAPTAIN THE',),
|
|
90
78
|
()))
|
|
91
79
|
expected = (
|
|
92
|
-
Duty(
|
|
93
|
-
start=datetime.datetime(2023, 6, 19, 14, 30),
|
|
80
|
+
Duty(start=datetime.datetime(2023, 6, 19, 14, 30),
|
|
94
81
|
finish=datetime.datetime(2023, 6, 19, 18, 0),
|
|
95
82
|
sectors=(
|
|
96
83
|
Sector(name='TAXI227', reg=None, type_=None,
|
|
@@ -98,8 +85,7 @@ class Test_duty(unittest.TestCase):
|
|
|
98
85
|
off=datetime.datetime(2023, 6, 19, 14, 30),
|
|
99
86
|
on=datetime.datetime(2023, 6, 19, 18, 0),
|
|
100
87
|
quasi=True, position=True, crew=()),)),
|
|
101
|
-
Duty(
|
|
102
|
-
start=datetime.datetime(2023, 6, 20, 12, 0),
|
|
88
|
+
Duty(start=datetime.datetime(2023, 6, 20, 12, 0),
|
|
103
89
|
finish=datetime.datetime(2023, 6, 20, 18, 30),
|
|
104
90
|
sectors=(
|
|
105
91
|
Sector(name='LOE', reg=None, type_=None,
|
|
@@ -107,7 +93,7 @@ class Test_duty(unittest.TestCase):
|
|
|
107
93
|
off=datetime.datetime(2023, 6, 20, 13, 30),
|
|
108
94
|
on=datetime.datetime(2023, 6, 20, 17, 30),
|
|
109
95
|
quasi=True, position=False, crew=()),)),
|
|
110
|
-
Duty(
|
|
96
|
+
Duty(start=datetime.datetime(2023, 6, 21, 8, 0),
|
|
111
97
|
finish=datetime.datetime(2023, 6, 21, 18, 30),
|
|
112
98
|
sectors=(
|
|
113
99
|
Sector(name='FFST', reg=None, type_=None,
|
|
@@ -129,8 +115,7 @@ class Test_duty(unittest.TestCase):
|
|
|
129
115
|
(), ('05:15 - 13:15',), (),
|
|
130
116
|
(), ('08:00',),
|
|
131
117
|
(), (), ())
|
|
132
|
-
expected = Duty(
|
|
133
|
-
start=datetime.datetime(2024, 7, 25, 5, 15),
|
|
118
|
+
expected = Duty(start=datetime.datetime(2024, 7, 25, 5, 15),
|
|
134
119
|
finish=datetime.datetime(2024, 7, 25, 13, 15),
|
|
135
120
|
sectors=(
|
|
136
121
|
Sector(name='ESBY', reg=None, type_=None,
|
|
@@ -149,8 +134,7 @@ class Test_duty(unittest.TestCase):
|
|
|
149
134
|
('11:45 - 13:20', '13:20 - 14:00',
|
|
150
135
|
'A16:14 - A17:23', 'A17:56 - A18:56'),
|
|
151
136
|
('19:26',), ('02:09',), ('07:41',), (), crew, ())
|
|
152
|
-
expected = Duty(
|
|
153
|
-
start=datetime.datetime(2023, 7, 2, 11, 45),
|
|
137
|
+
expected = Duty(start=datetime.datetime(2023, 7, 2, 11, 45),
|
|
154
138
|
finish=datetime.datetime(2023, 7, 2, 19, 26),
|
|
155
139
|
sectors=(
|
|
156
140
|
Sector(name='LSBY', reg=None, type_=None,
|
|
@@ -158,13 +142,13 @@ class Test_duty(unittest.TestCase):
|
|
|
158
142
|
off=datetime.datetime(2023, 7, 2, 11, 45),
|
|
159
143
|
on=datetime.datetime(2023, 7, 2, 13, 20),
|
|
160
144
|
quasi=True, position=False,
|
|
161
|
-
crew=
|
|
145
|
+
crew=()),
|
|
162
146
|
Sector(name='ADTY', reg=None, type_=None,
|
|
163
147
|
from_='BRS', to='BRS',
|
|
164
148
|
off=datetime.datetime(2023, 7, 2, 13, 20),
|
|
165
149
|
on=datetime.datetime(2023, 7, 2, 14, 0),
|
|
166
150
|
quasi=True, position=False,
|
|
167
|
-
crew=
|
|
151
|
+
crew=()),
|
|
168
152
|
Sector(name='227', reg=None, type_='319',
|
|
169
153
|
from_='BRS', to='BFS',
|
|
170
154
|
off=datetime.datetime(2023, 7, 2, 16, 14),
|
|
@@ -189,8 +173,7 @@ class Test_duty(unittest.TestCase):
|
|
|
189
173
|
'A19:21 - A21:56', 'A22:33 - A00:57⁺¹/00:24'),
|
|
190
174
|
('01:27⁺¹',), ('07:08',), ('11:27',), ('M',),
|
|
191
175
|
crew, ())
|
|
192
|
-
expected = Duty(
|
|
193
|
-
start=datetime.datetime(2023, 6, 25, 14, 0),
|
|
176
|
+
expected = Duty(start=datetime.datetime(2023, 6, 25, 14, 0),
|
|
194
177
|
finish=datetime.datetime(2023, 6, 26, 1, 27),
|
|
195
178
|
sectors=(
|
|
196
179
|
Sector(name='239', reg=None, type_='320',
|
|
@@ -220,10 +203,6 @@ class Test_duty(unittest.TestCase):
|
|
|
220
203
|
)
|
|
221
204
|
self.assertEqual(roster._duty(src), expected)
|
|
222
205
|
|
|
223
|
-
def test_not_published(self):
|
|
224
|
-
src = ((), ('01/08/2024 Thu',), (), (), (), (), (), (), (), ('D',), ())
|
|
225
|
-
self.assertEqual(roster._duty(src), None)
|
|
226
|
-
|
|
227
206
|
def test_emptyrow(self):
|
|
228
207
|
with self.assertRaises(InputFileException):
|
|
229
208
|
roster._duty(())
|
|
@@ -232,8 +211,11 @@ class Test_duty(unittest.TestCase):
|
|
|
232
211
|
|
|
233
212
|
def test_bad_date(self):
|
|
234
213
|
with self.assertRaises(InputFileException):
|
|
235
|
-
roster._duty(((), ('
|
|
236
|
-
('
|
|
214
|
+
roster._duty(((), ('32/07/2024 Thu',),
|
|
215
|
+
('ESBY',), ('Early Standby',),
|
|
216
|
+
(), ('24:00 - 13:15',), (),
|
|
217
|
+
(), ('08:00',),
|
|
218
|
+
(), (), ()))
|
|
237
219
|
|
|
238
220
|
def test_bad_time(self):
|
|
239
221
|
with self.assertRaises(InputFileException):
|
|
@@ -242,3 +224,15 @@ class Test_duty(unittest.TestCase):
|
|
|
242
224
|
(), ('24:00 - 13:15',), (),
|
|
243
225
|
(), ('08:00',),
|
|
244
226
|
(), (), ()))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Test_ade(unittest.TestCase):
|
|
230
|
+
|
|
231
|
+
def test_simple(self):
|
|
232
|
+
src = ((),
|
|
233
|
+
('04/06/2023 Sun',),
|
|
234
|
+
('D/O',),
|
|
235
|
+
('Day off',),
|
|
236
|
+
(), (), (), (), (), (), (), ())
|
|
237
|
+
self.assertEqual(roster._ade(src),
|
|
238
|
+
AllDayEvent(datetime.date(2023, 6, 4), "D/O"))
|
aims_convert-2.1/aims/parse.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from bs4 import BeautifulSoup # type: ignore
|
|
2
|
-
|
|
3
|
-
from aims.data_structures import Duty, InputFileException
|
|
4
|
-
import aims.roster
|
|
5
|
-
import aims.logbook_report
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def parse(html: str) -> tuple[Duty, ...]:
|
|
9
|
-
# check it's an html5 file
|
|
10
|
-
html5_header = "<!DOCTYPE html><html>"
|
|
11
|
-
if html[:len(html5_header)] != html5_header:
|
|
12
|
-
raise InputFileException("HTML5 header not found.")
|
|
13
|
-
# make some soup
|
|
14
|
-
soup = BeautifulSoup(html, "html5lib")
|
|
15
|
-
# route the soup
|
|
16
|
-
strings = list(soup.find("body").stripped_strings)
|
|
17
|
-
if "Personal Crew Schedule Report" in strings:
|
|
18
|
-
return aims.roster.duties(soup)
|
|
19
|
-
elif "Pilot Logbook" in strings:
|
|
20
|
-
return aims.logbook_report.duties(soup)
|
|
21
|
-
else:
|
|
22
|
-
raise InputFileException(
|
|
23
|
-
"Report marker not found: \n" +
|
|
24
|
-
"; ".join(strings[:20]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|