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,18 +1,17 @@
1
1
  """Mixin for a WorkflowDataTaskBase subclass which implements Metadata Store data access functionality."""
2
- import json
2
+
3
3
  import logging
4
4
  from functools import cached_property
5
+ from typing import Literal
6
+
7
+ from pydantic import validate_call
5
8
 
6
9
  from dkist_processing_common._util.graphql import GraphQLClient
7
- from dkist_processing_common.codecs.quality import QualityDataEncoder
8
10
  from dkist_processing_common.config import common_configurations
9
11
  from dkist_processing_common.models.graphql import DatasetCatalogReceiptAccountMutation
10
12
  from dkist_processing_common.models.graphql import DatasetCatalogReceiptAccountResponse
11
13
  from dkist_processing_common.models.graphql import InputDatasetPartResponse
12
14
  from dkist_processing_common.models.graphql import InputDatasetRecipeRunResponse
13
- from dkist_processing_common.models.graphql import QualitiesRequest
14
- from dkist_processing_common.models.graphql import QualityCreation
15
- from dkist_processing_common.models.graphql import QualityResponse
16
15
  from dkist_processing_common.models.graphql import RecipeRunMutation
17
16
  from dkist_processing_common.models.graphql import RecipeRunMutationResponse
18
17
  from dkist_processing_common.models.graphql import RecipeRunProvenanceMutation
@@ -23,11 +22,8 @@ from dkist_processing_common.models.graphql import RecipeRunStatusMutation
23
22
  from dkist_processing_common.models.graphql import RecipeRunStatusQuery
24
23
  from dkist_processing_common.models.graphql import RecipeRunStatusResponse
25
24
 
26
-
27
25
  logger = logging.getLogger(__name__)
28
26
 
29
- input_dataset_part_document_type_hint = list | dict | str | int | float | None
30
-
31
27
 
32
28
  class MetadataStoreMixin:
33
29
  """Mixin for a WorkflowDataTaskBase which implements Metadata Store access functionality."""
@@ -37,6 +33,8 @@ class MetadataStoreMixin:
37
33
  """Get the graphql client."""
38
34
  return GraphQLClient(common_configurations.metadata_store_api_base)
39
35
 
36
+ # RECIPE RUN STATUS
37
+
40
38
  def metadata_store_change_recipe_run_to_inprogress(self):
41
39
  """Set the recipe run status to "INPROGRESS"."""
42
40
  self._metadata_store_change_status(status="INPROGRESS", is_complete=False)
@@ -49,237 +47,6 @@ class MetadataStoreMixin:
49
47
  """Set the recipe run status to "TRIALSUCCESS"."""
50
48
  self._metadata_store_change_status(status="TRIALSUCCESS", is_complete=False)
51
49
 
