1#!/usr/bin/env python3
  2
  3"""Command line event and event class viewer, and ingester.
  4
  5Requests that include sid, start or stop switch to event retrieval mode.
  6Otherwise, they should switch to event class display mode.
  7"""
  8
  9import sys
 10import logging
 11from datetime import timedelta
 12
 13import django
 14from lxml.etree import Element
 15
 16from chart.db.connection import db_connect
 17from chart.project import settings
 18from chart.common.args import ArgumentParser
 19from chart.common.prettyprint import Table
 20from chart.events.eventclass import EventClass
 21from chart.events.db import find_events
 22from chart.events.db import find_single_event
 23from chart.common.xml import XMLElement
 24from chart.events.emails import email_event
 25
 26db_conn = db_connect('EVENTS')
 27
 28logger = logging.getLogger()
 29
 30NEWLINE = '\n'
 31
 32
 33def show_all_classes(target=sys.stdout):
 34    """List all defined event classes."""
 35    for e in EventClass.all():
 36        target.write(e.name + NEWLINE)
 37
 38
 39def show_single_event(event_id, target=sys.stdout):
 40    """Display information about a single event."""
 41    # initialise django templaing
 42    django.setup()
 43
 44    e = find_single_event(event_id=event_id)
 45    if e is None:
 46        raise ValueError('Event {id} not found'.format(id=event_id))
 47
 48    t = Table('Common properties')
 49    t.append(('ID', e.event_id))
 50    t.append(('Class', e.event_classname))
 51    t.append(('SID', e.sid.name))
 52    t.append(('Gen_method', e.gen_method))
 53    t.append(('Gen_time', e.gen_time))
 54    t.append(('Start_time', e.start_time))
 55    t.append(('Stop_time', e.stop_time))
 56    t.write(target)
 57
 58    t = Table('Instance properties')
 59    for k, v in e.event_class.instance_properties.items():
 60        desc = [v['type']]
 61        if 'optional' in v:
 62            desc.append('optional')
 63
 64        value = e.instance_properties.get(k)
 65        if isinstance(value, dict):
 66            # quick hack for json property lists - should use instance property type instead
 67            value = str(value)
 68
 69        t.append((k, ','.join(desc), '<not set>' if value is None else value))
 70
 71    t.write(target)
 72
 73    t = Table(title='Class properties')
 74    for k, v in e.event_class.class_properties.items():
 75        t.append((k, v))
 76
 77    t.write(target)
 78
 79    target.write('Description:\n\n')
 80    target.write(e.description() + NEWLINE)
 81
 82
 83def delete_single_event(event_id):
 84    """Delete a single event identified by event id."""
 85    db_conn.query('DELETE FROM events WHERE id=:id', id=event_id)
 86    db_conn.commit()
 87    logger.info('Deleted {id}'.format(id=event_id))
 88
 89
 90def delete_from_stdin():
 91    """Read lines from stdin, each containing an event ID to be
 92    deleted."""
 93    cur = db_conn.prepared_cursor('DELETE FROM events WHERE id=:id')
 94    cc = 0
 95    for event_id in sys.stdin.readlines():
 96        event_id = int(event_id)
 97        logger.info('Deleting {id}'.format(id=event_id))
 98        cur.execute(None, id=event_id)
 99        cc += 1
