1#!/usr/bin/env python3
2
3"""CHART-MSG Source IDentifier."""
4
5
6
7from enum import Enum
8from fnmatch import fnmatch
9from collections import defaultdict
10
11from chart.common.xml import SubElement
12from chart.common.xml import load_xml
13from chart.common.xml import parsechildbool
14from chart.common.xml import parsechildstr
15from chart.common.xml import parsechilddatetime
16from chart.db.func import ANY
17from chart.plots.sampling import Sampling
18from chart.sids.exceptions import BadSID
19
20
21ELEM_SCID = 'scid'
22ELEM_NAME = 'name'
23ELEM_SATELLITE = 'satellite'
24ELEM_OPERATIONAL = 'operational'
25ELEM_VISIBLE = 'visible'
26ELEM_LAUNCH_DATE = 'launch-date'
27ELEM_CFID = 'cfid'
28ELEM_OGSID = 'ogsid'
29
30CFID = Enum('CFID', 'OPER VALI')
31OGSID = Enum('OGSID', 'PRIM BACK')
32
33# class CFID(Enum):
34# OPER = 'oper'
35# VALI = 'vali'
36
37# CFID.OPER.description = 'Operational'
38# CFID.OPER.db = 'O'
39# CFID.VALI.description = 'Validation'
40# CFID.VALI.db = 'V'
41
42# class OGSID(Enum):
43# PRIM = 'prim'
44# BACK = 'back'
45
46# OGSID.PRIM.description = 'Primary'
47# OGSID.PRIM.db = 'P'
48# OGSID.BACK.description = 'Backup'
49# OGSID.BACK.db = 'B'
50
51class Satellite:
52 """Information about a satellite."""
53
54 _cache = None
55
56 def __init__(self, elem):
57 self.scid = parsechildstr(elem, ELEM_SCID)
58 self.name = parsechildstr(elem, ELEM_NAME)
59 self.operational = parsechildbool(elem, ELEM_OPERATIONAL)
60 self.visible = parsechildbool(elem, ELEM_VISIBLE)
61 self.launch_date = parsechilddatetime(elem, ELEM_LAUNCH_DATE, None)
62 self.orbit_duration = None
63 self.acronym = None
64
65 @staticmethod
66 def cache():
67 """Populate _cache with information read from satellites.xml."""
68 from chart.project import settings
69 if Satellite._cache is None:
70 Satellite._cache = {}
71 for sat_elem in load_xml(settings.SATELLITES).findall(ELEM_SATELLITE):
72 sat = Satellite(sat_elem)
73 Satellite._cache[sat.scid] = sat
74
75 return Satellite._cache
76
77
78class SID_MSG:
79 """MSG Source ID object.
80 Data is identified using SCID, either CFID or OGSID, and sensing_time.
81 """
82
83 # based on this we include the correct xxx_selector.html file from sids/templates
84 selector = 'msg'
85 # to set when plot page in browser is first brought up
86 default_sid = {'scid': 'MSG3', 'cfid': 'PRIM'}
87 shifts = {}
88
89 plot_aux_events_label = None
90
91 # sampling options used by this SID class
92 sampling_options = [Sampling.AUTO,
93 Sampling.ALL_POINTS,
94 Sampling.FIT,
95 # Sampling.DAILY,
96 Sampling.HOURLY,
97 # Sampling.HALF_HOURLY,
98 ]
99
100 # subsampling options to be considered when auto-selecting best stats subsampling
101 # should be sorted from longest to shortest
102 def auto_sampling_options(self):
103 """Return the list of stats subsampling options available for this sid.
104 """
105 return [Sampling.HOURLY]
106
107
108 #
109 ##
110 ### Construction and testing
111 ##
112 #
113
114 # def __init__(self, scid=None, cfid, ogsid, wildcard=False):
115 def __init__(self,
116 scid=None,
117 cfid=None,
118 ogsid=None,
119 wildcard=False): # (unused arg) pylint:disable=W0613
120 # If `wildcard` is set we are creating a matching SID.
121 # Set cfid and ogsid to None to match anything
122 # in the db we don't allow NULL SCID, OGSID or CFID all must be specified.
123 # In system tables we might just autoset ...
124
125 # ok = False
126
127 # if scid is not None:
128 # for s in all_satellites():
129 # if s.scid.lower() == scid.lower():
130 # ok = True
131
132 if wildcard:
133 self.satellite = None
134 self.scid = scid
135
136 elif scid is None or scid == SID_MSG.SIDLESS.scid:
137 self.satellite = None
138 self.scid = None
139
140 elif scid != None:
141 self.scid = None
142 for k, v in Satellite.cache().items():
143 if k.upper() == scid.upper():
144 self.scid = k
145 self.satellite = v
146
147 if self.scid is None:
148 raise BadSID(scid)
149
150 self.scid = None if scid is None else scid.upper()
151
152 if cfid is None:
153 self.cfid = None
154
155 elif cfid in list(CFID.__members__.values()):
156 self.cfid = cfid
157
158 else:
159 self.cfid = CFID[cfid.upper()]
160
161 if ogsid is None:
162 self.ogsid = None
163
164 elif ogsid in list(OGSID.__members__.values()):
165 self.ogsid = ogsid
166
167 else:
168 self.ogsid = OGSID[ogsid.upper()]
169
170 if self.cfid is None and self.ogsid is None:
171 self.ogsid = OGSID.PRIM
172
173 def __repr__(self):
174 things = ['scid={scid}'.format(scid=self.scid)]
175
176 if self.cfid is not None:
177 # things.append('cfid={cfid}'.format(cfid=self.cfid))
178 things.append('cfid={cfid}'.format(cfid=self.cfid.name))
179
180 if self.ogsid is not None:
181 # things.append('ogsid={ogsid}'.format(ogsid=self.ogsid))
182 things.append('ogsid={ogsid}'.format(ogsid=self.ogsid.name))
183
184 return 'SID_MSG({things})'.format(things=', '.join(things))
185
186 def __str__(self):
187 if self.scid is None:
188 return 'Non satellite specific'
189
190 res = self.scid
191
192 if self.cfid is not None:
193 res += ', {cfid}'.format(cfid=self.cfid.name)
194
195 if self.ogsid is not None and self.ogsid is not OGSID.PRIM:
196 res += ', {ogsid}'.format(ogsid=self.ogsid.name)
197
198 return res
199
200
201 def __eq__(self, other):
202 if other is None:
203 # needed for 'if sid in sids...'
204 return False
205
206 else:
207 return self.scid == other.scid and self.cfid == other.cfid and self.ogsid == other.ogsid
208
209 def __ne__(self, other):
210 # this is required otherwise the SID non-uniqueness check in eventsfile.py
211 # gives a false failure
212 return self.scid != other.scid or self.cfid != other.cfid or self.ogsid != other.ogsid
213
214
215 def __hash__(self):
216 things = ['scid={scid}'.format(scid=self.scid)]
217
218 if self.cfid is not None:
219 # things.append('cfid={cfid}'.format(cfid=self.cfid))
220 things.append('cfid={cfid}'.format(cfid=self.cfid.name))
221
222 if self.ogsid is not None:
223 # things.append('ogsid={ogsid}'.format(ogsid=self.ogsid))
224 things.append('ogsid={ogsid}'.format(ogsid=self.ogsid.name))
225
226 return hash('SID_MSG({things})'.format(things=', '.join(things)))
227
228
229 @property
230 def orbit(self):
231 """No orbital information for MSG."""
232 return None
233
234 def replace(self, scid=ANY, cfid=ANY, ogsid=ANY):
235 """Return a new SID with any of our fields replaced."""
236 return SID_MSG(scid=self.scid if scid is ANY else scid,
237 cfid=self.cfid if cfid is ANY else cfid,
238 ogsid=self.ogsid if ogsid is ANY else ogsid)
239
240 #
241 ##
242 ### Web site support
243 ##
244 #
245
246 @staticmethod
247 def from_django_request(get):
248 """Extract a SID from `query` (URL parameter string).
249 Allowed inputs:
250
251 scid:MSG1
252 scid:MSG1 cfid:PRIM # shouldn't happen
253 scid:MSG1 cfid:OPER
254 scid:MSG1 cfid:VALI.
255 """
256 sid = {}
257
258 if 'scid' in get:
259 sid['scid'] = get['scid']
260
261 if 'cfid' in get:
262 sid['cfid'] = get['cfid']
263
264 if len(sid) == 0:
265 return None
266
267 # currently the gs parameter is always called CFID in the URL even if its an OGSID
268
269 elif len(sid) == 1:
270 # scid only
271 return SID_MSG(scid=sid['scid'], ogsid=OGSID.PRIM)
272
273 elif len(sid) == 2:
274 if sid['cfid'] == OGSID.PRIM.name:
275 return SID_MSG(scid=sid['scid'], ogsid=OGSID.PRIM)
276
277 elif sid['cfid'] == CFID.OPER.name:
278 return SID_MSG(scid=sid['scid'], cfid=CFID.OPER.name)
279
280 elif sid['cfid'] == CFID.VALI.name:
281 return SID_MSG(scid=sid['scid'], cfid=CFID.VALI)
282
283 else:
284 raise BadSID('Invalid source ID')
285
286 def as_url_query(self, base=None):
287 """Return a dictionary representation of ourselves suitable for passing into
288 the urllib.urlencode() function to create/extend a URL query fragment."""
289 # we should insert either ogsid or cfid depending on which was explicitly selected on
290 # construction
291 if base is None:
292 base = {'scid': self.scid}
293
294 else:
295 base['scid'] = self.scid
296
297 if self.cfid is not None and self.cfid is not CFID.OPER:
298 base['cfid'] = self.cfid.name
299
300 return base
301
302 #
303 ##
304 ### DDL creation support
305 ##
306 #
307
308 # SID fields implicitly added to the start of all TS tables
309 # @memoized
310 @staticmethod
311 def ddl_id_fields():
312 """Standard ID fields to be prepended to every TS table definition."""
313 from chart.db.model.table import FieldInfo
314 return [
315 FieldInfo(name='SCID',
316 description='Spacecraft ID',
317 datatype=str,
318 length=4),
319 FieldInfo(name='CFID',
320 description='Core Facility Identifier (physical GS)',
321 allow_null=True, # in stats table must be optional. for ap shouldn't be
322 datatype=str,
323 # choices=('OPER', 'VALI'),
324 length=4),
325 FieldInfo(name='OGSID',
326 description='Operational Ground Segment Identifier (virtual GS)',
327 allow_null=True, # in stats table must be optional. for ap shouldn't be
328 # choices=('PRIM', 'BACK'),
329 datatype=str,
330 length=4),
331 ]
332
333 # Indexes for AP tables (first is primary key)
334 @staticmethod
335 def ddl_ap_indexes():
336 """Standard indexes for TS AP tables."""
337 return [{'suffix': '_O', 'fields': ('SCID', 'OGSID', 'SENSING_TIME'), 'compress': 2},
338 # {'suffix': '_C', 'fields': ('SCID', 'CFID', 'SENSING_TIME'), 'compress': 2},
339 ]
340
341
342 # Indexes for stats tables
343 @staticmethod
344 def ddl_stats_indexes():
345 """DB indexes for stats tables."""
346 return [{'suffix': '_P',
347 'fields': ('SCID', 'OGSID', 'REGION', 'SENSING_TIME'),
348 'compress': 3}]
349
350 @staticmethod
351 def ddl_sys_fields(table_name=None): # (unused arg) pylint:disable=W0613
352 """Events, jobs, reports, subscriptions tables using the {{sid}} expansion."""
353 # don't import at module level because it probably imports back to us
354 from chart.db.model.table import FieldInfo
355 if table_name != 'PRODUCTS':
356 return [
357 FieldInfo(name='SCID',
358 datatype=str,
359 length=4,
360 allow_null=True,
361 description='Spacecraft ID'),
362 FieldInfo(name='CFID',
363 datatype=str,
364 length=4,
365 allow_null=True,
366 description='Core (physical) Facility ID'),
367 FieldInfo(name='OGSID',
368 datatype=str,
369 length=4,
370 allow_null=True,
371 description='Operational Ground Segment ID'),
372 ]
373
374 else:
375 return [
376 FieldInfo(name='SCID',
377 datatype=str,
378 length=4,
379 allow_null=True,
380 description='Spacecraft ID'),
381 FieldInfo(name='CFID',
382 datatype=str,
383 length=4,
384 allow_null=True,
385 description='Core (physical) Facility ID'),
386 ]
387
388 @staticmethod
389 def ddl_sys_indexes(table_name=None):
390 """DB indexes for sys tables."""
391 pass
392
393 #
394 ##
395 ### Generic binary ingester support
396 ##
397 #
398
399 # Construct an SQL where clause which will filter for `sid`.
400 # Used by ingestion code to test/delete duplicates (this means we have CFID but not OGSID)
401 sql_where_bind = "SCID=:scid AND OGSID IN ('PRIM', 'BACK') AND CFID=:cfid"
402
403 def sql_where(self):
404 """Construct an SQL WHERE clause (without bind variables) that will search for
405 us in TS tables. Note the MSG TS tables have an index on OGSID but not CFID.
406 Not used for JOBS, EVENTS or REPORTS tables.
407 """
408 return "SCID='{scid}' AND {ogsid}{cfid}".format(
409 scid=self.scid,
410 #ogsid="ogsid='{ogsid}'".format(
411 # ogsid=self.ogsid.name) if self.ogsid is not None else "OGSID IN ('PRIM') ")
412 ogsid="ogsid='{ogsid}'".format(
413 ogsid=self.ogsid.name) if self.ogsid is not None else "OGSID IN ('PRIM', 'BACK') ",
414 cfid=" AND cfid='{cfid}'".format(cfid=self.cfid.name) if self.cfid is not None else '')
415 # ogsid="OGSID IN ('PRIM','BACK')" if self.ogsid is None else "OGSID='{ogsid}'".format(
416 # ogsid=self.ogsid.name),
417 # cfid="" if self.cfid is None else " AND CFID='{cfid}'".format(cfid=self.cfid.name))
418
419 def sql_where(self, table_name=None, match_on_none=False, prefix=None):
420 """Return an SQL clause that will test for ts rows containing us.
421
422 If `table_name` is set it will be the name (not Tableinfo) of the table to be written
423 to, which may be a system table.
424
425 If `match_on_none` is set then we will match table rows contains null values in the SID
426 column.
427
428 `prefix` can be used to prefix column names for queries that join tables.
429
430 Avoids bind variables."""
431 if table_name == 'EVENT_SUBSCRIPTIONS':
432 # In event subscriptions table we just store the SCID and don't allow subscribing
433 # to non-primary events
434 if match_on_none:
435 return "(SCID is null or SCID='{scid}')".format(scid=self.scid)
436
437 else:
438 return "SCID='{scid}'".format(scid=self.scid)
439
440 else:
441 return "SCID='{scid}' AND {ogsid}{cfid}".format(
442 scid=self.scid,
443 cfid=" AND cfid='{cfid}'".format(cfid=self.cfid.name) if self.cfid is not None else '',
444 ogsid="ogsid='{ogsid}'".format(
445 ogsid=self.ogsid.name) if self.ogsid is not None else "OGSID IN ('PRIM', 'BACK') ")
446 # ogsid="OGSID IN ('PRIM','BACK')" if self.ogsid is None else "OGSID='{ogsid}'".format(
447 # ogsid=self.ogsid.name),
448 # cfid="" if self.cfid is None else " AND CFID='{cfid}'".format(cfid=self.cfid.name))
449
450 @staticmethod
451 def rdr_kwargs(row):
452 """Extract selection args from a row as passed to the rdr.handle_x() functions."""
453 return {'scid': row[0], 'cfid': row[1]}
454
455 # When creating an insert cursor, prepend it with these fields
456 insert_fields = ['SCID', 'CFID', 'OGSID']
457
458 def bind_insert(self):
459 """When executing an insert cursor with bind variables as an array,
460 pass these fields first."""
461 return [self.scid, self.cfid.name, self.ogsid.name]
462
463 # values to extract from each SID in rdr.py handler functions
464 rdr_insert = 'sid.scid, sid.cfid.name, sid.ogsid.name'
465
466 # before inserting we may delete existing data. This means we need a way to
467 # delete all data from a physical ground segment regardless of whether it is primary
468 # def remove_ogsid(self):
469 # return SID(scid=self.scid, cfid=self.cfid)
470
471 # Only really used by sf00_ingester. Be careful if you make a SID where
472 # the first element is not a string.
473 # This setting is probably not needed at all.
474 # ??? must be used for ts ddl ???
475 SCID_MAX_CHARS = 4
476
477 @staticmethod
478 def from_sf(sfreader):
479 """Extract a SID from an SFReader or SFInMemory object."""
480 if sfreader.sfid == 0:
481 cfid = 'OPER'
482
483 elif sfreader.sfid == 1:
484 cfid = 'VALI'
485
486 else:
487 raise ValueError('Bad CFID {cfid}'.format(cfid=sfreader.sfid))
488
489 return SID_MSG(scid=sfreader.scid, cfid=cfid, ogsid=None)
490
491 @staticmethod
492 def ts_indexes():
493 """Return list of TS AP table field names which should be indexed."""
494
495 return ['SCID', 'OGSID', ]
496
497 #
498 ##
499 ### sys table support
500 ##
501 #
502
503 @staticmethod
504 def sql_sys_where(table_name, sid):
505 """Return an SQL clause which will test for us in the JOBS or REPORTS table."""
506 if table_name == 'REPORTS' or table_name == 'EVENTS' or table_name == 'JOBS':
507 if sid is None:
508 return '1=1'
509
510 elif sid.scid is None:
511 return 'SCID IS NULL'
512
513 else:
514 return "SCID='{scid}'".format(scid=sid.scid)
515
516 else:
517 return '{scid}{cfid}{ogsid}'.format(
518 scid='' if sid.scid is None else "SCID='{scid}'".format(scid=sid.scid),
519 cfid='' if sid.cfid is None else " AND CFID='{cfid}'".format(cfid=sid.cfid.name),
520 ogsid='' if sid.ogsid is None else " AND OGSID='{ogsid}'".format(
521 ogsid=sid.ogsid.name))
522
523 @staticmethod
524 def sql_sys_where_bind(table_name): # (unused arg) pylint:disable=W0613
525 """Where clause using bind variables."""
526 if table_name != 'PRODUCTS':
527 return 'SCID=:scid AND CFID=:cfid AND OGSID=:ogsid'
528
529 else:
530 return 'SCID=:scid AND CFID=:cfid'
531
532
533 def bind_sys_where(self, table_name):
534 """Bind variable values fo us using SQL from sql_sys_where_bind."""
535 if table_name != 'PRODUCTS':
536 return {'scid': self.scid,
537 'cfid': self.cfid.name,
538 'ogsid': self.ogsid.name}
539
540 else:
541 return {'scid': self.scid,
542 'cfid': self.cfid.name}
543
544 @staticmethod
545 def sql_sys_select(table_name):
546 """SQL fragment to add to the (end of the) list of fields to retrieve
547 in order to construct a SID later with from_sys_select()."""
548 if table_name == 'EVENTS' or table_name == 'REPORTS' or table_name == 'JOBS':
549 # no reason for events to be special but I haven't added CFID and OGSID to the tablers
550 # return ',SCID'
551 return ['SCID']
552
553 else:
554 # subscriptions, products
555 # return ',SCID, CFID, OGSID'
556 return ['SCID','CFID','OGSID']
557
558
559 @staticmethod
560 def from_sys_select(table_name, args):
561 """Construct a SID from the fields requested by sql_sys_select()."""
562 if table_name == 'EVENTS' or table_name == 'REPORTS' or table_name == 'JOBS':
563 if args[0] is None:
564 return None
565
566 else:
567 return SID_MSG(scid=args[0])
568
569 else:
570 return SID_MSG(scid=args[0], cfid=args[1], ogsid=args[2])
571
572 @staticmethod
573 def sql_sys_insert(table_name):
574 """SQL fragment to insert a new row.
575 Return value is a tuple of field names, values.
576 """
577 if table_name == 'EVENTS' or table_name == 'REPORTS' or table_name == 'JOBS':
578 return (',SCID', ',:scid')
579
580 elif table_name == 'PRODUCTS':
581 return (',SCID, CFID', ',:scid, :cfid')
582
583 else:
584 return (',SCID, CFID, OGSID', ',:scid, :cfid, :ogsid')
585
586 @staticmethod
587 def sql_sys_update(table_name):
588 """Modify the SID in a system table."""
589 if table_name == 'EVENTS':
590 return ',scid=:scid'
591
592 else:
593 raise NotImplementedError()
594
595 @staticmethod
596 def bind_sys_insert(table_name, sid):
597 """Bind variables to go with sys_sys_update()."""
598 # convert ourselves into a list to be inserted into an insert statement variable list
599 if table_name == 'EVENTS' or table_name == 'JOBS':
600 if sid is None:
601 return {'scid': None}
602
603 else:
604 return {'scid': sid.scid}
605
606 elif table_name == 'PRODUCTS':
607 return {'scid': sid.scid, 'cfid': sid.cfid.name}
608
609 elif table_name == 'REPORTS':
610 # in the reports table only we use 'SYS' for sidless reports
611 # print('we are reports scid is ' + str(self.scid) + ' type ' + str(type(self.scid)))
612 # print(len(self.scid))
613 if sid is None:
614 # print(' is sidless')
615 return {'scid': SID_MSG.SIDLESS.scid}
616
617 else:
618 # print(' is not sidless')
619 return {'scid': sid.scid}
620
621 else:
622 return {'scid': sid.scid,
623 'cfid': None if sid.cfid is None else sid.cfid.name,
624 'ogsid': None if sid.ogsid is None else sid.ogsid.name}
625
626 #
627 ##
628 ### XML support
629 ##
630 #
631
632 def to_xml(self, elem):
633 """Annotate parent `elem` with ourselves, creating child nodes."""
634 SubElement(elem, ELEM_SCID).text = self.scid
635 if self.cfid is not None:
636 SubElement(elem, ELEM_CFID).text = self.cfid.name
637
638 if self.ogsid is not None:
639 SubElement(elem, ELEM_OGSID).text = self.ogsid.name
640
641 @staticmethod
642 def from_xml(elem, wildcard=False):
643 """Create a SID from childs of `elem`."""
644 scid = parsechildstr(elem, ELEM_SCID, None)
645 if scid is None or scid == '':
646 return None
647
648 elif scid == SID_MSG.SIDLESS.scid:
649 return SID_MSG.SIDLESS
650
651 return SID_MSG(scid=scid,
652 cfid=parsechildstr(elem, ELEM_CFID, None),
653 ogsid='PRIM',
654 wildcard=wildcard)
655
656 #
657 ##
658 ### Wildcard support
659 ##
660 #
661
662 @staticmethod
663 def from_string(instr, wildcard=False):
664 """Create a SID from a single string
665 (wildcards in activity and schedule files)."""
666 if instr == SID_MSG.SIDLESS.scid or instr.lower() == SID_MSG.SIDLESS.scid.lower():
667 return SID_MSG.SIDLESS
668
669 else:
670 cfid = None
671 ogsid = None
672 if ':' in instr:
673 bits = instr.split(':')
674 scid = bits[0]
675 for bit in bits[1:]:
676 if bit.upper() in ('PRIM', 'BACK'): # !
677 ogsid = bit.upper()
678
679 elif bit.upper() in ('OPER', 'VALI'):
680 cfid = bit.upper()
681
682 else:
683 scid = instr
684 ogsid = 'PRIM'
685
686 return SID_MSG(scid=scid, ogsid=ogsid, cfid=cfid, wildcard=wildcard)
687
688 def expand(self):
689 """If we were created as a wildcard M* return a series of separate SIDS.
690 Used for job expansion if the schedule file says MSG*.
691 # yield instead?.
692 """
693 # raise NotImplementedError()
694 result = []
695 for s in SID_MSG.all():
696 # if not s.operational:
697 # continue
698
699 if fnmatch(s.scid, self.scid):
700 result.append(SID_MSG(s.scid))
701
702 return result
703
704 def match(self, other):
705 """Test is we match `other`.
706 Handles wildcards."""
707 return fnmatch(other.scid, self.scid)
708
709 #
710 ##
711 ### Command line arguments
712 ##
713 #
714
715 @staticmethod
716 def from_cmdline(arg):
717 """Convert a command line parameter to a SID.
718 We assume PRIM data unless otherwise specified.
719 -s MSG2
720 -s MSG2:ogsid=BACK
721 -s MSG2:bare
722 -s MSG2:ogsid=none
723 -s MSG2:cfid=oper
724 -s MSG%:cfid=oper
725 -s MSG*:cfid=oper
726 -s MSG[1,2]:cfid=oper
727
728 ok symbols: # @ : .
729 """
730 return SID_MSG(scid=arg, ogsid=OGSID.PRIM)
731
732 #
733 ##
734 ### Job handling support
735 ##
736 #
737
738 # job_module = 'chart.common.job'
739 # fields, search, store, retrieve
740
741 # @staticmethod
742 # def from_job(job):
743 # """Extract a SID from a JOB object
744 # (to be removed when Jobs store SID natively).
745 # """
746 # return SID(scid=job.scid, cfid='OPER', ogsid='PRIM')
747
748 #
749 ##
750 ### Satellite database support
751 ##
752 #
753
754 # List of valid satellites. Cached XML load.
755 satellites_elem = None
756
757 @staticmethod
758 def all(operational=True):
759 """Yield all satellites matching parameters.
760 CFID and OGSID will not be set.
761 """
762 from chart.project import settings
763 if SID_MSG.satellites_elem is None:
764 SID_MSG.satellites_elem = load_xml(settings.SATELLITES)
765
766 for sat_elem in SID_MSG.satellites_elem.findall(ELEM_SATELLITE):
767 if operational is None or operational == parsechildbool(sat_elem, ELEM_OPERATIONAL):
768 yield SID_MSG(parsechildstr(sat_elem, ELEM_SCID))
769
770 @staticmethod
771 def django_all():
772 """Return SID information sent to the web client."""
773 return [sid.as_dict() for sid in SID_MSG.all()]
774
775 # @staticmethod
776 # def all_operational():
777 # """Return all operational."""
778 # return [sid.as_dict() for sid in SID.all(operational=True)]
779
780 # @staticmethod
781 # def all_ground():
782 # """Return all ground."""
783 # return [sid.as_dict() for sid in SID.all(operational=False)]
784
785 def as_dict(self):
786 """Convert ourselves into a dictionary of data for the web interface client."""
787 operational = self.satellite is not None and self.satellite.operational
788 sid_dict = {'menu_value': {'scid': self.scid},
789 'initial_year': None,
790 'menu_name': '{name} ({scid})'.format(scid=self.scid, name=self.satellite.name),
791 'time_offset': 0,
792 'title': str(self),
793 'operational': operational
794 }
795
796 # Leave the cfid out completely if it is not set
797 # Otherwise the plotviewer and eventviewer will not be able to select sid properly
798 if self.cfid is not None:
799 sid_dict['menu_value']['cfid'] = self.cfid.name
800
801 return sid_dict
802
803 #
804 ##
805 ### Reporting
806 ##
807 #
808
809 @staticmethod
810 def all_report_sids():
811 """Scan REPORTs table and return a dictionary of report name against a set of
812 SIDs for which we have reports of that activity."""
813 # only used by reportviewer. can be removed
814 from chart.db.connection import db_connect
815 res = defaultdict(list)
816 db_conn = db_connect('REPORTS')
817 for scid, activity in db_conn.query(
818 'SELECT DISTINCT scid, activity FROM reports ORDER BY scid'):
819 res[activity].append(SID_MSG(scid) if scid is not None else SID_MSG.SIDLESS)
820
821 return res
822
823 @staticmethod
824 def report_sids(activity):
825 """Only used by reportviewer. can be removed."""
826 from chart.db.connection import db_connect
827 db_conn = db_connect('REPORTS')
828 res = []
829 for scid, activity in db_conn.query(
830 'SELECT DISTINCT scid, activity FROM reports '
831 'WHERE activity=:reportname ORDER BY scid',
832 reportname=activity.name):
833 res.append(SID_MSG(scid) if scid is not None else SID_MSG.SIDLESS)
834
835 return res
836
837 @staticmethod
838 def get_reportname_part(sid):
839 """Return a string to be the source ID part of a report filename
840 e.g. SVM_DHSA_REPORT_M02_20130901.zip."""
841 if sid is None or sid.scid is None:
842 return SID_MSG.SIDLESS.scid
843
844 else:
845 return sid.scid
846
847 @staticmethod
848 def from_reportname_part(fragment):
849 """Decode a report name fragment."""
850 if fragment == SID_MSG.SIDLESS.scid:
851 return SID_MSG.SIDLESS
852
853 else:
854 return SID_MSG(fragment)
855
856 @staticmethod
857 def ddl_cal_clause(sids):
858 """Return an SQL clause suitable for a CASE statement which will recognise
859 any of `sids`.
860 We don't distinguish ground segments here because calibration is only
861 satellite dependant.
862 """
863
864 if len(sids) == 1:
865 if sids[0].scid == '*':
866 return '1=1'
867
868 else:
869 return 'SCID=\'{scid}\''.format(scid=sids[0].scid)
870
871 else:
872 return 'SCID in ({members})'.format(
873 members=','.join('\'{s}\''.format(s=s.scid) for s in sids))
874
875 @property
876 def acronym(self):
877 return self.scid
878
879 @property
880 def name(self):
881 return self.scid
882
883 @property
884 def short_name(self):
885 return self.name
886
887# Used to represent a job or report which is has no source, usually a system
888# job like purge or a system digest report
889SID_MSG.SIDLESS = SID_MSG(scid='SYS', wildcard=True)