dkist-processing-common 10.5.4__py3-none-any.whl → 12.1.0rc1__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.
Files changed (122) hide show
  1. changelog/280.misc.rst +1 -0
  2. changelog/282.feature.2.rst +2 -0
  3. changelog/282.feature.rst +2 -0
  4. changelog/284.feature.rst +1 -0
  5. changelog/285.feature.rst +2 -0
  6. changelog/285.misc.rst +2 -0
  7. changelog/286.feature.rst +2 -0
  8. changelog/287.misc.rst +1 -0
  9. dkist_processing_common/__init__.py +1 -0
  10. dkist_processing_common/_util/constants.py +1 -0
  11. dkist_processing_common/_util/graphql.py +1 -0
  12. dkist_processing_common/_util/scratch.py +9 -9
  13. dkist_processing_common/_util/tags.py +1 -0
  14. dkist_processing_common/codecs/array.py +20 -0
  15. dkist_processing_common/codecs/asdf.py +9 -3
  16. dkist_processing_common/codecs/basemodel.py +22 -0
  17. dkist_processing_common/codecs/bytes.py +1 -0
  18. dkist_processing_common/codecs/fits.py +37 -9
  19. dkist_processing_common/codecs/iobase.py +1 -0
  20. dkist_processing_common/codecs/json.py +1 -0
  21. dkist_processing_common/codecs/path.py +1 -0
  22. dkist_processing_common/codecs/quality.py +1 -1
  23. dkist_processing_common/codecs/str.py +1 -0
  24. dkist_processing_common/config.py +64 -25
  25. dkist_processing_common/manual.py +6 -8
  26. dkist_processing_common/models/constants.py +373 -37
  27. dkist_processing_common/models/dkist_location.py +27 -0
  28. dkist_processing_common/models/fits_access.py +48 -0
  29. dkist_processing_common/models/flower_pot.py +231 -9
  30. dkist_processing_common/models/fried_parameter.py +41 -0
  31. dkist_processing_common/models/graphql.py +66 -75
  32. dkist_processing_common/models/input_dataset.py +117 -0
  33. dkist_processing_common/models/message.py +1 -1
  34. dkist_processing_common/models/message_queue_binding.py +1 -1
  35. dkist_processing_common/models/metric_code.py +2 -0
  36. dkist_processing_common/models/parameters.py +65 -28
  37. dkist_processing_common/models/quality.py +50 -5
  38. dkist_processing_common/models/tags.py +23 -21
  39. dkist_processing_common/models/task_name.py +3 -2
  40. dkist_processing_common/models/telemetry.py +28 -0
  41. dkist_processing_common/models/wavelength.py +3 -1
  42. dkist_processing_common/parsers/average_bud.py +46 -0
  43. dkist_processing_common/parsers/cs_step.py +13 -12
  44. dkist_processing_common/parsers/dsps_repeat.py +6 -4
  45. dkist_processing_common/parsers/experiment_id_bud.py +12 -4
  46. dkist_processing_common/parsers/id_bud.py +42 -27
  47. dkist_processing_common/parsers/l0_fits_access.py +5 -3
  48. dkist_processing_common/parsers/l1_fits_access.py +51 -23
  49. dkist_processing_common/parsers/lookup_bud.py +125 -0
  50. dkist_processing_common/parsers/near_bud.py +21 -20
  51. dkist_processing_common/parsers/observing_program_id_bud.py +24 -0
  52. dkist_processing_common/parsers/proposal_id_bud.py +13 -5
  53. dkist_processing_common/parsers/quality.py +2 -0
  54. dkist_processing_common/parsers/retarder.py +32 -0
  55. dkist_processing_common/parsers/single_value_single_key_flower.py +6 -1
  56. dkist_processing_common/parsers/task.py +8 -6
  57. dkist_processing_common/parsers/time.py +178 -72
  58. dkist_processing_common/parsers/unique_bud.py +21 -22
  59. dkist_processing_common/parsers/wavelength.py +5 -3
  60. dkist_processing_common/tasks/__init__.py +3 -2
  61. dkist_processing_common/tasks/assemble_movie.py +4 -3
  62. dkist_processing_common/tasks/base.py +59 -60
  63. dkist_processing_common/tasks/l1_output_data.py +54 -53
  64. dkist_processing_common/tasks/mixin/globus.py +24 -27
  65. dkist_processing_common/tasks/mixin/interservice_bus.py +1 -0
  66. dkist_processing_common/tasks/mixin/metadata_store.py +108 -243
  67. dkist_processing_common/tasks/mixin/object_store.py +22 -0
  68. dkist_processing_common/tasks/mixin/quality/__init__.py +1 -0
  69. dkist_processing_common/tasks/mixin/quality/_base.py +8 -1
  70. dkist_processing_common/tasks/mixin/quality/_metrics.py +166 -14
  71. dkist_processing_common/tasks/output_data_base.py +4 -3
  72. dkist_processing_common/tasks/parse_l0_input_data.py +277 -15
  73. dkist_processing_common/tasks/quality_metrics.py +9 -9
  74. dkist_processing_common/tasks/teardown.py +7 -7
  75. dkist_processing_common/tasks/transfer_input_data.py +67 -69
  76. dkist_processing_common/tasks/trial_catalog.py +77 -17
  77. dkist_processing_common/tasks/trial_output_data.py +16 -17
  78. dkist_processing_common/tasks/write_l1.py +102 -72
  79. dkist_processing_common/tests/conftest.py +32 -173
  80. dkist_processing_common/tests/mock_metadata_store.py +271 -0
  81. dkist_processing_common/tests/test_assemble_movie.py +4 -4
  82. dkist_processing_common/tests/test_assemble_quality.py +32 -4
  83. dkist_processing_common/tests/test_base.py +5 -19
  84. dkist_processing_common/tests/test_codecs.py +103 -12
  85. dkist_processing_common/tests/test_constants.py +15 -0
  86. dkist_processing_common/tests/test_dkist_location.py +15 -0
  87. dkist_processing_common/tests/test_fits_access.py +56 -19
  88. dkist_processing_common/tests/test_flower_pot.py +147 -5
  89. dkist_processing_common/tests/test_fried_parameter.py +27 -0
  90. dkist_processing_common/tests/test_input_dataset.py +78 -361
  91. dkist_processing_common/tests/test_interservice_bus.py +1 -0
  92. dkist_processing_common/tests/test_interservice_bus_mixin.py +1 -1
  93. dkist_processing_common/tests/test_manual_processing.py +33 -0
  94. dkist_processing_common/tests/test_output_data_base.py +5 -7
  95. dkist_processing_common/tests/test_parameters.py +71 -22
  96. dkist_processing_common/tests/test_parse_l0_input_data.py +115 -32
  97. dkist_processing_common/tests/test_publish_catalog_messages.py +2 -24
  98. dkist_processing_common/tests/test_quality.py +1 -0
  99. dkist_processing_common/tests/test_quality_mixin.py +255 -23
  100. dkist_processing_common/tests/test_scratch.py +2 -1
  101. dkist_processing_common/tests/test_stems.py +511 -168
  102. dkist_processing_common/tests/test_submit_dataset_metadata.py +3 -7
  103. dkist_processing_common/tests/test_tags.py +1 -0
  104. dkist_processing_common/tests/test_task_name.py +1 -1
  105. dkist_processing_common/tests/test_task_parsing.py +17 -7
  106. dkist_processing_common/tests/test_teardown.py +28 -24
  107. dkist_processing_common/tests/test_transfer_input_data.py +270 -125
  108. dkist_processing_common/tests/test_transfer_l1_output_data.py +2 -3
  109. dkist_processing_common/tests/test_trial_catalog.py +83 -8
  110. dkist_processing_common/tests/test_trial_output_data.py +46 -73
  111. dkist_processing_common/tests/test_workflow_task_base.py +8 -10
  112. dkist_processing_common/tests/test_write_l1.py +298 -76
  113. dkist_processing_common-12.1.0rc1.dist-info/METADATA +265 -0
  114. dkist_processing_common-12.1.0rc1.dist-info/RECORD +134 -0
  115. {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/WHEEL +1 -1
  116. docs/conf.py +1 -0
  117. docs/index.rst +1 -1
  118. docs/landing_page.rst +13 -0
  119. dkist_processing_common/tasks/mixin/input_dataset.py +0 -166
  120. dkist_processing_common-10.5.4.dist-info/METADATA +0 -175
  121. dkist_processing_common-10.5.4.dist-info/RECORD +0 -112
  122. {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,63 +1,115 @@
1
+ import collections
2
+ from enum import StrEnum
3
+ from itertools import chain
4
+
1
5
  import pytest
2
6
  from astropy.io import fits
3
7
 
4
8
  from dkist_processing_common.models.constants import BudName
5
9
  from dkist_processing_common.models.fits_access import FitsAccessBase
10
+ from dkist_processing_common.models.fits_access import MetadataKey
6
11
  from dkist_processing_common.models.tags import StemName
7
12
  from dkist_processing_common.models.task_name import TaskName
13
+ from dkist_processing_common.parsers.average_bud import TaskAverageBud
8
14
  from dkist_processing_common.parsers.cs_step import CSStepFlower
9
15
  from dkist_processing_common.parsers.cs_step import NumCSStepBud
10
16
  from dkist_processing_common.parsers.dsps_repeat import DspsRepeatNumberFlower
11
17
  from dkist_processing_common.parsers.dsps_repeat import TotalDspsRepeatsBud
12
18
  from dkist_processing_common.parsers.experiment_id_bud import ContributingExperimentIdsBud
13
19
  from dkist_processing_common.parsers.experiment_id_bud import ExperimentIdBud
20
+ from dkist_processing_common.parsers.id_bud import TaskContributingIdsBud
21
+ from dkist_processing_common.parsers.lookup_bud import TaskTimeLookupBud
22
+ from dkist_processing_common.parsers.lookup_bud import TimeLookupBud
14
23
  from dkist_processing_common.parsers.near_bud import NearFloatBud
15
24
  from dkist_processing_common.parsers.near_bud import TaskNearFloatBud
25
+ from dkist_processing_common.parsers.observing_program_id_bud import (
26
+ TaskContributingObservingProgramExecutionIdsBud,
27
+ )
16
28
  from dkist_processing_common.parsers.proposal_id_bud import ContributingProposalIdsBud
17
29
  from dkist_processing_common.parsers.proposal_id_bud import ProposalIdBud
30
+ from dkist_processing_common.parsers.retarder import RetarderNameBud
18
31
  from dkist_processing_common.parsers.single_value_single_key_flower import (
19
32
  SingleValueSingleKeyFlower,
20
33
  )
21
- from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
22
34
  from dkist_processing_common.parsers.task import PolcalTaskFlower
23
35
  from dkist_processing_common.parsers.task import TaskTypeFlower
36
+ from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
24
37
  from dkist_processing_common.parsers.time import AverageCadenceBud
25
38
  from dkist_processing_common.parsers.time import ExposureTimeFlower
26
39
  from dkist_processing_common.parsers.time import MaximumCadenceBud
27
40
  from dkist_processing_common.parsers.time import MinimumCadenceBud
28
41
  from dkist_processing_common.parsers.time import ObsIpStartTimeBud
29
42
  from dkist_processing_common.parsers.time import ReadoutExpTimeFlower
43
+ from dkist_processing_common.parsers.time import TaskDateBeginBud
44
+ from dkist_processing_common.parsers.time import TaskDatetimeBudBase
30
45
  from dkist_processing_common.parsers.time import TaskExposureTimesBud
31
46
  from dkist_processing_common.parsers.time import TaskReadoutExpTimesBud
47
+ from dkist_processing_common.parsers.time import TaskRoundTimeBudBase
32
48
  from dkist_processing_common.parsers.time import VarianceCadenceBud
33
49
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
34
50
  from dkist_processing_common.parsers.unique_bud import UniqueBud
35
51
  from dkist_processing_common.parsers.wavelength import ObserveWavelengthBud
36
52
 
37
53
 
54
+ class FitsReaderMetadataKey(StrEnum):
55
+ thing_id = "id_key"
56
+ constant_thing = "constant"
57
+ near_thing = "near"
58
+ proposal_id = "ID___013"
59
+ experiment_id = "ID___012"
60
+ observing_program_execution_id = "ID___008"
61
+ ip_task_type = "DKIST004"
62
+ ip_start_time = "DKIST011"
63
+ fpa_exposure_time_ms = "XPOSURE"
64
+ sensor_readout_exposure_time_ms = "TEXPOSUR"
65
+ num_raw_frames_per_fpa = "NSUMEXP"
66
+ num_dsps_repeats = "DSPSREPS"
67
+ current_dsps_repeat = "DSPSNUM"
68
+ time_obs = "DATE-OBS"
69
+ gos_level3_status = "GOSLVL3"
70
+ gos_level3_lamp_status = "GOSLAMP"
71
+ gos_level0_status = "GOSLVL0"
72
+ gos_retarder_status = "GOSRET"
73
+ gos_polarizer_status = "GOSPOL"
74
+ wavelength = "LINEWAV"
75
+ roundable_time = "RTIME"
76
+
77
+
38
78
  class FitsReader(FitsAccessBase):
39
79
  def __init__(self, hdu, name):
40
80
  super().__init__(hdu, name)
41
- self.thing_id: int = self.header.get("id_key")
42
- self.constant_thing: float = self.header.get("constant")
43
- self.near_thing: float = self.header.get("near")
81
+ self.thing_id: int = self.header.get(FitsReaderMetadataKey.thing_id)
82
+ self.constant_thing: int = self.header.get(FitsReaderMetadataKey.constant_thing)
83
+ self.near_thing: float = self.header.get(FitsReaderMetadataKey.near_thing)
44
84
  self.name = name
45
- self.proposal_id: str = self.header.get("ID___013")
46
- self.experiment_id: str = self.header.get("ID___012")
47
- self.ip_task_type: str = self.header.get("DKIST004")
48
- self.ip_start_time: str = self.header.get("DKIST011")
49
- self.fpa_exposure_time_ms: float = self.header.get("XPOSURE")
50
- self.sensor_readout_exposure_time_ms: float = self.header.get("TEXPOSUR")
51
- self.num_raw_frames_per_fpa: int = self.header.get("NSUMEXP")
52
- self.num_dsps_repeats: int = self.header.get("DSPSREPS")
53
- self.current_dsps_repeat: int = self.header.get("DSPSNUM")
54
- self.time_obs: str = self.header.get("DATE-OBS")
55
- self.gos_level3_status: str = self.header.get("GOSLVL3")
56
- self.gos_level3_lamp_status: str = self.header.get("GOSLAMP")
57
- self.gos_level0_status: str = self.header.get("GOSLVL0")
58
- self.gos_retarder_status: str = self.header.get("GOSRET")
59
- self.gos_polarizer_status: str = self.header.get("GOSPOL")
60
- self.wavelength: str = self.header.get("LINEWAV")
85
+ self.proposal_id: str = self.header.get(FitsReaderMetadataKey.proposal_id)
86
+ self.experiment_id: str = self.header.get(FitsReaderMetadataKey.experiment_id)
87
+ self.observing_program_execution_id: str = self.header.get(
88
+ FitsReaderMetadataKey.observing_program_execution_id
89
+ )
90
+ self.ip_task_type: str = self.header.get(FitsReaderMetadataKey.ip_task_type)
91
+ self.ip_start_time: str = self.header.get(FitsReaderMetadataKey.ip_start_time)
92
+ self.fpa_exposure_time_ms: float = self.header.get(
93
+ FitsReaderMetadataKey.fpa_exposure_time_ms
94
+ )
95
+ self.sensor_readout_exposure_time_ms: float = self.header.get(
96
+ FitsReaderMetadataKey.sensor_readout_exposure_time_ms
97
+ )
98
+ self.num_raw_frames_per_fpa: int = self.header.get(
99
+ FitsReaderMetadataKey.num_raw_frames_per_fpa
100
+ )
101
+ self.num_dsps_repeats: int = self.header.get(FitsReaderMetadataKey.num_dsps_repeats)
102
+ self.current_dsps_repeat: int = self.header.get(FitsReaderMetadataKey.current_dsps_repeat)
103
+ self.time_obs: str = self.header.get(FitsReaderMetadataKey.time_obs)
104
+ self.gos_level3_status: str = self.header.get(FitsReaderMetadataKey.gos_level3_status)
105
+ self.gos_level3_lamp_status: str = self.header.get(
106
+ FitsReaderMetadataKey.gos_level3_lamp_status
107
+ )
108
+ self.gos_level0_status: str = self.header.get(FitsReaderMetadataKey.gos_level0_status)
109
+ self.gos_retarder_status: str = self.header.get(FitsReaderMetadataKey.gos_retarder_status)
110
+ self.gos_polarizer_status: str = self.header.get(FitsReaderMetadataKey.gos_polarizer_status)
111
+ self.wavelength: str = self.header.get(FitsReaderMetadataKey.wavelength)
112
+ self.roundable_time: float = self.header.get(FitsReaderMetadataKey.roundable_time, 0.0)
61
113
 
62
114
 
63
115
  @pytest.fixture()
@@ -71,6 +123,7 @@ def basic_header_objs():
71
123
  "DKIST004": "observe",
72
124
  "ID___012": "experiment_id_1",
73
125
  "ID___013": "proposal_id_1",
126
+ "ID___008": "observing_program_execution_id_1",
74
127
  "XPOSURE": 0.0013000123,
75
128
  "TEXPOSUR": 10.0,
76
129
  "NSUMEXP": 3,
@@ -89,6 +142,7 @@ def basic_header_objs():
89
142
  "DKIST004": "observe",
90
143
  "ID___012": "experiment_id_1",
91
144
  "ID___013": "proposal_id_1",
145
+ "ID___008": "observing_program_execution_id_2",
92
146
  "XPOSURE": 0.0013000987,
93
147
  "TEXPOSUR": 10.0,
94
148
  "NSUMEXP": 3,
@@ -97,6 +151,7 @@ def basic_header_objs():
97
151
  "DATE-OBS": "2022-06-17T22:00:01.000",
98
152
  "DKIST011": "2023-09-28T10:23.000",
99
153
  "LINEWAV": 666.0,
154
+ "GOSRET": "incorrect",
100
155
  }
101
156
  ),
