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.
- osc_metadata_client/__about__.py +15 -0
- osc_metadata_client/__init__.py +275 -0
- osc_metadata_client/cli.py +161 -0
- osc_metadata_client/experiment.py +136 -0
- osc_metadata_client/models.py +78 -0
- osc_metadata_client/osc_extension.py +222 -0
- osc_metadata_client/product.py +151 -0
- osc_metadata_client/themes_extension.py +236 -0
- osc_metadata_client/workflow.py +92 -0
- osc_metadata_client-0.3.0.dist-info/METADATA +44 -0
- osc_metadata_client-0.3.0.dist-info/RECORD +14 -0
- osc_metadata_client-0.3.0.dist-info/WHEEL +4 -0
- osc_metadata_client-0.3.0.dist-info/entry_points.txt +2 -0
- osc_metadata_client-0.3.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|
+
)
|