efjtk 0.5__py3-none-any.whl
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.
- efjtk/__init__.py +0 -0
- efjtk/cli.py +78 -0
- efjtk/config.py +67 -0
- efjtk/convert.py +167 -0
- efjtk/gui.py +472 -0
- efjtk/logbook-template.html +201 -0
- efjtk/modify.py +121 -0
- efjtk/summary-template.html +100 -0
- efjtk-0.5.dist-info/LICENSE +674 -0
- efjtk-0.5.dist-info/METADATA +48 -0
- efjtk-0.5.dist-info/RECORD +14 -0
- efjtk-0.5.dist-info/WHEEL +5 -0
- efjtk-0.5.dist-info/entry_points.txt +5 -0
- efjtk-0.5.dist-info/top_level.txt +1 -0
efjtk/__init__.py
ADDED
|
File without changes
|
efjtk/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import os.path
|
|
7
|
+
|
|
8
|
+
import efj_parser
|
|
9
|
+
import efjtk.convert
|
|
10
|
+
import efjtk.modify
|
|
11
|
+
from efjtk.config import build_config, aircraft_classes
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _args():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description=(
|
|
17
|
+
"""Process an electronic Flight Journal (eFJ) file. Tools to aid in
|
|
18
|
+
manual creation of eFJ files (expand, night, vfr, ins, fo) and
|
|
19
|
+
tools to convert to useful formats (logbook, summary) are included.
|
|
20
|
+
Also included is a tool to help create a config file, which is
|
|
21
|
+
required for generation of the FCL.050 logbook."""))
|
|
22
|
+
parser.add_argument('format',
|
|
23
|
+
choices=['expand', 'night', 'vfr', 'ins', 'fo',
|
|
24
|
+
'logbook', 'summary',
|
|
25
|
+
'config'])
|
|
26
|
+
parser.add_argument('-c', '--config', default=None)
|
|
27
|
+
return parser.parse_args()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _config(filename: Optional[str]) -> str:
|
|
31
|
+
if filename and os.path.exists(filename):
|
|
32
|
+
with open(filename) as f:
|
|
33
|
+
return f.read()
|
|
34
|
+
else:
|
|
35
|
+
for filename in (os.path.expanduser("~/.efjtkrc"),
|
|
36
|
+
os.path.expanduser("~/.config/efjtkrc")):
|
|
37
|
+
if os.path.exists(filename):
|
|
38
|
+
with open(filename) as f:
|
|
39
|
+
return f.read()
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_func_map = {
|
|
44
|
+
"expand": efjtk.modify.expand_efj,
|
|
45
|
+
"night": efjtk.modify.add_night_data,
|
|
46
|
+
"summary": efjtk.convert.build_summary,
|
|
47
|
+
"vfr": efjtk.modify.add_vfr_flag,
|
|
48
|
+
"fo": efjtk.modify.add_fo_role_flag,
|
|
49
|
+
"ins": efjtk.modify.add_ins_flag,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> int:
|
|
54
|
+
args = _args()
|
|
55
|
+
data = sys.stdin.read()
|
|
56
|
+
try:
|
|
57
|
+
if args.format == "logbook":
|
|
58
|
+
ac_classes = aircraft_classes(_config(args.config))
|
|
59
|
+
print(efjtk.convert.build_logbook(data, ac_classes))
|
|
60
|
+
elif args.format == "config":
|
|
61
|
+
sys.stdout.write(
|
|
62
|
+
build_config(data, _config(args.config)))
|
|
63
|
+
elif args.format in _func_map:
|
|
64
|
+
print(_func_map[args.format](data))
|
|
65
|
+
else:
|
|
66
|
+
return -1
|
|
67
|
+
return 0
|
|
68
|
+
except efj_parser.ValidationError as ve:
|
|
69
|
+
print(str(ve), file=sys.stderr)
|
|
70
|
+
return -1
|
|
71
|
+
except efjtk.convert.UnknownAircraftType as t:
|
|
72
|
+
print(f"No class for type: {t}", file=sys.stderr)
|
|
73
|
+
return -3
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
retval = main()
|
|
78
|
+
sys.exit(retval)
|
efjtk/config.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import efj_parser as ep
|
|
3
|
+
import configparser as cp
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _parse_config(config: str) -> cp.ConfigParser:
|
|
7
|
+
"""Create a clean ConfigParser from a string
|
|
8
|
+
|
|
9
|
+
:param config: The contents of an INI file as a string
|
|
10
|
+
:return: A ConfigParser that is guaranteed to have an aircraft.classes
|
|
11
|
+
section, with the entries within guaranteed to have a valid value.
|
|
12
|
+
:raises configparser.Error: Any configparser exception when parsing the
|
|
13
|
+
string is not caught, so must be handled at a higher level
|
|
14
|
+
"""
|
|
15
|
+
parser = cp.ConfigParser()
|
|
16
|
+
parser.read_string(config) # raises cp.Error on failure
|
|
17
|
+
if "aircraft.classes" not in parser:
|
|
18
|
+
parser.add_section("aircraft.classes")
|
|
19
|
+
for name, value in parser.items("aircraft.classes"):
|
|
20
|
+
if value not in {"spse", "spme", "mc"}:
|
|
21
|
+
parser.remove_option("aircraft.classes", name)
|
|
22
|
+
return parser
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_config(in_: str, config: str, raise_on_error: bool = False) -> str:
|
|
26
|
+
"""Build a template for an INI file incorporating any unknown types
|
|
27
|
+
|
|
28
|
+
:param in_: An eFJ file in string form
|
|
29
|
+
:param config: An INI file in string form. This can be an empty string or
|
|
30
|
+
can be the contents of an existing INI file to update.
|
|
31
|
+
:param raise_on_error: If an exception occurs while parsing the config
|
|
32
|
+
string, reraise it rather than continuing with empty parser
|
|
33
|
+
:return: An updated INI file in string form. Any non pre-existing types are
|
|
34
|
+
added to the [aircraft.classes] section and assigned "spse" as a value.
|
|
35
|
+
:raises configparser.Error: Any configparser exception caused by errors in
|
|
36
|
+
config must be handled at higher level if raise_on_error is True
|
|
37
|
+
"""
|
|
38
|
+
_, sectors = ep.Parser().parse(in_)
|
|
39
|
+
try:
|
|
40
|
+
parser = _parse_config(config)
|
|
41
|
+
except cp.Error as e:
|
|
42
|
+
if raise_on_error:
|
|
43
|
+
raise e
|
|
44
|
+
parser = cp.ConfigParser()
|
|
45
|
+
parser.add_section("aircraft.classes")
|
|
46
|
+
for s in sectors:
|
|
47
|
+
if s.aircraft.type_ not in parser["aircraft.classes"]:
|
|
48
|
+
parser["aircraft.classes"][s.aircraft.type_] = "spse"
|
|
49
|
+
f = io.StringIO()
|
|
50
|
+
parser.write(f)
|
|
51
|
+
return f.getvalue()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def aircraft_classes(config: str) -> cp.SectionProxy:
|
|
55
|
+
"""Extract the [aircraft.classes] section from an INI string.
|
|
56
|
+
|
|
57
|
+
:param config: An INI file in string form
|
|
58
|
+
:return: A ConfigParser SectionProxy object. This can be treated as a non
|
|
59
|
+
case-sensitive dict, with the aircraft type as key and its category as
|
|
60
|
+
value. Uses empty parser if parsing of config fails.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
parser = _parse_config(config)
|
|
64
|
+
except cp.Error:
|
|
65
|
+
parser = cp.ConfigParser()
|
|
66
|
+
parser.add_section("aircraft_classes")
|
|
67
|
+
return parser["aircraft.classes"]
|
efjtk/convert.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import configparser as cp
|
|
2
|
+
import importlib.resources as res
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
5
|
+
import efj_parser as ep
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnknownAircraftType(Exception):
|
|
9
|
+
"""Aircraft type with no matching class encountered"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, type_):
|
|
12
|
+
self.missing_type = type_
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_template(filename):
|
|
16
|
+
template_file = res.files("efjtk").joinpath(filename)
|
|
17
|
+
with template_file.open() as f:
|
|
18
|
+
template = f.read()
|
|
19
|
+
for old, new in (("{", "{{"), ("}", "}}"),
|
|
20
|
+
("<!--{{", "{"), ("}}-->", "}")):
|
|
21
|
+
template = template.replace(old, new)
|
|
22
|
+
return template
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _duration(minutes):
|
|
26
|
+
if minutes:
|
|
27
|
+
return f"{minutes // 60}:{minutes % 60:02}"
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _aircraft_class_cells(
|
|
32
|
+
sector: ep.Sector,
|
|
33
|
+
ac_classes: cp.SectionProxy,
|
|
34
|
+
duration: str
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
if sector.aircraft.class_:
|
|
37
|
+
aircraft_class = sector.aircraft.class_
|
|
38
|
+
else:
|
|
39
|
+
try:
|
|
40
|
+
aircraft_class = ac_classes[sector.aircraft.type_]
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise UnknownAircraftType(sector.aircraft.type_)
|
|
43
|
+
if aircraft_class == "mc":
|
|
44
|
+
return ["", "", duration]
|
|
45
|
+
if aircraft_class == "spse":
|
|
46
|
+
return ["✓", "", ""]
|
|
47
|
+
return ["", "✓", ""] # must be "spme"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_logbook(in_: str, ac_classes: cp.SectionProxy) -> str:
|
|
51
|
+
_, sectors = ep.Parser().parse(in_)
|
|
52
|
+
rows = []
|
|
53
|
+
for s in sectors:
|
|
54
|
+
cells = [f"{s.start:%d/%m/%Y}",
|
|
55
|
+
s.airports.origin, f"{s.start:%H:%M}",
|
|
56
|
+
s.airports.dest,
|
|
57
|
+
f"{s.start + dt.timedelta(minutes=s.total):%H:%M}",
|
|
58
|
+
s.aircraft.type_, s.aircraft.reg]
|
|
59
|
+
duration = _duration(s.total)
|
|
60
|
+
cells.extend(_aircraft_class_cells(s, ac_classes, duration))
|
|
61
|
+
cells.append(duration)
|
|
62
|
+
cells.append(s.captain)
|
|
63
|
+
cells.extend([str(s.landings.day or ""), str(s.landings.night or "")])
|
|
64
|
+
night, ifr = "", ""
|
|
65
|
+
if s.conditions.night:
|
|
66
|
+
night = _duration(s.conditions.night)
|
|
67
|
+
if s.conditions.ifr:
|
|
68
|
+
ifr = _duration(s.conditions.ifr)
|
|
69
|
+
cells.extend([night, ifr])
|
|
70
|
+
cells.extend([_duration(X) if X else ""
|
|
71
|
+
for X in (s.roles.p1 + s.roles.p1s, s.roles.p2,
|
|
72
|
+
s.roles.put, s.roles.instructor)])
|
|
73
|
+
cells.append(s.comment)
|
|
74
|
+
rows.append(f"<tr><td>{'</td><td>'.join(cells)}</td></tr>")
|
|
75
|
+
return _get_template("logbook-template.html").format(rows="\n".join(rows))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_roles(sectors):
|
|
79
|
+
rpt = {}
|
|
80
|
+
for s in sectors:
|
|
81
|
+
type_ = s.aircraft.type_
|
|
82
|
+
roles = [s.roles.p1, s.roles.p1s, s.roles.p2, s.roles.put]
|
|
83
|
+
if s.aircraft.type_ not in rpt:
|
|
84
|
+
rpt[type_] = roles
|
|
85
|
+
else:
|
|
86
|
+
rpt[type_] = [X + Y for X, Y in zip(rpt[type_], roles)]
|
|
87
|
+
rows = []
|
|
88
|
+
role_total = [0, 0, 0, 0]
|
|
89
|
+
for type_ in sorted(rpt.keys()):
|
|
90
|
+
role_total = [X + Y for X, Y in zip(role_total, rpt[type_])]
|
|
91
|
+
total = sum(rpt[type_])
|
|
92
|
+
data = '</td><td>'.join([_duration(X) for X in rpt[type_]])
|
|
93
|
+
rows.append(f"<tr><th>{type_}</th><td>{data}</td>"
|
|
94
|
+
f"<td class='total'>{_duration(total)}</td></tr>")
|
|
95
|
+
data = '</td><td class="total">'.join([_duration(X) for X in role_total])
|
|
96
|
+
rows.append(f"<tr class='col_total'><th>Total</th><td class='total'>{data}"
|
|
97
|
+
f"</td><td class='total'>{_duration(sum(role_total))}"
|
|
98
|
+
f"</td></tr>")
|
|
99
|
+
return rows
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_conditions(sectors):
|
|
103
|
+
cond_pt = {}
|
|
104
|
+
for s in sectors:
|
|
105
|
+
type_ = s.aircraft.type_
|
|
106
|
+
conditions = [s.total - s.conditions.ifr, s.conditions.ifr,
|
|
107
|
+
s.total - s.conditions.night, s.conditions.night]
|
|
108
|
+
if s.aircraft.type_ not in cond_pt:
|
|
109
|
+
cond_pt[type_] = conditions
|
|
110
|
+
else:
|
|
111
|
+
cond_pt[type_] = [X + Y for X, Y in
|
|
112
|
+
zip(cond_pt[type_], conditions)]
|
|
113
|
+
rows = []
|
|
114
|
+
cond_total = [0, 0, 0, 0]
|
|
115
|
+
for type_ in sorted(cond_pt.keys()):
|
|
116
|
+
cond_total = [X + Y for X, Y in zip(cond_total, cond_pt[type_])]
|
|
117
|
+
data = '</td><td>'.join([_duration(X) for X in cond_pt[type_]])
|
|
118
|
+
rows.append(f"<tr><th>{type_}</th><td>{data}</td></tr>")
|
|
119
|
+
data = '</td><td class="total">'.join([_duration(X) for X in cond_total])
|
|
120
|
+
rows.append(f"<tr class='col_total'><th>Total</th>"
|
|
121
|
+
f"<td class='total'>{data}</td></tr>")
|
|
122
|
+
return rows
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_landings(sectors):
|
|
126
|
+
ldg_pt = {}
|
|
127
|
+
for s in sectors:
|
|
128
|
+
type_ = s.aircraft.type_
|
|
129
|
+
landings = [s.landings.day, s.landings.night]
|
|
130
|
+
if s.aircraft.type_ not in ldg_pt:
|
|
131
|
+
ldg_pt[type_] = landings
|
|
132
|
+
else:
|
|
133
|
+
ldg_pt[type_] = [X + Y for X, Y in
|
|
134
|
+
zip(ldg_pt[type_], landings)]
|
|
135
|
+
rows = []
|
|
136
|
+
landing_total = [0, 0]
|
|
137
|
+
for type_ in sorted(ldg_pt.keys()):
|
|
138
|
+
landing_total = [X + Y for X, Y in zip(landing_total, ldg_pt[type_])]
|
|
139
|
+
data = '</td><td>'.join([str(X) for X in ldg_pt[type_]])
|
|
140
|
+
total = sum(ldg_pt[type_])
|
|
141
|
+
rows.append(f"<tr><th>{type_}</th><td>{data}</td>"
|
|
142
|
+
f"<td class='total'>{total}</td></tr>")
|
|
143
|
+
data = '</td><td class="total">'.join([str(X) for X in landing_total])
|
|
144
|
+
rows.append(f"<tr class='col_total'><th>Total</th>"
|
|
145
|
+
f"<td class='total'>{data}</td>"
|
|
146
|
+
f"<td class='total'>{sum(landing_total)}</td></tr>")
|
|
147
|
+
return rows
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_summary(in_: str) -> str:
|
|
151
|
+
"""Build an HTML file with a summary table.
|
|
152
|
+
|
|
153
|
+
:param in_: An EFJ format text file as a string
|
|
154
|
+
:return: An HTML file as a string
|
|
155
|
+
"""
|
|
156
|
+
_, sectors = ep.Parser().parse(in_)
|
|
157
|
+
roles = _build_roles(sectors)
|
|
158
|
+
conditions = _build_conditions(sectors)
|
|
159
|
+
landings = _build_landings(sectors)
|
|
160
|
+
return _get_template("summary-template.html").format(
|
|
161
|
+
roles_body="\n".join(roles[:-1]),
|
|
162
|
+
roles_totals=roles[-1],
|
|
163
|
+
cond_body="\n".join(conditions[:-1]),
|
|
164
|
+
cond_totals=conditions[-1],
|
|
165
|
+
ldg_body="\n".join(landings[:-1]),
|
|
166
|
+
ldg_totals=landings[-1]
|
|
167
|
+
)
|