1#!/usr/bin/env python2
2
3import stat
4import shutil
5from argparse import ArgumentParser
6from datetime import timedelta
7
8from django.conf import settings
9from django.template import Template
10from django.template import Context
11from django.template.base import TemplateSyntaxError
12from django.template.base import TemplateEncodingError
13from pathlib import Path
14
15GENERATE = object()
16
17TMPL_DIR = Path(__file__).parent.joinpath('tmpl')
18
19# files with the following extensions are binary and not passed through Django template expansion.
20BINARY_EXT = ('.png', )
21
22# If any file, after template expansion, contains this string, do not generate it
23DISABLE_CODE = 'TUTORIAL%DISABLE%FILE'
24
25# Inline Django settings
26settings.configure(
27 # LOGGING_CONFIG=None,
28)
29
30out_path = {
31 'name': 'path',
32 'display_name': 'Path',
33 'datatype': Path,
34 'default': None}
35
36proj = {
37 'name': 'CHART tutorial project',
38 'description': 'Set up your project then generate the code.',
39 'datatype': [
40 {'name': 'Generate the project now',
41 'datatype': GENERATE,
42 },
43 {'name': 'name',
44 'display_name': 'Project name',
45 'datatype': str,
46 'description': 'String displayed on the initial web interface',
47 'default': 'myproj'},
48 out_path,
49 {'name': 'description',
50 'display_name': 'Description',
51 'datatype': str,
52 'default': 'An example project'},
53 {'name': 'sid',
54 'display_name': 'SID',
55 'datatype': str,
56 'choices': [
57 {'name': 'chart.sids.sid_eps'},
58 ],
59 'default': 'chart.sids.sid_eps'},
60 {'name': 'raw_table',
61 'display_name': 'Raw table',
62 'description': 'Define a raw table containing a field and an ingester',
63 'datatype': [
64 {'name': 'enabled',
65 'description': 'Create a raw table',
66 'default': True},
67 {'name': 'table_name',
68 'description': 'Name of the table to create',
69 'default': 'CATS'},
70 {'name': 'table_description',
71 'description': 'Description of our table',
72 'default': 'A test table showing cat populations on board satellites'},
73 {'name': 'table_period',
74 'description': 'Nominal frequency of raw table',
75 'default': timedelta(minutes=60)},
76 {'name': 'field_name',
77 'default': 'POPULATION'},
78 {'name': 'field_description',
79 'default': 'Number cats in the satellite'},
80 {'name': 'field_datatype',
81 'default': 'int'},
82 {'name': 'field_length',
83 'default': 32},
84 {'name': 'activity_name',
85 'default': 'CATS_INGESTER'},
86 {'name': 'activity_description',
87 'default': 'Ingest CSV files into CATS table'},
88 {'name': 'algorithm_name',
89 'default': 'cats_ingester.py'},
90 ]},
91
92 # settings related to sample derived table
93 # self.derived_table = {
94 # 'enabled': True,
95 # 'name': 'DELTA_CATS',
96 # 'trigger': 'CATS',
97 # 'activity': 'DELTA_CATS',
98 # 'period': 'PT60M',
99 # 'field_name': 'change',
100 # 'field_description': 'Change in cat population',
101 # 'field_type': 'int',
102 # 'field_length': 32,
103 # 'algorithm': 'double_cats.py',
104 # }
105 # settings related to sample event
106
107 # self.event = {
108 # 'enabled': True,
109 # 'name': 'TEST_ERROR',
110 # 'prop_name': 'prop_a',
111 # 'prop_type': 'int',
112 # 'activity': 'TEST_ERROR_EVENT',
113 # }
114 # settings related to sample schedule files
115
116 # self.schedule = {
117 # 'enabled': True,
118 # dir monitor
119 # timed report
120 # }
121 # settings related to sample report + widget
122
123 # 'name': 'report',
124 # 'description': 'Create a report including a custom widget'
125 # 'enabled': True,
126 # 'widget_module': 'test_widget.py',
127 # 'widget_name': 'TestWidget',
128 # 'report_name': 'test_report.xml',
129 # 'activity': 'TEST_WIDGET.xml',
130 # }
131
132 # settings related to sample crontab
133 # self.crontab = {
134 # 'enabled': True,
135 # }
136 ]
137}
138
139
140def display_value(option, item='value'):
141 value = option.get(item, option.get('default'))
142 if option['datatype'] is str or option['datatype'] is Path:
143 value = '"{value}"'.format(value=value)
144
145 return value
146
147
148def show_page(options, parent):
149 """Show a top level or subpage menu."""
150 name = options.get('display_name', options['name'])
151 print('\n' + name)
152 print('=' * len(name) + '\n')
153 if 'description' in options:
154 print(options['description'] + '\n')
155
156 if parent is None:
157 start = 1
158
159 else:
160 print('1. Return to previous menu')
161 start = 2
162
163 for cc, o in enumerate(options['datatype'], start):
164 if is_submenu(o):
165 # submenu
166 # print('{cc} submenu: {desc}'.format(cc=cc, desc=o.get('description', o['name'])))
167 print('{cc}. {desc}'.format(cc=cc, desc=o.get('description', o['name'])))
168
169 elif o.get('datatype') is GENERATE:
170 print('{cc}. {desc}'.format(cc=cc, desc=o.get('description', o['name'])))
171
172 else:
173 # normal option
174 print('{cc}. {name} = {value}'.format(
175 cc=cc,
176 name=o.get('display_name', o['name']),
177 value=display_value(o)))
178
179 print('\nSelect 1-{cc} to select options or type "?" for help'.format(cc=cc))
180
181
182def show_help():
183 print("""
184Commands:
185Type "<x>" to alter setting x or open submenu x.
186Type "desc <x>" to see all information for option x.
187Type "desc" to see descriptions for all options.
188Type "gen" or select the generate option to create the project.
189""")
190 try:
191 raw_input('Press <enter> to return to menu')
192 except:
193 pass
194
195
196def change_option(option):
197 if 'display_name' in option:
198 name = '{long} ({short})'.format(long=option['display_name'], short=option['name'])
199
200 else:
201 name = option['name']
202
203 print('\nChanging option: {name}'.format(name=name))
204 if 'description' in option:
205 print('Description: {desc}'.format(desc=option['description']))
206
207 print('Current value: {value}'.format(value=display_value(option)))
208 print('Default value: {default}'.format(default=display_value(option, 'default')))
209 while True:
210 try:
211 res = raw_input('Enter new value or press <enter> to leave unchanged: ')
212 except EOFError:
213 # ctrl-d
214 return
215
216 if res == '':
217 # exit without change
218 return
219
220 datatype = option['datatype']
221 if datatype is str or datatype == 'outdir':
222 option['value'] = res
223 return
224
225 elif datatype is int:
226 try:
227 option['value'] = int(res)
228 except ValueError:
229 print('Could not decode integer value')
230 continue
231
232 return
233
234 elif datatype is bool:
235 option['value'] = res[:1].lower() in ('1', 't')
236
237 elif datatype is timedelta:
238 try:
239 float_res = float(res)
240 except ValueError:
241 print('Type number of seconds')
242 continue
243
244 option['value'] = timedelta(seconds=float_res)
245 return
246
247 else:
248 raise ValueError('no handler for datatype ' + str(datatype))
249
250
251def set_datatypes(options):
252 for o in options['datatype']:
253 if is_submenu(o):
254 set_datatypes(o)
255
256 else:
257 if 'datatype' not in o:
258 if 'default' in o:
259 o['datatype'] = type(o['default'])
260
261
262 # print('post set datatypes ' + str(o))
263
264
265def loop(options, parent=None):
266 set_datatypes(options)
267 while True:
268 show_page(options, parent)
269 try:
270 res = raw_input(':')
271 except EOFError:
272 return None
273
274 if res == '?' or res[:1] == 'h':
275 show_help()
276 continue
277
278 elif res == 'gen':
279 return GENERATE
280
281 try:
282 int_res = int(res)
283 except ValueError:
284 int_res = None
285
286 if int_res is not None:
287 # user typed a number
288 if parent is None:
289 # no parent menu so our options start at 1
290 sel = options['datatype'][int_res - 1]
291
292 else:
293 # we have a parent menu ...
294 if int_res == 1:
295 # ... and user selected it
296 return
297
298 else:
299 # otherwise our options begin at 2
300 sel = options['datatype'][int_res - 2]
301
302 if is_submenu(sel):
303 # selected option is a submenu
304 loop(sel, options)
305
306 elif sel['datatype'] is GENERATE:
307 return GENERATE
308
309 else:
310 # selected option is a variable
311 change_option(sel)
312
313
314def show_options(options, indent=''):
315 """Tree view of current options."""
316 for o in options['datatype']:
317 if is_submenu(o):
318 print('{indent}{name}:'.format(indent=indent, name=o['name']))
319 show_options(o, indent + ' ')
320
321 else:
322 print('{indent}{name}: {value}'.format(indent=indent,
323 name=o['name'],
324 value=o.get('value', o.get('default'))))
325
326
327def is_submenu(option):
328 """options are submenus if their 'datatype' member is a list not a datatype."""
329 # print('pre set datatype ' + str(option))
330 return isinstance(option.get('datatype'), list)
331
332
333def make_context(options):
334 res = {}
335 for o in options['datatype']:
336 if is_submenu(o):
337 res[o['name']] = make_context(o)
338
339 else:
340 res[o['name']] = o.get('value', o.get('default'))
341
342 return res
343
344def fix_paths(context, in_base, out_base, rel_path):
345 """Perform Django template expansion on filenames.
346 We allow single { and }.
347
348 Args:
349 `in_base` (Path): the top 'tutorial' directory?
350 `out_base` (Path): the top output directory?
351 `rel_path` (Path?): relative path to the file being expanded
352 """
353 # first we take the project relative path and expand by context
354 # as this is a filename we allow for just { } instead of the normal {{ and }}
355 exp_path = Path(str(rel_path).replace('{', '{{').replace('}', '}}'))
356 exp_path = Path(str(Template(exp_path).render(context)))
357 # print('exp path ' + str(exp_path))
358 in_full = in_base.joinpath(rel_path)
359 out_full = out_base.joinpath(exp_path)
360 # print('join ' + str(out_base) + ' and ' + str(exp_path) + ' to ' + str(out_full))
361 if not out_full.parent.is_dir():
362 out_full.parent.mkdir(parents=True)
363
364 # print('Expanding {rel} from {inp} to {out}'.format(rel=rel_path, inp=in_base, out=out_base))
365 return in_full, out_full
366
367
368def expand_file(context, in_base, out_base, rel_path):
369 # print('exp rel path ' + rel_path)
370 # print('final in {inp} out {out}'.format(inp=in_full, out=out_full))
371 # print('context ' + str(context))
372 # print('out base ' + str(out_base))
373 # print('rel path ' + str(rel_path))
374 in_full, out_full = fix_paths(context, in_base, out_base, rel_path)
375 # print('out full ' + str(out_full))
376 try:
377 content = Template(in_full.open('r').read()).render(context)
378 except (TemplateSyntaxError, TemplateEncodingError) as e:
379 error('Cannot expand {path}'.format(path=rel_path))
380 raise
381
382 if DISABLE_CODE in content:
383 return
384
385 out_full.open('w').write(content)
386
387 if in_full.stat().st_mode & stat.S_IXUSR:
388 mode = in_full.stat().st_mode | 0o111
389 # mode |= stat.S_IXUSR # 0o111
390 # os.fchmod(fd.fileno(), mode & 0o7777)
391 out_full.chmod(mode)
392
393 print('Expanded template to {path}'.format(path=out_full))
394
395
396def copy_file(context, in_base, out_base, rel_path):
397 """Transfer `rel_path` within top dir `in_base` to top dir `out_base`."""
398 in_full, out_full = fix_paths(context, in_base, out_base, rel_path)
399 # in_full.copy(out_full)
400 shutil.copy(str(in_full), str(out_full))
401 print('Duplicated file to {path}'.format(path=out_full))
402
403
404def generate(options, out_path):
405 """Expand all django templates to create sample project in `path`."""
406 print('Generating project in {path}...'.format(path=out_path))
407 context = Context(make_context(options))
408 bin_count = 0
409 exp_count = 0
410 for obj in Path(TMPL_DIR).glob('**/*'):
411 # print('scan ' + str(obj))
412 if obj.is_dir():
413 # print('dir')
414 continue
415
416 if obj.name == Path(__file__).name:
417 print('same')
418 continue
419
420 if obj.suffix in BINARY_EXT:
421 # print('bin')
422 copy_file(context, TMPL_DIR, out_path, obj.relative_to(TMPL_DIR))
423 bin_count += 1
424
425 else:
426 # print('exp')
427 expand_file(context, TMPL_DIR, out_path, obj.relative_to(TMPL_DIR))
428 exp_count += 1
429
430 print('Generation complete expanded {exp} template files copies {bin} binary files'.format(
431 exp=exp_count, bin=bin_count))
432
433
434def main():
435 """Command line entry point."""
436 parser = ArgumentParser()
437 parser.add_argument('--out', '-o',
438 metavar='DIR',
439 default='./myproj',
440 help='Output directory')
441 args = parser.parse_args()
442
443 #absout = Path(os.path.expanduser(os.path.expandvars(args.out))).absolute()
444 out = Path(args.out)
445 if out.is_file():
446 parser.error('{path} exists and is a file'.format(path=out))
447
448 out_path['default'] = out
449 if loop(proj) is GENERATE:
450 generate(proj, out_path.get('value', out_path['default']))
451
452 else:
453 print('\nExiting without writing anything')
454
455if __name__ == '__main__':
456 main()