dkist-processing-common 11.6.0rc1__py3-none-any.whl → 11.7.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 (29) hide show
  1. changelog/267.feature.1.rst +1 -0
  2. changelog/267.feature.2.rst +1 -0
  3. changelog/267.feature.rst +1 -0
  4. changelog/267.misc.rst +1 -0
  5. changelog/267.removal.1.rst +2 -0
  6. changelog/267.removal.rst +1 -0
  7. dkist_processing_common/models/constants.py +394 -16
  8. dkist_processing_common/models/fits_access.py +16 -25
  9. dkist_processing_common/models/telemetry.py +9 -2
  10. dkist_processing_common/parsers/average_bud.py +48 -0
  11. dkist_processing_common/parsers/experiment_id_bud.py +8 -4
  12. dkist_processing_common/parsers/id_bud.py +35 -17
  13. dkist_processing_common/parsers/l0_fits_access.py +3 -3
  14. dkist_processing_common/parsers/l1_fits_access.py +47 -21
  15. dkist_processing_common/parsers/near_bud.py +4 -4
  16. dkist_processing_common/parsers/proposal_id_bud.py +11 -5
  17. dkist_processing_common/parsers/single_value_single_key_flower.py +0 -1
  18. dkist_processing_common/parsers/time.py +141 -27
  19. dkist_processing_common/tasks/base.py +21 -3
  20. dkist_processing_common/tasks/parse_l0_input_data.py +289 -36
  21. dkist_processing_common/tests/test_fits_access.py +19 -44
  22. dkist_processing_common/tests/test_parse_l0_input_data.py +39 -5
  23. dkist_processing_common/tests/test_stems.py +127 -10
  24. dkist_processing_common/tests/test_task_parsing.py +6 -6
  25. {dkist_processing_common-11.6.0rc1.dist-info → dkist_processing_common-11.7.0rc1.dist-info}/METADATA +3 -3
  26. {dkist_processing_common-11.6.0rc1.dist-info → dkist_processing_common-11.7.0rc1.dist-info}/RECORD +28 -22
  27. changelog/268.misc.rst +0 -1
  28. {dkist_processing_common-11.6.0rc1.dist-info → dkist_processing_common-11.7.0rc1.dist-info}/WHEEL +0 -0
  29. {dkist_processing_common-11.6.0rc1.dist-info → dkist_processing_common-11.7.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,36 +1,27 @@
1
1
  """Base classes for ID bud parsing."""
2
2
 
3
3
  from enum import StrEnum
4
+ from typing import Callable
4
5
  from typing import Type
5
6
 
6
7
  from dkist_processing_common.models.flower_pot import SpilledDirt
7
8
  from dkist_processing_common.models.flower_pot import Stem
8
9
  from dkist_processing_common.models.task_name import TaskName
9
10
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
11
+ from dkist_processing_common.parsers.task import passthrough_header_ip_task
10
12
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
11
13
 
12
14
 
13
- class IdBud(TaskUniqueBud):
14
- """Base class for ID buds."""
15
-
16
- def __init__(self, constant_name: str, metadata_key: str | StrEnum):
17
- super().__init__(
18
- constant_name=constant_name,
19
- metadata_key=metadata_key,
20
- ip_task_types=TaskName.observe,
21
- )
22
-
23
-
24
15
  class ContributingIdsBud(Stem):
25
16
  """Base class for contributing ID buds."""
26
17
 
27
- def __init__(self, stem_name: str, metadata_key: str | StrEnum):
28
- super().__init__(stem_name=stem_name)
18
+ def __init__(self, constant_name: str, metadata_key: str | StrEnum):
19
+ super().__init__(stem_name=constant_name)
29
20
  if isinstance(metadata_key, StrEnum):
30
21
  metadata_key = metadata_key.name
31
22
  self.metadata_key = metadata_key
32
23
 
33
- def setter(self, fits_obj: L0FitsAccess) -> str | Type[SpilledDirt]:
24
+ def setter(self, fits_obj: L0FitsAccess) -> str:
34
25
  """
35
26
  Set the id for any type of frame.
36
27
 
@@ -44,9 +35,9 @@ class ContributingIdsBud(Stem):
44
35
  """
45
36
  return getattr(fits_obj, self.metadata_key)
46
37
 
47
- def getter(self, key) -> tuple:
38
+ def getter(self, key) -> tuple[str, ...]:
48
39
  """
49
- Get all ids seen in non observe frames.
40
+ Get all ids seen for any type of frame.
50
41
 
51
42
  Parameters
52
43
  ----------
@@ -55,6 +46,33 @@ class ContributingIdsBud(Stem):
55
46
 
56
47
  Returns
57
48
  -------
58
- IDs from non observe frames
49
+ IDs from all types of frames
59
50
  """
60
51
  return tuple(set(self.key_to_petal_dict.values()))
52
+
53
+
54
+ class TaskContributingIdsBud(ContributingIdsBud):
55
+ """Base class for contributing ID buds for a particular task type."""
56
+
57
+ def __init__(
58
+ self,
59
+ constant_name: str,
60
+ metadata_key: str | StrEnum,
61
+ ip_task_types: str | list[str],
62
+ task_type_parsing_function: Callable = passthrough_header_ip_task,
63
+ ):
64
+ super().__init__(constant_name=constant_name, metadata_key=metadata_key)
65
+
66
+ if isinstance(ip_task_types, str):
67
+ ip_task_types = [ip_task_types]
68
+ self.ip_task_types = [task.casefold() for task in ip_task_types]
69
+ self.parsing_function = task_type_parsing_function
70
+
71
+ def setter(self, fits_obj: L0FitsAccess) -> str | Type[SpilledDirt]:
72
+ """Ingest an object only if its parsed IP task type matches what's desired."""
73
+ task = self.parsing_function(fits_obj)
74
+
75
+ if task.casefold() in self.ip_task_types:
76
+ return super().setter(fits_obj)
77
+
78
+ return SpilledDirt
@@ -27,6 +27,6 @@ class L0FitsAccess(L1FitsAccess):
27
27
  auto_squeeze: bool = True,
28
28
  ):