102
157
  "thing2": fits.header.Header(
@@ -107,6 +162,7 @@ def basic_header_objs():
107
162
  "DKIST004": "dark",
108
163
  "ID___012": "experiment_id_2",
109
164
  "ID___013": "proposal_id_2",
165
+ "ID___008": "observing_program_execution_id_2",
110
166
  "XPOSURE": 12.345,
111
167
  "TEXPOSUR": 1.123456789,
112
168
  "NSUMEXP": 1,
@@ -115,6 +171,8 @@ def basic_header_objs():
115
171
  "DATE-OBS": "2022-06-17T22:00:02.000",
116
172
  "DKIST011": "1903-01-01T12:00.000",
117
173
  "LINEWAV": 0.0,
174
+ "GOSRET": "wrong",
175
+ "RTIME": 2.3400000009999,
118
176
  }
119
177
  ),
120
178
  "thing3": fits.header.Header(
@@ -125,6 +183,7 @@ def basic_header_objs():
125
183
  "DKIST004": "observe",
126
184
  "ID___012": "experiment_id_1",
127
185
  "ID___013": "proposal_id_1",
186
+ "ID___008": "observing_program_execution_id_1",
128
187
  "XPOSURE": 100.0,
129
188
  "TEXPOSUR": 11.0,
130
189
  "NSUMEXP": 4,
@@ -133,6 +192,28 @@ def basic_header_objs():
133
192
  "DATE-OBS": "2022-06-17T22:00:03.000",
134
193
  "DKIST011": "2023-09-28T10:23.000",
135
194
  "LINEWAV": 666.0,
195
+ "GOSRET": "clear",
196
+ },
197
+ ),
198
+ "thing4": fits.header.Header(
199
+ {
200
+ "DKIST004": "gain",
201
+ "ID___013": "proposal_id_1",
202
+ "ID___008": "observing_program_execution_id_1",
203
+ "id_key": 0,
204
+ "constant": 6.28,
205
+ "near": 1.23,
206
+ "ID___012": "experiment_id_1",
207
+ "XPOSURE": 100.0,
208
+ "TEXPOSUR": 11.0,
209
+ "NSUMEXP": 5,
210
+ "DSPSNUM": 2,
211
+ "DSPSREPS": 2,
212
+ "DATE-OBS": "2022-06-17T22:00:03.000",
213
+ "DKIST011": "2023-09-28T10:23.000",
214
+ "LINEWAV": 666.0,
215
+ "GOSRET": "clear",
216
+ "RTIME": 2.340000004444,
136
217
  }
137
218
  ),
