Skip to content

Commit 2e58607

Browse files
committed
Issue #391/#651 add docs, changelog, review tweaks
1 parent 3056541 commit 2e58607

File tree

8 files changed

+167
-12
lines changed

8 files changed

+167
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added `MultiResult` helper class to build process graphs with multiple result nodes ([#391](https://github.com/Open-EO/openeo-python-client/issues/391))
13+
1214
### Changed
1315

1416
### Removed

docs/api.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ openeo.rest.mlmodel
4747
:inherited-members:
4848

4949

50+
openeo.rest.multiresult
51+
-----------------------
52+
53+
.. automodule:: openeo.rest.multiresult
54+
:members: MultiResult
55+
:inherited-members:
56+
:special-members: __init__
57+
58+
5059
openeo.metadata
5160
----------------
5261

docs/datacube_construction.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,53 @@ Re-parameterization
196196
```````````````````
197197

198198
TODO
199+
200+
201+
202+
.. _multi-result-process-graphs:
203+
Building process graphs with multiple result nodes
204+
===================================================
205+
206+
.. note::
207+
Multi-result support is added in version 0.35.0
208+
209+
Most openEO use cases are just about building a single result data cube,
210+
which is readily covered in the openEO Python client library through classes like
211+
:py:class:`~openeo.rest.datacube.DataCube` and :py:class:`~openeo.rest.vectorcube.VectorCube`.
212+
It is straightforward to create a batch job from these, or execute/download them synchronously.
213+
214+
The openEO API also allows multiple result nodes in a single process graph,
215+
for example to persist intermediate results or produce results in different output formats.
216+
To support this, the openEO Python client library provides the :py:class:`~openeo.rest.multiresult.MultiResult` class,
217+
which allows to group multiple :py:class:`~openeo.rest.datacube.DataCube` and :py:class:`~openeo.rest.vectorcube.VectorCube` objects
218+
in a single entity that can be used to create or run batch jobs. For example:
219+
220+
221+
.. code-block:: python
222+
223+
cube1 = ...
224+
cube2 = ...
225+
multi_result = MultiResult([cube1, cube2])
226+
job = multi_result.create_job()
227+
228+
229+
Moreover, it is not necessary to explicitly create such a
230+
:py:class:`~openeo.rest.multiresult.MultiResult` object,
231+
as the :py:meth:`Connection.create_job() <openeo.rest.connection.Connection.create_job>` method
232+
directly supports passing multiple data cube objects in a list,
233+
which will be automatically grouped as a multi-result:
234+
235+
.. code-block:: python
236+
237+
cube1 = ...
238+
cube2 = ...
239+
job = connection.create_job([cube1, cube2])
240+
241+
242+
.. important::
243+
244+
Only a single :py:class:`~openeo.rest.connection.Connection` can be in play
245+
when grouping multiple results like this.
246+
As everything is to be merged in a single process graph
247+
to be sent to a single backend,
248+
it is not possible to mix cubes created from different connections.

openeo/rest/_testing.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import collections
44
import json
55
import re
6-
from typing import Callable, Iterable, Iterator, Optional, Sequence, Union
6+
from typing import Callable, Iterable, Iterator, Optional, Sequence, Tuple, Union
77

88
from openeo import Connection, DataCube
99
from openeo.rest.vectorcube import VectorCube
@@ -82,7 +82,7 @@ def __init__(
8282
requests_mock.post(connection.build_url("/validation"), json=self._handle_post_validation)
8383

8484
@classmethod
85-
def at(cls, root_url: str, *, requests_mock, capabilities: Optional[dict] = None) -> DummyBackend:
85+
def at_url(cls, root_url: str, *, requests_mock, capabilities: Optional[dict] = None) -> DummyBackend:
8686
"""
8787
Factory to build dummy backend from given root URL
8888
including creation of connection and mocking of capabilities doc
@@ -92,12 +92,36 @@ def at(cls, root_url: str, *, requests_mock, capabilities: Optional[dict] = None
9292
connection = Connection(root_url)
9393
return cls(requests_mock=requests_mock, connection=connection)
9494

95-
def setup_collection(self, collection_id: str):
95+
def setup_collection(
96+
self,
97+
collection_id: str,
98+
*,
99+
temporal: Union[bool, Tuple[str, str]] = True,
100+
bands: Sequence[str] = ("B1", "B2", "B3"),
101+
):
96102
# TODO: also mock `/collections` overview
103+
# TODO: option to override cube_dimensions as a whole, or override dimension names
104+
cube_dimensions = {
105+
"x": {"type": "spatial"},
106+
"y": {"type": "spatial"},
107+
}
108+
109+
if temporal:
110+
cube_dimensions["t"] = {
111+
"type": "temporal",
112+
"extent": temporal if isinstance(temporal, tuple) else [None, None],
113+
}
114+
if bands:
115+
cube_dimensions["bands"] = {"type": "bands", "values": list(bands)}
116+
97117
self._requests_mock.get(
98118
self.connection.build_url(f"/collections/{collection_id}"),
99119
# TODO: add more metadata?
100-
json={"id": collection_id},
120+
json={
121+
"id": collection_id,
122+
# define temporal and band dim
123+
"cube:dimensions": {"t": {"type": "temporal"}, "bands": {"type": "bands"}},
124+
},
101125
)
102126
return self
103127

@@ -201,7 +225,6 @@ def get_batch_pg(self) -> dict:
201225
def get_validation_pg(self) -> dict:
202226
"""
203227
Get process graph of the one and only validation request.
204-
:return:
205228
"""
206229
assert len(self.validation_requests) == 1
207230
return self.validation_requests[0]

openeo/rest/connection.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,13 @@ def validate_process_graph(
11351135
"""
11361136
Validate a process graph without executing it.
11371137
1138-
:param process_graph: (flat) dict representing process graph
1138+
:param process_graph: openEO-style (flat) process graph representation,
1139+
or an object that can be converted to such a representation:
1140+
a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object,
1141+
a string with a JSON representation,
1142+
a local file path or URL to a JSON representation,
1143+
a :py:class:`~openeo.rest.multiresult.MultiResult` object, ...
1144+
11391145
:return: list of errors (dictionaries with "code" and "message" fields)
11401146
"""
11411147
pg_with_metadata = self._build_request_with_process_graph(process_graph)["process"]
@@ -1754,8 +1760,12 @@ def create_job(
17541760
"""
17551761
Create a new job from given process graph on the back-end.
17561762
1757-
:param process_graph: (flat) dict representing a process graph, or process graph as raw JSON string,
1758-
or as local file path or URL
1763+
:param process_graph: openEO-style (flat) process graph representation,
1764+
or an object that can be converted to such a representation:
1765+
a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object,
1766+
a string with a JSON representation,
1767+
a local file path or URL to a JSON representation,
1768+
a :py:class:`~openeo.rest.multiresult.MultiResult` object, ...
17591769
:param title: job title
17601770
:param description: job description
17611771
:param plan: The billing plan to process and charge the job with
@@ -1765,6 +1775,9 @@ def create_job(
17651775
:param validate: Optional toggle to enable/prevent validation of the process graphs before execution
17661776
(overruling the connection's ``auto_validate`` setting).
17671777
:return: Created job
1778+
1779+
.. versionchanged:: 0.35.0
1780+
Add :ref:`multi-result support <multi-result-process-graphs>`.
17681781
"""
17691782
# TODO move all this (BatchJob factory) logic to BatchJob?
17701783

openeo/rest/multiresult.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010

1111
class MultiResult(FlatGraphableMixin):
1212
"""
13-
Adapter to create/run batch jobs from process graphs with multiple end/result/leaf nodes.
13+
Helper to create and run batch jobs with process graphs
14+
that contain multiple result nodes
15+
or, more generally speaking, multiple process graph "leaf" nodes.
1416
15-
Usage example:
17+
Provide multiple
18+
:py:class:`~openeo.rest.datacube.DataCube`/:py:class:`~openeo.rest.vectorcube.VectorCube`
19+
instances to the constructor,
20+
and start a batch job from that,
21+
for example as follows:
1622
1723
.. code-block:: python
1824
@@ -21,10 +27,25 @@ class MultiResult(FlatGraphableMixin):
2127
multi_result = MultiResult([cube1, cube2])
2228
job = multi_result.create_job()
2329
30+
.. seealso::
2431
32+
:ref:`multi-result-process-graphs`
33+
34+
.. versionadded:: 0.35.0
2535
"""
2636

2737
def __init__(self, leaves: List[FlatGraphableMixin], connection: Optional[Connection] = None):
38+
"""
39+
Build a :py:class:`MultiResult` instance from multiple leaf nodes
40+
41+
:param leaves: list of objects that can be
42+
converted to an openEO-style (flat) process graph representation,
43+
typically :py:class:`~openeo.rest.datacube.DataCube`
44+
or :py:class:`~openeo.rest.vectorcube.VectorCube` instances.
45+
:param connection: Optional connection to use for creating/starting batch jobs,
46+
for special use cases where the provided leaf instances
47+
are not already associated with a connection.
48+
"""
2849
self._multi_leaf_graph = MultiLeafGraph(leaves=leaves)
2950
self._connection = self._common_connection(leaves=leaves, connection=connection)
3051

tests/rest/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def dummy_backend(requests_mock, con120) -> DummyBackend:
112112
@pytest.fixture
113113
def another_dummy_backend(requests_mock) -> DummyBackend:
114114
root_url = "https://openeo.other.test/"
115-
another_dummy_backend = DummyBackend.at(
115+
another_dummy_backend = DummyBackend.at_url(
116116
root_url, requests_mock=requests_mock, capabilities={"api_version": "1.2.0"}
117117
)
118118
another_dummy_backend.setup_collection("S2")

tests/rest/test_connection.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
refresh_token_store = refresh_token_store
5454

5555

56-
5756
@pytest.mark.parametrize(
5857
["base", "paths", "expected_path"],
5958
[
@@ -3803,3 +3802,41 @@ def test_create_job_with_mixed_connections(self, con120, dummy_backend, another_
38033802

38043803
with pytest.raises(OpenEoClientException, match="Mixing different connections"):
38053804
other_connection.create_job([save1, save2])
3805+
3806+
def test_create_job_intermediate_resultst(self, con120, dummy_backend):
3807+
cube = con120.load_collection("S2")
3808+
save1 = cube.save_result(format="GTiff")
3809+
reduced = cube.reduce_temporal("mean")
3810+
save2 = reduced.save_result(format="GTiff")
3811+
con120.create_job([save1, save2])
3812+
assert dummy_backend.get_batch_pg() == {
3813+
"loadcollection1": {
3814+
"process_id": "load_collection",
3815+
"arguments": {"id": "S2", "spatial_extent": None, "temporal_extent": None},
3816+
},
3817+
"saveresult1": {
3818+
"process_id": "save_result",
3819+
"arguments": {"data": {"from_node": "loadcollection1"}, "format": "GTiff", "options": {}},
3820+
},
3821+
"reducedimension1": {
3822+
"process_id": "reduce_dimension",
3823+
"arguments": {
3824+
"data": {"from_node": "loadcollection1"},
3825+
"dimension": "t",
3826+
"reducer": {
3827+
"process_graph": {
3828+
"mean1": {
3829+
"arguments": {"data": {"from_parameter": "data"}},
3830+
"process_id": "mean",
3831+
"result": True,
3832+
}
3833+
}
3834+
},
3835+
},
3836+
},
3837+
"saveresult2": {
3838+
"process_id": "save_result",
3839+
"arguments": {"data": {"from_node": "reducedimension1"}, "format": "GTiff", "options": {}},
3840+
"result": True,
3841+
},
3842+
}

0 commit comments

Comments
 (0)