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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: efj-parser
3
- Version: 0.9.2
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, FO, PU, FA)
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 = "" #: Override to class based on type: spse, spme or mc
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 registration of the aircraft
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 (possible empty) list of crew
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.datetime.strptime(mo.group(1), "%Y-%m-%d")
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.datetime.strptime(mo.group(1), '%H%M').time()
114
- t_end = dt.datetime.strptime(mo.group(2), '%H%M').time()
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 flags:
126
- if f and (mo_cor := re.match(r"r(:\d+)?", f)):
127
- if mo_cor.group(1):
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
- self.aircraft = Aircraft(mo.group(1).strip(), mo.group(2).strip())
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 __parse_sector(self, mo: re.Match) -> Sector:
174
- self.__pre_validate_sector(mo)
175
- self.airports = Airports(mo.group(1) or self.airports.dest,
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.datetime.strptime(mo.group(3), '%H%M').time() # Off blocks
179
- te = dt.datetime.strptime(mo.group(4), '%H%M').time() # On blocks
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
- aircraft_class, unused_flags = _process_class_override(unused_flags)
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._replace(class_=aircraft_class),
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"\A#(.*)\Z"),
244
- self.__parse_comment, "comment"),
245
- (re.compile(r"\A(\d{4}-\d{2}-\d{2})\Z"),
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"\A\s*(\++)\s*\Z"),
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})\s*:\s*([-\w]+)"),
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"\{([^}]*)}\Z"),
254
- self.__parse_crewlist, "crewlist"),
255
- (re.compile(r"(\w+)?/(\w+)?\s*(\d{4})/(\d{4})([^#]*)?(#.+)?"),
256
- self.__parse_sector, "sector"),
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.match(line):
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: tuple[str, ...],
317
+ flags: Flags,
283
318
  duration: int,
284
319
  night: int
285
- ) -> tuple[Landings, tuple[str, ...]]:
320
+ ) -> tuple[Landings, Flags]:
286
321
  night_ldg, day_ldg = 0, 0
287
322
  found_landing_flag = False
288
- re_day = re.compile(r"\Ald(:\d+)?\Z")
289
- re_night = re.compile(r"\Aln(:\d+)?\Z")
290
- unused = []
323
+ unused: list[Flag] = []
291
324
  for f in flags:
292
- if f == "m":
293
- night_ldg, day_ldg = 0, 0
294
- found_landing_flag = True
295
- break
296
- if mo := re_day.match(f):
297
- day_ldg = int(mo.group(1)[1:]) if mo.group(1) else 1
298
- found_landing_flag = True
299
- elif mo := re_night.match(f):
300
- night_ldg = int(mo.group(1)[1:]) if mo.group(1) else 1
301
- found_landing_flag = True
302
- else:
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
- flags: tuple[str, ...],
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
- p1, inst = duration, 0
323
- roles = [0, 0, 0, 0]
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
- if mo := re_role.match(f):
329
- idx = ("p1s", "p2", "put", "p0").index(mo.group(1))
330
- roles[idx] = int(mo.group(2)[1:]) if mo.group(2) else duration
331
- p1 -= roles[idx]
332
- if p1 < 0:
333
- raise _VE("Too many roles")
334
- elif mo := re_inst.match(f):
335
- inst = int(mo.group(2)[1:]) if mo.group(2) else duration
336
- else:
337
- unused.append(f)
338
- return Roles(p1, roles[0], roles[1], roles[2], inst), tuple(unused)
339
-
340
-
341
- def _process_conditions(
342
- flags: tuple[str, ...], duration: int
343
- ) -> tuple[Conditions, tuple[str, ...]]:
344
- """Processes sector condition flags
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[str] = []
352
- re_flag = re.compile(r"\A(n|v)(:\d+)?\Z")
385
+ unused: list[Flag] = []
353
386
  for f in flags:
354
- mo = re_flag.match(f)
355
- if not mo:
356
- unused.append(f)
357
- continue
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
- elif mo.group(1) == "v":
365
- vfr = duration
366
- if mo.group(2):
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
- return Conditions(night, duration - vfr), tuple(unused)
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
1
+ Metadata-Version: 2.2
2
2
  Name: efj-parser
3
- Version: 0.9.2
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
 
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="efj-parser",
8
- version="0.9.2",
8
+ version="0.9.3",
9
9
  author="Jon Hurst",
10
10
  author_email="jon.a@hursts.org.uk",
11
11
  description="Parse an electronic Flight Journal file",
@@ -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
- / 1300/1400 ins:20 test spme
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("N1", "320", "spme"),
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:2",), 2, 0),
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:2",), 2, 1),
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:2", "ld"), 2, 1),
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:0",), 2, 1),
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:0",), 2, 1),
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:1",), 2),
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:1",), 2),
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:1",), 2),
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:1", "put:1"), 2),
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:2",), 1)
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:1",), 2),
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:1",), 2),
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:1",), 2),
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:2",), 1),
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:2",), 1),
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
- def test_class_override(self):
448
- with self.subTest("SPSE"):
449
- self.assertEqual(
450
- efj._process_class_override(("spse",)),
451
- ("spse", ()))
452
- with self.subTest("SPME"):
453
- self.assertEqual(
454
- efj._process_class_override(("spme",)),
455
- ("spme", ()))
456
- with self.subTest("MC"):
457
- self.assertEqual(
458
- efj._process_class_override(("mc",)),
459
- ("mc", ()))
460
- with self.subTest("SPSE"):
461
- self.assertEqual(
462
- efj._process_class_override(("test",)),
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