dkist-header-validator 5.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,751 @@
1
+ """
2
+ Definition of the base objects for the creation of a spec validator
3
+ """
4
+ import logging
5
+ import os
6
+ from collections.abc import Mapping
7
+ from io import BytesIO
8
+ from numbers import Integral
9
+ from numbers import Real
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from typing import Callable
13
+ from typing import IO
14
+ from typing import Optional
15
+ from typing import Type
16
+
17
+ import astropy.time as t
18
+ import astropy.units as u
19
+ import numpy as np
20
+ import voluptuous as vol
21
+ from astropy.io import fits
22
+ from astropy.io.fits.hdu.hdulist import HDUList
23
+ from dkist_fits_specifications import spec214
24
+ from dkist_fits_specifications.utils import schema_type_hint
25
+ from voluptuous.error import Invalid
26
+
27
+ from dkist_header_validator.exceptions import ReturnTypeException
28
+ from dkist_header_validator.exceptions import SpecSchemaDefinitionException
29
+ from dkist_header_validator.exceptions import SpecValidationException
30
+ from dkist_header_validator.exceptions import TranslationException
31
+ from dkist_header_validator.exceptions import ValidationException
32
+ from dkist_header_validator.translator import translate_spec122_to_spec214_l0
33
+ from dkist_header_validator.utils.expansions import expand_naxis
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ __all__ = ["SpecValidator", "SpecSchema"]
38
+
39
+
40
+ class FormatInvalid(Invalid):
41
+ """
42
+ An value which does not match the schema format.
43
+ """
44
+
45
+
46
+ class FormatValidator:
47
+ """
48
+ Validate that the format is valid.
49
+ """
50
+
51
+ supported_formats = ["isot", "unit"]
52
+
53
+ def __init__(self, format_value):
54
+ if format_value not in self.supported_formats:
55
+ raise SpecSchemaDefinitionException(f"{format_value} is an unknown format to validate.")
56
+ self.format_value = format_value
57
+
58
+ def __call__(self, value):
59
+ if self.format_value == "unit":
60
+ try:
61
+ return u.Unit(value, format="fits")
62
+ except Exception:
63
+ raise FormatInvalid(f"{value} is not a valid FITS unit")
64
+
65
+ if self.format_value == "isot":
66
+ try:
67
+ return t.Time(value, format="fits")
68
+ except Exception:
69
+ raise FormatInvalid(f"{value} is not a valid FITS time")
70
+
71
+ def __repr__(self):
72
+ return f"FormatValidator(format='{self.format_value}')"
73
+
74
+
75
+ class FITSFloatInvalid(Invalid):
76
+ """
77
+ Validates a header value is a valid FITS float.
78
+
79
+ This means it's not NaN or ±Inf.
80
+ """
81
+
82
+
83
+ class FITSFloatValidator:
84
+ """
85
+ Validate a FITS floating point value.
86
+
87
+ This validator assumes the type has also been validated independently.
88
+ """
89
+
90
+ def __call__(self, value):
91
+ if not np.isfinite(value):
92
+ raise FITSFloatInvalid(
93
+ f"{value} is not finite, floats in a FITS header must be finite."
94
+ )
95
+
96
+ return value
97
+
98
+
99
+ class SpecSchema:
100
+ """
101
+ Define a schema that is used to validate FITS headers for a spec based upon structured definitions
102
+ in YAML or dicts.
103
+
104
+ Parameters
105
+ ----------
106
+ spec_schema_definitions
107
+ Definition of the spec's schema in one of the following forms:
108
+ - Dict definition of the spec schema
109
+ - List of Dict definitions of the spec schema
110
+ - Path to a YAML file defining the spec schema
111
+ - Path to a directory containing YAML files defining spec schema
112
+ """
113
+
114
+ # Schemas defined for a spec have the following structure per key
115
+ definition_schema_definition = {
116
+ vol.Required("required"): vol.Any(True, False),
117
+ vol.Required("type"): vol.Any("int", "float", "str", "bool"),
118
+ "values": list,
119
+ "values_range": list,
120
+ "expected": vol.Any(True, False),
121
+ "expand": vol.Any(True, False),
122
+ "format": vol.Any("unit", "isot"),
123
+ }
124
+ # Voluptuous schema instance used to validate the definitions
125
+ definition_schema = vol.Schema(definition_schema_definition, extra=vol.ALLOW_EXTRA)
126
+
127
+ def __init__(self, spec_schema_definitions: schema_type_hint):
128
+ # convert spec schema definitions to a list of dicts
129
+ self.spec_schema_definitions = self._parse_spec_schema_definitions(spec_schema_definitions)
130
+ # validate the dict definition of the spec schema
131
+ self._validate_spec_schema_definitions()
132
+
133
+ @classmethod
134
+ def _parse_spec_schema_definitions(cls, spec_schema: schema_type_hint) -> dict[str, Any]:
135
+ """
136
+ Convert from a list of dicts multiple formats to a dict
137
+ :param spec_schema: Definition(s) of the spec's schema
138
+ :return: Dictionary capturing the spec schema definition
139
+ """
140
+
141
+ # Test for proper types and non-emptiness
142
+ if not spec_schema:
143
+ raise SpecSchemaDefinitionException("spec_schema is empty or invalid.")
144
+
145
+ if not isinstance(spec_schema, Mapping):
146
+ raise SpecSchemaDefinitionException(
147
+ f"spec_schema is not a Mapping it is {type(spec_schema)}"
148
+ )
149
+
150
+ schema = {}
151
+ for section_name, section_schema in spec_schema.items():
152
+ if not section_schema:
153
+ raise SpecSchemaDefinitionException(
154
+ f"The {section_name} section has an empty schema: {section_schema!r}"
155
+ )
156
+ if not isinstance(section_schema, Mapping):
157
+ raise SpecSchemaDefinitionException(f"{type(section_schema)} is not a Mapping")
158
+ schema.update({key: dict(key_schema) for key, key_schema in section_schema.items()})
159
+ return schema
160
+
161
+ def _validate_spec_schema_definitions(self) -> None:
162
+ """
163
+ Validate the spec schema definitions against the class schema for
164
+ spec schema definitions and raise a SpecSchemaDefinitionException
165
+ on failure
166
+ :return: None
167
+ """
168
+
169
+ for key, spec_schema in self.spec_schema_definitions.items():
170
+ schema_errors = {}
171
+ try:
172
+ self.definition_schema(spec_schema)
173
+ except vol.MultipleInvalid as e:
174
+ schema_errors = {error.path[0]: error.msg for error in e.errors}
175
+ if schema_errors:
176
+ logger.debug(
177
+ f"Errors during schema definition validation. key={key} errors={schema_errors}"
178
+ )
179
+ raise SpecSchemaDefinitionException(
180
+ f"Errors during schema definition validation. key={key}",
181
+ errors=schema_errors,
182
+ )
183
+
184
+ def expand_schema(self, headers) -> dict:
185
+ """
186
+ Expand schema indices using information from fits headers
187
+
188
+ Parameters
189
+ ----------
190
+ headers
191
+ Fits file headers whose values are to be used to know how to expand spec keys
192
+
193
+ Returns
194
+ -------
195
+ schema dictionary
196
+ """
197
+ # 214 Expansion
198
+ if {"DAAXES", "DEAXES", "DNAXIS"}.issubset(headers.keys()):
199
+ # This function includes the expansion for NAXIS
200
+ return spec214.expand_214_schema(self.spec_schema_definitions, **headers)
201
+
202
+ # 122 Only expands "n" with NAXIS
203
+ return expand_naxis(headers["NAXIS"], self.spec_schema_definitions)
204
+
205
+ @staticmethod
206
+ def generate_schema_for_key(key_schema):
207
+ """
208
+ Generate voluptuous schema for a key
209
+
210
+ Parameters
211
+ ----------
212
+ key_schema
213
+ Spec schema (from yml) used to generate voluptuous schema
214
+ Returns
215
+ -------
216
+ Voluptuous schema for a key
217
+ """
218
+ type_map = {"int": Integral, "float": Real, "str": str, "bool": bool}
219
+
220
+ checks = []
221
+ # Always check type
222
+ checks.append(type_map[key_schema.get("type")])
223
+
224
+ def case_insenstive_values(value):
225
+ allowed_values = key_schema.get("values")
226
+ if key_schema.get("type") == "str":
227
+ value = value.lower()
228
+ allowed_values = [v.lower() for v in allowed_values]
229
+
230
+ return vol.Any(*allowed_values)(value)
231
+
232
+ def expand_values_range(value):
233
+ min_value = key_schema.get("values_range")[0]
234
+ try:
235
+ max_value = key_schema.get("values_range")[1]
236
+ except IndexError:
237
+ max_value = None
238
+
239
+ return vol.Range(min=min_value, max=max_value)(value)
240
+
241
+ if key_schema.get("values"):
242
+ checks.append(case_insenstive_values)
243
+
244
+ if key_schema.get("values_range"):
245
+ checks.append(expand_values_range)
246
+
247
+ if "format" in key_schema:
248
+ checks.append(FormatValidator(key_schema["format"]))
249
+
250
+ if key_schema.get("type") == "float":
251
+ checks.append(FITSFloatValidator())
252
+
253
+ return vol.All(*checks)
254
+
255
+ def _add_keys_to_schema(self, schema: dict) -> dict:
256
+ schema_keys = {} # keys to be added to voluptuous schema
257
+ required_keys = {
258
+ vol.Required(k): self.generate_schema_for_key(v)
259
+ for k, v in schema.items()
260
+ if v.get("required")
261
+ }
262
+ schema_keys.update(required_keys)
263
+ remaining_keys = {
264
+ k: self.generate_schema_for_key(v) for k, v in schema.items() if not v.get("required")
265
+ }
266
+ schema_keys.update(remaining_keys)
267
+ return schema_keys
268
+
269
+ def _check_for_expansion(self, headers):
270
+ for value in self.spec_schema_definitions.values():
271
+ # check the whole schema to see if there is an expand = True keyword
272
+ if value["expand"]:
273
+ # if there is an 'expand' keyword set to true in the schema, expand the whole spec schema
274
+ expanded_schema = self.expand_schema(headers)
275
+ return expanded_schema
276
+ return self.spec_schema_definitions
277
+
278
+ def _create_spec_schema(self, headers, extra) -> vol.Schema:
279
+ """
280
+ A voluptuous.spec_validator object to validate headers against.
281
+ Constructed from Spec keywords.
282
+ :param headers: Fits file headers
283
+ :return: Voluptuous schema
284
+ """
285
+
286
+ spec_schema = {}
287
+
288
+ schema = self._check_for_expansion(headers)
289
+ spec_schema.update(self._add_keys_to_schema(schema))
290
+ if extra:
291
+ return vol.Schema(spec_schema, extra=vol.ALLOW_EXTRA)
292
+ return vol.Schema(spec_schema)
293
+
294
+ def __call__(self, headers: dict, extra) -> vol.Schema:
295
+ """
296
+ Validate headers against the instance spec schema
297
+ raising voluptuous errors on failure
298
+
299
+ Parameters
300
+ ----------
301
+ headers
302
+ header dict to validate
303
+ extra
304
+
305
+ Returns
306
+ -------
307
+ vol.Schema
308
+
309
+ """
310
+
311
+ spec_schema = self._create_spec_schema(headers, extra)
312
+ return spec_schema(headers)
313
+
314
+
315
+ class SpecValidator:
316
+ """
317
+ Validates FITS Headers against a schema
318
+
319
+ Parameters
320
+ ----------
321
+ spec_schema
322
+ Definition of the spec's schema in one of the following forms:
323
+ - SpecSchema instance
324
+ - A dict of sections of key schemas as returned by the fits specification repo
325
+ - None. Used only for `ProcessedSpecValidator` where the actual schema will be updated dynamically
326
+
327
+ SchemaValidationException
328
+ SpecValidationException or subclass of SpecValidationException
329
+ to raise if spec_validator validation fails
330
+
331
+ """
332
+
333
+ def __init__(
334
+ self,
335
+ spec_schema: schema_type_hint | SpecSchema | None,
336
+ SchemaValidationException: Type[SpecValidationException] = SpecValidationException,
337
+ ):
338
+ # `spec_schema` is the callable for validating a dict against the defined spec_validator
339
+ if isinstance(spec_schema, SpecSchema) or spec_schema is None:
340
+ self.spec_schema = spec_schema
341
+ else:
342
+ self.spec_schema = SpecSchema(spec_schema)
343
+
344
+ # Exception raised when spec validation fails
345
+ self.SchemaValidationException = SchemaValidationException
346
+
347
+ @staticmethod
348
+ def _headers_to_dict(headers: HDUList | dict | fits.header.Header | str | IO) -> dict:
349
+ """
350
+ Convert headers from multiple types to a dict
351
+ :param headers: Headers to convert to a dict
352
+ :return: Dict of the headers
353
+ """
354
+ if isinstance(headers, dict):
355
+ return headers
356
+ if isinstance(headers, fits.header.Header):
357
+ return dict(headers)
358
+ if isinstance(headers, HDUList):
359
+ if len(headers) > 1:
360
+ return dict(headers[1].header)
361
+ return dict(headers[0].header)
362
+
363
+ def verify_headers(self, headers, extra) -> dict:
364
+ """
365
+ Validates file headers against the instance spec_validator
366
+
367
+ Parameters
368
+ ----------
369
+ headers
370
+ file headers
371
+ extra
372
+ switch for validation to allow extra keys in schema
373
+ Returns
374
+ -------
375
+ dict of headers
376
+
377
+ Raises
378
+ ------
379
+ SchemaValidationException
380
+
381
+ """
382
+
383
+ validation_errors = {}
384
+ try:
385
+ self.spec_schema(headers, extra)
386
+ except vol.MultipleInvalid as e:
387
+ for error in e.errors:
388
+ value_str = "Required keyword not present"
389
+ if error.path[0] in headers:
390
+ value_str = (
391
+ f"Actual value: {headers[error.path[0]]!r} {type(headers[error.path[0]])}"
392
+ )
393
+ validation_errors[error.path[0]] = f"{error.msg}. {value_str}"
394
+ # Raise exception if we have errors
395
+ if validation_errors:
396
+ missing_list = []
397
+ badtype_list = []
398
+ other_list = []
399
+
400
+ for key in validation_errors.keys():
401
+ if "required key not provided" in validation_errors[key]:
402
+ missing_list.append(key)
403
+ elif "expected" in validation_errors[key]:
404
+ badtype_list.append(key)
405
+ else:
406
+ other_list.append(key)
407
+ # Log the bad keys
408
+ for sublist, message in zip(
409
+ [missing_list, badtype_list, other_list],
410
+ [
411
+ "The following keys are missing:",
412
+ "The following keys have bad types:",
413
+ "The following keys have other errors:",
414
+ ],
415
+ ):
416
+ if len(sublist) > 0:
417
+ logger.debug(f"\n{message}")
418
+ for k in sorted(sublist):
419
+ logger.debug(f"{str(k):<10}:\t{validation_errors[k]}")
420
+
421
+ raise self.SchemaValidationException(errors=validation_errors)
422
+ logger.debug("Schema validation succeeded")
423
+
424
+ return headers
425
+
426
+ def _validate_headers(
427
+ self, input_headers: HDUList | dict | fits.header.Header, extra
428
+ ) -> tuple[dict, dict]:
429
+ """
430
+ Validates open input headers against the instance spec_schema
431
+ :param input_headers: The input headers to validate in the following formats:
432
+ - HDUList object
433
+ - fits.header.Header object
434
+ - Dictionary of header keys and values
435
+ :param extra: switch for validation to allow extra keys in schema
436
+ :return: dictionary of verified headers to be used later
437
+ """
438
+ if isinstance(input_headers, HDUList):
439
+ if len(input_headers) > 2:
440
+ raise ValidationException(
441
+ "Too many HDUs in your HDUList! May only have two HDUs at most."
442
+ )
443
+ headers = self._headers_to_dict(input_headers)
444
+ fits_cards = self._capture_fits_cards(headers)
445
+ verified_headers = self.verify_headers(headers, extra)
446
+ return verified_headers, fits_cards
447
+
448
+ def _validate_file(self, input_headers: str | IO, extra) -> tuple[dict, dict, np.ndarray]:
449
+ """
450
+ Validates files against the astropy and then instance spec_schema
451
+ :param input_headers: The input headers to validate in the following formats:
452
+ - string file path
453
+ - File like object
454
+ :param extra: switch for validation to allow extra keys in schema
455
+ :return: dictionary of verified headers to be used later
456
+ """
457
+ try:
458
+ with fits.open(input_headers) as hdul:
459
+ # verify fits headers with astropy verify library
460
+ hdul.verify("exception")
461
+ # normalize headers into a dict
462
+ try:
463
+ data = hdul[1].data
464
+ except IndexError: # non-compressed
465
+ data = hdul[0].data
466
+ verified_headers, fits_cards = self._validate_headers(hdul, extra)
467
+ return verified_headers, fits_cards, data
468
+ except (ValueError, FileNotFoundError, OSError, IndexError) as exc:
469
+ logger.debug(f"Cannot parse headers: detail = {exc}")
470
+ raise ValidationException("Cannot parse headers", errors={type(exc): str(exc)})
471
+
472
+ @staticmethod
473
+ def _return_HDU(validated_headers, data, fits_cards):
474
+ """
475
+ Returns validated headers as an HDU
476
+ :param validated_headers: Already validated/translated headers to be written out
477
+ into a FITS file
478
+ :param data: original data
479
+ :param fits_cards: Any special cards to be included in the FITS file
480
+ :return: HDU
481
+ """
482
+ new_hdu = fits.PrimaryHDU(data, header=fits.Header())
483
+ for (key, value) in validated_headers.items():
484
+ new_hdu.header[key] = value
485
+ for key in fits_cards:
486
+ try:
487
+ if key == "HISTORY":
488
+ for line in fits_cards["HISTORY"].splitlines():
489
+ new_hdu.header.add_history(line)
490
+ elif key == "COMMENT":
491
+ for line in fits_cards["COMMENT"].splitlines():
492
+ new_hdu.header.add_comment(line)
493
+ else:
494
+ new_hdu.header[key] = str(fits_cards[key])
495
+ except ValueError as e:
496
+ raise TranslationException("Error writing new header. Invalid header value") from e
497
+
498
+ return new_hdu
499
+
500
+ @classmethod
501
+ def _return_hdulist(cls, validated_headers, fits_cards) -> HDUList:
502
+ """
503
+ Returns validated headers as an HDUList
504
+ :param validated_headers: Already validated/translated headers to be written out into
505
+ an HDUList
506
+ :param fits_cards: Any special cards to be included in the HDUList
507
+ :return: HDUList
508
+ """
509
+ if isinstance(validated_headers, HDUList):
510
+ return validated_headers
511
+ temp_array: np.ndarray = np.ones((1, 1, 1), dtype=np.int16)
512
+ new_hdu = cls._return_HDU(validated_headers, data=temp_array, fits_cards=fits_cards)
513
+ new_hdu_list = fits.HDUList([new_hdu])
514
+ return new_hdu_list
515
+
516
+ @staticmethod
517
+ def _return_dictionary(validated_headers, fits_cards) -> dict:
518
+ """
519
+ Returns validated headers as a dictionary
520
+ :param validated_headers: Already validated/translated headers to be written out into
521
+ a dictionary
522
+ :param fits_cards: Any special cards to be included in the dictionary
523
+ :return: dictionary
524
+ """
525
+ for key in fits_cards:
526
+ validated_headers[key] = str(fits_cards[key])
527
+ return validated_headers
528
+
529
+ @classmethod
530
+ def _return_BytesIO(cls, validated_headers, input_headers, data, fits_cards) -> BytesIO:
531
+ """
532
+ Returns validated headers as a BytesIO object
533
+ :param validated_headers: Already validated/translated headers to be written out into
534
+ the BytesIO object
535
+ :param input_headers: original filepath or BytesIO object
536
+ :param data: original data
537
+ :param fits_cards: Any special cards to be included in the BytesIO object
538
+ :return: BytesIO object
539
+ """
540
+ new_hdu = cls._return_HDU(validated_headers, data, fits_cards)
541
+ new_hdu_list = fits.HDUList([new_hdu])
542
+ return BytesIO(
543
+ new_hdu_list.writeto(
544
+ str(os.path.basename(input_headers)),
545
+ overwrite=True,
546
+ output_verify="exception",
547
+ checksum=True,
548
+ )
549
+ )
550
+
551
+ @classmethod
552
+ def _return_file(cls, validated_headers, input_headers, data, fits_cards) -> (str, IO):
553
+ """
554
+ Returns validated headers as a FITS file
555
+ :param validated_headers: Already validated/translated headers to be written out
556
+ into a FITS file
557
+ :param input_headers: original filepath or BytesIO object
558
+ :param data: original data
559
+ :param fits_cards: Any special cards to be included in the FITS file
560
+ :return: FITS file
561
+ """
562
+ new_hdu = cls._return_HDU(validated_headers, data, fits_cards)
563
+ new_hdu_list = fits.HDUList([new_hdu])
564
+ new_hdu_list.writeto(
565
+ str(os.path.basename(input_headers)),
566
+ overwrite=True,
567
+ output_verify="exception",
568
+ checksum=True,
569
+ )
570
+ return Path(os.path.basename(input_headers))
571
+
572
+ def _format_output(
573
+ self, return_type, validated_headers, input_headers=None, data=None, fits_cards=None
574
+ ):
575
+ fits_cards = fits_cards or {}
576
+ if return_type == Path:
577
+ if data is None:
578
+ raise ReturnTypeException("No data. Cannot write file.")
579
+ return self._return_file(validated_headers, input_headers, data, fits_cards)
580
+ if return_type == BytesIO:
581
+ if data is None:
582
+ raise ReturnTypeException("No data. Cannot write BytesIO object.")
583
+ return self._return_BytesIO(validated_headers, input_headers, data, fits_cards)
584
+ if return_type == dict:
585
+ return self._return_dictionary(validated_headers, fits_cards)
586
+ if return_type == HDUList:
587
+ return self._return_hdulist(validated_headers, fits_cards)
588
+ if return_type == fits.header.Header:
589
+ return self._return_HDU(validated_headers, data, fits_cards).header
590
+ if return_type == fits.PrimaryHDU:
591
+ if data is None:
592
+ raise ReturnTypeException("No data. Cannot write PrimaryHDU.")
593
+ return self._return_HDU(validated_headers, data, fits_cards)
594
+
595
+ def _capture_fits_cards(self, validated_headers) -> dict:
596
+ """
597
+ Pull special fits cards out of validated_headers dict.
598
+ This is necessary for astropy header formatting.
599
+
600
+ :param validated_headers: validated headers
601
+ :return: fits_cards: dictionary containing special fits header keys and values
602
+ """
603
+ fits_cards = {}
604
+ if "HISTORY" in validated_headers:
605
+ fits_cards["HISTORY"] = str(validated_headers["HISTORY"])
606
+ validated_headers.pop("HISTORY")
607
+ if "COMMENT" in validated_headers:
608
+ fits_cards["COMMENT"] = str(validated_headers["COMMENT"])
609
+ validated_headers.pop("COMMENT")
610
+ # Remove any blank cards
611
+ if "" in validated_headers:
612
+ validated_headers.pop("")
613
+ return fits_cards
614
+
615
+ def validate(self, input_headers, return_type=HDUList, extra=True):
616
+ """
617
+ Validates against the instance spec_schema
618
+
619
+ Parameters
620
+ ----------
621
+ input_headers
622
+ The headers to validate in the following formats:
623
+ - string file path
624
+ - File like object
625
+ - HDUList object
626
+ - fits.header.Header object
627
+ - Dictionary of header keys and values
628
+
629
+ return_type
630
+ Determines return type. Default is HDUList. May be one of:
631
+ - dict
632
+ - BytesIO
633
+ - fits.header.Header
634
+ - Path (file)
635
+ - HDUList
636
+ - fits.PrimaryHDU
637
+ extra
638
+ Switch for validation to allow extra keys in schema. Default is true, which will
639
+ allow extra keys. Ingest validation should allow extra keys.
640
+
641
+ Returns
642
+ -------
643
+ Formatted headers
644
+
645
+ Raises
646
+ ------
647
+ dkist_header_validator.SpecValidationException or subclass
648
+ """
649
+ if isinstance(input_headers, (dict, fits.header.Header, HDUList)):
650
+ validated_headers, fits_cards = self._validate_headers(input_headers, extra)
651
+ return self._format_output(
652
+ return_type, validated_headers, input_headers, None, fits_cards
653
+ )
654
+ validated_headers, fits_cards, data = self._validate_file(input_headers, extra)
655
+ return self._format_output(return_type, validated_headers, input_headers, data, fits_cards)
656
+
657
+ def validate_and_translate_to_214_l0(self, input_headers, return_type=HDUList, extra=True):
658
+ """
659
+ Validates against the instance spec_schema and then translates to the spec214_l0 schema
660
+
661
+ Parameters
662
+ ----------
663
+ input_headers
664
+ The headers to validate in the following formats:
665
+ - string file path
666
+ - File like object
667
+ - HDUList object
668
+ - fits.header.Header object
669
+ - Dictionary of header keys and values
670
+
671
+ return_type
672
+ Determines return type. Default is HDUList. May be one of:
673
+ - dict
674
+ - BytesIO
675
+ - fits.header.Header
676
+ - Path (file)
677
+ - HDUList
678
+ - fits.PrimaryHDU
679
+ extra
680
+ Switch for validation to allow extra keys in schema. Default is true, which will
681
+ allow extra keys. Ingest validation should allow extra keys.
682
+
683
+ Returns
684
+ -------
685
+ Formatted 214 l0 headers
686
+
687
+ Raises
688
+ ------
689
+ dkist_header_validator.SpecValidationException or subclass
690
+ """
691
+ if isinstance(input_headers, (dict, fits.header.Header, HDUList)):
692
+ validated_headers, fits_cards = self._validate_headers(input_headers, extra)
693
+ translated_headers = translate_spec122_to_spec214_l0(validated_headers)
694
+ return self._format_output(return_type, translated_headers, None, None, fits_cards)
695
+ else:
696
+ validated_headers, fits_cards, data = self._validate_file(input_headers, extra)
697
+ translated_headers = translate_spec122_to_spec214_l0(validated_headers)
698
+ return self._format_output(
699
+ return_type, translated_headers, input_headers, data, fits_cards
700
+ )
701
+
702
+
703
+ class ProcessedSpecValidator(SpecValidator):
704
+ """
705
+ Validates FITS Headers against a schema that is updated based on the actual headers.
706
+
707
+ The two current examples of a "processed" spec are keys that are updated based on expansion or on conditional
708
+ requiredness.
709
+
710
+ Parameters
711
+ ----------
712
+ spec_processor_function
713
+ A function that can process a spec based on an input header. Probably `load_processed_spec214`.
714
+
715
+ SchemaValidationException
716
+ SpecValidationException or subclass of SpecValidationException
717
+ to raise if spec_validator validation fails
718
+
719
+ """
720
+
721
+ def __init__(
722
+ self,
723
+ spec_processor_function: Callable,
724
+ SchemaValidationException: Type[SpecValidationException] = SpecValidationException,
725
+ ):
726
+ self.spec_processor_function = spec_processor_function
727
+
728
+ # Initializing with `spec_schema=None` is done to avoid logging spam from the fits-spec.
729
+ # The actual `spec_schema` will be updated when a header is verified.
730
+ super().__init__(spec_schema=None, SchemaValidationException=SchemaValidationException)
731
+
732
+ def verify_headers(self, headers, extra) -> dict:
733
+ """
734
+ Validates file headers against the instance spec_validator
735
+
736
+ Parameters
737
+ ----------
738
+ headers
739
+ file headers
740
+ extra
741
+ switch for validation to allow extra keys in schema
742
+ Returns
743
+ -------
744
+ dict of headers
745
+
746
+ Raises
747
+ ------
748
+ SchemaValidationException
749
+ """
750
+ self.spec_schema = SpecSchema(self.spec_processor_function(**headers))
751
+ return super().verify_headers(headers, extra)