From 021772c17c3c87f3c092942c0c9affc0c4487b47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:11:12 +0000 Subject: [PATCH 1/3] Initial plan From ee402ae71932d33059ab875f1d9e78a157d19d78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:19:54 +0000 Subject: [PATCH 2/3] Add on_response_headers to DataCube.execute and VectorCube.execute, log OpenEO-Identifier Agent-Logs-Url: https://github.com/Open-EO/openeo-python-client/sessions/72ac5e9e-be54-4fc5-acef-10648287ee35 Co-authored-by: soxofaan <44946+soxofaan@users.noreply.github.com> --- CHANGELOG.md | 3 ++ openeo/rest/connection.py | 4 +++ openeo/rest/datacube.py | 16 +++++++++-- openeo/rest/vectorcube.py | 4 +-- tests/rest/datacube/test_datacube100.py | 8 ++++++ tests/rest/datacube/test_vectorcube.py | 7 +++++ tests/rest/test_connection.py | 38 +++++++++++++++++++++++++ 7 files changed, 76 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4310324..1076cecaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add public `split_area` function for tile-grid based job splitting +- Expose `OpenEO-Identifier` response header from synchronous processing requests (POST `/result`): + add `on_response_headers` argument to `DataCube.execute()` and `VectorCube.execute()`, + and automatically log the identifier at debug level when it is present in the response. ### Changed diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index d4d4d5995..ccafa4cb6 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -1765,6 +1765,8 @@ def download( stream=True, timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, ) + if openeo_identifier := response.headers.get("OpenEO-Identifier"): + _log.debug("Synchronous processing request identifier: %s", openeo_identifier) if on_response_headers := (on_response_headers or self._on_response_headers_sync): on_response_headers(response.headers) @@ -1820,6 +1822,8 @@ def execute( expected_status=200, timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, ) + if openeo_identifier := response.headers.get("OpenEO-Identifier"): + _log.debug("Synchronous processing request identifier: %s", openeo_identifier) if on_response_headers := (on_response_headers or self._on_response_headers_sync): on_response_headers(response.headers) diff --git a/openeo/rest/datacube.py b/openeo/rest/datacube.py index 73ac8ae9e..c26cfc762 100644 --- a/openeo/rest/datacube.py +++ b/openeo/rest/datacube.py @@ -2794,18 +2794,30 @@ def save_user_defined_process( returns=returns, categories=categories, examples=examples, links=links, ) - def execute(self, *, validate: Optional[bool] = None, auto_decode: bool = True) -> Union[dict, requests.Response]: + def execute( + self, + *, + validate: Optional[bool] = None, + auto_decode: bool = True, + on_response_headers: Optional[Callable[[Mapping], None]] = None, + ) -> Union[dict, requests.Response]: """ Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed. :param validate: Optional toggle to enable/prevent validation of the process graphs before execution (overruling the connection's ``auto_validate`` setting). :param auto_decode: Boolean flag to enable/disable automatic JSON decoding of the response. Defaults to True. + :param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers. :return: parsed JSON response as a dict if auto_decode is True, otherwise response object + + .. versionchanged:: 0.33.0 + Added argument ``on_response_headers``. """ # TODO: deprecated this. It's ill-defined how to "execute" a data cube without downloading it. - return self._connection.execute(self.flat_graph(), validate=validate, auto_decode=auto_decode) + return self._connection.execute( + self.flat_graph(), validate=validate, auto_decode=auto_decode, on_response_headers=on_response_headers + ) @staticmethod @deprecated(reason="Use :py:func:`openeo.udf.run_code.execute_local_udf` instead", version="0.7.0") diff --git a/openeo/rest/vectorcube.py b/openeo/rest/vectorcube.py index 921cae94e..51f7b1bc7 100644 --- a/openeo/rest/vectorcube.py +++ b/openeo/rest/vectorcube.py @@ -236,9 +236,9 @@ def _auto_save_result( options=options, ) - def execute(self, *, validate: Optional[bool] = None) -> dict: + def execute(self, *, validate: Optional[bool] = None, on_response_headers: Optional[Callable[[Mapping], None]] = None) -> dict: """Executes the process graph.""" - return self._connection.execute(self.flat_graph(), validate=validate) + return self._connection.execute(self.flat_graph(), validate=validate, on_response_headers=on_response_headers) def download( self, diff --git a/tests/rest/datacube/test_datacube100.py b/tests/rest/datacube/test_datacube100.py index eb7fc3409..ea3098970 100644 --- a/tests/rest/datacube/test_datacube100.py +++ b/tests/rest/datacube/test_datacube100.py @@ -3895,6 +3895,14 @@ def test_download_auto_add_save_result(s2cube, dummy_backend, tmp_path, auto_add assert set(n["process_id"] for n in dummy_backend.get_pg().values()) == process_ids +def test_datacube_execute_on_response_headers(s2cube, dummy_backend): + """Test that on_response_headers callback is called with response headers from execute.""" + dummy_backend.next_result = {"result": 42} + results = [] + s2cube.execute(on_response_headers=results.append) + assert results == [{"OpenEO-Identifier": "r-001"}] + + class TestBatchJob: _EXPECTED_SIMPLE_S2_PG = { "loadcollection1": { diff --git a/tests/rest/datacube/test_vectorcube.py b/tests/rest/datacube/test_vectorcube.py index ef0fc7c6d..90fb1c376 100644 --- a/tests/rest/datacube/test_vectorcube.py +++ b/tests/rest/datacube/test_vectorcube.py @@ -872,3 +872,10 @@ def test_vector_cube_validate(vector_cube, dummy_backend): ] assert isinstance(result, ValidationResponse) assert result == [{"code": "OfflineRequired", "message": "Turn off your smartphone"}] + + +def test_vectorcube_execute_on_response_headers(vector_cube, dummy_backend): + """Test that on_response_headers callback is called with response headers from VectorCube.execute.""" + results = [] + vector_cube.execute(on_response_headers=results.append) + assert results == [{"OpenEO-Identifier": "r-001"}] diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 6da731f71..fe521c811 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -4371,6 +4371,44 @@ def test_connection_on_response_headers_sync_download(dummy_backend, tmp_path): assert results == [{"OpenEO-Identifier": "r-001"}] +def test_execute_on_response_headers(dummy_backend): + results = [] + dummy_backend.next_result = {"result": 42} + dummy_backend.connection.execute( + {"foo1": {"process_id": "foo"}}, + on_response_headers=results.append, + ) + assert results == [{"OpenEO-Identifier": "r-001"}] + + +def test_connection_on_response_headers_sync_execute(dummy_backend): + results = [] + dummy_backend.next_result = {"result": 42} + connection = openeo.connect(dummy_backend.connection.root_url, on_response_headers_sync=results.append) + connection.execute({"foo1": {"process_id": "foo"}}) + assert results == [{"OpenEO-Identifier": "r-001"}] + + +def test_download_openeo_identifier_logging(dummy_backend, tmp_path, caplog): + import logging + + with caplog.at_level(logging.DEBUG, logger="openeo.rest.connection"): + dummy_backend.connection.download( + {"foo1": {"process_id": "foo"}}, + tmp_path / "result.data", + ) + assert "r-001" in caplog.text + + +def test_execute_openeo_identifier_logging(dummy_backend, caplog): + import logging + + dummy_backend.next_result = {"result": 42} + with caplog.at_level(logging.DEBUG, logger="openeo.rest.connection"): + dummy_backend.connection.execute({"foo1": {"process_id": "foo"}}) + assert "r-001" in caplog.text + + @pytest.mark.parametrize( "pg", [ From fb8646fa66421006207a8a1f3d1957205a2d630e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:40:36 +0000 Subject: [PATCH 3/3] Address review: revert on_response_headers from execute methods, remove related tests, fix local imports Agent-Logs-Url: https://github.com/Open-EO/openeo-python-client/sessions/a5d2dc5a-87ff-4776-8246-7f2a8e337784 Co-authored-by: soxofaan <44946+soxofaan@users.noreply.github.com> --- openeo/rest/datacube.py | 16 ++-------------- openeo/rest/vectorcube.py | 4 ++-- tests/rest/datacube/test_datacube100.py | 8 -------- tests/rest/datacube/test_vectorcube.py | 7 ------- tests/rest/test_connection.py | 22 ---------------------- 5 files changed, 4 insertions(+), 53 deletions(-) diff --git a/openeo/rest/datacube.py b/openeo/rest/datacube.py index c26cfc762..73ac8ae9e 100644 --- a/openeo/rest/datacube.py +++ b/openeo/rest/datacube.py @@ -2794,30 +2794,18 @@ def save_user_defined_process( returns=returns, categories=categories, examples=examples, links=links, ) - def execute( - self, - *, - validate: Optional[bool] = None, - auto_decode: bool = True, - on_response_headers: Optional[Callable[[Mapping], None]] = None, - ) -> Union[dict, requests.Response]: + def execute(self, *, validate: Optional[bool] = None, auto_decode: bool = True) -> Union[dict, requests.Response]: """ Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed. :param validate: Optional toggle to enable/prevent validation of the process graphs before execution (overruling the connection's ``auto_validate`` setting). :param auto_decode: Boolean flag to enable/disable automatic JSON decoding of the response. Defaults to True. - :param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers. :return: parsed JSON response as a dict if auto_decode is True, otherwise response object - - .. versionchanged:: 0.33.0 - Added argument ``on_response_headers``. """ # TODO: deprecated this. It's ill-defined how to "execute" a data cube without downloading it. - return self._connection.execute( - self.flat_graph(), validate=validate, auto_decode=auto_decode, on_response_headers=on_response_headers - ) + return self._connection.execute(self.flat_graph(), validate=validate, auto_decode=auto_decode) @staticmethod @deprecated(reason="Use :py:func:`openeo.udf.run_code.execute_local_udf` instead", version="0.7.0") diff --git a/openeo/rest/vectorcube.py b/openeo/rest/vectorcube.py index 51f7b1bc7..921cae94e 100644 --- a/openeo/rest/vectorcube.py +++ b/openeo/rest/vectorcube.py @@ -236,9 +236,9 @@ def _auto_save_result( options=options, ) - def execute(self, *, validate: Optional[bool] = None, on_response_headers: Optional[Callable[[Mapping], None]] = None) -> dict: + def execute(self, *, validate: Optional[bool] = None) -> dict: """Executes the process graph.""" - return self._connection.execute(self.flat_graph(), validate=validate, on_response_headers=on_response_headers) + return self._connection.execute(self.flat_graph(), validate=validate) def download( self, diff --git a/tests/rest/datacube/test_datacube100.py b/tests/rest/datacube/test_datacube100.py index ea3098970..eb7fc3409 100644 --- a/tests/rest/datacube/test_datacube100.py +++ b/tests/rest/datacube/test_datacube100.py @@ -3895,14 +3895,6 @@ def test_download_auto_add_save_result(s2cube, dummy_backend, tmp_path, auto_add assert set(n["process_id"] for n in dummy_backend.get_pg().values()) == process_ids -def test_datacube_execute_on_response_headers(s2cube, dummy_backend): - """Test that on_response_headers callback is called with response headers from execute.""" - dummy_backend.next_result = {"result": 42} - results = [] - s2cube.execute(on_response_headers=results.append) - assert results == [{"OpenEO-Identifier": "r-001"}] - - class TestBatchJob: _EXPECTED_SIMPLE_S2_PG = { "loadcollection1": { diff --git a/tests/rest/datacube/test_vectorcube.py b/tests/rest/datacube/test_vectorcube.py index 90fb1c376..ef0fc7c6d 100644 --- a/tests/rest/datacube/test_vectorcube.py +++ b/tests/rest/datacube/test_vectorcube.py @@ -872,10 +872,3 @@ def test_vector_cube_validate(vector_cube, dummy_backend): ] assert isinstance(result, ValidationResponse) assert result == [{"code": "OfflineRequired", "message": "Turn off your smartphone"}] - - -def test_vectorcube_execute_on_response_headers(vector_cube, dummy_backend): - """Test that on_response_headers callback is called with response headers from VectorCube.execute.""" - results = [] - vector_cube.execute(on_response_headers=results.append) - assert results == [{"OpenEO-Identifier": "r-001"}] diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index fe521c811..f847b8847 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -4371,27 +4371,7 @@ def test_connection_on_response_headers_sync_download(dummy_backend, tmp_path): assert results == [{"OpenEO-Identifier": "r-001"}] -def test_execute_on_response_headers(dummy_backend): - results = [] - dummy_backend.next_result = {"result": 42} - dummy_backend.connection.execute( - {"foo1": {"process_id": "foo"}}, - on_response_headers=results.append, - ) - assert results == [{"OpenEO-Identifier": "r-001"}] - - -def test_connection_on_response_headers_sync_execute(dummy_backend): - results = [] - dummy_backend.next_result = {"result": 42} - connection = openeo.connect(dummy_backend.connection.root_url, on_response_headers_sync=results.append) - connection.execute({"foo1": {"process_id": "foo"}}) - assert results == [{"OpenEO-Identifier": "r-001"}] - - def test_download_openeo_identifier_logging(dummy_backend, tmp_path, caplog): - import logging - with caplog.at_level(logging.DEBUG, logger="openeo.rest.connection"): dummy_backend.connection.download( {"foo1": {"process_id": "foo"}}, @@ -4401,8 +4381,6 @@ def test_download_openeo_identifier_logging(dummy_backend, tmp_path, caplog): def test_execute_openeo_identifier_logging(dummy_backend, caplog): - import logging - dummy_backend.next_result = {"result": 42} with caplog.at_level(logging.DEBUG, logger="openeo.rest.connection"): dummy_backend.connection.execute({"foo1": {"process_id": "foo"}})