138
219
  }
@@ -150,7 +231,12 @@ def task_with_gains_header_objs():
150
231
 
151
232
 
152
233
  @pytest.fixture
153
- def task_with_polcal_header_objs():
234
+ def retarder_name():
235
+ return "Foo Bar"
236
+
237
+
238
+ @pytest.fixture
239
+ def task_with_polcal_header_objs(retarder_name):
154
240
  header_dict = {
155
241
  "polcal_dark": fits.header.Header(
156
242
  {"DKIST004": "polcal", "GOSLVL0": "DarkShutter", "GOSPOL": "clear", "GOSRET": "clear"}
@@ -158,7 +244,9 @@ def task_with_polcal_header_objs():
158
244
  "polcal_gain": fits.header.Header(
159
245
  {"DKIST004": "polcal", "GOSLVL0": "FieldStop", "GOSPOL": "clear", "GOSRET": "clear"}
160
246
  ),
161
- "just_polcal": fits.header.Header({"DKIST004": "polcal", "GOSLVL0": "something"}),
247
+ "just_polcal": fits.header.Header(
248
+ {"DKIST004": "polcal", "GOSLVL0": "something", "GOSRET": retarder_name}
249
+ ),
162
250
  }
163
251
  return (FitsReader.from_header(header, name=path) for path, header in header_dict.items())
164
252
 
@@ -190,28 +278,61 @@ def bad_header_objs():
190
278
  "LINEWAV": 1.0,
191
279
  }
192
280
  ),
281
+ "thing2": fits.header.Header(
282
+ {
283
+ "id_key": 1,
284
+ "constant": 2.78,
285
+ "near": 1.76,
286
+ "DKIST004": "gain",
287
+ "DSPSREPS": 2,
288
+ "DSPSNUM": 2,
289
+ "DATE-OBS": "2022-06-17T22:00:03.000",
290
+ "LINEWAV": 1.0,
291
+ }
292
+ ),
293
+ "thing4": fits.header.Header(
294
+ {
295
+ "id_key": 1,
296
+ "constant": 6.66,
297
+ "near": 1.76,
298
+ "DKIST004": "dark",
299
+ "DSPSREPS": 2,
300
+ "DSPSNUM": 2,
301
+ "DATE-OBS": "2022-06-17T22:00:03.000",
302
+ "LINEWAV": 1.0,
303
+ }
304
+ ),
193
305
  }
