aims-convert 1.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-1.2/PKG-INFO +50 -0
- aims-convert-1.2/README.md +33 -0
- aims-convert-1.2/aims/__init__.py +0 -0
- aims-convert-1.2/aims/airframe_lookup.py +20 -0
- aims-convert-1.2/aims/cli.py +43 -0
- aims-convert-1.2/aims/gui.py +439 -0
- aims-convert-1.2/aims/output.py +260 -0
- aims-convert-1.2/aims/roster.py +500 -0
- aims-convert-1.2/aims_convert.egg-info/PKG-INFO +50 -0
- aims-convert-1.2/aims_convert.egg-info/SOURCES.txt +16 -0
- aims-convert-1.2/aims_convert.egg-info/dependency_links.txt +1 -0
- aims-convert-1.2/aims_convert.egg-info/entry_points.txt +5 -0
- aims-convert-1.2/aims_convert.egg-info/requires.txt +2 -0
- aims-convert-1.2/aims_convert.egg-info/top_level.txt +1 -0
- aims-convert-1.2/pyproject.toml +3 -0
- aims-convert-1.2/setup.cfg +4 -0
- aims-convert-1.2/setup.py +35 -0
- aims-convert-1.2/tests/test_detailed_roster.py +201 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aims-convert
|
|
3
|
+
Version: 1.2
|
|
4
|
+
Summary: Extract information from AIMS detailed roster
|
|
5
|
+
Home-page: https://github.com/JonHurst/aims-convert
|
|
6
|
+
Author: Jon Hurst
|
|
7
|
+
Author-email: jon.a@hursts.org.uk
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: nightflight
|
|
17
|
+
|
|
18
|
+
# AIMS Roster Data Extraction #
|
|
19
|
+
|
|
20
|
+
This will primarily be of interest to easyJet pilots. It extracts data from a
|
|
21
|
+
“detailed roster” as can be downloaded from AIMS. Pilots from other airlines
|
|
22
|
+
that use AIMS *may* also find this useful — it has only been tested against the
|
|
23
|
+
files from easyJet’s version of AIMS (as that is all that I have access to), so
|
|
24
|
+
its utility will depend on whether whatever version is being used by your
|
|
25
|
+
airline produces sufficiently similar output.
|
|
26
|
+
|
|
27
|
+
As well as extracting the data from the AIMS roster, the registration and type
|
|
28
|
+
of the aircraft operating the sectors is looked up, and a night flying
|
|
29
|
+
calculation is carried out.
|
|
30
|
+
|
|
31
|
+
The three main formats that the data can be extracted to are electronic Flight
|
|
32
|
+
Journal (eFJ), iCalendar and CSV.
|
|
33
|
+
|
|
34
|
+
An eFJ is a text file that can be used to store personal flight data in an
|
|
35
|
+
intuitive non-tabular form. Full details of the format of this text file can be
|
|
36
|
+
found at <https://hursts.org.uk/efjdocs/format.html>, and an online tool
|
|
37
|
+
capable of converting this format to an FCL.050 compliant logbook in HTML
|
|
38
|
+
format can be found at <https://hursts.org.uk/efj>.
|
|
39
|
+
|
|
40
|
+
The iCalendar format can be imported into most calendar applications. There is
|
|
41
|
+
an option to include full day events such as days off. This is useful for
|
|
42
|
+
managing and sharing your future roster.
|
|
43
|
+
|
|
44
|
+
The CSV format is for keeping logbook data in a spreadsheet.
|
|
45
|
+
|
|
46
|
+
The package provides entry points for a command line interface, a Tk based
|
|
47
|
+
graphical interface and an AWS lambda function. The AWS function is hooked up
|
|
48
|
+
to a website at <https://hursts.org.uk/aims>, which requires no installation,
|
|
49
|
+
just a browser. The other two interfaces require the installation of a Python
|
|
50
|
+
interpreter.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# AIMS Roster Data Extraction #
|
|
2
|
+
|
|
3
|
+
This will primarily be of interest to easyJet pilots. It extracts data from a
|
|
4
|
+
“detailed roster” as can be downloaded from AIMS. Pilots from other airlines
|
|
5
|
+
that use AIMS *may* also find this useful — it has only been tested against the
|
|
6
|
+
files from easyJet’s version of AIMS (as that is all that I have access to), so
|
|
7
|
+
its utility will depend on whether whatever version is being used by your
|
|
8
|
+
airline produces sufficiently similar output.
|
|
9
|
+
|
|
10
|
+
As well as extracting the data from the AIMS roster, the registration and type
|
|
11
|
+
of the aircraft operating the sectors is looked up, and a night flying
|
|
12
|
+
calculation is carried out.
|
|
13
|
+
|
|
14
|
+
The three main formats that the data can be extracted to are electronic Flight
|
|
15
|
+
Journal (eFJ), iCalendar and CSV.
|
|
16
|
+
|
|
17
|
+
An eFJ is a text file that can be used to store personal flight data in an
|
|
18
|
+
intuitive non-tabular form. Full details of the format of this text file can be
|
|
19
|
+
found at <https://hursts.org.uk/efjdocs/format.html>, and an online tool
|
|
20
|
+
capable of converting this format to an FCL.050 compliant logbook in HTML
|
|
21
|
+
format can be found at <https://hursts.org.uk/efj>.
|
|
22
|
+
|
|
23
|
+
The iCalendar format can be imported into most calendar applications. There is
|
|
24
|
+
an option to include full day events such as days off. This is useful for
|
|
25
|
+
managing and sharing your future roster.
|
|
26
|
+
|
|
27
|
+
The CSV format is for keeping logbook data in a spreadsheet.
|
|
28
|
+
|
|
29
|
+
The package provides entry points for a command line interface, a Tk based
|
|
30
|
+
graphical interface and an AWS lambda function. The AWS function is hooked up
|
|
31
|
+
to a website at <https://hursts.org.uk/aims>, which requires no installation,
|
|
32
|
+
just a browser. The other two interfaces require the installation of a Python
|
|
33
|
+
interpreter.
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from aims.roster import Sector
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sector_id(sector: Sector) -> str:
|
|
6
|
+
return f'{sector.off:%Y%m%dT%H%M}F{sector.name}'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def airframes(sectors: tuple[Sector, ...]) -> dict[str, tuple[str, str]]:
|
|
10
|
+
ids = [sector_id(X) for X in sectors if X.from_]
|
|
11
|
+
try:
|
|
12
|
+
r = requests.post(
|
|
13
|
+
"https://efwj6ola8d.execute-api.eu-west-1.amazonaws.com/"
|
|
14
|
+
"default/reg",
|
|
15
|
+
json={'flights': ids})
|
|
16
|
+
r.raise_for_status()
|
|
17
|
+
regntype_map = r.json()
|
|
18
|
+
except requests.exceptions.RequestException:
|
|
19
|
+
return {} # if anything goes wrong, just return empty dict
|
|
20
|
+
return regntype_map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
import aims.roster as roster
|
|
7
|
+
import aims.output as output
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _args():
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description=(
|
|
13
|
+
'Process an AIMS detailed roster into various useful formats.'))
|
|
14
|
+
parser.add_argument('format',
|
|
15
|
+
choices=['roster', 'efj', 'csv', 'ical'])
|
|
16
|
+
parser.add_argument('--ade', action="store_true")
|
|
17
|
+
return parser.parse_args()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
args = _args()
|
|
22
|
+
lines = roster.lines(sys.stdin.read())
|
|
23
|
+
columns = roster.columns(lines)
|
|
24
|
+
sectors = roster.sectors(columns)
|
|
25
|
+
if args.format == "roster":
|
|
26
|
+
print(output.roster(sectors))
|
|
27
|
+
elif args.format == "efj":
|
|
28
|
+
print(output.efj(
|
|
29
|
+
sectors,
|
|
30
|
+
roster.crew_dict(lines)))
|
|
31
|
+
elif args.format == "csv":
|
|
32
|
+
print(output.csv(
|
|
33
|
+
sectors,
|
|
34
|
+
roster.crew_dict(lines)))
|
|
35
|
+
elif args.format == "ical":
|
|
36
|
+
ade = roster.all_day_events(columns) if args.ade else ()
|
|
37
|
+
print(output.ical(sectors, ade))
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
retval = main()
|
|
43
|
+
sys.exit(retval)
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import json
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from tkinter import ttk
|
|
5
|
+
from tkinter import messagebox
|
|
6
|
+
from tkinter import filedialog
|
|
7
|
+
import ctypes
|
|
8
|
+
|
|
9
|
+
import aims.roster as roster
|
|
10
|
+
|
|
11
|
+
from aims.output import csv, ical, efj
|
|
12
|
+
|
|
13
|
+
VERSION = "1.2"
|
|
14
|
+
|
|
15
|
+
SETTINGS_FILE = os.path.expanduser("~/.aimsgui")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModeSelector(ttk.Frame):
|
|
19
|
+
|
|
20
|
+
def __init__(self, parent, with_ade):
|
|
21
|
+
ttk.Frame.__init__(self, parent)
|
|
22
|
+
self.output_type = tk.StringVar()
|
|
23
|
+
self.output_type.set('efj')
|
|
24
|
+
self.with_ade = tk.BooleanVar()
|
|
25
|
+
self.with_ade.set(with_ade)
|
|
26
|
+
self.__make_widgets()
|
|
27
|
+
|
|
28
|
+
def __make_widgets(self):
|
|
29
|
+
frm_output_type = ttk.LabelFrame(self, text="Output type")
|
|
30
|
+
frm_output_type.pack(fill=tk.X, expand=True, ipadx=5, pady=5)
|
|
31
|
+
efj_output = ttk.Radiobutton(
|
|
32
|
+
frm_output_type, text=" Flight Journal",
|
|
33
|
+
variable=self.output_type, value='efj',
|
|
34
|
+
command=self.output_type_changed)
|
|
35
|
+
efj_output.pack(fill=tk.X)
|
|
36
|
+
csv_output = ttk.Radiobutton(
|
|
37
|
+
frm_output_type, text=" Logbook (.csv)",
|
|
38
|
+
variable=self.output_type, value='csv',
|
|
39
|
+
command=self.output_type_changed)
|
|
40
|
+
csv_output.pack(fill=tk.X)
|
|
41
|
+
ical_output = ttk.Radiobutton(
|
|
42
|
+
frm_output_type, text=" Roster (.ics)",
|
|
43
|
+
variable=self.output_type, value='ical',
|
|
44
|
+
command=self.output_type_changed)
|
|
45
|
+
ical_output.pack(fill=tk.X)
|
|
46
|
+
|
|
47
|
+
self.frm_ical_options = ttk.LabelFrame(self, text="Options")
|
|
48
|
+
with_ade = ttk.Checkbutton(self.frm_ical_options,
|
|
49
|
+
text=" All Day Events",
|
|
50
|
+
variable=self.with_ade,
|
|
51
|
+
command=self.options_changed)
|
|
52
|
+
with_ade.pack(fill=tk.X)
|
|
53
|
+
|
|
54
|
+
def output_type_changed(self):
|
|
55
|
+
assert self.output_type.get() in ('csv', 'ical', 'efj')
|
|
56
|
+
if self.output_type.get() == 'ical':
|
|
57
|
+
self.frm_ical_options.pack(fill=tk.X, expand=True, ipadx=5, pady=5)
|
|
58
|
+
else:
|
|
59
|
+
self.frm_ical_options.pack_forget()
|
|
60
|
+
self.event_generate("<<ModeChange>>", when="tail")
|
|
61
|
+
|
|
62
|
+
def options_changed(self):
|
|
63
|
+
self.event_generate("<<OptionChange>>", when="tail")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Actions(ttk.Frame):
|
|
67
|
+
|
|
68
|
+
def __init__(self, parent):
|
|
69
|
+
ttk.Frame.__init__(self, parent)
|
|
70
|
+
self.__make_widgets()
|
|
71
|
+
|
|
72
|
+
def __make_widgets(self):
|
|
73
|
+
frm_1 = ttk.Frame(self)
|
|
74
|
+
frm_1.pack(fill=tk.X)
|
|
75
|
+
btn_convert = ttk.Button(
|
|
76
|
+
frm_1, text="Load Roster", width=0,
|
|
77
|
+
command=lambda: self.event_generate("<<Action-Import>>"))
|
|
78
|
+
btn_convert.pack(fill=tk.X)
|
|
79
|
+
|
|
80
|
+
frm_2 = ttk.Frame(self)
|
|
81
|
+
frm_2.pack(fill=tk.X, pady=10)
|
|
82
|
+
btn_save = ttk.Button(
|
|
83
|
+
frm_2, text="Save", width=0,
|
|
84
|
+
command=lambda: self.event_generate("<<Action-Save>>"))
|
|
85
|
+
btn_save.pack(fill=tk.X, pady=2)
|
|
86
|
+
self.btn_copy = ttk.Button(
|
|
87
|
+
frm_2, text="Copy All", width=0,
|
|
88
|
+
command=lambda: self.event_generate("<<Action-Copy>>"))
|
|
89
|
+
self.btn_copy.pack(fill=tk.X, pady=2)
|
|
90
|
+
|
|
91
|
+
frm_3 = ttk.Frame(self)
|
|
92
|
+
frm_3.pack(fill=tk.X)
|
|
93
|
+
btn_quit = ttk.Button(
|
|
94
|
+
frm_3, text="Quit", width=0,
|
|
95
|
+
command=lambda: self.event_generate("<<Action-Quit>>"))
|
|
96
|
+
btn_quit.pack(fill=tk.X)
|
|
97
|
+
|
|
98
|
+
def set_copy_selected(self, selected):
|
|
99
|
+
if selected:
|
|
100
|
+
self.btn_copy.config(text="Copy Selected")
|
|
101
|
+
else:
|
|
102
|
+
self.btn_copy.config(text="Copy All")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TextWithSyntaxHighlighting(tk.Text):
|
|
106
|
+
|
|
107
|
+
def __init__(self, parent=None, **kwargs):
|
|
108
|
+
tk.Text.__init__(self, parent, background='white',
|
|
109
|
+
wrap="none", **kwargs)
|
|
110
|
+
self.highlight_mode = None
|
|
111
|
+
self.tag_configure("grayed", foreground="#909090")
|
|
112
|
+
self.tag_configure("keyword", foreground="green")
|
|
113
|
+
self.tag_configure("datetime", foreground="blue")
|
|
114
|
+
self.bind(
|
|
115
|
+
'<KeyRelease>',
|
|
116
|
+
lambda *args: self.edit_modified() and self.highlight_syntax())
|
|
117
|
+
|
|
118
|
+
def insert(self, idx, text, mode=None, *args):
|
|
119
|
+
tk.Text.insert(self, idx, text, *args)
|
|
120
|
+
if mode:
|
|
121
|
+
self.highlight_mode = mode
|
|
122
|
+
self.highlight_syntax()
|
|
123
|
+
|
|
124
|
+
def highlight_syntax(self):
|
|
125
|
+
if not self.highlight_mode:
|
|
126
|
+
return
|
|
127
|
+
for tag in ("keyword", "datetime", "grayed"):
|
|
128
|
+
self.tag_remove(tag, "1.0", "end")
|
|
129
|
+
if self.highlight_mode == 'ical':
|
|
130
|
+
self.highlight_vcalendar()
|
|
131
|
+
elif self.highlight_mode == 'csv':
|
|
132
|
+
self.highlight_csv()
|
|
133
|
+
elif self.highlight_mode == 'efj':
|
|
134
|
+
self.highlight_efj()
|
|
135
|
+
self.edit_modified(False)
|
|
136
|
+
|
|
137
|
+
def highlight_vcalendar(self):
|
|
138
|
+
count = tk.IntVar()
|
|
139
|
+
start_idx = "1.0"
|
|
140
|
+
while True:
|
|
141
|
+
new_idx = self.search(
|
|
142
|
+
"(BEGIN|END):VEVENT",
|
|
143
|
+
start_idx, count=count, regexp=True,
|
|
144
|
+
stopindex="end")
|
|
145
|
+
if not new_idx:
|
|
146
|
+
break
|
|
147
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
148
|
+
self.tag_add("keyword", new_idx, start_idx)
|
|
149
|
+
start_idx = "1.0"
|
|
150
|
+
while True:
|
|
151
|
+
new_idx = self.search(
|
|
152
|
+
r"^[\w-]+:",
|
|
153
|
+
start_idx, count=count, regexp=True,
|
|
154
|
+
stopindex="end")
|
|
155
|
+
if not new_idx:
|
|
156
|
+
break
|
|
157
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
158
|
+
self.tag_add("grayed", new_idx, start_idx)
|
|
159
|
+
start_idx = "1.0"
|
|
160
|
+
while True:
|
|
161
|
+
new_idx = self.search(
|
|
162
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}",
|
|
163
|
+
start_idx, count=count, regexp=True,
|
|
164
|
+
stopindex="end")
|
|
165
|
+
if not new_idx:
|
|
166
|
+
break
|
|
167
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
168
|
+
self.tag_add("datetime", new_idx, start_idx)
|
|
169
|
+
|
|
170
|
+
def highlight_csv(self):
|
|
171
|
+
count = tk.IntVar()
|
|
172
|
+
start_idx = "1.0"
|
|
173
|
+
while True:
|
|
174
|
+
new_idx = self.search(
|
|
175
|
+
r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}",
|
|
176
|
+
start_idx, count=count, regexp=True,
|
|
177
|
+
stopindex="end")
|
|
178
|
+
if not new_idx:
|
|
179
|
+
break
|
|
180
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
181
|
+
self.tag_add("datetime", new_idx, start_idx)
|
|
182
|
+
start_idx = "1.0"
|
|
183
|
+
while True:
|
|
184
|
+
new_idx = self.search(
|
|
185
|
+
r'(:00)?[",]+',
|
|
186
|
+
start_idx, count=count, regexp=True,
|
|
187
|
+
stopindex="end")
|
|
188
|
+
if not new_idx:
|
|
189
|
+
break
|
|
190
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
191
|
+
self.tag_add("grayed", new_idx, start_idx)
|
|
192
|
+
|
|
193
|
+
def highlight_efj(self):
|
|
194
|
+
count = tk.IntVar()
|
|
195
|
+
start_idx = "1.0"
|
|
196
|
+
while True:
|
|
197
|
+
new_idx = self.search(
|
|
198
|
+
r"\d{4}-\d{2}-\d{2}",
|
|
199
|
+
start_idx, count=count, regexp=True,
|
|
200
|
+
stopindex="end")
|
|
201
|
+
if not new_idx:
|
|
202
|
+
break
|
|
203
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
204
|
+
self.tag_add("datetime", new_idx, start_idx)
|
|
205
|
+
start_idx = "1.0"
|
|
206
|
+
while True:
|
|
207
|
+
new_idx = self.search(
|
|
208
|
+
r"\d{4}/\d{4}",
|
|
209
|
+
start_idx, count=count, regexp=True,
|
|
210
|
+
stopindex="end")
|
|
211
|
+
if not new_idx:
|
|
212
|
+
break
|
|
213
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
214
|
+
self.tag_add("datetime", new_idx, start_idx)
|
|
215
|
+
start_idx = "1.0"
|
|
216
|
+
while True:
|
|
217
|
+
new_idx = self.search(
|
|
218
|
+
"CP:|FO:|PU:|FA:",
|
|
219
|
+
start_idx, count=count, regexp=True,
|
|
220
|
+
stopindex="end")
|
|
221
|
+
if not new_idx:
|
|
222
|
+
break
|
|
223
|
+
start_idx = f"{new_idx} + {count.get()} chars"
|
|
224
|
+
self.tag_add("keyword", new_idx, start_idx)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class MainWindow(ttk.Frame):
|
|
228
|
+
|
|
229
|
+
def __init__(self, parent):
|
|
230
|
+
ttk.Frame.__init__(self, parent)
|
|
231
|
+
self.parent = parent
|
|
232
|
+
try:
|
|
233
|
+
with open(SETTINGS_FILE) as f:
|
|
234
|
+
self.settings = json.load(f)
|
|
235
|
+
except Exception:
|
|
236
|
+
self.settings = {}
|
|
237
|
+
self.__make_widgets()
|
|
238
|
+
self.txt.insert(tk.END, f"Version: {VERSION}")
|
|
239
|
+
self.copy_mode = "all"
|
|
240
|
+
|
|
241
|
+
def __make_widgets(self):
|
|
242
|
+
self.columnconfigure(1, weight=1)
|
|
243
|
+
self.rowconfigure(0, weight=1)
|
|
244
|
+
sb = ttk.Scrollbar(self, orient='vertical')
|
|
245
|
+
sbx = ttk.Scrollbar(self, orient='horizontal')
|
|
246
|
+
sb.grid(row=0, column=2, sticky=tk.NS)
|
|
247
|
+
sbx.grid(row=1, column=1, sticky=tk.EW)
|
|
248
|
+
sidebar = ttk.Frame(self, width=0)
|
|
249
|
+
sidebar.grid(row=0, column=0, sticky=tk.NS, padx=5, pady=5)
|
|
250
|
+
self.txt = TextWithSyntaxHighlighting(self)
|
|
251
|
+
self.txt.grid(row=0, column=1, sticky=tk.NSEW)
|
|
252
|
+
sb.config(command=self.txt.yview)
|
|
253
|
+
sbx.config(command=self.txt.xview)
|
|
254
|
+
self.txt.config(yscrollcommand=sb.set)
|
|
255
|
+
self.txt.config(xscrollcommand=sbx.set)
|
|
256
|
+
self.txt.bind("<<Selection>>", self.__on_selection_change)
|
|
257
|
+
|
|
258
|
+
sidebar.rowconfigure(1, weight=1)
|
|
259
|
+
self.ms = ModeSelector(sidebar,
|
|
260
|
+
self.settings.get('ADE', True))
|
|
261
|
+
self.ms.bind("<<ModeChange>>", self.__on_mode_change)
|
|
262
|
+
self.ms.bind("<<OptionChange>>", self.__on_option_change)
|
|
263
|
+
self.ms.grid(row=0, sticky=tk.N)
|
|
264
|
+
self.act = Actions(sidebar)
|
|
265
|
+
self.act.grid(row=1, sticky=(tk.EW + tk.S))
|
|
266
|
+
for event, func in (
|
|
267
|
+
("<<Action-Import>>", self.__import),
|
|
268
|
+
("<<Action-Copy>>", self.__copy),
|
|
269
|
+
("<<Action-Save>>", self.__save),
|
|
270
|
+
("<<Action-Quit>>", lambda _: self.parent.destroy())):
|
|
271
|
+
self.act.bind(event, func)
|
|
272
|
+
self.act.set_copy_selected(False)
|
|
273
|
+
|
|
274
|
+
def __on_mode_change(self, _):
|
|
275
|
+
self.txt.delete('1.0', tk.END)
|
|
276
|
+
|
|
277
|
+
def __on_option_change(self, _):
|
|
278
|
+
self.settings['ADE'] = self.ms.with_ade.get()
|
|
279
|
+
self.txt.delete('1.0', tk.END)
|
|
280
|
+
|
|
281
|
+
def __on_selection_change(self, _):
|
|
282
|
+
if self.txt.tag_ranges("sel"):
|
|
283
|
+
if self.copy_mode == "sel":
|
|
284
|
+
return
|
|
285
|
+
self.copy_mode = "sel"
|
|
286
|
+
self.act.set_copy_selected(True)
|
|
287
|
+
else:
|
|
288
|
+
self.copy_mode = "all"
|
|
289
|
+
self.act.set_copy_selected(False)
|
|
290
|
+
|
|
291
|
+
def __import(self, _):
|
|
292
|
+
assert self.ms.output_type.get() in ('csv', 'ical', 'efj')
|
|
293
|
+
try:
|
|
294
|
+
if self.ms.output_type.get() == 'efj':
|
|
295
|
+
self.__efj()
|
|
296
|
+
elif self.ms.output_type.get() == 'csv':
|
|
297
|
+
self.__csv()
|
|
298
|
+
else:
|
|
299
|
+
self.__ical()
|
|
300
|
+
except roster.RosterException as e:
|
|
301
|
+
self.txt.delete('1.0', tk.END)
|
|
302
|
+
messagebox.showerror("Error", str(e))
|
|
303
|
+
|
|
304
|
+
def __roster_html(self):
|
|
305
|
+
retval = ""
|
|
306
|
+
path = self.settings.get('openPath')
|
|
307
|
+
fn = filedialog.askopenfilename(
|
|
308
|
+
filetypes=(
|
|
309
|
+
("HTML file", "*.htm"),
|
|
310
|
+
("HTML file", "*.html"),
|
|
311
|
+
("All", "*.*")),
|
|
312
|
+
initialdir=path)
|
|
313
|
+
if fn:
|
|
314
|
+
self.settings['openPath'] = os.path.dirname(fn)
|
|
315
|
+
try:
|
|
316
|
+
with open(fn) as f:
|
|
317
|
+
retval = f.read()
|
|
318
|
+
except Exception:
|
|
319
|
+
raise roster.InputFileException
|
|
320
|
+
return retval
|
|
321
|
+
|
|
322
|
+
def __csv(self):
|
|
323
|
+
txt = ""
|
|
324
|
+
html = self.__roster_html()
|
|
325
|
+
if not html:
|
|
326
|
+
return
|
|
327
|
+
self.txt.delete('1.0', tk.END)
|
|
328
|
+
self.txt.insert(tk.END, "Getting registration and type info...")
|
|
329
|
+
self.txt.update()
|
|
330
|
+
|
|
331
|
+
lines = roster.lines(html)
|
|
332
|
+
sectors = tuple(X for X in roster.sectors(roster.columns(lines))
|
|
333
|
+
if X.from_)
|
|
334
|
+
if not sectors:
|
|
335
|
+
messagebox.showinfo('Sectors', 'No relevant sectors found')
|
|
336
|
+
self.txt.delete('1.0', tk.END)
|
|
337
|
+
return
|
|
338
|
+
crewlist_map = roster.crew_dict(lines)
|
|
339
|
+
# note: normalise newlines for Text widget - will restore on output
|
|
340
|
+
txt = csv(sectors, crewlist_map).replace("\r\n", "\n")
|
|
341
|
+
self.txt.delete('1.0', tk.END)
|
|
342
|
+
self.txt.insert(tk.END, txt, 'csv')
|
|
343
|
+
|
|
344
|
+
def __ical(self):
|
|
345
|
+
html = self.__roster_html()
|
|
346
|
+
if not html:
|
|
347
|
+
return
|
|
348
|
+
lines = roster.lines(html)
|
|
349
|
+
columns = roster.columns(lines)
|
|
350
|
+
sectors = roster.sectors(columns)
|
|
351
|
+
ade = roster.all_day_events(columns) if self.ms.with_ade.get() else ()
|
|
352
|
+
if not sectors:
|
|
353
|
+
self.txt.delete('1.0', tk.END)
|
|
354
|
+
messagebox.showinfo('Duties', 'No relevant sectors found')
|
|
355
|
+
return
|
|
356
|
+
# note: normalise newlines for Text widget - will restore on output
|
|
357
|
+
txt = ical(sectors, ade).replace("\r\n", "\n")
|
|
358
|
+
self.txt.delete('1.0', tk.END)
|
|
359
|
+
self.txt.insert(tk.END, txt, 'ical')
|
|
360
|
+
|
|
361
|
+
def __efj(self):
|
|
362
|
+
html = self.__roster_html()
|
|
363
|
+
if not html:
|
|
364
|
+
return
|
|
365
|
+
self.txt.delete('1.0', tk.END)
|
|
366
|
+
self.txt.insert(tk.END, "Working…", 'efj')
|
|
367
|
+
self.txt.update()
|
|
368
|
+
lines = roster.lines(html)
|
|
369
|
+
columns = roster.columns(lines)
|
|
370
|
+
sectors = roster.sectors(columns)
|
|
371
|
+
txt = efj(sectors, roster.crew_dict(lines))
|
|
372
|
+
self.txt.delete('1.0', tk.END)
|
|
373
|
+
self.txt.insert(tk.END, txt, 'efj')
|
|
374
|
+
|
|
375
|
+
def __copy(self, _):
|
|
376
|
+
self.clipboard_clear()
|
|
377
|
+
if self.copy_mode == "all":
|
|
378
|
+
start, end = '1.0', tk.END
|
|
379
|
+
else:
|
|
380
|
+
start, end = self.txt.tag_ranges("sel")
|
|
381
|
+
text = self.txt.get(start, end)
|
|
382
|
+
# ical and excel dialect csv need DOS style line endings
|
|
383
|
+
if self.ms.output_type in ('ical', 'csv'):
|
|
384
|
+
text = text.replace("\n", "\r\n")
|
|
385
|
+
self.clipboard_append(text)
|
|
386
|
+
messagebox.showinfo('Copy', 'Text copied to clipboard.')
|
|
387
|
+
|
|
388
|
+
def __save(self, _):
|
|
389
|
+
output_type = self.ms.output_type.get()
|
|
390
|
+
assert output_type in ('csv', 'ical', 'efj')
|
|
391
|
+
if output_type == 'csv':
|
|
392
|
+
pathtype = 'csvSavePath'
|
|
393
|
+
filetypes = (("CSV file", "*.csv"),
|
|
394
|
+
("All", "*.*"))
|
|
395
|
+
default_ext = '.csv'
|
|
396
|
+
elif output_type == 'efj':
|
|
397
|
+
pathtype = 'efjSavePath'
|
|
398
|
+
filetypes = (("Text file", "*.txt"),
|
|
399
|
+
("All", "*.*"))
|
|
400
|
+
default_ext = '.txt'
|
|
401
|
+
else:
|
|
402
|
+
pathtype = 'icalSavePath'
|
|
403
|
+
filetypes = (("ICAL file", "*.ics"),
|
|
404
|
+
("ICAL file", "*.ical"),
|
|
405
|
+
("All", "*.*"))
|
|
406
|
+
default_ext = '.ics'
|
|
407
|
+
path = self.settings.get(pathtype)
|
|
408
|
+
fn = filedialog.asksaveasfilename(
|
|
409
|
+
initialdir=path,
|
|
410
|
+
filetypes=filetypes,
|
|
411
|
+
defaultextension=default_ext)
|
|
412
|
+
if fn:
|
|
413
|
+
self.settings[pathtype] = os.path.dirname(fn)
|
|
414
|
+
with open(fn, "w", encoding="utf-8", newline='') as f:
|
|
415
|
+
text = self.txt.get('1.0', tk.END)
|
|
416
|
+
# ical and excel dialect csv need DOS style line endings
|
|
417
|
+
if output_type in ('ical', 'csv'):
|
|
418
|
+
text = text.replace("\n", "\r\n")
|
|
419
|
+
f.write(text)
|
|
420
|
+
messagebox.showinfo('Saved', 'Save complete.')
|
|
421
|
+
|
|
422
|
+
def destroy(self):
|
|
423
|
+
with open(SETTINGS_FILE, "w") as f:
|
|
424
|
+
json.dump(self.settings, f, indent=4)
|
|
425
|
+
ttk.Frame.destroy(self)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def main():
|
|
429
|
+
if "windll" in dir(ctypes):
|
|
430
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
|
431
|
+
root = tk.Tk()
|
|
432
|
+
root.title("aimsgui")
|
|
433
|
+
mw = MainWindow(root)
|
|
434
|
+
mw.pack(fill=tk.BOTH, expand=True)
|
|
435
|
+
root.mainloop()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == "__main__":
|
|
439
|
+
main()
|