1#!/usr/bin/env python3
2
3"""Generic source-ID for PUS based missions.
4
5MTG
6JCS
7"""
8
9import logging
10from collections import namedtuple
11from collections import defaultdict
12from datetime import timedelta
13from datetime import datetime
14from enum import Enum
15from typing import Union
16from typing import Callable
17
18from chart.common.xml import SubElement
19from chart.plots.sampling import Sampling
20from chart.common.xml import parsechildstr
21from chart.sids.exceptions import NoSuchSID
22from chart.sids import SIDBase
23from chart.common.decorators import memoized
24from chart.common.decorators import memoized2
25from chart.common.xml import XMLElement
26from chart.project import settings
27from chart.common.path import Path
28
29logger = logging.getLogger()
30
31ELEM_SATELLITES = 'satellites'
32ELEM_SATELLITE = 'satellite'
33ELEM_NAME = 'name'
34ELEM_DESCRIPTION = 'description'
35ELEM_LAUNCH_DATE = 'launch-date'
36ELEM_DECOMMISION_DATE = 'decommision-date'
37ELEM_ORBIT_DURATION = 'orbit-duration'
38ELEM_SIDS = 'sids'
39ELEM_SID = 'sid'
40ELEM_SID_NUM = 'sid-num'
41ELEM_LONG_NAME = 'long-name'
42ELEM_COLOUR = 'colour'
43ELEM_VISIBLE = 'visible'
44ELEM_OPERATIONAL = 'operational'
45ELEM_GROUP = 'group'
46ELEM_DB_PATH = 'db-path'
47ELEM_ORBITER = 'orbiter'
48ELEM_GOOD_FRAME_SPID = 'good-frame-spid'
49ELEM_BAD_FRAME_SPID = 'bad-frame-spid'
50ELEM_UNKNOWN_PACKET_SPID = 'unknown-packet-spid'
51ELEM_BAD_PACKET_SPID = 'bad-packet-spid'
52ELEM_IDLE_PACKET_SPID = 'idle-packet-spid'
53ELEM_IDLE_FRAME_SPID = 'idle-frame-spid'
54ELEM_TIME_COUPLES_SPID = 'time-couples-spid'
55ELEM_SPECIAL_PACKETS = 'special-packets'
56ELEM_DYNAMIC_PACKETS = 'dynamic-udc-spid'
57
58
59ELEM_HIGH_RATE_PACKETS = 'high-rate-packets'
60ELEM_HR_TIME_LOC = 'high-rate-time-loc'
61ELEM_HR_SERVICE_IDS = 'high-rate-service'
62ELEM_HR_SUBSERVICE_IDS = 'high-rate-subservice'
63
64ELEM_SCOS_EV_PACKETS = 'scos-events-packets'
65ELEM_FIXED_SCOS_PACKETS = 'fixed-scos-events-packet'
66ELEM_VARIABLE_SCOS_PACKETS = 'variable-scos-events-packet'
67ELEM_OOL_TYPES = 'ool-event-types'
68ELEM_OOL_HEADER_ID = 'ool-header-id'
69ELEM_SCC_PACKET_ID = 'scc-packet-id'
70ELEM_SCC_PACKET_DEF_ID = 'scc-packet-def-id'
71
72ELEM_PACKET = 'packet'
73ELEM_ID = 'id'
74
75ELEM_SAMPLING_OPTIONS = 'sampling-options'
76ELEM_SAMPLING = 'sampling'
77ELEM_AUTO_SAMPLING_OPTIONS = 'auto-sampling-options'
78
79# Select the type of dropdown box(s) used to select a SID in the plot and event viewers
80ELEM_SELECTOR = 'selector'
81
82
83class Satellite:
84 def __init__(self,
85 name,
86 description=None,
87 launch_date=None,
88 decommision_date=None,
89 orbit_duration=None,
90 ground_test=None):
91 self.name = name
92 self.description = description
93 self.launch_date = launch_date
94 self.decommision_date = decommision_date
95 self.orbit_duration = orbit_duration
96 self.ground_test = ground_test
97 # the below is for backward-compatibility with SID_EPS implementation
98 # derived logic, as in line 1195 of chart/chart/plotviewer/retrieval.py:
99 self.ground_test = orbit_duration is not None
100
101@memoized
102def all_satellites(satellites_elem):
103 """Parse <satellites> from sattings.SOURCES and return a list of Satellites."""
104 result = []
105 for sat_elem in satellites_elem.findall(ELEM_SATELLITE):
106 result.append(Satellite(
107 name=sat_elem.parse_str(ELEM_NAME),
108 description=sat_elem.parse_str(ELEM_DESCRIPTION, None),
109 launch_date=sat_elem.parse_datetime(ELEM_LAUNCH_DATE, None),
110 decommision_date=sat_elem.parse_datetime(ELEM_DECOMMISION_DATE, None),
111 orbit_duration=sat_elem.parse_timedelta(ELEM_ORBIT_DURATION, None)))
112
113 return result
114
115@memoized
116def all_sids():
117 """Parse <satellites> from settings.SOURCES and return a list of SIDs."""
118 SID_PUS.construction_started = True
119 # print('all_sids')
120 root_elem = XMLElement(filename=settings.SOURCES)
121 sids_elem = root_elem.find(ELEM_SIDS)
122 satellites_elem = root_elem.find(ELEM_SATELLITES)
123 result = []
124 for sid_elem in sids_elem.findall(ELEM_SID):
125 name = sid_elem.parse_str(ELEM_NAME)
126 satellite_s = sid_elem.parse_str(ELEM_SATELLITE)
127 satellite = None
128 for s in all_satellites(satellites_elem):
129 if s.name == satellite_s:
130 satellite = s
131
132 if satellite is None:
133 raise ValueError('SID {n} has no satellite found'.format(n=name))
134
135 result.append(SID_PUS(
136 sid_num=sid_elem.parse_int(ELEM_SID_NUM),
137 name=name,
138 long_name=sid_elem.parse_str(ELEM_LONG_NAME, None),
139 description=sid_elem.parse_str(ELEM_DESCRIPTION, None),
140 satellite=satellite,
141 colour=sid_elem.parse_str(ELEM_COLOUR, None),
142 operational=sid_elem.parse_bool(ELEM_OPERATIONAL, True),
143 visible=sid_elem.parse_bool(ELEM_VISIBLE, True),
144 group=sid_elem.parse_str(ELEM_GROUP, None),
145 db_path=sid_elem.parse_str(ELEM_DB_PATH, None) if sid_elem.parse_str(ELEM_DB_PATH, None) else name,
146 orbit=sid_elem.parse_str(ELEM_ORBITER, None)))
147
148 # now fix the orbit members to point to an instance of the orbit<>timestamp converter class
149 for sid in result:
150 # check for sids that use another sid's orbiter
151 for s in result:
152 if sid.orbit == s.name:
153 sid.orbit = s.orbit
154
155 orbiter = s.orbit
156
157 # otherwise the SID should use it's named class as the orbit determiner
158 # i'm being lazy import the class properly here
159 if sid.orbit == 'chart.common.orbits_geo_events.OrbitDeterminer':
160 from chart.products.fdf.orbits_geo_events import OrbitDeterminer
161 sid.orbit = OrbitDeterminer(sid)
162 if sid.orbit == 'chart.common.orbits_epssg_events.OrbitDeterminer':
163 from chart.products.fdf.orbits_epssg_events import OrbitDeterminer
164 sid.orbit = OrbitDeterminer(sid)
165 if sid.orbit == 'chart.products.fdf.orbit_s3.Orbit':
166 from chart.products.fdf.orbit_s3 import Orbit
167 sid.orbit = Orbit(sid)
168
169 # read special packets definitions
170 SID_PUS.special_packets = SpecialPackets(root_elem.find(ELEM_SPECIAL_PACKETS))
171
172 # read special HR packets definitions
173 special_packets_elem = root_elem.find(ELEM_HIGH_RATE_PACKETS)
174 if special_packets_elem is not None:
175 SID_PUS.special_HR_packets = HR_SpecialPackets(special_packets_elem)
176
177 else:
178 # logger.warn('No special HR packets found')
179 SID_PUS.special_HR_packets = None
180
181 SCOS_EV_packets_elem = root_elem.find(ELEM_SCOS_EV_PACKETS)
182 if SCOS_EV_packets_elem is not None:
183 SID_PUS.SCOS_EV_packets = SCOS_EV_Packets(SCOS_EV_packets_elem)
184
185 else:
186 SID_PUS.SCOS_EV_packets = None
187
188 SID_PUS.construction_allowed = False
189
190 # Used to represent a report which is has no source, usually a system
191 # job like purge or a system digest report
192 # uses wildcard to switch off the validity check
193 # SID_PUS.SIDLESS = SID_PUS(name='SYS', sidless=True)
194 result.append(SID_PUS(sidless=True, visible=False))
195
196 from chart.plots.sampling import Sampling
197 SID_PUS.sampling_options = []
198 samplings_elem = root_elem.find(ELEM_SAMPLING_OPTIONS)
199 if samplings_elem is None:
200 logger.warn('Sources XML file has no samplings element')
201
202 else:
203 for s in samplings_elem.parse_strs(ELEM_SAMPLING):
204 SID_PUS.sampling_options.append(Sampling[s])
205
206 SID_PUS.global_auto_sampling_options = []
207 auto_samplings_elem = root_elem.find(ELEM_AUTO_SAMPLING_OPTIONS)
208 for s in auto_samplings_elem.parse_strs(ELEM_SAMPLING):
209 SID_PUS.global_auto_sampling_options.append(Sampling[s])
210
211 SID_PUS.selector = root_elem.parse_str(ELEM_SELECTOR)
212
213 SID_PUS.default_sid = {'name': result[0].name}
214
215 return result
216
217class SID_USER:
218 sid=None
219
220class SID_PUSMeta(type):
221 """Cached constructor.
222
223 During application startup, SID_PUS.construction_allowed is set and new instances
224 of SID_PUS can be freely created and added to the SID_PUS.all_sids list.
225
226 Later, `construction_allowed` is cleared and no new SIDs can be created, only retrieved
227 from the existing list.
228 """
229
230 def __call__(cls, name=None, sid_num=None, sidless=False, *args, **kwargs):
231 # print('metasid',cls,'name',name,'sid_num',sid_num,'args',args,'kwargs',kwargs)
232
233 if not SID_PUS.construction_started:
234 all_sids()
235
236 if SID_PUS.construction_allowed or sidless:
237 # allow sids from file and sidless to be actualy constructed
238 res = super(SID_PUSMeta, cls).__call__(name=name, sid_num=sid_num, *args, **kwargs)
239 return res
240
241 # SID_PUS.construction_allowed = False
242 for sid in all_sids():
243 # print('testing ',sid_num,name,' against ',sid)
244 if sid.sid_num == sid_num or (
245 sid.name is not None and name is not None and sid.name.upper() == name.upper()) or (
246 sid.db_path is not None and name is not None and sid.db_path.upper() == name.upper()
247 ):
248 SID_USER.sid = sid
249 return sid
250
251 # print('none found')
252
253
254class SpecialPackets:
255 """Mission-specific PUS RAPIDFILE SPID values that have special meanings in decoding code."""
256
257 def __init__(self, elem):
258 self.good_frame_spid = elem.parse_int(ELEM_GOOD_FRAME_SPID)
259 self.bad_frame_spid = elem.parse_int(ELEM_BAD_FRAME_SPID)
260 self.unknown_packet_spid = elem.parse_int(ELEM_UNKNOWN_PACKET_SPID)
261 self.bad_packet_spid = elem.parse_int(ELEM_BAD_PACKET_SPID)
262 self.idle_packet_spid = elem.parse_int(ELEM_IDLE_PACKET_SPID)
263 self.idle_frame_spid = elem.parse_int(ELEM_IDLE_FRAME_SPID)
264 self.time_couples_spid = elem.parse_int(ELEM_TIME_COUPLES_SPID, None)
265 self.dynamic_packet_spid = elem.parse_int(ELEM_DYNAMIC_PACKETS, None)
266
267
268class HR_SpecialPackets:
269 """Mission-specific High Rate Packets which use COLLECTION_DATA reference time,
270 contained within the packet, rather than packet sensing time.
271 """
272
273 def __init__(self, elem):
274 self.hr_service = []
275 hr_service_elem = elem.find(ELEM_HR_SERVICE_IDS)
276 for id_elem in hr_service_elem.findall(ELEM_PACKET):
277 self.hr_service.append(id_elem.parse_int(ELEM_ID))
278
279 self.hr_subservice = []
280 hr_subservice_elem = elem.find(ELEM_HR_SUBSERVICE_IDS)
281 for id_elem in hr_subservice_elem.findall(ELEM_PACKET):
282 self.hr_subservice.append(id_elem.parse_int(ELEM_ID))
283
284 self.hr_time_location = elem.parse_int(ELEM_HR_TIME_LOC, None)
285
286
287class SpecialPacketDefinition:
288 """Representation of a SCOS special packet.
289
290 For some reason we store their definitionss in sources.xml instead of in the SRDB directory."""
291 def __init__(self, elem):
292 self.spid = elem.parse_int(ELEM_ID)
293 self.name = elem.parse_str(ELEM_NAME)
294 self.description = elem.parse_str(ELEM_DESCRIPTION, None)
295
296
297class SCOS_EV_Packets:
298 """Mission-specific SCOS Event Packets which defined manually, as are not defined in MIB."""
299
300 def __init__(self, elem):
301 self.fixed_scos_events = []
302 for packet_elem in elem.find(ELEM_FIXED_SCOS_PACKETS).findall(ELEM_PACKET):
303 self.fixed_scos_events.append(SpecialPacketDefinition(packet_elem))
304
305 self.variable_scos_events = []
306 for packet_elem in elem.find(ELEM_VARIABLE_SCOS_PACKETS).findall(ELEM_PACKET):
307 self.variable_scos_events.append(SpecialPacketDefinition(packet_elem))
308
309 self.ool_event_types = []
310 for packet_elem in elem.find(ELEM_OOL_TYPES).findall(ELEM_PACKET):
311 self.ool_event_types.append(SpecialPacketDefinition(packet_elem))
312
313 self.ool_hdr_id = elem.parse_int(ELEM_OOL_HEADER_ID)
314 self.scc_packet_id = elem.parse_int(ELEM_SCC_PACKET_ID)
315 self.scc_packet_def_id = elem.parse_int(ELEM_SCC_PACKET_DEF_ID)
316
317 def is_variable_spid(self, spid):
318 """Test if `spid` is the SPID of a variable packet."""
319 return spid in (p.spid for p in self.variable_scos_events)
320
321 def is_fixed_spid(self, spid):
322 """Test if `spid` is the SPID of a fixed packet."""
323 return spid in (p.spid for p in self.fixed_scos_events)
324
325
326class SID_PUS(SIDBase, metaclass=SID_PUSMeta): # , metaclass=sid_mtg_meta):
327 """MTG Source ID object. To become general PUS or non-PUS sid_num based source.
328
329 For any SID identified by a simple name i.e. only SID_MSG is not suitable for folding
330 into this object."""
331
332 selector = None # 'mtg'
333 default_sid = None # {'sid': 'MTGi1'}
334 # all_sids = []
335
336 # For some crazy reason when you try to print CCSDS packets from a binary file
337 # the all() function doesn't get called so we put this in as a hack to get chartmtg packet
338 # display working. This is quite bad and very likely to cause bad results in chartjcs
339 special_HR_packets = None
340
341 # If exists and not null, this is the label for the aux events check box
342 # ("ECL" for eclipse FDF events in CHART-EPS, "OOL" for out out limits events in
343 # PUS projects)
344 plot_aux_events_label = 'OOL'
345
346 # these objects are only created during startup when settings.SOURCES is parsed. Afterward
347 # clients can only create SIDs via the metaclass cached lookup
348 construction_allowed = True
349 construction_started = False
350
351 global_auto_sampling_options = None
352 sampling_options = None
353
354 special_packets = None
355 # class SpecialPackets(Enum):
356 # GOOD_FRAME_SPID = 90010
357 # BAD_FRAME_SPID = 90020
358 # UNKNOWN_PACKET_SPID = 90030
359 # BAD_PACKET_SPID = 90040
360 # IDLE_PACKET_SPID = 90050
361 # IDLE_FRAME_SPID = 90060
362
363 # After reading the SPID from a RapidFile block, we determine it's a CADU block
364 # if it matches any of these values
365 # CADU_RECORDS = [
366 # SpecialPackets.GOOD_FRAME_SPID,
367 # SpecialPackets.BAD_FRAME_SPID,
368 # SpecialPackets.UNKNOWN_PACKET_SPID,
369 # SpecialPackets.BAD_PACKET_SPID,
370 # SpecialPackets.IDLE_PACKET_SPID,
371 # SpecialPackets.IDLE_FRAME_SPID,
372 # ]
373
374 # With NIS header, i.e. Frames
375 # WITH_NIS_HEADER = [
376 # SpecialPackets.GOOD_FRAME_SPID,
377 # SpecialPackets.BAD_FRAME_SPID,
378 # SpecialPackets.IDLE_FRAME_SPID,
379 # ]
380
381 # Without NIS Header, i.e. Packets
382 # NO_NIS_HEADER = [
383 # SpecialPackets.UNKNOWN_PACKET_SPID,
384 # SpecialPackets.BAD_PACKET_SPID,
385 # SpecialPackets.IDLE_PACKET_SPID,
386 # ]
387
388
389 def __init__(self,
390 sid_num: int=None,
391 name: str=None,
392 long_name: str=None,
393 description: str=None,
394 satellite: Satellite=None,
395 colour: str=None,
396 operational: bool=True,
397 visible: bool=True,
398 default: bool=False,
399 orbit: Union[str, Callable]=None,
400 group=None,
401 db_path: str=None):
402 """Args:
403
404 `sid_num`: Unique SID_NUM entry for database storage
405 `name`: Normal short unique name for this source
406 `long_name`: Longer and more descriptive name for example for graph titles
407 `description`: Very long name. Not currently used.
408 `satellite`: The Satellite object this SID refers to. Multiple SIDs can refer
409 to the same satellite.
410 `colour`: Suggested rendering colour for this SID used in the report viewer
411 `operational`: The SID is currently potentially in-flight and generating data.
412 Used to determine if regular algorithms and reports should be generated.
413 `visible`: Allow this SID to be hidden from the user interface, for internal testing
414 SIDs
415 `default`: True if this is the normal SID the user interface selects by default
416 `orbiter`: A callable (fn or class) used to map orbit numbers to times
417 `group`: Allow related SIDs to be grouped together in a user interface
418 `db_path`: contains spacecraft packet definations path i.e SGA or SGB
419 """
420 # print('sid init ' + str(name))
421 self.sid_num = sid_num
422 self.name = name
423 self.long_name = long_name
424 self.description = description
425 self.satellite = satellite
426 self.colour = colour
427 self.operational = operational
428 self.visible = visible
429 self.default = default
430 self.orbit = orbit
431 self.group = group
432 self.db_path = db_path
433
434 def __str__(self):
435 """Used for plotviewer titles and report viewer calendar."""
436 if self.name is None:
437 return 'Non satellite specific'
438
439 else:
440 return self.name
441
442 def __repr__(self):
443 return self.__str__()
444
445 def get_db_path():
446 logging.info("SID DB_path:{p}".format(p=SID_USER.sid.db_path))
447 return SID_USER.sid.db_path
448 @staticmethod
449 def ddl_id_fields():
450 """SID fields implicitly added to the start of all TS tables."""
451 from chart.db.model.table import FieldInfo
452 return [
453 FieldInfo(name='SID_NUM',
454 description='Source ID number',
455 datatype=int,
456 length=8),
457 ]
458
459
460 @staticmethod
461 def ddl_cal_clause(sids):
462 """Return an SQL clauses suitable for a CASE statement which will recognise
463 any of `sids`."""
464 if len(sids) == 1:
465 if sids[0].name == '*':
466 return '1=1'
467
468 else:
469 return 'SID_NUM={n}'.format(n=sids[0].sid_num)
470
471 else:
472 return 'SID_NUM in ({members})'.format(
473 members=','.join('{n}'.format(n=s.sid_num) for s in sids))
474
475
476 @staticmethod
477 def all(operational=None, visible=None, include_sys=False):
478 """Yield all satellites matching parameters."""
479 for s in all_sids():
480 if (operational is None or operational == s.operational) and\
481 (visible is None or visible == s.visible) and\
482 (include_sys or s.name is not None):
483 yield s
484
485 @staticmethod
486 def sql_sys_select(table_name): # (unused arg) pylint: disable=W0613
487 """Craft SQL fragment to retrieve the fields of a SID from a system table.
488
489 SID is constructed later from_sys_select()."""
490
491 return ['SID_NUM']
492
493 def to_xml(self, elem):
494 """Annotate parent `elem` with ourselves, creating child nodes."""
495 if self.name is not None:
496 SubElement(elem, ELEM_SID).text = self.name
497
498 @staticmethod
499 def from_xml(elem, wildcard=False):
500 """Create a SID from childs of `elem`, or None if not found.."""
501 name = parsechildstr(elem, ELEM_SID, None, expand_settings=True)
502 if name is None:
503 return None
504
505 return SID_PUS(name=name)
506
507 @staticmethod
508 def django_all():
509 """Return SID information sent to the web client."""
510 return [{'initial_year': s.satellite.launch_date.year if s.satellite is not None and
511 s.satellite.launch_date is not None else None,
512 'time_offset': 0, # self.satellite.time_offset.total_seconds()
513 # if self.satellite is not None and self.satellite.time_offset is not None
514 # else 0,
515 'title': s.name, # for plot viewer title
516 'menu_value': {'sid': s.name},
517 } for s in SID_PUS.all(operational=None)]
518
519 @staticmethod
520 def ts_indexes():
521 """Return list of TS AP table field names which should be indexed."""
522 return ['SID_NUM', ]
523
524 # @staticmethod
525 # def ts_fields():
526 # """Return a list of field names needed to store our instances in timeseries tables."""
527 # return ('SID_NUM', )
528
529 # def ts_values(self):
530 # """Return a list of values to store this specific instance in a timeseries table."""
531 # return [self.sid_num]
532
533 insert_fields = ['SID_NUM']
534
535 def bind_insert(self):
536 return [self.sid_num]
537
538 @staticmethod
539 def from_django_request(request):
540 """Extract SID(s) from `query`, either a Django request GET object
541 or a dictionary or extracted pairs from a plot URL datapoint (dict)."""
542 sid = request.get('sid')
543 if sid is None:
544 return None
545
546 if sid == 'SYS':
547 result = SID_PUS()
548 # return SID_PUS.SIDLESS
549
550 result = SID_PUS(name=sid)
551 assert result.sid_num is not None
552 return result
553 # a=result.name
554 # b=result.sid_num
555 # print(result)
556 # 1/0
557 # return SID_PUS(name=sid)
558
559 @staticmethod
560 def from_django_params(params, remove=False):
561 """Extract a SID from `query` (URL parameter string)."""
562 if 'sid' in params:
563 res = SID_PUS(params['sid'])
564 if remove:
565 del params['sid']
566
567 return res
568
569 else:
570 return None
571
572 def as_url_query(self, base=None):
573 """Return a dictionary representation of ourselves suitable for passing into
574 the urllib.urlencode() function to create/extend a URL query fragment."""
575 if base is None:
576 if self.name is None:
577 return {'sid': 'SYS'}
578
579 else:
580 return {'sid': self.name}
581
582 else:
583 if self.name is None:
584 base['sid'] = 'SYS'
585
586 else:
587 base['sid'] = self.name
588
589 return base
590
591 @staticmethod
592 def from_string(st):
593 for sid in SID_PUS.all():
594 if sid.name is not None and st.lower() == sid.name.lower():
595 return sid
596
597 raise ValueError('Unknown source {s}'.format(s=st))
598
599 def expand(self):
600 """Wildcard support, probably not needed but this still gets called."""
601 return [self]
602
603 @staticmethod
604 def get_reportname_part(sid):
605 """Return the SID fragment of a report name e.g. the M02 in
606 SVM_DHSA_REPORT_M02_20130901.zip.
607 Also used as subdirectory name in the reports archive.
608 Also used as part of the statefiles name.
609 """
610 if sid is None or sid.name is None:
611 return 'SYS'
612
613 else:
614 return sid.name
615
616 # subsampling options to be considered when auto-selecting best stats subsampling
617 # should be sorted from longest to shortest
618 @memoized2
619 def auto_sampling_options(self):
620 """Return the list of stats subsampling options available for this sid.
621 """
622 return SID_PUS.global_auto_sampling_options
623
624 def sql_where(self, table_name=None, match_on_none=False, prefix=None):
625 """Return an SQL clause that will test for ts rows containing us.
626
627 If `table_name` is set it will be the name (not Tableinfo) of the table to be written
628 to, which may be a system table.
629
630 If `match_on_none` is set then we will match table rows contains null values in the SID
631 column.
632
633 `prefix` can be used to prefix column names for queries that join tables.
634
635 Avoids bind variables."""
636 # if self.sid_num is None:
637 # we are the wildcard SID that matches anything
638 # return '1=1'
639
640 if match_on_none:
641 return '(SID_NUM is null or SID_NUM={sidnum})'.format(sidnum=self.sid_num)
642
643 elif self.sid_num is None:
644 raise ValueError('Cannot query for null SID')
645
646 return 'SID_NUM={sidnum}'.format(sidnum=self.sid_num)
647
648 @staticmethod
649 def from_reportname_part(fragment):
650 """Decode a report name fragment."""
651 if fragment == 'SYS': # for system reports like digest
652 return SID_PUS()
653
654 else:
655 return SID_PUS(fragment)
656
657 @staticmethod
658 def all_report_sids():
659 """Scan REPORTs table and return a dictionary of report name against a set of
660 SIDs for which we have reports of that activity."""
661 # only used by reportviewer. can be removed
662 from chart.db.connection import db_connect
663 res = defaultdict(list)
664 db_conn = db_connect('REPORTS')
665 bindvars = {}
666 project_clause = ''
667 if settings.DATABASE_PROJECT_ID is not None:
668 project_clause = 'WHERE project=:project '
669 bindvars['project'] = settings.DATABASE_PROJECT_ID
670
671 for activity, sid_num in db_conn.query((
672 'SELECT DISTINCT activity, sid_num '
673 'FROM reports {project_clause}'
674 'ORDER BY sid_num').format(project_clause=project_clause), **bindvars):
675 res[activity].append(SID_PUS(sid_num=sid_num) if sid_num is not None else SID_PUS())
676
677 return res
678
679 @staticmethod
680 def ddl_sys_fields(table_name=None): # (unused arg) pylint: disable=W0613
681 """Events, jobs, reports, subscriptions tables using the {{sid}} expansion."""
682 from chart.db.model.table import FieldInfo
683 return [
684 FieldInfo(name='SID_NUM',
685 datatype=int,
686 length=4,
687 allow_null=True,
688 description='Spacecraft ID'),
689 ]
690
691 def cal_dirname(self):
692 """Compute a subdirectory to write named calibration files to."""
693 return self.short_name
694
695 def cal_name(self):
696 """Compute a suitable plSQL function name suffix."""
697 return self.satellite.name
698
699 @property
700 def short_name(self):
701 return self.name
702
703 @staticmethod
704 def sql_sys_where(table_name, sid): # (unused arg) pylint: disable=W0613
705 """Return an SQL clause which will test for us in the JOBS or REPORTS or EVENTS table.
706 If `sid` is None then don't apply any filtering."""
707 # should be removed and reported with all bind variables
708 if sid is None:
709 return '1=1'
710
711 else:
712 return "SID_NUM={sidnum}".format(sidnum=sid.sid_num)
713
714 @staticmethod
715 def sql_sys_where_bind(table_name): # (unused arg) pylint: disable=W0613
716 """SQL fragment to use in WHERE clauses filtering for us, using bind variables."""
717 # Should be renamed to something better like sys_where_bind()
718 # and used exclusively with sql_sys_where() completely avoided due to potential
719 # security issue
720 return 'SID_NUM=:sid_num'
721
722 def bind_sys_where(self, table_name): # (unused arg) pylint: disable=W0613
723 """Convert ourselves into a bindvars dict for use with the sql emitted by
724 sql_sys_where_bind()."""
725 return {'sid_num': self.sid_num}
726
727 def db_dir(self, subdirectory:Path) -> Path:
728 """Return our configuration directory for `subdirectory`."""
729 # This is the bit that should be self.satellite.db_path instead so multiple SIDs
730 # can refer to the same satellite configuration
731 return settings.SID_SPECIFIC_DIR.joinpath(self.db_path, subdirectory)
732
733 @staticmethod
734 def sql_sys_insert(table_name):
735 """Return 2 SQL fragments for an insert statement:
736 - Fields
737 - Bind variables
738
739 E.g.:
740 sid_vars, sid_binds = SID.sql_sys_insert('JOBS')
741 insert_job.ins_cur = db_conn.prepared_cursor(
742 'INSERT INTO JOBS (CATEGORY, ACTIVITY, FILENAME, DIRNAME, {sidfield}ORBIT, '
743 'SENSING_START, SENSING_STOP, TABLENAME, EARLIEST_EXECUTION_TIME '
744 'VALUES '
745 '(:category, :activity, :filename, :dirname, {sidbind}:orbit, :sensing_start, '
746 ':sensing_stop, :tablename, earliest_execution_time'.format(
747 sidfield=sid_fields, sidbind=sid_binds))
748 .
749 """
750 return (',SID_NUM', ',:sidnum')
751
752 @staticmethod
753 def sql_sys_update(table_name): # (unused arg) pylint:disable=W0613
754 """SQL fragment, using bind variables, to update the SID part of a system table."""
755 return ',sid_num=:sidnum'
756
757 @staticmethod
758 def bind_sys_insert(table_name, sid):
759 """Convert ourselves into a dict to be inserted into an insert statement variable
760 list.
761 usage:
762 db_conn.query('INSERT INTO PRODUCTS (activity, filename, result{sidfields}, sensing_start) '
763 'VALUES (:activity, :filename, :result{sidbinds}, :sensing_start)'.format(
764 sidfields=sid_fields, sidbinds=sid_binds),
765 activity=activity,
766 filename=filename,
767 result=result,
768 sensing_start=sensing_start,
769 **SID.bind_sys_insert('PRODUCTS', sid))
770 .
771 """
772 if sid is None:
773 return {'sidnum': None}
774
775 else:
776 return {'sidnum': sid.sid_num}
777
778 @staticmethod
779 def from_sys_select(table_name, args): # (unused arg) pylint: disable=W0613
780 """Construct a SID from the fields requested by sql_sys_select()."""
781 if args[0] is None:
782 return None
783
784 else:
785 return SID_PUS(sid_num=args[0])
786
787 @staticmethod
788 def report_sids(activity):
789 """Only used by reportviewer. can be removed."""
790 from chart.db.connection import db_connect
791 db_conn = db_connect('REPORTS')
792 res = []
793 for activity, sid_num in db_conn.query(
794 'SELECT DISTINCT activity, sid_num FROM reports WHERE activity=:reportname',
795 reportname=activity.name):
796 res.append(SID_PUS(sid_num=sid_num))
797
798 return res
799
800 @memoized2
801 def all_tables(self):
802 """Yield all timeseries tables."""
803 from chart.db.model.table import TableInfo
804 return list(TableInfo.all())