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)