29
29
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
30
- self._set_metadata_key_value(MetadataKey.ip_task_type)
31
- self._set_metadata_key_value(MetadataKey.ip_start_time)
32
- self._set_metadata_key_value(MetadataKey.ip_end_time)
30
+ self.ip_task_type: str = self.header[MetadataKey.ip_task_type]
31
+ self.ip_start_time: str = self.header[MetadataKey.ip_start_time]
32
+ self.ip_end_time: str = self.header[MetadataKey.ip_end_time]
@@ -2,9 +2,12 @@
2
2
 
3
3
  from astropy.io import fits
4
4
 
5
+ from dkist_processing_common.models.fits_access import HEADER_KEY_NOT_FOUND
5
6
  from dkist_processing_common.models.fits_access import FitsAccessBase
6
7
  from dkist_processing_common.models.fits_access import MetadataKey
7
8
 
9
+ NOT_A_FLOAT = -999
10
+
8
11
 
9
12
  class L1FitsAccess(FitsAccessBase):
10
13
  """
@@ -28,25 +31,48 @@ class L1FitsAccess(FitsAccessBase):
28
31
  ):
29
32
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
30
33
 
31
- self._set_metadata_key_value(MetadataKey.elevation)
32
- self._set_metadata_key_value(MetadataKey.azimuth)
33
- self._set_metadata_key_value(MetadataKey.table_angle)
34
- self._set_metadata_key_value(MetadataKey.gos_level3_status)
35
- self._set_metadata_key_value(MetadataKey.gos_level3_lamp_status)
36
- self._set_metadata_key_value(MetadataKey.gos_polarizer_status)
37
- self._set_metadata_key_value(MetadataKey.gos_retarder_status)
38
- self._set_metadata_key_value(MetadataKey.gos_level0_status)
39
- self._set_metadata_key_value(MetadataKey.time_obs)
40
- self._set_metadata_key_value(MetadataKey.ip_id)
41
- self._set_metadata_key_value(MetadataKey.instrument)
42
- self._set_metadata_key_value(MetadataKey.wavelength)
43
- self._set_metadata_key_value(MetadataKey.proposal_id)
44
- self._set_metadata_key_value(MetadataKey.experiment_id)
45
- self._set_metadata_key_value(MetadataKey.num_dsps_repeats)
46
- self._set_metadata_key_value(MetadataKey.current_dsps_repeat)
47
- self._set_metadata_key_value(MetadataKey.fpa_exposure_time_ms)
48
- self._set_metadata_key_value(MetadataKey.sensor_readout_exposure_time_ms)
49
- self._set_metadata_key_value(MetadataKey.num_raw_frames_per_fpa)
34
+ self.elevation: float = self.header[MetadataKey.elevation]
35
+ self.azimuth: float = self.header[MetadataKey.azimuth]
36
+ self.table_angle: float = self.header[MetadataKey.table_angle]
37
+ self.gos_level3_status: str = self.header[MetadataKey.gos_level3_status]
38
+ self.gos_level3_lamp_status: str = self.header[MetadataKey.gos_level3_lamp_status]
39
+ self.gos_polarizer_status: str = self.header[MetadataKey.gos_polarizer_status]
40
+ self.gos_retarder_status: str = self.header[MetadataKey.gos_retarder_status]
41
+ self.gos_level0_status: str = self.header[MetadataKey.gos_level0_status]
42
+ self.time_obs: str = self.header[MetadataKey.time_obs]
43
+ self.ip_id: str = self.header[MetadataKey.ip_id]
44
+ self.instrument: str = self.header[MetadataKey.instrument]
45
+ self.wavelength: float = self.header[MetadataKey.wavelength]
46
+ self.proposal_id: str = self.header[MetadataKey.proposal_id]
47
+ self.experiment_id: str = self.header[MetadataKey.experiment_id]
48
+ self.num_dsps_repeats: int = self.header[MetadataKey.num_dsps_repeats]
49
+ self.current_dsps_repeat: int = self.header[MetadataKey.current_dsps_repeat]
50
+ self.fpa_exposure_time_ms: float = self.header[MetadataKey.fpa_exposure_time_ms]
51
+ self.sensor_readout_exposure_time_ms: float = self.header[
52
+ MetadataKey.sensor_readout_exposure_time_ms
53
+ ]
54
+ self.num_raw_frames_per_fpa: int = self.header[MetadataKey.num_raw_frames_per_fpa]
55
+ self.camera_id: str = self.header[MetadataKey.camera_id]
56
+ self.camera_name: str = self.header[MetadataKey.camera_name]
57
+ self.camera_bit_depth: int = self.header[MetadataKey.camera_bit_depth]
58
+ self.hardware_binning_x: int = self.header[MetadataKey.hardware_binning_x]
59
+ self.hardware_binning_y: int = self.header[MetadataKey.hardware_binning_y]
60
+ self.software_binning_x: int = self.header[MetadataKey.software_binning_x]
61
+ self.software_binning_y: int = self.header[MetadataKey.software_binning_y]
62
+ self.observing_program_execution_id: str = self.header[
63
+ MetadataKey.observing_program_execution_id
64
+ ]
65
+ self.telescope_tracking_mode: str = self.header.get(
66
+ MetadataKey.telescope_tracking_mode, HEADER_KEY_NOT_FOUND
67
+ )
68
+ self.coude_table_tracking_mode: str = self.header.get(
69
+ MetadataKey.coude_table_tracking_mode, HEADER_KEY_NOT_FOUND
70
+ )
71
+ self.telescope_scanning_mode: str = self.header.get(
72
+ MetadataKey.telescope_scanning_mode, HEADER_KEY_NOT_FOUND
73
+ )
74
+ self.light_level: float = self.header[MetadataKey.light_level]
75
+ self.hls_version: str = self.header[MetadataKey.hls_version]
50
76
 
51
77
  @property
52
78
  def gos_polarizer_angle(self) -> float:
@@ -54,7 +80,7 @@ class L1FitsAccess(FitsAccessBase):
54
80
  try:
55
81
  return float(self.header[MetadataKey.gos_polarizer_angle])
56
82
  except ValueError:
57
- return -999 # The angle is only used if the polarizer is in the beam
83
+ return NOT_A_FLOAT # The angle is only used if the polarizer is in the beam
58
84
 
59
85
  @property
60
86
  def gos_retarder_angle(self) -> float:
@@ -62,4 +88,4 @@ class L1FitsAccess(FitsAccessBase):
62
88
  try:
63
89
  return float(self.header[MetadataKey.gos_retarder_angle])
64
90
  except ValueError:
65
- return -999 # The angle is only used if the retarder is in the beam
91
+ return NOT_A_FLOAT # The angle is only used if the retarder is in the beam
@@ -66,14 +66,14 @@ class NearFloatBud(Stem):
66
66
  -------
67
67
  The mean value associated with this input key
68
68
  """