52
- def metadata_store_add_dataset_receipt_account(
53
- self, dataset_id: str, expected_object_count: int
54
- ):
55
- """Set the number of expected objects."""
56
- params = DatasetCatalogReceiptAccountMutation(
57
- datasetId=dataset_id, expectedObjectCount=expected_object_count
58
- )
59
- self.metadata_store_client.execute_gql_mutation(
60
- mutation_base="createDatasetCatalogReceiptAccount",
61
- mutation_parameters=params,
62
- mutation_response_cls=DatasetCatalogReceiptAccountResponse,
63
- )
64
-
65
- def metadata_store_record_provenance(self, is_task_manual: bool, library_versions: str):
66
- """Record the provenance record in the metadata store."""
67
- params = RecipeRunProvenanceMutation(
68
- inputDatasetId=self.metadata_store_input_dataset_id,
69
- isTaskManual=is_task_manual,
70
- recipeRunId=self.recipe_run_id,
71
- taskName=self.task_name,
72
- libraryVersions=library_versions,
73
- workflowVersion=self.workflow_version,
74
- )
75
- self.metadata_store_client.execute_gql_mutation(
76
- mutation_base="createRecipeRunProvenance",
77
- mutation_parameters=params,
78
- mutation_response_cls=RecipeRunProvenanceResponse,
79
- )
80
-
81
- def metadata_store_add_quality_data(self, dataset_id: str, quality_data: list[dict]):
82
- """Add the quality data to the metadata-store."""
83
- if self.metadata_store_quality_data_exists(dataset_id):
84
- raise RuntimeError(f"Quality data already persisted for dataset {dataset_id!r}")
85
- for metric in quality_data:
86
- if (metric_code := metric.get("metric_code")) is None:
87
- name = metric.get("name")
88
- raise ValueError(f"No metric_code for {name!r} in dataset {dataset_id!r}")
89
- params = QualityCreation(
90
- datasetId=dataset_id,
91
- metricCode=metric_code,
92
- facet=metric.get("facet"),
93
- name=metric.get("name"),
94
- description=metric.get("description"),
95
- statement=metric.get("statement"),
96
- # JSON array
97
- warnings=json.dumps(metric.get("warnings")),
98
- # JSON objects
99
- plotData=json.dumps(metric.get("plot_data"), cls=QualityDataEncoder),
100
- tableData=json.dumps(metric.get("table_data"), cls=QualityDataEncoder),
101
- histogramData=json.dumps(metric.get("histogram_data"), cls=QualityDataEncoder),
102
- modmatData=json.dumps(metric.get("modmat_data"), cls=QualityDataEncoder),
103
- raincloudData=json.dumps(metric.get("raincloud_data"), cls=QualityDataEncoder),
104
- efficiencyData=json.dumps(metric.get("efficiency_data"), cls=QualityDataEncoder),
105
- )
106
- self.metadata_store_client.execute_gql_mutation(
107
- mutation_base="createQuality",
108
- mutation_parameters=params,
109
- mutation_response_cls=QualityResponse,
110
- )
111
-
112
- def metadata_store_quality_data_exists(self, dataset_id: str) -> bool:
113
- """Return True if quality data exists in the metadata-store for the given dataset id."""
114
- params = QualitiesRequest(datasetId=dataset_id)
115
- response = self.metadata_store_client.execute_gql_query(
116
- query_base="qualities",
117
- query_response_cls=QualityResponse,
118
- query_parameters=params,
119
- )
120
- return bool(response)
121
-
122
- def metadata_store_recipe_run_configuration(self) -> dict:
123
- """Get the recipe run configuration from the metadata store."""
124
- configuration_json = self._metadata_store_recipe_run().configuration
125
- if configuration_json is None:
126
- return {}
127
- try:
128
- configuration = json.loads(configuration_json)
129
- if not isinstance(configuration, dict):
130
- raise ValueError(
131
- f"Invalid recipe run configuration format. "
132
- f"Expected json encoded dictionary, received json encoded {type(configuration)}"
133
- )
134
- return configuration
135
- except (json.JSONDecodeError, ValueError, TypeError, UnicodeDecodeError) as e:
136
- logger.error(f"Invalid recipe run configuration")
137
- raise e
138
-
139
- @cached_property
140
- def metadata_store_input_dataset_parts(self) -> list[InputDatasetPartResponse]:
141
- """Get the input dataset parts from the metadata store."""
142
- params = RecipeRunQuery(recipeRunId=self.recipe_run_id)
143
- response = self.metadata_store_client.execute_gql_query(
144
- query_base="recipeRuns",
145
- query_response_cls=InputDatasetRecipeRunResponse,
146
- query_parameters=params,
147
- ) # queried independently of other recipe run metadata for performance
148
- recipe_run = response[0]
149
- return [
150
- part_link.inputDatasetPart
151
- for part_link in recipe_run.recipeInstance.inputDataset.inputDatasetInputDatasetParts
152
- ]
153
-
154
- def _metadata_store_filter_input_dataset_parts(
155
- self, input_dataset_part_type_name: str
156
- ) -> InputDatasetPartResponse | list[InputDatasetPartResponse] | None:
157
- """Filter the input dataset parts based on the input dataset part type name."""
158
- target_parts = [
159
- part
160
- for part in self.metadata_store_input_dataset_parts
161
- if part.inputDatasetPartType.inputDatasetPartTypeName == input_dataset_part_type_name
162
- ]
163
- if not target_parts:
164
- return
165
- if len(target_parts) == 1:
166
- return target_parts[0]
167
- raise ValueError(
168
- f"Multiple ({len(target_parts)}) input dataset parts found for "
169
- f"{input_dataset_part_type_name=}."
170
- )
171
-
172
- @property
173
- def _metadata_store_input_dataset_observe_frames_part(
174
- self,
175
- ) -> InputDatasetPartResponse | None:
176
- """Get the input dataset part for observe frames."""
177
- return self._metadata_store_filter_input_dataset_parts(
178
- input_dataset_part_type_name="observe_frames",
179
- )
180
-
181
- @property
182
- def metadata_store_input_dataset_observe_frames_part_id(self) -> int | None:
183
- """Get the input dataset part id for observe frames."""
184
- if part := self._metadata_store_input_dataset_observe_frames_part:
185
- return part.inputDatasetPartId
186
-
187
- @property
188
- def metadata_store_input_dataset_observe_frames_part_document(
189
- self,
190
- ) -> input_dataset_part_document_type_hint:
191
- """Get the input dataset part document for observe frames."""
192
- if part := self._metadata_store_input_dataset_observe_frames_part:
193
- return part.inputDatasetPartDocument
194
-
195
- @property
196
- def _metadata_store_input_dataset_calibration_frames_part(
197
- self,
198
- ) -> InputDatasetPartResponse | None:
199
- """Get the input dataset part for calibration frames."""
200
- return self._metadata_store_filter_input_dataset_parts(
201
- input_dataset_part_type_name="calibration_frames"
202
- )
203
-
204
- @property
205
- def metadata_store_input_dataset_calibration_frames_part_id(self) -> int | None:
206
- """Get the input dataset part id for calibration frames."""
207
- if part := self._metadata_store_input_dataset_calibration_frames_part:
208
- return part.inputDatasetPartId
209
-
210
- @property
211
- def metadata_store_input_dataset_calibration_frames_part_document(
212
- self,
213
- ) -> input_dataset_part_document_type_hint:
214
- """Get the input dataset part document for calibration frames."""
215
- if part := self._metadata_store_input_dataset_calibration_frames_part:
216
- return part.inputDatasetPartDocument
217
-
218
- @property
219
- def _metadata_store_input_dataset_parameters_part(
220
- self,
221
- ) -> InputDatasetPartResponse | None:
222
- """Get the input dataset part for parameters."""
223
- return self._metadata_store_filter_input_dataset_parts(
224
- input_dataset_part_type_name="parameters"
225
- )
226
-
227
- @property
228
- def metadata_store_input_dataset_parameters_part_id(self) -> int | None:
229
- """Get the input dataset part id for parameters."""
230
- if part := self._metadata_store_input_dataset_parameters_part:
231
- return part.inputDatasetPartId
232
-
233
- @property
234
- def metadata_store_input_dataset_parameters_part_document(
235
- self,
236
- ) -> input_dataset_part_document_type_hint:
237
- """Get the input dataset part document for parameters."""
238
- if part := self._metadata_store_input_dataset_parameters_part:
239
- return part.inputDatasetPartDocument
240
-
241
- @property
242
- def metadata_store_input_dataset_id(self) -> int:
243
- """Get the input dataset id from the metadata store."""
244
- return self._metadata_store_recipe_run().recipeInstance.inputDatasetId
245
-
246
- @property
247
- def metadata_store_recipe_instance_id(self) -> int:
248
- """Get the recipe instance id from the metadata store."""
249
- return self._metadata_store_recipe_run().recipeInstanceId
250
-
251
- @property
252
- def metadata_store_recipe_id(self) -> int:
253
- """Get the recipe id from the metadata store."""
254
- return self._metadata_store_recipe_run().recipeInstance.recipeId
255
-
256
- @property
257
- def metadata_store_recipe_run_provenance(self) -> list[RecipeRunProvenanceResponse]:
258
- """Get all the provenance records for the recipe run."""
259
- return self._metadata_store_recipe_run().recipeRunProvenances
260
-
261
- def _metadata_store_recipe_run(self, allow_cache: bool = True) -> RecipeRunResponse:
262
- is_cached = bool(getattr(self, "_recipe_run_cache", False))
263
- if is_cached and allow_cache:
264
- return self._recipe_run_cache
265
- params = RecipeRunQuery(recipeRunId=self.recipe_run_id)
266
- response = self.metadata_store_client.execute_gql_query(
267
- query_base="recipeRuns",
268
- query_response_cls=RecipeRunResponse,
269
- query_parameters=params,
270
- )
271
- self._recipe_run_cache = response[0]
272
- return self._recipe_run_cache
273
-
274
- def _metadata_store_change_status(self, status: str, is_complete: bool):
275
- """Change the recipe run status of a recipe run to the given status."""
276
- recipe_run_status_id = self._metadata_store_recipe_run_status_id(status=status)
277
- if not recipe_run_status_id:
278
- recipe_run_status_id = self._metadata_store_create_recipe_run_status(
279
- status=status, is_complete=is_complete
280
- )
281
- self._metadata_store_update_status(recipe_run_status_id=recipe_run_status_id)
282
-
283
50
  def _metadata_store_recipe_run_status_id(self, status: str) -> None | int:
