diff --git a/CHANGELOG.md b/CHANGELOG.md index f97e1edc2..9f99ada63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed +- `Connection.execute()` and `DataCube.execute()` now have a `auto_decode` argument. If set to True (default) the response will be decoded as a JSON and throw an exception if this fails, if set to False the raw `requests.Response` object will be returned. ([#499](https://github.com/Open-EO/openeo-python-client/issues/499)) ### Removed diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index dddb5a4d8..1067024ff 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -1576,25 +1576,36 @@ def execute( process_graph: Union[dict, str, Path], timeout: Optional[int] = None, validate: Optional[bool] = None, - ): + auto_decode: bool = True, + ) -> Union[dict, requests.Response]: """ - Execute a process graph synchronously and return the result (assumed to be JSON). + Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed. :param process_graph: (flat) dict representing a process graph, or process graph as raw JSON string, or as local file path or URL :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. - :return: parsed JSON response + :return: parsed JSON response as a dict if auto_decode is True, otherwise response object """ pg_with_metadata = self._build_request_with_process_graph(process_graph=process_graph) self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate) - return self.post( + response = self.post( path="/result", json=pg_with_metadata, expected_status=200, timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, - ).json() # TODO: only do JSON decoding when mimetype is actually JSON? + ) + if auto_decode: + try: + return response.json() + except requests.exceptions.JSONDecodeError as e: + raise OpenEoClientException( + "Failed to decode response as JSON. For other data types use `download` method instead of `execute`." + ) from e + else: + return response def create_job( self, diff --git a/openeo/rest/datacube.py b/openeo/rest/datacube.py index 5a81542c3..fcf5ddc35 100644 --- a/openeo/rest/datacube.py +++ b/openeo/rest/datacube.py @@ -18,6 +18,7 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, Callable import numpy as np +import requests import shapely.geometry import shapely.geometry.base from shapely.geometry import MultiPolygon, Polygon, mapping @@ -2306,9 +2307,17 @@ def save_user_defined_process( returns=returns, categories=categories, examples=examples, links=links, ) - def execute(self, *, validate: Optional[bool] = None) -> dict: - """Executes the process graph.""" - return self._connection.execute(self.flat_graph(), validate=validate) + 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. + + :return: parsed JSON response as a dict if auto_decode is True, otherwise response object + """ + 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/tests/rest/datacube/test_datacube.py b/tests/rest/datacube/test_datacube.py index 7d247d39b..88d938c68 100644 --- a/tests/rest/datacube/test_datacube.py +++ b/tests/rest/datacube/test_datacube.py @@ -10,10 +10,11 @@ import numpy as np import pytest +import requests import shapely import shapely.geometry -from openeo.rest import BandMathException +from openeo.rest import BandMathException, OpenEoClientException from openeo.rest._testing import build_capabilities from openeo.rest.connection import Connection from openeo.rest.datacube import DataCube @@ -546,6 +547,35 @@ def test_download_pathlib(connection, requests_mock, tmp_path): assert path.read_bytes() == b"tiffdata" +def test_execute_json_decode(connection, requests_mock): + requests_mock.get(API_URL + "/collections/S2", json={}) + requests_mock.post(API_URL + "/result", content=b'{"foo": "bar"}') + result = connection.load_collection("S2").execute(auto_decode=True) + assert result == {"foo": "bar"} + + +def test_execute_decode_error(connection, requests_mock): + requests_mock.get(API_URL + "/collections/S2", json={}) + requests_mock.post(API_URL + "/result", content=b"tiffdata") + with pytest.raises(OpenEoClientException, match="Failed to decode response as JSON.*$"): + connection.load_collection("S2").execute(auto_decode=True) + + +def test_execute_json_raw(connection, requests_mock): + requests_mock.get(API_URL + "/collections/S2", json={}) + requests_mock.post(API_URL + "/result", content=b'{"foo": "bar"}') + result = connection.load_collection("S2").execute(auto_decode=False) + assert isinstance(result, requests.Response) + assert result.content == b'{"foo": "bar"}' + + +def test_execute_tiff_raw(connection, requests_mock): + requests_mock.get(API_URL + "/collections/S2", json={}) + requests_mock.post(API_URL + "/result", content=b"tiffdata") + result = connection.load_collection("S2").execute(auto_decode=False) + assert isinstance(result, requests.Response) + assert result.content == b"tiffdata" + @pytest.mark.parametrize(["filename", "expected_format"], [ ("result.tiff", "GTiff"), ("result.tif", "GTiff"),