-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpigor.py
424 lines (340 loc) · 14.9 KB
/
pigor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#!/usr/bin/env python3
import re
import os
import json
import numpy as np
import multiprocessing as mp
import matplotlib
#matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import glob
import measurement
import datetime
import webbrowser
from pathlib import Path
from markdown import markdown
# name of the program
PROGRAM_NAME = "PIGOR v1.2"
# functions that the user can utilize
USER_FUNCTIONS = dict()
# global config dict
configuration = dict()
# global vars for init() default
configuration_fallback = {
'PIGOR_ROOT' : Path(os.path.dirname(os.path.abspath(__file__))),
'PIGOR_ROOT_RECURSIVE' : True,
'FILE_EXTENTION' : '.dat',
'IMAGE_FORMAT' : '.png',
'CREATE_HTML' : True,
'CREATE_MD' : True,
'EXPORT_THEME' : 'github',
'EXPORT_FIT_FUNCTIONS' : True
}
# decorator for registering functions
def show_user(func):
"""Register a function to be displayed to the user as an option"""
global USER_FUNCTIONS
try:
key = re.search(r'\[(.+?)\]', func.__doc__).group(1)
except AttributeError as e:
key = func.__name__
print(e)
USER_FUNCTIONS[key] = func
return func
def bool2yn(b):
"""Converts a boolean to yes or no with the mapping: y = True, n = False."""
return 'y' if b else 'n'
def yn2bool(s):
"""Converts yes and no to True and False."""
return True if s.casefold().startswith('y') else False
def is_valid_theme(theme):
"""Checks if this theme exists."""
themes = sorted(Path('./markdown_themes').glob('*.css'))
themes = [t.stem for t in themes]
if theme in themes:
return True
else:
return False
def list_themes():
"""Returns a list of all themes available."""
themes = sorted(Path('./markdown_themes').glob('*.css'))
return [t.stem for t in themes]
@show_user
def init(create_new_config_file=True):
"""This function will read the config file and initialize some variables
accordingly. This function can be used by the command [i].
Available options:
- root directory where PIGOR will start to look for measurement files
- Should PIGOR look for files to analyse recursively?
- Which file extention do the measurement files posess?
- What plot output format should PIGOR use?
- Should PIGOR automatically create an html file?
- Should PIGOR automatically create a md file?
- Should PIGOR create a txt file containing all used fit functions for the use in Mathematica?
.. note:: If no config file can be found, it will create one.
"""
# make configuration editable
global configuration
# try to read the configuration
try:
with open('pigor-config.json', 'r', encoding='utf-8') as f:
c = json.load(f)
# make it a path object
c['PIGOR_ROOT'] = Path(c['PIGOR_ROOT']).resolve()
# check if new item has been added to configuration
if len(c) != len(configuration_fallback):
create_new_config_file = True
for k, v in configuration_fallback:
if k not in c:
c[k] = v
except Exception:
print('Could not read configuration file. Creating a new one now:\n')
c = configuration_fallback
create_new_config_file = True
# questions to aks the user
questions = {
'PIGOR_ROOT' : (f'Where should {PROGRAM_NAME} start looking for measurement files? [{c["PIGOR_ROOT"]}]', 'path'),
'PIGOR_ROOT_RECURSIVE' : (f'Should {PROGRAM_NAME} look for files recursively? (y/n) [{bool2yn(c["PIGOR_ROOT_RECURSIVE"])}]', 'bool'),
'FILE_EXTENTION' : (f'Which file extention should {PROGRAM_NAME} look for? (string) [{c["FILE_EXTENTION"]}]', 'file-extention-data'),
'IMAGE_FORMAT' : (f'What image format should the plots have? (.png,.svg,.eps,.pdf) [{c["IMAGE_FORMAT"]}]', 'file-extention-image'),
'CREATE_HTML' : (f'Should {PROGRAM_NAME} create an HTML file automatically after analysis? (y/n) [{bool2yn(c["CREATE_HTML"])}]', 'bool'),
'CREATE_MD' : (f'Should {PROGRAM_NAME} create a Markdown file automatically after analysis? (y/n) [{bool2yn(c["CREATE_MD"])}]', 'bool'),
'EXPORT_THEME' : (f'Please choose a theme for the exported html files: ({list_themes()}) [{c["EXPORT_THEME"]}]', is_valid_theme),
'EXPORT_FIT_FUNCTIONS' : (f'Should {PROGRAM_NAME} create a txt file containing all used fit functions for later use in Mathematica? (y/n) [{bool2yn(c["EXPORT_FIT_FUNCTIONS"])}]', 'bool')
}
for k, q in questions.items():
if not k in c.keys() or create_new_config_file:
# ask user a question
print(q[0])
user_input = input()
# if a Path object must be read
if q[1] == 'path':
while True:
try:
if user_input == '':
value = None
break
value = Path(user_input)
if value.exists():
break
else:
print('The path you entered does not exist. Please try again.')
user_input = input()
except Exception:
user_input = input('Could not find the path, please input another one:\n')
# if treating booleans
elif q[1] == 'bool':
if not user_input == '':
value = yn2bool(user_input)
else:
value = None
# if answer is a file extention
elif not callable(q[1]) and q[1].startswith('file-extention'):
if user_input == '' and q[1] == 'file-extention-image':
value = None
elif user_input == '' and q[1] == 'file-extention-data':
value = None
else:
# adding . to file extention if not given by the user
value = user_input.casefold() if user_input.startswith('.') else '.' + user_input.casefold()
elif callable(q[1]):
while True:
if user_input == '':
value = None
break
if q[1](user_input) == True:
value = user_input
break
else:
user_input = input('Sorry, PIGOR does not know this input. Try again.')
else:
value = user_input
# renew old configuration value
if value != None:
c[k] = value
# write config to global var
configuration = c.copy()
# write configuration into file
with open('pigor-config.json', 'w') as f:
if isinstance(c['PIGOR_ROOT'], Path):
c['PIGOR_ROOT'] = str(c['PIGOR_ROOT'].resolve())
json.dump(c, f, ensure_ascii=False, indent=4)
def print_header(text):
"""This function prints a beautiful header followed by one empty line.
:param text: text to be displayed as header
"""
print("\n" + "=" * len(text))
print(text)
print("=" * len(text) + "\n")
@show_user
def print_help(display="all"):
"""Prints a help menu on the screen for the user. This function can be used by the command [h].
:param display: specify the lenght of the help menu, options are 'all' or 'quick' (Default value = "all")
"""
# print a list of all available commands
if display == 'all':
for k,v in USER_FUNCTIONS.items():
print(f'{k} ... {v.__name__}')
print(f'q ... quit {PROGRAM_NAME}\n')
# show only the function that should be displayed, but this time with a docstring
else:
try:
f = USER_FUNCTIONS[display[0]]
print(f'{display[0]}: \n\n {f.__doc__}\n\n')
except Exception:
print(f'Sorry, but there is not help page for {display[0]} available.\n')
def find_all_files():
"""Finds all dat files recursively in all subdirectories ignoring hidden directories
and Python specific ones.
Returns a list of filepaths.
"""
if configuration['PIGOR_ROOT_RECURSIVE']:
return sorted(configuration['PIGOR_ROOT'].rglob('*' + configuration['FILE_EXTENTION']))
else:
return sorted(configuration['PIGOR_ROOT'].glob('*' + configuration['FILE_EXTENTION']))
@show_user
def analyse_files(filepaths='all'):
"""Analyses all given files in list. This function can be used by the command [a].
:param filepaths: list of files to analyse with
their relative dir path added
.. todo:: Change to no override mode. measurement.Measurement.plot(override=False)
.. todo:: a + today => only analyse files for today
.. todo:: a + override => override=True
"""
# get the theme from config
theme = configuration['EXPORT_THEME']
if filepaths == 'all':
filepaths = find_all_files()
elif filepaths == 'today':
pass # TODO: only analyse the files of today
# analyse all files
for f in filepaths:
try:
m = measurement.Measurement(f)
export_fit = 'Mathematica' if configuration['EXPORT_FIT_FUNCTIONS'] == True else False
m.fit(fit_function_export=export_fit)
m.plot(file_extention=configuration['IMAGE_FORMAT'])
m.export_meta(make_html=True, theme=theme)
except Exception as e:
print(f'The following exception occured during runtime:\n\n{e}\n\nContinuing operation.')
@show_user
def create_index():
"""Creates an index.html listing all directories and subdirectories and their HTML and Markdown files. If
a default browser is found, it will automatically open index.html. This function can be used by the
command [j]."""
# theme for the index.html
theme = configuration['EXPORT_THEME']
# find all measurements
files = sorted(configuration['PIGOR_ROOT'].rglob('*' + configuration['FILE_EXTENTION']))
files = [f.relative_to(configuration['PIGOR_ROOT'].parent) for f in files]
files.sort(key=lambda x: x.name, reverse=True)
# list all measurement files, if they have a corresponding html or md file
l = []
for f in files:
buffer = f'- {f.parent}: [{f.stem}]({f.resolve().as_uri()}): '
m = f.with_suffix('.md').exists()
h = f.with_suffix('.html').exists()
p = f.with_suffix(configuration['IMAGE_FORMAT']).exists()
if m:
buffer += f'[MD]({f.with_suffix(".md").resolve().as_uri()})'
if h:
buffer += f' | [HTML]({f.with_suffix(".html").resolve().as_uri()})'
if p:
buffer += f' | [PLOT]({f.with_suffix(configuration["IMAGE_FORMAT"]).resolve().as_uri()})'
if m or h:
l.append(buffer)
if l:
# write header of index.html
t = [
f'# {PROGRAM_NAME} Index File',
'',
'Here is a handy list of all the files that have been analysed so far:',
''
]
t.extend(l)
# write html template
h1 = [
'<!DOCTYPE html>',
'<html>',
'<head>',
'<meta charset="UTF-8">',
f'<title>{PROGRAM_NAME} Index File</title>',
'<style>'
]
h2 = [
'</style>',
'</head>',
'<body>',
markdown('\n'.join(t)),
'</body>',
'</html>'
]
index_file_path = configuration['PIGOR_ROOT'].joinpath('index_pigor.html')
with open(index_file_path, 'w') as htmlfile:
# writing html head
for line in h1:
htmlfile.write(f'{line}\n')
# copying the css file
css_path = Path('./markdown_themes/{}.css'.format(theme))
with open(css_path, 'r') as cssfile:
for line in cssfile:
htmlfile.write(line)
# write actual content of html file and end
for line in h2:
htmlfile.write(f'{line}\n')
print(f'Wrote index file at {index_file_path}')
webbrowser.open(index_file_path.as_uri(), new=2, autoraise=True)
@show_user
def remove_generated_files(files='all'):
"""Removes the generated png, html and md files. This function can be used by the command [r].
:param files: list of Path objects to files that should be removed; if set to 'all' it will delete all generated files (Default value = 'all')
.. todo:: Cover the case when files are not a list of path, e.g. wrong input given.
"""
if files == 'all':
files = find_all_files()
# TODO: case when files is not a list of Paths
try:
for f in files:
f.with_suffix(configuration['IMAGE_FORMAT']).unlink()
f.with_suffix('.md').unlink()
f.with_suffix('.html').unlink()
f.with_name(f.stem + '_fit_functions.txt').unlink()
except Exception as e:
print(e)
#raise NotImplementedError
@show_user
def print_root():
"""Prints the root for PIGOR, e.g. where it will look for files to analyse. This function
can be used by the command [x].
"""
print(f"{PROGRAM_NAME} will look for measurement files in {configuration['PIGOR_ROOT'].resolve()}.\n")
def main():
"""Main Loop"""
# perform initialization
init(create_new_config_file=False)
# starting main loop
print_header("Welcome to {}.".format(PROGRAM_NAME))
# print help menu
print_help(display="all")
# print where PIGOR will look for files in
print_root()
print('If you need more information about a command, just type h + [command] + <ENTER> to get more help. For example: h + a + <ENTER>.\n')
while True:
print("Please type a command you want to perform and press <ENTER>.")
# get the user's input
user_input = input()
# split it for additional but optional arguments
cmd, *args = user_input.split(' ')
if cmd == "q":
break
elif cmd in USER_FUNCTIONS.keys():
f = USER_FUNCTIONS[cmd]
if args:
f(args)
else:
f()
else:
print('The command you typed does not exist. Press h + <ENTER> for help.')
if __name__ == '__main__':
mp.freeze_support()
main()