100
101    db_conn.commit()
102    logger.info('Deleted {cc} events'.format(cc=cc))
103
104
105def daily_events(sid, start_time, stop_time, event_classes):
106    """Read counts of events per day."""
107    classnames = [e.name for e in event_classes]
108    t = Table(title='Daily event counts for {sid}'.format(sid=sid),
109              headings=['Data'] + classnames)
110    acc = start_time
111    DAY = timedelta(days=1)
112    while acc < stop_time:
113        cells = [0] * len(event_classes)
114        for e in find_events(sid=sid,
115                             start_time=acc,
116                             stop_time=acc + DAY,
117                             event_class=None if len(event_classes) == 0 else list(event_classes)):
118            pos = classnames.index(e.event_classname)
119            cells[pos] += 1
120
121        t.append([acc.date()] + cells)
122        acc += DAY
123
124    t.write()
125
126
127def list_events(sid,
128                start_time,
129                stop_time,
130                event_classes,
131                min_duration,
132                max_duration,
133                xml,
134                csv,
135                duplicates,
136                datefmt,
137                description):
138    """Show a table / CSV file / XML file of matching events."""
139    events = list(find_events(sid=sid,
140                              start_time=start_time,
141                              stop_time=stop_time,
142                              event_class=None if len(event_classes) == 0 else list(event_classes),
143                              min_duration=min_duration,
144                              max_duration=max_duration))
145
146    if xml:
147        logger.info('Writing matching events to {path}'.format(path=xml))
148        root = XMLElement('events')
149
150        for e in events:
151            e.to_xml(root.elem)
152
153        root.write(xml)
154        return
155
156    if csv:
157        t = Table(headings=('Id', 'Class', 'Start', 'Stop', 'SID', 'Parameters'))
158
159    else:
160        t = Table(title='Events in range {start} to {stop}'.format(start=start_time,
161                                                                   stop=stop_time),
162                  headings=('Id', 'Class', 'Start', 'Stop', 'SID', 'Parameters'))
163
164    last_e = None
165
166    for e in events:
167        show_id = e.event_id
168        if duplicates and last_e is not None:
169            if (e.start_time == last_e.start_time and
170                e.stop_time == last_e.stop_time and
171                e.event_classname == last_e.event_classname and
172                e.sid == last_e.sid):
173
174                show_id = str(show_id) + ' *'
175
176            last_e = e
177
178        if datefmt is None:
179            start = e.start_time
180
181        else:
182            start = e.start_time.strftime(datefmt)
183
184        if datefmt is None:
185            stop = e.stop_time
186
187        else:
188            stop = e.stop_time.strftime(datefmt)
189
190        t.append((show_id,
191                  e.event_classname,
192                  start,
193                  stop,
194                  e.sid.name,
195                  e.description() if description else
196                  ','.join(k + '=' + str(v) for k, v in e.instance_properties.items())))
197
198        last_e = e
199
200    if csv:
201        t.write_csv()
202
203    else:
204        t.write()
205
206
207def show_class_info(eventclass, target=sys.stdout):
208    """Display information about a class definition."""
209    ec = EventClass(eventclass)
210    target.write('Definition of class ' + ec.name + NEWLINE)
211    target.write('Description:\n')
212    target.write(ec.description + NEWLINE)
213    target.write('Template:\n')
214    target.write(ec.template + NEWLINE)
215    target.write('Instance properties:\n')
216    for k, v in ec.instance_properties.items():
217        target.write('    ' + k + ': ' + str(v) + NEWLINE)
218    target.write('Raised by: ' + ', '.join(a.name for a in ec.raised_by()) + NEWLINE)
219
220
221def write_xml(event, output):
222    logger.info('Writing matching events to {path}'.format(path=output))
223    root = XMLElement('events')
224    event.to_xml(root.elem)
225    root.write(output)
226
227
228def write_html(event, target=sys.stdout):
229    """Write a single event as an HTML table to `target`."""
230    target.write(event.as_table().to_html_str())
231
232
233def main():
234    """Command line entry point."""
235    parser = ArgumentParser(__doc__)
236
237    setup = parser.add_argument_group('Setup')
238    setup.add_argument('--db',
239                        metavar='CONNECTION',
240                        help='Use database connection CONNECTION')
241
242    time_selection = parser.add_argument_group('Time range')
243    time_selection.add_argument('--start',
244                                type=ArgumentParser.start_time,
245                                metavar='TIME',
246                                help='Begin search at TIME.')
247    time_selection.add_argument('--stop',
248                                type=ArgumentParser.stop_time,
249                                metavar='TIME',
250                                help='End search at TIME. Default is current system time')
251
252    filtering = parser.add_argument_group('Filtering')
253    filtering.add_argument('--id', '--show', '-i',
254                           metavar='ID',
255                           type=int,
256                           help='Just display event ID')
257    filtering.add_argument('--class', '--classname', '--cls', '--event',
258                           metavar='CLASS',
259                           dest='event_class',
260                           nargs='*',
261                           help=('Just search for events of specified classes, including Unix '
262                                 'style wildcards'))
263    filtering.add_argument('--sid', '-s',
264                           metavar='SID',
265                           type=ArgumentParser.sid,
266                           help=('Restrict search to events where spacecraft-id is specified and '
267                                 'equal to source'))
268    filtering.add_argument('--min-duration',
269                           type=ArgumentParser.timedelta,
270                           metavar='MIN',
271                           help='Discard any events with no duration or duration less than MIN')
272    filtering.add_argument('--max-duration',
273                           type=ArgumentParser.timedelta,
274                           metavar='MAX',
275                           help='Discard any events with no duration or duration more than MAX')
276
277    display_options = parser.add_argument_group('Display')
278    display_options.add_argument('--desc',
279                                 action='store_true',
280                                 help='Show event description instead of instance parameter list')
281    display_options.add_argument('--duplicates',
282                                 action='store_true',
283                                 help='Highlight duplicate events')
284    display_options.add_argument('--csv',
285                                 action='store_true',
286                                 help='Output in CSV format')
287    display_options.add_argument('--xml',
288                                 metavar='FILE',
289                                 help='Output events in XML format to FILE')
290    display_options.add_argument('--datefmt',
291                                 help='Date format to use')
292    display_options.add_argument('--daily',
293                                 action='store_true',
294                                 help='Show daily counts')
295    display_options.add_argument('--html',
296                                 action='store_true',
297                                 help='Write as HTML')
298    other_actions = parser.add_argument_group('Other actions')
299    other_actions.add_argument('--remove',
300                               action='store_true',
301                               help='Delete event referenced by --id')
302    other_actions.add_argument('--list', '-l',
303                               action='store_true',
304                               help='List all event class names')
305    other_actions.add_argument('--class-info',
306                               help='Show description of an event class')
307    other_actions.add_argument('--email',
308                               metavar='ADDRESS',
309                               help='Send event email to ADDRESS (EUM recepients only)')
310    parser.add_argument_group(other_actions)
311
312    args = parser.parse_args()
313
314    if args.class_info:
315        show_class_info(args.class_info)
316        parser.exit()
317
318    if args.db:
319        settings.set_db_name(args.db)
320
321    if args.list:
322        show_all_classes()
323        parser.exit()
324
325    if args.id:
326        if args.email:
327            email_event(first_name=args.email,
328                        last_name='',
329                        email=args.email,
330                        event=find_single_event(event_id=args.id))
331            parser.exit()
332
333        if args.xml:
334            event = find_single_event(event_id=args.id)
335            write_xml(event, args.xml)
336            parser.exit()
337
338        if args.html:
339            event = find_single_event(event_id=args.id)
340            write_html(event)
341            parser.exit()
342
343        if args.remove:
344            if args.id == '-':
345                delete_from_stdin()
346
347            else:
348                delete_single_event(args.id)
349
350        else:
351            show_single_event(args.id)
352
353        parser.exit()
354
355    elif args.email:
356        parser.error('The --email option only works for sending single events using --id')
357
358    if args.event_class:
359        event_classes = EventClass.expand(args.event_class)
360
361    else:
362        event_classes = set()
363
364    if args.daily:
365        daily_events(sid=args.sid,
366                     start_time=args.start,
367                     stop_time=args.stop,
368                     event_classes=list(event_classes))
369        parser.exit()
370
371    list_events(sid=args.sid,
372                start_time=args.start,
373                stop_time=args.stop,
374                event_classes=event_classes,
375                min_duration=args.min_duration,
376                max_duration=args.max_duration,
377                xml=args.xml,
378                csv=args.csv,
379                duplicates=args.duplicates,
380                datefmt=args.datefmt,
381                description=args.desc)
382
383
384if __name__ == '__main__':
385    main()