Repository navigation
Add PRR collection generation; point OSC item assets to PRR (v0.2.0) #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
e9e6344
generate prr collection
TejasMorbagal d9a765d
updated docs
TejasMorbagal 3ac3d71
refactor
TejasMorbagal ff854a6
update CHANGES.md
TejasMorbagal b0a199e
cli for generate prr collection
TejasMorbagal f5d491e
pin ruff for now
TejasMorbagal 9981208
check changes
konstntokas 13252fb
check changes
konstntokas f6e01cc
mini error found
konstntokas 7e9f98d
error in time extent
konstntokas 79d3714
relative self href
konstntokas 604f57d
remove normalize_href
konstntokas 904d378
add absolute hrefs
konstntokas 8de4224
link changed
konstntokas 9352ade
links missiong
konstntokas a1cf6cf
allow cf spatial ref
konstntokas 334eca5
ready for review
konstntokas 9528fcd
ruff
konstntokas 7edfc00
CI fixed
konstntokas 5ac982f
add collection title
konstntokas 9240823
Merge pull request #24 from deepesdl/konstntokas/update_prr
konstntokas 896b741
add coord position parameter
konstntokas bf23ae7
change cube:dimension accoringly
konstntokas 5452869
adjut coord names for cube extension
konstntokas ab39367
adjut coord names for cube extension
konstntokas 7d6d0ad
adjut coord names for cube extension
konstntokas e122f67
fix tests
konstntokas 04765d2
added tests for coverage
konstntokas 47a3fba
Update deep_code/utils/dataset_stac_generator.py
konstntokas 28672ac
Update deep_code/utils/dataset_stac_generator.py
konstntokas 47c907f
Update deep_code/utils/dataset_stac_generator.py
konstntokas 878cf00
Update deep_code/tools/prr.py
konstntokas 3256ab2
Update deep_code/tools/new.py
konstntokas 1a3b755
Update deep_code/utils/dataset_stac_generator.py
konstntokas 4e21ce7
Merge pull request #25 from deepesdl/konstntokas/update_prr
konstntokas a52712f
backup
konstntokas e898b0f
add file size and extensions
konstntokas 8146dd8
debug
konstntokas 1fa680c
file size added
konstntokas 496fad9
ready for review
konstntokas 1993749
remove test files
konstntokas b1c94ad
adjusted relate path
konstntokas 45dda78
extract data var names correctly
konstntokas 7ef83f4
rename dataset_status to osc_status
konstntokas 7c45310
remove printing
konstntokas ed9c065
upated docs
TejasMorbagal 2a9a1f5
Merge pull request #26 from deepesdl/konstntokas/final_check_prr
konstntokas 8a5bbcf
multiple fixes: osc stac item point to prr, renamed dataset_status fi…
TejasMorbagal be966ad
updated version
TejasMorbagal 016459e
updated docs
TejasMorbagal eabf41b
fix test
TejasMorbagal d9a44ad
report clear message when items_config is missing entries
TejasMorbagal cef61cd
stac catalog s3 root is optional, PRR link is the default
TejasMorbagal 084c164
Allow https Zarr URLs as dataset_id in publish,
TejasMorbagal 60a3288
updated docs
TejasMorbagal 99cf418
Fixed `publish` overwriting upstream OSC catalogs
TejasMorbagal de927a7
fix self link in publish
TejasMorbagal ff6599a
fix validation failures
TejasMorbagal 8a272cc
Update deep_code/utils/dataset_stac_generator.py
TejasMorbagal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) 2025 by Brockmann Consult GmbH | ||
| # Permissions are hereby granted under the terms of the MIT License: | ||
| # https://opensource.org/licenses/MIT. | ||
|
|
||
| import click | ||
|
|
||
| from deep_code.tools.prr import generate_prr_collection | ||
|
|
||
|
|
||
| @click.command(name="generate-prr-collection") | ||
| @click.argument("dataset_config", type=click.Path(exists=True)) | ||
| @click.option( | ||
| "--output-dir", | ||
| "-o", | ||
| type=click.Path(file_okay=False), | ||
| default=None, | ||
| help=( | ||
| "Directory to write the PRR collection tree into " | ||
| "(default: 'prr_output_dir' from the config, else 'prr/<collection_id>')." | ||
| ), | ||
| ) | ||
| def generate_prr_collection_cmd(dataset_config, output_dir): | ||
| """Generate a PRR-style STAC Collection as local files. | ||
|
|
||
| Reads a dataset config (the same YAML used by 'publish') and writes a | ||
| self-contained Collection -> Item -> Assets tree for submission to the | ||
| ESA EarthCODE Project Results Repository. | ||
|
|
||
| Example: | ||
| deep-code generate-prr-collection dataset-config.yaml -o ./prr | ||
| """ | ||
| out_dir = generate_prr_collection(dataset_config, output_dir=output_dir) | ||
| click.echo(f"PRR collection written to: {out_dir}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) 2025 by Brockmann Consult GmbH | ||
| # Permissions are hereby granted under the terms of the MIT License: | ||
| # https://opensource.org/licenses/MIT. | ||
|
|
||
| import os | ||
| import tempfile | ||
| import unittest | ||
|
|
||
|
|
||
| from deep_code.tools.new import TemplateGenerator | ||
|
|
||
|
|
||
| class TestTemplateGenerator(unittest.TestCase): | ||
| def test_generate_dataset_template_writes_file(self): | ||
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: | ||
| path = f.name | ||
| try: | ||
| TemplateGenerator.generate_dataset_template(output_path=path) | ||
| with open(path) as f: | ||
| content = f.read() | ||
| finally: | ||
| os.unlink(path) | ||
|
|
||
| # Required fields present | ||
| self.assertIn("dataset_id", content) | ||
| self.assertIn("collection_id", content) | ||
| self.assertIn("license_type", content) | ||
| self.assertIn("stac_catalog_s3_root", content) | ||
| # Optional fields present | ||
| self.assertIn("osc_themes", content) | ||
| self.assertIn("visualisation_link", content) | ||
| self.assertIn("osc_project_title", content) | ||
| # Section headers | ||
| self.assertIn("REQUIRED", content) | ||
| self.assertIn("OPTIONAL", content) | ||
|
|
||
| def test_generate_workflow_template_writes_file(self): | ||
| with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: | ||
| path = f.name | ||
| try: | ||
| TemplateGenerator.generate_workflow_template(output_path=path) | ||
| with open(path) as f: | ||
| content = f.read() | ||
| finally: | ||
| os.unlink(path) | ||
|
|
||
| # Required fields present | ||
| self.assertIn("workflow_id", content) | ||
| self.assertIn("title", content) | ||
| self.assertIn("license", content) | ||
| # Optional fields present | ||
| self.assertIn("jupyter_notebook_url", content) | ||
| self.assertIn("contact", content) | ||
| # Section headers | ||
| self.assertIn("REQUIRED", content) | ||
| self.assertIn("OPTIONAL", content) | ||
|
|
||
| def test_generate_dataset_template_no_path_returns_none(self): | ||
| result = TemplateGenerator.generate_dataset_template() | ||
| self.assertIsNone(result) | ||
|
|
||
| def test_generate_workflow_template_no_path_returns_none(self): | ||
| result = TemplateGenerator.generate_workflow_template() | ||
| self.assertIsNone(result) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) 2025 by Brockmann Consult GmbH | ||
| # Permissions are hereby granted under the terms of the MIT License: | ||
| # https://opensource.org/licenses/MIT. | ||
|
|
||
| import json | ||
| import os | ||
| import tempfile | ||
| import unittest | ||
| from datetime import datetime | ||
| from unittest.mock import patch | ||
|
|
||
| import numpy as np | ||
| import yaml | ||
| from xarray import Dataset | ||
|
|
||
| from deep_code.tools.prr import generate_prr_collection | ||
|
|
||
|
|
||
| def _make_dataset(): | ||
| return Dataset( | ||
| coords={ | ||
| "lon": ("lon", np.linspace(-20, 20, 4)), | ||
| "lat": ("lat", np.linspace(-10, 10, 3)), | ||
| "time": ("time", [np.datetime64(datetime(2021, 1, 1), "ns")]), | ||
| }, | ||
| data_vars={ | ||
| "sst": ( | ||
| ("time", "lat", "lon"), | ||
| np.random.rand(1, 3, 4), | ||
| {"units": "K", "long_name": "Sea surface temperature"}, | ||
| ) | ||
| }, | ||
| attrs={ | ||
| "description": "PRR tool test cube", | ||
| # set by open_dataset from the store's file sizes | ||
| "size": 123456, | ||
| "metadata_size": 123, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| class TestGeneratePrrCollection(unittest.TestCase): | ||
| def _write_config(self, tmp, **overrides): | ||
| config = { | ||
| "collection_id": "tool-prr", | ||
| "items_config": [{"dataset_id": "test.zarr", "item_id": "tool-item"}], | ||
| "license_type": "CC-BY-4.0", | ||
| "osc_project": "tool-project", | ||
| "osc_project_url": "https://project.example.org", | ||
| "osc_status": "ongoing", | ||
| "osc_region": "Global", | ||
| "osc_themes": ["oceans"], | ||
| "documentation_link": "https://example.org", | ||
| } | ||
| config.update(overrides) | ||
| path = os.path.join(tmp, "dataset-config.yaml") | ||
| with open(path, "w") as f: | ||
| yaml.safe_dump(config, f) | ||
| return path | ||
|
|
||
| @patch( | ||
| "deep_code.utils.dataset_stac_generator.open_dataset", | ||
| side_effect=lambda *args, **kwargs: _make_dataset(), | ||
| ) | ||
| def test_writes_collection_tree(self, _mock_open_ds): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| config_path = self._write_config(tmp) | ||
| out = os.path.join(tmp, "out") | ||
| result = generate_prr_collection(config_path, output_dir=out) | ||
|
|
||
| self.assertEqual(result, out) | ||
| coll_path = os.path.join(out, "tool-prr", "collection.json") | ||
| item_path = os.path.join(out, "tool-prr", "items", "tool-item.json") | ||
| self.assertTrue(os.path.isfile(coll_path)) | ||
| self.assertTrue(os.path.isfile(item_path)) | ||
| with open(coll_path) as f: | ||
| coll = json.load(f) | ||
| self.assertEqual(coll["id"], "tool-prr") | ||
| self.assertEqual(coll["type"], "Collection") | ||
| self.assertEqual(coll["osc:type"], "product") | ||
| self.assertEqual(coll["osc:status"], "ongoing") | ||
| with open(item_path) as f: | ||
| item = json.load(f) | ||
| # PRR ingests the Zarr next to the item, so asset hrefs are relative. | ||
| self.assertEqual(item["assets"]["zarr-data"]["href"], "./test.zarr") | ||
|
|
||
| @patch( | ||
| "deep_code.utils.dataset_stac_generator.open_dataset", | ||
| side_effect=lambda *args, **kwargs: _make_dataset(), | ||
| ) | ||
| def test_default_output_dir_from_collection_id(self, _mock_open_ds): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| config_path = self._write_config(tmp) | ||
| cwd = os.getcwd() | ||
| os.chdir(tmp) | ||
| try: | ||
| out = generate_prr_collection(config_path) | ||
| self.assertEqual(out, "prr") | ||
| self.assertTrue( | ||
| os.path.isfile(os.path.join(out, "tool-prr", "collection.json")) | ||
| ) | ||
| finally: | ||
| os.chdir(cwd) | ||
|
|
||
| @patch( | ||
| "deep_code.utils.dataset_stac_generator.open_dataset", | ||
| side_effect=lambda *args, **kwargs: _make_dataset(), | ||
| ) | ||
| def test_output_dir_from_config(self, _mock_open_ds): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| out = os.path.join(tmp, "from-config") | ||
| config_path = self._write_config(tmp, prr_output_dir=out) | ||
| result = generate_prr_collection(config_path) | ||
| self.assertEqual(result, out) | ||
| self.assertTrue( | ||
| os.path.isfile(os.path.join(out, "tool-prr", "collection.json")) | ||
| ) | ||
|
|
||
| @patch( | ||
| "deep_code.utils.dataset_stac_generator.open_dataset", | ||
| side_effect=lambda *args, **kwargs: _make_dataset(), | ||
| ) | ||
| def test_prr_spec_fields_plumbed_from_config(self, _mock_open_ds): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| out = os.path.join(tmp, "out") | ||
| config_path = self._write_config( | ||
| tmp, | ||
| osc_missions=["sentinel-3"], | ||
| osc_project_description="Project description.", | ||
| osc_project_website="https://project.example.org", | ||
| osc_contract_number="4000114410/15/NL/BW", | ||
| thumbnail="https://example.org/logo.jpeg", | ||
| sci_doi="10.1000/xyz123", | ||
| ) | ||
| generate_prr_collection(config_path, output_dir=out) | ||
| with open(os.path.join(out, "tool-prr", "collection.json")) as f: | ||
| coll = json.load(f) | ||
| self.assertEqual(coll["osc:initiative"], "earthcode") | ||
| self.assertEqual(coll["osc:project_website"], "https://project.example.org") | ||
| self.assertEqual(coll["osc:project_description"], "Project description.") | ||
| self.assertEqual(coll["osc:contract-number"], "4000114410/15/NL/BW") | ||
| self.assertEqual(coll["sci:doi"], "10.1000/xyz123") | ||
| self.assertIn("thumbnail", coll["assets"]) | ||
| self.assertEqual(coll["assets"]["thumbnail"]["roles"], ["thumbnail"]) | ||
|
|
||
| @patch( | ||
| "deep_code.utils.dataset_stac_generator.open_dataset", | ||
| side_effect=lambda *args, **kwargs: _make_dataset(), | ||
| ) | ||
| def test_deprecated_dataset_status(self, _mock_open_ds): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| out = os.path.join(tmp, "out") | ||
| config_path = self._write_config(tmp, osc_status=None) | ||
| with open(config_path) as f: | ||
| config = yaml.safe_load(f) | ||
| del config["osc_status"] | ||
| config["dataset_status"] = "planned" | ||
| with open(config_path, "w") as f: | ||
| yaml.safe_dump(config, f) | ||
| generate_prr_collection(config_path, output_dir=out) | ||
| with open(os.path.join(out, "tool-prr", "collection.json")) as f: | ||
| coll = json.load(f) | ||
| self.assertEqual(coll["osc:status"], "planned") | ||
|
|
||
| def _assert_missing_raises(self, field: str): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| config_path = self._write_config(tmp) | ||
| with open(config_path) as f: | ||
| config = yaml.safe_load(f) | ||
| del config[field] | ||
| with open(config_path, "w") as f: | ||
| yaml.safe_dump(config, f) | ||
| with self.assertRaisesRegex(ValueError, f"{field} is required"): | ||
| generate_prr_collection(config_path) | ||
|
|
||
| def test_items_config_entry_missing_dataset_id_raises(self): | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| config_path = self._write_config( | ||
| tmp, items_config=[{"item_id": "tool-item"}] | ||
| ) | ||
| with self.assertRaisesRegex( | ||
| ValueError, "dataset_id is required in items_config entry 0" | ||
| ): | ||
| generate_prr_collection(config_path) | ||
|
|
||
| def test_missing_required_fields_raise(self): | ||
| for field in ( | ||
| "collection_id", | ||
| "osc_project", | ||
| "osc_project_url", | ||
| "license_type", | ||
| "items_config", | ||
| ): | ||
| with self.subTest(field=field): | ||
| self._assert_missing_raises(field) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Log change is quiet massive. Maybe summarize a bit.