284
51
  """Find the id of a recipe run status."""
285
52
  params = RecipeRunStatusQuery(recipeRunStatusName=status)
@@ -291,6 +58,7 @@ class MetadataStoreMixin:
291
58
  if len(response) > 0:
292
59
  return response[0].recipeRunStatusId
293
60
 
61
+ @validate_call
294
62
  def _metadata_store_create_recipe_run_status(self, status: str, is_complete: bool) -> int:
295
63
  """
296
64
  Add a new recipe run status to the db.
@@ -305,10 +73,6 @@ class MetadataStoreMixin:
305
73
  "marked complete.",
306
74
  }
307
75
 
308
- if not isinstance(status, str):
309
- raise TypeError(f"status must be of type str: {status}")
310
- if not isinstance(is_complete, bool):
311
- raise TypeError(f"is_complete must be of type bool: {is_complete}")
312
76
  params = RecipeRunStatusMutation(
313
77
  recipeRunStatusName=status,
314
78
  isComplete=is_complete,
@@ -321,6 +85,15 @@ class MetadataStoreMixin:
321
85
  )
322
86
  return recipe_run_status_response.recipeRunStatus.recipeRunStatusId
323
87
 
88
+ def _metadata_store_change_status(self, status: str, is_complete: bool):
89
+ """Change the recipe run status of a recipe run to the given status."""
90
+ recipe_run_status_id = self._metadata_store_recipe_run_status_id(status=status)
91
+ if not recipe_run_status_id:
92
+ recipe_run_status_id = self._metadata_store_create_recipe_run_status(
93
+ status=status, is_complete=is_complete
94
+ )
95
+ self._metadata_store_update_status(recipe_run_status_id=recipe_run_status_id)
96
+
324
97
  def _metadata_store_update_status(
325
98
  self,
326
99
  recipe_run_status_id: int,
@@ -338,3 +111,95 @@ class MetadataStoreMixin:
338
111
  mutation_parameters=params,
339
112
  mutation_response_cls=RecipeRunMutationResponse,
340
113
  )
114
+
115
+ # RECEIPT
116
+
117
+ def metadata_store_add_dataset_receipt_account(
118
+ self, dataset_id: str, expected_object_count: int
119
+ ):
120
+ """Set the number of expected objects."""
121
+ params = DatasetCatalogReceiptAccountMutation(
122
+ datasetId=dataset_id, expectedObjectCount=expected_object_count
123
+ )
124
+ self.metadata_store_client.execute_gql_mutation(
125
+ mutation_base="createDatasetCatalogReceiptAccount",
126
+ mutation_parameters=params,
127
+ mutation_response_cls=DatasetCatalogReceiptAccountResponse,
128
+ )
129
+
130
+ # PROVENANCE
131
+
132
+ def metadata_store_record_provenance(self, is_task_manual: bool, library_versions: str):
133
+ """Record the provenance record in the metadata store."""
134
+ params = RecipeRunProvenanceMutation(
135
+ inputDatasetId=self.metadata_store_recipe_run.recipeInstance.inputDatasetId,
136
+ isTaskManual=is_task_manual,
137
+ recipeRunId=self.recipe_run_id,
138
+ taskName=self.task_name,
139
+ libraryVersions=library_versions,
140
+ workflowVersion=self.workflow_version,
141
+ )
142
+ self.metadata_store_client.execute_gql_mutation(
143
+ mutation_base="createRecipeRunProvenance",
144
+ mutation_parameters=params,
145
+ mutation_response_cls=RecipeRunProvenanceResponse,
146
+ )
147
+
148
+ # INPUT DATASET RECIPE RUN
149
+
150
+ @cached_property
151
+ def metadata_store_input_dataset_recipe_run(self) -> InputDatasetRecipeRunResponse:
152
+ """Get the input dataset recipe run response from the metadata store."""
153
+ params = RecipeRunQuery(recipeRunId=self.recipe_run_id)
154
+ response = self.metadata_store_client.execute_gql_query(
155
+ query_base="recipeRuns",
156
+ query_response_cls=InputDatasetRecipeRunResponse,
157
+ query_parameters=params,
158
+ )
159
+ return response[0]
160
+
161
+ def _metadata_store_input_dataset_part(
162
+ self, part_type: Literal["observe_frames", "calibration_frames", "parameters"]
163
+ ) -> InputDatasetPartResponse:
164
+ """Get the input dataset part by input dataset part type name."""
165
+ part_types_found = set()
166
+ input_dataset_part = None
167
+ parts = (
168
+ self.metadata_store_input_dataset_recipe_run.recipeInstance.inputDataset.inputDatasetInputDatasetParts
169
+ )
170
+ for part in parts:
171
+ part_type_name = part.inputDatasetPart.inputDatasetPartType.inputDatasetPartTypeName
172
+ if part_type_name in part_types_found:
173
+ raise ValueError(f"Multiple input dataset parts found for {part_type_name=}.")
174
+ part_types_found.add(part_type_name)
175
+ if part_type_name == part_type:
176
+ input_dataset_part = part.inputDatasetPart
177
+ return input_dataset_part
178
+
179
+ @property
180
+ def metadata_store_input_dataset_observe_frames(self) -> InputDatasetPartResponse:
181
+ """Get the input dataset part for the observe frames."""
182
+ return self._metadata_store_input_dataset_part(part_type="observe_frames")
183
+
184
+ @property
185
+ def metadata_store_input_dataset_calibration_frames(self) -> InputDatasetPartResponse:
186
+ """Get the input dataset part for the calibration frames."""
187
+ return self._metadata_store_input_dataset_part(part_type="calibration_frames")
188
+
189
+ @property
190
+ def metadata_store_input_dataset_parameters(self) -> InputDatasetPartResponse:
191
+ """Get the input dataset part for the parameters."""
192
+ return self._metadata_store_input_dataset_part(part_type="parameters")
193
+
194
+ # RECIPE RUN
195
+
196
+ @cached_property
197
+ def metadata_store_recipe_run(self) -> RecipeRunResponse:
198
+ """Get the recipe run response from the metadata store."""
199
+ params = RecipeRunQuery(recipeRunId=self.recipe_run_id)
200
+ response = self.metadata_store_client.execute_gql_query(
201
+ query_base="recipeRuns",
202
+ query_response_cls=RecipeRunResponse,
203
+ query_parameters=params,
204
+ )
205
+ return response[0]
@@ -1,4 +1,5 @@
1
1
  """Mixin for a WorkflowDataTaskBase subclass which implements Object Store data access functionality."""
2
+
2
3
  from pathlib import Path
3
4
 
4
5
  from object_clerk import ObjectClerk
@@ -54,6 +55,27 @@ class ObjectStoreMixin:
54
55
  },
55
56
  )
56
57
 
58
+ def object_store_upload_quality_data(
59
+ self,
60
+ quality_data: Path | bytes,
61
+ bucket: str,
62
+ object_key: str,
63
+ content_type: str = "application/json",
64
+ ):
65
+ """Upload quality data to the object store."""
66
+ self.object_store_client.upload_object(
67
+ object_data=quality_data,
68
+ bucket=bucket,
69
+ object_key=object_key,
70
+ verify_checksum=True,
71
+ content_type=content_type,
72
+ metadata={
73
+ "groupname": "DATASET",
74
+ "groupid": self.constants.dataset_id,
75
+ "objecttype": "QDATA",
76
+ },
77
+ )
78
+
57
79
  def object_store_remove_folder_objects(self, bucket: str, path: Path | str) -> list[str]:
58
80
  """