194
306
  return (FitsReader.from_header(header, name=path) for path, header in bad_headers.items())
195
307
 
196
308
 
309
+ @pytest.fixture
310
+ def bad_polcal_header_objs():
311
+ # I.e., GOSRET has multiple values
312
+ header_dict = {
313
+ "thing1": fits.header.Header({"DKIST004": "polcal", "GOSRET": "clear"}),
314
+ "thing2": fits.header.Header({"DKIST004": "polcal", "GOSRET": "RET1"}),
315
+ "thing3": fits.header.Header({"DKIST004": "polcal", "GOSRET": "RET2"}),
316
+ }
317
+ return (FitsReader.from_header(header, name=path) for path, header in header_dict.items())
318
+
319
+
197
320
  def test_unique_bud(basic_header_objs):
198
321
  """
199
322
  Given: A set of headers with a constant value header key
200
323
  When: Ingesting headers with a UniqueBud and asking for the value
201
324
  Then: The Bud's value is the header constant value
202
325
  """
203
- bud = UniqueBud(
326
+ bud_obj = UniqueBud(
204
327
  constant_name="constant",
205
328
  metadata_key="constant_thing",
206
329
  )
207
- assert bud.stem_name == "constant"
330
+ assert bud_obj.stem_name == "constant"
208
331
  for fo in basic_header_objs:
209
332
  key = fo.name
210
- bud.update(key, fo)
333
+ bud_obj.update(key, fo)
211
334
 
212
- petal = list(bud.petals)
213
- assert len(petal) == 1
214
- assert petal[0].value == 6.28
335
+ assert bud_obj.bud.value == 6.28
215
336
 
216
337
 
217
338
  def test_unique_bud_non_unique_inputs(bad_header_objs):
@@ -220,54 +341,66 @@ def test_unique_bud_non_unique_inputs(bad_header_objs):
220
341
  When: Ingesting headers with a UniqueBud and asking for the value
221
342
  Then: An error is raised
222
343
  """
223
- bud = UniqueBud(
344
+ bud_obj = UniqueBud(
224
345
  constant_name="constant",
225
346
  metadata_key="constant_thing",
226
347
  )
227
- assert bud.stem_name == "constant"
348
+ assert bud_obj.stem_name == "constant"
228
349
  for fo in bad_header_objs:
229
350
  key = fo.name
230
- bud.update(key, fo)
351
+ bud_obj.update(key, fo)
231
352
 
232
- with pytest.raises(ValueError):
233
- assert next(bud.petals)
353
+ with pytest.raises(ValueError, match="Multiple constant values found! Values:"):
354
+ _ = bud_obj.bud
234
355
 
235
356
 
236
- def test_task_unique_bud(basic_header_objs):
357
+ @pytest.mark.parametrize(
358
+ "ip_task_type",
359
+ [
360
+ pytest.param("observe", id="single_task_type"),
361
+ pytest.param(["observe", "gain"], id="task_type_list"),
362
+ ],
363
+ )
364
+ def test_task_unique_bud(basic_header_objs, ip_task_type):
237
365
  """
238
366
  Given: A set of headers with a constant value header key
239
367
  When: Ingesting headers with a TaskUniqueBud and asking for the value
240
368
  Then: The bud's value is the header constant value
241
369
  """
242
- bud = TaskUniqueBud(
243
- constant_name="proposal", metadata_key="proposal_id", ip_task_type="observe"
370
+ bud_obj = TaskUniqueBud(
371
+ constant_name="proposal", metadata_key="proposal_id", ip_task_types=ip_task_type
244
372
  )
245
- assert bud.stem_name == "proposal"
373
+ assert bud_obj.stem_name == "proposal"
246
374
  for fo in basic_header_objs:
247
375
  key = fo.name
248
- bud.update(key, fo)
376
+ bud_obj.update(key, fo)
249
377
 
250
- petal = list(bud.petals)
251
- assert len(petal) == 1
252
- assert petal[0].value == "proposal_id_1"
378
+ assert bud_obj.bud.value == "proposal_id_1"
253
379
 
254
380
 
255
- def test_task_unique_bud_non_unique_inputs(bad_header_objs):
381
+ @pytest.mark.parametrize(
382
+ "ip_task_type",
383
+ [
384
+ pytest.param("observe", id="single_task_type"),
385
+ pytest.param(["dark", "gain"], id="task_type_list"),
386
+ ],
387
+ )
388
+ def test_task_unique_bud_non_unique_inputs(bad_header_objs, ip_task_type):
256
389
  """
257
390
  Given: A set of headers with a non-constant header key that is expected to be constant
258
391
  When: Ingesting headers with a UniqueBud and asking for the value
259
392
  Then: An error is raised
260
393
  """
261
394
  bud = TaskUniqueBud(
262
- constant_name="constant", metadata_key="constant_thing", ip_task_type="observe"
395
+ constant_name="constant", metadata_key="constant_thing", ip_task_types=ip_task_type
263
396
  )
264
397
  assert bud.stem_name == "constant"
265
398
  for fo in bad_header_objs:
266
399
  key = fo.name
267
400
  bud.update(key, fo)
268
401
 
269
- with pytest.raises(ValueError):
270
- assert next(bud.petals)
402
+ with pytest.raises(ValueError, match="Multiple constant values found! Values:"):
403
+ _ = bud.bud
271
404
 
272
405
 
273
406
  def test_single_value_single_key_flower(basic_header_objs):
@@ -285,13 +418,69 @@ def test_single_value_single_key_flower(basic_header_objs):
285
418
  petals = sorted(list(flower.petals), key=lambda x: x.value)
286
419
  assert len(petals) == 3
287
420
  assert petals[0].value == 0
288
- assert petals[0].keys == ["thing0", "thing3"]
421
+ assert petals[0].keys == ["thing0", "thing3", "thing4"]
289
422
  assert petals[1].value == 1
290
423
  assert petals[1].keys == ["thing1"]
291
424
  assert petals[2].value == 2
292
425
  assert petals[2].keys == ["thing2"]
293
426
 
294
427
 
428
+ @pytest.mark.parametrize(
429
+ "ip_task_type, expected_value",
430
+ [
431
+ pytest.param("dark", (1655503202.0,), id="single_task_type"),
432
+ pytest.param(["dark", "gain"], (1655503202.0, 1655503203.0), id="task_type_list"),
433
+ pytest.param(
434
+ ["dark", "gain", "observe"],
435
+ (1655503200.0, 1655503201.0, 1655503202.0, 1655503203.0, 1655503203.0),
436
+ id="task_type_list2",
437
+ ),
438
+ ],
439
+ )
440
+ def test_task_datetime_base_bud(basic_header_objs, ip_task_type, expected_value):
441
+ """
442
+ Given: A set of headers with a datetime value that does not need to be rounded
443
+ When: Ingesting headers with a `TaskDatetimeBudBase` bud and asking for the value
444
+ Then: The bud's value is the list of datetimes in seconds
445
+ """
446
+ bud_obj = TaskDatetimeBudBase(
447
+ stem_name="datetimes",
448
+ metadata_key=FitsReaderMetadataKey.time_obs,
449
+ ip_task_types=ip_task_type,
450
+ )
451
+ assert bud_obj.stem_name == "datetimes"
452
+ for fo in basic_header_objs:
453
+ key = fo.name
454
+ bud_obj.update(key, fo)
455
+
456
+ assert bud_obj.bud.value == expected_value
457
+
458
+
459
+ @pytest.mark.parametrize(
460
+ "ip_task_type, expected_value",
461
+ [
462
+ pytest.param("dark", (2.34,), id="single_task_type"),
463
+ pytest.param(["dark", "gain"], (2.34,), id="task_type_list"),
464
+ pytest.param(["dark", "gain", "observe"], (0.0, 2.34), id="task_type_list2"),
465
+ ],
466
+ )
467
+ def test_task_round_time_base_bud(basic_header_objs, ip_task_type, expected_value):
468
+ """
469
+ Given: A set of headers with a time value that needs to be rounded
470
+ When: Ingesting headers with a `TaskRoundTimeBudBase` bud and asking for the value
471
+ Then: The bud's value is the header constant value
472
+ """
473
+ bud_obj = TaskRoundTimeBudBase(
474
+ stem_name="rounded_time", metadata_key="roundable_time", ip_task_types=ip_task_type
475
+ )
476
+ assert bud_obj.stem_name == "rounded_time"
477
+ for fo in basic_header_objs:
478
+ key = fo.name
479
+ bud_obj.update(key, fo)
480
+
481
+ assert bud_obj.bud.value == expected_value
482
+
483
+
295
484
  def test_cs_step_flower(grouped_cal_sequence_headers, non_polcal_headers, max_cs_step_time_sec):
296
485
  """
297
486
  Given: A set of PolCal headers, non-PolCal headers, and the CSStepFlower
@@ -320,17 +509,15 @@ def test_num_cs_step_bud(grouped_cal_sequence_headers, non_polcal_headers, max_c
320
509
  When: Updating the NumCSStepBud with all headers
321
510
  Then: The bud reports the correct number of CS Steps (thus ignoring the non-PolCal frames)
322
511
  """
323
- num_cs_bud = NumCSStepBud(max_cs_step_time_sec=max_cs_step_time_sec)
512
+ num_cs_bud_obj = NumCSStepBud(max_cs_step_time_sec=max_cs_step_time_sec)
324
513
  for step, headers in grouped_cal_sequence_headers.items():
325
514
  for h in headers:
326
- num_cs_bud.update(step, h)
515
+ num_cs_bud_obj.update(step, h)
327
516
 
328
517
  for h in non_polcal_headers:
329
- num_cs_bud.update("foo", h)
518
+ num_cs_bud_obj.update("foo", h)
330
519
 
331
- bud = list(num_cs_bud.petals)
332
- assert len(bud) == 1
333
- assert bud[0].value == len(grouped_cal_sequence_headers.keys())
520
+ assert num_cs_bud_obj.bud.value == len(grouped_cal_sequence_headers.keys())
334
521
 
335
522
 
336
523
  def test_proposal_id_bud(basic_header_objs):
@@ -339,15 +526,13 @@ def test_proposal_id_bud(basic_header_objs):
339
526
  When: Ingesting the headers with a ProposalIdBud
340
527
  Then: The Bud's petal has the correct value
341
528
  """
342
- bud = ProposalIdBud()
343
- assert bud.stem_name == BudName.proposal_id.value
529
+ bud_obj = ProposalIdBud()
530
+ assert bud_obj.stem_name == BudName.proposal_id.value
344
531
  for fo in basic_header_objs:
345
532
  key = fo.name
346
- bud.update(key, fo)
533
+ bud_obj.update(key, fo)
347
534
 
348
- petal = list(bud.petals)
349
- assert len(petal) == 1
350
- assert petal[0].value == "proposal_id_1"
535
+ assert bud_obj.bud.value == "proposal_id_1"
351
536
 
352
537
 
353
538
  def test_contributing_proposal_ids_bud(basic_header_objs):
@@ -356,15 +541,13 @@ def test_contributing_proposal_ids_bud(basic_header_objs):
356
541
  When: Ingesting the headers with a ContributingProposalIdsBud
357
542
  Then: The Bud's petal is the tuple of all input proposal IDs
358
543
  """
359
- bud = ContributingProposalIdsBud()
360
- assert bud.stem_name == BudName.contributing_proposal_ids.value
544
+ bud_obj = ContributingProposalIdsBud()
545
+ assert bud_obj.stem_name == BudName.contributing_proposal_ids.value
361
546
  for fo in basic_header_objs:
362
547
  key = fo.name
363
- bud.update(key, fo)
548
+ bud_obj.update(key, fo)
364
549
 
365
- petal = list(bud.petals)
366
- assert len(petal) == 1
367
- assert sorted(list(petal[0].value)) == ["proposal_id_1", "proposal_id_2"]
550
+ assert sorted(list(bud_obj.bud.value)) == ["proposal_id_1", "proposal_id_2"]
368
551
 
369
552
 
370
553
  def test_experiment_id_bud(basic_header_objs):
@@ -373,15 +556,13 @@ def test_experiment_id_bud(basic_header_objs):
373
556
  When: Ingesting the headers with a ExperimentIdBud
374
557
  Then: The Bud's petal has the correct value
375
558
  """
376
- bud = ExperimentIdBud()
377
- assert bud.stem_name == BudName.experiment_id.value
559
+ bud_obj = ExperimentIdBud()
560
+ assert bud_obj.stem_name == BudName.experiment_id.value
378
561
  for fo in basic_header_objs:
379
562
  key = fo.name
380
- bud.update(key, fo)
563
+ bud_obj.update(key, fo)
381
564
 
382
- petal = list(bud.petals)
383
- assert len(petal) == 1
384
- assert petal[0].value == "experiment_id_1"
565
+ assert bud_obj.bud.value == "experiment_id_1"
385
566
 
386
567
 
387
568
  def test_contributing_experiment_ids_bud(basic_header_objs):
@@ -390,15 +571,53 @@ def test_contributing_experiment_ids_bud(basic_header_objs):
390
571
  When: Ingesting the headers with a ContributingExperimentIdsBud
391
572
  Then: The Bud's petal is the tuple of all input experiment IDs
392
573
  """
393
- bud = ContributingExperimentIdsBud()
394
- assert bud.stem_name == BudName.contributing_experiment_ids.value
574
+ bud_obj = ContributingExperimentIdsBud()
575
+ assert bud_obj.stem_name == BudName.contributing_experiment_ids.value
395
576
  for fo in basic_header_objs:
396
577
  key = fo.name
397
- bud.update(key, fo)
578
+ bud_obj.update(key, fo)
398
579
 
399
- petal = list(bud.petals)
400
- assert len(petal) == 1
401
- assert sorted(list(petal[0].value)) == ["experiment_id_1", "experiment_id_2"]
580
+ assert sorted(list(bud_obj.bud.value)) == ["experiment_id_1", "experiment_id_2"]
581
+
582
+
583
+ def test_task_contributing_ids_bud(basic_header_objs):
584
+ """
585
+ Given: A set of headers with experiment ID values for different tasks
586
+ When: Ingesting the headers with a TaskContributingIdsBud for the dark task
587
+ Then: The Bud's petal is just the experiment ID for the dark task
588
+ """
589
+ bud_obj = TaskContributingIdsBud(
590
+ constant_name=BudName.experiment_id,
591
+ metadata_key=MetadataKey.experiment_id,
592
+ ip_task_types=TaskName.dark,
593
+ )
594
+ assert bud_obj.stem_name == BudName.experiment_id.value
595
+ for fo in basic_header_objs:
596
+ key = fo.name
597
+ bud_obj.update(key, fo)
598
+
599
+ assert sorted(list(bud_obj.bud.value)) == ["experiment_id_2"]
600
+
601
+
602
+ def test_task_contributing_observing_program_execution_ids_bud(basic_header_objs):
603
+ """
604
+ Given: A set of headers with observing program execution ID values for different tasks
605
+ When: Ingesting the headers with a TaskContributingObservingProgramExecutionIdsBud for a task type
606
+ Then: The Bud's petal is the observing program execution IDs for the that task type
607
+ """
608
+ bud_obj = TaskContributingObservingProgramExecutionIdsBud(
609
+ constant_name="NOT_A_REAL_BUD",
610
+ ip_task_types=TaskName.observe,
611
+ )
612
+ assert bud_obj.stem_name == "NOT_A_REAL_BUD"
613
+ for fo in basic_header_objs:
614
+ key = fo.name
615
+ bud_obj.update(key, fo)
616
+
617
+ assert sorted(list(bud_obj.bud.value)) == [
618
+ "observing_program_execution_id_1",
619
+ "observing_program_execution_id_2",
620
+ ]
402
621
 
403
622
 
404
623
  def test_exp_time_flower(basic_header_objs):
@@ -420,7 +639,7 @@ def test_exp_time_flower(basic_header_objs):
420
639
  assert petals[1].value == 12.345
421
640
  assert petals[1].keys == ["thing2"]
422
641
  assert petals[2].value == 100.0
423
- assert petals[2].keys == ["thing3"]
642
+ assert petals[2].keys == ["thing3", "thing4"]
424
643
 
425
644
 
426
645
  def test_readout_exp_time_flower(basic_header_objs):
@@ -442,7 +661,7 @@ def test_readout_exp_time_flower(basic_header_objs):
442
661
  assert petals[1].value == 10.0
443
662
  assert petals[1].keys == ["thing0", "thing1"]
444
663
  assert petals[2].value == 11.0
445
- assert petals[2].keys == ["thing3"]
664
+ assert petals[2].keys == ["thing3", "thing4"]
446
665
 
447
666
 
448
667
  def test_task_type_flower(task_with_gains_header_objs):
@@ -493,15 +712,13 @@ def test_obs_ip_start_time_bud(basic_header_objs):
493
712
  When: Ingesting with a ObsIpStartTimeBud
494
713
  Then: The correct value from *only* the observe IP is returned
495
714
  """
496
- bud = ObsIpStartTimeBud()
497
- assert bud.stem_name == BudName.obs_ip_start_time.value
715
+ bud_obj = ObsIpStartTimeBud()
716
+ assert bud_obj.stem_name == BudName.obs_ip_start_time.value
498
717
  for fo in basic_header_objs:
499
718
  key = fo.name
500
- bud.update(key, fo)
719
+ bud_obj.update(key, fo)
501
720
 
502
- petals = list(bud.petals)
503
- assert len(petals) == 1
504
- assert petals[0].value == "2023-09-28T10:23.000"
721
+ assert bud_obj.bud.value == "2023-09-28T10:23.000"
505
722
 
506
723
 
507
724
  def test_fpa_exp_times_bud(basic_header_objs):
@@ -510,23 +727,19 @@ def test_fpa_exp_times_bud(basic_header_objs):
510
727
  When: Ingesting with a TaskExposureTimesBud
511
728
  Then: All (rounded) exposure times are accounted for in the resulting tuple
512
729
  """
513
- dark_bud = TaskExposureTimesBud(stem_name=BudName.dark_exposure_times, ip_task_type="DARK")
514
- obs_bud = TaskExposureTimesBud(stem_name="obs_exp_times", ip_task_type="OBSERVE")
515
- assert dark_bud.stem_name == BudName.dark_exposure_times.value
730
+ dark_bud_obj = TaskExposureTimesBud(stem_name=BudName.dark_exposure_times, ip_task_types="DARK")
731
+ obs_bud_obj = TaskExposureTimesBud(stem_name="obs_exp_times", ip_task_types="OBSERVE")
732
+ assert dark_bud_obj.stem_name == BudName.dark_exposure_times.value
516
733
  for fo in basic_header_objs:
517
734
  key = fo.name
518
- dark_bud.update(key, fo)
519
- obs_bud.update(key, fo)
735
+ dark_bud_obj.update(key, fo)
736
+ obs_bud_obj.update(key, fo)
520
737
 
521
- dark_petal = list(dark_bud.petals)
522
- assert len(dark_petal) == 1
523
- assert type(dark_petal[0].value) is tuple
524
- assert tuple(sorted(dark_petal[0].value)) == (12.345,)
738
+ assert type(dark_bud_obj.bud.value) is tuple
739
+ assert tuple(sorted(dark_bud_obj.bud.value)) == (12.345,)
525
740
 
526
- obs_petal = list(obs_bud.petals)
527
- assert len(obs_petal) == 1
528
- assert type(obs_petal[0].value) is tuple
529
- assert tuple(sorted(obs_petal[0].value)) == (0.0013, 100.0)
741
+ assert type(obs_bud_obj.bud.value) is tuple
742
+ assert tuple(sorted(obs_bud_obj.bud.value)) == (0.0013, 100.0)
530
743
 
531
744
 
532
745
  def test_readout_exp_times_bud(basic_header_objs):
@@ -535,35 +748,36 @@ def test_readout_exp_times_bud(basic_header_objs):
535
748
  When: Ingesting with a TaskReadoutExpTimesBud
536
749
  Then: All (rounded) exposure times are accounted for in the resulting tuple
537
750
  """
538
- dark_bud = TaskReadoutExpTimesBud(stem_name=BudName.dark_exposure_times, ip_task_type="DARK")
539
- obs_bud = TaskReadoutExpTimesBud(stem_name="obs_exp_times", ip_task_type="OBSERVE")
540
- assert dark_bud.stem_name == BudName.dark_exposure_times.value
751
+ dark_bud_obj = TaskReadoutExpTimesBud(
752
+ stem_name=BudName.dark_exposure_times, ip_task_types="DARK"
753
+ )
754
+ obs_bud_obj = TaskReadoutExpTimesBud(stem_name="obs_exp_times", ip_task_types="OBSERVE")
755
+ assert dark_bud_obj.stem_name == BudName.dark_exposure_times.value
541
756
  for fo in basic_header_objs:
542
757
  key = fo.name
543
- dark_bud.update(key, fo)
544
- obs_bud.update(key, fo)
758
+ dark_bud_obj.update(key, fo)
759
+ obs_bud_obj.update(key, fo)
545
760
 
546
- dark_petal = list(dark_bud.petals)
547
- assert len(dark_petal) == 1
548
- assert type(dark_petal[0].value) is tuple
549
- assert tuple(sorted(dark_petal[0].value)) == (1.123457,)
761
+ assert type(dark_bud_obj.bud.value) is tuple
762
+ assert tuple(sorted(dark_bud_obj.bud.value)) == (1.123457,)
550
763
 
551
- obs_petal = list(obs_bud.petals)
552
- assert len(obs_petal) == 1
553
- assert type(obs_petal[0].value) is tuple
554
- assert tuple(sorted(obs_petal[0].value)) == (10.0, 11.0)
764
+ assert type(obs_bud_obj.bud.value) is tuple
765
+ assert tuple(sorted(obs_bud_obj.bud.value)) == (10.0, 11.0)
555
766
 
556
767
 
557
768
  def test_dsps_bud(basic_header_objs):
558
- bud = TotalDspsRepeatsBud()
559
- assert bud.stem_name == BudName.num_dsps_repeats.value
769
+ """
770
+ Given: A set of filepaths and associated headers with DSPSREPS keywords
771
+ When: Ingesting with a TotalDspsRepeatsBud
772
+ Then: The total number of DSPS repeates is parsed correctly
773
+ """
774
+ bud_obj = TotalDspsRepeatsBud()
775
+ assert bud_obj.stem_name == BudName.num_dsps_repeats.value
560
776
  for fo in basic_header_objs:
561
777
  key = fo.name
562
- bud.update(key, fo)
778
+ bud_obj.update(key, fo)
563
779
 
564
- petal = list(bud.petals)
565
- assert len(petal) == 1
566
- assert petal[0].value == 2
780
+ assert bud_obj.bud.value == 2
567
781
 
568
782
 
569
783
  def test_dsps_flower(basic_header_objs):
@@ -592,17 +806,14 @@ def test_average_cadence_bud(basic_header_objs):
592
806
  When: Ingesting with the AverageCadenceBud
593
807
  Then: The correct values are returned
594
808
  """
595
- bud = AverageCadenceBud()
596
- assert bud.stem_name == BudName.average_cadence.value
809
+ bud_obj = AverageCadenceBud()
810
+ assert bud_obj.stem_name == BudName.average_cadence.value
597
811
  for fo in basic_header_objs:
598
812
  key = fo.name
599
- bud.update(key, fo)
600
-
601
- petal = list(bud.petals)
602
- assert len(petal) == 1
813
+ bud_obj.update(key, fo)
603
814
 
604
815
  # Because there are 3 observe frames in `basic_header_objs` spaced 1, and 2 seconds apart.
605
- assert petal[0].value == 1.5
816
+ assert bud_obj.bud.value == 1.5
606
817
 
607
818
 
608
819
  def test_max_cadence_bud(basic_header_objs):
@@ -611,17 +822,14 @@ def test_max_cadence_bud(basic_header_objs):
611
822
  When: Ingesting with the MaxCadenceBud
612
823
  Then: The correct values are returned
613
824
  """
614
- bud = MaximumCadenceBud()
615
- assert bud.stem_name == BudName.maximum_cadence.value
825
+ bud_obj = MaximumCadenceBud()
826
+ assert bud_obj.stem_name == BudName.maximum_cadence.value
616
827
  for fo in basic_header_objs:
617
828
  key = fo.name
618
- bud.update(key, fo)
619
-
620
- petal = list(bud.petals)
621
- assert len(petal) == 1
829
+ bud_obj.update(key, fo)
622
830
 
623
831
  # Because there are 3 observe frames in `basic_header_objs` spaced 1, and 2 seconds apart.
624
- assert petal[0].value == 2
832
+ assert bud_obj.bud.value == 2
625
833
 
626
834
 
627
835
  def test_minimum_cadence_bud(basic_header_objs):
@@ -630,17 +838,14 @@ def test_minimum_cadence_bud(basic_header_objs):
630
838
  When: Ingesting with the MinimumCadenceBud
631
839
  Then: The correct values are returned
632
840
  """
633
- bud = MinimumCadenceBud()
634
- assert bud.stem_name == BudName.minimum_cadence.value
841
+ bud_obj = MinimumCadenceBud()
842
+ assert bud_obj.stem_name == BudName.minimum_cadence.value
635
843
  for fo in basic_header_objs:
636
844
  key = fo.name
637
- bud.update(key, fo)
638
-
639
- petal = list(bud.petals)
640
- assert len(petal) == 1
845
+ bud_obj.update(key, fo)
641
846
 
642
847
  # Because there are 3 observe frames in `basic_header_objs` spaced 1, and 2 seconds apart.
643
- assert petal[0].value == 1
848
+ assert bud_obj.bud.value == 1
644
849
 
645
850
 
646
851
  def test_variance_cadence_bud(basic_header_objs):
@@ -649,17 +854,29 @@ def test_variance_cadence_bud(basic_header_objs):
649
854
  When: Ingesting with the VarianceCadenceBud
650
855
  Then: The correct values are returned
651
856
  """
652
- bud = VarianceCadenceBud()
653
- assert bud.stem_name == BudName.variance_cadence.value
857
+ bud_obj = VarianceCadenceBud()
858
+ assert bud_obj.stem_name == BudName.variance_cadence.value
654
859
  for fo in basic_header_objs:
655
860
  key = fo.name
656
- bud.update(key, fo)
657
-
658
- petal = list(bud.petals)
659
- assert len(petal) == 1
861
+ bud_obj.update(key, fo)
660
862
 
661
863
  # Because there are 3 observe frames in `basic_header_objs` spaced 1, and 2 seconds apart.
662
- assert petal[0].value == 0.25
864
+ assert bud_obj.bud.value == 0.25
865
+
866
+
867
+ def test_task_date_begin_bud(basic_header_objs):
868
+ """
869
+ Given: A set of filepaths and associated headers with time_obs metadata keys
870
+ When: Ingesting with the TaskDateBeginBud
871
+ Then: The correct value is returned
872
+ """
873
+ bud_obj = TaskDateBeginBud(constant_name=BudName.dark_date_begin, ip_task_types=TaskName.dark)
874
+ assert bud_obj.stem_name == BudName.dark_date_begin.value
875
+ for fo in basic_header_objs:
876
+ key = fo.name
877
+ bud_obj.update(key, fo)
878
+
879
+ assert bud_obj.bud.value == "2022-06-17T22:00:02.000000"
663
880
 
664
881
 
665
882
  def test_observe_wavelength_bud(basic_header_objs):
@@ -668,15 +885,13 @@ def test_observe_wavelength_bud(basic_header_objs):
668
885
  When: Ingesting the headers with the ObserveWavelengthBud
669
886
  Then: The petal contains the wavelength header value of the observe frames
670
887
  """
671
- bud = ObserveWavelengthBud()
672
- assert bud.stem_name == BudName.wavelength.value
888
+ bud_obj = ObserveWavelengthBud()
889
+ assert bud_obj.stem_name == BudName.wavelength.value
673
890
  for fo in basic_header_objs:
674
891
  key = fo.name
675
- bud.update(key, fo)
892
+ bud_obj.update(key, fo)
676
893
 
677
- petal = list(bud.petals)
678
- assert len(petal) == 1
679
- assert petal[0].value == 666.0
894
+ assert bud_obj.bud.value == 666.0
680
895
 
681
896
 
682
897
  def test_near_bud(basic_header_objs):
@@ -685,19 +900,17 @@ def test_near_bud(basic_header_objs):
685
900
  When: Ingesting headers with a NearBud and asking for the value
686
901
  Then: The Bud's value is the average of the header values
687
902
  """
688
- bud = NearFloatBud(
903
+ bud_obj = NearFloatBud(
689
904
  constant_name="near",
690
905
  metadata_key="near_thing",
691
906
  tolerance=0.5,
692
907
  )
693
- assert bud.stem_name == "near"
908
+ assert bud_obj.stem_name == "near"
694
909
  for fo in basic_header_objs:
695
910
  key = fo.name
696
- bud.update(key, fo)
911
+ bud_obj.update(key, fo)
697
912
 
698
- petal = list(bud.petals)
699
- assert len(petal) == 1
700
- assert petal[0].value == 1.23
913
+ assert bud_obj.bud.value == 1.23
701
914
 
702
915
 
703
916
  def test_task_near_bud(basic_header_objs):
@@ -706,17 +919,53 @@ def test_task_near_bud(basic_header_objs):
706
919
  When: Ingesting headers with a TaskNearBud and asking for the value
707
920
  Then: The bud's value is the average of the header values of that task type
708
921
  """
709
- bud = TaskNearFloatBud(
710
- constant_name="near", metadata_key="near_thing", ip_task_type="observe", tolerance=0.5
922
+ bud_obj = TaskNearFloatBud(
923
+ constant_name="near", metadata_key="near_thing", ip_task_types="observe", tolerance=0.5
711
924
  )
712
- assert bud.stem_name == "near"
925
+ assert bud_obj.stem_name == "near"
713
926
  for fo in basic_header_objs:
714
927
  key = fo.name
715
- bud.update(key, fo)
928
+ bud_obj.update(key, fo)
929
+
930
+ assert round(bud_obj.bud.value, 3) == 1.227
931
+
932
+
933
+ def test_multi_task_near_bud():
934
+ """
935
+ Given: A set of headers where multiple, but not all, task types have the same values
936
+ When: Ingesting the headers with a `TaskNearBud`
937
+ Then: When multiple tasks have the same value the correct value is returned. When a task has a different value, an
938
+ Error is raised.
939
+ """
940
+ header_dicts = [
941
+ {"DKIST004": "observe", "near": 3.2},
942
+ {"DKIST004": "dark", "near": 3.11},
943
+ {"DKIST004": "solar", "near": 1e3},
944
+ ]
945
+ header_objs = [FitsReader.from_header(h, f"{i}") for i, h in enumerate(header_dicts)]
946
+
947
+ good_bud_obj = TaskNearFloatBud(
948
+ constant_name="near",
949
+ metadata_key="near_thing",
950
+ ip_task_types=["observe", "dark"],
951
+ tolerance=0.1,
952
+ )
953
+ for fo in header_objs:
954
+ good_bud_obj.update(fo.name, fo)
955
+
956
+ assert round(good_bud_obj.bud.value, 0) == 3.0
957
+
958
+ bad_bud_obj = TaskNearFloatBud(
959
+ constant_name="near",
960
+ metadata_key="near_thing",
961
+ ip_task_types=["observe", "solar"],
962
+ tolerance=0.1,
963
+ )
964
+ for fo in header_objs:
965
+ bad_bud_obj.update(fo.name, fo)
716
966
 
717
- petal = list(bud.petals)
718
- assert len(petal) == 1
719
- assert round(petal[0].value, 3) == 1.227
967
+ with pytest.raises(ValueError, match="near values are not close enough"):
968
+ _ = bad_bud_obj.bud
720
969
 
721
970
 
722
971
  def test_near_bud_not_near_inputs(bad_header_objs):
@@ -725,18 +974,112 @@ def test_near_bud_not_near_inputs(bad_header_objs):
725
974
  When: Ingesting headers with a NearBud and asking for the value
726
975
  Then: An error is raised
727
976
  """
728
- bud = NearFloatBud(
977
+ bud_obj = NearFloatBud(
729
978
  constant_name="near",
730
979
  metadata_key="near_thing",
731
980
  tolerance=0.5,
732
981
  )
733
- assert bud.stem_name == "near"
982
+ assert bud_obj.stem_name == "near"
734
983
  for fo in bad_header_objs:
735
984
  key = fo.name
736
- bud.update(key, fo)
985
+ bud_obj.update(key, fo)
737
986
 
738
987
  with pytest.raises(ValueError):
739
- assert next(bud.petals)
988
+ _ = bud_obj.bud
989
+
990
+
991
+ def test_retarder_name_bud(basic_header_objs, task_with_polcal_header_objs, retarder_name):
992
+ """
993
+ Given: A set of headers with two values for LVL1STAT: "clear" and another name
994
+ When: Ingesting the headers with RetarderNameBud and asking for the value
995
+ Then: The retarder name is returned
996
+ """
997
+ bud_obj = RetarderNameBud()
998
+ input_objects = chain(basic_header_objs, task_with_polcal_header_objs)
999
+ for fo in input_objects:
1000
+ key = fo.name
1001
+ bud_obj.update(key, fo)
1002
+
1003
+ assert bud_obj.bud.value == retarder_name
1004
+
1005
+
1006
+ def test_retarder_name_bud_error(bad_polcal_header_objs):
1007
+ """
1008
+ Given: A set of headers with "clear" and two other values for LVL1STAT
1009
+ When: Ingesting the headers with RetarderNameBud and asking for the value
1010
+ Then: An error is raised
1011
+ """
1012
+ bud_obj = RetarderNameBud()
1013
+ for fo in bad_polcal_header_objs:
1014
+ key = fo.name
1015
+ bud_obj.update(key, fo)
1016
+
1017
+ # Crazy regex to handle non-deterministic order of sets.
1018
+ # https://regex101.com/r/zh9iG6/1
1019
+ with pytest.raises(
1020
+ ValueError,
1021
+ match=r"Multiple RETARDER_NAME values found! Values: {'RET(1)?(?(1)|2)', 'RET(?(1)2|1)'}",
1022
+ ):
1023
+ _ = bud_obj.bud
1024
+
1025
+
1026
+ def test_task_average_bud(basic_header_objs):
1027
+ """
1028
+ Given: A set of headers with a differently valued header key
1029
+ When: Ingesting headers with an TaskAverageBud and asking for the value
1030
+ Then: The bud's value is the average of the header values of that task type
1031
+ """
1032
+ bud_obj = TaskAverageBud(
1033
+ constant_name="average", metadata_key="near_thing", ip_task_types="observe"
1034
+ )
1035
+ assert bud_obj.stem_name == "average"
1036
+ for fo in basic_header_objs:
1037
+ key = fo.name
1038
+ bud_obj.update(key, fo)
1039
+
1040
+ assert round(bud_obj.bud.value, 3) == 1.227
1041
+
1042
+
1043
+ def test_time_lookup_bud(basic_header_objs):
1044
+ """
1045
+ Given: A set of headers with two differently valued header keys
1046
+ When: Ingesting headers with a TimeLookupBud and asking for the value
1047
+ Then: The bud's value is a dictionary of one key to sets of the other key as nested tuples
1048
+ """
1049
+ bud = TimeLookupBud(
1050
+ constant_name="lookup",
1051
+ key_metadata_key=FitsReaderMetadataKey.fpa_exposure_time_ms,
1052
+ value_metadata_key=FitsReaderMetadataKey.num_raw_frames_per_fpa,
1053
+ )
1054
+ assert bud.stem_name == "lookup"
1055
+ for fo in basic_header_objs:
1056
+ key = fo.name
1057
+ bud.update(key, fo)
1058
+
1059
+ assert type(bud.mapping) == collections.defaultdict
1060
+ assert bud.mapping == {0.0013: {3}, 12.345: {1}, 100.0: {4, 5}}
1061
+ assert bud.bud.value == {0.0013: [3], 12.345: [1], 100.0: [4, 5]}
1062
+
1063
+
1064
+ def test_task_time_lookup_bud(basic_header_objs):
1065
+ """
1066
+ Given: A set of headers with two differently valued header keys
1067
+ When: Ingesting headers with a TaskTimeLookupBud and asking for the value
1068
+ Then: The bud's value is a dictionary of one key to sets of the other key as nested tuples
1069
+ """
1070
+ bud = TaskTimeLookupBud(
1071
+ constant_name="task_lookup",
1072
+ key_metadata_key=FitsReaderMetadataKey.fpa_exposure_time_ms,
1073
+ value_metadata_key=FitsReaderMetadataKey.num_raw_frames_per_fpa,
1074
+ ip_task_types="dark",
1075
+ )
1076
+ assert bud.stem_name == "task_lookup"
1077
+ for fo in basic_header_objs:
1078
+ key = fo.name
1079
+ bud.update(key, fo)
1080
+
1081
+ assert bud.mapping == {12.345: {1}}
1082
+ assert bud.bud.value == {12.345: [1]}
740
1083
 
741
1084
 
742
- # TODO: test new stems that have been added to parse_l0_input_data
1085
+ # TODO: test new stem types that have been added to parse_l0_input_data