efj-parser 0.9.2__tar.gz → 0.9.3__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.
- {efj_parser-0.9.2 → efj_parser-0.9.3}/PKG-INFO +11 -2
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser/__init__.py +133 -118
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser.egg-info/PKG-INFO +11 -2
- {efj_parser-0.9.2 → efj_parser-0.9.3}/setup.py +1 -1
- {efj_parser-0.9.2 → efj_parser-0.9.3}/tests/test_parser.py +58 -74
- {efj_parser-0.9.2 → efj_parser-0.9.3}/LICENSE +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/README.md +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser/py.typed +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser.egg-info/SOURCES.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser.egg-info/dependency_links.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/efj_parser.egg-info/top_level.txt +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/pyproject.toml +0 -0
- {efj_parser-0.9.2 → efj_parser-0.9.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: efj-parser
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Parse an electronic Flight Journal file
|
|
5
5
|
Home-page: https://github.com/JonHurst/efj_parser
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -13,6 +13,15 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
16
25
|
|
|
17
26
|
# eFJ Parser #
|
|
18
27
|
|
|
@@ -12,7 +12,7 @@ class Duty(NamedTuple):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Crewmember(NamedTuple):
|
|
15
|
-
role: str #: The crewmember's role (e.g. CP
|
|
15
|
+
role: str #: The crewmember's role (e.g. ``CP``, ``FO``, ``PU``, ``FA``)
|
|
16
16
|
name: str #: The crewmember's name
|
|
17
17
|
|
|
18
18
|
|
|
@@ -37,7 +37,7 @@ class Landings(NamedTuple):
|
|
|
37
37
|
class Aircraft(NamedTuple):
|
|
38
38
|
reg: str #: The registration of the aircraft
|
|
39
39
|
type_: str #: The type of the aircraft
|
|
40
|
-
class_: str
|
|
40
|
+
class_: str #: Class: ``spse``, ``spme`` or ``mc``
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class Airports(NamedTuple):
|
|
@@ -51,12 +51,12 @@ class Sector(NamedTuple):
|
|
|
51
51
|
roles: Roles #: Minutes operating in each role
|
|
52
52
|
conditions: Conditions #: Minutes operating in ifr and at night
|
|
53
53
|
landings: Landings #: Number of day and night landings
|
|
54
|
-
aircraft: Aircraft #: Type and
|
|
54
|
+
aircraft: Aircraft #: Type, registration and class of the aircraft
|
|
55
55
|
airports: Airports #: Origin and destination airports
|
|
56
56
|
captain: str #: Name(s) of captain(s), Self for oneself
|
|
57
57
|
extra_flags: tuple[str, ...] #: Any flags not processed by the parser
|
|
58
58
|
comment: str #: Remarks
|
|
59
|
-
crew: tuple[Crewmember, ...] #: A (
|
|
59
|
+
crew: tuple[Crewmember, ...] #: A (possibly empty) list of crew
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class ValidationError(Exception):
|
|
@@ -82,6 +82,8 @@ class _VE(Exception):
|
|
|
82
82
|
|
|
83
83
|
ParseRet = Union[dt.date, Duty, Aircraft, list[Crewmember], Sector, str]
|
|
84
84
|
ParseHook = Optional[Callable[[str, int, str, ParseRet], None]]
|
|
85
|
+
Flag = tuple[str, Optional[int]]
|
|
86
|
+
Flags = tuple[Flag, ...]
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
class Parser():
|
|
@@ -89,13 +91,14 @@ class Parser():
|
|
|
89
91
|
def __init__(self) -> None:
|
|
90
92
|
self.date: Optional[dt.date] = None
|
|
91
93
|
self.airports = Airports("", "")
|
|
92
|
-
self.aircraft = Aircraft("", "")
|
|
94
|
+
self.aircraft = Aircraft("", "", "")
|
|
95
|
+
self.class_lookup: dict[str, str] = {}
|
|
93
96
|
self.captain = ""
|
|
94
97
|
self.crewlist: list[Crewmember] = []
|
|
95
98
|
|
|
96
99
|
def __parse_date(self, mo: re.Match) -> dt.date:
|
|
97
100
|
try:
|
|
98
|
-
self.date = dt.
|
|
101
|
+
self.date = dt.date.fromisoformat(mo.group(1))
|
|
99
102
|
return self.date
|
|
100
103
|
except ValueError:
|
|
101
104
|
raise _VE("Incorrect Date entry")
|
|
@@ -110,8 +113,8 @@ class Parser():
|
|
|
110
113
|
if not self.date:
|
|
111
114
|
raise _VE("Duty entry without preceding Date entry")
|
|
112
115
|
try:
|
|
113
|
-
t_start = dt.
|
|
114
|
-
t_end = dt.
|
|
116
|
+
t_start = dt.time.fromisoformat(mo.group(1))
|
|
117
|
+
t_end = dt.time.fromisoformat(mo.group(2))
|
|
115
118
|
except ValueError:
|
|
116
119
|
raise _VE("Invalid time string")
|
|
117
120
|
dt_start = dt.datetime.combine(self.date, t_start)
|
|
@@ -119,23 +122,23 @@ class Parser():
|
|
|
119
122
|
if dt_end < dt_start:
|
|
120
123
|
dt_end += dt.timedelta(days=1)
|
|
121
124
|
duration = int((dt_end - dt_start).total_seconds() / 60)
|
|
122
|
-
flags = tuple(X.strip() for X in mo.group(3).split())
|
|
123
125
|
ftl_correction = 0
|
|
124
126
|
unused_flags = []
|
|
125
|
-
for f in
|
|
126
|
-
if f
|
|
127
|
-
if
|
|
128
|
-
ftl_correction = int(mo_cor.group(1)[1:])
|
|
129
|
-
else:
|
|
130
|
-
ftl_correction = duration
|
|
127
|
+
for f in _split_flags(mo.group(3)):
|
|
128
|
+
if f[0] == "r":
|
|
129
|
+
ftl_correction += f[1] if f[1] else duration
|
|
131
130
|
else:
|
|
132
131
|
unused_flags.append(f)
|
|
133
132
|
comment = mo.group(4)[1:].strip() if mo.group(4) else ""
|
|
134
133
|
return Duty(dt_start, duration, ftl_correction,
|
|
135
|
-
tuple(unused_flags), comment)
|
|
134
|
+
_join_flags(tuple(unused_flags)), comment)
|
|
136
135
|
|
|
137
136
|
def __parse_aircraft(self, mo: re.Match) -> Aircraft:
|
|
138
|
-
|
|
137
|
+
reg, type_ = mo.group(1).strip(), mo.group(2).strip()
|
|
138
|
+
if mo.lastindex == 3:
|
|
139
|
+
self.class_lookup[type_] = mo.group(3).strip()
|
|
140
|
+
class_ = self.class_lookup.get(type_, "")
|
|
141
|
+
self.aircraft = Aircraft(reg, type_, class_)
|
|
139
142
|
return self.aircraft
|
|
140
143
|
|
|
141
144
|
def __parse_crewlist(self, mo: re.Match) -> list[Crewmember]:
|
|
@@ -170,22 +173,27 @@ class Parser():
|
|
|
170
173
|
if not mo.group(2) and not self.airports.origin: # Blank origin
|
|
171
174
|
raise _VE("Blank To without previous From")
|
|
172
175
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
mo.group(2) or self.airports.origin)
|
|
176
|
+
def __parse_sector_times(
|
|
177
|
+
self, t_start: str, t_end: str
|
|
178
|
+
) -> tuple[dt.datetime, int]:
|
|
177
179
|
try:
|
|
178
|
-
ts = dt.
|
|
179
|
-
te = dt.
|
|
180
|
+
ts = dt.time.fromisoformat(t_start) # Off blocks
|
|
181
|
+
te = dt.time.fromisoformat(t_end) # On blocks
|
|
180
182
|
except ValueError:
|
|
181
183
|
raise _VE("Incorrect time field")
|
|
182
|
-
flags = tuple(mo.group(5).split())
|
|
183
184
|
start = dt.datetime.combine(cast(dt.date, self.date), ts)
|
|
184
185
|
duration = int(
|
|
185
186
|
(dt.datetime.combine(cast(dt.date, self.date), te) - start)
|
|
186
187
|
.total_seconds() // 60)
|
|
187
188
|
if duration < 0:
|
|
188
189
|
duration += 1440
|
|
190
|
+
return (start, duration)
|
|
191
|
+
|
|
192
|
+
def __parse_sector_flags(
|
|
193
|
+
self,
|
|
194
|
+
flags: Flags,
|
|
195
|
+
duration: int
|
|
196
|
+
) -> tuple[Conditions, Roles, Landings, Flags]:
|
|
189
197
|
conditions, unused_flags = _process_conditions(flags, duration)
|
|
190
198
|
roles, unused_flags = _process_roles(unused_flags, duration)
|
|
191
199
|
if roles.p2 == duration:
|
|
@@ -193,7 +201,19 @@ class Parser():
|
|
|
193
201
|
else:
|
|
194
202
|
landings, unused_flags = _process_landings(unused_flags, duration,
|
|
195
203
|
conditions.night)
|
|
196
|
-
|
|
204
|
+
return conditions, roles, landings, unused_flags
|
|
205
|
+
|
|
206
|
+
def __parse_sector(self, mo: re.Match) -> Sector:
|
|
207
|
+
self.__pre_validate_sector(mo)
|
|
208
|
+
self.airports = Airports(mo.group(1) or self.airports.dest,
|
|
209
|
+
mo.group(2) or self.airports.origin)
|
|
210
|
+
start, duration = self.__parse_sector_times(mo.group(3), mo.group(4))
|
|
211
|
+
try:
|
|
212
|
+
flags = _split_flags(mo.group(5))
|
|
213
|
+
except ValueError:
|
|
214
|
+
raise _VE("Bad flags")
|
|
215
|
+
conditions, roles, landings, unused_flags = (
|
|
216
|
+
self.__parse_sector_flags(flags, duration))
|
|
197
217
|
if roles.p1 == duration:
|
|
198
218
|
self.captain = "Self"
|
|
199
219
|
else:
|
|
@@ -210,19 +230,16 @@ class Parser():
|
|
|
210
230
|
roles,
|
|
211
231
|
conditions,
|
|
212
232
|
landings,
|
|
213
|
-
self.aircraft
|
|
233
|
+
self.aircraft,
|
|
214
234
|
self.airports,
|
|
215
235
|
self.captain,
|
|
216
|
-
unused_flags,
|
|
236
|
+
_join_flags(unused_flags),
|
|
217
237
|
comment,
|
|
218
238
|
tuple(self.crewlist))
|
|
219
239
|
|
|
220
240
|
def __parse_comment(self, mo: re.Match) -> str:
|
|
221
241
|
return mo.group(1) or ""
|
|
222
242
|
|
|
223
|
-
def __invalid_syntax(self, mo: re.Match) -> None:
|
|
224
|
-
raise _VE("Bad syntax")
|
|
225
|
-
|
|
226
243
|
def parse(self, s: str, hook: ParseHook = None
|
|
227
244
|
) -> tuple[tuple[Duty, ...], tuple[Sector, ...]]:
|
|
228
245
|
"""Extract duties and sectors from an eFJ string
|
|
@@ -240,22 +257,21 @@ class Parser():
|
|
|
240
257
|
duties: list[Duty] = []
|
|
241
258
|
sectors: list[Sector] = []
|
|
242
259
|
func_map = [
|
|
243
|
-
(re.compile(r"\
|
|
244
|
-
self.
|
|
245
|
-
(re.compile(r"\
|
|
260
|
+
(re.compile(r"(\w*)/(\w*)\s*(\d{4})/(\d{4})([^#]*+)#?(.*)"),
|
|
261
|
+
self.__parse_sector, "sector"),
|
|
262
|
+
(re.compile(r"\{([^}]*)}"),
|
|
263
|
+
self.__parse_crewlist, "crewlist"),
|
|
264
|
+
(re.compile(r"(\d{4}-\d{2}-\d{2})"),
|
|
246
265
|
self.__parse_date, "date"),
|
|
247
|
-
(re.compile(r"\
|
|
248
|
-
self.__parse_nextdate, "short_date"),
|
|
249
|
-
(re.compile(r"\A(\d{4})/(\d{4})([^#]*)(#.+)?\Z"),
|
|
266
|
+
(re.compile(r"(\d{4})/(\d{4})([^#]*+)#?(.*)"),
|
|
250
267
|
self.__parse_duty, "duty"),
|
|
251
|
-
(re.compile(r"([-\w]{2,10})
|
|
268
|
+
(re.compile(r"([-\w]{2,10})(?>\s*:\s*)([-\w]+)"
|
|
269
|
+
r"(?:\s*:\s*(mc|spse|spme))?"),
|
|
252
270
|
self.__parse_aircraft, "aircraft"),
|
|
253
|
-
(re.compile(r"
|
|
254
|
-
self.
|
|
255
|
-
(re.compile(r"(
|
|
256
|
-
self.
|
|
257
|
-
(re.compile(r".+"),
|
|
258
|
-
self.__invalid_syntax, "invalid"),
|
|
271
|
+
(re.compile(r"(\++)"),
|
|
272
|
+
self.__parse_nextdate, "short_date"),
|
|
273
|
+
(re.compile(r"#(.*)"),
|
|
274
|
+
self.__parse_comment, "comment"),
|
|
259
275
|
]
|
|
260
276
|
for c, line in enumerate(s.splitlines()):
|
|
261
277
|
line = line.strip()
|
|
@@ -264,13 +280,15 @@ class Parser():
|
|
|
264
280
|
continue
|
|
265
281
|
ret = None
|
|
266
282
|
for rexp, func, entry_type in func_map:
|
|
267
|
-
if mo := rexp.
|
|
283
|
+
if mo := rexp.fullmatch(line):
|
|
268
284
|
try:
|
|
269
285
|
ret = func(mo)
|
|
270
286
|
except _VE as e:
|
|
271
287
|
raise ValidationError(c + 1, e.message, mo.group(0))
|
|
272
288
|
hook and hook(line, c + 1, entry_type, cast(ParseRet, ret))
|
|
273
289
|
break
|
|
290
|
+
else:
|
|
291
|
+
raise _VE("Bad syntax")
|
|
274
292
|
if isinstance(ret, Duty):
|
|
275
293
|
duties.append(ret)
|
|
276
294
|
elif isinstance(ret, Sector):
|
|
@@ -278,29 +296,43 @@ class Parser():
|
|
|
278
296
|
return (tuple(duties), tuple(sectors))
|
|
279
297
|
|
|
280
298
|
|
|
299
|
+
def _split_flags(flag_str: str) -> Flags:
|
|
300
|
+
"""Clean and split flag string.
|
|
301
|
+
|
|
302
|
+
:param flag_str: Input string such as "p2:20 ln:1 ins"
|
|
303
|
+
:returns: tuple of pairs like (("p2", 20), ("ln", 1), ("ins", None), ...)
|
|
304
|
+
:raises: ValueError if RHS of : is not convertible to integer
|
|
305
|
+
"""
|
|
306
|
+
fields = flag_str.strip().split()
|
|
307
|
+
return tuple((L[0], int(L[1])) if len(L) == 2 else (L[0], None)
|
|
308
|
+
for L in (X.split(":", 1) for X in fields))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _join_flags(flags: Flags) -> tuple[str, ...]:
|
|
312
|
+
"""Join Flags into a tuple of str"""
|
|
313
|
+
return tuple(f"{X[0]}:{X[1]}" if X[1] else X[0] for X in flags)
|
|
314
|
+
|
|
315
|
+
|
|
281
316
|
def _process_landings(
|
|
282
|
-
flags:
|
|
317
|
+
flags: Flags,
|
|
283
318
|
duration: int,
|
|
284
319
|
night: int
|
|
285
|
-
) -> tuple[Landings,
|
|
320
|
+
) -> tuple[Landings, Flags]:
|
|
286
321
|
night_ldg, day_ldg = 0, 0
|
|
287
322
|
found_landing_flag = False
|
|
288
|
-
|
|
289
|
-
re_night = re.compile(r"\Aln(:\d+)?\Z")
|
|
290
|
-
unused = []
|
|
323
|
+
unused: list[Flag] = []
|
|
291
324
|
for f in flags:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
unused.append(f)
|
|
325
|
+
match f[0]:
|
|
326
|
+
case "m":
|
|
327
|
+
found_landing_flag = True
|
|
328
|
+
case "ld":
|
|
329
|
+
day_ldg += 1 if f[1] is None else f[1]
|
|
330
|
+
found_landing_flag = True
|
|
331
|
+
case "ln":
|
|
332
|
+
night_ldg += 1 if f[1] is None else f[1]
|
|
333
|
+
found_landing_flag = True
|
|
334
|
+
case _:
|
|
335
|
+
unused.append(f)
|
|
304
336
|
if not found_landing_flag:
|
|
305
337
|
if duration == night:
|
|
306
338
|
night_ldg = 1
|
|
@@ -309,75 +341,58 @@ def _process_landings(
|
|
|
309
341
|
return Landings(day_ldg, night_ldg), tuple(unused)
|
|
310
342
|
|
|
311
343
|
|
|
312
|
-
def _process_roles(
|
|
313
|
-
|
|
314
|
-
duration: int
|
|
315
|
-
) -> tuple[Roles, tuple[str, ...]]:
|
|
316
|
-
"""Extract role durations from flags
|
|
344
|
+
def _process_roles(flags: Flags, duration: int) -> tuple[Roles, Flags]:
|
|
345
|
+
"""Extract role durations from sector flags
|
|
317
346
|
|
|
318
347
|
:param flags: Sector flags
|
|
348
|
+
:param duration: Total duration of sector
|
|
319
349
|
:return: A Roles object and the flags input parameter with processed flags
|
|
320
350
|
removed.
|
|
321
351
|
"""
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
re_role = re.compile(r"\A(p1s|p2|put|p0)(:\d+)?\Z")
|
|
325
|
-
re_inst = re.compile(r"\A(ins)(:\d+)?\Z")
|
|
326
|
-
unused: list[str] = []
|
|
352
|
+
p1s, p2, put, p0, ins = 0, 0, 0, 0, 0
|
|
353
|
+
unused: list[Flag] = []
|
|
327
354
|
for f in flags:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
match f[0]:
|
|
356
|
+
case "p1s":
|
|
357
|
+
p1s += f[1] if f[1] else duration
|
|
358
|
+
case "p2":
|
|
359
|
+
p2 += f[1] if f[1] else duration
|
|
360
|
+
case "put":
|
|
361
|
+
put += f[1] if f[1] else duration
|
|
362
|
+
case "p0":
|
|
363
|
+
p0 += f[1] if f[1] else duration
|
|
364
|
+
case "ins":
|
|
365
|
+
ins += f[1] if f[1] else duration
|
|
366
|
+
if ins > duration:
|
|
367
|
+
raise _VE("Bad instructor flag")
|
|
368
|
+
case _:
|
|
369
|
+
unused.append(f)
|
|
370
|
+
p1 = duration - (p1s + p2 + put + p0)
|
|
371
|
+
if p1 < 0:
|
|
372
|
+
raise _VE("Too many roles")
|
|
373
|
+
return Roles(p1, p1s, p2, put, ins), tuple(unused)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _process_conditions(flags: Flags, dur: int) -> tuple[Conditions, Flags]:
|
|
377
|
+
"""Extract sector conditions from sector flags
|
|
345
378
|
|
|
346
379
|
:param flags: The flags as a tuple of strings
|
|
380
|
+
:param dur: The total duration of the sector
|
|
347
381
|
:return: A tuple of the form (Conditions, unused_flags) where unused flags
|
|
348
382
|
is the input tuple with used flags removed.
|
|
349
383
|
"""
|
|
350
384
|
night, vfr = 0, 0
|
|
351
|
-
unused: list[
|
|
352
|
-
re_flag = re.compile(r"\A(n|v)(:\d+)?\Z")
|
|
385
|
+
unused: list[Flag] = []
|
|
353
386
|
for f in flags:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if mo.group(1) == "n":
|
|
359
|
-
night = duration
|
|
360
|
-
if mo.group(2):
|
|
361
|
-
night = int(mo.group(2)[1:])
|
|
362
|
-
if night > duration:
|
|
387
|
+
match f[0]:
|
|
388
|
+
case "n":
|
|
389
|
+
night += f[1] if f[1] else dur
|
|
390
|
+
if night > dur:
|
|
363
391
|
raise _VE("Night duration more than flight duration")
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
vfr = int(mo.group(2)[1:])
|
|
368
|
-
if vfr > duration:
|
|
392
|
+
case "v":
|
|
393
|
+
vfr += f[1] if f[1] else dur
|
|
394
|
+
if vfr > dur:
|
|
369
395
|
raise _VE("VFR duration more than flight duration")
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def _process_class_override(
|
|
374
|
-
flags: tuple[str, ...]
|
|
375
|
-
) -> tuple[str, tuple[str, ...]]:
|
|
376
|
-
retval = ""
|
|
377
|
-
unused = []
|
|
378
|
-
for f in flags:
|
|
379
|
-
if f in {"spse", "spme", "mc"}:
|
|
380
|
-
retval = f
|
|
381
|
-
else:
|
|
382
|
-
unused.append(f)
|
|
383
|
-
return retval, tuple(unused)
|
|
396
|
+
case _:
|
|
397
|
+
unused.append(f)
|
|
398
|
+
return Conditions(night, dur - vfr), tuple(unused)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: efj-parser
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Parse an electronic Flight Journal file
|
|
5
5
|
Home-page: https://github.com/JonHurst/efj_parser
|
|
6
6
|
Author: Jon Hurst
|
|
@@ -13,6 +13,15 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
16
25
|
|
|
17
26
|
# eFJ Parser #
|
|
18
27
|
|
|
@@ -10,10 +10,11 @@ class TestParser(unittest.TestCase):
|
|
|
10
10
|
data = """\
|
|
11
11
|
2024-01-21
|
|
12
12
|
1000/1430
|
|
13
|
-
N1:320
|
|
13
|
+
N1:320: mc
|
|
14
14
|
{FO: Bloggs}
|
|
15
15
|
BRS/BFS 1100/1200 ins #belfast
|
|
16
|
-
|
|
16
|
+
N2:320
|
|
17
|
+
/ 1300/1400 ins:20 test
|
|
17
18
|
{}
|
|
18
19
|
+
|
|
19
20
|
1000/1610 # Comment
|
|
@@ -47,7 +48,7 @@ OB-T-1274:A-321
|
|
|
47
48
|
efj.Roles(p1=60, instructor=60),
|
|
48
49
|
efj.Conditions(ifr=60),
|
|
49
50
|
efj.Landings(day=1),
|
|
50
|
-
efj.Aircraft("N1", "320"),
|
|
51
|
+
efj.Aircraft("N1", "320", "mc"),
|
|
51
52
|
efj.Airports("BRS", "BFS"),
|
|
52
53
|
"Self", (), "belfast",
|
|
53
54
|
(efj.Crewmember("FO", "Bloggs"),)),
|
|
@@ -56,7 +57,7 @@ OB-T-1274:A-321
|
|
|
56
57
|
efj.Roles(p1=60, instructor=20),
|
|
57
58
|
efj.Conditions(ifr=60),
|
|
58
59
|
efj.Landings(day=1),
|
|
59
|
-
efj.Aircraft("
|
|
60
|
+
efj.Aircraft("N2", "320", "mc"),
|
|
60
61
|
efj.Airports("BFS", "BRS"),
|
|
61
62
|
"Self", ("test",), "",
|
|
62
63
|
(efj.Crewmember("FO", "Bloggs"),)),
|
|
@@ -65,7 +66,7 @@ OB-T-1274:A-321
|
|
|
65
66
|
efj.Roles(p1=120),
|
|
66
67
|
efj.Conditions(ifr=120),
|
|
67
68
|
efj.Landings(day=3),
|
|
68
|
-
efj.Aircraft("OB-T-1274", "A-321"),
|
|
69
|
+
efj.Aircraft("OB-T-1274", "A-321", ""),
|
|
69
70
|
efj.Airports("BRS", "NCE"),
|
|
70
71
|
"Self", (), "", ()),
|
|
71
72
|
efj.Sector(
|
|
@@ -73,7 +74,7 @@ OB-T-1274:A-321
|
|
|
73
74
|
efj.Roles(p1=120),
|
|
74
75
|
efj.Conditions(night=10, ifr=90),
|
|
75
76
|
efj.Landings(night=1),
|
|
76
|
-
efj.Aircraft("OB-T-1274", "A-321"),
|
|
77
|
+
efj.Aircraft("OB-T-1274", "A-321", ""),
|
|
77
78
|
efj.Airports("NCE", "BRS"),
|
|
78
79
|
"Self", (), "", ()))
|
|
79
80
|
self.assertEqual(
|
|
@@ -110,7 +111,7 @@ G-EFGH:321
|
|
|
110
111
|
efj.Roles(p1s=60),
|
|
111
112
|
efj.Conditions(ifr=60),
|
|
112
113
|
efj.Landings(day=1),
|
|
113
|
-
efj.Aircraft("G-ABCD", "320"),
|
|
114
|
+
efj.Aircraft("G-ABCD", "320", ""),
|
|
114
115
|
efj.Airports("BRS", "BFS"),
|
|
115
116
|
"Bloggs Joe", (), "belfast",
|
|
116
117
|
(efj.Crewmember("CP", "Bloggs Joe"),)),
|
|
@@ -119,7 +120,7 @@ G-EFGH:321
|
|
|
119
120
|
efj.Roles(p2=60),
|
|
120
121
|
efj.Conditions(ifr=60),
|
|
121
122
|
efj.Landings(),
|
|
122
|
-
efj.Aircraft("G-ABCD", "320"),
|
|
123
|
+
efj.Aircraft("G-ABCD", "320", ""),
|
|
123
124
|
efj.Airports("BFS", "BRS"),
|
|
124
125
|
"Bloggs Joe", (), "",
|
|
125
126
|
(efj.Crewmember("CP", "Bloggs Joe"),)),
|
|
@@ -128,7 +129,7 @@ G-EFGH:321
|
|
|
128
129
|
efj.Roles(p2=120),
|
|
129
130
|
efj.Conditions(ifr=120),
|
|
130
131
|
efj.Landings(),
|
|
131
|
-
efj.Aircraft("G-EFGH", "321"),
|
|
132
|
+
efj.Aircraft("G-EFGH", "321", ""),
|
|
132
133
|
efj.Airports("BRS", "NCE"),
|
|
133
134
|
"Pugwash", (), "", (
|
|
134
135
|
efj.Crewmember("CP", "Pugwash"),
|
|
@@ -138,7 +139,7 @@ G-EFGH:321
|
|
|
138
139
|
efj.Roles(p1=90, p1s=30),
|
|
139
140
|
efj.Conditions(ifr=120),
|
|
140
141
|
efj.Landings(day=1),
|
|
141
|
-
efj.Aircraft("G-EFGH", "321"),
|
|
142
|
+
efj.Aircraft("G-EFGH", "321", ""),
|
|
142
143
|
efj.Airports("NCE", "BRS"),
|
|
143
144
|
"Self, Pugwash", (), "", (
|
|
144
145
|
efj.Crewmember("CP", "Pugwash"),
|
|
@@ -149,7 +150,7 @@ G-EFGH:321
|
|
|
149
150
|
(expected_duties, expected_sectors))
|
|
150
151
|
|
|
151
152
|
def test_flags(self):
|
|
152
|
-
aircraft = efj.Aircraft("G-ABCD", "320")
|
|
153
|
+
aircraft = efj.Aircraft("G-ABCD", "320", "")
|
|
153
154
|
roles = efj.Roles(p1=60)
|
|
154
155
|
airports = efj.Airports("BRS", "BFS")
|
|
155
156
|
with self.subTest("Instructor flag"):
|
|
@@ -294,7 +295,7 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
294
295
|
(efj.Landings(day=1, night=0), ()))
|
|
295
296
|
with self.subTest("PM, day"):
|
|
296
297
|
self.assertEqual(
|
|
297
|
-
efj._process_landings(("m",), 1, 0),
|
|
298
|
+
efj._process_landings((("m", None),), 1, 0),
|
|
298
299
|
(efj.Landings(day=0, night=0), ()))
|
|
299
300
|
with self.subTest("No flags, night"):
|
|
300
301
|
self.assertEqual(
|
|
@@ -302,7 +303,7 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
302
303
|
(efj.Landings(day=0, night=1), ()))
|
|
303
304
|
with self.subTest("PM, night"):
|
|
304
305
|
self.assertEqual(
|
|
305
|
-
efj._process_landings(("m",), 1, 1),
|
|
306
|
+
efj._process_landings((("m", None),), 1, 1),
|
|
306
307
|
(efj.Landings(day=0, night=0), ()))
|
|
307
308
|
with self.subTest("Partial night, day landing"):
|
|
308
309
|
self.assertEqual(
|
|
@@ -310,41 +311,37 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
310
311
|
(efj.Landings(day=1, night=0), ()))
|
|
311
312
|
with self.subTest("Partial night, night landing"):
|
|
312
313
|
self.assertEqual(
|
|
313
|
-
efj._process_landings(("ln",), 2, 1),
|
|
314
|
+
efj._process_landings((("ln", None),), 2, 1),
|
|
314
315
|
(efj.Landings(day=0, night=1), ()))
|
|
315
316
|
with self.subTest("Multiple day landings"):
|
|
316
317
|
self.assertEqual(
|
|
317
|
-
efj._process_landings(("ld
|
|
318
|
+
efj._process_landings((("ld", 2),), 2, 0),
|
|
318
319
|
(efj.Landings(day=2, night=0), ()))
|
|
319
320
|
with self.subTest("Multiple night landings"):
|
|
320
321
|
self.assertEqual(
|
|
321
|
-
efj._process_landings(("ln
|
|
322
|
+
efj._process_landings((("ln", 2),), 2, 1),
|
|
322
323
|
(efj.Landings(day=0, night=2), ()))
|
|
323
324
|
with self.subTest("Mix of landings, single"):
|
|
324
325
|
self.assertEqual(
|
|
325
|
-
efj._process_landings(("ln", "ld"), 2, 1),
|
|
326
|
+
efj._process_landings((("ln", None), ("ld", None)), 2, 1),
|
|
326
327
|
(efj.Landings(day=1, night=1), ()))
|
|
327
328
|
with self.subTest("Mix of landings, multi"):
|
|
328
329
|
self.assertEqual(
|
|
329
|
-
efj._process_landings(("ln
|
|
330
|
+
efj._process_landings((("ln", 2), ("ld", None)), 2, 1),
|
|
330
331
|
(efj.Landings(day=1, night=2), ()))
|
|
331
332
|
with self.subTest("Zero landings, day flag"):
|
|
332
333
|
self.assertEqual(
|
|
333
|
-
efj._process_landings(("ld
|
|
334
|
+
efj._process_landings((("ld", 0),), 2, 1),
|
|
334
335
|
(efj.Landings(day=0, night=0), ()))
|
|
335
336
|
with self.subTest("Zero landings, night flag"):
|
|
336
337
|
self.assertEqual(
|
|
337
|
-
efj._process_landings(("ln
|
|
338
|
+
efj._process_landings((("ln", 0),), 2, 1),
|
|
338
339
|
(efj.Landings(day=0, night=0), ()))
|
|
339
340
|
with self.subTest("Day landing, night flight"):
|
|
340
341
|
# Assume that user did this for a reason
|
|
341
342
|
self.assertEqual(
|
|
342
|
-
efj._process_landings(("ld",), 2, 2),
|
|
343
|
+
efj._process_landings((("ld", None),), 2, 2),
|
|
343
344
|
(efj.Landings(day=1, night=0), ()))
|
|
344
|
-
with self.subTest("Bad quantity"):
|
|
345
|
-
self.assertEqual(
|
|
346
|
-
efj._process_landings(("ln:xyz",), 1, 0),
|
|
347
|
-
(efj.Landings(1, 0), ("ln:xyz",)))
|
|
348
345
|
|
|
349
346
|
def test_roles(self):
|
|
350
347
|
with self.subTest("All p1"):
|
|
@@ -353,56 +350,52 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
353
350
|
(efj.Roles(p1=1), ()))
|
|
354
351
|
with self.subTest("All p1s"):
|
|
355
352
|
self.assertEqual(
|
|
356
|
-
efj._process_roles(("p1s",), 1),
|
|
353
|
+
efj._process_roles((("p1s", None),), 1),
|
|
357
354
|
(efj.Roles(p1s=1), ()))
|
|
358
355
|
with self.subTest("All p2"):
|
|
359
356
|
self.assertEqual(
|
|
360
|
-
efj._process_roles(("p2",), 1),
|
|
357
|
+
efj._process_roles((("p2", None),), 1),
|
|
361
358
|
(efj.Roles(p2=1), ()))
|
|
362
359
|
with self.subTest("All put"):
|
|
363
360
|
self.assertEqual(
|
|
364
|
-
efj._process_roles(("put",), 1),
|
|
361
|
+
efj._process_roles((("put", None),), 1),
|
|
365
362
|
(efj.Roles(put=1), ()))
|
|
366
363
|
with self.subTest("Split roles, put & p1"):
|
|
367
364
|
self.assertEqual(
|
|
368
|
-
efj._process_roles(("put
|
|
365
|
+
efj._process_roles((("put", 1),), 2),
|
|
369
366
|
(efj.Roles(p1=1, put=1), ()))
|
|
370
367
|
with self.subTest("Split roles, p1s & p1"):
|
|
371
368
|
self.assertEqual(
|
|
372
|
-
efj._process_roles(("p1s
|
|
369
|
+
efj._process_roles((("p1s", 1),), 2),
|
|
373
370
|
(efj.Roles(p1=1, p1s=1), ()))
|
|
374
371
|
with self.subTest("Split roles, p2 & p1"):
|
|
375
372
|
self.assertEqual(
|
|
376
|
-
efj._process_roles(("p2
|
|
373
|
+
efj._process_roles((("p2", 1),), 2),
|
|
377
374
|
(efj.Roles(p1=1, p2=1), ()))
|
|
378
375
|
with self.subTest("Split roles, p1s & put"):
|
|
379
376
|
self.assertEqual(
|
|
380
|
-
efj._process_roles(("p1s
|
|
377
|
+
efj._process_roles((("p1s", 1), ("put", 1)), 2),
|
|
381
378
|
(efj.Roles(p1s=1, put=1), ()))
|
|
382
379
|
with self.subTest("Role duration > duration"):
|
|
383
380
|
with self.assertRaises(efj._VE) as ve:
|
|
384
|
-
efj._process_roles(("p1s
|
|
381
|
+
efj._process_roles((("p1s", 2),), 1)
|
|
385
382
|
self.assertEqual(ve.exception.message, "Too many roles")
|
|
386
383
|
with self.subTest("Two untimed roles"):
|
|
387
384
|
with self.assertRaises(efj._VE) as ve:
|
|
388
|
-
efj._process_roles(("p1s", "put"), 1)
|
|
385
|
+
efj._process_roles((("p1s", None), ("put", None)), 1)
|
|
389
386
|
self.assertEqual(ve.exception.message, "Too many roles")
|
|
390
387
|
with self.subTest("Instructor flag"):
|
|
391
388
|
self.assertEqual(
|
|
392
|
-
efj._process_roles(("ins",), 1),
|
|
389
|
+
efj._process_roles((("ins", None),), 1),
|
|
393
390
|
(efj.Roles(p1=1, instructor=1), ()))
|
|
394
391
|
with self.subTest("Instructor flag, partial"):
|
|
395
392
|
self.assertEqual(
|
|
396
|
-
efj._process_roles(("ins
|
|
393
|
+
efj._process_roles((("ins", 1),), 2),
|
|
397
394
|
(efj.Roles(p1=2, instructor=1), ()))
|
|
398
395
|
with self.subTest("Unknown role"):
|
|
399
396
|
self.assertEqual(
|
|
400
|
-
efj._process_roles(("p3",), 2),
|
|
401
|
-
(efj.Roles(p1=2), ("p3",)))
|
|
402
|
-
with self.subTest("Bad duration"):
|
|
403
|
-
self.assertEqual(
|
|
404
|
-
efj._process_roles(("p1s:xyz",), 1),
|
|
405
|
-
(efj.Roles(p1=1), ("p1s:xyz",)))
|
|
397
|
+
efj._process_roles((("p3", None),), 2),
|
|
398
|
+
(efj.Roles(p1=2), (("p3", None),)))
|
|
406
399
|
|
|
407
400
|
def test_conditions(self):
|
|
408
401
|
with self.subTest("No flags"):
|
|
@@ -411,57 +404,48 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
411
404
|
(efj.Conditions(ifr=1, night=0), ()))
|
|
412
405
|
with self.subTest("VFR flag"):
|
|
413
406
|
self.assertEqual(
|
|
414
|
-
efj._process_conditions(("v",), 1),
|
|
407
|
+
efj._process_conditions((("v", None),), 1),
|
|
415
408
|
(efj.Conditions(ifr=0, night=0), ()))
|
|
416
409
|
with self.subTest("Night flag"):
|
|
417
410
|
self.assertEqual(
|
|
418
|
-
efj._process_conditions(("n",), 1),
|
|
411
|
+
efj._process_conditions((("n", None),), 1),
|
|
419
412
|
(efj.Conditions(ifr=1, night=1), ()))
|
|
420
413
|
with self.subTest("VFR at night"):
|
|
421
414
|
self.assertEqual(
|
|
422
|
-
efj._process_conditions(("n", "v"), 1),
|
|
415
|
+
efj._process_conditions((("n", None), ("v", None)), 1),
|
|
423
416
|
(efj.Conditions(ifr=0, night=1), ()))
|
|
424
417
|
with self.subTest("Part VFR"):
|
|
425
418
|
self.assertEqual(
|
|
426
|
-
efj._process_conditions(("v
|
|
419
|
+
efj._process_conditions((("v", 1),), 2),
|
|
427
420
|
(efj.Conditions(ifr=1, night=0), ()))
|
|
428
421
|
with self.subTest("Part Night"):
|
|
429
422
|
self.assertEqual(
|
|
430
|
-
efj._process_conditions(("n
|
|
423
|
+
efj._process_conditions((("n", 1),), 2),
|
|
431
424
|
(efj.Conditions(ifr=2, night=1), ()))
|
|
432
|
-
with self.subTest("Bad duration"):
|
|
433
|
-
self.assertEqual(
|
|
434
|
-
efj._process_conditions(("v:xyz",), 2),
|
|
435
|
-
(efj.Conditions(ifr=2, night=0), ("v:xyz",)))
|
|
436
425
|
with self.subTest("VFR duration > duration"):
|
|
437
426
|
with self.assertRaises(efj._VE) as ve:
|
|
438
|
-
efj._process_conditions(("v
|
|
427
|
+
efj._process_conditions((("v", 2),), 1),
|
|
439
428
|
self.assertEqual(ve.exception.message,
|
|
440
429
|
"VFR duration more than flight duration")
|
|
441
430
|
with self.subTest("Night duration > duration"):
|
|
442
431
|
with self.assertRaises(efj._VE) as ve:
|
|
443
|
-
efj._process_conditions(("n
|
|
432
|
+
efj._process_conditions((("n", 2),), 1),
|
|
444
433
|
self.assertEqual(ve.exception.message,
|
|
445
434
|
"Night duration more than flight duration")
|
|
446
435
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
("", ("test",)))
|
|
464
|
-
with self.subTest("Multiple overrides"):
|
|
465
|
-
self.assertEqual(
|
|
466
|
-
efj._process_class_override(("spse", "spme", "mc")),
|
|
467
|
-
("mc", ()))
|
|
436
|
+
|
|
437
|
+
class TestUtility(unittest.TestCase):
|
|
438
|
+
|
|
439
|
+
def test_split_flags(self):
|
|
440
|
+
res = tuple(efj._split_flags(" p2 put:20 ln:1 ins "))
|
|
441
|
+
exp = (("p2", None), ("put", 20), ("ln", 1), ("ins", None))
|
|
442
|
+
self.assertEqual(res, exp)
|
|
443
|
+
with self.assertRaises(ValueError):
|
|
444
|
+
tuple(efj._split_flags("ln:1 ab:no"))
|
|
445
|
+
|
|
446
|
+
def test_join_flags(self):
|
|
447
|
+
res = efj._join_flags(
|
|
448
|
+
(("p2", None), ("put", 20), ("ln", 1), ("ins", None))
|
|
449
|
+
)
|
|
450
|
+
exp = ("p2", "put:20", "ln:1", "ins")
|
|
451
|
+
self.assertEqual(res, exp)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|