59
81
  Remove folder objects (end with /) in the specified bucket and path.
@@ -4,4 +4,5 @@ To improve readability the top-level mixin, `QualityMixin`, contains only base f
4
4
  metrics are grouped into sub-mixins. To protect a user, this mixin-on-mixin stack is hidden in protected modules
5
5
  and only the top-level mixin (`QualityMixin`) is exposed.
6
6
  """
7
+
7
8
  from ._base import QualityMixin
@@ -1,4 +1,5 @@
1
1
  """Base QualityMixin class that contains machinery common to all metric types."""
2
+
2
3
  from typing import Iterable
3
4
 
4
5
  import numpy as np
@@ -11,10 +12,15 @@ from dkist_processing_common.tasks.mixin.quality._metrics import _PolcalQualityM
11
12
  from dkist_processing_common.tasks.mixin.quality._metrics import _SimplePlotQualityMixin
12
13
  from dkist_processing_common.tasks.mixin.quality._metrics import _SimpleQualityMixin
13
14
  from dkist_processing_common.tasks.mixin.quality._metrics import _TableQualityMixin
15
+ from dkist_processing_common.tasks.mixin.quality._metrics import _WavecalQualityMixin
14
16
 
15
17
 
16
18
  class QualityMixin(
17
- _SimpleQualityMixin, _SimplePlotQualityMixin, _TableQualityMixin, _PolcalQualityMixin
19
+ _SimpleQualityMixin,
20
+ _SimplePlotQualityMixin,
21
+ _TableQualityMixin,
22
+ _PolcalQualityMixin,
23
+ _WavecalQualityMixin,
18
24
  ):
19
25
  """Mixin class supporting the generation of the quality reports."""
20
26
 
@@ -81,6 +87,7 @@ class QualityMixin(
81
87
  "HISTORICAL": self.quality_build_historical,
82
88
  "AO_STATUS": self.quality_build_ao_status,
83
89
  "RANGE": self.quality_build_range,
90
+ "WAVECAL_FIT": self.quality_build_wavecal_results,
84
91
  }
85
92
 
86
93
  @property