69
- value_set = list(self.key_to_petal_dict.values())
70
- biggest_value = max(value_set)
71
- smallest_value = min(value_set)
69
+ value_list = list(self.key_to_petal_dict.values())
70
+ biggest_value = max(value_list)
71
+ smallest_value = min(value_list)
72
72
  if biggest_value - smallest_value > self.tolerance:
73
73
  raise ValueError(
74
74
  f"{self.stem_name} values are not close enough. Max: {biggest_value}, Min: {smallest_value}, Tolerance: {self.tolerance}"
75
75
  )
76
- return mean(value_set)
76
+ return mean(value_list)
77
77
 
78
78
 
79
79
  class TaskNearFloatBud(NearFloatBud):
@@ -2,21 +2,27 @@
2
2
 
3
3
  from dkist_processing_common.models.constants import BudName
4
4
  from dkist_processing_common.models.fits_access import MetadataKey
5
+ from dkist_processing_common.models.task_name import TaskName
5
6
  from dkist_processing_common.parsers.id_bud import ContributingIdsBud
6
- from dkist_processing_common.parsers.id_bud import IdBud
7
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
7
8
 
8
9
 
9
- class ProposalIdBud(IdBud):
10
+ class ProposalIdBud(TaskUniqueBud):
10
11
  """Class to create a Bud for the proposal_id."""
