Skip to content

Commit 0fa49c3

Browse files
committed
Merge branch 'issue720-export-workspace-et-al'
2 parents 5e70bdc + be0fd37 commit 0fa49c3

File tree

11 files changed

+436
-180
lines changed

11 files changed

+436
-180
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- Add support for `export_workspace` process ([#720](https://github.com/Open-EO/openeo-python-client/issues/720))
13+
1214
### Changed
1315

16+
- `DataCube.save_result()` (and related methods) now return a `SaveResult`/`StacResource` object instead of another `DataCube` object to be more in line with the official `save_result` specification ([#402](https://github.com/Open-EO/openeo-python-client/issues/402), [#720](https://github.com/Open-EO/openeo-python-client/issues/720))
17+
1418
### Removed
1519

1620
### Fixed

docs/api.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@ openeo.rest.mlmodel
4747
:inherited-members:
4848

4949

50-
openeo.rest.multiresult
51-
-----------------------
50+
51+
52+
Results
53+
--------
54+
55+
.. automodule:: openeo.rest.result
56+
:members:
57+
:inherited-members:
58+
59+
.. automodule:: openeo.rest.stac_resource
60+
:members:
61+
5262

5363
.. automodule:: openeo.rest.multiresult
5464
:members: MultiResult

docs/process_mapping.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ method or function in the openEO Python Client Library.
144144
- :py:meth:`ProcessBuilder.__eq__() <openeo.processes.ProcessBuilder.__eq__>`, :py:meth:`ProcessBuilder.eq() <openeo.processes.ProcessBuilder.eq>`, :py:meth:`eq() <openeo.processes.eq>`, :py:meth:`DataCube.__eq__() <openeo.rest.datacube.DataCube.__eq__>`
145145
* - `exp <https://processes.openeo.org/#exp>`_
146146
- :py:meth:`ProcessBuilder.exp() <openeo.processes.ProcessBuilder.exp>`, :py:meth:`exp() <openeo.processes.exp>`
147+
* - `export_workspace <https://processes.openeo.org/#export_workspace>`_
148+
- :py:meth:`StacResource.export_workspace() <openeo.rest.stac_resource.StacResource.export_workspace>`
147149
* - `extrema <https://processes.openeo.org/#extrema>`_
148150
- :py:meth:`ProcessBuilder.extrema() <openeo.processes.ProcessBuilder.extrema>`, :py:meth:`extrema() <openeo.processes.extrema>`
149151
* - `filter_bands <https://processes.openeo.org/#filter_bands>`_

openeo/rest/_datacube.py

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import requests
1212

13+
import openeo
1314
from openeo.internal.graph_building import FlatGraphableMixin, PGNode, _FromNodeMixin
1415
from openeo.internal.jupyter import render_component
1516
from openeo.internal.processes.builder import (
@@ -23,6 +24,8 @@
2324
if typing.TYPE_CHECKING:
2425
# Imports for type checking only (circular import issue at runtime).
2526
from openeo.rest.connection import Connection
27+
from openeo.rest.result import SaveResult
28+
from openeo.rest.stac_resource import StacResource
2629

2730
log = logging.getLogger(__name__)
2831

@@ -321,38 +324,3 @@ def build_child_callback(
321324
raise ValueError(process)
322325

323326
return PGNode.to_process_graph_argument(pg)
324-
325-
326-
def _ensure_save_result(
327-
cube: _ProcessGraphAbstraction,
328-
*,
329-
format: Optional[str] = None,
330-
options: Optional[dict] = None,
331-
weak_format: Optional[str] = None,
332-
default_format: str,
333-
method: str,
334-
) -> _ProcessGraphAbstraction:
335-
"""
336-
Make sure there is a`save_result` node in the process graph.
337-
338-
:param format: (optional) desired `save_result` file format
339-
:param options: (optional) desired `save_result` file format parameters
340-
:param weak_format: (optional) weak format indicator guessed from file name
341-
:param default_format: default format for data type to use when no format is specified by user
342-
:return:
343-
"""
344-
# TODO #278 instead of standalone helper function, move this to common base class for raster cubes, vector cubes, ...
345-
save_result_nodes = [n for n in cube.result_node().walk_nodes() if n.process_id == "save_result"]
346-
347-
if not save_result_nodes:
348-
# No `save_result` node yet: automatically add it.
349-
# TODO: the `save_result` method is not defined on _ProcessGraphAbstraction, but it is on DataCube and VectorCube
350-
cube = cube.save_result(format=format or weak_format or default_format, options=options)
351-
elif format or options:
352-
raise OpenEoClientException(
353-
f"{method} with explicit output {'format' if format else 'options'} {format or options!r},"
354-
f" but the process graph already has `save_result` node(s)"
355-
f" which is ambiguous and should not be combined."
356-
)
357-
358-
return cube

openeo/rest/datacube.py

Lines changed: 80 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@
5656
from openeo.rest._datacube import (
5757
THIS,
5858
UDF,
59-
_ensure_save_result,
6059
_ProcessGraphAbstraction,
6160
build_child_callback,
6261
)
6362
from openeo.rest.graph_building import CollectionProperty
6463
from openeo.rest.job import BatchJob, RESTJob
6564
from openeo.rest.mlmodel import MlModel
65+
from openeo.rest.result import SaveResult
6666
from openeo.rest.service import Service
6767
from openeo.rest.udp import RESTUserDefinedProcess
6868
from openeo.rest.vectorcube import VectorCube
@@ -2330,22 +2330,46 @@ def atmospheric_correction(self, method: str = None, elevation_model: str = None
23302330
@openeo_process
23312331
def save_result(
23322332
self,
2333+
# TODO: does it make sense for the client to define a (hard coded) default format here?
23332334
format: str = _DEFAULT_RASTER_FORMAT,
23342335
options: Optional[dict] = None,
2335-
) -> DataCube:
2336+
) -> SaveResult:
2337+
"""
2338+
Materialize the processed data to the given file format.
2339+
2340+
:param format: an output format supported by the backend.
2341+
:param options: file format options
2342+
2343+
.. versionchanged:: 0.39.0
2344+
returns a :py:class:`~openeo.rest.result.SaveResult` instance instead
2345+
of another :py:class:`~openeo.rest.datacube.DataCube` instance.
2346+
"""
23362347
if self._connection:
23372348
formats = set(self._connection.list_output_formats().keys())
23382349
# TODO: map format to correct casing too?
23392350
if format.lower() not in {f.lower() for f in formats}:
23402351
raise ValueError("Invalid format {f!r}. Should be one of {s}".format(f=format, s=formats))
2341-
return self.process(
2352+
2353+
pg = self._build_pgnode(
23422354
process_id="save_result",
23432355
arguments={
2344-
"data": THIS,
2356+
"data": self,
23452357
"format": format,
23462358
# TODO: leave out options if unset?
2347-
"options": options or {}
2348-
}
2359+
"options": options or {},
2360+
},
2361+
)
2362+
return SaveResult(pg, connection=self._connection)
2363+
2364+
def _auto_save_result(
2365+
self,
2366+
format: Optional[str] = None,
2367+
outputfile: Optional[Union[str, pathlib.Path]] = None,
2368+
options: Optional[dict] = None,
2369+
) -> SaveResult:
2370+
return self.save_result(
2371+
format=format or (guess_format(outputfile) if outputfile else None) or self._DEFAULT_RASTER_FORMAT,
2372+
options=options,
23492373
)
23502374

23512375
def download(
@@ -2365,12 +2389,12 @@ def download(
23652389
If outputfile is provided, the result is stored on disk locally, otherwise, a bytes object is returned.
23662390
The bytes object can be passed on to a suitable decoder for decoding.
23672391
2368-
:param outputfile: Optional, an output file if the result needs to be stored on disk.
2392+
:param outputfile: Optional, output path to download to.
23692393
:param format: Optional, an output format supported by the backend.
23702394
:param options: Optional, file format options
23712395
:param validate: Optional toggle to enable/prevent validation of the process graphs before execution
23722396
(overruling the connection's ``auto_validate`` setting).
2373-
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet.
2397+
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph.
23742398
:param additional: additional (top-level) properties to set in the request body
23752399
:param job_options: dictionary of job options to pass to the backend
23762400
(under top-level property "job_options")
@@ -2384,18 +2408,12 @@ def download(
23842408
Added arguments ``additional`` and ``job_options``.
23852409
"""
23862410
# TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ...
2387-
cube = self
23882411
if auto_add_save_result:
2389-
cube = _ensure_save_result(
2390-
cube=cube,
2391-
format=format,
2392-
options=options,
2393-
weak_format=guess_format(outputfile) if outputfile else None,
2394-
default_format=self._DEFAULT_RASTER_FORMAT,
2395-
method="DataCube.download()",
2396-
)
2412+
res = self._auto_save_result(format=format, outputfile=outputfile, options=options)
2413+
else:
2414+
res = self
23972415
return self._connection.download(
2398-
cube.flat_graph(), outputfile, validate=validate, additional=additional, job_options=job_options
2416+
res.flat_graph(), outputfile=outputfile, validate=validate, additional=additional, job_options=job_options
23992417
)
24002418

24012419
def validate(self) -> List[dict]:
@@ -2510,19 +2528,35 @@ def execute_batch(
25102528
**format_options,
25112529
) -> BatchJob:
25122530
"""
2513-
Evaluate the process graph by creating a batch job, and retrieving the results when it is finished.
2514-
This method is mostly recommended if the batch job is expected to run in a reasonable amount of time.
2531+
Execute the underlying process graph at the backend in batch job mode:
25152532
2516-
For very long-running jobs, you probably do not want to keep the client running.
2533+
- create the job (like :py:meth:`create_job`)
2534+
- start the job (like :py:meth:`BatchJob.start() <openeo.rest.job.BatchJob.start>`)
2535+
- track the job's progress with an active polling loop
2536+
(like :py:meth:`BatchJob.run_synchronous() <openeo.rest.job.BatchJob.run_synchronous>`)
2537+
- optionally (if ``outputfile`` is specified) download the job's results
2538+
when the job finished successfully
25172539
2518-
:param outputfile: The path of a file to which a result can be written
2540+
.. note::
2541+
Because of the active polling loop,
2542+
which blocks any further progress of your script or application,
2543+
this :py:meth:`execute_batch` method is mainly recommended
2544+
for batch jobs that are expected to complete
2545+
in a time that is reasonable for your use case.
2546+
2547+
:param outputfile: Optional, output path to download to.
25192548
:param out_format: (optional) File format to use for the job result.
2549+
:param title: job title.
2550+
:param description: job description.
2551+
:param plan: The billing plan to process and charge the job with
2552+
:param budget: Maximum budget to be spent on executing the job.
2553+
Note that some backends do not honor this limit.
25202554
:param additional: additional (top-level) properties to set in the request body
25212555
:param job_options: dictionary of job options to pass to the backend
25222556
(under top-level property "job_options")
25232557
:param validate: Optional toggle to enable/prevent validation of the process graphs before execution
25242558
(overruling the connection's ``auto_validate`` setting).
2525-
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet.
2559+
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph.
25262560
:param show_error_logs: whether to automatically print error logs when the batch job failed.
25272561
:param log_level: Optional minimum severity level for log entries that the back-end should keep track of.
25282562
One of "error" (highest severity), "warning", "info", and "debug" (lowest severity).
@@ -2546,27 +2580,23 @@ def execute_batch(
25462580
out_format = format_options["format"] # align with 'download' call arg name
25472581

25482582
# TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ...
2549-
cube = self
25502583
if auto_add_save_result:
2551-
cube = _ensure_save_result(
2552-
cube=cube,
2553-
format=out_format,
2554-
options=format_options,
2555-
weak_format=guess_format(outputfile) if outputfile else None,
2556-
default_format=self._DEFAULT_RASTER_FORMAT,
2557-
method="DataCube.execute_batch()",
2558-
)
2584+
res = self._auto_save_result(format=out_format, outputfile=outputfile, options=format_options)
2585+
create_kwargs = {}
2586+
else:
2587+
res = self
2588+
create_kwargs = {"auto_add_save_result": False}
25592589

2560-
job = cube.create_job(
2590+
job = res.create_job(
25612591
title=title,
25622592
description=description,
25632593
plan=plan,
25642594
budget=budget,
25652595
additional=additional,
25662596
job_options=job_options,
25672597
validate=validate,
2568-
auto_add_save_result=False,
25692598
log_level=log_level,
2599+
**create_kwargs,
25702600
)
25712601
return job.run_synchronous(
25722602
outputfile=outputfile,
@@ -2593,25 +2623,27 @@ def create_job(
25932623
**format_options,
25942624
) -> BatchJob:
25952625
"""
2596-
Sends the datacube's process graph as a batch job to the back-end
2597-
and return a :py:class:`~openeo.rest.job.BatchJob` instance.
2626+
Send the underlying process graph to the backend
2627+
to create an openEO batch job
2628+
and return a corresponding :py:class:`~openeo.rest.job.BatchJob` instance.
25982629
2599-
Note that the batch job will just be created at the back-end,
2600-
it still needs to be started and tracked explicitly.
2601-
Use :py:meth:`execute_batch` instead to have the openEO Python client take care of that job management.
2630+
Note that this method only *creates* the openEO batch job at the backend,
2631+
but it does not *start* it.
2632+
Use :py:meth:`execute_batch` instead to let the openEO Python client
2633+
take care of the full job life cycle: create, start and track its progress until completion.
26022634
26032635
:param out_format: output file format.
2604-
:param title: job title
2605-
:param description: job description
2606-
:param plan: The billing plan to process and charge the job with
2636+
:param title: job title.
2637+
:param description: job description.
2638+
:param plan: The billing plan to process and charge the job with.
26072639
:param budget: Maximum budget to be spent on executing the job.
26082640
Note that some backends do not honor this limit.
26092641
:param additional: additional (top-level) properties to set in the request body
26102642
:param job_options: dictionary of job options to pass to the backend
26112643
(under top-level property "job_options")
26122644
:param validate: Optional toggle to enable/prevent validation of the process graphs before execution
26132645
(overruling the connection's ``auto_validate`` setting).
2614-
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet.
2646+
:param auto_add_save_result: Automatically add a ``save_result`` node to the process graph.
26152647
:param log_level: Optional minimum severity level for log entries that the back-end should keep track of.
26162648
One of "error" (highest severity), "warning", "info", and "debug" (lowest severity).
26172649
@@ -2629,17 +2661,13 @@ def create_job(
26292661
# TODO: add option to also automatically start the job?
26302662
# TODO: avoid using all kwargs as format_options
26312663
# TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ...
2632-
cube = self
26332664
if auto_add_save_result:
2634-
cube = _ensure_save_result(
2635-
cube=cube,
2636-
format=out_format,
2637-
options=format_options or None,
2638-
default_format=self._DEFAULT_RASTER_FORMAT,
2639-
method="DataCube.create_job()",
2640-
)
2665+
res = self._auto_save_result(format=out_format, options=format_options)
2666+
else:
2667+
res = self
2668+
26412669
return self._connection.create_job(
2642-
process_graph=cube.flat_graph(),
2670+
process_graph=res.flat_graph(),
26432671
title=title,
26442672
description=description,
26452673
plan=plan,

openeo/rest/result.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from openeo.rest.stac_resource import StacResource
2+
3+
4+
class SaveResult(StacResource):
5+
"""
6+
Handle for a process graph that represents the return value
7+
of the openEO process ``save_result``,
8+
as returned by methods like
9+
:py:meth:`DataCube.save_result() <openeo.rest.datacube.DataCube.save_result>`
10+
and :py:meth:`VectorCube.save_result() <openeo.rest.vectorcube.VectorCube.save_result>`.
11+
12+
.. note ::
13+
This class is practically a just direct alias for
14+
:py:class:`~openeo.rest.stac_resource.StacResource`,
15+
but with a more self-explanatory name.
16+
17+
Moreover, this additional abstraction layer also acts somewhat as an adapter between
18+
the incompatible return values from the ``save_result`` process
19+
in different versions of the official openeo-processes definitions:
20+
21+
- in openeo-processes 1.x: ``save_result`` just returned a boolean,
22+
but that was not really useful to further build upon
23+
and was never properly exposed in the openEO Python client.
24+
- in openeo-processes 2.x: ``save_result`` returns a new concept:
25+
a "STAC resource" (object with subtype "stac")
26+
which is a more useful and flexible representation of an openEO result,
27+
allowing additional operations.
28+
29+
The openEO Python client returns the same :py:class:`SaveResult` object
30+
in both cases however.
31+
It does that not only for simplicity,
32+
but also because it seems more useful (even in legacy openeo-processes 1.x use cases)
33+
to follow the new STAC resource based usage patterns
34+
than to strictly return some boolean wrapper nobody has use for.
35+
36+
.. versionadded:: 0.39.0
37+
"""

0 commit comments

Comments
 (0)