osc-metadata-client 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ # Copyright 2026 Terradue
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = "0.3.0"
@@ -0,0 +1,275 @@
1
+ # Copyright 2026 Terradue
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from gzip import GzipFile
16
+ from io import BytesIO, TextIOWrapper
17
+ from loguru import logger
18
+ from ogc_api_processes_client.api_client import ApiClient
19
+ from ogc_api_processes_client.api.status_api import StatusApi
20
+ from ogc_api_processes_client.configuration import Configuration
21
+ from ogc_api_processes_client.models.status_code import StatusCode
22
+ from ogc_api_processes_client.models.status_info import StatusInfo
23
+ from pathlib import Path
24
+ from pydantic import BaseModel
25
+ from pystac import Catalog, Link as PystacLink, RelType
26
+ from requests import Session
27
+ from requests.adapters import BaseAdapter, HTTPAdapter
28
+ from session_adapters.file_adapter import FileAdapter
29
+ from session_adapters.http_conts import DEFAULT_ENCODING
30
+ from session_adapters.oci_adapter import OCIAdapter
31
+ from tempfile import NamedTemporaryFile
32
+ from typing import Any, Mapping, TypeVar
33
+ from transpiler_mate.metadata import MetadataManager
34
+ from transpiler_mate.metadata.software_application_models import SoftwareApplication
35
+ from transpiler_mate.ogcapi.records import OgcRecordsTranspiler
36
+ from transpiler_mate.ogcapi.records.ogcapi_records_models import (
37
+ Coordinate,
38
+ Link,
39
+ MultipointGeoJSON,
40
+ RecordGeoJSON,
41
+ )
42
+
43
+ import yaml
44
+ import json
45
+ import time
46
+
47
+ PENDING = {StatusCode.ACCEPTED, StatusCode.RUNNING}
48
+
49
+
50
+ def create_client(ogc_api_endpoint: str, authorization_token: str | None) -> ApiClient:
51
+ return ApiClient(
52
+ configuration=Configuration(
53
+ host=ogc_api_endpoint,
54
+ ),
55
+ header_name="Authorization" if authorization_token else None,
56
+ header_value=f"Bearer {authorization_token}" if authorization_token else None,
57
+ )
58
+
59
+
60
+ def retrieve_status_info(api_client: ApiClient, job_id: str) -> StatusInfo:
61
+ status_api = StatusApi(api_client=api_client)
62
+
63
+ logger.debug(f"Retrieving the Job {job_id} status...")
64
+
65
+ status_info: StatusInfo | None = None
66
+ while status_info is None or status_info.status in PENDING:
67
+ time.sleep(10)
68
+
69
+ status_info = status_api.get_status(job_id=job_id)
70
+
71
+ logger.debug(f"Job {job_id} status is {status_info.status}")
72
+
73
+ if StatusCode.SUCCESSFUL != status_info.status:
74
+ raise Exception(
75
+ f"Impossible to create the OGC API Records 'Experiment', job '{job_id}' terminated with status '{status_info.status}', report to your provider"
76
+ )
77
+
78
+ logger.success(f"Job {job_id} execution is complete.")
79
+ return status_info
80
+
81
+
82
+ def load_record_geojson(
83
+ source: str, project_id: str, project_name: str
84
+ ) -> RecordGeoJSON:
85
+ session: Session = Session()
86
+
87
+ def mount_session(scheme: str, adapter: BaseAdapter):
88
+ logger.debug(f"Mounting '{scheme}' scheme to '{type(adapter).__name__}'...")
89
+ session.mount(scheme, adapter)
90
+ logger.debug(
91
+ f"Scheme '{scheme}' successfully mount to '{type(adapter).__name__}'"
92
+ )
93
+
94
+ http_adapter = HTTPAdapter()
95
+ mount_session("http://", http_adapter)
96
+ mount_session("https://", http_adapter)
97
+ mount_session("file://", FileAdapter())
98
+ mount_session("oci://", OCIAdapter())
99
+
100
+ logger.debug(f"> GET {source}...")
101
+
102
+ response = session.get(source, stream=True)
103
+ response.raise_for_status()
104
+
105
+ logger.debug(f"< {response.status_code} {response.reason}")
106
+ for k, v in response.headers.items():
107
+ logger.debug(f"< {k}: {v}")
108
+
109
+ # Read first 2 bytes to check for gzip
110
+ magic = response.raw.read(2)
111
+ remaining = response.raw.read() # Read rest of the stream
112
+ combined = BytesIO(magic + remaining)
113
+
114
+ if b"\x1f\x8b" == magic:
115
+ logger.debug(f"gzip compression detected in response body from {source}")
116
+ buffer = GzipFile(fileobj=combined)
117
+ else:
118
+ buffer = combined
119
+
120
+ input_stream = TextIOWrapper(buffer, encoding=DEFAULT_ENCODING)
121
+
122
+ fd = NamedTemporaryFile(mode="w", suffix=".cwl", encoding=DEFAULT_ENCODING)
123
+
124
+ try:
125
+ tmp_path = Path(fd.name)
126
+
127
+ logger.debug(
128
+ f"Caching the CWL document to a temporary file on {tmp_path.absolute()}..."
129
+ )
130
+
131
+ with tmp_path.open("w") as output_stream:
132
+ output_stream.write(input_stream.read())
133
+
134
+ logger.success(f"CWL document stored to {tmp_path.absolute()} temporary file.")
135
+
136
+ logger.debug(
137
+ f"Reading Schema.org metadata from CWL document on {tmp_path.absolute()}..."
138
+ )
139
+
140
+ manager: MetadataManager = MetadataManager(tmp_path)
141
+ metadata: SoftwareApplication = manager.metadata
142
+
143
+ logger.success(
144
+ f"Schema.org metadata read from CWL document on {tmp_path.absolute()}."
145
+ )
146
+
147
+ logger.debug("Transpiling Schema.org metadata to OGCP API Records...")
148
+
149
+ transpiler: OgcRecordsTranspiler = OgcRecordsTranspiler()
150
+
151
+ data: Mapping[str, Any] = transpiler.transpile(metadata)
152
+
153
+ record_geojson: RecordGeoJSON = RecordGeoJSON.model_validate(
154
+ obj=data, by_alias=True
155
+ )
156
+
157
+ record_geojson.geometry = MultipointGeoJSON(
158
+ coordinates=[Coordinate([-180.0, -90.0, 180.0, 90.0])],
159
+ )
160
+
161
+ if record_geojson.properties.language:
162
+ record_geojson.properties.language.alternate = (
163
+ record_geojson.properties.language.name
164
+ )
165
+
166
+ record_geojson.properties.languages = [record_geojson.properties.language]
167
+
168
+ if record_geojson.properties.resource_languages:
169
+ for resource_language in record_geojson.properties.resource_languages:
170
+ resource_language.alternate = resource_language.name
171
+
172
+ if not record_geojson.links:
173
+ record_geojson.links = []
174
+
175
+ record_geojson.links.append(
176
+ Link(
177
+ rel="related",
178
+ href=f"../../projects/{project_id}/collection.json",
179
+ type="application/json",
180
+ title=f"Project: {project_name}",
181
+ hreflang="en-US",
182
+ created=None,
183
+ updated=None,
184
+ )
185
+ )
186
+ record_geojson.links.append(
187
+ Link(
188
+ rel="root",
189
+ href="../../catalog.json",
190
+ type="application/json",
191
+ title="Open Science Catalog",
192
+ hreflang="en-US",
193
+ created=None,
194
+ updated=None,
195
+ )
196
+ )
197
+
198
+ logger.success("Schema.org metadata transpiled to OGCP API Records.")
199
+
200
+ return record_geojson
201
+ finally:
202
+ fd.close()
203
+
204
+
205
+ T = TypeVar("T", bound=BaseModel)
206
+
207
+
208
+ def cast_model(src: BaseModel, dst_cls: type[T]) -> T:
209
+ # mode="python" preserves datetimes, URLs, etc.
210
+ data = src.model_dump(mode="python", by_alias=True, exclude_none=False)
211
+ return dst_cls.model_validate(data, by_alias=True)
212
+
213
+
214
+ def dump_data(data: Mapping[str, Any], output: Path, rel: RelType = RelType.ITEM):
215
+ logger.info(f"Serializing OGC API Records to {output.absolute()}...")
216
+
217
+ output.parent.mkdir(parents=True, exist_ok=True)
218
+
219
+ with output.open("w") as output_stream:
220
+ json.dump(
221
+ data,
222
+ output_stream,
223
+ indent=2,
224
+ )
225
+
226
+ logger.success(f"OGC API Records serialized to {output.absolute()}.")
227
+
228
+ catalog_file = Path(output.parent.parent, "catalog.json")
229
+
230
+ if catalog_file.exists():
231
+ logger.info(f"Updating STAC Catalog from {output.absolute()}...")
232
+
233
+ href: str = f"./{data['id']}/record.json"
234
+
235
+ catalog: Catalog = Catalog.from_file(catalog_file)
236
+
237
+ # Check whether the same rel + href is already present
238
+ for link in catalog.links:
239
+ if link.get_href() == href:
240
+ logger.info(
241
+ f"Link {href} already present in {output.absolute()}, update is not required."
242
+ )
243
+ return
244
+
245
+ catalog.add_link(
246
+ PystacLink(
247
+ rel=rel,
248
+ target=href,
249
+ media_type="application/json",
250
+ title=data["properties"]["title"]
251
+ if "properties" in data
252
+ else data["title"]
253
+ if "title" in data
254
+ else None,
255
+ )
256
+ )
257
+
258
+ logger.info(f"Saving STAC Catalog to {catalog_file.absolute()}...")
259
+ catalog.save_object(
260
+ include_self_link=False, dest_href=catalog_file.absolute().as_posix()
261
+ )
262
+ logger.success(f"STAC Catalog successfully saved to {catalog_file.absolute()}.")
263
+ else:
264
+ logger.warning(
265
+ f"Catalog file {catalog_file.absolute()} not found, skipping the update"
266
+ )
267
+
268
+
269
+ def serialize_yaml(data: Any, target_file: Path):
270
+ target_file.parent.mkdir(parents=True, exist_ok=True)
271
+ with target_file.open("w") as output_stream:
272
+ yaml.dump(
273
+ data,
274
+ output_stream,
275
+ )
@@ -0,0 +1,161 @@
1
+ # Copyright 2026 Terradue
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from osc_metadata_client import load_record_geojson
16
+ from osc_metadata_client.experiment import execute as execute_experiment
17
+ from osc_metadata_client.product import execute as execute_product
18
+ from osc_metadata_client.workflow import execute as execute_workflow
19
+ from pathlib import Path
20
+ from transpiler_mate.cli.cli import _track
21
+ from transpiler_mate.ogcapi.records.ogcapi_records_models import RecordGeoJSON
22
+
23
+ import click
24
+
25
+
26
+ @click.group()
27
+ @click.argument("source", type=click.STRING, required=True)
28
+ @click.option(
29
+ "--id", type=click.STRING, required=True, help="The OGC API Processes Job ID."
30
+ )
31
+ @click.option(
32
+ "--project-id",
33
+ type=click.STRING,
34
+ required=True,
35
+ help="The referencing Open Science Catalog project ID.",
36
+ )
37
+ @click.option(
38
+ "--project-name",
39
+ type=click.STRING,
40
+ required=True,
41
+ help="The referencing Open Science Catalog project Name.",
42
+ )
43
+ @click.option(
44
+ "--ogc-api-processes-endpoint",
45
+ type=click.STRING,
46
+ required=True,
47
+ help="The referencing OGC API Processes service URL.",
48
+ )
49
+ @click.option(
50
+ "--output",
51
+ type=click.Path(path_type=Path),
52
+ required=True,
53
+ help="The output directory path",
54
+ )
55
+ @click.pass_context
56
+ def main(
57
+ ctx,
58
+ source: str,
59
+ id: str,
60
+ project_id: str,
61
+ project_name: str,
62
+ ogc_api_processes_endpoint: str,
63
+ output: Path,
64
+ ):
65
+ ctx.ensure_object(dict)
66
+ ctx.obj["source"] = source
67
+
68
+ record_geojson: RecordGeoJSON = load_record_geojson(
69
+ source, project_id, project_name
70
+ )
71
+ record_geojson.id = id
72
+ ctx.obj["record_geojson"] = record_geojson
73
+
74
+ ctx.obj["ogc-api-processes-endpoint"] = ogc_api_processes_endpoint
75
+ ctx.obj["project-id"] = project_id
76
+ ctx.obj["output"] = output
77
+
78
+
79
+ @main.command(context_settings={"show_default": True})
80
+ @click.pass_context
81
+ def workflow(ctx):
82
+ source: str = ctx.obj["source"]
83
+ ogc_api_processes_endpoint = ctx.obj["ogc-api-processes-endpoint"]
84
+ record_geojson: RecordGeoJSON = ctx.obj["record_geojson"]
85
+ project_id: str = ctx.obj["project-id"]
86
+ output: Path = ctx.obj["output"]
87
+ execute_workflow(
88
+ source, ogc_api_processes_endpoint, record_geojson, project_id, output
89
+ )
90
+
91
+
92
+ @main.command(context_settings={"show_default": True})
93
+ @click.pass_context
94
+ @click.option(
95
+ "--workflow-id",
96
+ type=click.STRING,
97
+ required=True,
98
+ help="The referencing OGC API Records workflow URL.",
99
+ )
100
+ @click.option(
101
+ "--authorization-token",
102
+ type=click.STRING,
103
+ required=False,
104
+ default=None,
105
+ help="Authorization JSON Web Token'",
106
+ )
107
+ def experiment(
108
+ ctx,
109
+ workflow_id: str,
110
+ authorization_token: str,
111
+ ):
112
+ ogc_api_processes_endpoint = ctx.obj["ogc-api-processes-endpoint"]
113
+ record_geojson: RecordGeoJSON = ctx.obj["record_geojson"]
114
+ project_id: str = ctx.obj["project-id"]
115
+ output: Path = ctx.obj["output"]
116
+ execute_experiment(
117
+ project_id=project_id,
118
+ workflow_id=workflow_id,
119
+ record_geojson=record_geojson,
120
+ ogc_api_processes_endpoint=ogc_api_processes_endpoint,
121
+ output=output,
122
+ authorization_token=authorization_token,
123
+ )
124
+
125
+
126
+ @main.command(context_settings={"show_default": True})
127
+ @click.pass_context
128
+ @click.option(
129
+ "--experiment-id",
130
+ type=click.STRING,
131
+ required=True,
132
+ help="The referencing OGC API Records workflow ID.",
133
+ )
134
+ @click.option(
135
+ "--authorization-token",
136
+ type=click.STRING,
137
+ required=False,
138
+ default=None,
139
+ help="Authorization JSON Web Token'",
140
+ )
141
+ def products(
142
+ ctx,
143
+ experiment_id: str,
144
+ authorization_token: str,
145
+ ):
146
+ ogc_api_processes_endpoint = ctx.obj["ogc-api-processes-endpoint"]
147
+ record_geojson: RecordGeoJSON = ctx.obj["record_geojson"]
148
+ project_id: str = ctx.obj["project-id"]
149
+ output: Path = ctx.obj["output"]
150
+ execute_product(
151
+ ogc_api_processes_endpoint,
152
+ record_geojson,
153
+ project_id,
154
+ experiment_id,
155
+ output,
156
+ authorization_token,
157
+ )
158
+
159
+
160
+ for command in [workflow, experiment, products]:
161
+ command.callback = _track(command.callback)
@@ -0,0 +1,136 @@
1
+ # Copyright 2026 Terradue
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from loguru import logger
16
+ from osc_metadata_client import (
17
+ cast_model,
18
+ create_client,
19
+ retrieve_status_info,
20
+ dump_data,
21
+ serialize_yaml,
22
+ )
23
+ from ogc_api_processes_client.models.status_info import StatusInfo
24
+ from osc_metadata_client.models import ExperimentProperties
25
+ from pathlib import Path
26
+ from transpiler_mate.ogcapi.records.ogcapi_records_models import Link, RecordGeoJSON
27
+
28
+
29
+ def execute(
30
+ project_id: str,
31
+ workflow_id: str,
32
+ record_geojson: RecordGeoJSON,
33
+ ogc_api_processes_endpoint: str,
34
+ output: Path,
35
+ authorization_token: str,
36
+ ):
37
+ logger.debug("Enriching OGCP API Records...")
38
+
39
+ record_geojson.links.append( # type: ignore see osc_metadata_client.load_record_geojson
40
+ Link(
41
+ rel="parent",
42
+ href="../catalog.json",
43
+ type="application/json",
44
+ title="Experiments",
45
+ hreflang="en-US",
46
+ created=None,
47
+ updated=None,
48
+ )
49
+ )
50
+ record_geojson.links.append( # type: ignore see osc_metadata_client.load_record_geojson
51
+ Link(
52
+ href=f"{ogc_api_processes_endpoint}/jobs/{record_geojson.id}",
53
+ hreflang="en-US",
54
+ rel="via",
55
+ type="application/json",
56
+ title=f"OGC API Processes - Job: {record_geojson.id}",
57
+ created=None,
58
+ updated=None,
59
+ )
60
+ )
61
+
62
+ logger.debug("Reassembling OGC API Records 'Experiment' inputs...")
63
+
64
+ status_info: StatusInfo = retrieve_status_info(
65
+ create_client(ogc_api_processes_endpoint, authorization_token),
66
+ record_geojson.id,
67
+ )
68
+
69
+ logger.debug(status_info.properties)
70
+
71
+ target_file = Path(output, f"experiments/{record_geojson.id}/record.json")
72
+ input_files: Path = Path(target_file.parent, "input.yaml")
73
+
74
+ serialize_yaml(status_info.inputs, input_files)
75
+
76
+ record_geojson.links.append( # type: ignore see osc_metadata_client.load_record_geojson
77
+ Link(
78
+ href=f"./{input_files.name}",
79
+ hreflang="en-US",
80
+ rel="input",
81
+ type="application/yaml",
82
+ title="Input parameters",
83
+ created=status_info.started,
84
+ updated=status_info.started,
85
+ )
86
+ )
87
+ record_geojson.links.append( # type: ignore see osc_metadata_client.load_record_geojson
88
+ Link(
89
+ href="./environment.yaml",
90
+ hreflang="en-US",
91
+ rel="environment",
92
+ type="application/yaml",
93
+ title="Execution environment",
94
+ created=status_info.started,
95
+ updated=status_info.started,
96
+ )
97
+ )
98
+ record_geojson.links.append( # type: ignore see osc_metadata_client.load_record_geojson
99
+ Link(
100
+ href=f"../../workflows/{workflow_id}/record.json",
101
+ hreflang="en-US",
102
+ rel="related",
103
+ type="application/json",
104
+ title=f"Workflow: {record_geojson.properties.title}",
105
+ created=status_info.started,
106
+ updated=status_info.started,
107
+ )
108
+ )
109
+
110
+ logger.success(
111
+ f"OGC API Records 'Experiment' inputs saved to {input_files.absolute()}"
112
+ )
113
+
114
+ record_geojson.properties.type = "experiment"
115
+ experiment_properties: ExperimentProperties = cast_model(
116
+ record_geojson.properties,
117
+ ExperimentProperties,
118
+ )
119
+ experiment_properties.osc_project = project_id
120
+ experiment_properties.osc_workflow = workflow_id
121
+ experiment_properties.osc_prov_described_by_workflow = workflow_id
122
+ # experiment_properties.osc_prov_generated = "TODO"
123
+ experiment_properties.osc_prov_generated_by = "osc-client"
124
+ experiment_properties.osc_prov_started_at_time = status_info.started
125
+ experiment_properties.osc_prov_ended_at_time = status_info.finished
126
+
127
+ record_geojson.properties = experiment_properties
128
+
129
+ logger.success("OGCP API Records enriched")
130
+
131
+ dump_data(
132
+ record_geojson.model_dump(
133
+ by_alias=True, exclude_none=True, serialize_as_any=True
134
+ ),
135
+ target_file,
136
+ )
@@ -0,0 +1,78 @@
1
+ # Copyright 2026 Terradue
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # generated by datamodel-codegen:
16
+ # filename: osc_ogcapi_records_models.yaml
17
+ # timestamp: 2026-03-26T09:49:14+00:00
18
+
19
+ from __future__ import annotations
20
+
21
+ from datetime import datetime
22
+ from enum import Enum
23
+ from typing import Any, Optional
24
+
25
+ from pydantic import Field, RootModel
26
+ from transpiler_mate.ogcapi.records.ogcapi_records_models import RecordCommonProperties
27
+
28
+
29
+ class Model(RootModel[Any]):
30
+ root: Any
31
+
32
+
33
+ class OscStatus(Enum):
34
+ PLANNED = "planned"
35
+ ONGOING = "ongoing"
36
+ COMPLETED = "completed"
37
+
38
+
39
+ class WorkflowProperties(RecordCommonProperties):
40
+ osc_type: Optional[str] = Field("workflow", alias="osc:type")
41
+ osc_project: Optional[str] = Field(None, alias="osc:project")
42
+ osc_status: Optional[OscStatus] = Field(None, alias="osc:status")
43
+
44
+
45
+ class ExperimentProperties(RecordCommonProperties):
46
+ osc_project: Optional[str] = Field(None, alias="osc:project")
47
+ osc_workflow: Optional[str] = Field(None, alias="osc:workflow")
48
+ osc_prov_generated_by: Optional[str] = Field(None, alias="osc-prov:generatedBy")
49
+ osc_prov_started_at_time: Optional[datetime] = Field(
50
+ None, alias="osc-prov:startedAtTime"
51
+ )
52
+ osc_prov_ended_at_time: Optional[datetime] = Field(
53
+ None, alias="osc-prov:endedAtTime"
54
+ )
55
+ osc_prov_generated: Optional[str] = Field(None, alias="osc-prov:generated")
56
+ osc_prov_described_by_workflow: Optional[str] = Field(
57
+ None, alias="osc-prov:describedByWorkflow"
58
+ )
59
+
60
+
61
+ class ProductProperties(RecordCommonProperties):
62
+ osc_experiment: Optional[str] = Field(None, alias="osc:experiment")
63
+ osc_status: Optional[str] = Field(None, alias="osc:status")
64
+ osc_region: Optional[str] = Field(None, alias="osc:region")
65
+ osc_type: Optional[str] = Field(None, alias="osc:type")
66
+ osc_project: Optional[str] = Field(None, alias="osc:project")
67
+ osc_missions: Optional[str] = Field(None, alias="osc:missions")
68
+ osc_variables: Optional[str] = Field(None, alias="osc:variables")
69
+ osc_prov_type: Optional[str] = Field(None, alias="osc-prov:type")
70
+ osc_prov_was_derived_from: Optional[str] = Field(
71
+ None, alias="osc-prov:wasDerivedFrom"
72
+ )
73
+ osc_prov_was_output_from: Optional[str] = Field(
74
+ None, alias="osc-prov:wasOutputFrom"
75
+ )
76
+ osc_prov_described_by_parameter: Optional[str] = Field(
77
+ None, alias="osc-prov:describedByParameter"
78
+ )