11
12
 
12
13
  def __init__(self):
13
- super().__init__(constant_name=BudName.proposal_id, metadata_key=MetadataKey.proposal_id)
14
+ super().__init__(
15
+ constant_name=BudName.proposal_id,
16
+ metadata_key=MetadataKey.proposal_id,
17
+ ip_task_types=TaskName.observe,
18
+ )
14
19
 
15
20
 
16
21
  class ContributingProposalIdsBud(ContributingIdsBud):
17
- """Class to create a Bud for the proposal_ids."""
22
+ """Class to create a Bud for the supporting proposal_ids."""
18
23
 
19
24
  def __init__(self):
20
25
  super().__init__(
21
- stem_name=BudName.contributing_proposal_ids, metadata_key=MetadataKey.proposal_id
26
+ constant_name=BudName.contributing_proposal_ids,
27
+ metadata_key=MetadataKey.proposal_id,
22
28
  )
@@ -2,7 +2,6 @@
2
2
 
3
3
  from enum import StrEnum
4
4
 
5
- from dkist_processing_common.models.fits_access import MetadataKey
6
5
  from dkist_processing_common.models.flower_pot import Stem
7
6
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
8
7
 
@@ -22,7 +22,6 @@ from dkist_processing_common.parsers.single_value_single_key_flower import (
22
22
  )
23
23
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
24
24
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
25
- from dkist_processing_common.parsers.unique_bud import UniqueBud
26
25
 
27
26
 
28
27
  class ObsIpStartTimeBud(TaskUniqueBud):
@@ -36,15 +35,52 @@ class ObsIpStartTimeBud(TaskUniqueBud):
36
35
  )
37
36
 
38
37
 
39
- class CadenceBudBase(UniqueBud):
40
- """Base class for all Cadence Buds."""
38
+ class TaskDatetimeBudBase(Stem):
39
+ """
40
+ Base class for making datetime-related buds.
41
41
 
42
- def __init__(self, constant_name: str):
43
- super().__init__(constant_name, metadata_key=MetadataKey.time_obs)
42
+ Returns a tuple of sorted values converted from datetimes to unix seconds.
43
+
44
+ Complicated parsing of the header into a task type can be achieved by passing in a different
45
+ header task parsing function.
46
+
47
+ Parameters
48
+ ----------
49
+ stem_name
50
+ The name for the constant to be defined
51
+
52
+ metadata_key
53
+ The metadata key associated with the constant
54
+
55
+ ip_task_types
56
+ Only consider objects whose parsed header IP task type matches a string in this list
57
+
58
+ header_type_parsing_func
59
+ The function used to convert a header into an IP task type
60
+ """
61
+
62
+ key_to_petal_dict: dict[str, float]
63
+
64
+ def __init__(
65
+ self,
66
+ stem_name: str,
67
+ metadata_key: str | StrEnum,
68
+ ip_task_types: str | list[str],
69
+ header_task_parsing_func: Callable = passthrough_header_ip_task,
70
+ ):
71
+ super().__init__(stem_name=stem_name)
72
+
73
+ if isinstance(metadata_key, StrEnum):
74
+ metadata_key = metadata_key.name
75
+ self.metadata_key = metadata_key
76
+ if isinstance(ip_task_types, str):
77
+ ip_task_types = [ip_task_types]
78
+ self.ip_task_types = [task.casefold() for task in ip_task_types]
79
+ self.header_parsing_function = header_task_parsing_func
44
80
 
