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()