earthcode 0.1.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.
- earthcode/__init__.py +0 -0
- earthcode/fairtool.py +577 -0
- earthcode/git_add.py +383 -0
- earthcode/gitclerk_add.py +21 -0
- earthcode/metadata_input_definitions.py +338 -0
- earthcode/search.py +209 -0
- earthcode/static.py +569 -0
- earthcode/validator.py +605 -0
- earthcode-0.1.0.dist-info/METADATA +70 -0
- earthcode-0.1.0.dist-info/RECORD +12 -0
- earthcode-0.1.0.dist-info/WHEEL +4 -0
- earthcode-0.1.0.dist-info/licenses/LICENSE +21 -0
earthcode/git_add.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import pystac
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
REMOTE_URL = 'https://esa-earthcode.github.io/open-science-catalog-metadata/'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _add_link_if_missing(stac_obj: pystac.STACObject, link: pystac.Link) -> None:
|
|
10
|
+
"""Adds a link only when no existing link has the same rel and href."""
|
|
11
|
+
|
|
12
|
+
for existing in stac_obj.get_links():
|
|
13
|
+
if existing.rel == link.rel and existing.href == link.href:
|
|
14
|
+
return
|
|
15
|
+
stac_obj.add_link(link)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require_product_field(product_dict: Mapping[str, Any], key: str) -> Any:
|
|
19
|
+
"""Ensures a required product metadata key exists and is non-empty."""
|
|
20
|
+
|
|
21
|
+
value = product_dict.get(key)
|
|
22
|
+
if value is None:
|
|
23
|
+
raise ValueError(f"Missing required product field: {key}")
|
|
24
|
+
if isinstance(value, (list, str, dict)) and len(value) == 0:
|
|
25
|
+
raise ValueError(f"Empty required product field: {key}")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _collection_to_dict(
|
|
30
|
+
collection: dict[str, Any] | pystac.Collection, context_name: str
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Normalizes a collection input to a dictionary representation."""
|
|
33
|
+
|
|
34
|
+
if isinstance(collection, pystac.Collection):
|
|
35
|
+
return collection.to_dict()
|
|
36
|
+
if isinstance(collection, dict):
|
|
37
|
+
return collection
|
|
38
|
+
raise TypeError(f"{context_name} must be a dict or pystac.Collection")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_catalog_with_remote_selfhref(
|
|
42
|
+
catalog_object: pystac.Catalog,
|
|
43
|
+
local_catalog_path: Path,
|
|
44
|
+
catalog_extension: str,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Saves a catalog JSON file while forcing the self link to the configured remote OSC URL."""
|
|
47
|
+
|
|
48
|
+
# set remote href
|
|
49
|
+
remote_catalog_path = REMOTE_URL + catalog_extension
|
|
50
|
+
|
|
51
|
+
# overwrite self reference to be the online one
|
|
52
|
+
catalog_dict = catalog_object.to_dict()
|
|
53
|
+
for link in catalog_dict['links']:
|
|
54
|
+
if link['rel'] == 'self':
|
|
55
|
+
link['href'] = remote_catalog_path
|
|
56
|
+
|
|
57
|
+
with open(local_catalog_path, 'w', encoding='utf-8') as f:
|
|
58
|
+
json.dump(catalog_dict, f, indent=2, ensure_ascii=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# save project to catalog
|
|
62
|
+
def save_project_collection_to_osc(
|
|
63
|
+
project_collection: dict[str, Any] | pystac.Collection,
|
|
64
|
+
catalog_root: Path,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Writes a project collection into the local OSC tree and links it from the projects catalog."""
|
|
67
|
+
|
|
68
|
+
project_dict = _collection_to_dict(project_collection, "project_collection")
|
|
69
|
+
project_id = project_dict["id"]
|
|
70
|
+
project_title = project_dict.get("title")
|
|
71
|
+
|
|
72
|
+
# create a directory under /projects with the same ID as the project ID
|
|
73
|
+
project_dir = catalog_root / 'projects' / project_id
|
|
74
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
# save the collection in the new folder
|
|
77
|
+
with open(project_dir / 'collection.json', 'w', encoding='utf-8') as f:
|
|
78
|
+
json.dump(project_dict, f, indent=2, ensure_ascii=False)
|
|
79
|
+
|
|
80
|
+
# create a link from the parent Projects catalog to the new item.
|
|
81
|
+
catalog_extension = 'projects/catalog.json'
|
|
82
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
83
|
+
projects_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
84
|
+
_add_link_if_missing(
|
|
85
|
+
projects_catalog,
|
|
86
|
+
pystac.Link(
|
|
87
|
+
rel='child',
|
|
88
|
+
target=f'./{project_id}/collection.json',
|
|
89
|
+
media_type="application/json",
|
|
90
|
+
title=project_title
|
|
91
|
+
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
save_catalog_with_remote_selfhref(projects_catalog, local_catalog_path, catalog_extension)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def save_product_collection_to_catalog(
|
|
100
|
+
product_collection: dict[str, Any] | pystac.Collection,
|
|
101
|
+
catalog_root: Path,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Writes a product collection and updates all related reverse links in projects, themes, variables, and missions catalogs."""
|
|
104
|
+
|
|
105
|
+
product_dict = _collection_to_dict(product_collection, "product_collection")
|
|
106
|
+
product_id = product_dict["id"]
|
|
107
|
+
product_title = product_dict.get("title")
|
|
108
|
+
project_id = _require_product_field(product_dict, 'osc:project')
|
|
109
|
+
product_themes = [p['concepts'][0]['id'] for p in _require_product_field(product_dict, 'themes')]
|
|
110
|
+
product_variables = [v for v in _require_product_field(product_dict, 'osc:variables')]
|
|
111
|
+
product_missions = [m for m in _require_product_field(product_dict, 'osc:missions')]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# create a directory under /projects with the same ID as the project ID
|
|
115
|
+
product_dir = catalog_root / 'products' / product_id
|
|
116
|
+
product_dir.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# create a link from the parent products catalog to the new item.
|
|
120
|
+
catalog_extension = 'products/catalog.json'
|
|
121
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
122
|
+
products_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
123
|
+
_add_link_if_missing(
|
|
124
|
+
products_catalog,
|
|
125
|
+
pystac.Link(
|
|
126
|
+
rel='child',
|
|
127
|
+
target=f'./{product_id}/collection.json',
|
|
128
|
+
media_type="application/json",
|
|
129
|
+
title=f'{product_title}'
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
save_catalog_with_remote_selfhref(products_catalog, local_catalog_path, catalog_extension)
|
|
133
|
+
|
|
134
|
+
# add product to project Collection
|
|
135
|
+
with open(catalog_root / f'projects/{project_id}/collection.json') as f:
|
|
136
|
+
project_collection = json.load(f)
|
|
137
|
+
project_collection = pystac.Collection.from_dict(project_collection,
|
|
138
|
+
migrate=False,
|
|
139
|
+
root=None,
|
|
140
|
+
preserve_dict=True)
|
|
141
|
+
_add_link_if_missing(
|
|
142
|
+
project_collection,
|
|
143
|
+
pystac.Link(
|
|
144
|
+
rel='child',
|
|
145
|
+
target=f'../../products/{product_id}/collection.json',
|
|
146
|
+
media_type="application/json",
|
|
147
|
+
title=f'{product_title}'
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
with open(catalog_root / f'projects/{project_id}/collection.json', 'w') as f:
|
|
151
|
+
json.dump(
|
|
152
|
+
project_collection.to_dict(include_self_link=False, transform_hrefs=False),
|
|
153
|
+
f, ensure_ascii=False, indent=2)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# add theme return links
|
|
157
|
+
for theme in product_themes:
|
|
158
|
+
catalog_extension = f'themes/{theme}/catalog.json'
|
|
159
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
160
|
+
theme_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
161
|
+
_add_link_if_missing(
|
|
162
|
+
theme_catalog,
|
|
163
|
+
pystac.Link(
|
|
164
|
+
rel='child',
|
|
165
|
+
target=f'../../products/{product_id}/collection.json',
|
|
166
|
+
media_type="application/json",
|
|
167
|
+
title=f'{product_title}'
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
save_catalog_with_remote_selfhref(theme_catalog, local_catalog_path, catalog_extension)
|
|
171
|
+
|
|
172
|
+
# add variable return links
|
|
173
|
+
for var in product_variables:
|
|
174
|
+
catalog_extension = f'variables/{var}/catalog.json'
|
|
175
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
176
|
+
var_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
177
|
+
_add_link_if_missing(
|
|
178
|
+
var_catalog,
|
|
179
|
+
pystac.Link(
|
|
180
|
+
rel='child',
|
|
181
|
+
target=f'../../products/{product_id}/collection.json',
|
|
182
|
+
media_type="application/json",
|
|
183
|
+
title=f'{product_title}'
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
save_catalog_with_remote_selfhref(var_catalog, local_catalog_path, catalog_extension)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# add mission return links
|
|
190
|
+
for mission in product_missions:
|
|
191
|
+
catalog_extension = f'eo-missions/{mission}/catalog.json'
|
|
192
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
193
|
+
mission_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
194
|
+
_add_link_if_missing(
|
|
195
|
+
mission_catalog,
|
|
196
|
+
pystac.Link(
|
|
197
|
+
rel='child',
|
|
198
|
+
target=f'../../products/{product_id}/collection.json',
|
|
199
|
+
media_type="application/json",
|
|
200
|
+
title=f'{product_title}'
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
save_catalog_with_remote_selfhref(mission_catalog, local_catalog_path, catalog_extension)
|
|
204
|
+
|
|
205
|
+
# update link titles
|
|
206
|
+
for link in product_dict.get("links", []):
|
|
207
|
+
if link.get("rel") != "related":
|
|
208
|
+
continue
|
|
209
|
+
href = link.get("href", "")
|
|
210
|
+
link_elements = href.split('/')
|
|
211
|
+
if len(link_elements) > 3 and link_elements[2] in ['variables', 'eo-missions']:
|
|
212
|
+
catalog_title = pystac.Catalog.from_file(
|
|
213
|
+
catalog_root / f'{link_elements[2]}/{link_elements[3]}/catalog.json'
|
|
214
|
+
).title
|
|
215
|
+
prefix = 'Variable: ' if link_elements[2] == 'variables' else 'EO Mission: '
|
|
216
|
+
link["title"] = prefix + catalog_title
|
|
217
|
+
|
|
218
|
+
# save the collection in the new folder
|
|
219
|
+
with open(product_dir / 'collection.json', 'w', encoding='utf-8') as f:
|
|
220
|
+
json.dump(product_dict, f, indent=2, ensure_ascii=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def save_workflow_record_to_osc(
|
|
224
|
+
workflow_record: dict[str, Any], catalog_root: Path
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Writes a workflow record into the local OSC tree and links it from the workflows catalog."""
|
|
227
|
+
|
|
228
|
+
# create a directory under /projects with the same ID as the project ID
|
|
229
|
+
wf_dir = catalog_root / 'workflows' / workflow_record['id']
|
|
230
|
+
wf_dir.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
# save the record in the new folder
|
|
233
|
+
with open(wf_dir / 'record.json', 'w', encoding='utf-8') as f:
|
|
234
|
+
json.dump(workflow_record, f, indent=2, ensure_ascii=False)
|
|
235
|
+
|
|
236
|
+
# create a link from the parent Projects catalog to the new item.
|
|
237
|
+
catalog_extension = 'workflows/catalog.json'
|
|
238
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
239
|
+
wf_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
240
|
+
_add_link_if_missing(
|
|
241
|
+
wf_catalog,
|
|
242
|
+
pystac.Link(
|
|
243
|
+
rel='item',
|
|
244
|
+
target=f"./{workflow_record['id']}/record.json",
|
|
245
|
+
media_type="application/json",
|
|
246
|
+
title=workflow_record['properties']['title']
|
|
247
|
+
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
save_catalog_with_remote_selfhref(wf_catalog, local_catalog_path, catalog_extension)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def save_experiment_record_to_osc(
|
|
254
|
+
experiment_record: dict[str, Any], catalog_root: Path
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Writes an experiment record into the local OSC tree and links it from the experiments catalog."""
|
|
257
|
+
|
|
258
|
+
# create a directory under /projects with the same ID as the project ID
|
|
259
|
+
experiment_dir = catalog_root / 'experiments' / experiment_record['id']
|
|
260
|
+
experiment_dir.mkdir(parents=True, exist_ok=True)
|
|
261
|
+
|
|
262
|
+
# save the record in the new folder
|
|
263
|
+
with open(experiment_dir / 'record.json', 'w', encoding='utf-8') as f:
|
|
264
|
+
json.dump(experiment_record, f, indent=2, ensure_ascii=False)
|
|
265
|
+
|
|
266
|
+
# create a link from the parent Projects catalog to the new item.
|
|
267
|
+
catalog_extension = 'experiments/catalog.json'
|
|
268
|
+
local_catalog_path = catalog_root / catalog_extension
|
|
269
|
+
experiments_catalog = pystac.Catalog.from_file(local_catalog_path)
|
|
270
|
+
_add_link_if_missing(
|
|
271
|
+
experiments_catalog,
|
|
272
|
+
pystac.Link(
|
|
273
|
+
rel='item',
|
|
274
|
+
target=f"./{experiment_record['id']}/record.json",
|
|
275
|
+
media_type="application/json",
|
|
276
|
+
title=experiment_record['properties']['title']
|
|
277
|
+
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
save_catalog_with_remote_selfhref(experiments_catalog, local_catalog_path, catalog_extension)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def save_item_to_product_collection(
|
|
284
|
+
item: pystac.Item, product_collection: pystac.Collection | str, catalog_root: Path
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Adds parent and collection links to an item and saves it under the target product directory."""
|
|
287
|
+
|
|
288
|
+
if type(product_collection) is str:
|
|
289
|
+
with open(catalog_root/f'products/{product_collection}/collection.json', 'r', encoding='utf-8') as f:
|
|
290
|
+
product_collection = json.load(f)
|
|
291
|
+
product_collection = pystac.Collection.from_dict(product_collection,
|
|
292
|
+
migrate=False,
|
|
293
|
+
root=None,
|
|
294
|
+
preserve_dict=True)
|
|
295
|
+
|
|
296
|
+
item.add_link(pystac.Link.from_dict(
|
|
297
|
+
{
|
|
298
|
+
"rel": "collection",
|
|
299
|
+
"href": "./collection.json",
|
|
300
|
+
"type": "application/json",
|
|
301
|
+
"title": product_collection.title
|
|
302
|
+
}
|
|
303
|
+
))
|
|
304
|
+
|
|
305
|
+
item.add_link(pystac.Link.from_dict({
|
|
306
|
+
"rel": "parent",
|
|
307
|
+
"href": "./collection.json",
|
|
308
|
+
"type": "application/json",
|
|
309
|
+
"title": product_collection.title
|
|
310
|
+
},
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
item.save_object(
|
|
315
|
+
include_self_link=False,
|
|
316
|
+
dest_href=catalog_root/f'products/{product_collection.id}/{item.id}.json'
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# add to product collection if not already existing
|
|
320
|
+
with open(catalog_root / f'products/{product_collection.id}/collection.json') as f:
|
|
321
|
+
existing_product_collection = json.load(f)
|
|
322
|
+
existing_product_collection = pystac.Collection.from_dict(existing_product_collection,
|
|
323
|
+
migrate=False,
|
|
324
|
+
root=None,
|
|
325
|
+
preserve_dict=True)
|
|
326
|
+
_add_link_if_missing(
|
|
327
|
+
existing_product_collection,
|
|
328
|
+
pystac.Link(rel="item", target=f"./{item.id}.json", media_type="application/json", title=item.assets['data'].title)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
with open(catalog_root / f'products/{product_collection.id}/collection.json', 'w', encoding='utf-8') as f:
|
|
332
|
+
json.dump(
|
|
333
|
+
existing_product_collection.to_dict(include_self_link=False, transform_hrefs=False),
|
|
334
|
+
f, ensure_ascii=False, indent=2)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def save_item_links_to_product_collection(catalog_root: Path, product_id: str, item_link: str, access_link: str=None, documentation_link: str=None):
|
|
339
|
+
"""Adds links to an existing product collection"""
|
|
340
|
+
|
|
341
|
+
with open(catalog_root/f'products/{product_id}/collection.json', 'r', encoding='utf-8') as f:
|
|
342
|
+
product_collection = json.load(f)
|
|
343
|
+
product_collection = pystac.Collection.from_dict(product_collection,
|
|
344
|
+
migrate=False,
|
|
345
|
+
root=None,
|
|
346
|
+
preserve_dict=True)
|
|
347
|
+
links = [
|
|
348
|
+
pystac.Link.from_dict({
|
|
349
|
+
"rel": "child",
|
|
350
|
+
"href": item_link,
|
|
351
|
+
"type": "application/json",
|
|
352
|
+
"title": "PRR Data Collection"
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
if documentation_link:
|
|
358
|
+
links.append(
|
|
359
|
+
pystac.Link.from_dict({
|
|
360
|
+
"rel": "via",
|
|
361
|
+
"href": documentation_link,
|
|
362
|
+
"type": "application/json",
|
|
363
|
+
"title": "Documentation"
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if access_link:
|
|
369
|
+
links.append(
|
|
370
|
+
pystac.Link.from_dict({
|
|
371
|
+
"rel": "via",
|
|
372
|
+
"href": access_link,
|
|
373
|
+
"type": "application/json",
|
|
374
|
+
"title": "Access"
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
product_collection.add_links(links)
|
|
380
|
+
with open(catalog_root / f'products/{product_collection.id}/collection.json', 'w', encoding='utf-8') as f:
|
|
381
|
+
json.dump(
|
|
382
|
+
product_collection.to_dict(include_self_link=False, transform_hrefs=False),
|
|
383
|
+
f, ensure_ascii=False, indent=2)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
import pystac
|
|
5
|
+
|
|
6
|
+
def generate_osc_editor_link(json_object, object_type, session_title=None) -> None:
|
|
7
|
+
|
|
8
|
+
if type(json_object) is pystac.Collection:
|
|
9
|
+
json_object = json_object.to_dict()
|
|
10
|
+
|
|
11
|
+
if session_title is None:
|
|
12
|
+
session_title = json_object['title']
|
|
13
|
+
session_title = quote(session_title, safe="")
|
|
14
|
+
# Use URL-safe base64 encoding (replaces + with - and / with _)
|
|
15
|
+
|
|
16
|
+
base64_content = base64.urlsafe_b64encode(json.dumps(json_object).encode("utf-8")).decode("utf-8")
|
|
17
|
+
|
|
18
|
+
# https://workspace.earthcode-staging.earthcode.eox.at/osc-editor?session=<your session title, e.g. "Add File">&automation=add-file&type=<osc type, e.g. "product">&file=<base64encoded content>
|
|
19
|
+
url = f"https://workspace.earthcode-staging.earthcode.eox.at/osc-editor?session={session_title}&automation=add-file&&type={object_type}&file={base64_content}"
|
|
20
|
+
|
|
21
|
+
return url
|