45
81
  def setter(self, fits_obj: L0FitsAccess) -> float | Type[SpilledDirt]:
46
82
  """
47
- If the file is an observe file, its DATE-OBS value is stored as unix seconds.
83
+ Store the metadata key datetime value as unix seconds if the task type is in the desired types.
48
84
 
49
85
  Parameters
50
86
  ----------
@@ -52,16 +88,45 @@ class CadenceBudBase(UniqueBud):
52
88
  The input fits object
53
89
  Returns
54
90
  -------
55
- The observe time in seconds
91
+ The datetime in seconds
56
92
  """
57
- if fits_obj.ip_task_type.casefold() == TaskName.observe.value.casefold():
93
+ task = self.header_parsing_function(fits_obj)
94
+
95
+ if task.casefold() in self.ip_task_types:
58
96
  return (
59
97
  datetime.fromisoformat(getattr(fits_obj, self.metadata_key))
60
98
  .replace(tzinfo=timezone.utc)
61
99
  .timestamp()
62
100
  )
101
+
63
102
  return SpilledDirt
64
103
 
104
+ def getter(self, key: Hashable) -> tuple[float, ...]:
105
+ """
106
+ Return a tuple of sorted times in unix seconds.
107
+
108
+ Parameters
109
+ ----------
110
+ key
111
+ The input key
112
+
113
+ Returns
114
+ -------
115
+ A tuple that is sorted times in unix seconds
116
+ """
117
+ return tuple(sorted(list(self.key_to_petal_dict.values())))
118
+
119
+
120
+ class CadenceBudBase(TaskDatetimeBudBase):
121
+ """Base class for all Cadence Buds."""
122
+
123
+ def __init__(self, constant_name: str):
124
+ super().__init__(
125
+ stem_name=constant_name,
126
+ metadata_key=MetadataKey.time_obs,
127
+ ip_task_types=TaskName.observe,
128
+ )
129
+
65
130
 
66
131
  class AverageCadenceBud(CadenceBudBase):
67
132
  """Class for the average cadence Bud."""
@@ -82,7 +147,7 @@ class AverageCadenceBud(CadenceBudBase):
82
147
  -------
83
148
  The mean value of the cadences of the input frames
84
149
  """
85
- return np.mean(np.diff(sorted(list(self.key_to_petal_dict.values()))))
150
+ return np.mean(np.diff(super().getter(key)))
86
151
 
87
152
 
88
153
  class MaximumCadenceBud(CadenceBudBase):
@@ -104,7 +169,7 @@ class MaximumCadenceBud(CadenceBudBase):
104
169
  -------
105
170
  The maximum cadence between frames
106
171
  """
107
- return np.max(np.diff(sorted(list(self.key_to_petal_dict.values()))))
172
+ return np.max(np.diff(super().getter(key)))
108
173
 
109
174
 
110
175
  class MinimumCadenceBud(CadenceBudBase):
@@ -126,7 +191,7 @@ class MinimumCadenceBud(CadenceBudBase):
126
191
  -------
127
192
  The minimum cadence between frames
128
193
  """
129
- return np.min(np.diff(sorted(list(self.key_to_petal_dict.values()))))
194
+ return np.min(np.diff(super().getter(key)))
130
195
 
131
196
 
132
197
  class VarianceCadenceBud(CadenceBudBase):
@@ -147,11 +212,38 @@ class VarianceCadenceBud(CadenceBudBase):
147
212
  -------
148
213
  Return the variance of the cadences over the input frames
149
214
  """
150
- return np.var(np.diff(sorted(list(self.key_to_petal_dict.values()))))
215
+ return np.var(np.diff(super().getter(key)))
216
+
151
217
 
218
+ class TaskDateBeginBud(TaskDatetimeBudBase):
219
+ """Class for the date begin task Bud."""
152
220
 
