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
@@ -7,7 +7,6 @@ from dkist_processing_common._util.scratch import WorkflowFileSystem
7
7
  from dkist_processing_common.models.tags import Tag
8
8
  from dkist_processing_common.tasks import SubmitDatasetMetadata
9
9
  from dkist_processing_common.tasks.mixin import metadata_store
10
- from dkist_processing_common.tests.conftest import FakeGQLClient
11
10
 
12
11
 
13
12
  @pytest.fixture()
@@ -86,6 +85,7 @@ def submit_dataset_metadata_task(
86
85
  def test_submit_dataset_metadata(
87
86
  submit_dataset_metadata_task,
88
87
  mocker,
88
+ fake_gql_client,
89
89
  ):
90
90
  """
91
91
  :Given: An instance of SubmitDatasetMetadata with tagged processed data
@@ -93,15 +93,12 @@ def test_submit_dataset_metadata(
93
93
  :Then: Metadata files for the dataset are saved to the remote database
94
94
  """
95
95
  mocker.patch(
96
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
96
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
97
97
  )
98
- # intercept these two GraphQLClient calls so they can be confirmed
98
+ # intercept this GraphQLClient call so it can be confirmed
99
99
  mocked_metadata_store_add_dataset_receipt_account = mocker.patch.object(
100
100
  metadata_store.MetadataStoreMixin, "metadata_store_add_dataset_receipt_account"
101
101
  )
102
- mocked_metadata_store_add_quality_data = mocker.patch.object(
103
- metadata_store.MetadataStoreMixin, "metadata_store_add_quality_data"
104
- )
105
102
  task = submit_dataset_metadata_task
106
103
 
107
104
  # When
@@ -109,4 +106,3 @@ def test_submit_dataset_metadata(
109
106
 
110
107
  # Then
111
108
  mocked_metadata_store_add_dataset_receipt_account.assert_called_once()
112
- mocked_metadata_store_add_quality_data.assert_called_once()
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Tests for the tag cloud manager
3
3
  """
4
+
4
5
  import random
5
6
  from typing import Callable
6
7
  from uuid import uuid4
@@ -19,7 +19,7 @@ def test_task_name_tags():
19
19
  assert Tag.task_geometric() == f"TASK_{TaskName.geometric.value}"
20
20
  assert Tag.task_geometric_angle() == f"TASK_{TaskName.geometric_angle.value}"
21
21
  assert (
22
- Tag.task_geometric_sepectral_shifts() == f"TASK_{TaskName.geometric_spectral_shifts.value}"
22
+ Tag.task_geometric_spectral_shifts() == f"TASK_{TaskName.geometric_spectral_shifts.value}"
23
23
  )
24
24
  assert Tag.task_geometric_offset() == f"TASK_{TaskName.geometric_offsets.value}"
25
25
  assert Tag.task_polcal() == f"TASK_{TaskName.polcal.value}"
@@ -1,3 +1,5 @@
1
+ from enum import StrEnum
2
+
1
3
  import pytest
2
4
  from astropy.io import fits
3
5
 
@@ -9,6 +11,15 @@ from dkist_processing_common.parsers.task import parse_polcal_task_type
9
11
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
10
12
 
11
13
 
14
+ class DummyMetadataKey(StrEnum):
15
+ ip_task_type = "IPTASK"
16
+ gos_level3_status = "GOSLVL3"
17
+ gos_level3_lamp_status = "GOSLAMP"
18
+ gos_level0_status = "GOSLVL0"
19
+ gos_retarder_status = "GOSRET"
20
+ gos_polarizer_status = "GOSPOL"
21
+
22
+
12
23
  class DummyFitsAccess(FitsAccessBase):
13
24
  def __init__(
14
25
  self,
@@ -17,13 +28,12 @@ class DummyFitsAccess(FitsAccessBase):
17
28
  auto_squeeze: bool = False, # Because L1 data should always have the right form, right?
18
29
  ):
19
30
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
20
-
21
- self.ip_task_type: str = self.header["IPTASK"]
22
- self.gos_level3_status: str = self.header["GOSLVL3"]
23
- self.gos_level3_lamp_status: str = self.header["GOSLAMP"]
24
- self.gos_level0_status: str = self.header["GOSLVL0"]
25
- self.gos_retarder_status: str = self.header["GOSRET"]
26
- self.gos_polarizer_status: str = self.header["GOSPOL"]
31
+ self.ip_task_type = self.header[DummyMetadataKey.ip_task_type]
32
+ self.gos_level3_status = self.header[DummyMetadataKey.gos_level3_status]
33
+ self.gos_level3_lamp_status = self.header[DummyMetadataKey.gos_level3_lamp_status]
34
+ self.gos_level0_status = self.header[DummyMetadataKey.gos_level0_status]
35
+ self.gos_retarder_status = self.header[DummyMetadataKey.gos_retarder_status]
36
+ self.gos_polarizer_status = self.header[DummyMetadataKey.gos_polarizer_status]
27
37
 
28
38
 
29
39
  @pytest.fixture
@@ -5,10 +5,11 @@ import pytest
5
5
 
6
6
  from dkist_processing_common._util.scratch import WorkflowFileSystem
7
7
  from dkist_processing_common.codecs.str import str_encoder
8
- from dkist_processing_common.models.graphql import RecipeRunResponse
9
8
  from dkist_processing_common.models.tags import Tag
10
9
  from dkist_processing_common.tasks.teardown import Teardown
11
- from dkist_processing_common.tests.conftest import FakeGQLClient
10
+ from dkist_processing_common.tests.mock_metadata_store import RecipeRunResponseMapping
11
+ from dkist_processing_common.tests.mock_metadata_store import fake_gql_client_factory
12
+ from dkist_processing_common.tests.mock_metadata_store import make_default_recipe_run_response
12
13
 
13
14
 
14
15
  class TeardownTest(Teardown):
@@ -18,33 +19,36 @@ class TeardownTest(Teardown):
18
19
 
19
20
  @pytest.fixture()
20
21
  def make_mock_GQL_with_configuration():
21
- def class_generator(configuration: dict):
22
- class TeardownFakeGQLClient(FakeGQLClient):
23
- def execute_gql_query(self, **kwargs):
24
- response = super().execute_gql_query(**kwargs)
25
- if isinstance(response, list):
26
- if isinstance(response[0], RecipeRunResponse):
27
- response[0].configuration = json.dumps(configuration)
28
- return response
29
-
22
+ def class_generator(teardown_option: bool | None):
23
+ recipe_run_response = make_default_recipe_run_response()
24
+ config = recipe_run_response.configuration
25
+ if isinstance(teardown_option, bool):
26
+ config.teardown_enabled = teardown_option
27
+ else:
28
+ config_dict = config.model_dump(exclude="teardown_enabled")
29
+ config = json.dumps(config_dict)
30
+ response_mapping_override = RecipeRunResponseMapping(response=recipe_run_response)
31
+ TeardownFakeGQLClient = fake_gql_client_factory(
32
+ response_mapping_override=response_mapping_override
33
+ )
30
34
  return TeardownFakeGQLClient
31
35
 
32
36
  return class_generator
33
37
 
34
38
 
35
39
  @pytest.fixture(scope="session")
36
- def config_with_teardown_enabled() -> dict:
37
- return {"teardown_enabled": True}
40
+ def teardown_enabled() -> bool:
41
+ return True
38
42
 
39
43
 
40
44
  @pytest.fixture(scope="session")
41
- def config_with_teardown_disabled() -> dict:
42
- return {"teardown_enabled": False}
45
+ def teardown_disabled() -> bool:
46
+ return False
43
47
 
44
48
 
45
49
  @pytest.fixture(scope="session")
46
- def config_with_no_teardown() -> dict:
47
- return dict()
50
+ def teardown_default() -> None:
51
+ return None
48
52
 
49
53
 
50
54
  @pytest.fixture(scope="function")
@@ -75,14 +79,14 @@ def teardown_task_factory(tmp_path, recipe_run_id):
75
79
 
76
80
 
77
81
  def test_purge_data(
78
- teardown_task_factory, make_mock_GQL_with_configuration, config_with_teardown_enabled, mocker
82
+ teardown_task_factory, make_mock_GQL_with_configuration, teardown_enabled, mocker
79
83
  ):
80
84
  """
81
85
  :Given: A Teardown task with files and tags linked to it and teardown enabled
82
86
  :When: Running the task
83
87
  :Then: All the files are deleted and the tags are removed
84
88
  """
85
- FakeGQLClass = make_mock_GQL_with_configuration(config_with_teardown_enabled)
89
+ FakeGQLClass = make_mock_GQL_with_configuration(teardown_enabled)
86
90
  mocker.patch(
87
91
  "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClass
88
92
  )
@@ -102,14 +106,14 @@ def test_purge_data(
102
106
 
103
107
 
104
108
  def test_purge_data_disabled(
105
- teardown_task_factory, make_mock_GQL_with_configuration, config_with_teardown_disabled, mocker
109
+ teardown_task_factory, make_mock_GQL_with_configuration, teardown_disabled, mocker
106
110
  ):
107
111
  """
108
112
  :Given: A Teardown task with files and tags linked to it and teardown disabled
109
113
  :When: Running the task
110
114
  :Then: All the files are not deleted and the tags remain
111
115
  """
112
- FakeGQLClass = make_mock_GQL_with_configuration(config_with_teardown_disabled)
116
+ FakeGQLClass = make_mock_GQL_with_configuration(teardown_disabled)
113
117
  mocker.patch(
114
118
  "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClass
115
119
  )
@@ -129,14 +133,14 @@ def test_purge_data_disabled(
129
133
 
130
134
 
131
135
  def test_purge_data_no_config(
132
- teardown_task_factory, make_mock_GQL_with_configuration, config_with_no_teardown, mocker
136
+ teardown_task_factory, make_mock_GQL_with_configuration, teardown_default, mocker
133
137
  ):
134
138
  """
135
- :Given: A Teardown task with files and tags linked and teardown not specified in the configuration
139
+ :Given: A Teardown task with files and tags linked and default teardown configuration
136
140
  :When: Running the task
137
141
  :Then: All the files are deleted and the tags are removed
138
142
  """
139
- FakeGQLClass = make_mock_GQL_with_configuration(config_with_no_teardown)
143
+ FakeGQLClass = make_mock_GQL_with_configuration(teardown_default)
140
144
  mocker.patch(
141
145
  "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClass
142
146
  )
@@ -1,167 +1,312 @@
1
1
  import json
2
2
  import os
3
- from itertools import chain
4
3
  from pathlib import Path
5
- from uuid import uuid4
6
4
 
7
5
  import pytest
8
6
 
9
7
  from dkist_processing_common._util.scratch import WorkflowFileSystem
8
+ from dkist_processing_common.codecs.basemodel import basemodel_decoder
9
+ from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
10
10
  from dkist_processing_common.models.tags import Tag
11
+ from dkist_processing_common.tasks import WorkflowTaskBase
11
12
  from dkist_processing_common.tasks.transfer_input_data import TransferL0Data
12
- from dkist_processing_common.tests.conftest import create_parameter_files
13
+ from dkist_processing_common.tests.mock_metadata_store import InputDatasetRecipeRunResponseMapping
14
+ from dkist_processing_common.tests.mock_metadata_store import default_calibration_frames_doc
15
+ from dkist_processing_common.tests.mock_metadata_store import default_observe_frames_doc
16
+ from dkist_processing_common.tests.mock_metadata_store import default_parameters_doc
17
+ from dkist_processing_common.tests.mock_metadata_store import fake_gql_client_factory
18
+ from dkist_processing_common.tests.mock_metadata_store import (
19
+ make_default_input_dataset_recipe_run_response,
20
+ )
21
+
22
+
23
+ def create_parameter_files(
24
+ task: WorkflowTaskBase, parameters_doc: list[dict] = default_parameters_doc
25
+ ):
26
+ """
27
+ Create the parameter files specified in the parameters document returned by the metadata store.
28
+
29
+ This fixture assumes that the JSON parameters document has already been loaded into a python
30
+ structure (a list of dicts), but the parameter values themselves are still JSON.
31
+ """
32
+ for parameter in parameters_doc:
33
+ for value in parameter["parameterValues"]:
34
+ if "__file__" not in value["parameterValue"]:
35
+ continue
36
+ parameter_value = json.loads(value["parameterValue"])
37
+ param_path = parameter_value["__file__"]["objectKey"]
38
+ file_path = task.scratch.workflow_base_path / Path(param_path)
39
+ if not file_path.parent.exists():
40
+ file_path.parent.mkdir(parents=True, exist_ok=True)
41
+ file_path.write_text(data="")
42
+ task.tag(path=file_path, tags=Tag.parameter(param_path))
43
+
44
+
45
+ def create_input_frames(
46
+ task: WorkflowTaskBase,
47
+ input_frame_docs: list[dict] = default_observe_frames_doc + default_calibration_frames_doc,
48
+ ):
49
+ """
50
+ Create the observe and calibration frame files specified in the input dataset documents
51
+ returned by the metadata store.
52
+ """
53
+ for frame in input_frame_docs:
54
+ for object_key in frame["object_keys"]:
55
+ file_path = task.scratch.workflow_base_path / Path(object_key)
56
+ if not file_path.parent.exists():
57
+ file_path.parent.mkdir(parents=True, exist_ok=True)
58
+ file_path.write_text(data="")
59
+ task.tag(path=file_path, tags=[Tag.frame(), Tag.input()])
60
+
61
+
62
+ class TransferL0DataTask(TransferL0Data):
63
+ def run(self) -> None: ...
13
64
 
14
65
 
15
66
  @pytest.fixture
16
- def transfer_l0_data(recipe_run_id, tmp_path) -> dict:
17
- task = TransferL0Data(
67
+ def fake_gql_client_class_missing_calibration_part():
68
+ input_dataset_recipe_run_response = make_default_input_dataset_recipe_run_response()
69
+ dataset_parts = (
70
+ input_dataset_recipe_run_response.recipeInstance.inputDataset.inputDatasetInputDatasetParts
71
+ )
72
+ for index, part in enumerate(dataset_parts):
73
+ if (
74
+ part.inputDatasetPart.inputDatasetPartType.inputDatasetPartTypeName
75
+ == "calibration_frames"
76
+ ):
77
+ del dataset_parts[index]
78
+ new_response_mapping = InputDatasetRecipeRunResponseMapping(
79
+ response=input_dataset_recipe_run_response
80
+ )
81
+ FakeGQLClientMissingInputDatasetCalibrationPart = fake_gql_client_factory(
82
+ response_mapping_override=new_response_mapping
83
+ )
84
+
85
+ return FakeGQLClientMissingInputDatasetCalibrationPart
86
+
87
+
88
+ def _transfer_l0_data_task_with_client(recipe_run_id, tmp_path, mocker, client_cls):
89
+ mocker.patch(
90
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient",
91
+ new=client_cls,
92
+ )
93
+ with TransferL0DataTask(
18
94
  recipe_run_id=recipe_run_id,
19
95
  workflow_name="workflow_name",
20
96
  workflow_version="workflow_version",
97
+ ) as task:
98
+ task.scratch = WorkflowFileSystem(
99
+ recipe_run_id=recipe_run_id,
100
+ scratch_base_path=tmp_path,
101
+ )
102
+ yield task
103
+ task._purge()
104
+
105
+
106
+ @pytest.fixture
107
+ def transfer_l0_data_task(recipe_run_id, tmp_path, mocker, fake_gql_client):
108
+ yield from _transfer_l0_data_task_with_client(recipe_run_id, tmp_path, mocker, fake_gql_client)
109
+
110
+
111
+ @pytest.fixture
112
+ def transfer_l0_data_task_missing_calibration_part(
113
+ recipe_run_id, tmp_path, mocker, fake_gql_client_class_missing_calibration_part
114
+ ):
115
+ yield from _transfer_l0_data_task_with_client(
116
+ recipe_run_id, tmp_path, mocker, fake_gql_client_class_missing_calibration_part
21
117
  )
22
- task.scratch = WorkflowFileSystem(
23
- recipe_run_id=recipe_run_id,
24
- scratch_base_path=tmp_path,
25
- )
26
- task.scratch.scratch_base_path = tmp_path
27
- input_dataset_parameters_part = [
28
- {
29
- "parameterName": "param_name_1",
30
- "parameterValues": [
31
- {
32
- "parameterValueId": 1,
33
- "parameterValue": json.dumps([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
34
- "parameterValueStartDate": "2000-01-01",
35
- }
36
- ],
37
- },
38
- {
39
- "parameterName": "param_name_2",
40
- "parameterValues": [
41
- {
42
- "parameterValueId": 2,
43
- "parameterValue": json.dumps(
44
- {
45
- "__file__": {
46
- "bucket": "data",
47
- "objectKey": f"parameters/param_name/{uuid4().hex}.dat",
48
- }
49
- }
50
- ),
51
- "parameterValueStartDate": "2000-01-01",
52
- },
53
- {
54
- "parameterValueId": 3,
55
- "parameterValue": json.dumps(
56
- {
57
- "__file__": {
58
- "bucket": "data",
59
- "objectKey": f"parameters/param_name/{uuid4().hex}.dat",
60
- }
61
- }
62
- ),
63
- "parameterValueStartDate": "2000-01-02",
64
- },
65
- ],
66
- },
67
- {
68
- "parameterName": "param_name_4",
69
- "parameterValues": [
70
- {
71
- "parameterValueId": 4,
72
- "parameterValue": json.dumps(
73
- {"a": 1, "b": 3.14159, "c": "foo", "d": [1, 2, 3]}
74
- ),
75
- "parameterValueStartDate": "2000-01-01",
76
- }
77
- ],
78
- },
79
- ]
80
- input_dataset_observe_frames_part = [
81
- {
82
- "bucket": uuid4().hex[:6],
83
- "object_keys": [Path(uuid4().hex[:6]).as_posix() for _ in range(3)],
84
- }
85
- ]
86
- input_dataset_calibration_frames_part = [
87
- {
88
- "bucket": uuid4().hex[:6],
89
- "object_keys": [Path(uuid4().hex[:6]).as_posix() for _ in range(3)],
90
- },
91
- {
92
- "bucket": uuid4().hex[:6],
93
- "object_keys": [Path(uuid4().hex[:6]).as_posix() for _ in range(3)],
94
- },
95
- ]
96
- # load parameters file
97
- file_path = task.scratch.workflow_base_path / Path(f"{uuid4().hex[:6]}.ext")
98
- file_path.write_text(data=json.dumps(input_dataset_parameters_part))
99
- task.tag(path=file_path, tags=Tag.input_dataset_parameters())
100
- # create parameter files
101
- expected_parameters = dict()
102
- for item in input_dataset_parameters_part:
103
- expected_parameters[item["parameterName"]] = item["parameterValues"]
104
- create_parameter_files(task, expected_parameters)
105
- # load observe frames file
106
- file_path = task.scratch.workflow_base_path / Path(f"{uuid4().hex[:6]}.ext")
107
- file_path.write_text(data=json.dumps(input_dataset_observe_frames_part))
108
- task.tag(path=file_path, tags=Tag.input_dataset_observe_frames())
109
- # load calibration frames file
110
- file_path = task.scratch.workflow_base_path / Path(f"{uuid4().hex[:6]}.ext")
111
- file_path.write_text(data=json.dumps(input_dataset_calibration_frames_part))
112
- task.tag(path=file_path, tags=Tag.input_dataset_calibration_frames())
113
-
114
- yield {
115
- "task": task,
116
- "parameters": input_dataset_parameters_part,
117
- "observe": input_dataset_observe_frames_part,
118
- "calibration": input_dataset_calibration_frames_part,
119
- }
120
- task._purge()
121
-
122
-
123
- def test_format_frame_transfer_items(transfer_l0_data):
118
+
119
+
120
+ @pytest.mark.parametrize(
121
+ "expected_doc, tag",
122
+ [
123
+ pytest.param(
124
+ default_observe_frames_doc,
125
+ Tag.input_dataset_observe_frames(),
126
+ id="observe_frames",
127
+ ),
128
+ pytest.param(
129
+ default_calibration_frames_doc,
130
+ Tag.input_dataset_calibration_frames(),
131
+ id="calibration_frames",
132
+ ),
133
+ pytest.param(
134
+ default_parameters_doc,
135
+ Tag.input_dataset_parameters(),
136
+ id="parameters",
137
+ ),
138
+ ],
139
+ )
140
+ def test_download_dataset(transfer_l0_data_task, expected_doc, tag):
124
141
  """
125
142
  :Given: a TransferL0Data task with a valid input dataset
126
- :When: formatting items in the input dataset for transfer
127
- :Then: the items are correctly loaded into GlobusTransferItem objects
143
+ :When: downloading the dataset documents from the metadata store
144
+ :Then: the correct documents are written to disk, along with tags for file parameters
145
+ """
146
+ # Given
147
+ task = transfer_l0_data_task
148
+ # When
149
+ task.download_input_dataset()
150
+ # Then
151
+ doc_from_file = next(
152
+ task.read(tags=tag, decoder=basemodel_decoder, model=InputDatasetPartDocumentList)
153
+ )
154
+ doc_list_from_file = doc_from_file.model_dump()["doc_list"]
155
+ if (
156
+ tag == Tag.input_dataset_parameters()
157
+ ): # parameter doc gets written with tags for file objects
158
+ for item in expected_doc:
159
+ for val in item["parameterValues"]:
160
+ if "__file__" in val["parameterValue"]:
161
+ file_dict = json.loads(val["parameterValue"])["__file__"]
162
+ file_dict["tag"] = Tag.parameter(Path(file_dict["objectKey"]).name)
163
+ val["parameterValue"] = json.dumps({"__file__": file_dict})
164
+ assert doc_list_from_file == expected_doc
165
+
166
+
167
+ def test_download_dataset_missing_part(transfer_l0_data_task_missing_calibration_part):
128
168
  """
129
- task = transfer_l0_data["task"]
169
+ :Given: a TransferL0Data task with a valid input dataset without calibration frames
170
+ :When: downloading the dataset documents from the metadata store
171
+ :Then: the correct number of documents are written to disk
172
+ """
173
+ # Given
174
+ task = transfer_l0_data_task_missing_calibration_part
175
+ # When
176
+ task.download_input_dataset()
177
+ # Then
178
+ observe_doc_from_file = next(
179
+ task.read(
180
+ tags=Tag.input_dataset_observe_frames(),
181
+ decoder=basemodel_decoder,
182
+ model=InputDatasetPartDocumentList,
183
+ )
184
+ )
185
+ parameters_doc_from_file = next(
186
+ task.read(
187
+ tags=Tag.input_dataset_parameters(),
188
+ decoder=basemodel_decoder,
189
+ model=InputDatasetPartDocumentList,
190
+ )
191
+ )
192
+ with pytest.raises(StopIteration):
193
+ calibration_doc_from_file = next(
194
+ task.read(
195
+ tags=Tag.input_dataset_calibration_frames(),
196
+ decoder=basemodel_decoder,
197
+ model=InputDatasetPartDocumentList,
198
+ )
199
+ )
200
+
201
+
202
+ @pytest.mark.parametrize(
203
+ "task_name",
204
+ [
205
+ pytest.param(
206
+ "transfer_l0_data_task",
207
+ id="observe_and_calibration_frames",
208
+ ),
209
+ pytest.param(
210
+ "transfer_l0_data_task_missing_calibration_part",
211
+ id="calibration_frames_missing",
212
+ ),
213
+ ],
214
+ )
215
+ def test_build_frame_transfer_list_formatted(request, task_name):
216
+ """
217
+ :Given: a TransferL0Data task with downloaded input dataset docs
218
+ :When: building a list of frames in the input dataset formatted for transfer
219
+ :Then: the correct items are correctly loaded into GlobusTransferItem objects
220
+ """
221
+ # Given
222
+ task = request.getfixturevalue(task_name)
223
+ task.download_input_dataset()
224
+ # When
225
+ observe_transfer_objects = task.build_transfer_list(doc_tag=Tag.input_dataset_observe_frames())
226
+ calibration_transfer_objects = task.build_transfer_list(
227
+ doc_tag=Tag.input_dataset_calibration_frames()
228
+ )
229
+ transfer_objects = observe_transfer_objects + calibration_transfer_objects
230
+ formatted_transfer_items = task.format_transfer_items(input_dataset_objects=transfer_objects)
231
+ # Then
130
232
  source_filenames = []
131
233
  destination_filenames = []
132
- for frame_set in chain(transfer_l0_data["observe"], transfer_l0_data["calibration"]):
234
+ expected_frames = list(default_observe_frames_doc)
235
+ if "missing_calibration_part" not in task_name:
236
+ expected_frames += default_calibration_frames_doc
237
+ for frame_set in expected_frames:
133
238
  for key in frame_set["object_keys"]:
134
239
  source_filenames.append(os.path.join("/", frame_set["bucket"], key))
135
240
  destination_filenames.append(Path(key).name)
136
- assert len(task.format_frame_transfer_items()) == len(source_filenames)
137
- for item in task.format_frame_transfer_items():
241
+ assert len(formatted_transfer_items) == len(source_filenames)
242
+ for item in formatted_transfer_items:
138
243
  assert item.source_path.as_posix() in source_filenames
139
244
  assert item.destination_path.name in destination_filenames
140
245
  assert not item.recursive
141
246
 
142
247
 
143
- def test_format_parameter_file_transfer_items(transfer_l0_data):
248
+ def test_build_parameter_file_transfer_items(transfer_l0_data_task):
144
249
  """
145
- :Given: a TransferL0Data task with a valid input dataset
146
- :When: formatting items in the input dataset for transfer
147
- :Then: the items are correctly loaded into GlobusTransferItem objects
250
+ :Given: a TransferL0Data task with downloaded input dataset docs
251
+ :When: building a list of parameter files formatted for transfer
252
+ :Then: the correct items are correctly loaded into GlobusTransferItem objects
148
253
  """
149
- task = transfer_l0_data["task"]
254
+ # Given
255
+ task = transfer_l0_data_task
256
+ task.download_input_dataset()
257
+ # When
258
+ transfer_objects = task.build_transfer_list(doc_tag=Tag.input_dataset_parameters())
259
+ formatted_transfer_items = task.format_transfer_items(input_dataset_objects=transfer_objects)
260
+ # Then
150
261
  source_filenames = []
151
262
  destination_filenames = []
152
- for param in transfer_l0_data["parameters"]:
263
+ parameters = default_parameters_doc
264
+ for param in parameters:
153
265
  for value in param["parameterValues"]:
154
- param_value = value["parameterValue"]
155
- if "__file__" in param_value:
156
- value_dict = json.loads(param_value)
266
+ if "__file__" in value["parameterValue"]:
267
+ value_dict = json.loads(value["parameterValue"])
157
268
  bucket = value_dict["__file__"]["bucket"]
158
269
  object_key = value_dict["__file__"]["objectKey"]
159
270
  source_filenames.append(os.path.join("/", bucket, object_key))
160
271
  destination_filenames.append(Path(object_key).name)
161
- transfer_items = task.format_parameter_transfer_items()
162
- assert len(transfer_items) == len(source_filenames)
163
- for transfer_item in transfer_items:
272
+ assert len(formatted_transfer_items) == len(source_filenames)
273
+ for transfer_item in formatted_transfer_items:
164
274
  assert transfer_item.source_path.as_posix() in source_filenames
165
275
  assert transfer_item.destination_path.name in destination_filenames
166
276
  assert str(transfer_item.destination_path).startswith(str(task.scratch.workflow_base_path))
167
277
  assert not transfer_item.recursive
278
+
279
+
280
+ def test_tag_transfer_items(transfer_l0_data_task):
281
+ """
282
+ :Given: a TransferL0Data task with downloaded input dataset frames and parameter files
283
+ :When: tagging the downloaded files
284
+ :Then: the downloaded items are correctly tagged
285
+ """
286
+ # Given
287
+ task = transfer_l0_data_task
288
+ task.download_input_dataset()
289
+ observe_transfer_objects = task.build_transfer_list(doc_tag=Tag.input_dataset_observe_frames())
290
+ calibration_transfer_objects = task.build_transfer_list(
291
+ doc_tag=Tag.input_dataset_calibration_frames()
292
+ )
293
+ frame_transfer_objects = observe_transfer_objects + calibration_transfer_objects
294
+ create_input_frames(task)
295
+ parameter_transfer_objects = task.build_transfer_list(doc_tag=Tag.input_dataset_parameters())
296
+ create_parameter_files(task)
297
+ # When
298
+ transfer_objects = frame_transfer_objects + parameter_transfer_objects
299
+ task.tag_transfer_objects(input_dataset_objects=transfer_objects)
300
+ # Then
301
+ input_tags = [Tag.input(), Tag.frame()]
302
+ input_frames_on_disk = list(task.scratch.find_all(tags=input_tags))
303
+ for obj in frame_transfer_objects:
304
+ destination_path = task.scratch.absolute_path(obj.object_key)
305
+ assert destination_path in input_frames_on_disk
306
+ assert len(input_frames_on_disk) == len(frame_transfer_objects)
307
+ for obj in parameter_transfer_objects:
308
+ destination_path = task.scratch.absolute_path(obj.object_key)
309
+ param_tag = Tag.parameter(Path(obj.object_key))
310
+ param_file_on_disk = list(task.scratch.find_all(tags=param_tag))
311
+ assert destination_path in param_file_on_disk
312
+ assert len(param_file_on_disk) == 1