From e276b5f2830d20e79f423a2adec053ba0fa24fb7 Mon Sep 17 00:00:00 2001 From: Emile Sonneveld Date: Thu, 28 May 2026 17:25:54 +0200 Subject: [PATCH 1/4] Add extra stac validation to see if it passes CI. --- openeo_driver/views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openeo_driver/views.py b/openeo_driver/views.py index f0760564..6b0ad348 100644 --- a/openeo_driver/views.py +++ b/openeo_driver/views.py @@ -13,6 +13,7 @@ import flask import flask_cors +import pystac from flask import ( Blueprint, Flask, @@ -1122,6 +1123,8 @@ def job_results_canonical_url() -> str: } ], } + pystac_item = pystac.Collection.from_dict(result) + pystac_item.validate() return jsonify(result) with TimingLogger(f"backend_implementation.batch_jobs.get_result_metadata({job_id=}, {user_id=})", _log): @@ -1391,6 +1394,9 @@ def intersect_dicts(dict1, dict2): result["stac_extensions"].append(STAC_EXTENSION.PROJECTION_V120) # TODO "OpenEO-Costs" header? + + pystac_item = pystac.Collection.from_dict(result) # TODO: pystac.Item? + pystac_item.validate() return jsonify(result) # TODO: Issue #232, TBD: refactor download functionality? more abstract, just stream blocks of bytes from S3 or from a directory. @@ -1630,6 +1636,8 @@ def _get_job_result_item11(job_id, item_id, user_id): ) ) + pystac_item = pystac.Item.from_dict(stac_item) + pystac_item.validate() resp = jsonify(stac_item) resp.mimetype = stac_item_media_type return resp @@ -1808,6 +1816,8 @@ def _get_job_result_item(job_id, item_id, user_id): } ) ) + pystac_item = pystac.Item.from_dict(stac_item) + pystac_item.validate() resp = jsonify(stac_item) resp.mimetype = stac_item_media_type @@ -1837,6 +1847,9 @@ def _download_ml_model_metadata(job_id: str, file_name: str, user_id) -> flask.R 'links': ml_model_metadata.get("links", []), 'assets': ml_model_metadata.get("assets", {}) } + + pystac_item = pystac.Item.from_dict(stac_item) + pystac_item.validate() resp = jsonify(stac_item) resp.mimetype = stac_item_media_type return resp From 3685b8348e52e51663d49a305f01ccc3faf8044f Mon Sep 17 00:00:00 2001 From: Emile Sonneveld Date: Thu, 28 May 2026 17:41:17 +0200 Subject: [PATCH 2/4] pystac.Item --- openeo_driver/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo_driver/views.py b/openeo_driver/views.py index 6b0ad348..44fd3960 100644 --- a/openeo_driver/views.py +++ b/openeo_driver/views.py @@ -1395,7 +1395,7 @@ def intersect_dicts(dict1, dict2): # TODO "OpenEO-Costs" header? - pystac_item = pystac.Collection.from_dict(result) # TODO: pystac.Item? + pystac_item = pystac.Item.from_dict(result) # TODO: pystac.Item? pystac_item.validate() return jsonify(result) From 408dc1948ecab1648c2482a92a9d941f976e64aa Mon Sep 17 00:00:00 2001 From: Emile Sonneveld Date: Thu, 28 May 2026 18:05:23 +0200 Subject: [PATCH 3/4] fixes --- openeo_driver/views.py | 29 ++++++++++++++++++++--------- tests/test_views.py | 26 +++++++++++++++----------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/openeo_driver/views.py b/openeo_driver/views.py index 44fd3960..5d1282bb 100644 --- a/openeo_driver/views.py +++ b/openeo_driver/views.py @@ -869,17 +869,22 @@ def _properties_from_job_info(job_info: BatchJobMetadata) -> dict: "card4l:specification": "SR", "card4l:specification_version": "5.0", "processing:facility": get_backend_config().processing_facility, - "processing:software": get_backend_config().processing_software, + "processing:software": { + get_backend_config().processing_software: get_backend_config().capabilities_backend_version + }, } ) - properties["datetime"] = None - start_datetime = to_datetime(job_info.start_datetime) end_datetime = to_datetime(job_info.end_datetime) - if start_datetime == end_datetime: + if start_datetime is None and end_datetime is None: + # No temporal range available: fall back to job created time to produce a valid STAC item + # (STAC requires start_datetime+end_datetime when datetime is null) + properties["datetime"] = to_datetime(job_info.created) + elif start_datetime == end_datetime: properties["datetime"] = start_datetime else: + properties["datetime"] = None if start_datetime: properties["start_datetime"] = start_datetime if end_datetime: @@ -1336,8 +1341,9 @@ def intersect_dicts(dict1, dict2): "interval": [[to_datetime(job_info.start_datetime), to_datetime(job_info.end_datetime)]] }, }, - "summaries": {"instruments": job_info.instruments } if job_info.instruments else {}, - "providers": providers or None, + "summaries": {"instruments": job_info.instruments} if job_info.instruments else {}, + "providers": providers + or backend_implementation.batch_jobs._get_providers(job_id=job_id, user_id=user_id), "links": links, "assets": assets, "item_assets": item_assets, @@ -1381,7 +1387,6 @@ def intersect_dicts(dict1, dict2): result["stac_extensions"] = [ STAC_EXTENSION.PROCESSING, - STAC_EXTENSION.CARD4LOPTICAL, STAC_EXTENSION.FILEINFO, ] @@ -1395,8 +1400,14 @@ def intersect_dicts(dict1, dict2): # TODO "OpenEO-Costs" header? - pystac_item = pystac.Item.from_dict(result) # TODO: pystac.Item? - pystac_item.validate() + stac_type = result.get("type") + if stac_type == "Feature": + pystac_obj = pystac.Item.from_dict(result) + elif stac_type == "Collection": + pystac_obj = pystac.Collection.from_dict(result) + else: + pystac_obj = pystac.read_dict(result) + pystac_obj.validate() return jsonify(result) # TODO: Issue #232, TBD: refactor download functionality? more abstract, just stream blocks of bytes from S3 or from a directory. diff --git a/tests/test_views.py b/tests/test_views.py index ab8a40dd..8bc9522f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1948,17 +1948,18 @@ def test_get_job_results_100(self, api100): ], "properties": { "created": "2017-01-01T09:32:12Z", - "datetime": None, + "datetime": "2017-01-01T09:32:12Z", "card4l:processing_chain": {"process_graph": {"foo": {"process_id": "foo", "arguments": {}}}}, "card4l:specification": "SR", "card4l:specification_version": "5.0", "processing:facility": "Dummy openEO API", - "processing:software": "openeo-python-driver", + "processing:software": { + dummy_config.config.processing_software: dummy_config.config.capabilities_backend_version + }, }, "providers": EXPECTED_PROVIDERS, "stac_extensions": [ "https://stac-extensions.github.io/processing/v1.1.0/schema.json", - "https://stac-extensions.github.io/card4l/v0.1.0/optical/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/eo/v1.1.0/schema.json", ], @@ -2043,12 +2044,13 @@ def test_get_job_results_100(self, api100): "card4l:specification": "SR", "card4l:specification_version": "5.0", "processing:facility": "Dummy openEO API", - "processing:software": "openeo-python-driver", + "processing:software": { + dummy_config.config.processing_software: dummy_config.config.capabilities_backend_version + }, }, "providers": EXPECTED_PROVIDERS, "stac_extensions": [ "https://stac-extensions.github.io/processing/v1.1.0/schema.json", - "https://stac-extensions.github.io/card4l/v0.1.0/optical/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/projection/v1.2.0/schema.json", @@ -2394,17 +2396,18 @@ def test_get_job_results_signed_100(self, api100, flask_app, backend_config_over ], "properties": { "created": "2017-01-01T09:32:12Z", - "datetime": None, + "datetime": "2017-01-01T09:32:12Z", "card4l:processing_chain": {"process_graph": {"foo": {"process_id": "foo", "arguments": {}}}}, "card4l:specification": "SR", "card4l:specification_version": "5.0", "processing:facility": "Dummy openEO API", - "processing:software": "openeo-python-driver", + "processing:software": { + dummy_config.config.processing_software: dummy_config.config.capabilities_backend_version + }, }, "providers": EXPECTED_PROVIDERS, "stac_extensions": [ "https://stac-extensions.github.io/processing/v1.1.0/schema.json", - "https://stac-extensions.github.io/card4l/v0.1.0/optical/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/eo/v1.1.0/schema.json", ], @@ -2678,17 +2681,18 @@ def test_get_job_results_signed_with_expiration_100(self, api100, flask_app, bac ], "properties": { "created": "2017-01-01T09:32:12Z", - "datetime": None, + "datetime": "2017-01-01T09:32:12Z", "card4l:processing_chain": {"process_graph": {"foo": {"process_id": "foo", "arguments": {}}}}, "card4l:specification": "SR", "card4l:specification_version": "5.0", "processing:facility": "Dummy openEO API", - "processing:software": "openeo-python-driver", + "processing:software": { + dummy_config.config.processing_software: dummy_config.config.capabilities_backend_version + }, }, "providers": EXPECTED_PROVIDERS, "stac_extensions": [ "https://stac-extensions.github.io/processing/v1.1.0/schema.json", - "https://stac-extensions.github.io/card4l/v0.1.0/optical/schema.json", "https://stac-extensions.github.io/file/v2.1.0/schema.json", "https://stac-extensions.github.io/eo/v1.1.0/schema.json", ], From b055b76ee3c134e93f333fb91e652d8ce084fd25 Mon Sep 17 00:00:00 2001 From: Emile Sonneveld Date: Thu, 28 May 2026 18:22:59 +0200 Subject: [PATCH 4/4] more copilot fixes --- openeo_driver/views.py | 18 +++++++++++++++--- tests/test_views.py | 13 ++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/openeo_driver/views.py b/openeo_driver/views.py index 5d1282bb..baaf0112 100644 --- a/openeo_driver/views.py +++ b/openeo_driver/views.py @@ -1846,6 +1846,18 @@ def _download_ml_model_metadata(job_id: str, file_name: str, user_id) -> flask.R asset["href"] = backend_implementation.config.asset_url.build_url( asset_metadata=asset, asset_name=asset_file_name, job_id=job_id, user_id=user_id ) + links = [ + { + "rel": "self", + "href": url_for(".get_job_result_item", job_id=job_id, item_id=file_name, _external=True), + "type": stac_item_media_type, + }, + { + "rel": "collection", + "href": url_for(".list_job_results", job_id=job_id, _external=True), + "type": "application/json", + }, + ] + ml_model_metadata.get("links", []) stac_item = { "stac_version": ml_model_metadata.get("stac_version", "1.0.0"), "stac_extensions": ml_model_metadata.get("stac_extensions", []), @@ -1854,9 +1866,9 @@ def _download_ml_model_metadata(job_id: str, file_name: str, user_id) -> flask.R "collection": job_id, "bbox": ml_model_metadata.get("bbox", []), "geometry": ml_model_metadata.get("geometry", {}), - 'properties': ml_model_metadata.get("properties", {}), - 'links': ml_model_metadata.get("links", []), - 'assets': ml_model_metadata.get("assets", {}) + "properties": ml_model_metadata.get("properties", {}), + "links": links, + "assets": ml_model_metadata.get("assets", {}), } pystac_item = pystac.Item.from_dict(stac_item) diff --git a/tests/test_views.py b/tests/test_views.py index 8bc9522f..08aefa6a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -4139,7 +4139,18 @@ def test_download_ml_model_metadata(self, flask_app, api110, backend_config_over ], "type": "Polygon", }, - "links": [], + "links": [ + { + "rel": "self", + "href": "http://oeo.net/openeo/1.1.0/jobs/53c71345-09b4-46b4-b6b0-03fd6fe1f199/results/items/ml_model_metadata.json", + "type": "application/geo+json", + }, + { + "rel": "collection", + "href": "http://oeo.net/openeo/1.1.0/jobs/53c71345-09b4-46b4-b6b0-03fd6fe1f199/results", + "type": "application/json", + }, + ], "properties": { "datetime": None, "end_datetime": "9999-12-31T23:59:59Z",