153
- class TimeFlowerBase(SingleValueSingleKeyFlower):
154
- """Base task for SingleValueSingleKeyFlowers that need to round their values to avoid value jitter."""
221
+ def __init__(self, constant_name: str, ip_task_types: str | list[str]):
222
+ super().__init__(
223
+ stem_name=constant_name,
224
+ metadata_key=MetadataKey.time_obs,
225
+ ip_task_types=ip_task_types,
226
+ )
227
+
228
+ def getter(self, key) -> str:
229
+ """
230
+ Return the earliest date begin for the ip task type converted from unix seconds to datetime string.
231
+
232
+ Parameters
233
+ ----------
234
+ key
235
+ The input key
236
+ Returns
237
+ -------
238
+ Return the minimum date begin as a datetime string
239
+ """
240
+ min_time = super().getter(key)[0]
241
+ min_time_dt = datetime.fromtimestamp(min_time, tz=timezone.utc)
242
+ return min_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%f")
243
+
244
+
245
+ class RoundTimeFlowerBase(SingleValueSingleKeyFlower):
246
+ """Base flower for SingleValueSingleKeyFlowers that need to round their values to avoid value jitter."""
155
247
 
156
248
  def setter(self, fits_obj: L0FitsAccess):
157
249
  """
@@ -169,7 +261,7 @@ class TimeFlowerBase(SingleValueSingleKeyFlower):
169
261
  return round(raw_value, EXP_TIME_ROUND_DIGITS)
170
262
 
171
263
 
172
- class ExposureTimeFlower(TimeFlowerBase):
264
+ class ExposureTimeFlower(RoundTimeFlowerBase):
173
265
  """For tagging the frame FPA exposure time."""
174
266
 
175
267
  def __init__(self):
@@ -178,7 +270,7 @@ class ExposureTimeFlower(TimeFlowerBase):
178
270
  )
179
271
 
180
272
 
181
- class ReadoutExpTimeFlower(TimeFlowerBase):
273
+ class ReadoutExpTimeFlower(RoundTimeFlowerBase):
182
274
  """For tagging the exposure time of each readout that contributes to an FPA."""
183
275
 
184
276
  def __init__(self):
@@ -188,18 +280,18 @@ class ReadoutExpTimeFlower(TimeFlowerBase):
188
280
  )
189
281
 
190
282
 
191
- class TaskTimeBudBase(Stem):
283
+ class TaskRoundTimeBudBase(Stem):
192
284
  """
193
- Base class for making time-related buds that are computed for specific task types.
285
+ Base class for making buds that need a set of rounded times for computing for specific task types.
194
286
 
195
- By "time-related" we mean values that generally need rounding when ingested into the database.
287
+ Metadata key values are already floats. Returns tuple of sorted unique rounded values.
196
288
 
197
289
  Complicated parsing of the header into a task type can be achieved by passing in a different
198
290
  header task parsing function.
199
291
 
200
292
  Parameters
201
293
  ----------
202
- constant_name
294
+ stem_name
203
295
  The name for the constant to be defined
204
296
 
205
297
  metadata_key
@@ -212,6 +304,8 @@ class TaskTimeBudBase(Stem):
212
304
  The function used to convert a header into an IP task type
213
305
  """
214
306
 
307
+ key_to_petal_dict: dict[str, float]
308
+
215
309
  def __init__(
216
310
  self,
217
311
  stem_name: str,
@@ -229,8 +323,18 @@ class TaskTimeBudBase(Stem):
229
323
  self.ip_task_types = [task.casefold() for task in ip_task_types]
230
324
  self.header_parsing_function = header_task_parsing_func
231
325
 
232
- def setter(self, fits_obj: L0FitsAccess):
233
- """Return the desired metadata key only if the parsed task type matches the Bud's task type."""
326
+ def setter(self, fits_obj: L0FitsAccess) -> float | Type[SpilledDirt]:
327
+ """
328
+ Store the metadata key value if the parsed task type is in the desired types.
329
+
330
+ Parameters
331
+ ----------
332
+ fits_obj
333
+ The input fits object
334
+ Returns
335
+ -------
336
+ The rounded time
337
+ """
234
338
  task = self.header_parsing_function(fits_obj)
235
339
 
236
340
  if task.casefold() in self.ip_task_types:
@@ -240,12 +344,22 @@ class TaskTimeBudBase(Stem):
240
344
  return SpilledDirt
241
345
 
242
346
  def getter(self, key: Hashable) -> tuple[float, ...]:
243
- """Return a tuple of all the unique values found."""
244
- value_tuple = tuple(sorted(set(self.key_to_petal_dict.values())))
245
- return value_tuple
347
+ """
348
+ Return a tuple of the sorted unique values found.
349
+
350
+ Parameters
351
+ ----------
352
+ key
353
+ The input key
354
+
355
+ Returns
356
+ -------
357
+ A tuple that is the sorted set of unique times
358
+ """
359
+ return tuple(sorted(set(self.key_to_petal_dict.values())))
246
360
 
247
361
 
248
- class TaskExposureTimesBud(TaskTimeBudBase):
362
+ class TaskExposureTimesBud(TaskRoundTimeBudBase):
249
363
  """Produce a tuple of all FPA exposure times present in the dataset for a specific ip task type."""
250
364
 
251
365
  def __init__(
@@ -262,7 +376,7 @@ class TaskExposureTimesBud(TaskTimeBudBase):
262
376
  )
263
377
 
264
378
 
265
- class TaskReadoutExpTimesBud(TaskTimeBudBase):
379
+ class TaskReadoutExpTimesBud(TaskRoundTimeBudBase):
266
380
  """Produce a tuple of all sensor readout exposure times present in the dataset for a specific task type."""
267
381
 
268
382
  def __init__(
@@ -10,14 +10,12 @@ from typing import Any
10
10
  from typing import Generator
11
11
  from typing import Iterable
12
12
  from typing import Type
13
- from typing import TypeAlias
14
13
 
15
14
  from dkist_processing_core import TaskBase
16
15
  from opentelemetry.metrics import CallbackOptions
17
16
  from opentelemetry.metrics import Counter
18
17
  from opentelemetry.metrics import ObservableGauge
19
18
  from opentelemetry.metrics import Observation
20
- from pydantic import BaseModel
21
19
 
22
20
  from dkist_processing_common._util.scratch import WorkflowFileSystem
23
21
  from dkist_processing_common._util.tags import TagDB
@@ -27,13 +25,14 @@ from dkist_processing_common.config import common_configurations
27
25
  from dkist_processing_common.models.constants import ConstantsBase
28
26
  from dkist_processing_common.models.tags import StemName
29
27
  from dkist_processing_common.models.tags import Tag
28
+ from dkist_processing_common.models.telemetry import ObservableProgress
30
29
  from dkist_processing_common.tasks.mixin.metadata_store import MetadataStoreMixin
31
30
 
32
31
  __all__ = ["WorkflowTaskBase", "tag_type_hint"]
33
32
 
34
33
  logger = logging.getLogger(__name__)
35
34
 
36
- tag_type_hint: TypeAlias = Iterable[str] | str
35
+ tag_type_hint = Iterable[str] | str
37
36
 
38
37
 
39
38
  class WorkflowTaskBase(TaskBase, MetadataStoreMixin, ABC):
@@ -92,6 +91,20 @@ class WorkflowTaskBase(TaskBase, MetadataStoreMixin, ABC):
92
91
  unit="1",
93
92
  description="The number of writes executed in the processing stack.",
94
93
  )
94
+ self.outer_loop_progress = ObservableProgress()
95
+ self.outer_loop_progress_gauge: ObservableGauge = self.meter.create_observable_gauge(
96
+ name=self.format_metric_name("tasks.outer.loop.progress"),
97
+ description="The progress of a task through the main processing loop.",
98
+ callbacks=[lambda options: self.outer_loop_run_progress(options)],
99
+ )
100
+
101
+ def outer_loop_run_progress(
102
+ self, options: CallbackOptions
103
+ ) -> Generator[Observation, None, None]:
104
+ """Observe the progress of the current task as a percentage."""
105
+ yield Observation(
106
+ self.outer_loop_progress.percent_complete, attributes=self.base_telemetry_attributes
107
+ )
95
108
 
96
109
  @property
97
110
  def constants_model_class(self) -> Type[ConstantsBase]:
@@ -135,6 +148,11 @@ class WorkflowTaskBase(TaskBase, MetadataStoreMixin, ABC):
135
148
  with self.telemetry_span("Record Provenance"):
136
149
  self._record_provenance()
137
150
 
151
+ def post_run(self) -> None:
152
+ """Execute and post-task bookkeeping required."""
153
+ super().post_run()
154
+ self.outer_loop_progress.set_complete()
155
+
138
156
  def read(
139
157
  self, tags: tag_type_hint, decoder: callable = path_decoder, **decoder_kwargs
140
158
  ) -> Generator[Any, None, None]: