From 03fe168827720e71466fe952a866531ade29900f Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 13 Nov 2024 12:57:55 +0000 Subject: [PATCH] Update documentation --- .buildinfo | 4 + .doctrees/api-processbuilder.doctree | Bin 0 -> 14657 bytes .doctrees/api-processes.doctree | Bin 0 -> 1098930 bytes .doctrees/api.doctree | Bin 0 -> 2411759 bytes .doctrees/auth.doctree | Bin 0 -> 87690 bytes .doctrees/basics.doctree | Bin 0 -> 61887 bytes .doctrees/batch_jobs.doctree | Bin 0 -> 64328 bytes .doctrees/best_practices.doctree | Bin 0 -> 15074 bytes .doctrees/changelog.doctree | Bin 0 -> 299869 bytes .doctrees/configuration.doctree | Bin 0 -> 23158 bytes .doctrees/cookbook/ard.doctree | Bin 0 -> 26868 bytes .doctrees/cookbook/index.doctree | Bin 0 -> 3028 bytes .doctrees/cookbook/job_manager.doctree | Bin 0 -> 176751 bytes .doctrees/cookbook/localprocessing.doctree | Bin 0 -> 26209 bytes .doctrees/cookbook/sampling.doctree | Bin 0 -> 11547 bytes .doctrees/cookbook/spectral_indices.doctree | Bin 0 -> 100559 bytes .doctrees/cookbook/tricks.doctree | Bin 0 -> 13022 bytes .doctrees/cookbook/udp_sharing.doctree | Bin 0 -> 21784 bytes .doctrees/data_access.doctree | Bin 0 -> 51281 bytes .doctrees/datacube_construction.doctree | Bin 0 -> 35554 bytes .doctrees/development.doctree | Bin 0 -> 81219 bytes .doctrees/environment.pickle | Bin 0 -> 1455199 bytes .doctrees/index.doctree | Bin 0 -> 8432 bytes .doctrees/installation.doctree | Bin 0 -> 22886 bytes .doctrees/machine_learning.doctree | Bin 0 -> 18547 bytes .doctrees/process_mapping.doctree | Bin 0 -> 309182 bytes .doctrees/processes.doctree | Bin 0 -> 63077 bytes .doctrees/udf.doctree | Bin 0 -> 154769 bytes .doctrees/udp.doctree | Bin 0 -> 72088 bytes .nojekyll | 0 _images/apply-rescaled-histogram.png | Bin 0 -> 5777 bytes _images/batchjobs-jupyter-created.png | Bin 0 -> 56503 bytes _images/batchjobs-jupyter-listing.png | Bin 0 -> 46962 bytes _images/batchjobs-jupyter-logs.png | Bin 0 -> 24868 bytes _images/batchjobs-webeditor-listing.png | Bin 0 -> 63115 bytes _images/evi-composite.png | Bin 0 -> 31940 bytes _images/evi-masked-composite.png | Bin 0 -> 47434 bytes _images/evi-timeseries.png | Bin 0 -> 45092 bytes _images/local_ndvi.jpg | Bin 0 -> 96185 bytes _images/logging_arrayshape.png | Bin 0 -> 65288 bytes _images/welcome.png | Bin 0 -> 92455 bytes _modules/index.html | 149 + _modules/openeo/api/logs.html | 229 + _modules/openeo/api/process.html | 615 ++ _modules/openeo/extra/job_management.html | 1309 ++++ .../spectral_indices/spectral_indices.html | 620 ++ _modules/openeo/internal/graph_building.html | 630 ++ _modules/openeo/metadata.html | 869 +++ _modules/openeo/processes.html | 6173 +++++++++++++++ _modules/openeo/rest/_datacube.html | 494 ++ _modules/openeo/rest/connection.html | 2307 ++++++ _modules/openeo/rest/conversions.html | 263 + _modules/openeo/rest/datacube.html | 3142 ++++++++ _modules/openeo/rest/graph_building.html | 208 + _modules/openeo/rest/job.html | 751 ++ _modules/openeo/rest/mlmodel.html | 264 + _modules/openeo/rest/multiresult.html | 232 + _modules/openeo/rest/udp.html | 266 + _modules/openeo/rest/userfile.html | 242 + _modules/openeo/rest/vectorcube.html | 788 ++ _modules/openeo/testing.html | 170 + _modules/openeo/testing/results.html | 524 ++ _modules/openeo/udf/debug.html | 157 + _modules/openeo/udf/run_code.html | 458 ++ _modules/openeo/udf/structured_data.html | 174 + _modules/openeo/udf/udf_data.html | 283 + _modules/openeo/udf/udf_signatures.html | 248 + _modules/openeo/udf/xarraydatacube.html | 526 ++ _modules/openeo/util.html | 831 ++ _sources/api-processbuilder.rst.txt | 87 + _sources/api-processes.rst.txt | 68 + _sources/api.rst.txt | 177 + _sources/auth.rst.txt | 611 ++ _sources/basics.rst.txt | 459 ++ _sources/batch_jobs.rst.txt | 415 + _sources/best_practices.rst.txt | 93 + _sources/changelog.md.txt | 2 + _sources/configuration.rst.txt | 96 + _sources/cookbook/ard.rst.txt | 113 + _sources/cookbook/index.rst.txt | 14 + _sources/cookbook/job_manager.rst.txt | 122 + _sources/cookbook/localprocessing.rst.txt | 184 + _sources/cookbook/sampling.md.txt | 61 + _sources/cookbook/spectral_indices.rst.txt | 88 + _sources/cookbook/tricks.rst.txt | 82 + _sources/cookbook/udp_sharing.rst.txt | 133 + _sources/data_access.rst.txt | 345 + _sources/datacube_construction.rst.txt | 250 + _sources/development.rst.txt | 420 + _sources/index.rst.txt | 75 + _sources/installation.rst.txt | 126 + _sources/machine_learning.rst.txt | 118 + _sources/process_mapping.rst.txt | 332 + _sources/processes.rst.txt | 465 ++ _sources/udf.rst.txt | 701 ++ _sources/udp.rst.txt | 527 ++ _static/alabaster.css | 663 ++ _static/basic.css | 914 +++ _static/custom.css | 139 + _static/doctools.js | 149 + _static/documentation_options.js | 13 + _static/file.png | Bin 0 -> 286 bytes _static/github-banner.svg | 5 + _static/images/basics/evi-composite.png | Bin 0 -> 31940 bytes .../images/basics/evi-masked-composite.png | Bin 0 -> 47434 bytes _static/images/basics/evi-timeseries.png | Bin 0 -> 45092 bytes _static/images/batchjobs-jupyter-created.png | Bin 0 -> 56503 bytes _static/images/batchjobs-jupyter-listing.png | Bin 0 -> 46962 bytes _static/images/batchjobs-jupyter-logs.png | Bin 0 -> 24868 bytes .../images/batchjobs-webeditor-listing.png | Bin 0 -> 63115 bytes _static/images/local/local_ndvi.jpg | Bin 0 -> 96185 bytes .../images/udf/apply-rescaled-histogram.png | Bin 0 -> 5777 bytes _static/images/udf/logging_arrayshape.png | Bin 0 -> 65288 bytes _static/images/vito-logo.png | Bin 0 -> 8365 bytes _static/images/welcome.png | Bin 0 -> 92455 bytes _static/language_data.js | 192 + _static/minus.png | Bin 0 -> 90 bytes _static/plus.png | Bin 0 -> 90 bytes _static/pygments.css | 75 + _static/searchtools.js | 632 ++ _static/sphinx_highlight.js | 154 + api-processbuilder.html | 196 + api-processes.html | 4557 +++++++++++ api.html | 6809 +++++++++++++++++ auth.html | 665 ++ basics.html | 527 ++ batch_jobs.html | 446 ++ best_practices.html | 213 + changelog.html | 1336 ++++ configuration.html | 239 + cookbook/ard.html | 234 + cookbook/index.html | 218 + cookbook/job_manager.html | 766 ++ cookbook/localprocessing.html | 307 + cookbook/sampling.html | 193 + cookbook/spectral_indices.html | 450 ++ cookbook/tricks.html | 208 + cookbook/udp_sharing.html | 255 + data_access.html | 412 + datacube_construction.html | 344 + development.html | 519 ++ genindex.html | 1796 +++++ index.html | 378 + installation.html | 230 + lib/openeo/__init__.py | 29 + lib/openeo/_version.py | 1 + lib/openeo/api/__init__.py | 3 + lib/openeo/api/logs.py | 99 + lib/openeo/api/process.py | 443 ++ lib/openeo/capabilities.py | 209 + lib/openeo/config.py | 209 + lib/openeo/dates.py | 202 + lib/openeo/extra/__init__.py | 0 lib/openeo/extra/job_management.py | 1116 +++ lib/openeo/extra/spectral_indices/__init__.py | 2 + .../awesome-spectral-indices/LICENSE | 21 + .../awesome-spectral-indices/bands.json | 785 ++ .../awesome-spectral-indices/constants.json | 107 + .../spectral-indices-dict.json | 4616 +++++++++++ .../resources/extra-indices-dict.json | 98 + .../spectral_indices/spectral_indices.py | 475 ++ lib/openeo/internal/__init__.py | 0 lib/openeo/internal/documentation.py | 60 + lib/openeo/internal/graph_building.py | 476 ++ lib/openeo/internal/jupyter.py | 173 + lib/openeo/internal/process_graph_visitor.py | 265 + lib/openeo/internal/processes/__init__.py | 0 lib/openeo/internal/processes/builder.py | 120 + lib/openeo/internal/processes/generator.py | 305 + lib/openeo/internal/processes/parse.py | 164 + lib/openeo/internal/warnings.py | 95 + lib/openeo/local/__init__.py | 3 + lib/openeo/local/collections.py | 240 + lib/openeo/local/connection.py | 285 + lib/openeo/local/processing.py | 82 + lib/openeo/metadata.py | 706 ++ lib/openeo/processes.py | 5590 ++++++++++++++ lib/openeo/rest/__init__.py | 96 + lib/openeo/rest/_datacube.py | 358 + lib/openeo/rest/_testing.py | 338 + lib/openeo/rest/auth/__init__.py | 0 lib/openeo/rest/auth/auth.py | 54 + lib/openeo/rest/auth/cli.py | 376 + lib/openeo/rest/auth/config.py | 240 + lib/openeo/rest/auth/oidc.py | 943 +++ lib/openeo/rest/auth/testing.py | 292 + lib/openeo/rest/connection.py | 2015 +++++ lib/openeo/rest/conversions.py | 124 + lib/openeo/rest/datacube.py | 2769 +++++++ lib/openeo/rest/graph_building.py | 78 + lib/openeo/rest/job.py | 546 ++ lib/openeo/rest/mlmodel.py | 125 + lib/openeo/rest/multiresult.py | 102 + lib/openeo/rest/rest_capabilities.py | 54 + lib/openeo/rest/service.py | 58 + lib/openeo/rest/udp.py | 124 + lib/openeo/rest/userfile.py | 100 + lib/openeo/rest/vectorcube.py | 610 ++ lib/openeo/testing/__init__.py | 37 + lib/openeo/testing/results.py | 386 + lib/openeo/testing/stac.py | 110 + lib/openeo/udf/__init__.py | 13 + lib/openeo/udf/_compat.py | 65 + lib/openeo/udf/debug.py | 30 + lib/openeo/udf/feature_collection.py | 110 + lib/openeo/udf/run_code.py | 328 + lib/openeo/udf/structured_data.py | 47 + lib/openeo/udf/udf_data.py | 135 + lib/openeo/udf/udf_signatures.py | 109 + lib/openeo/udf/xarraydatacube.py | 381 + lib/openeo/util.py | 689 ++ machine_learning.html | 244 + objects.inv | Bin 0 -> 5924 bytes process_mapping.html | 609 ++ processes.html | 539 ++ py-modindex.html | 272 + search.html | 144 + searchindex.js | 1 + udf.html | 917 +++ udp.html | 606 ++ 220 files changed, 85477 insertions(+) create mode 100644 .buildinfo create mode 100644 .doctrees/api-processbuilder.doctree create mode 100644 .doctrees/api-processes.doctree create mode 100644 .doctrees/api.doctree create mode 100644 .doctrees/auth.doctree create mode 100644 .doctrees/basics.doctree create mode 100644 .doctrees/batch_jobs.doctree create mode 100644 .doctrees/best_practices.doctree create mode 100644 .doctrees/changelog.doctree create mode 100644 .doctrees/configuration.doctree create mode 100644 .doctrees/cookbook/ard.doctree create mode 100644 .doctrees/cookbook/index.doctree create mode 100644 .doctrees/cookbook/job_manager.doctree create mode 100644 .doctrees/cookbook/localprocessing.doctree create mode 100644 .doctrees/cookbook/sampling.doctree create mode 100644 .doctrees/cookbook/spectral_indices.doctree create mode 100644 .doctrees/cookbook/tricks.doctree create mode 100644 .doctrees/cookbook/udp_sharing.doctree create mode 100644 .doctrees/data_access.doctree create mode 100644 .doctrees/datacube_construction.doctree create mode 100644 .doctrees/development.doctree create mode 100644 .doctrees/environment.pickle create mode 100644 .doctrees/index.doctree create mode 100644 .doctrees/installation.doctree create mode 100644 .doctrees/machine_learning.doctree create mode 100644 .doctrees/process_mapping.doctree create mode 100644 .doctrees/processes.doctree create mode 100644 .doctrees/udf.doctree create mode 100644 .doctrees/udp.doctree create mode 100644 .nojekyll create mode 100644 _images/apply-rescaled-histogram.png create mode 100644 _images/batchjobs-jupyter-created.png create mode 100644 _images/batchjobs-jupyter-listing.png create mode 100644 _images/batchjobs-jupyter-logs.png create mode 100644 _images/batchjobs-webeditor-listing.png create mode 100644 _images/evi-composite.png create mode 100644 _images/evi-masked-composite.png create mode 100644 _images/evi-timeseries.png create mode 100644 _images/local_ndvi.jpg create mode 100644 _images/logging_arrayshape.png create mode 100644 _images/welcome.png create mode 100644 _modules/index.html create mode 100644 _modules/openeo/api/logs.html create mode 100644 _modules/openeo/api/process.html create mode 100644 _modules/openeo/extra/job_management.html create mode 100644 _modules/openeo/extra/spectral_indices/spectral_indices.html create mode 100644 _modules/openeo/internal/graph_building.html create mode 100644 _modules/openeo/metadata.html create mode 100644 _modules/openeo/processes.html create mode 100644 _modules/openeo/rest/_datacube.html create mode 100644 _modules/openeo/rest/connection.html create mode 100644 _modules/openeo/rest/conversions.html create mode 100644 _modules/openeo/rest/datacube.html create mode 100644 _modules/openeo/rest/graph_building.html create mode 100644 _modules/openeo/rest/job.html create mode 100644 _modules/openeo/rest/mlmodel.html create mode 100644 _modules/openeo/rest/multiresult.html create mode 100644 _modules/openeo/rest/udp.html create mode 100644 _modules/openeo/rest/userfile.html create mode 100644 _modules/openeo/rest/vectorcube.html create mode 100644 _modules/openeo/testing.html create mode 100644 _modules/openeo/testing/results.html create mode 100644 _modules/openeo/udf/debug.html create mode 100644 _modules/openeo/udf/run_code.html create mode 100644 _modules/openeo/udf/structured_data.html create mode 100644 _modules/openeo/udf/udf_data.html create mode 100644 _modules/openeo/udf/udf_signatures.html create mode 100644 _modules/openeo/udf/xarraydatacube.html create mode 100644 _modules/openeo/util.html create mode 100644 _sources/api-processbuilder.rst.txt create mode 100644 _sources/api-processes.rst.txt create mode 100644 _sources/api.rst.txt create mode 100644 _sources/auth.rst.txt create mode 100644 _sources/basics.rst.txt create mode 100644 _sources/batch_jobs.rst.txt create mode 100644 _sources/best_practices.rst.txt create mode 100644 _sources/changelog.md.txt create mode 100644 _sources/configuration.rst.txt create mode 100644 _sources/cookbook/ard.rst.txt create mode 100644 _sources/cookbook/index.rst.txt create mode 100644 _sources/cookbook/job_manager.rst.txt create mode 100644 _sources/cookbook/localprocessing.rst.txt create mode 100644 _sources/cookbook/sampling.md.txt create mode 100644 _sources/cookbook/spectral_indices.rst.txt create mode 100644 _sources/cookbook/tricks.rst.txt create mode 100644 _sources/cookbook/udp_sharing.rst.txt create mode 100644 _sources/data_access.rst.txt create mode 100644 _sources/datacube_construction.rst.txt create mode 100644 _sources/development.rst.txt create mode 100644 _sources/index.rst.txt create mode 100644 _sources/installation.rst.txt create mode 100644 _sources/machine_learning.rst.txt create mode 100644 _sources/process_mapping.rst.txt create mode 100644 _sources/processes.rst.txt create mode 100644 _sources/udf.rst.txt create mode 100644 _sources/udp.rst.txt create mode 100644 _static/alabaster.css create mode 100644 _static/basic.css create mode 100644 _static/custom.css create mode 100644 _static/doctools.js create mode 100644 _static/documentation_options.js create mode 100644 _static/file.png create mode 100644 _static/github-banner.svg create mode 100644 _static/images/basics/evi-composite.png create mode 100644 _static/images/basics/evi-masked-composite.png create mode 100644 _static/images/basics/evi-timeseries.png create mode 100644 _static/images/batchjobs-jupyter-created.png create mode 100644 _static/images/batchjobs-jupyter-listing.png create mode 100644 _static/images/batchjobs-jupyter-logs.png create mode 100644 _static/images/batchjobs-webeditor-listing.png create mode 100644 _static/images/local/local_ndvi.jpg create mode 100644 _static/images/udf/apply-rescaled-histogram.png create mode 100644 _static/images/udf/logging_arrayshape.png create mode 100644 _static/images/vito-logo.png create mode 100644 _static/images/welcome.png create mode 100644 _static/language_data.js create mode 100644 _static/minus.png create mode 100644 _static/plus.png create mode 100644 _static/pygments.css create mode 100644 _static/searchtools.js create mode 100644 _static/sphinx_highlight.js create mode 100644 api-processbuilder.html create mode 100644 api-processes.html create mode 100644 api.html create mode 100644 auth.html create mode 100644 basics.html create mode 100644 batch_jobs.html create mode 100644 best_practices.html create mode 100644 changelog.html create mode 100644 configuration.html create mode 100644 cookbook/ard.html create mode 100644 cookbook/index.html create mode 100644 cookbook/job_manager.html create mode 100644 cookbook/localprocessing.html create mode 100644 cookbook/sampling.html create mode 100644 cookbook/spectral_indices.html create mode 100644 cookbook/tricks.html create mode 100644 cookbook/udp_sharing.html create mode 100644 data_access.html create mode 100644 datacube_construction.html create mode 100644 development.html create mode 100644 genindex.html create mode 100644 index.html create mode 100644 installation.html create mode 100644 lib/openeo/__init__.py create mode 100644 lib/openeo/_version.py create mode 100644 lib/openeo/api/__init__.py create mode 100644 lib/openeo/api/logs.py create mode 100644 lib/openeo/api/process.py create mode 100644 lib/openeo/capabilities.py create mode 100644 lib/openeo/config.py create mode 100644 lib/openeo/dates.py create mode 100644 lib/openeo/extra/__init__.py create mode 100644 lib/openeo/extra/job_management.py create mode 100644 lib/openeo/extra/spectral_indices/__init__.py create mode 100644 lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/LICENSE create mode 100644 lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/bands.json create mode 100644 lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/constants.json create mode 100644 lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/spectral-indices-dict.json create mode 100644 lib/openeo/extra/spectral_indices/resources/extra-indices-dict.json create mode 100644 lib/openeo/extra/spectral_indices/spectral_indices.py create mode 100644 lib/openeo/internal/__init__.py create mode 100644 lib/openeo/internal/documentation.py create mode 100644 lib/openeo/internal/graph_building.py create mode 100644 lib/openeo/internal/jupyter.py create mode 100644 lib/openeo/internal/process_graph_visitor.py create mode 100644 lib/openeo/internal/processes/__init__.py create mode 100644 lib/openeo/internal/processes/builder.py create mode 100644 lib/openeo/internal/processes/generator.py create mode 100644 lib/openeo/internal/processes/parse.py create mode 100644 lib/openeo/internal/warnings.py create mode 100644 lib/openeo/local/__init__.py create mode 100644 lib/openeo/local/collections.py create mode 100644 lib/openeo/local/connection.py create mode 100644 lib/openeo/local/processing.py create mode 100644 lib/openeo/metadata.py create mode 100644 lib/openeo/processes.py create mode 100644 lib/openeo/rest/__init__.py create mode 100644 lib/openeo/rest/_datacube.py create mode 100644 lib/openeo/rest/_testing.py create mode 100644 lib/openeo/rest/auth/__init__.py create mode 100644 lib/openeo/rest/auth/auth.py create mode 100644 lib/openeo/rest/auth/cli.py create mode 100644 lib/openeo/rest/auth/config.py create mode 100644 lib/openeo/rest/auth/oidc.py create mode 100644 lib/openeo/rest/auth/testing.py create mode 100644 lib/openeo/rest/connection.py create mode 100644 lib/openeo/rest/conversions.py create mode 100644 lib/openeo/rest/datacube.py create mode 100644 lib/openeo/rest/graph_building.py create mode 100644 lib/openeo/rest/job.py create mode 100644 lib/openeo/rest/mlmodel.py create mode 100644 lib/openeo/rest/multiresult.py create mode 100644 lib/openeo/rest/rest_capabilities.py create mode 100644 lib/openeo/rest/service.py create mode 100644 lib/openeo/rest/udp.py create mode 100644 lib/openeo/rest/userfile.py create mode 100644 lib/openeo/rest/vectorcube.py create mode 100644 lib/openeo/testing/__init__.py create mode 100644 lib/openeo/testing/results.py create mode 100644 lib/openeo/testing/stac.py create mode 100644 lib/openeo/udf/__init__.py create mode 100644 lib/openeo/udf/_compat.py create mode 100644 lib/openeo/udf/debug.py create mode 100644 lib/openeo/udf/feature_collection.py create mode 100644 lib/openeo/udf/run_code.py create mode 100644 lib/openeo/udf/structured_data.py create mode 100644 lib/openeo/udf/udf_data.py create mode 100644 lib/openeo/udf/udf_signatures.py create mode 100644 lib/openeo/udf/xarraydatacube.py create mode 100644 lib/openeo/util.py create mode 100644 machine_learning.html create mode 100644 objects.inv create mode 100644 process_mapping.html create mode 100644 processes.html create mode 100644 py-modindex.html create mode 100644 search.html create mode 100644 searchindex.js create mode 100644 udf.html create mode 100644 udp.html diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 000000000..f2a8fdc02 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 679ccd4493083ab184125e8a170530d7 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/api-processbuilder.doctree b/.doctrees/api-processbuilder.doctree new file mode 100644 index 0000000000000000000000000000000000000000..7760bb97e83ab0c36b94fae4a95e3b18f5a297f9 GIT binary patch literal 14657 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 literal 0 HcmV?d00001 diff --git a/.doctrees/api-processes.doctree b/.doctrees/api-processes.doctree new file mode 100644 index 0000000000000000000000000000000000000000..52eafacb80b8a9eca6765e53c78400f96007fb86 GIT binary patch literal 1098930 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 =>qf!WCsIR;9Ek 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%0AH2(ky`m_ju*bO!}=uZ^@ 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 z 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 z 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�*Yl1)!{(zU<)nN!Dz5OF;*MjI)qh5Mw=r; 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�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 A00000 literal 0 HcmV?d00001 diff --git a/.doctrees/auth.doctree b/.doctrees/auth.doctree new file mode 100644 index 0000000000000000000000000000000000000000..55a4ee6827de045439b747ddcc8ef2a0f5d594c6 GIT binary patch literal 87690 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 literal 0 HcmV?d00001 diff --git a/.doctrees/basics.doctree b/.doctrees/basics.doctree new file mode 100644 index 0000000000000000000000000000000000000000..14c8ec411dbc9561c45e12475b63fec044bc5650 GIT binary patch literal 61887 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 literal 0 HcmV?d00001 diff --git a/.doctrees/batch_jobs.doctree b/.doctrees/batch_jobs.doctree new file mode 100644 index 0000000000000000000000000000000000000000..65f7e7b9caee168044eb6e616a1f765ea10ea047 GIT binary patch literal 64328 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 VQeGkpHI|_el8m-c1ht+T`~QqcT-X2r literal 0 HcmV?d00001 diff --git a/.doctrees/best_practices.doctree b/.doctrees/best_practices.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ad9dab3fdf856933c0614ead4ce82ff87c9e3082 GIT binary patch literal 15074 zcmeHOTaO&abtXmbZMeMYvJpg5ZY?8ocQrG+Tu}^7Shgrdb}U+yLdxrqJ5^oXJwxtr zDBFSplhCfVtE;O{ojT_`mpb*$neYAU^&{$^JS}3+kM`P*>qgvVBA4?n?`9#3()_#m z>aXSRhGm%d=^P5e%lf zMOf(iE>DK*JjSAVE8b5%9<{oG4`T1#2;#+h2RyV(9<~#Kl|6m)Sk?in7O+&OpE$8s zXZx?e<@v(Ge-0QP1bWLX-n06d?=o7tuoBjH615pLer=B$ckNLooIbN+@QEdnuzEbP zQjb}*j91^V-c-x5UQx@izU+4rC)sc7yT=T!+*~@vh+8n#*=rfQLk^HBxT%{T*f*X;r0&#;yZz8IM!O1N5U5=#AwY5I3KDWJ;s7&qlf+HyQFhO2PhD{m+nG8EEe@42rQv@ zqax65+yT{*8|*jL+IAr{iREN21QpAJ9GN9#f~Z%L76^`2@AxSLaomdi?$F9&P}k)d zsHd+;#}#QYChhvPChF>kal)~7$dsR0d^aML(C8q#i65szOVf(CBtQRSGCi+k;l;Wv zK$YfM7<&%*Sl6V*`YsunZd5y08}Ezt@g~dGtB|P|Ax}&4r3cvgAAIw7@80_w0iz50 zf{|6ZSeK7-4VBE)hIr?rtRehZWv)i;VGEvH({gFnI-?$I9}YjtGCLoGm>(jYY$~Lh zuOZb5v-`$6pD_*csh452`U%f~B7`JHxqo6-FR{7VXAs8GxMZTMu4~AFtm{R7SJ{7W3R{q#==hsLxOwqLV6&5t=Wip z3Qh$3$})Lb=tQE~fHQ=JV?L3^L_2+xat_K@Si9f0Fo+-e-(Xhaiy<*GWC=V3u(22T z-BhfWfW39+XMArJX#eDQw+3c;T@Aeo_Me?f0*uOEoI+e`lYUsA^!~;!SH|w0pPsl? zs5HWk>45VwgHCl+k{@bFA`LEF+`x(Y8M(qv0M&B$Ew~GCvB5bG1?eGF5~K|D&guAp zpYB_oeJH)~`;o#VGZ49ASw!#QKY~|Uza}Chua-Qg2Rek>ak780O_KrzU}jM7Ojia=>$wm zAHqf+!?MD;p&&{HL5Q_O#NEaK!?AHvaxsLZkb(R+^+&2wEC3&PGck{|z)6~x=l4AX z_Pj@~75+a!A^;AK-k?%;h##cJslBH3~JOHlfm}!6JN6Q0Cq)dj2CFx&> z^Z&8PTaDqwgF3^f(?<(*A6MKve1m9>MkUeTsN~@j$XRVra(T3+_UO-Q3avtw5oh3z z%BU4-49BaMqiZGpej;(=7iaco<7EXv85vY~zzu=HRI>MJ^;prvP{T)U&Qw zB1Wpv2tf(PgNO#EH29L7^i@TBTvGBNS7UY{*>%q(!r}w!OXDZWzCsd|$>wVFmsWKDG7rjB;MBs_mP8M}N za>si3Lxt$;MPk0$@KY;vhR7n~j~$kfL!P*0ewwVUj>3C>KZ7655!UXi%s`4`kv>J; zNH-9j?FRCST>zqrdINk&G{EnM$In0l`YMHH9$a)J96GG)AgM(23K-WYh5L>aX0n^W z0Tq=CBdmR%!SphGl?imx{9$gpF>Z#pt~})AN8)7RA}q+_q1{CXF(rohLY zLy$(tPjz4Y$-_j~V9Vq8lI+?%zdbh3M8F;V7WFiFhW zM#)4Lb;Dw;*mL0Q`--y@H=%SLsDA}>htDy134;!9(8zE+jO#!kX$(WwslJxW#VKct z3|C7TuGYBWs?4Y9ts{VYo4JW*l&sMNGy3G0DJQrBW$UNjDiAb6PD7mu7l!BX4wc#Y zQ>?x2%1!1knan>azfdOgc@SBey{*5)OKlgBE*YjgMp%Oc2}f>~_YhFyWDO__knLG3 z9{jIwaG29zRa;Rj;=4$>>?3mtKvpRga>nTBNga8OvYL>6%~2)`xm}z^tFv#-Y;`oc z)^6M~Sy4(Mt6uuhRM{7NPo_m#1SHXs|3z}wZ8j*KD?FaT=Tl~+Okk^^tv*diE+Whe zQQ1L`+Go4W#rKp<3MuV?b1#OJHXXF8uGE7PvCW3IUwDsmJ&Fn|W{`CuGZpcDdQqmu zk$o1zN0ixadYH7y#UJn_HR*;+m zN(syL0j=cwIz4sXRTPB~{OA%K#JxT$6aWpBoyCD%Ulkt-4J4F0$2;=bi+Bj>t1?h( ztPuQEBkKwaGlU*NZHwc!Jl?H;3jgU%`1+K(?*?3q(L)irPkV$)1KOE#V}5oPT*=J; zek7TBX;fwwhf2lGcVXp_)&`aDxV> zDl)+2qnTg@?f|j`6h{C=YOp3Rz4gW~zF~X-4^|9jzU!fw1Zm;AjWq7$)cwoFU%b!a z2T!5H;2eGt{m&5n72U;B-W~} z$x7gKR;{WGkdC#7#Zl^#yic#nWPO&Fs)}o;$#_!e`I+(y6?$%g+_7wJ?dyqD4gjb9qA*xX(B2#kk6v0E6<*FVXYozUnLZgdZfB7Jv_W5)5Nu_gKK zPn3MrmNm}1>EfhcfhxVO);5t1Wj2lazg94C(m}($vuX6TQQWLESjIr*?v9`-WgT=l zutu0ZT>p5eBPel9Dt>UJ=k5Ee5~e zosGK;!!eD`3TaUlak!|I*D<`Ne4LJ2M4(Q{aMuB)ibzwb>liaBN2V{oc| z&?p|WB*4?jly_}9J*BrGWd<}S`?-9yhhYf9YcD^LC+V6^l!f*l6@cU)$Wv4U79&1V zj)=g3=X487URB1yrqrBmuB>3b;)CK23)qq{${^Y(k!4`#BeLN+LHRf%&$2xqTto^Z z6QOpY-@s+XLVP{UVni==`%Yb{D`4ON(!;xc z)PoEZxbP?eg~&Q%-TV+oI+)8L$S1-Gk{~i+hq=&TT)qf{#-bg%H7DmanlO^FGf3L0M4+uJ29?~L1yPVJTvH;cap{iS zdqBI9R(+2S%^0m$K2XIXUBN^995w2{R|dZW9Upv;etiXw-rzPRu72eGaPVn*x=6qN zn11~M{klrso~K{mpB6!8t*`jb>RKz12xPCYKQ~%`GNZMAoHG~ z&NQ#Ls3(2j`=R%rdd3UojCAg&XQa?k&8WPWOrER@ky_Ttt}62dU$9i&LRew?Y5vt) zbU)lfgwTZ@9+Yyax;{SESoU5R8F9hPsy=#jWPHGWlA_#FJ~2L4@dnlxNQSFibgvDj zu1eIwmd(9Vw0{?j{Z@V(T8Z=?^c#03>FS`KumrS`<`ZSJlBE;47#3tM$!?!6UgD}1 OFt>mwaxAh=`~Lv=8UG&u literal 0 HcmV?d00001 diff --git a/.doctrees/changelog.doctree b/.doctrees/changelog.doctree new file mode 100644 index 0000000000000000000000000000000000000000..1688f010707336de7e9604b810032e67d67f7995 GIT binary patch literal 299869 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 M$BQ1ft4N~!H_SC?WdHyG literal 0 HcmV?d00001 diff --git a/.doctrees/cookbook/localprocessing.doctree b/.doctrees/cookbook/localprocessing.doctree new file mode 100644 index 0000000000000000000000000000000000000000..79eca6683eed4d75c863a65e8bb0fbf221089dc6 GIT binary patch literal 26209 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 H*2Dh-717z* literal 0 HcmV?d00001 diff --git a/.doctrees/cookbook/sampling.doctree b/.doctrees/cookbook/sampling.doctree new file mode 100644 index 0000000000000000000000000000000000000000..2a372287f5a840eb0e263a1926b7891867539de7 GIT binary patch literal 11547 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 literal 0 HcmV?d00001 diff --git a/.doctrees/cookbook/spectral_indices.doctree b/.doctrees/cookbook/spectral_indices.doctree new file mode 100644 index 0000000000000000000000000000000000000000..db72006866ab96f1afcac459552ac870d034ac6e GIT binary patch literal 100559 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 J$2oN3{{#MMbf^FT literal 0 HcmV?d00001 diff --git a/.doctrees/cookbook/tricks.doctree b/.doctrees/cookbook/tricks.doctree new file mode 100644 index 0000000000000000000000000000000000000000..978defdeaa3cf919bc96d5b9415f87a00148bf39 GIT binary patch literal 13022 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 literal 0 HcmV?d00001 diff --git a/.doctrees/data_access.doctree b/.doctrees/data_access.doctree new file mode 100644 index 0000000000000000000000000000000000000000..cfe752bc616c5d596060b8e246173ce48155aae0 GIT binary patch literal 51281 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 Bv%mlV literal 0 HcmV?d00001 diff --git a/.doctrees/datacube_construction.doctree b/.doctrees/datacube_construction.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ce53dc5cbd6d429d7479a4b039c89b1531c8deff GIT binary patch literal 35554 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 P?WV(xCyBVuZ0`R9<1Txl literal 0 HcmV?d00001 diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 0000000000000000000000000000000000000000..dbaafffce5266b8bfb845a1be47f94eee96da3e8 GIT binary patch literal 1455199 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Ɛ#j`>>CsDVuULR=2n8m$0g3_k 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 literal 0 HcmV?d00001 diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..b5cd1487d2a552c3ea9bc044bd6c19f0d7ce4dd0 GIT binary patch literal 8432 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 literal 0 HcmV?d00001 diff --git a/.doctrees/machine_learning.doctree b/.doctrees/machine_learning.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ce578f815683acdb101e30b06c5ff2bb0877e7d5 GIT binary patch literal 18547 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 literal 0 HcmV?d00001 diff --git a/.doctrees/process_mapping.doctree b/.doctrees/process_mapping.doctree new file mode 100644 index 0000000000000000000000000000000000000000..e89a81c095cd605156e1b5cd2fb6fb2f1614179a GIT binary patch literal 309182 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 literal 0 HcmV?d00001 diff --git a/.doctrees/processes.doctree b/.doctrees/processes.doctree new file mode 100644 index 0000000000000000000000000000000000000000..61609c6187e91b4e392c1a45f73b3e1c85ee684f GIT binary patch literal 63077 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%0ANZ7Ez*>6OlgQK(q4QI!l7EP*}!1Cd;T(3 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%0DYdxkBHh1VUv>$RtOXW$DLt) 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 literal 0 HcmV?d00001 diff --git a/.doctrees/udp.doctree b/.doctrees/udp.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0ad5d164b1d568500677b5bb47cdc64dddc27d96 GIT binary patch literal 72088 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 literal 0 HcmV?d00001 diff --git a/_images/batchjobs-jupyter-created.png b/_images/batchjobs-jupyter-created.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd25f34c04ba92c79540389a4291640ae846cc0 GIT binary patch literal 56503 zcmdSB1yGf3+cvteFi>nk5KI(BNom0ZK@bH6R0JiZyUPTX62YKVP((^V>6DgI0qO4U zuK&2y_uqTZd^6w7zxV$4Y-XPK;pJKDUiW>)c^-AGCzmdYuV1@!Ermi^FLC~?EQPYX zn?hOkVf8BfrP1k&6#lis^t8m4)%dYnt^OGQpWf`8qM4kbrkUk!6Ag-%fuX*}QByS& z4Gja+JBDUM%QHmqqGRMmXG}D1n`s*w?7pI{uR&4JP~XkLzgyDWU^f>B7uRl1PC;&7 zK_1TCr=)kEIVX2TR>44wLfK7`ID6`fm4APOtzGHFQvOK(*L`*Vnlu8%?ZU-1oorfr zLpN;jlzlFHzT$Al6_aB>Wo9&a`jnf zG+6Z2cG*v-iUjq=87NoH8TqhncUigV?@v;+-k>FA@86$yD{h|s^Rr*&(*0w9|Mr~r z(5W4NUt37skJl!d57sBm?YjT?&riXL@GD`mPWjAs6mf-h2X z_2>RxbCl{p_rK4Xxx3ly->d9Su1WrTrSl?h{^!Rt7x0{-UA;Q9sVV5kj~~JUKc8$l zI6O6VaQE)rGP6ZeQ-cixHsg2Pdjkbboh$}w7lKFHA5kADo$Gn=K7-q`t%tEHdR}sO zXjGo&wXR|x?(v}}(W;m)1`WP0JJ>rv8eF-ub(fGui1k=!>csCGNg`E_StBuOYnED^ z=XQz3GkNom)kfV+p%b#`U)XR4cOP=Hz+^>3e!jC#@l8q^oePV|*m z($dmujdhmHPq%sK)x^n+Lf?@x$}2RcK_(8szO8Z z{A|HK{{YSh-a?MqmrtGQH*9L`Wo!|h&uqzOrfzN*8VZR|>pM8oTgjMfV?U;kVWbi`BV3=M}D?b zogZ&rxwO$a$nJTA+fL&*atEXA<{~QvWMq84y|@~l7^|}_w9&EYQv`xbn~W5DJE zlJk9CFXqBE+@4oND)N-gy12V*c9jNv(XZpJ(zl)HeH0gG8kqaMN{^8-akjyd{$)*a zVlxAy)WD;4N#@fWp`lV#Zxq||_Ma0onTztF;+q-@wHoWBT(O?IX}5tfGfZ;ZC+Q&f zvU|(<6H~e*QgiL?S(TDZ0>>w0W)oVap18EN6nbf=T5}uROxZj-S`<>BzCMJd#W?ih z&qEx~*0QG;U=6v&M${g-nIblGJNwSojVE7BHMSK~GaK~{w=m{cB$;acj=1JEwc`kR zcxJC~n%c%jCM+K%C8b|49zA+=IZ8?3@Rg5uA3RuoK+-$2wDefMJ+q+M<1@}1I#?Kw zFgr}|A?sE-&8Fu=qG4mIk3mE7&s1yEJojA}q@-N0&a~qWe}$|$<3zJZSXj8u^neog zCP&r<8OhU=#(6n8!-FY9zdlGaT>o+>Ykp>oir;ZjIC99<>U8hs2RoFl_J|v=qNX0h zI&N!g%N}mYPO{MY!XuQ>>Sf*In9(9|_}SWhe4I0LqOZ8;Ekt|;+v2&@79@0BYwqVC z@vF8L-h5Z(^geA)^T`*KT8XzxnfB&itaSQwNKXkpCz7_uo^7Hm4ix>a14OTf6Puod>JV zF3pBt9_ozFkZrwq-yrsDZr;yLQY?D?{jw@unTzhb1dbc;{Ygz5o1rE?Y1x0rb+V~G z<<-J2q1cqm`@+N8*6d)9%{W87VX`+Dx5z)SIM=UR^W1N4YWcJ0&oXmwg2XJYujfshgP|09& zbaV_qu;N-FTWLYTc`@8rf#<%OB-4Xmb*nUMzi12ej(yb-Puvz%+(KKskt_J9^0o-^ zH|*ycSXi>83S2E1eHvdL9=v$=?7?4#gAR>@H{HgVE#I8#P<6ez*hRf}Fqd&8=!dII zkVS~Cz2z!){?#YF4(G1%sd3MWJk0Sa?%*Eox{SAu7K))LTw5={H}jKDevr=V#YV7kqyd#rXTy1t!=kXcgRl zAm1fq+`8h$64%b_(aI;5EnC)I8NP$;+ughG9=Pz5t|QxN{XVq21{(MNS`Cfy?w=K( zuDW17b7&UR<+;+Ik(J%7^|D4|nkG>B`Rv=*k#XFCTUaQ~0=Ji0oO`M`(bt$_`60Ml z+yB!Qs@;R76&@0vMRmo8-}4>1*rDBb@&EV}-yger z==T>^w`I$W^vhXp3YjV~PRK{?XFa^OYv=Kg#tduIxO0?mIc1g@(^wPYR1~N zgKhU<&Kk zxm--X`|ZAdsg|ePs2+=2x_@J0Qo5e(JErS;ED3DVM<9{p)x^vdc0k;I|;&Qml0rYEp@8V>-_cp!u0HHYj=0S zU_*+4X(!Fq!TEinLQ561;|(d6a^dnvck72tH{RjR_m%1ixI23^MyS^>D4xY znzXOp$Tj?;r2c-t_J#P$o%(^gHwvv5%)-}BM+&~wXFC{`v)P2zDKo(?<}2sVw-;IK zpYj_`EAhC6ho}?+o zhqY+>h2wDD%$=Rq9|GA01!vP`|mj5ZThaekInl&3OI_yA3Ii!Zu~om+f&G* z`O4SB)z#1FHf0U0+d-!Lr$CM_gc zzxMNEK(XR;V3!Sf%(<y zS!tUmI$%fHSWS%)_pv9c7bhcXO`lkc26fIxo@G1vH21zh?)^yVPPu4yu{=|upRQGX zH{8@>o@hojNEUyQw%Ge_3+tesM|QyUu715kEvhopC1>}YV!t_Frd|F1so=&<3tcl$ zw;%Nl?%1#tazr7@Xl_zpu<6CE{QUea+qOMYJFt$WZoJIz%m&kaey1N9={NZ5E-v~g z#42($rMnFT&>r>=zq;=Lr{}{X3XdBT$`l8@h+_$<~QHn** zsVIod;Qp>rwo{fSCf`&G1y7|hW93HvR#!=}Sg)X<&^CoJ_w@FDn3i?|lezWql|x1C zjLkn7+?ASw9L!IapIei7yvXh;0Rx;qQcCG|0u2|(Hf-1+Uw%eSEiz*nTO5BwLxXlx z`U$x~z0MbM?_C5{)t2A(9J#{gWBfeuvh2d8C*Q{Pt~f2(9$PS|yj|61JyEnz&!NcK zmv&oAZRFX*X{k?AnAb6EQT*(1k;&w!R88xn)jmF!DhCukeO%6`>UCb~VpdkE)Bdi zxHJykOg3{;k*1ps+jVezb*Rwd@R-CEM*&vxk;JLN5+4td?_Y!m)q4%*E?U|b$k*pP zcQ4E(B{_1FPKUPmGN5^l0YkTlu)8hj|T3V$!j61|D!{j2}cSK!zG99Po*3jNo77$#Y zd9NmZ`Q!TJ7cLiGUg|1hk2&f;Lv2YfU_Z+f7#LXH*}6#h-FcfNvkMdbW_smYR-DVC~U!7y5-k`I$v(X@82(v-Av|C%cD_MRmGLnK+8_{ z7C4|+-oAZXk!?G#eyw^XZyZ0fb^9h@j$9P|7)3TLtmdUxmw#Rjm;V_oP8+Uf5zjKq zdoF8}b*$;m|M0c*2?fq}oU&s_A= zqE9z-pKb&DJ+x+xqd}rVnCt<`mu!Wj9YqosFS79RZU;s(XiAq(I@U=Ol;xb=OG`uc@4r=;Ycs0ixbR~8(d+G>8c$&b}-rYbAqg@e{pknXJKQrj}^9kw!_eF zf_kE{TFm6>qprHE@5HmYdU_io%`S_-%&54#M#b4^-h?K1Z5{XLbJjL|$1apOKOKHp zc4j(tOU{|7N7_R#7MJ^m+7-o3W;zIquawPg@R2s0y~UOF)N?&u-25Tt`Wg3e`Qu7G zy*FnA2aSCXWT(-`O!JA%kL*8Ql=N9Wk?%ta%gQV4eWzbszL&Vo_*UWYY<1n@HN`J? zE?_Rzh1_r7SCJ3ZZYWVGXz%CaBtFYLfLT)upnW+%^TAt9m8 zvLKmCA>G&9y3{{D-#!DLej!{w@{rsI8r`a>Ty-u^+etl)`1fy*)=}N5Pvq3E{gU8Sbm>XXgh{!&Q~N=uApf4ha{(g#D8@V3xK+p=o2 zV`lY<6^eirKNTXeWG`3L`GF#-#(sU@_~|NtV$vo-2jQPtS%ki;l`)k%&a!R`bPQ~* z=~uWs((y`wPisTg37cE)F4;+9>p!JVY^SV2+xu4L5yHS{bm-c1o$?R#ecf?7uU-_l znhn)W1*#Q(e~(>5L#q`X9c>&j`R?4-sgahqd(J!-GJmR4_ciuWv_imwjq@ zQ5i0gYV+cO#|cgE7mkb8htozS<3}jKgWRT_#f+8PAZK|N`>^>GA8klBKO!L^fo0Qf z6*>~(&u@I6TdVYzp`oKp)6sy=`Xp2D!&eWNk3wPM(kk^AesYb8k$qu^Pv;f;_s1KU ztbcd;aBErhMJ7!5hDVP^$avl-D#!eAHf`~1&Nc^U!oNmOVr#sSeJ@G>Qp5L6m%_CJ z-7PIG!n57MdX1^q{I)GP@AoQ8#=o=%vE`s(;p(wtn37AJuN2 zEqv3s(opL^jc~7ZbmoC>VC$~#pTPp3Y&(nTYrhty3^i{5_pu5qkDX%vD}<#q-6q=_ zKkbtDs`n2X;B6UAd;b3Ear^aq^rx2atUc8+`@je<@a5X0h@18OcIzzhLrW+h?6T1o+>K_aR|E_zJ2{pp3I*=Jx@JY z6=OeV5nTU|Cm0%*fhRPy6Cne@`^8>{2j{q z8*A-qKY(W92Wz6*Em95PHD$+LHr|Mw{2 zQrlf}O)?%7xS4j9UjB3?OsWW@^c}_SqkLp-2HlIlS3f_A2l+^pUyYSID^3Ut`{SAA zqLd&EKmGTLsOp9x%IG&Jw{$#rHpa)tr;h)+keHZQKlJSHXPq^9e{z2}nyyA!;E5?A zn!jts(^me?heT+{bS=eyu?^XB95P7@9fqKv1 zp&nHUWgYzc>)!ufcmA(s@Bi*5{%bk=)~jo$PM)M}cX{;v#Sv(lUo-6AVb>YWj%!P? zYyp*@uUq&xSzGt}uyj*IL`1e8yr4H9bAH zIq01i0|XOOQnoNKxVX7#{`k+xo{idUW@~E;#W2x&Ofxz5^Jfm+vG}94+bFJ{WB#DK z8!3&AjY&EO|Bj>V#`eRf%D$KQ@fn+#sA3&rd0&Eb*;^6j)pmIt!guQ0e^b?d&r54f z@_)(6KV2!jY|G3X^nZD+OOC$2KI@HmwsTLnW`RaQ$uoisoZ74WcXmTg7R9Y-C@f@+ zPTu@?z08yqn%@VoJS{1?_L*R0gK7-rQoPNSD8nsj_eb@6O-FlsJrxU)vl%S zI-tLLo;}{6K@wY!iHYe-xcq=&PA8v3($dj!F2SJTwTQhSL^*4RfImy(xnqZoSK@ppiYvgCNMdqwqnxcv z?rAeK9?aqjYU(X?bOi^4?CwWK9t{w()C2`PD<)>rUa$#xLbsAv|BUcm^ux~C`Z%pJ zmSe}<-FFH73X==9eX6pUj(?BNbOkYFh>MGxLaV)h|Nd)XYc3cC)VuLgtswi~hYLlZ zrWs}|#CxiD#h0NR{(#v2E;!gNdC#6b4nRWAHtW9R9}qwVyGd)H_Um2qUuoujzs>AJ4v&hpW(OzD zvcQu5nmBrXqg#O!K0&a=Y7#r^?T)J`^Fv=*nrm9348w&1Hnm%v>GD3Ju;IM)mwo&8 zQ2~Q_dir}X3Ot9(R34)yP65Cw!?T+-bYX}eySwKBQ#s6AIZU@u2U>U1Q%VDb@&SD3 z4eM9^Sx|-4Ow%iNHTxN+w@i<;CFzvMY0213_DeLS+oR66;qO_c$IMS>rCN`jzj*P( z?QAEk?B$=~l7vqlyy)Kyb?jts#Y{cFML!*kr@edjP~F8?asvz;Q%Tp`b_0JjsT_Ln z37s+T_)=YPE~YTAYSqO+VV@7ue!rq_3MtzT9N%qTF+bJhNPP=8%VWs+_X2vhd<`8*$~ly|-e{$ddjj7rFr@MGfO6 z-*w!t#BQ|k;P_wz13NqW^{+a)b%q)Gb{s|t+0L}wcGE__e1;csM|YqK;2wB)uq&sA zij@EEB$m|2kB3k{#M<)Q?iE+P>0)Wb9TNAX{2xxnK2c0G+GA{Nd@X$kFdVmX(}MZ^ z7l*HMDXS!#JxNUDFZCB#eiAq1#syOm8+}=(=RR?h%Fr4;-a#$EA2%+q!m}viZZ?0m z*=W;nX7nj$Q3v6D;_mIQR2=qja6FqFsB1D+0TMVacB(8$)B}d#%@hlbo?q`lcrE&? zPeGcxD=J>0Tk-Ms{yRwmWmx1RqoZ8E->@qsa)|3cfTB# z3=%0R6J09C>X5;`n^tP=bjkh3V3?W(2p7kAv`o5b-FCL-U=NX4ouGJkTQm@IJzLv3 znlf(S6J@KGl@zu8jJtVwDy(jzuY}XCe7b|(pi=zjyK_{DHWNBgvtPb`?MP^HSpNBG znI10#^QsVOmp?Go^Zw91=2M2hLY{EmKcVq`>+|Q&2a}=+P>wDaJH^1OcOK(%PtfL7N1l{|)x^^Z;S-FH#gZYFy- zH{a6J)3fRdu&l;YACZs9EG&EiA~e$JXSlNqn|l*F4!@94oZXBG8pL-Pa#V>93xXG3 z9V;;p(gukv#e8WuB!A@;+Otn!piVW0j@iJ<+B(Kz!G<`>z?F9^JkK1x{+}!WEyu6H zQ)2xC1L(D}v!9#6v+`<(!1v1`-KtqES3KBo_b0FZ7m-l=4`0pk8p$78CB7PVyN7eO z$f)m3`2!2KveC}377THJN4;v5$zZ)e`uymHr(0P~WENgaeK5<%BXw7VZNWtBJ9KDu zTn&N0ZnVn=HKryrf?+`y`|;7Dtzj>e!tuUWp*?~%zr6XaGt)bCmy^B6TAn3eWR?y6 zPD{_Vf&vPT(gBs?2tO7PD9&3n(!BG)vs^HVKHz%76BTk-qLh5<5)2uHI=eoU@vwM3 zK|O>u9KaUM9cusT*iB!kc~oH+s2!FTEQw!v!!Yd}kSN9EcyF{Lz{ZP+&= z3*&O2F>!icll6*hFEhyY(fsbs%62Vw#rX!8e*uF6$Nc4@_cw&?dtN3o$M!5a>qhN{ znk4YH+o>1pLe?!xeKV|2%xcMYcJmxZ9k_NNXvvA#{lSrzjvHGB2J}-e`k4g4ZeZYE z7O!8YIdNjx($RSe&56?8lp%@-fJ`hh+r2N@yiW|~&CDpIu$Y*)Fl&L zvfiC@84UAC;k%k*dnP*1jY00y6+%O*V>MhjHD;Rv8#mBwCm zYz;WByIn?jWCbuue|5|QOz5WV+r<2!Zb+@2lp$!nyd=kzN{oocjYQ%Pr}K*~W; z5C2AE^gX%4w1<-yZq!Xm5tcTgs#Srxmik zUMqEfu)n_;_h#MZ#`pN?)9>bUZTTK!V`ZYtiSdZw#BJ~Q(xxgBJd!F&1aPxsc2!=*NA%Ucim z{yVlgH{cT`cD$vCJ)<2&yXJSp=iAG9d3nJZH|^Z{T|vcOBjO12aUJiG zfNt$%ur|Eit-Zdes7S|qK{@1r9+<)bASRR$Ey2@^mgYUb9-&bCqj$tWV8eZ8LwZ2$ zh3)Ozx8B%-XxyqXidJZlm@^*zT5piK*4|!8tV>qekktxNN^enGQ9>A}e7r7?*}C!J zq28WxUdCZHd@Im?@*1{f*rm72pr*Yp>11mPEf1B6vl>y|eSD4K?+AJZK7%ix+6D%s z(Iu?=WAaz8S<`~WOforN?)d{Tx9b0GVkRc-hLwh+&x52mb=`48N#Sab|nc$^jX7 zqwaN~d*8TR}6 zz$((5nYihw!R2!ZlK^5S6(ka{a@}@y{%r#KzP7%p=YKRgF4?E1rW%=qrRR@Pu}Hnn1cMnK8F?!Ocdf2Y?t>Kl z^#p^GSdAjrIX^#T9sa@{5Yr!FFNXwTnn8WL>5Vr5BlaC4MRCHhE{% z@{RBi8Z-o?d;IJWwZkK&WMJ`RRRc)jM) zmW`hv+!=J2yamqBm%~UesZS5LSlQaj0j(Y!m?$x>`|;yapiM6;$fXOo@>o|{ znQC3F3Y^P+_b(kZK^RCq>&^x zphGkd4K2}%-O&ffM0?z$G%13oVu2{9IGcghYO-G-Gg$>O?ztEmAt7E;&MJ-{ zfVeGS-n$japgeeS=mcE4zu6ic=JbP0b6s|@%16+>mwXYcgRtIJ6h-pbphazub|_~; zHN}S>t{X$RwEXZByTPO`)S;M9rZBEW=7-NPfe-7=73q?m`r?Q}=~X`zQEqq-hp&D* ziz?2ZG0z2f{|1n4>5_L?`qH9p`r@o^`b;MSuyYR(8iX1eIu6QRFs#=0_8iM5d%`DR zQ;v)7EK|7&qE>EegrPpH5ak*f8Cf|3FX&;A!|W5(%-8r7qxtC(GjlE-U0y$WPSq87 zAF1E{O&LL2f!3?o*`paxh#xz4On9uAn^DNZ6?&_NVwZAY5u!@U)*ah0=cj{2?59D5 z=><&opE`A_XE6$rQqJ{zJ5Oc!d3kMtPE53_^9#;WkY#}nrv$h>@0Y2@a99p1`SNHJ z*(U?QJy{U_lwVK~0hh-hZ&!T)ck2_4K_ZBI3C~o=8&7i&x&Z<$;<(4Kn<>2L=mI*n zcsyX~Q^=N*{*5Nk3JHOdlauo!R^x&bE%QZn_5EmqA(&g(up94UH~a+Omwa_h8YqxZ zYU&M)1wR*zuuh^T0H*pIQeKJJ&ni~>@EI~8J@-z%@Oe+|5{MMW`Ywsbe$kd&99mi| zJCtHQ#)69k!_|aBh-a#2^yptsP$Fis9H`k$1GYVpnXtG4xJxdur>T1bo4A3|Uy4Qa(VQ+fWe%+}GSfbH)DJ-SBZ3H`dbtz#A@~o1OjOV4&4~ zB*d15#U0teEK-a-ur)RaD=b0iY239W7y122bTe>>2h)i}5Vt5*fTx|9;I~jL(92yx z^n>-ff4u#oSMwb4-c=hI&x0#nOSR&pU>?a|AFbOF3+=S6=6*%S3!sG2#kryKL}N81 z6h=lyI2T#4=oja$k0~W)0^!6YBp`_44B#*dpvqvZF){T6gRXkLzRD}sZY(q zYFgTZ5ahtc)VO@Q>yx-O<7yO5CM}z&;Wgcl$Ykku?0~)xI)5!rvt(hzxsc3tt5-K8 zJgf@oK(G4qp6s7V2;D;GF9NLuVGX)^4J!+Xff1on;*^7J!|bDguC$@(w}P6&aNfLg zr=%aRzDb3}k2j}Ck5EdnaKjJ~*5jI*l9;%0>((5UOk;EN1BVVhe)DEGDqqmCNk}YM ze7=3=InM;Ireg^~cVfsVlox6qG)AN3Sxhil7x?JUhD{=b^uVRLEP_ZIY@KRkYx{mj zW;Zo8omY(8!pv+aRajc!!k+%v&22qyNEJHw7rZUo2Eelga|~yL`YxbO0G4S>SC=>z zB?)lm?oi=rboS>*AwtI@tOE`QgTJUF(_OEFGO`F4tBRFEA&UYd6KFdqz=W_tTg@`8EH2opjG_+p(x>^G2}i!=;-cMLdrh z0GdF=q|;NJiPJq2e{p8ADbP7Q*?`ULeS2Aeyr!C3HsBA*PYu*1bSBBW!B3jqd(D110;4;uu z&^&`nF&4TxFMf&{SVmtxcJ-6@2$P_Iz$ONUA_Ny+3z)KSa&n5fq5>1;92E_o(OHy> zkIHF+*#A_rK}9J+xfvp_YSNr7Wma06nqnt2B>8T8<_RLK7NbbVt49ZUCKxJ}8B|>& zp4R;ae321d@WvJf4$47sL=U_PzV-O>!SD>{2=ytY)16ao$jFl=iHzw+5O6GyyvLjN z5n=t=0-4HD75O*3Pv#)u)MPk{zG7El*}a>B{9&NuqV-AmK=XdT7GnZC2dn7O5fUWL zqEs^6b5*m{xjbLLE`SEYdiB#*D9-y2AAX9YQXA$L_o-bKwF@hqSL_?N?z%9!XlVN9 zvD~P-%R^~_4@}&nb+Hr)`Ldg+RDcz8{l$wzm-p7KFC>5nOV2H!A!w=Z1YA8-7cDf< zyclRIqsYmR)o2BgGYA%g^**;9QNfY#Aj$wZ9(M$`?<<BWzGGz6dHKx{Q`8 z1wG)&)2Dk`SP+PLWj1hh`#PZ6gAi3QgcUm8U%^3vPB^h!rG|2Chtl%#&ns=~vu7X1 z3<~Vsdmof`*)TT24Y!;iVe4PjnZ?kb8k`k2;8*vRPf({gpL4l8CVsQ2MNCmj}MuMHFgRyLiHkF`CvXKcC<=W;v+8~#5@6;s`C2HKuRAT9)2VF0>?Qn-9rR*WFVml|6vWz z3ZZx8RrE&Gp4QFQEF!{*q?omCYng9lqDfB*c`95^T=U!f>b~4y$ zXD?lPRyl5Q7s;K+Xi5yCz(*t;F1EP>)suHLx(d&zCcGFwC0h6ly6XHS-5ok5Hu0@7_TjEa4a zuZyce>frv!o(mZc3)85VYc_1iQ;pfUZCftt3X&DyM;3rBH$g!ops>kduH?1(4|r4Q zbAv*}5||n5nt@8*T@!ckZj5$ASCFGSfGdu20WBAw9~44{N-5iofe$dilvLgI6Yh)x z-!L*{ZeSXgUY&D=CQ<(O4E1()Whpy5VLV+ccIG=^908prcw|N$-?x3s&h`h@neyJ)>j&}=uPtwwS<>inip3eg6xHXjDA4*+z)cE0-QN(&Bd z#YuXHUk%SARuzzm+mK3pOiUKiBj;SUBNfF%WNK2HprJ;HWs$ur1=5T1-3-r?a#ie9m6!D@$J$pBHF$ZWxO zB$I{|i&<~Q6Lh`HNhTWL950jt_U%Ivg*L-w{5Z=ey1#X@Fnp(XvVOjrf^&u~m#m+a&;G9^|UN zg`AM5&`?uTGlCidFqRMd(FoZDklwu2bGp%Kc`zmm;rU23ico3;-BL(j$Pg(XuX{~{9w zP}Zc4{~dC)zV!c*;B=3g_Z01h4VsyEsVbsXm?0B@KEZbqGO3uvrp_ua_rexh8sNu< zc7mcK3J0WVurC_bheRPq+Y7B56ABi1?WTtrE9>I*8Jg_IN<<@C?WveifS7eT~<@cZ%U3kehqtLcFsR@Nx9GjZ(y6B8WL6j1)YiG}{08U$W3xxtH zu=S|(7?xLqhE`x;=>MzWRzX&})0irNu+}X+V@_@^)kc5=2s~jb86qU(2m@Pe4mF!o zubKl#KkkA^Lv|{@d`-SINVE*dI1Hb7!e;!lU{eFz_I2c(9hOrzb_20vp~VN!1K~Co z9t;Wr5+qRb^XJbrbA`RL$RZ$0c?1nt4QLS^MV)?*c%NrzPY_i?Gfv><4626PoUbNg zY$#t^HHPw-Q_cD6+S*uy2En!|RQuQ1bg>A2G}=$yMDHyGp}hu?70yG)i6A*#wf)ri zNtW&u%ONCuUqS~$=um1qn_?#L58y1OaN%a6er-5 z-AhRkLXfc?!-dR*6Uy962ubw&l9}y>sStQXWF`QI2=9@9(6T7Rwt)+m4N~a?S7>R< zaY?J#s_+vV;j!m?rv~csNm=-OI~)8?T7itiZrIs?`oz7#DTQ3zCe>qCS9RoeXqZL( zF&vNZaPJu^FM8EX;J73w{P{c&<}yfAA8#Zag9iDXYY`oU>o2;kPO&sh9>r?evURJv zIUBA`q;rfvBO{|LGX-gqwkbrNV7o&l<|2KsVyyj2z; zv;+UC(QZtdEIqIlQoo6@to>^R>@Tc&YDF32AdpJvAk=Tf+;%EuI0RtPEdOlOa+<^` z&|-q@M%TcNLC{t(b-LB%thhJ>=TYTUIu#Wa49TO6jEqWi@pI>XV&f{?PgJI+zu#-E z15`*}ER5_ZadVayh8)Q@6q^A~06S;_hX+X^DNsr8Ls^6%S`IP-Od~R{3y4G^j^92p z*EcW&ko0>5nak;f?c_6m|6+(|`Q(r?1T?aNVIu(8-$hF?9&SDbbS;h|e+T^n*$_3b z_G>RSIzZ}>IV3v}Xi~c_{wOA@6Wx2+Eo?R9M>d|{rOk|>AlOqVK9IC1EHA3z627J% zpROK(bbSUFea&%u8TtTkX*vFeLin;{LLcc4QkU=tTp3+Yf!|@CANi3oj!Edi1T>-L zVp`3en8nPxe`NJOD8-e**IhzGwc>~7(yCD`rG0r?;Ah7QHUdWhDEB$G`x9?srV{ln z!LTXt)75a|@{n`M2*}V0njONf+Q-W)kP!l%iA1bwm!^m5n=iezDTC7UbS45|(neHc`UpQmO#!bV$)9=j1 zw}yM1y^|drr;{D7ubty!9!qU9XqcQ)s+zM=iSA32F*p%DWqvbg(hN8sJ%!tPR2{l$ zE-dz3j483Roo;_dae_vB0rZe_KZh`S3xNZWT2D4@jAAWm9~H*FeiD10EncF=%CGKv5m6Kbn(PR{Ur-d{ZNR z(HZa&w_&PsfcK!xeMuU8|4Ml;ik-0a=x0Q4K?2s*&+K~oYAvcix5ZT<>eKx zUGph|%P0T`m1Gn<*{mV5T8)+P&6GJVoF1j3d~ps@Au#DoUckI&*RUBHqX@p_yx zng-RMsZ>;&^zz#sjpwJ0aaS+2lfHgE?(Xg`qnAOJ&dY8enr`dS_B~u&&xy?nE(v~} zhayYIZ}euyc_jPewKx_apmx-|e4I1GI?_m#GDo>{-6izf?#EATz!~I;;Qtfj8V2f3 z#3DH#oJ%t4_#7Y4J0#7n_<1*Cnyo}FfXIR#M)nFiHxVWPfqAuD(5p?IXy{Kv+vFM_ z!kI`k-D}BaJ;AiT1x<%IIX%$?IMGgU!~AyBp9O?ck*FFo(T1A}*(VRO(nHAf08y?? zCu;kNZJC857!3Qv%(Q zM9{0!2(_CTZ7&N70$F{Ib?{lEXgi9aR^tuj2t`gPYyQdHcpN~@4{LOY7v>Tg}D8Fekua-O5D{j zMp)}kTX*;QFxmGc++%d`4IeOh9p{Wn{g8+iK{8#H-1I--W=Q_U$OPvSbJp%VNk1|t z2cZD?Mg!oB+1Lo;nfP8IZB3JCnz8@ZBB$fjf{HOx?+{y@aP$NayE82iZc8hw9<->} zZ_`oM9j!(8%E&Fw&sZ-lOd&+|_Ri0D5DsIs%UPOBBdpNADExrAWIVBGTdPzYH2~{V z$4Z3&X`l$4zqf29SMimh`Xoqfq6E2+gcYI_g+Zc@AG|aWBj4w^IG-6n{T14Q$KAV> zPx6tH*kss@4$a9C?cZOxKq)4l2d3vE>L`Mu2WYgXNS`>~;GXBOcZzNM;g`t(K#~AF zwkxDKQ(zk@TI-_0Y0%kB^g()L`2n|pEtn9ko%?Nq9RO652f6AH!n?3qLf*gENPe+u zP^mK=$tLtl;tOFGi!7uTh1UgD#?Fy#ZatBe2HbxNm}Fvp17^WGs@=|LOA+>O69*_5 z)C&DZ51f0`hB)&mARu6d+Fn)lU6M2)<_CujOu~247r_NS$Hu<18fo=-pBJ&ne-zFJ z1R0*CL+K_RYcVLn=RNmsJ#>kPzb0_^5$*)Av}XJIc)kI4g3oQuFSoMpcP{^i5j9*4 z)CY{gW|M=v)?C_ANi;%0&w8XkvZcmR9Z6UgzIaw$U7eCL4%C=*7E9>qMOd5kVYha& zEdY#?egZZ1L}_U$eQ&YBO?jGZ5)*?1=C|}4Gv3nM>l-!zqT_+SEpS8yiZr2HIy#5J zc<_}Ixq5w&dcc0UP8>av(sn^Yf~1JPAl{fbcIi7VBNwY)DD@RFl%0UXi)ib9MMMpS zmh96r83>f~>#ME;k^?e;`EhCH1^94?^N2}uD?N9>5TMcYh+F9C3lWL2n(26T zPe^6C_0)GdoCw1-6k@E9AW{Pq1i?~OQ#%7PAUZ$19HfmK*CNR;EHb~irk_85;&WK) z+FTIrhbM&7FwiI06E7BC2G@#)6x4tRNLe{)psJ9UmMhdHB`4#W*$5yrTP<0mEPMpf z0V*MG9x-cB(l;?N8HnuX7J`V^QK_Ipf-C*CUtQo{@ftQ319WpKjgqf&7!|f+S)T0T zvJR2V%%UO}M3S($HX{*8cU)&H?uNP<3!@XBiaI(czkoo))RFrDvY8MhNs~dDRfUJQ zWU3#XHn|fPX6KeVe3mp^XnvFS)zD#E8!Vd+pa!B`k}Zvz^c^n28Hmb6`(C|vEf;%} z{lM2MuJK6coWi(wxOw6^;HV~ya~Uuz2u-C%)y zEq~3awhBGY$GznH_opy|kejPcw?Q?xE{@2 zB)Xs}tD$idV;pB(Xk?6;Q70hNmQGIN+g<4R3>XPbA~lnhwXlCl8PPy^{QTus(V-?1X!{M(E>q`6 zi~8#lh9q7c+v{-(J{q@Xu~qHg^E1NoW$GtA+}%4t8;J)6;S_Zt3q>x;evTIb14=XM z2WE~gn}exZ1tKIOt(}-8nPjs~Gtu7O-WjR-_$vfPTi41z!ys&rOD4p}uet@r0$L}{ zU3B(<*RSasZHG>Xc`%g#{7@Z&&Zn)d{ik4}LEz{P2$&U4X(YilB-u%@3JL)ABRDdM zY2C#s6=ZFwZ$ZIHOiJR!3ZZ~p5Sbg>EC+Q2Jap{rNmbRofC8a7zv9^U`my432>#qg z%_pIV6G)4(ps-#4@))DUg;Ni(pVyv*+=4oTqV+4o(Glrs%J~Zy)PU^~&1u73p|p~N z-Pk>(5JQ>ZfT&MBi3%=>N^qy*<0e?Vj!U=xK6!{1*PA(aZq&f_R{dwF2-)Fw8n?3>|?hFvh!F&_x;!#7TAN)mXzy}*;s35wC2N~2}E-3+nVa5(8Wdvqk?prAbS zMfOkUy@1T0`T9yirzp_?6I*c$XbmmKrwA7;Lai^>5~m@v>JbS$#(5D zl!Bp@jQ-g}@JyWQqz5A)N2)5oHNU@{HRn$b+3-^hGT|cI5M|tYt{1 z5~73clLhESGDc~(mK1WR2yq!79PNUkqXEhRDIpf=1snj9g8Tr7u@w{7`Jj`8yKovw z0%JHbHPwZU10&8y#&6X}d?yeZG5JCX8HBg69)Cft9!r-8dRzgW97Yg{^o;jbx8i-ay9wFKP;_}1Ysr^YUMoMR5SS# zAR9z#4GqnHXp0+m397%&`qS@X_RHsG+g(ol+XWvBUoLB>`PUN*MBV;xw#WZhdhY+e zi=HeEy_u;(@EBl`lLpML_s}#6gMc_7^8SAwsV2*%siVF9VL-qxY^M%b^;ktj*MENC z0{EUyZO6Z3l)_)MW;ah^jsK2TDTAtg9;Xc`1ZPA4aQOb874hWIVzW7DU_W0#mqBn%6^v@WPi1M@Ed+N`VG%3g zY#a#SXC(2!JhNf)5DWs7`-^Usdqu?+lCwo2s7bM8hXRQ2@u5dcz$+hs#Xw1Ifhy7k z&w4AX>_ec5WgO}S8}Z-(UpEvH?{d0kL!dCE1_$AC?rxGl1rtRjyZrgqDPWpxXwtrK zrGX%!((MMJA*$v){B8<9egoymks~S(ojOVg9SWJwF>*8eo^Mf)>c;hni z)nZ1-nLxvA#lj^4OL7_;`Za3?4hlU<3#2`-h7*{(svPRMdl9rgpvK|a)Wa#oRieFq#^5{Uy`+6TE3u^SxqBXQ+3 z?sxyr_PiV!F2{2aL@347vGh(^*oa&aIBLsC0n1x9DEtEmP=^==pgp6%F0fM(sSbm6 z0o)gIS0~LJreL}%WAPk76G=#rBXl?@6cQR5XWDfU6}=GrmB33Pfq@v~+%Dxa*nEZ| z$KMT-qx$zAmKGPD{Q0&OR|tjB-0}eLkf1q;)EBX(M}FThBn}M}tz6>9V{Iaqw-<@i z)VaYFe34)vGG3n%*CY})cx>3^;8ozg#9JepBxb=0JIm{c!ZB_u=mX5u)YPQIqn#*z z)!7F9g`BwW{LeC46t?8um+fJ@kwu+7jxbCE0p z^5eq&HJ57inB6A55Jdz@YE`a=P zGeq+bu>4>#5FZivkjRQX;5FrpO#fE6WQ6NngZI%tsA%tA$M7zLRsSJ$n_2(25xW0< z&Em)Bq`Um@E+l+b{rPQfkT;PCQ9&Dp6{r@JA#=8u z2KYf?L`yscECqR$985+}#F(u`b@9FJR}-fdhh++q@ft~SaDfg;O6~B)NU0_q1CY{h z0Oyy>;CvF&52lI3sE;uWB<)F(O}Ir)ng_TSE|`cU)Q=;pB*qEjz8Ra7TqGo9KR7En zh)4B5*n1D4DzkP?6htwAVnTuhMFkZ_6cveE8xty`B1wso1PMx%j0sGDh?0|{AQBbH zQIVWfKqP}C$$696_gUKg|L5MCTXW~mnVOlZ>8f-3?{3Atzi+K~z3-FO6dYrI5CTD{ z_8BqK_?xg+uFI}#QcyHtw&?^*Nnjjs5k%_)cY&J#<;&l1_gqNy>Bb?zD;ruIC%GY| zdW8nQG1Jx?fsYuX4x>ueW!Z}&AG(z+n*?>FvynsuDA08rL>`0r?Y(|E zCwdYW3Ex!6Pak0bxBzq3r%#{gFs!V_T;C1Welpeyrz@cM5tQGrATo)34SiKnpy*J` z{-1w;yG@mfhw(E6ktxREvt;Q~m^Zbd4ax;9Q4WBS$QS8cwRNvH;AnuNdFf%)Z;Uy* zt+}^=oa{kkuUlULNEzo3-IG0<(!`qyH6DH0YBToVBG28s-AN+Wj}}b4`?zJ0J@PlS z;qTFj3O^Z}1Xi0Y8O$T+v43FFJc36G$7pKY1H|Wr^y@tw_STq60 ztwoRonj7?H5fJ-9mnK+6#jLVV(-Zkzfc;z1G6HA7>LJ84M}2qE$aS05Wcb4iKmtIp zwk;07#2DcBf%pM>D#~ycD8*|8M}5clA*AO9A+;i+g9S?jn0hk~1&nsm7v2AUdb(1| zlVL56C)Lnq;%qu#@B~o+fG*b_-k~=Kc=+lYjjNP)RtYfr;LRWEz_`NW#VgKT}_oP?%uX_^}l*oryRvI9R0p z-F~@t8!<`=g8o~ih3yK4qQN2E&iX|Le6Mp56FOi?1EaP;LreGV+czFtTClhkpSXAc zeDi^Tx5m)GJOv1jM|Dd=q?JTNfYN`7t=ZK%MW%nyQw~73fiLOVooiG_r?drY@&5f~ z0B5GLbLlZ|jc%#k`RBpN7g!v)6T%Ac4e$OlFnf5Er8t^_{rDAc;F&2SFeBLmTw z!R`9IbK3-=1h6m(>W$b)2w%**d?A1RXlW#Po>Yrik{{KIK z@_*iWwsUAh!arI7Qh@(kVAcQdsY4#QAQ;N0vl5y(!W=-Doimh-HPnta#n~cXz7W+K zrXgyT5J|s>{3z|&m?7J+&HV=t_G&7>z4!oqd)$K2NTfAw*)PBa9L?RR(^c(W4{@j~LG&EIH&jW9g$ag4ysZ{w1Wa1=wHL zgfx?lIRKHTgIDvyT5vF1$|YTZzG% z@Mh$PF*A2SlCR(JXVXHiL_ky6#qoKRFh}5F&&DKY+$%&?AKcHsjJYhJdjo9(7_aazFi^QqpmBbL~YB zTj3xPUA5T@;MCDVv%`VL`vBkqz5Um|m!XdcxiL_G0MS@^D%lQcUI-UOOlTQwgL`o# z1?ag$aDNaB&VZ<;fGmoDjnhGkv`Jxrd}>5UKE~<&107H@=~KdimKNY>c|0|B^Y{OT zP~$O18BPLRLL6p<&-_Iv5#(4Na^6%D_1(P!0*tUC3$E721#7b(PsU4x@Qp;d0WCa8 z!gOGw`vU3Zet2Q7vb$=NuYleUt%vAibcUS3$M!<>qKp2Z1Yv4V0izoMYv0n4Y(rT$ zlrABf2n*N`?9P_mGvK zfSghgk7w|pag|CgrdPC5D>m#pdGX>!p92xEUu&~7;)mygeZx;1hSIVz#X3(IycQFP z0pv~SfbWD@2vP?MpqNNZc{pCZFmGEt=Fu|-SV9IayjIZ71k83ZF8t{P{mOAb{Uj+I zpawr5-y#<>-x1s$;x9x|0ohSQhKLK&ZEzr-0IAFSV?!7=X-Anq(OAU9#E?@RZN)3N z7Q0xN;Xl1b(?T@;c)TJ&7Vu{Mdd9}WtS6m>c=RNH$DVbBwx^{oo!3UhNu2}o%z_Q) zo6a3XXN^8y1~QK+at;FbjX>yWMHdOACL|_EEIQ!*UFqm@h>L5?5fAI6yu85X&9|V) zFi;^`uXKc5hFTPG!FgB5h=_=9=pJ`nBfK!kO?@!2WB^CQ0^G^{OcEPROW24ET6%iN z05X)-5{e!N2N9ya88}}M0cS#2UyvA$*6!P+0#B*_FN{509Q8gs6-zQ30g7i4=`WTY zIGj3W+8)^dZ?@fIE1RrK%4*;lbl^ctdxYpxY8;Wv0GJ8!t{cnr$oJ~(V`&&{uFMaA z9QnJN&skh$oX;6#{2D`=8_=f-XpX@063Lf87zW3N=Gi1;RB(dDpe5a;GldB5ldj3Y z4)CtehaIMe9qfv$oeMGL0-=D~4=P4&l*6t{lx&P9+6WMV6dBa87a8IKo1KF;$aNL> z%*l4r+VO4KLd*mh)U?3WIvX0}1rKIOX1tq993LNNX3>-Y_y9f@r^hYe0wOqEP}_2` z#}h#DHZ)04S&U{A@aOi_MEg$DuAte5HF`(h^1E(^HfAQC`~I$=l`mWtp5S*LMhhx? z=1d1MIAh6!9h^;8Pcz?!g3d=UY8Z@gt;sp4vCa$tIiEszO@wUN$y%VL$*MOokwb(a zP%?rek>>`u-y2dOvWn2-siv56L!J$nKoH8Jww~T|J$ItF0!)bN{S3?EDFB*X*D6bZ zF<=|AVAqp52IqUAqh!es-EA4~QbVBuv6!E+!Zb)`Gs2aE9olPlIV{(_&CfKiaBScp^)jMD^A#Vn2D+lgK|0%tfH z7jqnN7^_ZVYluvYY&S_Hp=aqfLy&o9-T`IjKT zobxjJoj>PgaOXKnbd}K0P+b6<;Jnswam{@=J$(Y?8C)0*b4FsM!CT|yyrTTyK#f%X z{9cg3b((fR{3L@=fheP$V}JVE%0=q`q_JM1w|wv?_UZorb{#JH&i}EiC7mOhtWV|T zyQ@6W@ZdeN)D&V7iJ|=iHs}1QrkOMW5IcEda_Z^n=_1iyINPs6>XY0MDrFC)gD9x& znOCPtNG~2~A^D*;tWbnuF=5-`S0XA{ZS$^OFnYgPYMNnOw~shxQCSyUPkn^05=g*P zIP$P)%Q`WrGT>e+npxDJ-uUPO59aATe*&h7!Wo$a$V^#D1TwcI600DvyNK};Ly<6H z>CqLGVWHEbZQM_+eYDg`6)Sl$8N}T|@B_*_iVgELkX z>GRN)VYcj@{oa7tsg))(ofVJF90IZ9m*zvq1Ms00QxX`6!G_v9y-P?C9dadL$*#(F zw8vR6x-6VrLxjI*kGgk;8N9}^4kVVCbfK0yi?6u@&@K{>bZ{(^xodd%Ivvbas$k7z zCQ(|AQ4`!C$xAZ^Jgk0OqjKozCRT|*=rFcKK9oc{5XkR-W>X*y~!&!b7NHBdWM5yS~BJjJ= zt*qweE=Q3h;1IeZfddDKX=NS!5O{lNuC;M+0#_a`*k~p>p4EpAj2Of_N_{=?;ONi2 zIk%ZOukn1)rnKTk5JADS9SZz;$-vghU;xCKs2NeF@tN9^YBd-(L3H_FgnUk%C_KR# z2>e3Z#H1XD2#Me@LvNaQnDLb`YLIBX(!H8C?(opb4A6OIei* zZJY|o3D*JO%Cu2ws8CMgCG6W34Tr0kd1Knmj}Q-&)b#4=>RHnk_~(d;1T8md*@kJ8 zp-6ork_O}!6rs9f)LsODN76i)ICB36YgC+ULjNp>Q6L=jSr}R%4Oy1DPEr%$71Glk z>)?x*2S7ph3ymG5V5JG_&6&3NI73wX_>4BL!Jw^0XK_#weG*J3GpNL=SQ0iO%*equs)PQlZu+X1tj#n1E z_+~YMD~Y=ZKeDU#UDMFfk9T?puu_2$X3kR^;QI5`&kSLcs2|<$CMI8PtndnK3eX=8qc`*p2q5v3q1Lj$U(zTWC=1+>aWHQdXoub<>Y>-KUgH2ZGZ3 z+1Z&7N5ZV$xwrx8h{LVUE;U5^(bSu&CdU zPf4^c7(ZTM*wUu}+W_>4ss1kj$iP0kTWAZSXcYlMfQ%rKxMxs?h$|J_-z4G^B9ac{ zNtEl*K{~=ffP4G|Xunp$Nr^H<0_C9ysHi>x=>5K@r)Udt^gQkp3R4B(H=)7jBQgBOw(Rc{y6JT1BV+h)g-UZV);h9NmiZ`&F ziD?T?JSfwMz6j5T1Tqaa43UE!nk4vC&IBG@2?gQJ#=6*OAaF!{3qYEn=2H$$f~d*B zpJ2&3hCUw-xUvLO=>H%vsCsjL3z2jZA|70@g$kw_&^+f|PdB1VFG0z(1+7cy6F@e) zpknkxCubd`-wZ zLWYFYpyVYw)0_wskK>QX-pHtr5Eq>DM50ad;c!Cps?;Jo5J=m$ojX?m8gN2vCv#pa z-qI^j(*$|z=;&Z>Kx73gv`u5|ILd%6YXW0lf;mC}oHL2gf%SykTBJu;Q4;>R7$+nd z8KB4}12?9n^_v>RkuU9mdJYU(3w~*0c!Wr>F@;8$6hbnA)+Ih7JnEIXNSyuz41y5b zWIaHWd2roswCk{ov|s+KLHNvK{Dh& zqXtwEwn_{tZUFiA!e{NWixSH6_Rq!-;ac+lz-Eepr(ms;U;#|XPjK`Qxd6TgCThGQ zbT1^3jX>;ZD6r#Xzy=frONc@Zrj4%5)4>plu=_BkZUOmC$L>l5WpEA^Kw$=d4m`vdqA6zK-VgKkaWpbS zjtGl~HvZH)p2@t&#h_tbq4$Fs0*+*FVwbEp`?n$y5&6&H_zdU+Cr@q%!dHF?hX*kX zA~8~OSmpl)2KmjF503e(bN{J<9g=L5VU)+HOM&so_@e&7yDh5;N8~K9_(`{s;M3vSNK0%qCs4Tw;UH9{J!F!6OkXDp;g)nB12) zruj-ib%z!iWYke$KEQf@G4!R{`%i{@$Rhr^tX9lofmXOXLmaJV9#TV&WAg&(>3*)P zp$+>tM{-UyfEoW8d0E*u%(5JRWhEvi+D_YB$Hc}WM9ptH8#T!oM%9LPC%?UEDHR!^ z@sy0~ZHw>#_P5|n@{b4daIK=OrSsRXsoeIomZ`>pJ<*j0)>eNIdGBzS1*dKU;iDJoN$$)b+C1!5 zU~j)^RrVvht-%0n-O8J;nJ|gAW(Qc>u`@^c~^_d_c|# z==6buc}0b09B3^4{(S>DI^u-EmbxWzYkcrd^?Pq0pW|w3LALK9!}7sH(X(&FkjKL1 zJzFzvfI3F{bNx5jlXq`JM+;!0D}@EYlIA#gms0&}oDydp^p)NIunA)6pQSc}o#!K$ z_yFJrIW`6n1sKJfEpBh;RYBPyo`YOrLfj)Lz*xAtaXI!aH?c@bniT5dCX{BhOHw}d zu`w~^_CRcFk`T@Oh@`t9Wfrz>Vxb1;mPRo4fw9igLJUUa?ZWUQuy^m8sg{Bcjk+3= zlRyf<1qFrTK+!$;QlQ@@FVG15LQ)ZMFydK>;4UkSsAUt**=95iZ0U9Ym0VC8;X|xp zXZIVpL5bT74cI0Ouc(7ho;=CLw6c#&9&G_=cp@o*DKG)u{E1$gMxCQ{Vl)wFLM=Am zIscm-m}rbZ;1?{|kbwEL7o7_<{Hvl9os!DiE;)>{~zG z4Jc6?L9VbH$50`0v%mz!vpDO0v<`B|AelM`-k&7rfo>GJ+vm0ynX-gZ$B~G%mE8~? zhoSe)4W^0WvFM))ChhJV>PZv5NMvFhL7 znf_lMk^k-=+WK_aFT_`@1oRub@H!%56k-k_&0b9ESyfL%L*;>$lm6Fs2Te3*n*1t~ zm1Cf;Bqz5L_jzFq4!RSN6dx1wcy7)iARvH_pda{EL}n;%Hv8vJ9Wh>^2QJNga3+zE zhZ;p?ZDg6HTkb%gF@7)ZyWz<2uonJEv^uSSDR;iYlKu=91c2yQ+lE4HGIkhzkat1{ zc8R3cqoJv6kp6S+Nrl_#onezWkO&f8t6}dB0mmW&KnDZ{4k&vd&Lw|$O5-Oe4Oir1 z4l6>&0=b$9Y9DerA$2rHNs#1U;56QXGJw_m&Y+TKc8q}cqD_|T5VoLZRij93Ley?F zL3!vg$W;-1Xm zMK(=VY#2!4?+w6)s+&wgHxQRay+q&wRD#1`csS5*$$Ba)DQ&=0l$>h41F+~Gq?n8> zJY_hb2KvZnQL`vNKu%#G!1eKJzP`TRfq_hiO-+SxEig4b8>!CZf-2y(HPY$YsK3;G zL#genfC+8ixpUDb0Rgw=>-e8MefkOF$(tY@3{6e>_w4Z;9W|%hw{PE@*w}5NqP{5I z3Ft=YkXq8}>ej3DA0$ZzC}7qf5>(_H#qB(8Q^l)c~pt-F2@dF~^ z#UKuTB5;D_owH*i%gK!d+8{hMsGz#AmVR&t_E=$McAaj~^1Q_CVS`!H4h#A#5&%gpIyonakU^J4t4#(|n&MA?2NB8(KoX zk09Po7a%CR`*BPlvM`gUrKRZ_7$As_AZy_9Nr(+`pX0O=fzoYZJd&~(HYM`+8-?69 z!qA7(q1yOM*&hT3MD?QMCJe{1W5*himOqEwgNRqJ?xL{?0nJQ;mlZBuB1!P7@2)HX zFOg=kgk`y9C&QM3pcQ%0e;&RO+*C=vqITm~ zNtytv4L&vs%Fnj88N%LG$=5$HH|2cet`4|z?F-TAy9q< z74PWmyitCj_BxEC-x?c@d99_<9?)sn^=v~!MFPMmvvrlf2t*yf`@jKKIs~O4^>ZM6 z5nuwk!)R;ifmcWjv|HEd`D*X4Z{7(*Cf)vn2T2ez&;n>LRwB7l7bF7wVCD!F2}#IA zVZ(E`I5k^QTui@7OpJqYQ@|HL;bA*{GeM7znnm)Pz~|5tIKV|oxnX{8u75HnB{SRA z)m5pl#iCM*4A0;^EikDI2K_c*7Kz!&s{amuf;fZ@@7)4_VDfVB-O5$5N`hKUW{h{} z6YhPRnXyAX>hH5$zyET{bT@@2tE>CUF%8tD)<&KjR1g@8J^|n)o;3{BBxFYIMm{U< zf_g&g43rxQ&^eI28$bu*L31(N4l__^7FE~QGHC6qTc}1WBp`J*@=|h64h6KKF1T}Y z=LrVA_vmFwb}sU_ziQhSda4}q_i3AoHrV}R`C!#?`;=KRtY8Lei%3m(?Kd{HqZHwbW{^sl2tNYi&;Q`gcz@!tB zs-9!7~(y_eIS z430eGIWxV2E(!siO}N^~pn?l$zb#fPNk1h}7)mfOnjlO2-_VR#)eWowiaI$!$o&~e z$r6CVEy=NK(nt1_Y0kX$!l-Y~bu{AGG@tN2AZUMyG4*f4X<@36^3>nI*qJt~Nw9we zG~v{lLo$E>I8)8JpKN%^3KuT?1+)*cPon)N*f9#k)1V;Z(2rSqev>_^sO7eh&XW5e z55b=Wn*5MKVPsgCOU_*99+Gqp({ZD;gxzVW%W=Z-;At@UCS5)b;VMGrU^^R*)|`Kc z?{;2EN#A)P;96Xpne*Y?c&gS@cYw-@o){o33*8{Lk76v9bltVk9c5(c!fSRX^e ziD^X`>OV#1aO+2<43k_rcg%4>h~P>4C9?QXc+Ck8OU%x0-dl%Bo6E zbOinaR@h^j5tj?@_9hk=hqS{XI<$+TfX83HfBzJS5LN&$IUYbt!>&dcE{G8cb7yqz z8aY9M9!Y_(B8hxpYxbeyqWwDxc8$QbT7kB6zBzM*#s7(k{l&|dw@Jh_=Aj_!H5fc} z03?xI51gJnPE*>m^FO+OPDze@UUyMhnFJ`*4^=whVbT**Br)wHyd_bSp7sLYjg(sm z)<}Q^`WMrQjY9-vglS>CuhtWvn>g{{I5%ebfi)VA_rFUcGqt9yfA<37RxB{QTsU_w z7kDel0>$(wuz&yfGmUm~^%9z@DX4{PiRB(vMtpAYo&A<9zJE63a*h}QPUl^0Ss2?P z`7%N{Y$`~2Rp3=Q$c-XLcrniFS|)X=-|KbFH1Cq}&{Q;M_&8AEUh6M~%%TLN* zJP5;tELs2NtN45a0_-$ObG9xL527*reZMC~olYKHAMu1z4CAOI=c1RJ3D1sO5MDwGbj^QNtxp9hIGvA!y_!MZ_n!(3I|SGE(9sX z>wNZW=<#ZdEWakSqoh6Ov~)zwt7Ut5HHCPsrPS>sp7n_Y`_uN0p!V`%OFc}h3(P#lkxNBqL<4mo%CMq%qk zbWF{*&%?e5XFASLn~v2s&i<$}p}w=9<1S7$b;0bD(|J(YAa2SqsSMW?gQY>_^*fba zz-DmGjtAl(?nQOb2s&a?|Dnp%CbAFuyH;FhrVIR;bVwws1L)mYat_3H@8DVk)oS-$ zPj%M@06xdV*MxsXe^t7}Eb#gBV^9YH1d!^pgRD|3>C(GPv4M&Toy99AeKZyUELjmzgEAYuc!0#s|7+H zFPbq-b^dnIgL{Ye$42Eey-#MtOv~Q8CH0x6-zloNfh>!Rb58;pa-}?_?|szjC{;ZD zBi(7`h}2X1lRcvM7w)(zXjng6VwUsiMgOiV>6VegL#w-H>|)Bx*Ryu4*|4UZDyMnD z>BFySp-Yepaz`wRR4a zmfJ`lJ-@J7$#E1CF#5&!j?-Pw`>}kn+ucOFy1%~1Yn;s~jbGfF{Xk*C<|Q4LO}*5r zK55!wP1@l&UJHqg6z9C|M=g=3C%2L{d!uP>kCy|-Sc}~Q9@(hHZpS`cK3=iFJZer| z$GWa%@J3v8ly==GTjQxK9O`yk6Gy#S@D5aV$*u3u7a4vSyZy}|=?m~YlGox@|U)?)BhBaUJ zezxp;5bB(3n8SFYrn>ql);1E9_V3>xl;Uz;K|$v1*;UXLuJ~nsAwNGK<9@Pu9l0wF zK91As>fFBS)*P&?tgC3pIvi|gdLk=lukpeej(=Szwj&zL1os@HDz>$?gdR%16wA)+ zYVqE)3RfdpYm`!&uA3eV+_7`#%c!X07oX%IwF*O~muC4jun~|E{);aJf|J&l@{RSnI zuZO};Pafsm$oV7mop-|ht7qbb@MCre?&cTg53%PByq)2;w8d#`8-Dz&`MFEExd*}v zy+Q+Jj+m}GacV`{Ou>!2t|!)QSR*$>E$^SPUPs~G%SdO#BA@9nf8EL=awJvoh3hJf z!jT|#^{BY*k?C)5V~u+U&4@5FGBiE4TUVPT5YjBo{+Mx>ocn+&%WAF-#d!1PeU9rI zq$&K5C$SAIGtPVeP>eAMzGu0!w& z%nI%5(#JQtZ`ytLm__2f#vlfjz}$OKkSD71wUPv>*W%7nHc9w!JvW2uS?3VA?U0D=O>yRy$8;>gW{f{!91 z_~{^*J$2*=J$M8FbGWM>zSNGcu4Gllc@F|^JOh%DglJ;eUAhPSisik5_|LB} zP*7M$Lf>HGp#w%2Tq|fzpd^f|VA%*__arp563SY*bO@SGvN@3g^$w~H5CQ!SFra!9 z4?MXB7@3qr6C|R~OeAv0`u1k2i50JFe!3EmFfTqELpOzH)G)c?vs8XQn?=w&|a zaR#8ofzUMo>Ev=lC&in9<;Z;)B=YtIkL_J@<2A;5&W#(>df!H~10oC(TN$EWdD)?KgdY;!Ut_l zqL7$yi3ebvdWK{|tm2xVRoSEz~v-fi+i9QMnHsP)<&coQA^e z!kq5-TioE_cWi3sThZ3RM% znOl53(}RoL(f7B1z5Q);9N+HUv2qmE3l}~ClO>{ml14oZblM*gizN2~XDSGAf^gO6 zFz^gbB|CwV8$k}}2P0pAASU(4Wn>tDqy6@bL!3b_y#z7RP)e@Z{O$IhY8rB?)ej|i z49CPUN7uktxDxMxKAX3VDEYBxipt82YuJWxJq6Hd$V6n(CctzEoJeWtD_&e(gA&ND zh7WEq#Uw%TYToXKR$BFa8|F7Fo3=ZO@vo$%$^N z91=YtH#txZ6Dc|Zbh5rH!z>(Zl$?`YltTkQ1?Vy16pMGLR8ckdpnn`Gmu^M5$1LoMXZnO~6@LGuWj=U`@^o0qrD9GMI4j}K3OJ0D8VF+SFJ z-P)=R?f1u=kl8H_2V?TJ_J1*GWu02W_|B=^d~WW}vIqAEhi`794tl0#H!pBDw091i zmb2PcAyi@6`^{ZuDfPx7d)^yOcJ=Y2p(lNLWVytj$S$-o+_art%eCRiNtN}*QIz%{ zUoU*iX{aAu?UWFF`1<5H-}bG{wA@V&bJ;nZYJN8ND{eIX`nJXE;b{2&0|%}c8u5w8 z2?l3aMrQUegO>N=Gv75WTONEF-rgYY!;d!VHIAoG68RS{tLJ-tZbZCK6ZEu=?`0zu?ZEEndwOm+SG9xWatFf_ZggQadmZEoXFwr|u zU0VgipKr<@r^rK(v!x2%Ebe%F4L-=KymrXU$mnH4!V^*!V8Vo=QQsNr+7RHsiLe4Z zd}KlyP*PPTsr+C+wIMB`T4h-Fkn8wJB0q$F*fGhooh?;W2p{G}9YglZmd%^#qiY@l z+=L8+n9_jUM`75|? zNqT5@_pV*9V1q;?-zZd_Nz2)!kPvS-p8B8Jp zNwN>X=HklArD5;hc|p>FZs!xYV=>(94V5>EOn(V|A1Zy)P8v)y`rrU17pB^QfZu|W zc|(FDW0Q4*3CcX&UcJ_RZ0pzG#T6zfBB0{daL+r^>L6ampWOqIKS*c{s^D+(!1LhC zlSpa5a_-7$k}8kPXIW_3aM}=V4&jiXpx_8zzhT4Ez`)aR$%7R~N1wDK59S?oRJg|h zav;J-qoVH=6vP-pqLjcH%+3e9ySr2Ma5mtNyRx5?JTK6DDzQqr2V+o`LopJd!vaqW z+#075vX`&Yc1^-yxOMBRyxN}a9xUC*!@V%xgw$SNm7E{}md-T2hktZztYJU^S;nV9 zdZT5;*^GF*5>Uwxq51^oEFd8fh{47x)zc?g9 z+lEJhd`WHX&5*{y8O`%3|91Wr!zwmt`7bCbx#G%|w6r?u57TZ#=M$1*sr7j*lNKac z1akJ(P&9qbJh5j9+go`_Wy>Y=Txru4n(Me5Y8DPj2r?wBypeJ#cGvt(p$1Xs{O7fG zf7z9;od+{Rzt&YN25K*{WNeYb9zXYXUTNKrg5 z2LB9tzo|lh0q#K^Vb0XHt_)nq)_J(g)W>Ojm6YAYWXA@`)R zAD6nR7uH7m(to$Ejf#q!Yk6{w0rS#Hnkkd}O7~YQv%2n1TwBv3RGXH&1 z!pbF@bIpCs?B=Y?cQ`f}*H%}bJm0KcW|g$Qt5YlK3a=P_vbJi4Q1a}Yq-54r;aYr{ z?M3Cpo^Z`V1Px}0Msj*adRn{gzOPpgws*BHXUBdnEH4jk4B9ovU~Hf*mFXZ{Tl;hT zj(uZ!mtC*XwWz4%VBrz!2-u{0v-o9?`l|=~%;H0m=YP1q+bGAMknG$quV|%b8-2a` zz>8X2ihVy)DzFW3?L-bris3}QC5S(~xnU^hK+`ND_w+#aN){}rtA^SG?97A#044Da z^Gi#%Neh$K#(jzxFCN82@%;Jy{>Z6+v;fb^_Tb?;3rY?$)bMxjzVakwWtkLy$F}(S z^U4{1g+RUCn$@TX@ z&CB)V?(X^+7;*G-?6~mxAn1fX6r=Td(9=YA3t<~{)8R2?)-U%rny`^L{fkRV7A3T}HC*8JE#^378rOry4Lm``iE<*6|>_#W8rG&xgm!KyOs-LP`Bub^=vWHVvurJ2H9{? zNy$&#lBExC4T&l44zXVq{`Kak^J=?Iva+n}G#UPMxab~UNusc(>H>Qi+xd&*mQuXkM~D;UQUy?sVN zhF&H|Je--iG;5A|5y$MZ<%uirn10>&Im<%L25ntZQjM-&|E4&5zDfCjl6milwcJ(D zV;%=Pr07YFb(g(N6=nhnYSu0)3s`-@x>55N9e z%qYwmJY_dwK5D*7c+_V(d+_7^p&c?j5i$yVnQoob9u|VPfC%E-+dGu;fwehk2z9Y`(M_6!1kc z074p+=8lAZ)sXslP^>lWg1Vw@`0J^lW zg*JPhNfC^zo}S+8d7e?K&#i&ehq0NkqS9N`DlXaDiqXOQOiB&Lgy7+~y=mV@^&<6B zyK$p+DJ~l#SA*b;L5X?d?OoN|;Rny9t*tF)j&tYEX~o;f$}Wfgj*#c%f;y~fwAOy; zDnuKvGeAyZe3{^ZkB+SjQp(|Mjj)Q06-9M=&QT>SWHBw9rlE53sDRdOf>*QuOB3N{6|M zex*{4*}{uK>IjGN*f!vtSd(FIw`K!}d!(;TpkSH6+LkT;!3_u3)a_t&$y>TFDmqrX zy}q(8m1^-RTwG+Zxv<_?SyS_or{*T0JJjeeyFYD_u=sVDAKDttoWp1M4dxczDZ&i? zk&Yh|`?a?tBVz`%<|Y6;sp)Cj`^4J&c#A%kSz^22J$6*b(wuLPJ*Cj&*%k8}{AHAY z_gh}aQrT8>of>?>aM8zC+dTRC8Xi^|Yb8b@p}47~_1WyO`6}2pnlKO4j5WUhInXDs zHW>oMsja8;n0E7Yj$69kO(Ka?#=G5tVUy+{clO+)W4MWxf9ux8cRp&dh*NxUmJ>jUX!Wpxi7N{YJ4rN##Rp0A zlitcmdUys2I+0Aw`gUSP5;+_!t_Xc1=sD5cAhep?GYp6$t=&VSzG)I-M&eGIo^7Eu zcWrHqN(F;u3&9Ifui>~#Uiyf4#mbeP6xuWkAfv64l0nc_HS-w3s_BnwpWu-qSA5U) z8&KNcmkM2D{V5L{Oq&U6?$`=0hDy0B805&vIDp$?U_p(I;7DG|NeZ_3R*dXMhUDb}u&c+37Bc2#| z5QBOZN~7wC(3q&G7nqxfG;5=2D!mIVoMQk(#_()H@HCwk#zI3aHA%^=5>i)FtN-AR zzzcLpg;4fD)i!XCHT)|i!z+l58iG*6F$aJha2BBuyi$B)Y8!vg%`K&YE#>jE9eehq zr1FxDkLWm$diew+ci_uWR`eerph%lpA+>GG#2R` zK*wgSL`mG&o|UvU-!;VHnAhpUj*b{q9Nd%cJm};T&rkNBaI&%9EAPQ;_kFgasOYNb z7kz#GdVz4U<`M7nNrh-|aJ3N9$JoWk`+I%Sl4)saMZbs`*e#(^y7*xC&q%M1Z=`ja zP3Yz|H#aANVX@7ZpFe+2eXyeP_=$mY9T%!cXG^%3kUPUa_}`jSOUfrV2;#U1C7&m^ zhx!0^sGlG~#RuktOyVS2({BrLzuiSCh_DJxQFY2cAeziM+T;~9Z(rw|omI1sMczz^ z*C|gddP^>$Q!WLy7^&)Z3vHziI-i}~au7)Q`00%IO%D`|w=6q5-&>mYv!|zS&^H)m z*lNnYosKw}%gbZ)UL-~$2Pn2YOKxEIH8P;$7-6mGd<<9R7c3|1P!az#Y~@+jk-adK z>{5OIs(kdaXl|}WmB;>Yv7*om8#tJ87<>S zC&PGXi3)rMy;o*Nos-ezW9NLj7w39%Bfoc4UT8#?-y8Sh{K3d?0!B{{{ZaC)`ESwv zv(cV;Q=Uf8+GKrET%C4xph8I6K<}QbE7;|uLfi1(x?^@F$#{uGCU55xcq#bLwGzK2 z3QlT1=0%R2F%xX*pska`dH?F{Wm0GIIvtoqZyx%70ttIp@6Q?n&z(G*UV)%M=eq&- z)l>``H>{f9aH{)o9pA6fgEuE=Bh=la>nwJf(AOKX$i2Qk%yTvF&J)ce-Q6F5-O!-M zCvW$?En>>Fr1Ho|&9HIq`}Qm6;wXD&2d~rkD0?aQoMh?enS*|L+P&p{%12pZ^SYvU zIL!tw(1mqho$nP~*si(AUdXK|YW2ug_Ml2y_A#g1XR9}a-Jxo&aGjdIJ}kZ6cID2T zyFzBSmFvZta_V2IovL`V`!0o1Zri#2O#94STJ1M?g=&q98_Y_#E#P+xQDY)poI;>2 z9c{Fz9C5})hz5#h(RC58iKnEw5pN8HaJopwLV(g@7laL6hcl8v7|tK`7FUgo0D|9! z1k4kd%r|6WL14_33u!G$&qcMujS`O`c(zQ1-C-AzANrp`rPFX2IrcU>dO$A+DlUYz ze5l#$x8HCrrnyFl{|xL-em<|dF>Ivh1`AWzi`H3Mx7@6gdZl#oVU29&(Gb^{zMEV5 zudfmxk>+HSxnmIhejwy*O)fqHvAup75H zom5k6lr3F-!?^s*IbG-Kw=G87T{2m6G;4}^Zx6bJKYB5~yJ1Axz@#T|RCfDz| z-lonT^`@{l$){$`9G!kgWU8=Al5r6H1N^m;!?_F~9D>i=0$0ZFyStRucnS+Bv+;%HlPQex}G3va|n6@x)fsr(` z7>iA2a1uv}@{fgJml{JJKlRJcO$^2*U%NQ*;RqmiaTqAN7A-MIviu=n{$Bd4IrFKm zIkP+uWs&{Ti`?%GOg=cC;>|d|?tJL>?s^m?4rmYQh~68QYHou+M7+=3`Jtohq8VFD z1jCYOI4n>i%Y>I2p=ysoofAqFHV2~YC2qg`{3U<_ahnA3U=T72fY)M1#-IKDkMpmz zA^G|xtQ2VC(X;RnNh%Jn>4|t}la@$G6`;0nC20&E5`(`IOo`eOIl7&&91wIrGA)h6 z1!7;YP!JED?$e)b>|xe2Xf#!t?|2 zk8Dp8_1>w}e~5%2pKJwiSeVI){Uqj6(!UJR#Wx|Ey)mGl-x)mM@bZ-#HVx=r)%zKdY8e;z&c`HT9RgAOmF#<_S8n0tc%gFc* zR^>R&BUzo;?MWg4vbCT1`r7cjDR2dUT@sWxz!F>0MlDq8zTS}4RT$l*xACWG>{5jv zQ7dmyj0DG~-VZPf4Vb=%miXX6RAy!n7`|`7gh}(cV#Nx=%wbay%K&O0tOuVkQ;{hU z>gWkW-c?>q1!BvBGIN~!zZFv}YnWfeCu^icttgSc9T4lXW-gAA<`p!2LgOJ#xp_n{ zMc7*MVJFEEsFWOIMwy`QQ3mb==2i(DBMr<;sS>b$z|P_2c%t9j_3tj?Iwi zSd(Ihr6WWG1P=#bLkIVI1-zltb(Ky9c$F&fkfYZnEZ;4LrRJ#niO4=!=OYG08M3PA z(2IwbE*T%&$S!lV(J~8e25%0$U2~PWv*~YcM;50seo?O082dw=G z7V!W({UEv=N7< zU8-()2H!EEo#|XX+B#Y4x%*b*tns>wY1@ME^G=8ucXDe|WwBMUk%!9ZJh%}~`@unAC9TmY0XBsbhx%FLeoCkTL584UW8xQTl&T8>aW(WHWR zg?8ken%ZMjG9;?#!z2w!`1_?>qFy402Qasr6N`kQXQ_tLNhstM1+EQ)5J)E~byMqAi|3%o_4#Hm7rluI@wK=-ZZ=cOUU*|7wwG*Z2 zu3Lj^I~l9Vqd*DA;Qa$ppm>bL3pS8k1;buEQF1nVlP{rA%w&U8?*mZOjik zUcu@?W`$z3(-0;D131}nm)4u6p*RqV25QP)2-_rQFnVfo6cJ}Fb`f&I&%+c0`Q2m4 z*x;SXzq^sk6c|DNopq+^FXPPYv=`uHFp4+Yy3JDgzLghB%%ei zMVc>{zS7iiT6Jx!xuNYsUc9aO+?PZlfh}9YYko1W5wijSkBSszC1&KtXVCp;eCxiO zGr5b8n{@iAaE6_=&qyqP9{HCt=AX{aP7*q8;)C*DW8(Bl?vKpA!eddoWl?3N{N%QFi|*OH&6$o#-fn*5++hlX zz>JGT4U6m=F$sK7k0U1MJ3dLMPAOeppLTP5m_!(ng=A&|#n(aTOe1a!GWkw$3)D(H z>0h*ljLFpPrE|1)vx+TCm)xSuXI}I9;y$@u^179(FkLjHkRaksPOW(cDc2cy_y&{v2 z&V#21)8Zx@YaLtIKDfhLh=ZU99vegIKF}Y%UtZj)nuJN^6^u1!(XgWVuYzSW0!GvM z+eQ*i;542E2ghQ)4Lu&XA%xIKu$A8B`taxm60K1F=g(?SXI#e4tEjwz`3>pM*5WsQ z-h~uq0!>7z?mqQ_Z!Use?xM|K&}!J@?r{|yHlq_0Sse{1h-{4p`_rFfuN6)QDinp# zoZglk+BS8w_Nf}A8TQOV;ZhAESy5d*78PeD$G)SG&eS3YWt%K^_(Km5pRE%E4KPM0 zeOoEo7zUha=ia^NQCxjeQd01lvrk_V)!M(P~X&ky)T}5*_dlRAUEL_K! zdLEtS&Jn-df^*`zpWhp7-Z$CV8VE^HLirugDFtloc0*rV8xEnD@SMLua95&-%q`6o zi)Qpbz=%E2lB0PrP!6;=8dJ}Uc=C9y(=#)@J&~bG*n!~L1web@-xy&!tj~7LfZ;3> z!`@Ydvv5jS$Ka}WMQ)0d8h!|Tqlfye<%15rKq2pg2w-$_vKDr|7dX9+!GKeZM&K2y zVIOM1zQFR{sz^`Rlat^Fw6(MAg#=4cMa56UdEP25HMPhni9CIXA@M8g-|4tv<>M9} ze%Qq-dZ3fF0UF8K#I|hj|4#T}G&% zmvWr4a7IkHQRa?7cUKikn={BlM`jRHnV)}b!=q!G4kP!nB2bi1V234djY5j}%z&A5 zG_-0*hBC)IfY%;?1(d96lt47`kSPShFdPZ}lSH-U%p{uR+D6Lef#)CL7{*=yF}OGke*&3!!%w=^h? zGq{dXQA;aKMso~l9wkjp>afbCfLVljARI|pvbbL39NgO&{P)9!jK>^&V@@1Y4Q5HtPC9CVfe$YVs0~NqT(F3EZv=Q6?(rJR4!aK!6`Lbw8akhHDM$ihbj%} z*N!Enr3%-tCv;T=N2B+{6WYFQ+cE6i?|96}bCJX`WC+b7;y9ln$2Sb=YO)u+XE=eJ zLJK1gRoQtAJr@-e!ht#%A>!w4T-SrM#LfFFSEqmZ{ zC!T!8bLYNvj78RBRqOx-l?CWv1{XU9nYKe~{{*)HV|R>qc*}>p%SOy;9|UmKD7q&P z(e5SG8QbA##72FC=@`4SSU+(GJ~rI{x3I|$AYKWd&-UuMR64qig|bJFTr`9N2(gW! z0Ow<1cvXcuPuyuP6~n7Hf1xZg)R9+j*e3nzg+O}|Fuk1$NL@cmx ztDm2jBM<8oi}X|&+|F0xcN{pNh;s4)Q|7aPfVZ%TXW?9d-0nGw^@cqfF=#i)>4YzI z?)-T#a?59v<7AVZ(c9Qqt0|tJW}M2UDCyCdF;V<`FoA~W&0v<6v5L_84bNX`pl5?{i|`1bw#3#eE-w{QOfh0a@;%@HYc5{pOyx35=- zJ4wx-u~Aj^&!UMrOeHisjLFHaRH)l%@I=f4OR2P>;WDz{;)w?KP+|Nc`uTmhs0Y3L zZ2~tWnKoTwks5!pH45O&(^p^=jbMbvx~+op^#g1T*ajS8aPtam6Red;1AR#o_Jb~X&BeEl-AZNqn(qh2;-KryNG>IQdahCE4BqG*W%)r z;NHe@Y<0<7#9g8BwdI(zmzI=0%FdDjte7_y*R zA3l8e4UNUt;)O{?4UIUAu&0csW<>+bac+=z@yd1M5SO&WkG4Zd-wVeFIiOdv-fFN> z6)L#z2nKkAu$~e)u&qw@hy$4M*Oy2v zOQffteTqm73f@5I$dn-Xk;yFd{g*I~Z7piEXkZ-*qb}8F|p`kRS1sGon^@rLzRK@l#HZO8mHccklkt zEG;Q{3N`ru)!x~M)tsk){3J6ThRg$@q0H)v!8SxGRA^CBQiwJ+Oe!TLVM-5Uc9$89 z=_HMw8>!Br4kglq%(SFz4|*~^OqnfGN}|F!%Dz7BUcdRxwb%as|6SMqb%?8T&iDKM z-1qzazVG{e7y2^(!;<-hbagwB#!AY|@8RH)fH$cNoY5)oV9)3G>NbU^q@;+>YQ-zy zY*?wg1KkPSuqIp`vwRvh(#5A`9-N08>G=2G{~p-5P__ec!?r-&cXBZU74p>7i>_YP z&SlC4#g}x9t9mA~NTECM`Sa&pK|X$}YYwWU) zvS>^^);#L|#G~^cO-wL3DaVuW&*ZZ#v@U^-0G0U|n<+SsI1!apRXHFY9p{o}!9GPk z(+jsUF)A_wz)GkESYg;iEwBO>BH@PV_I1`d;4emwI?Y+A^66`@S|zeRDLpe; zlX?0rSPEB&#NE>JS;0GxWN`0~!47n2zo{Z{&j>GWwQI)I<+J#`OT2665PRlj-AyQK z3!BGVAO^3)j78LXCunNcH(Og;+O1sq7laZHPr?!H0884|YH8TdBJizPwdy>Mg&IXx zo}Q^9`;tFP+zbRD14Trc_$A{9m$>*zwgr!F08RW|_c9mWLxwcOL4knZj_AITtY%0z ze(87fEPz5<#(NSdG7HxtsF^i#5FYACOOD}D_?lqXQ_~taGP^n|aE=)`Aj&K#hvP*9 zO>Jjid{YY7qG;>J2wj>=3{3{LXuGjt(qKIs3 zG%z|?tBGFp%(5l%#hsm>MC=VIIF{Zr@S@ofle?;?M~^wITbGE7L$k7Ucko#bOT{mX zf}OWawl?ah3%lyl8^G_1u{~iLO>C28XKzBd%YD?PZtelX=$AE3Y+3$|h2%~1xkZ|p z85w1-Ui(SP7#H$}4z=Ys-h5c{@ctO(1it+KTOJlJM@czem_uqPnjy3WIUcj9c-RZH zi(G@*=ZqgsI-RcUES`T{O(J=kxpZm4Vf{Y89|rJIS(tGcStcbrJNx{Wx5wxi7*qzU zFOx_lH*RNJP2p7{d3)*75#sfRWaK09Dw!lvn>zFd_4|6hZ8DScGp%70Z#-z}d{rb# z*HHf9euwvy`4uLbU&N0XrhLn|VWP8Nfv#Fifpt&gCP}1b(C_y2#m+wDH)oi+4!{wo zc1w;-wpW8In^-_v)yQk_I9->^R}<&j`o^?pc0I@a=Tvz3bhK|)cv}W3=?~hH+lxYC zE!D;~bqiKYpsh;s77azoWU`KWZ-u|IUPd(`nV5Zt!wx$Gwrp6v`s@6DV(r4sw}7m4 z=gBWzSkIt~h_|Xhu09z3+uYpeg=a5cUN7)Jy43ioQ_~QnmZBuvgUF#1M1g78|1SR7 z)k6_mvN@26huge|bOB6(pEsD}w6p;G3qk4#D<_vcJ4%x2s?bItDpkHwmrTdC20x$r z8h!PbHH-Ym7;_wL;+DLUriWmCEl7X1Z;P%K9@&v~J8Lg)E!1z{9~wVBeObZrL0GG6 zSl6bs+B+Fd$Qfy_1%r8~O&ZMbu3yh2Q2_ca(SOE~{a9b$mVw|JI<^f7JNdnp)&Z14rVJY(#*Nq3PDjs{aZ~UB z17Rwm^1W9J25hpjvOF430%HX>k23O!Vn6(Osv(c?fvTZVB;xexWDy#+U5fDySsT*T zYqTiKbf;0Y=-5*EdKpJ10U<=-0E+%AxyD`@A(?dZK}TkK|CLZzsY6j{N$@wCFep!O69tRx|p=4;pA2TISJZ>N}Nq7VG{q=*jkMMJMKs11^3Bt}uNIO06 z5K(FH>z-9XtW~3*Mye~gBu~nQ-_Wet3l};PhXoTjd(upf4gw_Eg3cDuAxkN9awXp| zH8c&(Pmn3qc4vN7j&^vS)LWn9fBtXonN)esinISKS)6g~=J!;`wt(%pdc?K$zP^|E zpV$f~(#$K8t@^ndubRSf=5y#NK$qnc$bfE4%slJw7vjk`+NQGrJ7aasfG=%rGx%>Q zidZ?ZgI}DlE92%lWw?FCIhSQ*801qf?tn<*IMc6phd?1PNdI=O-~=r#Id6F_CvM=t z`G#G&Hi!wk?bq^o#8JAIlOso-U3BniLgC;d?=KxpyiW@92>x-p_~{ochCK&GP!tbx zN?pk4;+hK>$}WPGE;Wf`L*D1lTf4hI2?+_|EbU-Q;aP8QHC>128FW|&J*h$rA-yDn zsjO_ulTsj`TrMDRweY-o_44IR6O+xNWK$WB6U~L`te9o084=m&;~H#vpaMsPslShwY9U1jfGYQJgk9xcY56U z@Wd1odvbjMYa@v!l_u-w^j{TtY=C-@I7>cV$b(F7!l(iaj!NX95UI<`22H5|ZTg%w+ZF8lK zN?3!@}Apn*toF-6Y9aqoCCBOSP*mQYiQ;QKogI>zF*!_bh063Pheo+ zdDtBM&5kz7eGr5W7@;G`i(9_cH8~@r^7JG#Lw5}>pU#xxs4!S_hS*E4} zfeDob2d~N`a}2<*xsY^CS(5rs9S_aAe&r>yu=JntG!Thk@+BJDR`63`skx& zz&vGn$lzIFnzrYg__W4G=OaCC)QG3tip7};mKFzpm~JJP$?j4}6{u>1qoX%J6RHzI zybqmi1D(T6&^rRrek%|NNfqVA;`h)M!M-R_&Q@hVKpRW>Re%tpgER>Vz?3;4r<{E< z3g~Bv97itoQSnT7wip)qp4z}@q;vUWojlDtif@ugx9qU)QIokow5RKC?+v2x{VDbNwU>UXZL&3?PqdKr7ap$owiXWF5s z`&p}%cn}7ITS7fph2oSSkR>EMR-vRQxFw!gR(n+?i`AffIDJ{7dQX4KcbOFQp%-1M2ju2vAT2ZuJ$YCv z_OeyTQmP%;IqyjS$gyK<;LI~+zl?~bNoYaci)K6`A=M7swtY{@Dn-m&u!bPsB2W_g zo^!}R-=cutRGzhnGiN@QC9WMJx`D)Ap%Ua^+aRu_t}Nuupfu)%$BBkT;j~zj9gA5T z(ZuMclH_~4u+Rc5drF~DKo)cj59z$dLl0+!hoxFuADq#wTh#EExW#(abhMM@K=HsS7!=B$~nax}8R_%Hrl`F*jF# zaVut8T1i}X`0r}zz)E*UEGniRXiQ{;YX{F+n{xU0HE(^67ud1@tQ^M8S~+ z?+_e(ooWbG47d1KRmL^>Nl8o>`Hvno>Mk5pL4QU&h0;nIqFOGSdpFkp`6Y2M0)3NC zT2qRwQZ4;uLN%wcQA_AnFqHh^X==`&duXkVGkbDM>sIIqp_q_?!6`iG4Qpq{nx!40 zxY$vf7EOPa+IDw$uuAjb*(fnWlf&@KWg4}3IPCc1{oVSv&p-Oi z`+nQu&|tMFRqEGxn0f+G1#PAzgLYtqY+3S4+0SJ^?KHAEW3U1=BEVQRsKci*dmW&6 zudnZ2PGdxLwD+4s)YykY$^UTr{>))zOI|-M48XUq6*(N7Ao3a)_AZn3y=*D;6uRD5 zU)|;YKtNncF28cnY8I}=re6h(fs`B(Ji@&Pfp+VioaEGKti49QKz__&(7cbD780~4 zEKLaj_eX$L_1+@DQP?xdGooo&Wd!M}fjH*lyDCk>Zf?2IQ$RmVZw|#;`rT|gXc@R_ zFn@kTSXd(QSI`^V7j2p4X5qzdUItTxP)_R$LJGtJ39CxVG2(Ig{icgcA@OMP>yTXv z2k{Oy_W9Bwwn_WM)E!PvQ9Kca`EN0NDdXS?$VF8rYVOv;L%S+_(kNr5Y`^sCBAIc3$7fOn#$+_~s0vPL;+kcBJ3tRIwYgZGS@fNTadsF@*3y zjwo^?1$kMG&3tRVfdRbnEb-8^x59#lHnz|JG19F2lHpUoU$!moYrEu{imFk+d*uF) z+&w~Mr&OvfKYm@d3sf$YVQ0I z*~Wsg?|3+I`s~?{W6I5UYut)`X;@NLwrlT?ifj5tjz%LqD#oUQ@7nr0dooQ3W&P(Dx7$%5#sM@;~5POMau4LFE@%q0wd$yJHd_MBQENdA2y8zSl z{i7t+uiN73s=ROVPeLD47;!sdjqiVA`O^~A#qa!7GJKzSm5d1u)ex`$*W3P^RPf(l z^6&D=f3L~^&ufzH+BGcw6k)@q>wR$$B*xPxMwhzh)~^`)+f(nN@dt+f^y^7^ZEmlg Wz52be*Am1(Sgo-AL;CVh5B~$w`zC?_ literal 0 HcmV?d00001 diff --git a/_images/batchjobs-jupyter-listing.png b/_images/batchjobs-jupyter-listing.png new file mode 100644 index 0000000000000000000000000000000000000000..6e94d16b17d2ab58919e7b8ff894dc5d0f4183b5 GIT binary patch literal 46962 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%0Aj%N za$BuD{dsBD+1BP|Jes?-xcFW~9{vxHI@J<)wXXfC@)NS_%cDqM@0XX#8+KmFBn(N@`_%K#a$IR69;DAd6Xd?i? zA`;yDkjP_ugaX0B#5{Het)2bBfa?G{ZNK1P3gD$dk5`V5JaEkb-H3hshyvcIvD}~d z1$2lY&vyLXc{yMeCGa`sin|+#|9&VL-A`m~0)h1R#lL?`NtrI6<8ZR^0w6UYY0}kG z2msA(h=_iz`oc(d5_bh5s9 zPQ8Vu2Ou(9?#(!&prV4_uvD-P->tPOiG?_oI-SNvvtn|k~#z;StSr+O~wm_GvrA_#X)x~=v|V5-Ssh7fop9cwP*+3H+V57*KHzx4ekjF2w1L~ zuLMDpTCEt`ISZf50(4nkSZxmJpPZgDb8>dJx5ti*jG$Mqb4w_a9LuUdeT)fS z1R^gV#*^aNd#S@~yY(w+O8D>Xmvs6QYHm)3Pu%JV9&_05@fq}9Yu)=uzg`(iCK&Ilc1kLJ*=r?zeQrroA4O;A z--vD`A@MP9_UC4_j6#3CRqFO)mD4Mosy!x$)-9rK5NK#JFms}mQ;%JyomSV!?>uw1 zSHuIFA3iU?eTz&;aY2+J)JJd$$y22AFaKYieRn*Y@7uQO@I{qUTD$eNXVEsqs-jj= z1huQEO>5Q&Rja5~LF^fO)Cy{sT0zt%R?QHEm<{2%`@FyReV@OdfAhJoJ6CdD=XoCI zaa{Ltquu^pB`|u&vh|kZ?kd(yQ5+hGX@2?grrtwLZ~|$&SLGD%7&LvLNf%1~vg947 zLK2FCw&c<+u$)^@V92j=33+bEzqHZQeE$=M{F{%*Ppd;8>EskqOL?>>qvK@G#K;4D ziDA_|L9V|#OcTD0KUhE!U$syT*j=|6Xy6R>v9rBvG#kefY~CP>m&O0;6;MC7(gOJ8SDxLzWcUSnjd7!&QFOx7Ywp*QSOImta z)``Ug44y$Y|K1F<5V(3F?H^oMz39mQ95{X}@rC#$t zHz<4fjwH6#@uXuu#G`%Zz6AHR6~~FwH$Pr&fu+B)WOtxl*#5l5@RmL)w!|XouI1X# z_3Ef3!@8YyXzLbFv-heK>}=j)fiqum*-@A%70z~6fs~DZVxy?T%(NW?$dho>Ohm_vbQeT_GdRUopr3zKk}&Uj z0^cv-;W;>(w7f#8x-M#S3EMn0zaVxgK1gBkT;`6oR{dRXbG;!z=T~urBpKpM%<1Co z6KCH-9==-WslB629ZEOLjMo|!|Jxpiedvk0QRn4#<@5{=I^)&J?`=ta1#%k@Y1enY z^uC!-yZv)~_A6|h%!K5g1ZnEV&7BBj<*lP;@4|y| zN_z*V+Tu(G^FX-rd~B)_*}oS}z4Q^o!v20bneuzz`1lMfd8NVOCN3twJT8PaU?OKV z3k^~dc2{y08~AN)l!47?KiM7!-noPOE_LC?&EE4x7}B~bVVp(!UZY71#D(I=W(vZg zrwR1&KZ^!V4wBCN5gWp0R_TI(~J&7N7Z@~Wf*uq&gxZzXiK>9zP>5D-eb_&oWn%Dg*-5sf37l0&)_iLmD+5FJ^&J_*Sj)Ae&UMbR;NR)y?l-sQw*!EX(fbzTH2jW zp!)q6a3jd{%tGb^{4L3^?4)SuTkg5XF?kTXT=>DD1MG?TZOf(Wzpv^GBuSHFtZ^WE ztPfQyJaW8!R_Twxen`>j0nBR zU@K!kt*np6M~-B3Z68v{{4efHi{64>y~^U_o|ozY0t(Lwd1bZJq^t%r3wh4Vz7iW3 ztwm10{IeR-SumJ0;8eLfD90I~ExxdVVu3F1j&$5z%E)fnz^^+=$*^l3$q%~3%OMHB zC<5(9h=6+*OUH}|hA^mufYdmQfYi9@pYJXXO__oS*jY=qH1WHAY*T;E8Y3T_)reWs z?&O;#4P-2wruVcs`q=k&cnOJD*SuETqw&;C?y&AL+s$n5=%^QBMp{$v|Gw?aDGpvF zK?LtOpA&@YJU#D0mwfz0;X~Q}Co2tpa%2f7oZtKhKWxvN-NSpqMfLfXo84(>E3c6) zH8`>d?arv8|I{)gp$s~%bAw(j5*@K_y>zTBJ$v(}o8m!Cy-cEt0!GDC8nTyH|E#VchcWw$|QDl zsqR~Rmsos4t`-@+c35SKZFi?s0+gFQ8J|DUIc_rgY}J}dqabJYS@8wj!+E=l&r@F+ z&%F*C!O$Z0m2a*KmLU)~RIv>cm9&F`TDrgt}jKA^-HzvAn6aHJ=N8pHHQfi%(P@<2^N)YSR`(G;$e?LflpOB@f?v*Cc?pE}? z>&B_;{H>0PgS&eeWXM7y-$UQ2C*h)L==riMr}%}k;pT9tk{7YRxgjwP)@M)^< zrgdcmoIikP&-(i#0eh}2fZ$;Q$CaJY*5n|RkW&*-3SB^?w%S(9u+z@ZtDu<_Nz<=2 zpkDrwwU%nxw`2fW zeO=w-MYexv+cf(S3e4a9qz&ZDB7pSinZ2Ly2erZ=pNTl#sIXLF?>&Z3d@EcVQS7so z_-g&e!#Vv9qso$%4pG?rvu`VS{JpadFzp1C&2>j>gqb4#=Rt|yrKB(mC=WdvHg|l$ z_dxcOX7llDlCR_}^o(Y$%qZWr_g3AR>%9w{rNZ_YFjYu{+pA(f#yU%pZPw9%rAuWJ+g7bXUs+W)*ftE?Lcm8ok?VZ zNbq`sK@dNtm@6Jrvge!embdrYh9}S@rVsIp(Ks!QcKIsi$y)4sb~S19Ka3hA>cd! zv`(4g);%u)Hw1Eo3u!3cx#4|njSsFUlmJ{?^F7zE|6S(D*wI{;6kBkk#C z)zT@4$rik+NSkrhG;|vO2%DC&;O2b;`I!L}@ArYYTR`-pS2|d`!@{q`)3d5-n8_My zv6hXo+)F|oCEG&!lm;LVyBbXljwDcp$6M|7o}N?_NT!$_%2wv?{s~-Uz}`c&P!Eu8 zbMmWFDnwY0Jact*Qq0C|5cyR)Yv1c9@MY7`aAc*ZE-mM6WdVbT z6l@r@*s^uB?fov8Sg)5kJ3|oGMLOq(fTD$i8(bslA%Ds_&`N~%gM`KRrf_*d=S!zj z)Ln+&l0{8!dH}lWjCZMS*}5SObX&B7HlA}ON!W~mFW`q)YgwMCHrP2 zxoh6220&6Kt!?$l=%Rw$2Wx^x%lH*}Y4Tyywj#CI7=2pfK8@tBKcGrqo|aaRKj*JTi#$Y^IAy`128$-d)t7=W**`e9Cp2st?5Y&P=}!vP zIYAe281tcKy~V<{5BUpCa>f7t8j#L}OhSd7F&1-86jeV__M=4rYQl^V%7V+c^Q7tY z`qtFysRFS=600+~N-Ho!BqS<1_Ld*w=4rsoOSATyYsT6Sa7U$ct!*ui=TSD+@FV6`68 zst{pb*{5K3aInX1acQb|`u9_5q{GSHt-wKKW#)c%nkn>mRf*Aex2j#^eN_(}f0QufzvG z+AQ^D=}wdh+V~NyRQxjG4g>1$VnA41@D;+Bn>GR^!p!l$R$Ou-H0*Nb-vdEhl2)R% zKDMMFcd@1KV5p-Wmm$q|v0=k-Gp3r0|G0~O!N$R%|L;R81cF~#2h-9*Q#6o;9tLbM znb5E+$0TRlu}Y?~>N^Q#EmA*tEU(zQjm%a^kFW_G6R)pX>dZE(Mfe7gs>mn$8yy*e z4pirYXxb_aGzOHn(Fd9Po!vFy+dg<()`pW)qUAl9WH(Uluk3QXdW0;(6I32;t~^g7 zW+!B&HMvCA*b3M>!zvbPR$)hz{i*{rYu8$oA=q zg`Ctw7bYEYH#&}h6uCv6o*IXh05_gV=lQRrg}mG8hgX4A{I~s(y(eHOZyksn*mqLWWDDy9%EJPd|^OH|$v)YsShRYY`K^8@qVAka43%Pczq;d&xw&dVbzuiD{gQ6*Yv$aD^! zGi}{IRu6hnA$)LNooaM=Q)ASl(^+(9Ha}49@USYq+RHvdqvZbK+jt_vVf&yyB%*1} zUvSYK5KDZRVIm<86Q$hBDo$o@`^Iw1|K^inAAYz`ky6QhY%CQo?QFG()^wQf)mo8! zY%)6wDRa?$X0v*R2`1-1?yP8PlGaQST?0;R0Je#3V-B5;^$sei+>q)0`B%x_*g+tc zZ!UaGivScDSJbiW*sYnmScyX3bK)A^aiBsNn4KLf>qSmxIFvTx_LE-JdA>htxDwb9 zN>iKq?n9`&YB?Snlti;G^{8b_hu7}S^0dcRm_1!*tjuTNHaBM)pDdCR938~poEQRE zyiW2EJJ!m2J|^$#c`xzLRi>Rb_V7qsFGJ7wR_6FCuqwkOQH2*j{AV<>E})6GtW`%F zeQ1FJJ;b)FK>ycW>Ay>x@pry}`&&6)GG1dFZB4T1Z zuJrXi4MIM z?rc)VG5K4xmruK@6s|Il7UkonLWdU_8XgIldUV(T`L&U{NBRfAFV*M83v z=8=iEA?>>DlQA4AIOLk3;nJ_6`!TpLg7DkNXa$?T(Gtveo*jxKJNzdVVfIHbn#=T$ zwj{qg>}Hz(0^{HZxaI<9lrqoTC8ldJcig23kBqBZ7|Q@{CLtbfy}Lqb;P`NJb)TGc zUj0`~{jCLXK-b&SidfHIk+5)R<>|?e!pYOr`*&c;$R{RD(f^*z*P Uu~j}f@Ev68D%#4G&t8A}FVHY)fdBvi literal 0 HcmV?d00001 diff --git a/_images/batchjobs-webeditor-listing.png b/_images/batchjobs-webeditor-listing.png new file mode 100644 index 0000000000000000000000000000000000000000..4462f6d4206415cb09418e560ddc38f3ea2f45fd GIT binary patch literal 63115 zcmbq*by!th_bs7-3W9>PARr;#U6KOQ-QC?C(v668gLHRyDJk6@hwje1&ij7f{r!3G zbFa@MBAmU?-fPb_=a^%Rc|v5QL{VPizJ!5+K@s~RBnJZn-v++FLP7vvC*jwj|2(r7 z5K}+`e>{*3g2DfB9E4RIweTCoJ*Y5%f1$l>%8`L&E)IXv1(Pzn9Do!BxX~l zk(z&z_L&xAypb!)7l)}JbeC`<&z&QzC6+H^J!a~1;{&!7@sd4cg^!q~*L?k3l-jx>c-*dRUy?^z z9cBMHHkd%#SNp$j&y)77?LWs7xp8L?e-sxNC+6XKP?{G0&!PU?)$Q&4Sy@?}u}V1< zp<}YXzkYq}?Cfm(rT5_{GYK zR#jCM0}IPp*t1)6aCG#Iwzf7mH+N|wD;HOCaWQp&e}7}9QC8E;?Ck2^URYh-70b9d z9-8K++gvL2-X5J76^`%Lqxj?$6ogb%u&u1DG!@Bhc^!t4VlXCt0Dg;mYkn%>y>ctL*t2V&x^MTap2A?00*CUkJl?x$Kjg3FZTWC>Z> zH`$_5aqY@5D_dK=+oRc^C@G2A*y@IuXJ=eZj|Gudqa81eH!NFjq{Tujc zbZY9N5w0nGR*_@w;tS}iFW4L>$HtN_KT*wLU}B0WDJdn-S5{Vn%h_If8DfQnhW5dd z-=Qh;-+SstL2T>k=^0z6n%mggnmWWkuB>F-Ik+QS+1{Qx)X*}8UsKx2|M$$l%|`zF zMBJK_wGKe!G{&s15j{3p5Z<7OpS_}#=MEe(I^@T4O(Op%NaiL>I;<@$sGFRRwyeGH z)SWS@+=6>69o~9>AChL&pPbnpfq7yDKISYmnLKXGzbd-NX3IHd zICyy1t3_vPXT{QFIeTBpqD$M%$IxS?(P(!a()4tuoXT>z8bSK5JigyHe6JK1m6{Ob zl$!srHuYT(328GgDp~akbE+E;zZq|~SBZ6ArkQ@}Q_S#%lZ(lN<; zK@#pZMT7-?1lw<{T#wClbvtS;=Vz)-(O+Rw!?aIN$4?e1FUxod2EE=~>F}cn^(`r( z@w~ed6q?(OYm=i*`lLFRRq zylgYVbG7h$eutIGXs~^D_Qpwh9f4vZjkmWT$?4WHRxR|@JlUI$v*+?1X8ps~XWyCw zNd43JQy*`X_Homn$M?^QP*PM*l&2_APrdC)wm&rOZQCmS-6YUGi}Oqwq|MP9)!W$+ zKKEA*f3Z8~qq&YIGzeqdTyhxvy(7xoR}m1{5>9{X98ydU+dD+v^UeZ@^>#C z#HPqJqP$-SeiiSev@(&?8ADpxPTMn1o>4tEzT10FckDA#;*Ff8fUmp5$7a(w1w+Zw zyT!`x{v4gh0|VZZ#t^9S8kkQ_iqPof$h7%wAIDGovkhK3?0hl9Ke|RSe~;rQ|Qz_{~Om z+A^z)x4h+RslGbm$8qURL6zennBP!_!-76JUiQDEu6{RW7#c!#HtdA`y z+|F%?YaW->hv=xO!Ix+R09<-CZE}jXde!TxD)z>f3pn9Po!0%0! zkZ^OSM3aaznoUq3px}1fkMK;dmP0lN;_JB1XUE3gl9Q9Cq@)CghU&HZz#`)?`m0tM zF@RmcYBAGJ=Y1mq68`bY$!Y}Kl1^_p4$a-u<5e5vs_lvAFaB3HR#ry6ZX`Qb1Zu+4c_2I$&85)r}5%gYmZp36$5af=BHKR<(vK(wE3u?!6j zxu5ROEM_a8qobo64JGy9SvQX|p5IOs$a6oOkAM95QODHu&+_9X+tx_>NKY7+>tO?A zrq&Vjimf#H6Ipsi>%yTp{ljM>47!H$>&Evgm|G`P7+zF7v<*W3X%rb$&m=&ek(Ga{~TTvgjcwsahlvd zh5fUTP)w{WQ!Zw3wo!(7NP*(wW+OIn?NB!eH(+kx&!cV=&G?j3rr2Yf&5Lur>p{(mB37ke!{7z+s^9{#m`4{ACdb5cs z!m_EDkng%*6W!>Rj3|GeD(ETq*%P<UDpy6y(lYU+fEAu9jt_ z-~@A8%G*%YIwvx=?>JY_2T(Dxn(QW;GDFA&37%Xo?x+X|^68zzbuZUZ(I$)x-MR>0ZX@pTN`qEI%55(kIMt!s%feiR{q)wBC25KkM}|b4v_%Hhf6FL4_E|nenmGSc2SPv{a6efSPIKMD0g;%< zX!uehj+%&+G+?pG*%|wum6g@WZRrS9gPrLz!emxUy}4>rWNdn$>uVPaOUs-4vk}#D z9oVa7Z@#IiDVXNR+r9fOwl>)Ii3yBUsXmbMPEJqPj#sJKugiKto3d zfrdC`Z0PZ^O=wuyY>PXaXcR$DSC?OJ@0MyvmC;aEX6Cn4E@wG)^?}ZSR|Q2y>i{_A zHaDlNb_FpR597DqpGqVpC6!lKKTC2qm;Czm%h#_TNl5%Y$>*Yhz4l8i#^rL>I3zTb zn3XjyDG9e)jmFW@ky@qPXN31I+;luQIwr=it<5|6g4gYw;?JKyGjns5Mnkw;L&<%A zGX+o1&o@$?msSa%t`VUt0@Ac~>rG%3?`^(3Bx{fP2Tolf$8k$qY%#K7N~HGZ-U1J= zifb>Ac@qJnv?X?yUv`q*v)9zt!%Q5+{< zvL=yL*3~KJ@&(+nR@RaI==0)2R+dD`aU`og<6fO&Q1}s}n z6ye-wF3mmNCGKPiSEgVzKlyivdVlD8WVrXWYG<)Idy76xQ5&nyO%dX;;_1ZJT^}LoSz@GJ}PYRZf3cDYJ7=W{?S~FBB)tEoE>* zXWBe)f@pdfucqIqV3L+Q^@y`_w6!n%c8m0=ONC1ha&?L(G(s^{C@RUkWS`d`FHb&| z*a}h|Q&0`iE6QE3^$M!yfhP(s(=oaB9AoQa547y}j5I1tlmxZvEgu zQdgG<=J4>azNrcR_;i5oY|gwaIxS7A&FjHuOSqm^`?$Zm+yC+54xZk0?Bng#ku<1_ zO&!pk$$*W#?Z63ugi52sCji$ruTR!to;`b}V{ZP6Urh~{%V~dQFp=qeck-2w^Rj0= z*dfP{cSlublazylgV&%;$`pAY&WTA$ZQUKUk&}?X^YQWB-`^*&TH-=?{Maus6;;*f z*jQnGeG)G(FaEP~3wwL};WVBwr6N*B#?z#}!KtY@EG?IBY|9=o06jA3_aZ|Pjh5@? zyWR*qM4!Aoh+|3!ZPo}GY0dd9+%o1%l$*O5r(b+wLF^hVzf=ykBl2dg>}V&l>UTI| zlY*4fgRoVi|0sVl&JPRzb=mpcEVb*HG2m2C^b1qi*2x{5?O&g+m1rSSkB%Mtng#u3 z{O<{F&(QaGd=L<`2(f#jtgSaxdf}f1Vry&BUk$qB&I`J8U_V)3_RNjmrHzI+(P1De zl8K@Z91~tWlRd=DM>0HbfyKXhWWKrOZ%&fvm|MbqCGif^=PgE{_c*Imsx>jYjATbt zUcm{WRCz<8<8jf*+IF-YTrbU9n$K24&(xF;Z;2n{0|U!w)&z!qMdb@=Y4-(cs|}5X zkFhvuvVwo>{n6j*j8a~guzbp!7>X@N^QNZd`V%_r+fVGiPcs*ifbpJ%B_hA1xT5)6 zhIkH3NByVxtTkNZQd;)<>bX!gegEiDX??hJ9DE+R(5HG~XptL{H)uj1g5oFBh21;kRclv)j7htAy17UAq z-JJiEGnVAFlo8+in8>-?_hX09=5Cb4aUf-RWkpBJ`@xYpNxcqf@a7B>5*!Spb1+u} zu$(s$56^oLXfU|AxO0naEG)6m5Cc7K7RAemh={L=j5&+vMM@=E*2}E`Dd`y*P3g9P zPF1Erp=1mGYIia$H?-rr@5IMMnFJ7O^XQ|dBOYX0mkEEQn~qj zOyU_LBIs1Gm%N`mLF5OAg{^PKYxgcLrUtyiI-PENtl7k_7R`}c-QWLeYWfKU1!ZG% zvxf4jRt9%_<)@>bH&1{o*66y8BMIB-$9B=K+%pblgr7Ft%R#XA#kM6O?U(HgQcBM= zIlSMPw~5UZp=>BAWYY1pw>>%>L-yarf}lGn$?=t#|)1 zgA)nV$uw(0iq9&_$XdcjbRSo8l*OgwqL7amDQ_4YwI+GwJ3=!aPj>H>TxtD~Y^Cv~ zAUbrxBMJY(W(*Q7zv~y@g8A+xN)E>cE5{mSMo3HQ(~{O@LcUKgBkjh7J_O<`mmpa& zh1W;bgrZ0WxD5UL{45dJWLCJ9NIkuXBb6=bFk|z3NOMh3VsPd<4&O{rtsZmE>aKnn zV@20e<+p{?#v&{U`9vMFmpv#E6eh9-U+I|?MOBzTq$&%uj zEhCB;6qICgYP&ivAyxSm&fQ9XGMmMv4PBqlT=pD>##rPRW>oIL&GWeg)W@+I^S@J( zfM@Ln`H=hQIv)eyhijedq zO~(<_2zM8Gx<@A@-Fp~7j$sg#HwV_<^Pp(~C&)HLG)ju*j}M}zkL8di6Ace(Al2tXesqLKUM+;ntw&CSi6ifn|0 zgc!oB3k~+qo07KjhDh zq4^2nMj!%9oD!yqMz+gVqHMiJ?CRv3MmgoLwh@?k<}sIk6tN7ou!7iZ^oiaCV%ak2 z{y7CWtMXcUR^5~OHS^7K3P$ILguTw@QTCjJ;S;xL1V{{`!h0*qIW^sK>JqAnuX{%B zAs+^3D!UjvzWrrDMd!@&J?Al3sjz@&ZA%nAk8p@zf)SMB*7j%&T1#JQp0mZ4=H-L6 zR;1;M_vk&$ibFy6-hJNd{&}_4`kIr!Z zV`FYV(_4KdOewOfd$IA7YcV4>**06wLS5olS^%61`+7pp6_g5xFI9JBlvx5Y-}uo+t0}OP-`(O zU}QulCMKq!pz!zaU!N?BiRtO_nVFe_vah?vS}kr2b{k*PJWjiRGhOy1)N3 z0PGnU7&4uYmVU;?WlitRHaXM9#>N7gXnki#%+z$paoQEM^!I=-BV}NS26TxHASG$J z2vTz^NpBx@T}-U^HS($5nW%ePQV~i!Df8J5V?!+X6a-D4R-0Z2^e_)NYuYCx4QLh)7 zd&n(%ML@UCH*zHrqP}|?XGn7Oqe54i@_4m3Vc0EsYvbs17ak5K2-E=o}Cw)x6 z>1Z?S>4TX1B7Z2Ab`DQtCEsW7iw$RIH3se$swH{#e<|pPc<7QkM(Av91h$BgTwJ(e zM7)ozWl53Xk(4!jE{sJhS8|!{T}=aVvRXU7k)xEb{}LVnDK_NOgX2o;a;M5)%nClK z|NSWGE=E`Qi<>UCL_JFI8b*M60=kN2Tl5w4(%;pUgcVhZTzeez7jEBf{^&Z4xD%r) z&S-4>v9_Qgjt7(TZ7t>1&nadd`r3}J*|)oUCV%6j-CKjrQVAkrU(vGS?(Xd-&#%8< z__yNQJ4%ckg}$Q-j*5bGAQC?NKt`6ksI_ci%x@`!Y_IJJQx(vp1XXmRpj=@E-J)We#v~p6fJ|d~jIQcTR42Kz% zVu2ino?)t#%=JDr!)dRT;>uSLYfXAHsdo;lyGeV2no0 z4bG;HuI-#TE-2+KT7u4w^}9}F3My>RI?s<09v^dG_sqPEZ+{%rq<@=ErH7YO8klwV zVt=VF`ZP&I!N{o|F>)mu@m4=uPM^sOmWes}#_J}?B7aohVD)SIrznP@-pYr%;uF1g ze&^R#<4guR?#qrvJ>K_lMrN%fUOhn`%Go+*gu}ncjmGL08HXr%D-t*FXk{|t(ec=P z`=rPk{q)_G8SHd_-;-C&*7&v9J{d{}fNO$tuY*BoI4Aw>{*kFqv0Qh+xLI@2f3Ev2 z*A?#!F4cBrRHf~~o0mn&GKq6*IK0sXt48?~1Pe@$1eL5MTYg;#gBl2oXMaM@ahcL* z3sq{p&zxCzs@bnane-o3?yLEoy;$D*%!RlUdi~6uDJg!Wk5r4GS&EKPp(mv6Z^M2R<GqgEOX?3Ms|g;m`_!6 zJUOoSMwGPjemTc?b#(=)x45JP=r3y^s=5KGAEEt{?^h_!gr9<{r~kXJSXmF^(zI=zRU7O`0Y)Hzl7}1|47O`ROCD!K^O`g#d?JSP zK|x-Nc@Z?MLPH*r`K+}nfzpua)w)Kbe^i+@tB)OJ_5IL21OD1H75!I&(-aYb|9{u= z%H)f9wCinFm8J#H`7LXbx3^7zPG%)&DdZd$85zg=p;6QQKnkj&WXX4w0YWBE3w`a! zu*aWjD9;WWg@s~(p`>zRbpJ;@;(p=ve-;`Y%{r+5xdmGY#ivh1TwHj59pJ8r{m~W$ zN|Zp$9pUHKg#xja@33}rD!vg2w#ppeVRzh}IC$j+$}={*&4;b6Ew-45`hm%*snaf0zUpU^;Bcw` z98T-k@wcE|dHV0~D9Et9j6!G@1^e7A+S_fvoCa042|1vIMTUg@k&JR`C|FT0o0jk) zp=V>Wo*bY$SUYssbj1)rvUMJ4lxfdRMJyowrjU=mdZnrE<1gk{wA0aa< z=ueZ%;GCtq3`|tJ%)Z8gta*5T0R_p?LX^d#J2nXki2%H(m9t_1TGl)eXKeM3Lmon8(V&LzzvaDxV zKUgyi4h{~$tlJzeG&JcYSbBN!^4%YI9h3$_o5D{y?-VgG<8>h$fi*4Tiu04WC^RCUWNxN-@UCAlYKoqu6_K-S|sse-`Pqx+F^_ zt*gca0#qbH$HHB^3-SBL}kEJ$U!mqumelE^_iWPVLuNQ!Y zdHZuVOW#ya{IRTkU-Ac6AsL5Fp%ST2xeYvk@bmUr_KldlX0T%D&K#|0rj)le7r^g3dC#S#LeU^=N|%gyADI#bL|SX3pX0h?UYdCONh(C){ym+<~`QG`g3 zU|Zf=Y&M1O-mMq|v~69iSItY;8Hr?OMZ2~*Cm-#zX-tbhk&ttw4VlMfKlR9mUv6JV zg?=t;3#oLE1X?oyoX4l9oy*IFB-usDxg9zgxw(O$YhMGcx}hN{;%oes<)=qBK>AYW z=H_Z@mBo-t>lqpba3qk?;@~SEmY#SfCQZdv9qo2 z`;wj6OhKsJre&G?sVxE3C(oRmkbrILdvtc6d#B&m`o6=wQ8S|R25w3DPvq-u`o?x- z0g99s54Q|XyX?g8!(re95aTKxARPT>7uf0zqwd=Vxvx(;zCOQgem3P5&oAP|pvd_S z6L#H2Z?%QJAellcWWDJq%C@xrbxigPhDR6Rh+sx1c|N(9Uz*SFz0>+@N>A4`FnSl_xi{<~t&v13h{-eP zSsGtx669B8Z66ByX3e@}!dK!2rV~1oSD(E&8nA9t2xl;z6RLg1k>j+^-!mmLWICD1 z_P;oEUaiy}~vb|Dr30KmR&~tgaZA zsm>-)8dsDL%QE>$C7Y#dVi1mr=~d?;o`}&HqurUAEO&7RcSwzdvrTRDYrl~9yqwZI2`}aM#buk^J$0W**pzo@Q}vCn>yxgD#N9hY&brjFWQ|%pPKaLA*n}ss}E%l z-1L$k4xiVDw7Toj(ku!CV`F(48Gap|4}gHR%w=L>=~-C72h3A%}8f3M$rnP$BF#=Hm3u{=H}-8 z!;v>+CokO!2hMBzFVT-5KU{8i@?6h`*b-SS)dtETpxG;Tzp|pFqy#%u2hgyQ#wQ9!LFYC^SYY+(U;z zo|=;VO^lH~Vp>^4pJ5tc`~3|yH-c6&hczBu)Lvb&gF_D~nl}Eq7GJLH5rZ#9d~H$Q z1jhGhbAfYTZFGG^&X!Liu>fl6(KW)NR+z-WPZlJrR_Ve!S%HYn>zuIvFz(HOk0;L~ z+{ap}H{iq#4|!_{o6+Gpp@;d}i9h0H&6|llOT(KfIDy;|<3@M%ofGq!`vb3qH2e^C zQT^;#1aYsgM1scB2~FVP%#Nk6G7%a#*Dy3L_!Vomgrv4Uetl)-C=mhuGO)4(D=v1$M=m(^F!UG4HtrM`UO2h8W%nt-Jx4KP3fX9Kpm`CeSZu=#YUaQ0}q%ZVO9 zbSf2k&xePH0g<{6xGLam$ z(9(4b_rTXgzhQZog8@nF8}9c-6j>O*37^yoPhm;>5o_ylIm`e@e8;5o7p9Y&`Y(+5 z(2Glg0uqlYYLP&esP0pb59#vhsVf^jZ|WNg8SM9je@oB0HbxMi%_wX9E)hAYHT9A; z46-&s^NQzv{FHNF&XyQ>oXCC4Z&0+OE6(BbqO|1>?QI6E2SeL6NqRb`WCy&LOFzB= zkr1BMkRQXQTi_ciiogLmDwK%{)~wpf-mEVEfoD`)=G4>*Dj70qHsSWUo~rZ{gzhLa zsQ4lq(PbRTssk`wuN2~>>Qz~fc9=fAOmAuWM9J1J%@8AA(((KEb{W zA{S1b*xT;f%pvJ1A6w3O7x2rPvv>b2oxC{Ohx7D{L}-6KSDLf<(w^R41YpMiMhb$8=!)ddo7Q{kW=BI;WLf~B|EQTQA`yzkcu zp-ww&>@OhFKtEq!U;jNZarV?2wD^$W)NG(8q;g7u-s6xKkY{vpC7UX{p8$WG9f;a7 zJNj)sZ;btE-4^DnWcB$g6oW4g&&br)_kL>z6kN#=JV<)e=91c<|35G=@ zdp8o2zsY*bOGBeFf`*FdHO5M(Qz3C&?4f#o%9`q2rZ$AP6il~jEU0_5oPC)Gc-L1( zG}4kU{qM88EuX$s1-#qF&<{s;+xMM+HG!<4sNfsHQ#q zDp}XsdTva(X0gK662dJKLfXiQ>0S6iaAjT7tll3hYPJ)693n^iEZ@06R z98r*vU_LN2|I}=9l2ug|6A?kUyu7re-B46i?3t}H<^V16fVo*s5;y{QHvx1crlE-d zLec*IJ`yS_WPi2_WI@2Fj)3pDiu0goKT4|%5@u*F#(6^W_ugj)3EnTe6C3}Xh@>V@ zXqJwyQK+~hy7$1O5j!HPp1?6o;IGoH^-a9sS1-0cgI4bnIe{ykh&JBaM^@jgs8^;u zW|ImvyT$osRbHcB>4?#^H=Ep)uI|g!`iwnZ7Uh}8KjWv@%PRCq6`G|lV4qON?cZCc4u=2AA@y$1^eQCXR zYussJHbV2?`**LLAFCAu)Jig}8!d!|Vmab4aV_)+wv^F5xE#NSe=RAmtrdAbGs8jO zOrJOsX)LQ(PUN}nM0-7~H`?`W23PU>@kQS+jlu@Ch_PP~$k?QciW_(ltf zd^&-*oDSIf7R3UF76CP4?+pz(G19kmXT=3^3|V?r<`Zf-92yI%{a?nuo2qjetP;=J zwdp950F4EBUn9V|d7O`mf)3^Fn>XLoN_v6Jy}LW?ZRsbTW5%RHOF@xQU!MQ~&g-kX zii(cj-duS@P4O#WJkd8bb)9qqA+fNy2srJX@}Mb@TG-gw$Rv{{wl0CQCwRu18sMhxY+%B7vT zy|Dz&ZJbxak=|iulHeSwb{;;J8Yqin29W0b)M)0PJJC-mGTC!YdpZA|*F_oMXfJJi zBVJivUQy(;6@%l;3~YLej81-edGy2Fg%ee-nBmX6dH zHKWZ@WKoFM-stSvbxJ4O#oNsTp`1v_mB{;8|v2VcB+Q#4D7x zvttC*ZfcUmf-1lfE^cm>x?KpTXJ^nJ4LE?9Sy^j;g@l9<5fOa@KpObH=LX3+Ig{4= zqGD1~z5)QAlam7s%e5A$sHk}v89v?JLI8Z&+Ss)B_lE+N%&Fcb9{SXkZU2nu8hlSj z9ier5CXXINJD^>_C%MqCa)~dw`E`zCOp5gMp^i&T8UOgA_Os=i;qNARwB6oayF7nV z{`~%Mu<_<~N_{Xay`5ce$M?^j!v@2f8`1H~Ukuk-cxUt9Z?kk|xx?((EHuGKvJetonY3?dbO2J9Hj3Vu zpBZ)7(!gB<|49t=V)`NTE2`A^a}8a26VtMBE$Q%gVFe5cdXV=WKMUYZ(+Vy8A;AsY zzm;0GZy3}a)b>HYV|n<+quQqf&KT8w%-OSdxeoVl0@uoWh5%8~=gU8iPMCmVq+F&& z6UMDs2Db3)_nbl=9^CSIQidQ?KtUwvdb+B=iheVLnh(7caJR0pJNdV9=bPr?1bz%1Q>T z_M6yhDk^Ai-wN2;+76WO{QmvL>1c@tI+t)R6LUoPSzjJqDGrnSx*hv#wWT*#4~jWs z$3lH@_q9HT;)E_eW8V~w&uUb-{;xU&dJ>Z{VgiJkfnORK2qj2&bo=iV7l zb|!Mga?>NX#}@SSgICFuEM;RMZBmd^+b_0P3x+$z`~~&bQB!);%I|-5^#SU zv33zJet^DH7YJqtR(L#a4&ghET066FGJD>h&uc#ahlM2)!fJxYYJU4KsSEhdDaq$;xDkPKb91GIVf7M# zHmyyn`yaX!9F#@gP@p>knrJ`|q5^i-oE(!UP9)p3CbYadtZ6(JA`80MREZL$?C~A? zoz60*w*tXjiBfrjC9MtL2H4)+BOo9EoaZ9{4vizRLexmop9`A|h}PTRF3U7nc&h()wp40_vFmzZzn3?s}zVXkg%A$OCn_ zK<_*F_08Sg@^LF$&D_}#2DN{^BO@c@#_hfjJ|U4+xCWPi2491f|28MvJS^4!&-l!= zzVC8xuLwws2~5UPb@6!U=v{z?UMSgoNpuJV^poejyu2UD$(>!7aBy%ywRLiBO=YlJ zU>AwNo!mJO&63g5dj0pb49&S}uLS{I_(EbM^Yzg1uo1w-77O*!2Y~2JNcbWs*yRX0 z0~E&DAk&D(G_PAr8py!%%F51*o0@j_*S5g6%`Yt6;~49HzAX!w4Xb($qQ)?(y!Dw1 zeT(_p2=MAUvpPVxQ>#@61Cyz?b1R$O&{7B|CvU3l;Q~ixRh2A8&cGfkC>>CPoS4`P zn7!#T5rSwh@{0_`b5dPIT+TGoyEeflz0>XB=ZdaiCX*=Sa zHDvL?t?V0D&eEE@~(fHK4J*dl?iZhd?EHxTfG zJ9M@kc+@ zUcHBbI>C#|TC*}TRvOP*HK#3<8>dbh7NNFsFx^v8Q30E@*JL(XSVTa-H0X-4=4NPQ z#0ZS;K;Il!|KaU^a4+RtH>JKn0Ge!z89?lIcJ9rURaX9hq`5}|E#t4}6i9hO}HQtv_5X=SipYLPs`y00kf?FAp6y0KzH| zQh+N&eN$djb0`o652n1b(u`#xKP$@@m{VZT2nZsJip&RCfpIreFo=SVj+}xbxV^nS z*-IKYo}g6H+?*P)9!9macqZ=v|DObCu?PqV{DXp)9U252VgaTG7~W5*=pTl% z1Zp~PaBwK9hWdMfE7&I?0UH2stLr2XTw5S0Xm0q=2yZ}GFzK|z04N7*VrE7R%mi6k z#K0+ogn|M%hK1d(u`H%O2zWUl>zKUmUBJYS&z8O;AhpT9Cj8xX0WF=~N;@ow18jO7 z7;vv8{ebS+03cqY+2svrKwVte0ki)zJ|0AIuy_u1*Z|OVwfjuKt5{fANY7~IIlly) z{k65VFyVxJt(zCM!|SB+hl;@Q2oU66;ri&rL^m)+0Hj6A%zPdgS%4Pru=N<7oIFq< zpGOZ$$Ujy>Bi?!DVZY&@;isPrlw|* zC+)*vI^UC;bwv;Gg5<)8%gMGix6mbJQ1YWWGt@xTz4<-OZydM*P* z{A`NQtP~D=k(0AC^O~ph$Md{WP7KS&=yfLVM~~++01c{ZCG)t~!chRm6=-Pv5_ByN zlMn46^QqQZX%6rL5;jz+$nwIA1vdgp#rDk1)Niu5yStMT5oH>-eS=4&1eSK?NyuqB z@cF$S(Kj^A0Tk?p(mbfE*&sUVHr!7)#7IAW{58p=1Qua*bTpN>6}+2xITv{4SJhK6YA4PVNUB1z@tQoYT!0`Lw)l3_-Y7-*TyIYnM@o-yPhGr#4LQ&(-V| zwoL+supjsY7AebyHQ=pVE|iKqEG#U3N+z?`SRCaqfms=O;Q3P5s;jJ&Q!3SP9nz;e z)Uxz6YTX!HCIof^;i5@O@K7vs%}q>XUgNW8SS&Uwa(LcWXHkUyNK7n%=?6Q%3}7TJ z)i$pdXfhU-O`1OeSyX*-`JJ$=17~U_06d0wmj~jYVM^XiVYd~Stuc?UG#rRin%><6 zwx0qp3?t0pu>Bomjq!r@4?@Z&M{Z_xMlEvjbS}^y8aRmiK<0Dn0PE=}9> z0>B{5!8D#GL(YIK3T9?zdJtq#OG%^So{hb|KBxiryUJ~Pz@XQBci8kZHuePUIgP5< zV1m(#<1jKZlBTTAk-+<5PrJ%=oa}M=>CSt9xh)+?R9E{m6@Js&-tSEg58RGB5XC?? zhniy4>#R1x@Kb$Zooevj=;*^b$Z1dmc{m`mTHLQH?IBXYYu#1k<}_~(dEan)TU=Jw zr$b*3+%v#uaC5s?1|7l3iPzGbtu*9^lzn@FPFC)AVRm`6JOaj)zzF31X?$CA;cMlo zJ8){?6h{O-G(g4R<*xw8!hJcTWSJ;8O<><3UcNH4iyHpBbzEL}MVsHA)?ZWn7n z9yGkaJ~=;JY|0sevj7wtK-V=%zU2-(V@0{Az>ctba4-bsDjJ?1@1XNe;EqgYOUHLW z2_gmqV$jhL-OgvezP>8uI-ezy8XGymh7<(GQgpJfD{1bBx}E+n>Q;Uu*@kIQakeg{ zY0qtYMnpy`0gst_&d|t+GQj+K`0O_Pplr*6zE8dO=K8vN_n^h#c%>sWI$Abax`+0n zeK?h?ZnFlIP%58O5U`qimcZTRIC2Da@f4+fgg_uvWMpJC(mr&tT!#HWm0X2IMX7Kj zL<&ku4irEk%c(6L0ePJ2^XJbmGgQ>osle7(0*FqnwrBCN>byt8IEuBvM+Pr!9*6^h+{NTYMBjNJNAp%qEy)YssSETV{fatv|NF^R9xU z08iY~wFFe~(3~8@fv5Sso0dwf;+8WN)tjf;rnCB_u|Cw5^m(^=iOejymjMA(g5KYu?r0bw!NLvY{VD9*vAX14mnT-Nc_9iXC+ni5dt zEpKv=hzO@oCBAe`M}a;Y&Ifo1j~B_&#*SUJ(&?v@%FF5Dd_dL;KW26 zqF=6t8Tt9aLZYHEptsuMj7v_QH)(a94(5Bj_WR`5c7OT`mS5ewO&6s`0ib9!K<5If z0O06f-cOH0!omTQKax^YCBf8k2pH%AQB53Ar%jfYmiCc}DaQ21(aw$-90JDuz5zA= zt-!T+6abSUKYqNGm6s<$L?!qL=GRGqwotsRy>t{48~d)o2ruUJ%vr z(8GQQWiSzXl|Ye*`TqU85|L9^)_x7%V;~koxUkG0xHXR*bpXdWK+!`3@I6gb;s?g@X9{H~qK)(-+IRygG zXKTlSnw-(|RU7u=BQQ~3@3@BvIv5=TgJ+--1M^NRF_AaVc@M8Ama#sAc@G#EfW3=1 zCnqNdm(|qNVA_*#l)z=667mWGx%?w3X>tqS1&{Y5K1lZ9!VDr%5X|~0;A={3V05gk zme$TGUYgwAj<&cv0|1Y7cXcERa^C9pb})q}EbGyH0fYAZOkjI`@tH-Q?SAjLOaL6hu1_*~?!omPxK@X5I z@ZjhHmk10D5RJf7Uj;Baf!&rE2Gn~5{)2ia7#UevQ@c_e@atyxtJmaGDeYkH3uYn> z0+zfJ0CiO!X3+Bg{*_r;`WooD0I2GB2fxWLE`A2{MNACpO!Zj5`m`%Serh!`HZ}%^ z<`KgqB6KY+Bfy1b1AVLB?E=WFl;xmoLMLiKP6`eQ`3xq~IURSqhkqs{v?~WtP*JUe zj1rNnebpDM{Gq6@(7)du^cyhrY)Q>l$`?)iadB~s=zo~~otZujM$rj-QbI*uMgt^J z8qsiYLV$|_jQud}!5qi7#QjtS{TYl-%Wkfha`#e;`})p!-rz?;H=N( zavuk@P+e-Z`hBn6^q&Yx2QSt~_@0C`H1Kn!(<4F2>j2EFtIEXR8;#|@IO&X9-{It9 z@dQRRz{i8w?)v>33AExa*T+Oa79V&yuE4?Ve2A~jv7zXc{}c!mLwrsLB+$*foNl}T zy}>JhFu)pD7)fJIpq()!jc3STIR`0gxuS>x!D|15e9a5B69M+IT1z0cMS~<9a-Vp3Qg4#cd_he5b*<8u&^J#Jczb)3EdYh_I{>;S5~`qIq1C8|0ihku-IALh zN9=#xVm({S#;0l~=+ubz-cq_nEHv3WCzrhQprtJu{>EKm@V9EsKBUok+ z-s!7~O(rrj1XEMf?fE)7nALt5Iyz7l+rj9Uu7$3{>c=LX{Gpra0qOiZ9P`363bqFA#D6aO_LxU-p7 zPcEaugl}N(3p#KHFlsSjSBD?sI(S6b40_MbFD`)jAsp-{Fb$1@MW^KtlsmAO`~Y%V z1s}sOQ)f+hW*|P(e*{7Unp(hM0}M!vF2^gdfWv11$Y3iL9$>02keNdw@Yon*6jL}I zUxFkbXCqPqN1O8tjuxgJt5JEr$ch1k>#W*kiqWIj|)x`in8YtF+%F0;4S5XhL zeqm8j;-8$sp`ngVy9(DcBS44{lanKM1)`YCCbF=xiC!Hpu0kL|U^L(1Z~+GtiQ?v= zB$ikHXeQ?7>!2`xk&y7Ks5rrL_Q;h=4b}F3cn9q3-LX{48K9d6%l9JO8l3F!*q8ud zFH1F>5a8kAp)^)8Jx$3ypuVv||I`PT8k%dsPSu?*)kJwDgp8y|fp$O_;8@U527=EQ zFfcIqo|1yBtgH;E%q&pA64JUl!Smbd_OPC-kH$*BzlF7TlN-5@)zuB`Zgnn(;_ zFR*i2jy}q1*8*JJ+0zqHV*&7`q!!=davLhJL93{!Kp)lW>MD#Gc_utA@5r7uiGRb? zF=!tMKwb<0MGCr$!JX#@1<@O309h0&y8>f0_y-nzng-YsL9rr&H)(v{ydbmqfn7!n zVvEP^od5G@SP)RSsTALDFM*ooGqAY^nvmj58sMew0#3S>^>uiVjsaT4gR%k@`hC}A zNf?)_XfpE|_P6Jz*LQdNVD(@gr!$z`&P|kNau0KQy1St)E*BRUl-B@$hA-mc0-z`J zpK}hqEl$G4SV(zMhJcLS-W>QvK)|=!dN~dGJ?Bq+KChH_R^WZR+uMD>A#Z8pC912d ze}fH?cmZ;c!Rf{yAt|Xyki^l!{65qJh*XfO<_S6wAuX*mkinwUc-(&g^g!=-{vID+ zBPHxoYk`}YrEW{m{+7~z-qLO`4P2XwLEBMn}z+BI|o`Jub7 zk7*B_{tqB9LekO*!0a&z2wuR)__Nre2z#1c3>brPqC_5d*3t3*#n_ubW8HS|-&c}Q zL?LOA290EBPLq`8Xi!9mlvJox3aJdGc@oVU6-uGOZB~Ye216rBG^mIaQvRQ_XZZd8 z@4Mc$-h1_|b>B~y%XOXK^W6K`$8qd^KJ86`Z8B}zF1APD=T~>dU>Jik9!8Ge$5(d^ zzy0{3?H3!F>%`9B>~!L^rB$q*Mi$;BAkD_=9$J0}G`BV%T(M$>5={2FOP8hq2sFHP z?!7k}H^Q}~4?UuN#jlt+aodNF9}n^-hXG08EKuz|d`+>dUk0;Ohd7AzDul3>-`2DU;Mh%{-ptcX`0Mt&Qe*G3PsdKg#tuy@a}%C^>44(G!oD;UUI4%nUJS0cj=gb13r5M2vgaMZ z0m>9kYA5Vg=s54up`+GmTi$Wg`nx*pgC_*F$|atF8L{dFkHcUikm7;&^| zgV)1lrjJ(TtJDuD7%~EP#ooM{hY@#rjCAu=n)_18aN}2N@j&j~zrSnWKE+WRKC5e8 z_)+%7w^i$%M~gK|sHp7p_iqD8 zzA3w}<7BzZ7mwO1YVGZyA}cRXHP)#i`0}Mo#&hOezps<8GI>{zzn?Ae+_=1A>kYa~ z5@@`bVnrX z`5BMPCz)#o26qoyInHWL|4%uO&OI1%>ei`qNi)C4$+M1pRz;mLDurILugs2oi2XjX zsGMl|N2P%gPJNIKltAt#N~0we?Q6b_iP$^T1-7W>P<7*6p9WG4j-5R zH8^j`*bZkL&LsN{9q>5pzaPEHkHXx3%^F2Z%jf&|osW%Gfn}Q6W}1q-&qvROZoiuQ zUYNE^@qa$c88h(o?m=^~WJMO4n%`@@_`Yo2=elDDwEK>Ztrg4kuRna^+spsGJWWfL zRQjFXR(x=)`K#xx^428>UUJM?`W>e%d3mC2D@m;LQyI>?*}Qq>?H1eED43d>7X6wZ zsMtM->~?!_Cyxa0Uuvj5%texttZ;U=X9!TQ-N{L`p$Tsm)#+r;FEH4T=X{pL6cwy3 zf$zY?(;BFOGL&+4LBu220*hIPS9XhPo~u${C`PBWS5TnMEGz$%>OtZ8Vd72ns^K_ZtSV7RzfY=mAU`m~ZbE?Xur zM`S&=Bc;yQU(bLbqH*)~|Ni}BN9s&u2+V%c zyhPGtoYzi5n02yGca-HdN56+(`n^1UENfU!8RM1nFgI6k%a70b2hIn}DJlkFFX1N| z?#39KD==kalABv}Rz}pOO`CvlZF}vinh`^N0U3AhxoRnhVUv`G99cR2Ug~#XqKCe6Jx$O|! zBtMv~s&zlV+Bp3NTP;4hgog%Vk=N8bBU3$zUT8a4*C9_zAHlYRKJycThRh+TLj=l6 zqBfkolCdkcHiEW4IHR+d0+M$Q95l#*Oqcs}oOja%V`C}sA3^a$N#Yov=5MCpT0MJs zr4EXLI|zi^KJL{RCO>-g=$?Ld;3@ELLrvL^lxTdb_o!+bgMWT_Tpn?9S`=O#BB*JM z>k~2GSZDwH+K$Q%5%8;-7^Lq;O#;3xF zu=D3zBbump=|VAnVn`GP=Y++JyTZ$;8*S2~k}ydcU+{h%>Jok8=WgDtUAMd<24BJU zGiNjd6uZlVq#~{F9vU)}#hHC(++O&6DG9&o&DXDrW58Ir8whs#nlPVwYS=;B1 zwJ!UjF8je6BxM?Xw1y1X5B-$RM7SYK5|o-kK$A;(BmkvtWJVt#sm%4)3FYZ!ZVlsb zMdXf)vWk-j-|~Wq?9Lk-zF1uvV>8L`?jPB@mWXP6yR4PMtwU6WlcrA(s^7Bk7@u@x zH$|Mq($|vLR><@3P;HsF*T8N?-?(A_M{WJ9O`0VNi>Qt;xjepZJ3i~d17m;~i8mF` z#feu7gPN`f?cM8-)gN6Yq-eT%S*)gi-@46{CZBBWd`fxIxXkuCpX&!df&{z)$(l6B zWme^pBS-ugTc$l8Ab|&Gh5KgcmOFd^$ppL`ytlHtuD-r9G@;Jy4&kRR-UZ{ktzEkv zhXw=d8pn~#0{Q3^0A1<5bV96Xb3BD50cCaF3-2N zZjaUgK3Gb$ar5v{ZHWHD&tyG(c>lh1X25nX~?9&!67`M7IZi?Bt=qqPx6g zj3Ey02^JPV4j*|9Xb=QT?*P1-y;i%-e3j>0Vrbodj~aH9X)pJ{LPyk zYinzziaQy<`TThgGf7gOJUL|f=j-b+1qB5t$QPG&c@v(xa+37iq9f<5tkj-pZatCn zsHiA|I%84JlzrXgz|%?5n&Z5D;ZLek!*AW%u)FTS>C^jspbfg@AvGS;wOMIyMXB;E zW_gFY*BnoZE>kx*y`pP2IUBtdc-kUGp>?WV?(O|{21tfaF3swC?#h*EB*8h<^_h_!vNnXWdups;Y)?b{f5(3!v`lZ9xZj%(`R>Q z$!fKhY2S18i&!nr^#ypLK1^-0gwxL)xX~kLg~R)?T7F{W<*0R);%nlSFlL6;E{@jo zJMIn<-8Icmz%DX0;oUzzg(Kh}87Y$CC4j%6rt1>pS8i|pPdVwKO;n~TSv zf+#vA=~-Qx?7Dt@72}6sT%ME8zuzr|s;oK4WEy?^)Qs;^wzc-Mr@h!1@N6tOK!b90(P}&a;*r8q-Gs#3=4_qh z6)NdT+OJsr6Xox))}(+M9oG*do-nsW(i5Iad1cGE1<%96=X@dEUa(E}5tVLZ;-}{^ z{XCn#53qeEoY4e_L(KM()qyz`5t1eV(iSn&S4X*{jHV^A7jQ_lIfzKXl}XNyko|viWV4iErLKvTMlAop~rE zB+CnwV0E%>iL1pt&8}Tf$doG$Peh@<@s>_%-I%ZNi;i#}kdY@)?!$Ko}OL`N_rW4HFjj(=aNK0 zcLm187*fc{G8=GDkFm!;KB@flDTqhGq|D9&S@xTOsRIvd096uX9W4@4@H7pLjjh7J z>{C-$-$`WYHFW5FyFnw(UK)$}t6mK?t*~Zf5Cq(r{fG5jA_1l7(FDZgLuvR`#;m50 z|J(Fvy(B`CkMSk7%q_&hyF5cdVmE8_J(q`xKEFrD%$!@h_0)ReCj@wE?C}eiFPD?I zB1ewi@OdYB?&jOGCYi_*f@p(l zah&h)_=bb2FvQfys)vuw_gpr|$|^p)u7^aBbzA>5wuMZ}V4WWSv(c4k=C!?dUyV7Z zjZy(jL|(JPt912e~rAc%%8qhYV7U$pjkt(Nh<2h=31J;F$FN%f8hS+a_X z8GJoK4Hk`F@}E<5+of1n2$&qv%t*@^=^xuS*HtQT$OSP5Gpe7nJ18g`#q`eIyJ3j6 zI%^tN%WFx)?)b&U4a?ZCS$fskyx`bZO5mxpXHQ|dAco&i$x*EfCPwwz*|QSB&Z1Zesl}H9&SUIxPvpep+mn8cT5){y*>Ba0w+w$@!Ydt*fBh*K)emPtc z*P#PvQp^X~M#TIx>m5l~oWxLDV=Cz6|z5Q4Ij2GSd?|s0|AR z89d>GLoh2TN*&|?0efQFOIXSfH%KEvOL9;e-x3_t;N7S>hlqI(sUv+~bdhOue`1_y z1by}DRkU5I6eQ>*J9Tew?@Jxq$ZlNI+85{q zjbD2j7<8nz6%%hI1r|>rZu_6>{iKPlEHI6xPeUsn`(!3V;~v;VF7Zorz6G0-?De_q~+xP%e6d#vo`*-xIX2i*%GK2s?(F7+bPn^QTdNm~10$*g3P*h` zSf5|s?m$cu$QP~T(Wj%Ow_QtnVNgnK(V6`F7&@#yky*%BlC$IB=0R2R%xrAssJNjC3&B4y$fer>htl_Cu3OG&brobx;`<9 zJ8f~|izl=vokI7pbXv}-@i!k~6qufhTSG)wr?s6>YXuQ}rTuMf>NrW9;jM2UK74os zfRLUT;3?Y2iE)f+nWp70Utv?a*jS^u$PjLxx~$!2C| zG1~jf?jJsLrUSrI`!qzWu&`sjGlhQ811Awa6{*!LPoyn50D;)P+ejtx%-uT~)n{7O zX`5f6o^G$GsE8?MGHA_q_dz{(Go7E*5%1o=j~RG-mU88*KQ(2oaRBYowTY+BI(XN4 z#Y&lUb|e{0qBK(Nqszh2P&uHTpu&GQ)u+R?MSE0PG&8?1a@SFI7qY^`*q)L&ox^b4 ziy$JIS4Jjr!xNO`VoGaj#Ql)>^N!e@y>;2$^E>i^XHMW zzgi+(XcHIpH`W-Lvp)i!J$N8b3h6hJ^B&9ZSgo8rkGx?)z21&G&0=1i?)Afq6eP7V?OKP%wS?jd42kV66 z%KcrIGUc~Ovr{lELMT&*>3cP}Srm_uJoH(bRk2pYae{B1M$`v~|9AnSo<4c9o%Nsv z&i!|j2Xn+yS!~fd2Wu~#@e@wPtL67k0aYl*t15RETLJm*ukTC=AvCjyz7+`A(YJ4# z@@THvCg%-&$ zwl-o^iu+q>#%VU>Z9(oL3QUB9q(Hy{yZpg{6DPU~&nxB^xo(Pqqk?%M68El@@|1t{ zb3vMmkDWL%$ZfmoL^kImYNTz~TU`imIRs(=L%-!jE)O2-)gTIvTt5pxCDA@hl6Fix z8XQcKB?Z6;LH1#+ZflA=QkW3=&MdP%?ofDjfrzr=1t_dR^Ti%22Hu*ian)4VcTJT+z z^N8Vl>oLVTrp3V}=LGAdgD-^q31n|Ap~S%s*p5tFbnBt_??W&gaI~$6tVv!4-c$9k zcMA!Z%@UZLJYJARZy^_VFmZmcj*d77Nfxb2I!t`+{tUg?F{<^-%tgzWEfaHit1B&M z%xDL#7&%gi(VCk1K|4Aslz47#tgUczr4aNcLOpx=(t(b6u}^J}Z7%7s==aUy0?+)a zyn!F=^NQ0jGx#Z44B5@0p%}l^wU&|v;(nh{x$LP`4xBpm@M;PxiS1!$Ss(DD3$gx@Py?YVKb?J*-! z;5Aa08Y zH!8H)(yHr%z@m7N_kOCw)0}o#y9^^G$WUb^vWgmI+_s#Gn*npJ|Jk5`Qjlvj2e7HJNeaH@8lnJse<^bc#goP9LwDBM>z6veq|~u2t*fmKY1Fpbp?Cd*N}-EUOG9~w z${x^B>O}q!6fN8Poh8FvO9nrY-#V{@7Tn2-`#%kx%B1;eci=b>?f%fK}!_ z{39X;!D)yYw#Kr+%8+6`*1J8l@QTu;v4WVQ7I5koi}_PVvD;3*-_nu36n3(F_E>%vC2k%Dk0;V5P!(ZV1*Uca_CjjE*2b~|pz?{g%_$~=2-=pN+-uyp z)0WprEIix~pFS;x#1SMK2L+n@FMSEcn=3tCIH%zB@Z+W=6t@pgbgpzI5noypagr=-dJtXFMSD>YAj&){E?%zCL9+56 z?_Ilh)BdbVwKj!pjEsBSs9J~%bg>GRfC!B@W|>uPk{XTB0%=&z_y@ zw~@9wL4Z~TCb85XDE{HU+JPS6IC~lybsMqfl`nLwBtoHP5lo^x^HV~taH++7!4c1f?(yk>5;%W;h zOFw#^Jxtk}YQ7cnIVj6!n}D z^F@nxaw|qT{KWLkO~L2}NWyPYh(6q|xh)~sPS|NQPcVoBkx`R6XlehahT zN$3_zhUZX8ayqIaHCMX+h5@pyG zF`H;x=1TYosmqSHZ{J>?#w|OCsjjv`tt*3I4JcKyX5>efCT~qn8A)JR6k0j0y=Q*<6(r4G4#YN@F=Mw_2(bQ>+pe~?n5AyN z_@*33vR0-_!8~LNwU`Z132`Z)gq00USH3eI`cD{4W?Bv(ICMT#0P7g%gSpEDWOR2n^3*9BWIB>V z1R-kx|55wlac}Krtl>3C+#SdJ--#P;K60ZUWC|sCqxGS+-j>ezg?Vv-_vmGC)Da zL~>x~PFc9RNs#x%Va73C6Pg>s!^017at~$h*KF6m^qK2B zHgMMQVJ;zS^Dv!oPDm#+8q%ID>~3D#i(OL7Ce|-yzSJ%jz7bZEjYOpCL{Xho%=Ghy z18B+LYz9@?&N%+_>UqbT-Un2EH4VHptujE?r_gMjZR)%hfNR$zmqr5l302c|wG2GJ zpl#dgJ4bNW;6%vCwXk6ZfMQGv*%!35wWrWZp2aygiQi9`mI(!upvy6!oM>jwxg$c! zJC3j;ESDojrFXEgf(wcQ+09l3Rwc;3CKZ z?+M=aZe(?@Dpmw`ZyU*C0!o1EzE%_5?$QLYeeYg{t=liimPJglU$9AQ<)0SCQ@smS z+cnRzGavCM`{On1yJ@d3k5FIUTL5MrS>-?5wzcAxg&{k4$&EL-Im%P`eMzb>-neB^ zE%xl$^Xy}L9nF98A;(3*hS4o7D(ZFQLIt@K+FH`dzkhUP(m_MXRI_59q$nCupjwk9 zR~yXwOR}yE&$F_P*tq2q&WDCJ($Xo9AK$Q4wLQC`+j!mKgH zPl-^(tIX~0cMk8~5#M9)c#}~<>bHi;r{4*al{`%Y!18_K=JT>Q&wwrs5%N(~= z&Ux;+N&hvV)XROWsv1#sH2BP*|GriEOmCyI%WFkb)4%?NUsn-6i2wLmN{y@bYyf--q|FH%{{We>g<`FHcL{iroL2S{~LRNnunwA0OXzzFfF| z3G4{BlhxiII~|yRBj48e_>m8Pm-z2D@HVE^LR40fk)KU71&POK$4lG$1dUAoZsfJk z`%lLw7fb^G`&q*0gd?QgKiUtX#iHtb|F_Lg>S{*)Xq++iaL4Zd_d3MBH2jaFWg_Ce z8+HtJb#*%oD10hcb1&*o0h495B)-E}zv$V&zbvrO%bp{K4?S!1nNQH2H>ix%D#EV8 zU8TXg31_cFM@NU8X6yK^_}KB|V%(7MR)=4^HU;`mk86a!Q+gLZkrsL)d`8cK1Lc`K zzbI~{su*E#)p_pTzwVt-2ozC<@ z4jdR12l2d4=UQ_f_EQeE#23BFX!G}-apMLL-gD*M%NH+%M_y-o2MP5P^TYQ6^1gZW z{(~`AG$(QTumObKca}4thfb7$K@n=t<8a zHd66<)R}DQ`y#D7+kq4yyh_+Vc3?DwKe7B~eRpje3i`-pJ7g4u<}Z1Jk^^(@6je-J zsQLlKLVT9tPAr6qZBXpc`imWIcdc)#ccEYki;vgfC#8D65)9x(M)KlV76TqAb<*%g zTH4Z3fg9j0wHH>`09=YmEq}JKxU#vXbcKnE2-!(+8*kR$jrym&VC&o^OL_>qZQHhi zYUAT%Bn*QXY!mr^7-V%dJX{uViu}K9V3H9Gz)0L95YaWL+#s7>eP&JH6g9f?CtXRxPb>%` zM%Snas-OwWLnn~=1VZE4t5<^vxYBLg#=PBxo5JZ1RBXflUUmN5IRTuZUPLDY@Gg}^ZndtsF~O^`Bb>~AujegZp#6YnkWIuibHj-XMX%(01i>y7F7!QMP!wC7PceAFHUz`_$#LEqpp!Nd1f-PLue)#FyqU7imj)s9r#2Z| zX}`qQiNi+*p*<7Z1OpvqSQ5H*-y&;7)*!TILh**3J10faE7BKJoN}8)$+IL;171Lg zE|BhU7nbhy#iqP?p@CE;>PGHAL_cU3wtw2nl>-qvWYAL>f>}m$U# zzgHD^0K>B;6RVJUOS*n@@6$qDekqfo~YIpxE% z8@=b5>kJ%dEHuB5`Q`Un%zP-!tH1p(0<$!-6jn|5mOe4QmJ zWo}9u8uG@nhlguPGrom_#_!0H`8gdXbcb|rK%&)i%u<5n-hcgQ+!YA7gDy`mDM(Ua zZ^!{+<6~_7t3n9!FJ>=?M6j3B-%}UY)Ytn1l`sbgv--iW{U)2Qg6b0_Cy@gxu?<@7 zw(B}VzC&x$$7v)<;S4LQtIM*+LM9-Ms2e)9XW>Pk#f#y3dP>_loHJi-jpGl1(0X%MzpWJtj;B3z_Xte=gFQoz&B zS+IM2Zn9{W5G*AeWGZDpGvY9^ z(ziHy@kz*rl9gMUWADLXg|_Vq^4)gqSZC1Zz2*!Kl%RiTNf515PI)JH;fa|D@c~%i zfvqraQmKy{IAjPHAN2(Vwz<~NkNY9J&_XN}t(a!SEDoe{_V2>$$$x_WB?#QqSU)cK z)~(^TyT4-3v0t|Ae&-+X)%XLQ#;P+nWQ}_d_ys&~$=d0m$B(~7z15i>L3j`qSJD`+ zhN0tG2*y0N#%tHNZQCZ2`e=&BbL$B#`h@&$qt;5}I_5Iox9x=2&@Ujsn0m`|(KPST zG$u_kF?qv1AP~Z09)&PbQM#Rvjuw4Gn1Y5n<)2nSV4$?of2nLNriU%OLvckb%TJl7 zTmCeUA^_S!61jQf@1QCT2^wOySQ%%@GF@$4N@?uZ&|qIw2nT9lol))gkP4b9{X<53 zTuqUZoXsvbYkA9;SX}AcbAO2XKg#1desTMWgu-2iOh9Wy@!Q|$l4gq@I075|K@iyJ z4Rk^}HEHy+95rbf87|K4&SAjvW*C_umxW4$^>NLsJKF#Ty~mB~0S>2Kx0Tqjl_z!ZVzTnzDAPzkDX(57 zJPuA6@;Je>PMiwj@PxT@Z>G9Y(&`x+It8Z4X$Y&7M8a2iL%R)ap%xWCS!q1} z8EgP~$w)3|t13H1q!qMq%OCGO>iSY}cPT&u;npxRd*{(^_wL=3M65*L6&iDPHQF_9qAj>30%$wf4_hz0*PHr8nU2c;fkHw$BjRCwaGS*J!H z?UMfjAE=+de;68w=FwwdGs6yjh_lRDxvRGD0ca*L&ZT&}(sslOChw(|mfG4?9LuQ? z<2ULhpB;W))V2SoA?Vzo8jhtozFV*Nulw!S9Nz=hOI(rQR`X_}Q#ZF+vlN6$_j}69 zUAUpeO_-pQDdfK$WhhczyPcjI*W=*EEky+dCdod(McXWuaT*3aEU;Pm`I)d1k8uSg z1!+ixX;9?%16q?pmXX%cRWp$YME4kcLedwgjkC=5qPX5RLY^!sB!3G1-7xFIjEx$% z?057=y7XXQ8@R&h5eonXhGKF3ekR=zGO4MlAINoReQtG?vy41zFFal?-90cKnC1qr zg)OCKucgngA3CQYDbeH41L5e}&y5G4Y_`==DlhBf3?;3g7bEVAwE5Jq>G|_FAiJ zLci}ky_vgLdwbNG$WFAdP%Bk%Sy%o}z($c-h(hf($0aDh8s`kVo2$xA@&Bd>3Uos9KD~2ouy~TZjjmSNI>|yGro^)S`=CIkb zGeFgrWeA0DaBUG_C(-3BAqe{-cZkAIG#!%Kl{(cmHlbBdJq?c0lJEHF5Wh)u>ZzDX)BN)13O?lnU+5jr{^>0Qoa4Tv^0 zN7m5RD|mfcywT(JNqU+0%ts^<=*!43QiDD`Jj!w6h@QiUz=NRmZ4!;KY_SHXWvf4r zEEwAZ<>*7fl}=0}X)OBH&_LJzUkwJ`_UWv6BoWXVrFbT*DylVB>@4LrX+=3ZiH<|; zUHGf)Q@6LlOdOb^$1$JTS3dm9kGTIa5lvVZ8{7VMcNrARj^L<`XR7 zeprI~^l2DU0aGES8VhGS8%!@p5`B6oK4DP}fHf+2pTBxloO#l)TK@TZiO)UB3oWtpG9cJa?oc6&58EEQOgzS8uP* z;#)n*?jCiGH{2C9hROq{|Ju(i8iI`W;FWIZs6dsMf$1&Me`7Fp|=M4uZ~;)sUuVY zRDjEqK@&~P8&#cp6_aOTo^-tb?z%vQ-oJ*IuEmU#^!rI+p+7IgWRx=E!mgELqYNs4 zLW+P6TH~}K!a&iB%Py1LSRzq5laIwz6Q`LPa{x^tCvM?sE~kYC(|;!4Iz%s_Yqij^ z>+;8oE`97LVG$7+{^Yxj-K>HwI2G=0R12&xxj+=QH)KI(rt zf6C7h`WU}57wvv~df6MydLcudJFeS9QE-rm!mq2lcq%wJfNG6gd}LVVuFHSwcZ_H? z*9J@+qGvCKA4R!KmjQ0NegGGH+8o7{`S=whF7lv9f;IpE8SA6o?~PGi|E0AAqh7+M zZ&H3rq+9#@L8^46q>?`yqyUU8Cg2d(>*m;S_> zo>1VQLxhD7cL_&}yHQbU>TBjPc4XN6X$43A>`ye@AK|>ErQ!Yht53t{Uoh)6_s|)w zr>fKX`(Hb8VAT4_1|^4rT!PD{-@Qs9;O!_h@sU2sn9hBpC26&0k40$EGuaS60 zB6d(#!jLYCxO);jO#IyvxwEjYxrrXJBf5hJwZ8gtTm(=lT77?BIqTiB)yhb*fL%SkiKH_o!x;1{UG`5r7JhO3c@r zwJEbb8mV0E;Zz47o+FvLe0z1{ZHSM&iq+2I%t3|0ckH>VUixzaxr|$;w}AzPTV&XI zo4IqP=3Lx;Jj(WMwbk0c+;0h&iN zJi7cEZVyc$<|+uc3xzCPfF5M5b_q2;UZrjrzPD^ZLpZ060#n)R@n9^^+Z|A zvWoBtS%7n3c}&5@aU=vgEEQrBNT*9UUGbz0e?Ge>zuZ`7@6}7r@U817QjUfETr8%b zynzl`g#E(7{oZACrd*52z1zn73Njl1&(#;}3BW{?15lx@Icu6zm=u8g#*l3 z^^Vd&kKrG;*Sr>ZSohd&b}R<_Fy07KW$mdcr$|W%Yg&&UN6duN+ho*3w4`z?2d)JP zWR-nrJ!+3(OgjwwxucEFp0gMA5bmeFcNN;hS`MDfHd29UTyC_xsA3$OURZuHQw+Xr zGmOz`E)cVZhSR_iQs+0Zr6yB_fBdtwMVZYf!aAW*-|3!5qBgjbY4^&5_tG{%S6H^z>qMO(eDm1jVk$$E&p489 z?Uyc<;->`#dAIG+Q)?G6J*zDxn?R~A3bhbPHIzZ4$E;EzfWUZuqkfSOahlh^r7xgY zt8d@?8bkS-8USqdw!*eTQ}uG4Fp+F9neQda8>sCN3_6>-$Fa<%zEcDB~2QC-*z z{!Gb4OOP>^g*wS-+LnK6j0lZ*1N2=U`3CK~iO2xb{w8W}hm**-2(e;cJgDZ3a*1Jp zFvpA^uL;K`=tLe!O8GZ42tp_TQAfdjijWCz_=Z;R^v^}QVs>)JFkzXBV=^IBzFoU6 z6=EmCw67?>)*Zmw=IptrlqJ(pM(lZDl7UlN=bs#fSt%#`2;>a^*sWX{#FIEVb_;V@ z0>Ak!e(4RhXN;>!G_`^ioV3OTrrYP%@>&rrfC?f|0%ruhLb40pg1^ZRcNv7PaNTmu z7+2!8V@%YM>JI!oDp(333T;3fLJl*zy^WJcBcDATh8y%F3MQC)SuHX32gh=DZU~EM z;Gz{Vy5hsr%N+yr2#0yl#0_Bl5IO&A0$d!4xkVtsD=Xwi^67!txZ@rH&G|j zC?47+rrKXjm4PjWdIc(o`yy!3ws&wyk1eG;Y)|(w!S!L-g~x9G@mX%;#*HX>+zE_6(M0wYq^B@dx}kD7$*hBM6TU7A13uynE;GSB9OUlv zgNH56bg$y#W6uvOg_|ih8g^j=2hhkO)1gQ1CgsXwiTLdybo^1nC?x#{o};Y$%lKN~ z@r44n&pMzLId)mZW+$9!hVdJ|$de33YcNIZikG)^>ORxcAW{-6-#}K@a@iAn{jss@ z6PM9Z#N1E?$cFwnbiw9O4{s_fwZH%T@$+XO49=sbeF!9+KheP$(zlgY1@636&xFLu z$S+?xCW}T`G_EeZsw#W~NH+}8R~NmcL>OU#hKd!PQpVRiWw7Oym}1L0aS)@HcsNuW z;<&LdS}h;1@?d`!B#|E7OTo44zPuPFKpGn=D=#kty8MRILk)qHof~F1F{($nlffcj zfKHU|F7w62iyfHVDp4T1K_a=%UH8s)vW>Ql$uRw=a5Ff9QYiB=4k}-5SUa+tm?>mo zVbLqQQ+SAacwVx+xJOpFI2y2$Ob#A$am~*8jeB?P+6K;dm<`dR14(&Pr4)fx1bkMe z0rYQKJm>x>`<$Zuad4$kDd(<#BM>V`i9?Oa zOa^_1R2KQ$m)WWAKR#PpT#kUU&;7gr>h{m?pSDG-U<@@x=A5`gM&Dj@ypPGMRgo!` z&w*xw@)7PdV_ED++db_3Y4a1oSd+=7l3nVAsf5LY$dzYFGFxN!Lm3nH3pua|w>RP*gy zD!ZNkw*%fd(K<}IB;W_kmLNvKeiP9~QQP)R8immbzmq81i2Pzm5y895kbGj}ec}GK zOnIPqYG1ce+o?t0h#_H+Nw}LH(}`JhI#Q<#d6)@*!pRJC1cf0C-82D9=C^VJAki(c`)3bJZA=m6@tV*hoi@&=Sr$mSP0<=R5hSXF z87SKB&UvMs@6cMuUM*acmACN~bsf~@%j%47flu^~$9wx!$NK$>q4r}WtSTCl@u_JG zgpNlMBc_!$Gmw1NqO<_6o(e#ijk$I2T(f$t->peuUNfL-WhYIV1W6&kU}X;NV${5N zhhe=86`Ke4j(m--Za8DFQ;AyP}YOiPd#ejmB%~Uv{KAUG5M5I%TEWBoIp9eM( zooWRx{SGu8DI=WA1_pSk^yxSAp}(U@L!u|Z0; z(iNYZ>Zggx7e#d!Ts&Xi-?)+SP`I^}aitaJRj$2jT@)O8E(JM|!4AEST6QlfDJkuv zIZkNH<(qyi?%l6n@w*}iFT+nSgt;1i=0xmZm9p4H6~~Oj%zWmHIEdRf_^!K+9MlMDCpN zbm*f*hXE=Ne(vLrXTn=?pPP5Bby?i5aTSj2uPU2C@}WA3?H`)I4?fUu@_MtsAAtAg zD+OQ=5_d_PTNqgDcBVV1*U{%savpW_SR>~-*QD(Np8?`!VdA&$MCCJUYoRmCX~vLA z>k97neYJdg`jvkMjri9K{`I<~dEvD)c=>;RO0&n+=DGj*FBSQUz{}RR4LnHK1&FPAGk`ZXzRX!{o% z`X6bU^isjU7jwT4m^?iFpHIiHt4*JD)b@XUl;)cM^*#ASF3w~bSAq?d>I3hR#LLWu{MH5-AU3A01{v&_-f|) zJx2f1&N-aV%Y@LxQL>{g1h}>O@cZW{yN&Zw$HpX@fkZPAH;rAqyZfH_><_l?JR8!} zFVnw#-Jj(%yN*6?Lifh!%T_geo}XGVZl>qX^pLmr3W-Q9pVvoJf>r|%C!*v*h z?)e51PBO4tt6>)UaA&lbB&Ly^X459#RjFg0W?i*e` zi)OAlRqtEMFcNB*)$@HMKpH6wPht#m%7qQMc!hR|4F*t9`XS+}LwQC^{WhiwY)6wE zZn@;)UTpv3R>+g3@g9seWJKZ!hbzyTm*<{#Sh|$SGOf4O04W?W%|0|cmmv}ue6W#E z^PRi9jT>h_+u||gQuh!U{e9|9F7<)iW2NTaoO?FGq>D>>QIX~BtQ|_>*Ud7^3tpT_ z+}R*2ogqE^(lg_`^`F#!SikPQ_U^-MN-kFJ+vbgFbb^T6-Z2CWGtcgSB={l5S|v?Q z+v+Nf7nS{wT+#;Xn0vJneBG==`L4;o1L(Dt>jrFeKGeLXG-)qK?G1=YxT7c@7nw4l zwNneV=9KpfnIeQeYbgk)@LFGh4=m385x)XzmzSJ4#26SmCbiIS5eT3hI&7FeC;`VD z1j)R2jjC`gz-Eca>f1K?EaIdf>zSP1>aBdGHiCkR*;VHf60BF8gvMQmli!uXh>0_T zAz}<-3ib}sV{qX)N|PAaQ<6AnXfjK0%w#G*CCYAe^0U#=W-DHZW*!iXR2i6c*4G7* z14Mm*_wvNM^O!eW5W`5JZOD$IAxvnk)49B{Exz;EHLo?;b!ns$(Xw>!JuNeg4H1{; z;R$2r1fv#v4^0*d4F|4Qm$LKo^TW0(+Al5LHFEe*^`d=?+s$Y_BRuPhUFF$Oy(0Z< zF(+@;t_~|*q8W9;Iq-%qS8nvee@p9_A3XZWQ>WTI5io+!DvB0~SRN`mT!e-^J0aqN zXqSXH)8IA%)gAt~#cB0V$^?XPacdK5w-|VdX=-%oVQ__L)P%+sQw*>UM0jspSV8D# z!js?8qYKVE7F|??=r=~>PA&g-7IMtiwsiE-Z&3csM=H!aM16{>;81;H#LNj3BrGw` zxL!m5@Z9^&Dp)I4j&5q0Dc}eCQohQW!zYrUxC;LjcE^wTCTy>rI zt+8&on14yVF6>vEHr<(U9JpiT-E_ox486|xF-RakOV~xiNV>u_Y2}HCoVpqw5d$JL z#5Y;EFcT)xvitKFj!MRK;A)i3dNcS$^Smd=%%hkg6EKlMXK3`n!H%lt+{t#jgPHR6A6M%wx4V z|87Ah@K7}RZ##eGWY_zdndN-IxE@F&e%zLPzOL@aj|@OjD+xQ!7;m=!r7cVmp}y~W z%68phrh)=|W}X_YD(n@Q%tchhyV{8^6iOChI-}#;{9xr6dKN@iG<+^R#1vS&Dp4`; zyV|l?Oy~+4S{=}HZOnU+oG3|gschW)5fZ}A!C?p1F^-HF4K6Ie%o^M8>nq`WECu$9 zCLmF@5VK;gwKRO|O`Cr7+KTB+iIO??lU<{bbZeBpfq^kk{&3cYjT=R~D|`nwv}pJ8 zDNqS&V3O@b0*NS&M>5{{$Jf_lkTD->DKm1o#$HMy{KlN8dt&Yw?K2z=QNiLA7G5#m zpxHlA+Js8MCbxqDm^D{(dYb`f=IRysu3p)@_kbP04Sy8R*k``5w~6lRYlN*CJ#0X<5D006qPJNn*Aok{$$3Y75`S(nC zAD3iMH8&8-F`c|ZeDtF`Qy8AeHBgfUD|{9)-U*u`MZdW10JA5SJ(WSTIPPpvG0N}c zn2oc~>axM5%^n|gZci1kXYN`?t%wwk3lb}uw1l~%z*||qHw`*zRJdYz3J;Z1ybS;B z-UrG0cr&CSaJQk2GaO&SY>_aRI#kd{j3|MbgDq@BU%e2jgzQe*gy7d2G-!rh%|C0! zspJIvk|jj>Cq}Mtt{61co`(sPE|@mO%gYD=AIR90bi@FNI&mc}z6)9+ima-qO*)-T zyyDovUI)rWG#iZ*A18C)ebRa~oou+Nmcks}VHyi`(BSz_tE&$*HTxW$hVg&_DGVIn zM^!J^zYM|&NhXu9|K`Jo{*Q);5sa8XG{(mK*xK@Qt8mG!TLWE50jU2o#Uv+DI6lg`^w$&F3r(dkR#=wsAl ze;;ja=Fx^SjY)Kmp?XBdBKT>;a?)}bq-31Mo6AY&((4qPz|1mf$-0`l9^CsQ=4Ktv zd;UBWZCwdGDO%KM51qQN$JptVE}4Foix(e8T@Cg^rau=EF#}d)8i6SbD?;cUjP*-h zTxO8qLZ4~uEK478Hf>2UFn ztjn1PUDO88`i~c2#*tmGj7Ob|pLsM~cV6GFX(Q;qGq<@kU9rT7vAh>9MEZqzh8~=) zduk^yOFNO@d&=o4eg3RA>is_La69RB6Az8kRyLN+evs4amd`B{&s_#(58o>%7H)p$ z;WYV=?q!>uu{(x-{91BqYTmIvD{~67nriAVL?-Mxcx>Sj)8JFqQ$~;8*e7Uq(7oK8 zJ$(}9*d6_C{c6;?+gAH)D(j|>xV^^lQRPHGmweTazn$f2EE4n7+LYKX5FL6SOOwa1 zyLRy+l%H+hNyfq}v$}69D(=UB#d{cet1;@unnEve*O=r0%#%iy<)~!BkoIQ;Bfs>r z1B3%cn=vK5C#;h;>dnmKh&Rg6qi?IKW(dQgflqV8lin1^NN@q>3$8uZtVofuTqYi9^CZESO}`wFm)IFfAZ@s2D7=w!ma{{yWarG(;op zVhWu!m6RsnZZ(U_#n-6J=9{}{(Nx(h9|Aq9OHMJKWOjINe8QwpO}`c_!*eW}Nt+=j z#InIH#k_TWb!VbP%mdoCyqPp2exEo_g6pb-Deq0KX)tjUp#$*vR}D5pgTMpdi+&i^szubh`9iM@pXgilCE)`Ckrk?Ba8P!TGAJ z@!Tmi9ocmX|I2vbKv%XSC(9A4vCr7CiI3~wwOEMfTPt|~PPEdRAoA_xnKXX?*okS| z4o~Q4nc-M(b7Se`sAQK}KNl`as&}5#ytb&#!Rw8~;KQks@W$g8e7M;rhk5aYf7np{ zd9{>X_sK2NQ!*jWf(XIq!FV3Q<8kAQ>?I~+k;wfo(eWCOtq%?R!gc9a3#z zIxV!uL*v?VYxnXBg@*hZ)7W)Gt#hp(CDpe~neExpXw#z2ZxWt*KI;&d`RP$%%|Bl= z@6D+{6I64@?eodlPFjjuSp#xYLu?$(tgUYNyt7Ihw&?3EzlmE~pWHA~VMMzTDo0g@ z73y^C@^70ka}MP>VPfF1m`p}-Hpm9xCz>doiaJXWa@{H(sAlMAVWFgg?xokM6SEqi zTJ7Ft9h4;5yi^UIgX&x5I56|fG+{bVLz$W=Yd>nd)&eJ2Ebekloend!Ryb(6xI2z2 zp6|Wm?)o(23+Md&`M>LKGjR--H4NiS$S{{+vq9ltYB5q4%2r6@o&MBUEo}alsVOXu zoOX?|b&F}VM(9qxckjYv&vN>``cqxtX_g`l(Eqy>n~app58=foI$ipLF{72lVeNET zF~zCPi@njq7rv}>BYD9IFO3v}eH({lUbI0~522NdDk{Cqj&==@x zZ{D#ETGFDOf`hrJ3?Y3aqs|eqUa#m-B2t7evC;57R7)nnOq4gm5{J4VrVf-OZ_Pg* zYC}5!|2E{10rm88kFOs;?gAIaq;bk8A6n?(+|e~3_?W@8MBzsxP)9G(O=RStEJln= z`huw<%Vco=Lx(LPPaz`c!XaB;Uwb7ye1ffQa?yG~3RlOlJVMyTj3Ue+5+$xn3vu9= z*waygxhe%?UMmT|Ma=#oqXZCQ%Q=v+N#f!yu(23!q&)IVPNB+ICeO)G!+ zPuti@;(PO?!IaPH+d?j=w|}fQbk^2mzZF-?uZ;e@OwL{3z5OGNl%RzNEPTIECRNJ5 zR^GX}hyR;jsSO^_t~b1|AKy?_W_zb<_hUTwmzv8ReRLXv`u00Ev|*fAJ1TxrH&8){8DJ$_^ zWayimM15W=M&P1h4B6fEr`a>Ex;Y>V9Ki+)q@oi3JvQPwid0JE)>?l0eWM=mcfx_u z(72%eS7Efz%xnw6w=7i6hhB2L3ENJc>c08s*S!#)4z#Na8@aG^F|?2rGwg2dFBs5- ztu09}N^jj24uhB;xA+U;z){n&5|BSQbP2pawx`rh5u~g%H~@Vtsbazotf|29v>s(0 z8WQ<%!IWoWN^aiD5fl7Pt8{FhY44-WZTA@;cgo@W^yOt)de#rZ(|M&r(K?k z6Y};>cI}n+$8!85BUeS`T{H=6wy;^#t34W)#S)LZm#&F%Xnb9wXs4Zq!-r2XGdu9e zb>pP)PcFOC%X&h?ikrIzJ>*a<`gm2*V1%D2(du0(*78<4++WTt7$|h1nL-X1en{v# z{jjqp$J`gU95d6cs!uE)HFm4c3{I!vXj(@*@QYgo_M6petI74+@DupN4W$}sFQoEM zVIlx@uCQ(sU=6L>N?>Cdw=T8b!$Vm6igLv@a#X{q)?$(vmv3$!wZAMU8~X;jd6!X) z3Nfb6z=HYL-W{UXO5FHEGo-Zjvv$H^3iG2kbf{z0i8u2+6y!_0m@H$f3y(Uin&9B< z98W%}eZ1gl8?&%?@($0lmQCGsMq<;!X4U>@`yvwthkYEFQ=SvwQaO%px~H;p73UsU zd%*a5oa(8pcXP5OzWrR!*lpUFI&7NGDN4fandSB%PeGbNv5THh;jtZ@9}6!5vo{f4 zEF>Dp?e(X*3z$UMe<7`iVA}tuyYr6cdjJ3aN4uz`r6JMYOKBS^E$yf@q(LExR3aQy z(k7y!&_vSEP&A~IC@K_+29=I7T2i|1&(8Pz8{g~tUE_BBbzSG5@AsU<$NT*n&*x)3 z%RUGKpT4lX%G6Gm7r-7A`kLV$@0}I_a0;Bk{6kI3-oo|ft_cT zrmb;X1`LRDSdx|4s3>bh%YrtvIgn7TPWi61k@e{~Q?z!X&5%2UKl9($mlKZ~TFxxy zRr&FzkAU0(Mx?dX1N(CQ@hvXptZu8Ci>9;AH4T5swV9EXbz7pgIr7BLA;n$I&tH{O zsR{m|UON~1i=K}|SmF;6Da(i^60Nifdrz5j;ML4WgZe~2YoHuEDM7+PB{6~%eYp9V z!LNgzxYdyiaCeoV6O@op{%L&5%r2+wR!K$mY>>A4{Iq4Ex3MLRnLK%W(f%z3t2`PV zcL1mX#*}$Ka`J%pj@wi9JZ}nEb|$jju6xptL-0w0Ien>^ii@pF=2l+m9v?n!lZ;=s zn@_PO!Fohu$2psvd>68rmDaI;;Vm=Qu_vt)u3R~r-IJkC4Wna9@i#HBZqAhc=renN z8Z}3gA_rmdrQs%hD;}Ayz4A3?#lRvnI$QKt`h3QW`(|r_EDKr2Jh){yNmWf$q_XHcw3nmC36Xcix|055sN>pz#YyjQ)7^P1AL z&g9`qv-29`0YOD12}WHemFOz#LG_@?dPxBwFrb%LZ9cPv`g@)SzirJG?@cW;sP;yH z8azwtQS(3VnHw}*G?R-T)3fb4bf_`jT`u_X)?0J|7$Ln{m$g84FbwhYAyJ3#G9V+%)q&whor$Yqsj z1S}_8^+V`{R^?m(Xv!iI1<>2pqRP-mUVt>i6RfuX*NMM3N2SyhD|Hq7gYT!>9q(Iu zldcBNCYemYlA^b9i4T@8L%PY~f+*4xp}sg} zROZa}3&;@r{ez|5+g^)9X*-~yR>zJWzF$9owmbjMA<8sHwS9XPUcaJcU~Nd9p__gT zWl*&TkE9Tg{Rv}_mYA>?Jy;LAr_Y`^lQD{h{m_>q3n@j35)-S-kl_U>8IjJ%X4_y%T}D4?gZF@7DdLZNXL3opkwG8%9D%}l7+NrA-0+*AkzkGptL1~cKjTkouO6s|0R)@~&!=1WIv4oG| z1yOnciY<4~Moj`=Cox;JDr(&o_+!#y~E)5R^41&D^>dw4Qi& z;+!FC+JwfqZ_Yj&6W7mWQE6+X{%@zfxcqH)aW5zPoI`87r_ORRtY_U=-^kVIg3V6z zo`7#8KS%c#@dkeU{aWI?fZOM2ALQQ^bEQ~c359Kr6=eiq@_^zkViF;SgEx9JwbJ~; zvu;yEWlkjbw?)8(@7g>D9*C$A@7?p8ZW>j=4fYB}bF}BE8kfN}-*0_Sjh?Cj9+-LA z(F$}|CYH^Kw++s4;*t|lJ(MD!=Fj$cpVA8ZlqF5MOh@IhtxX5eOzG56?DUAGFu9_MR!yX(e&6itR{y~K|ZhFh?VvpRgEm~tX3lG{mY6s~2VHVzG^O$>sn3Gfg zAHlwX`dM49jP}1-SfH2K@-|M&)m0VCmhZZcdY4maR#ZSvk;aUmg4lteh$8fkdPYa1 z%BB-c{M_WuBSsupp{qaEcsP-{N)=5T$aZ~A8 zO%g{QIeGGLA+EZ6a}$FF@gxWi2{n(mUHttz8h*5+qAIH!SYOeyIN@gCxyP+BYvB+Z zAn6WbO2Wa}&L%rr0z!JBiNBD2s7Z+->UjswD`AQVWFmSeg)j4i$_+>D37LKqxJQCY zC?kE-9t{dhb!&yp{@}bvuDwE*rrR$Q?>X1D^0R_c9@iyg#gqdQ0}o*&EhdnO>gtX{ zSjf0J1W=sRF<*NNQS1O)A#v9H1&ky&f&AOf);%14u84UsV)DU1PxJ1r3drOAbn`n* z=@M@OZzLrjG;Flj7fkfrLwJ@45SxY>;1$G~`Y0EtYc0e`zJ!xSOkXggN0_Bdi6!_t zRFV-|U<^@W1@}_@gA)>dbH>}Zx$IHgW1HzC#2nYq3~qyI9^4<21jwT_;2fYF62G5# z-7cMR$7%#w0t0#Pugbc!T-U$kCi12V4k-iocKVot}2%Cmk3;o|Kl&H~X zFM`(aXc!M|h`KT(IEU>lvta<(uT%Pn;5|F~3+39)IkAXY8(ck3DbVC$}xP6v4UQ;yX2U=KYW*@n@Bn33f zxzPN=N*`ixEx;E@ph4B1s9FfK^ds{YrBXc+p#brFY@7Q9C8g)u<%mbcp(D~}ig->I3}lVSZ{Hq#=**D% zk837RWeg6jfdr(2cW&e2zd^7FNe>q7m9n}&ecW;#d5I3nMEe0nsMn`AviF4NQ-P)9Gf`wP%gjF}RxB4cjkW9xbAxm6L(P18MV3A0mVk`QI4&M(LL2S&X8zH04xDh1CnU=z=noucNwq|RmCl^>X# znYoAhx54}ecT!Wg;RdSs^lql-rQG=86B&Dg_ssjwt7fQ}UcP-hzCpA6k`pBx;Kh~n zB{c-Sc6_rAyY3te3JUV)7gytY5&BGc2l2_5w!)_?W3HREZrzO%r4z0E61x`;64QtW zRT2%^8@{l`C`PrsXW6z9;H zeX{JD7a|=+_zLd4+XS0pr z=jZrt&GYkjzL&RW!AzM$+_)8&`4gUXuDCA}W| zCdaB7x%mvQ`usLF+NQ(R4f>Pg52yy$9cYwm5Mq7BdapymoviA_vMN(^4P1oV@c64D zCVqHocCQK|W(IydfpuKwJhGtco}Of4(uFjV#kptcCS^Qjb9BVE4>4JLzE7N;N6X(b zWI50qV_g9L3>E$JRAwJ#@keqGEoH}ahP30&#J%h`dA~jf^qh}(Q)fLZIiWSsCh8U! zzOYG|4(;N|T=AMa;Wg!69mVBMKV2?VFg^$jH7l`S z!;??;F6Y%oA{{l738xfrzsH&6-6}izu3c5pz1C+r$co#+AdjV$+lmK3Ki59X84`2? zG_Wt|BSRmpM*ZTd@5*L+&23UEhcaWU6TA=w%Wig;8V2VIFbu=->2{~%-*~DT(&HK+ zp%F?{78jTB2%Eekxl5Ti5uMvd|686Z{rVmZ(yEIef0=ChEU-I(hIlBrI)BGhVj?_C z61wTeZr-~=2bxoF%kq_O^FiF1Qva13x2Vpj1YqU~He)<1{Ev4Et;KHc*}a^kdy%{V z!!-x9=%l4}Uc(ZAg1`&j&}lg*^SRqvBPDTJC+WAkM44XX;wo z>6}k@o3!csJ8{$B%E`HiUay(f&0G^^U-2q^jtSo$yqpH;6)r5pKDDS((>_7X&D9`s zNXEN4K1VdfFL!L=h&7Lot3Y+$M2&E2L6fKOpH#=$m)0&E^Zp5ne zRbM8H+SC9>3YFM`k5kZh4Nctia~oMNy3_BreLx=*JSwF}i|KpuU?DKkv9<}^9Dmtf zAWH7nlr95HMYf3p(5g!j3}mw4qYYxD#Y>lZbNdZ3xN9Ww_1-RPk4|fQ4;dLKRLQl` zu>DvibaZq|JKuXkJ%k{AI8tH~*q{ z&%|FENbu1&+vt3Z*+GAronZRkD{w#c`8Tnz>llh|_Q9j>y)a;iu53@L?Ym{xJBR-= ztFZlWHL5so)_)KSS8cYosTb;H`%f)^O^(f^d&PVI)}Om*J7E>bawBHb2*FbaZhBO8du}xvP+F8bkyXa8dZ3 zYTu&KSZ|i~!!L7df2x_*y|GukMx&!O8O~#lY~7jCr1SK1R{v9>^KY5>KQ+XuO{Yd= z|GNe|;JeH<2a7vGTH%d>JV>4mQCfWl{+AN*e*kyrmsgzG&f|sz?%!x{_kReb%?(3a zWE%Xt6g$A4p=@&__I1N+ko{N0fBv$6RcQbFFLJalarC=LmpF`-hN!XnLHFzet_sya zt>&$&@ZgrlnN4(^_U?E}He@)_KWDKud?|r+PpRyyLbZnGOr{qq^eIV@OZOXCHJfl} zBldzgS8x@OFVc{D@h$h2e^EmPJ(}q2oSbwx4+~$u{DY*A!7D-oLXM>NL%6+40BvAr zO5UgWW19`*FcUNam*i$ZsjT<&z;1BNEk@H|GHbZJl388APWdH7;x>RWXnXF`Ip1dD zuav%umiKqHjZ)`UXh!g5nv^Nz_WAU*Bn%2U-*V)&K7;2~9pVf|ai*leL%p6^X}xM7 zx3=%Kr~V_UcSuh?1p$bju<(E#9WBOPC7?=VZ5#}{gM#dx7T)CP-Qg=03fYtVZdk5& za>?jS*c6ak^{0;7 zQKQ6rx#!Q7EBIo;!(uPKJV5XRx;9{~Sl@}`v~thATK$-X(A3c@w79JdM(gwoaS4KSUx+F}27}ud|L0 zEPN~NpPNBqTk7Bb8XWtr0wvUSQKTNe`(e=0y+C)#6xAr*9(O2$-#S`#K)3nY?pUfqoEd%~sw@3ViHSAq-c5+1 zh(zWbyUzG>U$LZ$4T7GPg(!M(R%_ftio_d)qKIRO3#~soQ-IS!<;)=vG#>4t7Pt$^ z9eDQ6EER;OYuB#3{rx2oALWh^x)tB2h3ulAK}e2UDDtx3L)-s*0r9HbRXFd2H>IH= z2tlTmWhEvkU8l|mm-3<7Mz*$#!632}3?GdfJN5>+4|UCTu)CGL*+nA`A337L-xgy% zss1h95$X{3+8myn7K^07n3+E%PpHY3nWepbS(Ly;a|;qusgga2&Y&VQe8pQ;;fd31 zG)!*Ot{pUaKvmeS7qd2&jj(`ok|-96EO|tjTK^HEa%!eqkAPp=qS%0VWnbr6`@cRV z-b-YyyeY{WLnv$1GwcD2@}J7vVgEGc1moR%PZcmV~vV)4dtl*sIa9Xm`ecxm7i8b|v^tzDm|o$>X#+nH10)=-sIKgWNqh&dKy0gT(XnALs14gb(!HiVn879Y zU!yUIOOOq?(}mO<}a3lvD9V3#5*xWRZC5cK_sBAMt)fPyDtb~w7r)2n87 zFX%XTOVY&gzqCKZKZoFe$kk;VAE!mbagAWmr%m-?_&rC?D(%jnz`SwdBiLqfr zfTD!!)q{C(6s!cZxX}$Jwd4Kt^c`zI;NhD%bKbm(U8fuz523)l*%?(I7?}w5SWR)XX zAmb4mo{Nv~O54-B3bT&{^GmA&Db@o!vId)_Fq#1)$4te>GN1L9t{qc6U-FZ%G#h1J{znpB? zW8(Bzdp~^s`7_gA?@s(W-O~BXv#US5EQ}5P?3(la^aavv9?W~6kf`hgXxsO%qF;0E z?8qYRkqMY|;4yQ=+SbxX{uAb` z!Rhqv{-o@Fy$aFb)&f4KPs1G8|(8}&=mA2o2$}K&x9|?;jv#><`cu&^s3blb` zAuV0J*g)}?yTI}RIw})@qew)`bgMJgBIVeonC9YgCHdUc(N*3^EV|r0;9^YBb#ukU z$&+p4=W9QoRj|1Ka~&@1rAwBK!FG{feG05Rz~8@6tmWWwrEHirkWK&I*6;;yV`1n?!EI!Or zrUi_n!d!KGu9VSs9M6CIwjLdJr(wh9mh>E&ap~q#d;49KLHtk&-lLxP%3VZ-on( zTt&)*6V(Y4(*XY=+^OLaRg|#B%Po8iwZs(yUP}H;1IlN`yj6J$xm{?zwE5tGDn7@L zAAe#yAmhpXv2ME_yt(+~gOUZ+{bcEZN-$m+S^TiQq{CY*;$Hdij$1YfY`JtP~%O z3tC=iOcX(1TcF*J+5_Hn`}C{BghiXTijN*G&m7xb8qMT#DXgp#Tbqy>4D}F2escY` zW|{zs$t6dqOhtBL))aA&Cp+ZHwtG}{GIDU1nVIg~M06d3gUkF%VB4ef3^)_zQbBy7 z4+Yn{2C7p62D$7p$J->BZ8HC3t4x5!Cu4#6P~()}jrBRhAP!eOzuNG~H0EikkeU37 zbQ1J|5&QGQ+-rPcGo&xzm{LeBBC0Aj*%RjX$s##2?OX1K%Z%aRqCoEH4Qa=oJ|AVP z043k0D+9Y+>>0!)G%6L5pwKhbBHkT-1 z$D5nboCT|{X)j#xb1M1mcQoDeqg7s|XJ|bSPP<}anjotK^^PS_Tmc$mAn%2vB>$94P4&neKd4`*f1jxrPM_&r+0pkYN zBmr3PAuu_cYRXyS2~NV~(>JdD{|v z1bQBsN;To&)&1~VyLJjR)|ZCzT(=NeBV&{%pX5}#Jz39uriJzYmH(%!{V5nEf%l-jjw$#H^cE3T%D##n$t_K`Hx@RN&Ss^|j^ zVHH7~Z^6ioa4oAQ5eKID%-vsoyvo->D>SBw#Q#-C;ri{x7VJ|o$&ogwDS=`x-L9*6 z`z%5CD`6eYpxcht^t~8Jn71W;k4cls%M7SU(6&j(gDdRftxs3C|LqPnck0{4@5aZd zJd;S_;Rh^;@qMI5#2E=vSB);g0~=i?piKrMkQp@5c7>%=dyV4KVcZaSHl=BQ+~!B| z`ne0wCzZg;9`y1@%_*f-JC${vF-s^q4tdYG$+QhG5Fxb+)pOs8eU2j7fF(%nly9q{ z5K|yS%!zS-wXxm|?20z_blglX2vj$O3t*N-=PmOJ-RYSBhrkLjq`(aYbR8LT4KqTa>c#^FElhck`7 zUyQon`k?C5B10<8^}!UX4<+q7S`!YvzGBs)vpRID6g@lXGs3a%p;Iq1wHQ0-2Nf{Z zFX1HtCAJ{>)*Ue+cm62i$=WS)Ew9U-yZ)w9@?T_HGRKX{{cL;aQ^*I29aQLSQr*SL zsQ07HYyW)K8`pq^on2#0tzv^GoR||Vzk{BMiHR7`gF|ff!q>$_ANt&1{bMIudP_fa zYK=t+i+Jer8BTA|klIirMfEO<#+&3Ns^#w!*aw|j!ou7x@{kP z`~7fb)$|X3OB4bwqg$A(lr-Xh zacl2uI*E@w)Nv2YI_I*XZI`1Q7wk1L3d8C}I)C6D-C}qSZ-CQu%7N50>rvs(`GK1< zLgF=4RXu1=B>ELqFK3vgRr-*lu}7WzRs5)4leDowoX`N?)6C891^Wf>->;xI>G|bXK|ySqdz@EA_a%hntFuq zY0TVnd10cDYGVu;$BAW(WCPY*6oYb2LAAWOZ{_;bz#j&Wb~Q3YIdOi|elzFH`7_Ip zV}?>c&bf3RqfTCVH6882xxId5=;IqN>D|iNbmHg_e@4ijM1R-vR50I$89~M0@sp}@ zo!#8v+%k)y>0RN4yIkmNq`4XNN9_TxA#(VfJ)6-aL2N~KsQstKk3*6a&L5QB?H0Cr zF%q9K3_W<0v2Cl|ZP(q==1pw9l;=(;+uj61n8al93 zIJGdVY1ibmyKU?u#yqGJ$tTsgDrOfJLaU$FuVZ_^kPr;8x zBA#fw)Jewa;Rl%9$l-vmLbh;nUrL!NQ^25j-zo0DsuyuCE`cIdcz|}k0IfhR+uScN z`&AHK@D|D=n8As!UlmRGgK*>*RN#V8iF{)@YXl(Ri6O{ z?)yc>I)BLmN-Q|klbR10_9mt#^pEYnIrk5Dc7cB$a%aV9J8~h~u#hEF3x1&Uajve8PT}#eO?!u$SGbiVP??JA8PbWRB>J9J_r^`yIy%+Je>cq?_;u?M**$c1 zTZ29qnJ!+GLRFTMl4AK;5;f*0ZCViB!6U`sB8C^R-POfuZMbsgtPJIl&LnLd^&Gy# z)n*i8q*h(>uWPh1RCMM#r^}>dg+oNRR9M0kx!IN)n23^3vb^$ zkEEj$##1B@DRZ0T^dvQQt4wUhT~uQH7dOXhlvNb&ofzUxfhhY!;y)diG-K%q$}cFC zre>IFaPzBa!266RYCtm~v9njM%(L|=S-U4FNK{iYrG+PemD^)0aNk{xTg-VMEv|CJ ztP;u0Z0<{L8E~t@BQU$WjM+Vs08iZxhCk~-z3ZayMnWd#mVk>Gs&0V{P;6oo-_9as zAfpP0OZ{dqU@oZZ1BNHWeZepcb~9N#2C-%q1wpjTk!$=5LbH{c7p_C#yyU*ZPfWo$f3>;RAd&m zRtN}-h5&VJm@;6j&6=_(gm*&zP&-lzQOC-3D+Y(uqaU;6%V)fru!O7o+t;LDJIE1u zaj##J_qA)IU`ZTsT=BRk4y|6?zh)a8eA_XbRhqT?A^(pyn2X{%%kK=D2iMAUT}$P5 z+TGkB1#rzVt8f_GYzy@8#a?>Sb6`YV=gzciW`ag#HTV8F&_4CAHr6_&JJFQEJCz|5 zI%(P$qJj!y?>o~}x^3Nei^dueN}Txfi@9rc>BWJM?pb%~bJ=Uv`{~97u9Z8CzOih( z>>PeJKE8PVcJv%GS!{O;3c~Ov9!T?}@C4H$QEu=0?1$4)|BrqB@};5c=rGo0g;8z-&3`+?kmyYw{& z(Roe~!0`0xDd=_>XEE?Y6|h|7u_LEWrQW6ZpL;5ezBIDfFViJtP3RIUd;K&__0ASk46{bYv4kU!95H|&c(Eyhi(~7Z zR}=b7rB^T^it_zEj>KcAQym`&G6);2R{nfnVgg55t?L>KKJ^4h?*dn`X6;&;CweM2 zw)IBLi4(xRXDwVfh0WH9efuychp>TR@;_92zUz3#{j9mG@pu~ZnZftp0t9Tdyi}=B zwA?kp$Y9otM=jzI7nM9bs$5>>+7`(Q<7w7BK;DcnZsJl)OG9~*9FPu{#Z zUw!)IiK-%(<9kHOi5=&^p6JwJdgb#Iu!Y^T_T1%+>0)ZtMkOUd>3vvwW#7jS^lHkR zN`w%As9nXPX^g<*{gERp35y#0Z{>7@^8f*{G`1X2)Ap*e+m;*a{kiXDx|gUjneg2? zG$$wL70@BGZT_Hwg+J)LH1hKSE+_7U38(IpU+!Q&!(-f}Xu5&%7aEA8OdX;6!Y!F$$kt zr^nN5)~Yp=a|PArw&2X9RGL3oDXw3hT8Z4iZ+M$u5huS+o4F+0?A_{fR`tAYSd5o} z4}_VWUs0&66Ou$Fq2k}wq~!3y3t`wJd^Kv{w2)rSsE)h4JAoJmCD&V9=c`#F%$<2n z8eR_lr6J2_+CDv7wBv2Fa#l>IQKNqDC}}~uPWGuzQ}ew3+QG-5TJ48h(|-udZG< z*1KDHE#kkfzXeTwvFe9ov!ovte%l=%4LEad39qtxCw3dA%;syYxOew1cUSn_%IAR) z&#Kii`^JWRbm{PXTyR0%Ln=>i)Jsy4C(vJACFS=>;Y%oh6QY-bb4zYZ@Ug1XZ(i61 zR4vH3yIbS$5B>Y=uNpuA<~l_d<6EsLw|&fApPfNMp$ z@=e;TAq6s9?|^4kQ?GKDGl{n>&TC}btE)WCeD2$M`IoeR{-FNq+mlBAhtK;z{P)t_ zN_h)^f8N{_^eYb7%>k~GoZBM0dz%xh$#1zdus9)f%f;!%>kPV$aXxdS-`|g`o>zH2 z9|P=BhBTv5Buj{oPgJM6DD z;z?o9@AvTc>u}$&|LLHA`$X(AkBd?~>Q8EcbhH@)J)z4+u=iH}75>+NXH-6h`ix%zT z-h*{ktHvImmS9J+VSK{a@ zqB7!36BU}IooJgraCHvBV7VA5V%68eb&%M|OX@3N(q1I}`WgGAlB&(Y z%asz4@k>z)zAJ<^p=`#X6y)8eT<=%aR0z)|m^nPJTyaTQJ3-O_Whk3Ma>igR1uDP_ zva3nv9qZtv9ZxH{e#E!oTLw3Tz`l*WmT{>#RsrZRVdUoBZ{(HPnVy-eX7AI)eHd6? z@SKAt%=_3@LQ#P>pA>uA1IURXof^Pt+I?ANT05u=^#O<|tpZnEfl4f_m#A3%@%gCW zWBn{SU4gM9lRrZOG|60tkXtZlF$?gFh4gYJ0^IR|9k}0apbH84(WF@; zAN<1p??!y$d{eY7co6OxyOozG!gxi-;?HT;9fF%|y70HtLaqz9cp}9{hCQR2RbawX_Vh^HWnxu6>I9 za-+WH_0-f;>6)oA5)@7RFt~09p+is;P*cvET+B0-l}iE<45{aX(bX!@AulN6Yb5AP z1~tn7Nn)Uwco(O`C5^RAK~+oj(+1-(IDKf^y<6)yO8%w%yPaKW-u+bbh|{N=(1r=S z&rf8Ecu%dFtYkFdlYkYVW9;=d_y`X?w@gM)|`F($sJuOn5hWHKAIHpeETv9 zxgkX!?bl*BIFlOZ+WJIY;{-X(cnCyW*MV>7s003NzUxl&1d6DvQ$Lt})=stMpI>V?vP10M*?alfG(y|1eOySvQ;!-L!f8g>ukY_;aV%VNtvZ{0 zjslf*Uf=43lLG-WKh%!qHtMEu|B zsW>oYLN(}& zyh=2|1>ismUXuwMNr2>3m3JU~!RZCcREZLLf!d*-LdL8i3mVG-D5t>3kGqi9zG51H z!UL{CM#?oyXs}>g#Qy-}$ZFcJYu{nqt`0k56Tdg2ZGaE;4hk~!_t)UkOyTpt zz)LDP5xapu3mE>?8My{jm?*UrGKz@l;v~+g0fvYHlB;_Q*!p2?oG^z#)KFC|TkiWB z{xL8pNMd{#qj810YI>cKGR~`hKwFGgXol2(BiR1f9*bby?vnU%Na9nY# z7gNUeDSktnDS{!ogE`-@J@$9|?(`vas|?Ep;$QQzbk)7U&Xf^Kk5(u6W#Am~p? z1zc%kBX%C&*`DzI0uLyr&7YrdS#zM%$dLyRJQ@?aB>N#xa(A^xTs0mG8&Lv;r$xpsp@>2LaYbdJZHiSFU!|E!P&z7lis3qnDg~y)!-pRx+?O!Jp7qyteJ_1W$0x- z)C`)M36m!Iv3spaPdv|&&~}7#@76;IopY&p?K|f?_v>-`8Gp7|WA*+X3>~;^#o_b% z)vGaLw0To`m)|40h6ZH}%{TzqIN*}=^z|#PtvhOmgF07~murCm%z$0tF|t&`xdfXj zQq$9m9^Pe`5O*q*7_>k&LpXFt(YkEh1i@YcLP5JkpLHBGV^T~M!>r?~>5`f$PEQ5_ zuj7I1|K?q)>t>281S_@vQt>M~K0Z(c9=uPJ5=3f^l@4>IjZbeK@p=6FFe4+Np~qND zq{pRC0EX4p4hQNdVoMuriBEV8^Vw>L5Su(BBiOdch=?(K(Na4*6VAf?a|}F=+w^sf zrrqgh&6`nqBW(V`o$M!3b+>Q(@DSfJdGP(2?|TL#4bb5k`WD2R5oUIj%h~ZO$Lgmn zvqcWLmOqOKiXCWKPMmI^Y~6w_iM<{o26^C9`Ya)cV}vkV)nFja;~5MPOjyUY3qsLE<3>sZPbYVl!|P=Mc}0`D`z(xeP2ks`eZ2v3`x^Znq07k*`zx=0;)Hby`(!!zE8#>&Usp;faE z9d^oXBx9by<_0j&gW581&vU%hnfK3KrWS&6n!(%n>k0OHDHGb8j2$25Ajk4<_%Fud>ZXFA8;Ie3uJ7MA?7MN@s86AUR750 ziQlM@krFkZW8Jk-WQ(4m>{iq1GiO%TTRCw^J|WnWQVux7TCGR3A#E>gSaW~Ep2c-~ zY4#$8Apc~YzEOWo%rH;d=g^@;bENwe1|6T^_3F%ZW9`PE2~=*UW339yP}T5flJUbJ zE$#W_;8TYOK3QVMl?JAje{JK%34^N59v$Sy0H>Mf)OjYNZ0a7Qp|_ZzJooAU=lT}& z+=mZa{V&xm=^drOQagn36JrWB`lvT<_$YbkcPxBOu`i`EEVQqVNMSJZGq7Xw zfgvWRLfMpjpH7Vb%i6Hyd?@#qz)9>FjtBpq7-~}*hkImSuv1WXzI=(5PlNl8cwf~) zN6(VmHRz}9$!CjbhB)}d)yh#;|FFYu@}s7-$r^vhp9~%9hqQykn!_hBn6Dwnx@5vG zcj?-q(e$}<@0tu%?0A0QS$@9s7cdR5=Cx5t39*puL`5eIaYe;my`qmR3p9WdW&Rpx zxIAS_VD!p$%k67gPo0^N=`U7Tj zibYzcElD60wQgzI4GF_8$G_6`*F}|-0$(1gOc*9SQBn3{i>4*cMm7J_5NQN~6t-%u zIE+jJgOWh5Wwu&cA1w0de=*e jdHUty|E6~3SN-USZ`}e?hP_ts&xEm4j7}R`{PEuaRU2Uz literal 0 HcmV?d00001 diff --git a/_images/evi-composite.png b/_images/evi-composite.png new file mode 100644 index 0000000000000000000000000000000000000000..5680bf03e55509af77051f8fd0fa483ca5337578 GIT binary patch literal 31940 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

!G}{+Y9*fOR|| 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≪02CyINip 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�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 ZIW$3#!(*ed#}NYlR1`E3MRHHy{vWgV6Bqyh literal 0 HcmV?d00001 diff --git a/_images/local_ndvi.jpg b/_images/local_ndvi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75c523dccbed143016ff1c8fc33c41b9f6cbd107 GIT binary patch literal 96185 zcmeFY2T)W|w=Q@{l0|Z+QKCf2nHG>FBBGL$f@H}#HiG2P0tyNUl0+m)&NMlQl5?iX zxf=u;x@lhfzkC08=iR?*?t4>HQ&TgC-o)!(KaKCZ41Asm?K=}6@0H|K- z)BKNf+}{A*m)`EL-Mn8rT66l^dU`p!xk`vgi--$xI(U1#d&!H6y8QD75jRhJQQVv7 z0W|<05AW~i-wpyIg1^TNLP7!};v2-oe;WxYIVlMV83{2l86_DRIR)+@CcQ~TNpbV< z`QLx?_wm0^;XV{3#3X<3_+LA&+W~44JbA*$1bBAnsr=PBC~Pxa$BRH4)8iF_jy%`Y(y^c+iQzNzNtV zQmyQue>#fbmaz5=CnaNGWMXFF;pOAMD*^aCn>xF? zdwTo&2Y!x?PfSit&&X{)U83H95DkgOp3+DT3bGbCisMTXK~L z`B$`mNcLY7Ec|~sz8+Au5r#Hs zO$ORR`yFD9s_qPBY?|;cIjUCoIx`u;uh{ri!MZj&0d)c{Y2q+lNIwkSAbdIswgSyz zI;suvU4C%&RTxh?`)LtlXBYQ2GVPVFc6GCLf!dLbU?I(Va6}=ezb4 zNqkn5mM*i>Lnbcx{l$ZV2l-6*J)NK!MuN5IUeWO7YG~IUzAJ^}f-BC{i?g1-F4g~wVf+qx6GQDen$PKxfdkql&MER|O!!Bz= zsZq&D@-7x=Vuc9L_)41_h2xx5#DY_0iwr4usP)oScGs3#^yiok^XXY-?yEANiYPF7 z3z093Liuj+)5#Xi-1%Kbw^d>Jp8Pr+I@>v;rpCHRlTQOA3C3aW4Rm;i6Gp$lx2}Pe zvK+ED=Fr|}!JZRekb$UYx=2E0#wkf;!A0Ou#JAdG+3<3*0$K~DUdD({PIWHz zE3zn;coi)>46paG3`kPq|ur^hE}Z) zL>a6OAC7ZDR^yB#*s921)Cy{WIE_j#_xIFQ)#IIi5+0%~%b7<1F>jEzquCk1ENg|} z&`EGZZ-)3R2WeQcrUzZ7XcI<-v+MMN2!l-|BMh^bmWB`7m|+P^J4T))Pi5a|J^G;_ z@TO@bjVGSvVy!)=zP=a6gdK+AhYAeCV{5KJXW>_QW!}GOby78}466|8?QJ?byu9oi zTRK{YkGd~lFgy#RjW1N=0)oG0^v5Prl0>2MBZo&BqJ(H65J>?n+Qer*9ll2k@P3b$a;kkJv*c?hr$)}%)|CZ zPEjJm)fX0r3^@F0-sauOa+n{R?Ay|mBW7bn=nx3;j9v7sWD*1RTBUk>4X}tP62psM z>2mcw+n(}pkf|)knB$#Q`@d^ctJ4;K&ptT9HbOi$VbldiJO-ogE+>HMC#kJ6 zJLdjmFr$x5c$=`leLI-ON2|t-6m@QnxHLuiSv6*Ts5M@AG<~nEgDLV*7_KcN!kT;^ zWUg-|hQT30c(K3e>Q>J%Gs;X4VbvqbYJ!m^gBQsc@r7y_hsC)U-Ue+-%UtqhMyI>6wK?T&kjq zYe9&L@c9qj-FLqQ>1<^Q;f>co3#F!rJg(U8o3jXzc(6SfFX>o0rL9p%p+ham>0EGh)B+=PMIO0!>3oypH}&MsKfjWl*iFLp;MO3S5<9v+He@f z{GO>p<)o;ju#xsunessKc3@(`SHVn$ZxQ#jZC>^p+Hw{XhEoE8K6Lu`iT<~nkqj~w zz8QfaU*(I%c)?40$6F*;t^~GBC(EZu`JA0~{4-1nsC!EfYN?YZpFWl)93dXap;5-e zdB123!}3aWTn+n;VLc>cQVacB@#%pIx#VT08ejASh1uAPC)&J|Oa@CcY#GR5;?Q}k zTE4ytI%i8+1l8NnyG|LF=}7!*pzAD@CE_8XAe6dc?9Y|8uJXFMgt7`Yh^Op6Gu-fF4ZR! zTc~Fjbl+PH3qAT}w8>k2Hgec@l8RQtO$0F4dBuq=G?B)?8V>3aom*S0PgEpEK;ewh z5PJ}#xyjs)r9|A}*7N9x<28eV)A3p!oDsUoc^?iLZ0IUHq&f`wlM6$ew&zL%H=)iU zWc@#uR8#Z%o6QCJUy$qvtQ&1I_K2eRH8HdmYu7-x^D+l27W1;i$p~!cVyT7GE5GET z&HhJ+oB58ERdY)hoFd~sY?%hnDFC7%^=J&6KAP95Zpo)2spX~DO?dVztDA}s_mb!D zbTQ}8t2Lp!_E2sY7ob+ir?XRf#Ns)IEdYIcCpG3U^tONAk_KM54&tQSp_?Q6MrRCU zWo>x&CVa^;9~B9Yy;I+pv4^{8Ycl}2f?uq0zZotAJKo%eSav~%`>*ye!;SnEaS-@z z@T}SXk6~Oy5}k<2j6Q~^V(E)w>weA%*Xo^xlD6{^{NT+`$KGVWdjGQ3*&bPu$gs*7 z^kA8?;UM+kVLOzkQ3xr8#3gt<^4n9%5!;be6`m=L=@OoVS6$cnV$I72q@v} zvZB5BBG86`-bx>LF&<~9pE@)VVieQ%EaDnqz$x4uvYrUW-ci4T_77u(+-TOOvGhkV*TBV% zoSJBFaK%g5)_UkE)_#%Bs52US^YK;EhZrzTi+b_|q!Wf@1YK}xK~^@XX4XX@tG52} z956%$M*SM-mkiQ6S2^7WV=&MqXfuCB>cB)#L66$$=o^imhzozV4Wp&OsTiB!LMfRJpXX_9}zDC+yknq|3}0d7ED`s z4Y>GM_hd6Rjn|ZbYIrJ4;57Zom*`R487zo{FR#%0CdA zVDgc&Qz)~4PS=?w0TLU2nOGaQd-+`E-m>XpEMM{+83w<5;t>ti>OOZO!ia+S`i9Aa zO;NZ=mB<0aq{xQ@eh|GP=vy#XJ;He~&$)@`ZC(0a>4L=%?+H7tiaHJsfe*B_xXvQ>x)bY_B;v`G z3K*YLu}7IrK$l>@wyyzux7lepwAXF^Z0#%#Rlm9@y1K6nLcGl;N7zTWoI0F34;q(? zJl#Bc($)||@?gk1PBM-m*#K{lVI5WU7QFIlnXYCSC1AQ!FK>~7{V7kCcHWNC!kzqhr z<+af0^UI?679(U)$6POT6-2Qh$&l8@?0nwP+EP>dVbO-%@0;G1gqHsEJc?B>gKNMU zJd@fRDvLTm)SDxXI_R(R+L*VkD9bRiOC;&t(hPN2b|n5I4qXkMpn?3_Mw8pKR3FQ@ z`qpW_shvzxq2&0^lw44#WVqT!A}*F=KzBp<`%f`Ol03@n8X$voLhxVv zG{PCPxhkhbFuKLNzVWtIcnk|;`UK{N#dO#ub7kXg7&}nffR|raW?@X=k7&9EBd>1-ofjmU{!Btv4 zH8d}YxdghnnsOcr<21HbF-vo3sMW@a`l_j)Qj9q2PS9LEBU36>_<|kjyaxV>`!Oz% z6*z2GaNEu@Xu>MlhV3w)`HrwG&eHgMSPM{n|X9KbVJIcb@0D_X(z z#3J0ZKS`d(n)9FiIE+5s|My;I$DvGME9=miQ&LJKs5?&eP!1IWS0*;U2Ku0np7LSX zGSTb<=b_xF*k%s|4!A9s|EwK?#q{RXnyCIeO8*I&R1fBn!LSvhS?4Qhf*&CXZT)Yz z;K6U>Kra`E(%_lZzCj!m5@&g=P*M9_1MPnyw(s|`MI?iE;{?lPB zOc-_U;0%f@v~NtK6lZE$KNp#Tm604&Sgpx7xC>07141L;aIVYG{8l9u$_TgU-H`)C zFNx(kdX?@58T+Z6V*Xt_;c=++$2bQPeGS;MTW)H(D7vgE6z8gOCwC#{yUY8(gc-ki z`A+=)aH~BIBtM`zxT`cmx&7a1lM3F+iC*IRN!>vh5)JKzI{yP~f}a0lW_*uBS>Uk1 zYUXNFYZ~X2OtqDFp*YA|SVVzde#PlnJEw99yk6iRAMx+r5`@7!1Aq7%2mBAr%JI>+ zqH+WwJks}}6$^^e%F0OF{mR_rvE|k2SZ-VW$rv+&FeT?)#4?7_>&y{pl&C1PiiXhp z_fqQh%(Z@vEEGQPTD~>Y^ubVtRftOc-)*rp@t0Vskda#?A7tTeBgV@ko#R~Z0_QYr+NX+KAG!n zG};T6u(H+(f%8($3}WMwaFPC7Pz>baM&W?)uR_Z#i$d<4;`0ym!x&yv(HD#-%4`+J z;%|E8qep>i)~TEF&K#xf7C8{PWAZZ9CemjismPiCW!5b!`Vj+`$p5qGn||{esL8>> z^75>L7UFa(fg4GUdw+!i7Uu=L!8e^)PESvlXN#J|3TqQ29(fRz9@Eg`*>ERYdR+rl z4^ht7fa>xA-Zc=V&<9>D(_()O{A>a) z<>M~;upA!{)7VJZ9Kkg};g^7mgN!sWN`k8uSLBcyVl2}U^EGf>9;Td>*XD5zL>C52 zT?4spXJNQdo_!6BH5mQ>YM(L72pkDw27iI`$oAR=YG3D3R%_E;kXVk}sAR;Ywwv&# zivQf?>0+t44qZYx`MmZontWM2kx+NC7a=_VN^AN5Vl3{1I8cITd^nFeh$Xc`-`N%q zzB?h66SGVbD%`c^MuU3XWi@8Fxilau^=W>vF=JOjo7H83iCj7~G=dW9gxW!r;J6w( z*k7(j3BDW&Md+=9!*N^f!49Z#WQ7)zuiZJwI~6`1X@}B8N*^4p)XE#a@d%(8{C=l@+cs@U z8U}qGR4C{zUEC`;19PRCB{o57zJ=KiQ(`0$`(6$pl28GZn^ALJ(k`p_n4}K5P6I{6 zZ=WSDDzbguA+OFXHgBu{JfFe;*+lTRm+LgcYTe>sRiGX{@@_X{&$`P(n&zt){@l-`*Y>P>FwtW|Y-0&CaLIX+yJ5|IIm zJZ=1a)xeOE^*jHgOI)-Fx@{1A#AT#XownfxzXooAUqRxssjmvLY`a!uZKSC2Gux5< zmWNUx4l^C3__QIyvf?Y<`-QnV2gJg!62%nx3(vyZqV({-a>1RW|KcDbA>rVv^X_3X zJ+xjhEfUnV=on0eJn2~yntrW($FZsR6K~E}Q)hr?!EF&>~Ccely)IXR0F|KxcUvFhr}19GhW;YjLOM1H~R(sYNoOLnsy;m zE6ijy)|y6EXomjJ(SOFAe>M4l;2!u9JL(8aS_mJWOV_DJ9ED>HR&Zfh+wC?~q$50j znPn%#T$wie8qljx@|SYfUT5T$om2YngbrjYPbuTbxejrRHYjQX z+lTW2zn|h9;PJ5Hy@3Kv$m&3DjWAtOm4afmMnj@Ws@#h&G)mEM@BuMe5Mzxzt&eN5 zKrr@k2q4uzs<@RpgQ_)SD{4vbLw=_Y=GVm6*Rdu|?%;UwRKy6?1w$M78u%wfcjF?Y zFQCfhIAtO1!kMKwlCixX>;p&pHyj@vcgDC%z?lqvaUY9=BM>yy~k+!$> zCB>nqQJCq{Iks7(8}mcQeM6Pf)kmcS#R`r6Lq%`#%H*HoU zv65GiTTj+c8=a7QlKTly?giEO`+uf*jE2DoBhd?Wql3FR0t(ihbLrerkz2BrgAz=~ zBW=G8apMabSg+2UOLs$euZ5cG8p zW#e+9=IqFH4d|esUTA^%JYJxF9`ZTL65a|J((4tZa|#~jm9=@dMXX?y9ig1%>2+Ej zZ-%P<)`ysEk)aq9E|Gqf_K5KqvK7Pa+N;IA?k+YShjN;bo2$eW3V|gLXt#%Br_w_? z5FCXZ@%9d{Mal|{%p_;|mm>YbTBJiQJsMotRC8 zv^lJ|)_in%V!qf?ZB;5Z;KLb5TX*kQ&&x;aD_*^i-H8KJ^kfV_=m`Jr14&4viBpdx zoDmSv7YfOTS5NE|MI_1`HRq2g60g!B_ivF@l)biAe{${_O$ORnY>5==X0*u;eBqJc zGk4TMBIH)KR*zQNDC^1SIE#NiFaLwHWtdjWkLF8?K^XBkfg&t27?+D$BL6XRs=%&m zI)^+gYN>5Dur+-0w$%3iLVPn&$5c6VkH~%}hbCC10G0W4q9$msB!pL7yt+!u^`(fu zpNMo#GxzHsK;rN4#-0cH(tl*HvI_a^i%xC6pgyg_m@rBcU&h_aMG7qLMi|>Yu=9W4 z)Vo1F?X`*@^?Czg+^5x^Uq z2_0_=3D+*%O}hr>=WI)DYc4*1$l$7>PsEzLt=#qQ6Od1G9#&L8%gnjOo2%%;mX0i~ zDt2pbxHTB&$#^@8ijz7c$8Nn$%5B1O$jZg6@{ey~{5-SakVaKgU6ho=pX2h|p3cE4}!k$|^`%L!FT#Vsw_;$eiw0vtD1BTf4efg*GIHCM-bSlHGu6 z;r#(lX=2XAJ~!sP&&(P|FDVq?kJ+)wXJs@djWbmCe~DRkfI0_vN57or&Nh=>d)LY{ zYB#~%>cCw;)ANH>!6W-#kYC@+iz#t8IlBu}>G}6A4cEZlwD#&j;I9Wi%!Q!!Z{5i% z2bA=#jmG+N8QcG8Q;`<=Hu%F>whsfsi+8+(I4fRV?#Wj>AZ2cw;hZ0#= z5Ru6-f9p|2vGUfbg0pn@PP@4K=>oluWa5#k%@HTVs-Fqi?hU1k+cXvCB4B#Qaym2A zUrdC5M%tv5=ZAxbo#Lhr9hhfxO=uGmQrso_D(dCRl!ElD(7}CrwPwaz1`kBc^1}4w zxVUNS`Ivhji#vQ!NW1UV5d6zy7;IH&I_?YR+61uC8HO6_mHRY9cw_I&+~|hdtBT!H*ZqukF~7nz;Yht3{}4C z{SCc$s~oy-5Dm8MJNYhC`Ee~pBQZ^z4U|q+GR?)7VCea^Fkf~fGGXHgymxtGpYMOX z+1HaC@_o#oVevj$zai_lpIe+2Hy))t0VKfep6L@ zi|^YI)w6`e12uTo5_o*yySYSgFb30ZQ~PFU>RkebH#>@{eM@eb`TQ=$D9LIqbrjE( z?;`hami2@~mlm6n*UUgWbvK(A!R|5aV2NOxzRgW=@t`5XO=$e`g9{&vja1KV4?Z3 zrh!eu>4pkVv`t1u-Zq2VicgD4z3!m)C$h9qV<;~>6vqrCXlPu@dROM>x z)9#%ybGNxp`6YngCbWiW{Mhd^N zKOu{H@=J&Q_?TR|9Xv%AYi4K5-|LoMk3(&!*LaIk=|ZqzeJ!$ZGu0!1=ibtcqi;C5 zPu!-hA)}q2VaKQC{>UhE4H^TGvYFe*;OD4ineK{M`SduW$JI%2=L&JzpONOo)}=cG zLW~amp(F&!VM4gILTg1_OMr6g#{4eBK?G$&&zVUq-LFvwC{g%;#J8&zpW}8;yc&;b zKOHzzIfH_N0?EtUDN^RIFBGb6XOkobc0**z5~Yv>PHqpWH_p>CAcm4YqVaxhcWE`h z8}}}~q-koUI25JvN@CCRA4M|8QmJD~0TpPh*%_{XK`!y7iNMQ97@JnrC0m-g>`Yw?H8f zMaeWECszIHRo)(dk3(G9tGeZl%dF|dxoL~3IUQTu`se*5a>1?_G3_983cSA8 z+3B4|rmoPheLR!vH{XRVJ$u;pYT=d!VEqMO`3$eGNUGoQO+t=%wPrIL<-A@wnBsZU z=e$@h2ZMrzP(#Z;%rziitg-igu8fEKTYa9qt{v70){v^#ko0r*qs(k~WW@SA5<()m zr-83PA3Z5`OC1eavYW-C^u%(t3{`&IJ0W!95nxPg?xL=-0e$MLj@s|oR8t(OBXy|H z$}vBgNCH6sqgbnL6 zj!GFD?`#s(x&~~_W&^a0gV&U_+m#pC2!`aXt9+m=i^kNH8ES07g!dz;y&`@9@+Z?u z5SAV!#hBByxMZ_!27@r~2!m{33UHLuow|@Q{$XHoKaef8OiCa_v+7{e#b57{TYX>2 z?6PLDrCEuN2W>?<1<4E%`$7Ufa~DwHj-N*OVX$?4vnG6@w|CFc?!}7cNw^;o^S~4Q zQG$a91=@T2xKYCF5UKo);JEB1h{OBH%{Uo~XTJiV)eEY81 zy&?2*Aj|1dnCWR-HTq%jEoXy>{R5;?r?FW1LNc6{I!EnXkE?=yO{Urri-$Pqi+b4> z=@Yay`)!eFe&CI}7lVej9Mh`SMUoa50p!eh^i|VUJ@&>f19^Z*1aS|Tq=)dFpVl7p zxKuOkp8GGkodE(5f((EEyG98u)0@7p{C$L`sh-&$+;#&8gxa4LsnZ!v=ZrE@`G6T0Z(!epW0x&xmng`2z4UAi~hEQ55;`j(I$OgStwbfP1 zHGoTv@02gKMOFA;z30!11O8(ZXe;?vo?N)F*q^G+UaBj>E%u4~#g`O%O`$$Eya6fe zH~bO<&R5FI=-H| z%hrD+wK4H}lsonFam<~_lKiVoESm%PCd95{2D&yKouPpm#Zm8<+{%(jrUu^$MN6q5 zg+?DUoiV+mwaAG6 z65>&wEx3%Y^3~rL7ePOECV1z9ng5jScNePo%b@8RRsuZnK(#o zYBy!#(^K!hNosuFygB#GeO>=A6y_fveF^hhC(!Jr`uji*)QUc*H(L;4?G$*5t9e|m zf8JVGIfeZ>U)Hv4PdDHavT;i~#R!6a$<;{Q&gYHqM_uHfNlx}9ydYU%ec_jslceqQvQ;UcNj4_C?8!2b88Yhafd z?sO?1M#i7_e{U1q)I;$PW9aO$OvKn*%cy+Fst_)dQ+)tCVL%hlhW(L%RT!hWj)C?p=N@FB(l=hy+Yx!IH#Su z$?U|}vuIZJm%$>a2z#XSYEE>p!Pg$2n>lb7@p;4AqzwbRq`PpFC$dj}G6{y=x0cAF zovL@;<;{8i;fDn-gnB3*gEorVnIKs&PitXQv!|C2T2*7FY^?_#@R=FAhY7 zrNt-&71hUUS&{gsxjf&6*4cfqJ1G^GrZaf@Cb+YsfX{U~b5vP;`?4>ljS5wvhrk@0 z?oH1rEw*J!dVS;&On)=QYjT6^)`r##|K6Y{#Mt zP@0AzM3o_b;)rLPV7U;>J#7TGHXs20nYRnBT(lxRC2Xaa&Cexs8CY`-@Cy-cU*$Xf!6K1FS$>3niKjh~Npj#u!n zU&v>ElXjaz?5&^H1i?|p#TJ$fSNg>N{i+bWpN>ks26X-CH^2xr7&OqT2l^I5U&HE_ z(6{zDcV#$cuJyaqv?Xu6SzCzIM$h|w{TWTd&bt|$Z>(>dYe<(9UIPeIC^S?6QL)d3 zn_`xIE5qFoYKL?frBweW(lsX__iOTts-1egC9SOq>lVRV3OR#yp7#|9vd93eyP;H- zuSr21xH>PHR?OaxlZ~?Q%^x|Xi*GF&FF*MSy4becQl1ZaWqBWh1%}6^Mp-{`Fcmu9 z8{YIvo&MGVpgFP(_CABrkLPs0Sqo&gY3Umy2V4pw-sLlMufOuyv*L1A(%Yo6Tb0~Y zb1fqpvzAIsKQGz5HKTvEdL=fbN#Bscr5pOP*3p%XFMC9_lv{ni<#fovNRFFenTas2 zjTM%uO{2p;ajT0(nmKns{Mdl${ouV9RA2Uzhotjr>jOlX+lHO#>$-#o-u6jIysDgU zKsIx|*-0Si^-Ptkq-TLlCr5-7S_Py?O@AUcE9b7i**u-FlDk8xos$UW_YZZ^T z=9&eMV&_6DSL2mL*7Twn7;hJ~p}|MbsiU5#CC)r)Bnp_jcON7pT88p<2GeWXkk!Py z?$Xh5D$o-X(gd!&*-H}iJini#k#Y2V@^-3jef@pm3AKir0$L6u#gyl1a_zkeVRq)_ z(-Ot|b8(Avdu?@G$f)M8+i7Xi4Dtpo?`rHExmzO!8+=`fFYAen0P$iuIl>fH`jm9j z^w@ke->?A+eoNUry%Y*S93Pn)ChpZx7hsc7Q=km?~% zNc)gP$C1SDxXIEWQSF4jY8TBd@v0Z=zKRIBqrHLe8+9@B$vlIUmgjzgJzsy-E4n~E z=H_QL)t31P(Lv8MEHJS_umilxtI=bur&fs&kMwROE`eo(N zuUDxZpr@3Y-y9C|XCAp6Nj4Aqer^w@{H4j%`xuXyi=*g9l54`J=dfmxHY)_Buvxc} zvgou(MR~&Ku<&G_dB zZbA7qJu2sP3l6yQ!UH)P!RrT-?{?-w4hkpkE$GCwBacKIHJ!Mj>#ObABTH}jKR>QgpuBJ4t{&#o5yi^T@98`oJVP5@3 zB0bMHVFJDqho$+6-5%9Ko)i(O&_^t-t1{Wk-A{JLoEX!5{T{zean|V0f~JmpMs>{h z2K3>!FJJ|0YLjf|JwUwbavl8y0vc4sa$EBmNoTUQ0(gu~(k<{SXf2ARIdsIb4q$VtyO?v|8hMQ!iT)UuEv6eA1__24T;=Se~(=VRb8IsOLlq%ZA+?l#8b$A<|oW4UC3mkx*%bwQms2$gMgnA|IWSj%s5dWuC%_zpiZOnS2#E5*cXdof2ec!#nZtGSn& zYT-J-e#)b+dOmJF9Y-^4*Vt;WF)9?#6`VXtB z^jS(|Q8;bq7S#|#Qa1+bBQIt+T7q6|CCQ$a7EAVvh;Y4X`0(n5j#~neW!8pqcH_Q$ zD1G7Ai_2HS?%&71Q;n;WZ(X3XY$UnS{$ML$ea_(5-` z1Q7}Jk^RHViWb|f8Tnbl32Fh(F8$W%=?lQ~W2)5_oObT=Z#ELHyl`ypX%jh&u#fTW ztGj2$heC&;0hAp&@rBlaMd`A#^w#-{dqnlFOkCu%y8(e3mVau+gk8%e2g;_*tRyV} zn^zaXYbmmPid#^VS{vlNhM`A?S4flXO6NIsv4HzeY{*LvRLyE{1jKwcD@{bdOYAp9?1f+wql2fNa(GRPG+}w(W zxu^U>y|g>?N?iK)vckYz7Pt}_jKr!-ka_rYw-Iljt|ue_U#sfi6|3#W&A=$UXeH$H zt-<2)gOJpxe$^MD_Mdm26@RvgobBC@B0mucsh+MNs8};f?=gMv>*qderDp2@_7jRx z6}$O3V547gU6g6BM|UlUY!0Q+?UxafRLoIa)O$E}(#HgHN{|qM5htwVv}mz68?u=u zr0qx*&@8IiduwLA`B|`Pwm}imaM6?+*UIx<-iAc?b`NgTX}2 zZA?CYpW@2OD;wGEAfxZtpURvqNYR4<1pB6=@}L_M+^OS&FU?t_cQ7cLOq+ExYfC6V zjyvP{)7OCIU&)23>!j(dl}W%Q5fBtgznfEKR~rOD+lCIErSvabSE6!O4Z>yHjo-*0 ze6o2nnWSO7zJiy9my`kOi9gY-fA9S1<+7zej!M$8xm?JL6WoDr(;}^~HwU7JQNw7N ziYpb2G2~b}(&~M+<2BIamR?ygrH7L5rK$;bzsfSge#sUFUw~9A24iblR2O`eN;a#t zOv3ZJR1PVEFCRU4LuX&3A-vfDY>7 zMf|b&n3Vo3@+loo!QJzq_-o)M*lju7iYizMtsbn6R7HJ9KSAon{Yf-ZMRZhEA{N8> zy%*gI)!n0l2IB=^L1gb4ehfG9REj@yzDjJ-MoabNmO;CVYttTm+3ipW`SLpRJ1MJ2?dP_>!zo) zv5xJhnXt@)ez{5h_VZ9YL_aIlsksRHNU(kl(1nhG8L(8?X=RZ~+z!9KwXHW;nt9BF zn(-EykOGu_=QIa-h`{s8B$A4)AQK-4!=S!~$Sopj2Ce=yi4&@k?-$W$5mzyxJfRck zEa{OLM`Wr_<_fl?Os&|$-|rE^bas!?@ZBQMuR!mNOiOttKTQ?|p1geFMh z^k>yiH5_}bs1M(q@J5wqkHLTad?}XjBWl-&n2Ysy2=ST~C5Ra%V>mjkKPl-|Vxns& z`e*j7Oq!*&c)&;_A$RfTpWSTth=~h>XK_r-D6+nbiV4R};i{|2me=aLA&C%sG}Rdt z!xLCqzdSSFHog&{aIiYR3D=!j%;AxLVZZAHTrA^ucpx5smeE4~?u%E{MykQLx~Z5o zzgVKY)TezGAO2LxA)B`(Vx%$Fw0CqP^rPJ9L7&HNT#Y5pdy=9JAsvckxLq%9VWBK2 zuaaOhRO=T9dG-Q^`r{p$K(i&(=P%BVx3jU%&CG+3QszoCbB`k=Q(P&?A}{a60Z1zv zZ2uA`lB%nM+@TT^h?*?iZDT<{{0(Q7t1+1Hnf%mceJnF>I@uZ_DD7Qt>HpDZJN{; zPBZ`2qNw9c7mi7*Ou+;!E+j&QAL%wY#|KqZEAksWUMax$I7V+&_o)rPo^}=u_cm*O z6A%z0ou1#po+y5qCo(BI}q@F*!L$CT%kK&?M6>c)Gv$q5X+2%Sg9ib>T>` z^_29OP}*6lIp;}kih=4k^!wBwn9@*lPl*9yuDHq~j7qA(WS3K-%b6jeIQjklf?`%o z?ObX5vAFw9+4dgKl6p{=slEzs{QM^|2ns^X6IU&ea;e2D3&4)a#P0BfASAI}GmVbZgk?SKP8lnr+2NbFU zHALKA$yx9Q9d@JrV>%NC+4T$amDL}C9$9fs-XUOvI-9d`HxH(t1^MeA0|~P8xfG#= z_WXi-m0eHY2BZ{+T4l<^qD&bY*I19wd@n<~XDRB^xivxkq+JXGH}3PLF%Lpza!a73bYCjKT$>46<(!p4*Ds^9FPx? zM7gxMq>7iksm-FGSU>GmcL{zreUKF4Jq?;|`moVWt9wV2MJp{;JXQw3Yjac6{8%$B zEOpdlvsTA=>sf-J<($>X>TB;ZQ8>9HLz;Zn?+{7CMNJ>KjZ$%;pn{j2_9A7qnIDKb zvnV!e6>lRnwI+Qi4VBDHQgGgdl*cTdDLG~Ru59n|8-phNjmHlY^A$aD_9><2^}Up4 zNvQ>)imSIv^;pRKWDq9H&7W9keM4e6*xtT-Nvdhgr^KMHA#Eq;%DkZR>0r`Ow>>zW zJ z$>may64fzW7Oyv;fA^k}e_xSY*||dLdKvpQP&SnMq&{kjWd5hvaLO*nz5apTn<`r= zv+PawiqHMzB`-geSO|aDbZs;DVzLRz5xem743Hy8@Ln;}Sp^e`#$vc$viBCd@k~xs zfM&nCSIud;CszvHpV3%2ZeZebzyBcjPq^Tj7xDu zud0;z`!d_KR!8ZC9BWg54EQ37bm_h4k))-BvUJkXBXTBpW}CjH#wN%+?!o=-J06dp zTz}``(`G0$(~$pn&+Rnd-%T#6BT>B4v5K9@)AbLkrHvi^-0+go48Mm~p0LwP8iznv z-r9z7H=^=fPQn*22|jndpb&3A*cF!}DBjFE_hb3}ySLE8`@iva)mcY+kxLUD)Ul2V|QV#N~N-Q6u{@!-Mo+dTW%yZb(~^Ult@`^TBd z$xKcrC+D2`-q-!Pt{WZEATOZIoYGooWv`j7-adZr=X{gA za;SikoK$=Y^bc^L%b~^Z_mVk`3zf!AfAOZtXJM7)^mWA-27W7gsu;LT#~#~X-#&C- zOWA6eyrOVGej4Zfy_yqX}O4F~-T9Su;{{Rf?5$6Z#uBK}X`yvHac2g&ujjXy( zifLv^O70*J;x7%Af^{XF;*8eXY{Tk7($jQ^)2_KQkb0mm{+1%a_jP-6{@u+nO3R&2 z^Dm7*)kwNCZ<)f3Z=#Qx=HPXX{ytmQ_JTV@=gQJdnCT3kUdDOeCQ3e!Z>uObF&OoD z`e$*eAG)%%ob}XP0DBDKZB>0g+K{wjQGvAVg4j*IQP&?&^nw5k;CSxG3=Bk1L_dAs ziNxB~?!Z&-eFXVw z9tl{)-I}MlU|G?`dxK})^jOa_*tp7e**wDN3*pnhIpW`)k4>3#$}sunD_h>)T2hB< zlalm&dKuQI4w~RCL^$_VThkZf&t-pAkvJ5%Uiy{rk)4JU&}m@ZRIpdwYq#_Hvj@AZ znf{|gOz9cH`Ow&fS}==c_WO&rVQ1==Vu6oop?jekGE8Efr$#|E6TAVwr0L0mf=}-i z7pZrp1@4@xsdNr{Zfya-oap&PE`G1$bDxCgw_b}{O}$ah7A-$=u>{2$(!xJ2Eie4> z(vealO%~8fDg7JP-NbCEiW&+g_VMpEa;#bwI#v+mw-OMPaI7OqSk(HMCZ`|<6b4bv z#2~dtb&4oDDt37zt1hL(-zMbz*!TM!#BnB|RDE!!LcBR`!1k?=J(Dy+@&vHH8`;UA zc+P(!p!E&0BTj-lW*uCGk}87&cx(-Jq;oKGlz7gwgVh6mP%Q4= z8ugdCmyeUuC=CphsyP!>P?L>Q`9<@btuu;=6TgyAjPcjL8Kupv1Vx#>UsBG8`j-3` zzbrr*38Fi3c_$BG+fO|ywC)(MLICC*+z0HkmD$w5I^LdE0TiddbYz=HN1BE z+y}m+!m~C^&pgHg#XXPrKN#5`(x`&n@SKKJPw4#c0DeJaLey5(vee6iLhfqcpIL7} zWh6xbTMwHsaQ!fPb*v6{`_(r{*IbdSW6Lj_@DZC0!$+D&zf`I*lGRPOf~~gcKAWHW z(iZlOFI+}Z+&g5^6kpT(7k3Y+rPGa*VYwB?r^N@r`a}Eaa|I1({FrJ)Pzln)_4Ct{ zp6^%V(veiOsAp}=vqJ482LkGrHcbw{7=Diw%Ps+Mg)85>YxvE02w#ak78O0BBIqhm z^ETOZ$!UGq%aG~2dATh*_jUdHT2=8xF&^jnV@}58?S>AnBG1mf=|sP-d*-CxJ+64{ z1r_u6(M$wuk7)BMnR~avG#CDs=41J^T8cS%;TDNXvW?fLX2sFY`?DMJ1me%Vep`7|uQT*+yR z0V6?E)oH~=FKw)SDqx$I_deg&%(qFhHchizs;kZ+XUJhQNlTTCw z-)b_bu`rDe?__xJUkx5YQ0?_z8AfEVc%f!rArq+sF`+=o;#Qpkiv>3i zxZe~4=ZQQnKqlt@vk>#w0o@43L9;jLMsPNo+sHto@d0(OtIIcm+K*33wdDc-r|sVt z{AQx4<@R&;_8RHOG|>8^oP@fWq85fc>HoXUc)SN3pO243BLzs+785uG&Ewde-m_h| zl|D;cf;zA7+?F>(Pt~?WR~ipEe^!k^kfOsVyVJ+M_8Xw-JmHSphzG8Z*kG^9UDa#Y z9X*i5W_sBDAag-pxo=6bKE2)hI{BA#RBEpqf;3sj0gT$Iv6F(WNZ<`}-iSWEbNP zWW)bb(^P^NdC%BT*(aR;^FRDg&3saf(>vRTFlRK-j}{hQeVC#Ck9!jsW@y-=Q=v`D zah7ck{6o5As#2B|e4l;_)c1HzpiocfFs~!?a>V zcc2NVIWel?z=D;5Xd<1ECshwGbGCL{&#*R2ddD$M0(7g9`|#k%)1o}yem)~Zd9Ha1 zx0U*_3^$gT;-cxLDCccnQoDESj14PAkg~Ep;DY;fuNiKD_ZDw>KF^tVH@Evum!Dp8 zxskmTE&^|jtah>jTs&i`(^#&JRZuL(@yN}|f*a9dXbAg*| z-R;r(`8a45RpUleb@eSiLD!>ar)TLR$T>yzR}nu)KUe#~3_lkEJO*$Fjk$G1;ex6H z941du4h(6QQG?k^j>^Jq(FIO$uzquw}3Tzt_ zt0W~M|KOs{56K*_SGA>FLT+3Qj6>RTe(n2BsQz6(j?$hnQEX5v7aiO1e*W}RKED}- zf{`I)sDWcHzIggWn(T$uY>A5YwV> ztMxMlF3G(j>2Sly8;}cu*y-u=8su6v`topKZp+On>kDlNmPR8QcE0*`A|djoT=+(W zux4$`4HT9|Zr-p{o1{TVht*iYi-~LkK0Qb=R;JBP>mA{W24kKMic_hDJu~!SC^5x$ z5oJIAY_Yla$2%yJyF{5S{OZuZ*((7K?+F1Y?YIxPV=<5rNMlaSmBdJBJC_+6)i;|c zdOGaNawM`_dhF~pY&_}Mh$EhvMP5d~iI)XuRa_S3a3$BCSOeTW`o%Em`h&zIe%6x; zPi1o72&nj2Cg)nlxvjh$^Z&eCYNU2l@_O!94l?6%AO)qGQr@}!(KzVr27h&&_GVwd zg%_@RN_jB3u&CH%PquBLKuoM#D+rYNp8v=}2UO3|$rcCstp3bGJSmVMc);6m*Zgx` zajb&c>h?-*ZGKFDm+}f*M$AHielAM z-k}~W;Qb#yyE$PdJM)|7v;AB@y>8^e9XV{0H%GIry0(FpEk@|uD5C;~CC4uT9#ZG0 zNzW$~1R>(knYFQDCrFEpyCL@2(U+MkrYia?W{)-Q+18GA!jl{MdelJ}jLzeM4RAHk zZ!|jHwnzIX{%6sARTgjTn~x1=wA1~b-2Ef6X@$C6?*2~{Z!e$xfMtlhz&#G;50dK& zI|^inSmdAH{E$T1bL8khno~+(9C>KIwUl+F8&cg-rQ#_>egW`3U^CJGtefPdGu@1d ze~6vC!-LHOtqpAMWncdg(r(h(d-3OH{HhmhvR@s*Q`g{_W56^+?P0&8F~)Ob;?a;x zM0DB7p~OHOTA%F%zVfg3bpbh454}Ai-U-FXkG*i4`7BlMp*p=i$0a<{rJJV`wa(qx z>`aAAXzKpVnr-FcK-bSh@@P<)4lKr#(Uq&lzsU$%0 z@mCR~g)Yw$e2@H`D`7orOlQHp+zRSaRJ>KJ&E4weG()~)8{`Zc3INgNz6rl`-4xAt zWv?%I)L)`1wcC(sti^5t(lwVfo=G7GpBKC+R*ITZ!t6Vk>_ssW_+%LK^EM^lDO0($ z_0#u#{1~xDX_!ifA#axUEhJD@Y@D%`37*k`d=PV=_Q{_+5&IrHs>*pRxvl?cAVJN+ z06OQXo%aa8)bv@O14zbON(y0O9e$=U@sDNOIffC-bBSFD3M!WEl2V5BTJ zSN~^#++$QdVVD$2;~WobFwG3;O?@JvX(h*lyxDCQ)I{`2-|#K1<%RT zRGX{EJ}qdkO_wCEP7#}ln+uUP!ZArd+KzYuQ65!8g1GlMn~(<9f)3- z)ych1c@}@&Qc(DQai_0ZAhW9$C`KU=*qK_Pqb|Pa7xW{|mh-~%@XG+YpI8f`vPd6`ootz*D* zWnQZ3{3^*zt6-eC@{A1u^+`}s_)l~6KeUH@V=s#qsqoONe zpHgHn?u#=dcUBUt%YvEqOz@ptPO7?%RunBJGaS8}UkHVT%k5HLr-(ei&#gT9x(j;) z4o_O~dGDAlnKqLxnb-Uxx(LiarOk&GoL=CLZrSMs{bj8RAVr*{{pM*bKZIDHSMt78 zix7{Re@KtCR(#_4)zhcqzJ3G%XF2PFqvQE){Oqfa;|cgW9*~R+PoU z$1DoR;rO9Sfl4_Md6jsT$v0)%eR{FSkH)wE0JNJ^d$iJJWQoKf8UY8 zd=;ULI%Sjm&~Zmre|0rm+@%4Fptu*svQvHnePN4<5i-aIa5->`S8c~!v(L_Z?5$|M z%2{|fDn#fof=C_fryqW>!gC(Pb<6jcZOZP~jX0#Yc{v(3sD3gEmA79?IT*gltedIL0y+0%&_J9Z8l>tw?F z@kYG%6@yjk<&n5&sf6tBU$s>x{(J3tn{!oONt1q=j^etJ>cnN_1B-4`{?8wb$P=W_$vaj3S z+)9mCjhFjvg%+N1Tk_+DV>FCYQL)i_bYKEJg!OOqN_P~~#KWjacOpnbThErOBnZ+R zcJJ(;?^6pqM=*tri5=(r46v<}(PppgqyyLY!9CUX9@n7HkfE&OxMG|DO9uBw=^l)` zjr-aCC1;u*9eJtzxprG%%e{J=A=#F6htFzV3dWw)SGO<1-wW19Qgp#wWPq0$W*G(j z#-Hvi5oK>p%a^1I!M;wP5|m7A6s--(+3?RJ@UWTUj>X2?g>F^T2%x~nn(Aok)oihh zbeYV-K~eg!FMU`Sm8o4#1c|?!M=Mm_0^iKG#5=Q;H@7r~?&!%zyz<2a@(3eM(I0D_8W355g|X`QNq+NK2YgO+cwC`))<>p#p4v3Z) zzpfpx&`DEm1utmE>P@crNL+`+DJSMYX-z&k`mJdEW2WQ|LtSmCf9s6o_in6 z?0g{kOG`kd*OtdKhK(yprOMiCV~BF<^(Yb#%_p7kVD$#$IotnOI*IHdxAs1laV}w_ zwLPogE4?@174oB%rV6%$gDXn#+k0R~!G@o;OJtq-*m0B^GN_H9aXNT%dkd)^5aGdl zU6mq@aYh{6h1LY|G#3()jP3pHWIkor#E%dO-av%yrgY?IRo;kfT2~}Yt7T$csokJ% z*)@^Y)j?qG**E;bUC=8)Je)w@QyMxtSAXVlJ&vS9m`aoR)OJY`pV=gDYLXsa_<;2x ziZh;ON8?9-osXu@SJQn8d4FY@4SlvbCvsUbpwIbt!pVHlbbAeb{Jn;$kN(Hh9(pc> zW^4>?95k$7{d*3LK&~LPQtzCK!xd(06Wk$=)u(tp%zS!j5R?J z_MBoPW6fp*4B5JZqn6skJ|8>kW{l6gd4a9Bw!3^Ru;Du`PdD+Un8_nkMA)EOT0DRl z(cXiOQM-;pK&#HiHIZp$Amr92YiZ%%HI6#ue5C{W9?DL;ISnj*(H~zBUJjP!h_34nDvwa0~4yp*Ia0ZLf{}lXUkn}@8=_sXu!Z!5u=l?e2pj{X( z9Px&rwH%S-V?<;xJq1#6V>}#1)pi!Q6c8kc`1y05dL5mVUspEHrZVd+ktqyP_ej4` zgMF23VRI5+oc{8E>%|L^HuzOFNaxX!t=@kmi;hk5Q`!dBdF7n_K6nvKL=%acuHo}p zzo*B?(QxNl>$HRkmu#D}yB8-M{FSzA|GGwnTt!S@Um{z#^pd4s2T;z|i<@B{#(zFD zMUvnPc0(!dJPc^Vz`H9W7maGZyA~A*+s&WUk0VDu3dyuA}_6@1SDYrl)|$!<%i zib6^IRBf@=-#Y~jd#y$po%Ldt zZBiM>FFYL2d-wmh*0P5|G}IW^zMJq5V8!ttfZNrbv)MS|G1a~0gVJM#bT!(x^9h-V zA|XU7qP@_6f_vKL<_9iX+65oq+c$fs0oTTa&*E|)Zz3D5jc#N5WGjt8Yc~OtDSA`O zr(TI;4@#r#TEq0X_7{3fb2Z!FKy9#PN49f`KC}Y7nz(myl86-9LTU2$tY5RN2Jd@@ z2X*Ohtv}G5q2a5HP4GR*N&96}SGzeULYk<3+l?2|e=c3nBJvL)nF9@vg`$rVDTUod>L)RVnMwt`6cupcD_GpqkpUlHeU}3CP$mOii7c%`_>5= z=Onbx*t$`#vIF&;gRZC8)kE$6CYAFKknP#Uq~U5|AD39rp0~TsqpvR$!YHUP@aZc= zcfcD2Z;!ghe77kRGM?(p>W0Y3UU&^yA7Qu+^LK^;os!n%NG-%Hx>`oY>s9_>l?3&m zo*^yorBGA<04Uo?AKoCyBjE-C{O0gS+Yd8GE=7$EGrMh|D^>rDq}pf(Rhfh01?*Fz zkmiJ-=(EbKQpX_t+44FQ?az`H=R!d)`X4a_!ezK`LT zh4Uhb98d_K-;Ou?qaqna2icE|7QLeB}Ey* z%TYi;_m7|+bo{jXSxo;XTaR>{ky%uWIUGoMkS!8M5WP`9c{%!I^N~rp9;afvxzWF| zBZ38YnpxI6B&2Pl9Y%O$c&2*Y@|bZ6jF^+Aco2clIXcR2R-hdz9TlacRjWp$_HJZ7 z#J#~z>7T}(j?Pe*WSJb_7Sr#%{wIL+z>T~JMjUp{-I?|AV(ho^Ob1OhofL|6m%4mz zwG$7+i5b7wwuagzIgc+D5g2Ax$(w@V8AqV4;Kln5$+hSW-k5YKO8N{drt-mT(~Q(7 zvTN`+$Zl>%fdWh%2>1(;H4lD2rHa z!DCPz#i6GvaNdbi+pjl%`<;@Z0Cz`|+a#kKNcy*({(*`xvc8HyNS|CID&7WSA7ipB zLwd^cLt5Ot(}FT}fji)^FcE4nh;D*H;nz^vE-9Slzc^I=9uuG6pvQHPjKaaho&J`k z4&)-x?HsSVZ=I~WG-h}$r3}e^?xGB~WMBVQg19G#IZJGt?$I(WJ!*WZB)nc7q&EF? z$}^9?w{4!XZmy!Z;a-#mCUB}RO3)}}i1oCzpDj`9Mbr7fOu46{+4A*GORWUCy?v~wmJ-OSo+bGlFU&utmGm%b7 z^8nAE`)F!y?mB7Np2EB2^PfB<%~)S&x0pT`fm_QKYw4Jq%C*pSs;Xxd> zX)12bTba*Ji<^4yG_dF?+=XGKCbcQX{DhD6>NsfXWHgl{HNOI7Ef%0nyQXb-9KXqq z?>c;;LYuomkKJ!MaoI^Wiu`%5I{f#9E+D+Ym$sjVt~n9wVqEO-*hC~mh_vM_Kf5U; z6wK&pud|m->mT`XAC|jCH`vE#AM`r!rSTbkjvIVfKL*pBR)l6w$evYaGK07bzbM>P zm85@{jmu$kDlK_Zwu?+kQ4qcyUiYO^grmMqNmI~~zYpUUgO3Ut*z8-u{~nhxO!Tl z6zu3pQf@2w!k$`mLE!}_xlT?@6Qs4nCHKrlTjNF~O+}?JZ`zt>{YB21psy=vpdcVG z1#Xs@LpA=6;JMg9Kbd=f7mP;l^>LQ@MTkC&;c5t#&i6e)sMqm9%SHBZ%NcpA@25F; zs`NMF3yRorg9#_PPmnikWFDPM@7Gw)=pkrL#%C!YDw=@^AD^Z+<70N*9YXN z3vEqe`y1lNe5H!@A@vNwva~qP)2=cxG#-x4fH|}mGzO_-i$sr`l1@~upQX^8t>D?6 z;kAlq^s(`LS0M5SdjM`}mg)$2S7x=_N)2AR>Gar#22nYy+|Au+5!+t`ZGKEyP0^_) zGQ+ImG7i4jj3 z%mtlCq~6Cd;WW0Q^zJTGsx#h5A#VC>bwNH76AjThBfI-vqJz0~hO*kmJ2BOlIlH)WURzewBg);*Zj zoqb(`ma@!cNO-8zl}lSCNXR!bU9h_T1F$iObC5NzNng=i#hIO651!wa7rhW?A_?S> z4)Z#9@cUUacSEnZSW*5)lquU+kh-Z%zR}W>{6>Q^Kg(6xYo+L&Sqy~A*}Ac*eO~B3 zYHo8=j;<~Px@>AlF`RB~@X*>>Gs&f*usJ{==a+HJhARNQ21Z=^=x z{5ipN--fF%^N2%#YW`i~Vw=l{)M+Wq<=7W*&T_LgD#{(1%cJdX2w8S=Iig>_KZ9^X z;bz!mG2-ORrAG64=umCW%u8|AXrVxAnGIvZ{%hSYL|54gI&^ff(iIHPzaV>?lan^G zgRt&%WYDQnOSZ6~vyKluy70VBbM0v|1nc+j&?oT_Mt0{GsSLQYpwXFC5QT`^#6FA= zZ@!FhAzj%LAHxtP-ZjLx@w216S)YXGvgo&m%V0{{(YPk!>x7-u6U1YBt3P?Htv{_k zy+)Dliqe!kR|OT1;%T}X{ukwsv6P*+0hn8wJ?g6Orz@+3K!aeb>B^0-CR_+*g1i?} z3XC@WnKp@T7Xa&XCeJ0YN{W6dwwZdu=%(OEr-Igow^|L;pJzdfc~!*5@|3rk;rJJp zj-R-`S}|y`|D|V2-Gs!RydqIA{Cn{+6%jr!1 z%$}Q@!%iqA_*-ctro))bfcvNA9hfpE~gkAE&Vkn87qApg^gRWkZuN`|S_C zV%dZAtWfI*O;|Ru+ndYRPpzKYWhJI-9W$5e(a1PiVutXWroD|m@tW+BKUjraZR<`* zQ2q8v)vup+cG}LqzBA)ldr6t-Z~K!#sx5aezZQ-!2cLu91vT2z`juI*)HKFPB=0d; zx{5tL#~zT{*sZIt;Bl6d%XX})1$9#;n}VY7tfa6R;L)pTqEGR@7hG$`3TTzEeF=|0oUk?Y`R%)lLEz`GIDO?(pFQ~r z9;^pO3R8xu^3!rPkuLkBZlQ8aA#*YUQWtBQtZ2H9L;YS;0%yTq;DYe=p<9`0R3Xf2 zrItyY?WsU1)!_Y*0B#zK5*NFwbLblm#T!RWaWb$_AOX$mE%WPWJcllyVxm&O6FH_6 z@n($ku!QG+JCf7;HM_kTM?_U{{cV&O!=aRqk_hW`kRO=!}3!@^7Ap91YNP@ ztt3l^^uzIjL$t-7?evwkRm_G8q5Kvt0VP24TuF?M<40ZVi9Mx>S@zD)rfl|>zEwA7 z2)`t@q_1TvB!6fcA57FzR1uY;MS<4Z#v%vA|EP6pzvDYR2u8sFJPkB75b))P)r z+~pA1O69Zcz$z)-qmy(ndug<#Yxk-(Rja=KaYd)`Q)O8QHh$>y2|jErfGejJ=xtGx z@l^M-8Q?+w&ImpMf10aiP)MjN#?VG+MeS4&C`zu|jf*qlJTmi_BG#^PDW+7zU*9pj ziz^$Q9(5_-U{mv-4s+Swkze_Y78HeklYpW-vy#IyoMYA*o|*^kZ_s{9A1!Mq9cP(` zxcyf5cAbpM_RhQwe-f@cD9PU^)wq={K~(};Mkaec%O0cLR#SXwfm>6Hi#nfzSzh%718r0L?t8KqRnU)B` z35gbrI?Af-y(zS;<^RN|0CfM@JcYh1rD6iaK{o^kPupz+DO4k`r;s#~MHMu`T~mm! z$sJ4te4fp(AcEebNpdgitnx!FX)B92-b`KTL8)hdue4aKeraH(!r+i)(YGNWN-{_q z@)#id3wg@pQutVb!UP>qq)R(##?TY#$wS7o;StRi$~J%`U;GG$EljEpd4HxJAH@8t zMy>^@Ge%vhA*4gh-=C)O&Tqt!;#$1~sU_N6wZ2do~= zTiSL+&*L|=h3dZ?$kiIV3G36u4!}YLv@xFb$FzuStP^$|k~ZZxmv)yJ_8s5o4hwQT z@qgm(cucsSkPoP+dDb&MuqgX+GQUKF%b_>d00~ zcR=bRyT)V8h)!FW;E-rD!Ux68Q9eOayC$_DQ8cODZ?7P zVKKs=PH`So6> zv0R0TdDS!tr^;WK>Gb9dKt^J@`oSX}hi_S9ZQ&Kv+S3*LT9)VCVH)F|$P=YN+BARglkGRfy)tmc3CL`qzr+ROmAQe*nL1 zqs#st{4eos|1rw#zvDIMZEDqe;<6?R7iVLA;RA}e3aMH*{_;Q?krr5X1X(0UsP8;N z2G@6eYZc>e_v35}ZE0;0^z9aiVH~48j@do(LMc=IwK4nUNl1)OXnc z5a>W!aLm6z!){2xey^JLfC35Q()^*F6gYhQ{b!_Fu0SujZhtN*w;>0 z55{{CIAiMshvhQ{@}h_}pF66gr*!(07{{xiIG?7-S4V7()X5TP)f%;gQ;h8GugA4< zwIe9dzC^OaCX*I#zn`&%tLuxM%+8GJLR#WK1WNHbYC_&6Y4|Wq{40hEZN7Zk``Gwk zf3I_x4wWX7@I{=WNE{P!mk%?7uxgR^l}Kshsp6#W)}HLV%OB@+6t>|-q4?oTg1fI@ zfuQKql#iNNCvUgr4NXSg!xxa=Jh26EtVCghK@^UjqOW)A{9@xGXX@h$NdEz zkBK7duFD5#SX#xcM3Ge-Vyt~icjs<021;YIk(;|=o1FljAKEh zjn%@7vL%5g*0pub0&=wD%t?d5rIu)O0p%J4W3WpYTBqNrYlnJw0VAdV*dF7cM`Qs> z+vx3rq-NWw9g_@@Xk&7ZaQa>Sb_TU`S4Z-)M5w+!Wk?7+mlCEB#>3$ObUfJFv1A3b zuwP-x#FfgNvz@L0IQD3^=KxUByd!Z}@GykUL6!uHTZi718I=;f2RWq>Y3w{rFi5`1 zq0imGosxR}r=})WE&ZiGo@;pMxgO^(p$-Mo9YA#(G9da&#l|PTAWT1*HCw##mAGX- znRQeGpRPdZazaSRTd%{j>c)qvXF|td&BKeAinEp6r+6u-O#wV!`Vr3SB}H)Q9>cl1 z=Y$b5i1L`m52WrBZV@MXQSeD|;H2N7%!S1`cLVP%)YpW?`AQzV?$YL}n}!wQN5VfS zIWQ(Tk)J2hv22D{(!*Pg{BaKdd_SA=A~ut_7{t!!HKZFbhUu!39HLAUX;@kt7r|^!*<4Sn4t!_)eKf!m@)e;Am8uEy{>6TeWTnt zzpv>;`)F@1ZuQz&oJ>|X3H{lJCSqNw2#}>2$uiBs_#>PZM$3$V&ic$P~!mlka z5&VN9g4@182WUpHy+g+H1L17tOdG9xHK~mxcYh*`{iHzK#lPA7@f2j9H{=qpHqDMq z-k#G8#};QL_2T}3td@>l*{l`4W)>cyO;XVJ;9r^I(%a;uIaU}uz}J>(NVr@3@C5b^ zN4$X{4YzUW_lhl-(l}cwXXy4w`E$GojoW7#%)=UGUJ}>UA z(>z6mbIndqq$6YM%%{|}J`DvdOlRF6Hwv>q)>{dh^Txvi9No&1P<5Nlam}`dnp*3G zOHlud{;%e3;0&t}SHeunr0*`q*`u{*HesctO)u@CH}ffEAfIx&;yN~mkoIQw3myvg z!M-59&L4fYu^VIcj<&u(R^(iVpH7kytuJ$^zU0WzlF??`XlWn}iSekzkpesr%(OAt z*P0mhmvw_nhOF=9p?Pm6yl%ZX1VTj8DLa{-eS2IT$weJ#Xin^C-GIvkk4q3E)M>rP zea&R6W#oZSElrkgbWe(Fm?Sj?=eCA|S-&c1@WVm&PG!AqvAv&nbh(j%lZOwFd9^lQ zQf=f%t^?d2DkDY(Sr#%?pb^sKo&Y^)nBLf%p04ric@=lnlGNnyIvsoHhcsZ5U2=xc z3ZbqO!By*K9%r`{F*4(q!%)(JK6OpEEg3ZDC)b}9j#y85dlO~!=*D!ezIUMt+VO6> zz3;r^c*YWc|MEXSorMTK=sxCng2VWi6g}W*mH#l-(hVc4pt-3bo1a0vW%VcbkfDaa z_b*>A+#Ms4VOun1+(z6yXxD-n;iy$=`=Zki;fRHC+)P9asoOb;L9IXt&Z(K5F%pO( z;iYgeWu1LvMrhQ_n!!@>;1KI%v?wM>4tt#^-G51JWFhjh}qR?2@G7UgeP0AA?x zy>2>t&#d}b?sqS|>Y89y7QbV7nQmp?JK>*WM#`*^U~z`f)aXphRA@IB*ZczrDR1AP z3KE{uJMASJwycy05^QSDHc@%OzujF@YdeB30LhBB07OYCa}ILg#lly~4{lUr#*pg{ z`r(?(EcT!7Nxy(xc30LcU6_y6NA#FqgsRN(6mE*E0~J%2#(FD`7vreECJNqOIJ;Y} zWi2);Gl`DziEh`^@v5b67Oz%D7aI)B)8zX{PYw@%E_{qTrcv;7cUKnO(20fKkr%SF zG{qdE*^my!661$}oDS-p{0#b1@MeTaiEfVJRH4%LGkCl)gWZ@{W*BT0T|6)Jq#a4I zKYXr6tR?;w%C6t7X07M+y_s+copTj-=J=a)^V9(LxJlK?(W@k7xCMNU&J`#|T(w@3 zr)MMuCclW99@AafWx7ut;bVqhzDmow)J z`6AGvhZe|p1Z21kZDjt81+-PK^saf3r|i<+^H#n12fzumMe-bKo|SYD8rEI4G_+T< zdyqvX9^L53Jt^#@F7N2u0a>Jf3V>qn0KF>^^YKSu3A9i&S3%a7_e*BVXrZkq;*FNX zWW#T(a9YNUC32mfFXujx@OtMrtf=w&`Z*zK{9Yk#kYrSvRXiz>XNR-tvTK?UpcuCgfIa-A}TvOf)6rEWCgG8EovIyf%s z?85WB4`Nfu;LU_cb6li{%CuE;!k8SGe*ei*Pa^YV1lPHk9jv$3z4U&FPsv)T#eb73 zA^O)EQe(aAa8uPEHeJO$Q)KZSJG1E|Ou&T-KNMq$P%X?by$vvpqN&s4_Hc6xzP|9M zDOrbRn1clrER5gxMs@I>pS)XSlk(hbCs|xUc8N;JljRqe^i6{Hr+y5Q_MHbd#J;NL z%1re-@d_yRh@U0;nOoToAGJ1hGphv#1BE#>>%HXp8;YC~&y)Oa$UKB)eeu_tNs3q!7rUke;e!<>3)Re7^3Q55#D|+{OcO(tyjyn zm{kxT97yE_^uZY9Y`~%WhQ}jEn_a5lsvoGb{dN68z1?frWF$o`?d?a^&PZsYCat;@-f|{p^23go>O8{dIgJ9b*kYQ8BIcJ zMUDU}b%{y#UL=Vh-qXD1wzSsfo?)?2{qZ#;u!#ItR&2SU+L-A3c8H)chw*_CtjEOR zzK2h?5UKdSE@f?9J7vcbRD$28Z@qf{yTl!vbco{WvT_$?MYb95GE8gahz zV)JC7QBmNGoAMCHr0rlnBSLrn185mN?c;T(VXSUkH1QyV!tlJ8{?C;;R5$`lXC{Dj zDQza>`i4lIuD&AVkHE0kIr;mA?_qUqYnu1+?lM?YE&in?U1rq#Fltc6d^C+_4GH&% zl$BAyq%{>IP3W)JF*vWnI7y1V&3%KPqob+#5YMam8i>_jxO`h)0lo0q%KROkbZg1B z^U?Sd56z}z;uK&Uh;#p>g|MNq(|LtJf4?b8+7h|WxVp)G9O8{}63VP^uU$k#c{>d=P#lb}wHQ0fR2}g| zTZ6Lo>dgSTfD&o5bWO~rJ_@hDX+Nx33Io1++9(3Ttv|_va~MLOW0$tE+b@}H>jf{M z=vtZyvhSL&=6$NFCgB30F8iEi)uyc(B#H$pF+b#MtAaO=AhG2c1xbXy?S zD6*So^j(*5{khG>@@KuSt~CS&3eujYwhk#D}Hmg4N6T}1+%i#l(B30-n!xpGHflCvR$jhS+h0@ zo6QwU!?EvK58k(B00RzE8_s|uujhy zEm&8Bsta|f?tpwDf%-xEb{}&842W*+UiGo}-5D?G=BdA&-)(=Xf6sG1q?%a)<(e z^mTRu4RZ*>cDHGtJxx3D?PsfgOn(>!UoRl3JaN^SV8{YEMKXE{6Y*oCMJ5*W@v}5{ zzq7T1Gv4?Q@VRy{ee7>D2Lfn8rTO`jJOQuqJcliZvdr=4AGIo#DLYu9($?cd_BUqr z!5ujUNAT)4a14qx-Fc8kXyXvd;zgGD8f!GU9c#js)0oqx+@z^$s)&G9k=}N@2PG@(Bl{k4pByewNbx zFQAsUMSiO)lDedOQHt8mre9(Dfy~#kY`JgAxe;c_yRMtgPDO#S=uD&}%b=Kx4_W5C zr$v8WQ`#DWRBJ~kdBb0#&`-Z>@9T(lv_jJ*yS?9THHH?V#_xgGkhBMbi)s_tfA%FW zv~fPrAR_w{u~gyfv_AUXX!i24yS!#U`y`8?(3Yuc)sl2*K?9A@$oIl?`2ygN}d2&D3 zechj{#IR>Vc5P~Nfc#|&%*kmfC{mp4v%!Q7hT;yWj3KbF1~9?7JmDWG5zv_fQ;wJJ zGxJPUJgPS}HZV3$SDbL~re#=Ha31v$!W_1#+&_Ve^j^%SAl`Mh&>;qE5OJaQ<3Z+$ zEX3bMn%FU&vC8NlecLXMHL=&BRK0uJ-4T9}U^0bNkM~xih=IUWXxwbR|@)cn^VQ z2!Opi^7nJ$(G-Yygji0qJ5^p-I&x<}i(<@3T#yv~^FNdH{_FDISW@K?E-C1V)!P{8 zu^dbYa=wTm2Bh&cX~3OXb=N<)qYhO-2%y9NKo?hUOyYLNfK1PX3+S!aUyyP*Adp@Z z_p`V||J5LN#_Gb=Bj=wEngfhF850d$SNa;_8Fbd{loc=l>qL+b8cv={^1`V~84!kGMuzX1A=Bjkm^uHwR5PI>_o;0v>{ z7qh`qt$vp*%64Go*dU&?OWWx}eb?F$wCEP=0%rBUf0qS=u-wPY$g!g$HY(m-dQ8*w zC;ov4w22Ixn$uOd$H?w35S7+a34iwS##k0M%9Ik5Q7OT7?wy*#4w1;sjkzQC_0`A@ zsjzjHP!DLR9R2M137!p{xU9r(_9iR8qyXOv)|xiEuvxEJt8^QlYsEQChxZz174a4t z2-xj_8F;7p0~Hi>U0r;gu!C)G?PqpxV_IXl2b0)P*XiW`(1}A(zlG~rH^;uSu`h(>hb7~ z*C#oRjT_ZMbV48Rq)}gI84iWPzg;d<-rJP_+1Ldm_a{U>f~?)S{eqGNu*}==ZSCpF zQ6OLMhO;%rbog*LW$pKkDL9|j*4FaIunS8|T8aKM25#!Wmh?}=waUOm-y}&0hNGK` zjs?lnbOL_;uNG_$$#$G}ebdCFThYnU?H_}*-7kBHoqa7W76K1`u|sucSic@$|MUzTAMD#*(zLhSJ<<%%#uLeofSbjYLkJ4Jy?Tslcz;4jjXOQw zpNfFnZteB9zPz93+MkhWSWad8@+6_d6zFpR4r69)qyc?HBHSt4KE-pWCL*UTCdD}! z@9X!Sm&9IAF4jW$z$BVv^fCO!r^~7_^`BaSUF6T5`$hH6xP3CkApN!f53XI81vW39 zy}f8hS?P`9NT)+o(%}I^c`>t>d>eDvT~-^2*!P^awRqo6WxeWfIHWPL*Gi~dY1?~# zO}MJ{gW?9sGFCBFJK|vN@<#>S3>7+lL~fJClqgxbQ){zwx}dV&V;ub6BKePmWS_dj z6HX+)E_Epw0XVRi?Dtt7()`opI9Oq^IpN)j+=5`)iTcry0FraSQY;QJic8PC<{(^c-bF?z^G#pw9? zzLW?86KSbHwg`;1)`LmX`-NY#TV5pZ_Xxoee)j2Lfb%=iLw6RLbmsR-5 zx@cjUH_F4KMA|i%;o&yn_AMc?v5kyX3kfAat%QC-Kaok6(~UoxFMfS&-%8RRzp&TYxrzVvQ^B=U<2RsIib;<9#9mtMTJ6FC{rrE$(abQ5o!dzD!iDW{o*uS@7x%;Qv`a!*>ovslA`3+7eG_OYY-% zrHjMLtYx*_M=7qMxP$HbuBfDue(0bus9;m_iQ?74K~eu$mc$JDHBwD4c5JR!-)p!; z?3Pb-E464q?`a5|qN1eL>rU=?+_&+9_41(s@+i0UKSrkj^*i)tTgOPj`Bk3<-D6YA zWbCqMpy-Q-OeBkD)ToE+5aSpzf=mIWi3_U+uc=}OiABJOCH~;m3-o(*bR`~9Jf3Lw zh7+~YE$fPUi5i1l8sy}iOwG944J%yw{E(qM(ud$4&jOT(Zpxwn&qrwQY^3 zz)h&Dt}Gm+e$@Ga1HSFID9EQW&scY_rJ3RgE zq@%xXg{>`vL9drclJwwK<+So?T*K>MPbnmZurhSxaQ4l}+Go!1G_JYYr}6RF`3l-R z_nbNJF)tX&C}Sz#7Ak!z{1$;fWLvH?*&b1}WLjB)GFBK0VDNw1lmec<$aFzVGtC-n zQA?TL^^{>F*+GtC&9vl_gK@mXhYn_$M024sG;6EOMhD4qba6(ow&%5bP7Xw8(yyua z1lb*XLZ6To3Nu}kR*jALS_-1bn$A?e+PvG+y8nGp>z5P1_^khw zFKnd$btyV3>mK6cT%-q-)H`VWMdOrvg^`|t!|MUh`b4deLK_1K0#=8Su#Pyy4B6AMTY71+C53XF@f6?x!tw5Ebp>!d+^FClpy4h0=g>Vk;WIGXcg7=h-pG`0Nl`5%c2yv$_#-x20dL|N6mvV6`RTa7NxCsdjcj9%}`LbeW zq-lZQE4^4zacHckmDG2@QzlZ)!@r!L8iVkPs^UCAj$Z2p4S zsh2EGnrPpN#cYs}d;2=PbWjNxcZ3H{!T0eVrJd?8A&Zm)9WfZ(C5>5*N6wt4ioL3n z*}~AzzYfG2!Q_<}4p_JJkl|0R@YT1qO=;V3<;k^S?u;}3ZFrmmNY}jbt;W5+sbRDj zDkKj@)72r^Fc)|ECRs>nqt$_5)Wv>|&EP&rCU2<2-vwv!+vzd!3xbe~P9#a6IEEX^ z;p-K)H6yCd-!-YsS(CPmJv7elGrG%f;l?0gFzA;2M*+)3{_qN!Z*rH15!Ng~33p$S z9wd8d+a+>h@&-RzTZ?FQvL@*28tQFY{t8+0?*j1}mf5(8oKF~@+&N0Q1Np1`7|q|m z@4f?dAQ5*jyO|9uQWWh8Pd9*d?6MqOxkYOLwBb=d5FM(Je5pF>zW@Emjh!BPW!SH?E)4+}Qh&;w|K9T916=*Q*M z>$ObCNU%@Z+w*-k{Wh|C_<^kF_i92co&76(XK}X(1X$HEKgg7?rAijgn)u#rh|L{xpxB z>@vf>tJ$lcqUArhM~3H1*a>mRT+8v35`wi%)ClSCO`SuaQqm&Sot&9Rn~x}oI^OHC zN0XQ!6EqS1JuLT&!`{}J`U0nX<(b6&L;mUyUbfzB1no1?W!s0kPrXuJb*<}w1T|GA z-0!42r|gWF%IsPo*w6iJErc!={}>BdMPrqABW}AA4ulVk(b7(Kho`2^ zi3W;Vj~t>)oyo$MmedH}9}@u&qP{=YL!DqfHr-{-IDJ&}h(2{LrW|QvM?SCAR7Znr z`n@lXO(Zh$zPM#zA*ixVCOw7ic3iBGV{>Gss`z5+=KME;_?urL%9Tl83t=+6LG-+z z;XrF2zj#D@(MoxqhrctST7f+B0hnsPMW9nRZD@A2N8WBulz<`F0~F4dZuR)BKDXW4 zV=esBtuo1bnZ5q&2&u)5{ys2;?M=hCJXA{0&wk-r*)2Z)tmdsUIzbj`&@UOL0+2rY z(ZMqLqVlxJ0{Z!-C;e9jhj|4DPE4*7J67g|aY4GpR^Z`Kv25)b@5`@d*`JFZk{nr# zauy{!y}mkvz61OfHn0Wo3)Jd*L&G%rf$B^WWN}~b$L;*(Uyx6z_>-;m)@&`Fwd^=3 zu44x|DoFjI-rR={Sm?4=Md zu(cB3koGC1BHJtK>OR%QW!q#=Bhew&U$6XM%XtPq92#hE`21PVeH_IDi*WDcz7Yto zGSVsqWF;iefH582uP;xNGBQi=^=o`TZ?0yc`~bZ5Zn~*_+VNEI@n4W6BX?K;A=nQ);3UCtW{#3!Y0jTiHJ9J8lwen6q_$2@I%<;+g_fM zK2RByRgsw^L$+QPqx^1_Z0H#SSSQiyxhu90^!gI49jhgi9MMiG#RIt5mTrUNbVjY7 zU&+smFokL)YL5CS?whQy*){TC0*Jj!1xWqhpd!{5xatkRtT5NZ&fv&YW6PJc&xRt8 z>H4+_@j6YAZVGo%a#WCHefq-8Gw;wb+7%V{DaTCxWCePeNKS%oQ1=PQ>phO>88pPH zgWlWPh;SCVNbg7XbN^JGs0S+}p06j|En__erSzlnV4dbjJ%6u5_V?k@x#^4Z2>!J? z2v7QfWW{FOb~Gvdh}6*|=BSJf>oTH4`3 z=N6*3FOI9s^K$1(14)=NY*B6bF=Z$VF80lfpXpUZ^D~|EG2$P$WZs_Y(nhU_n{JDZ z13%Ja&tW&1N^@quzFL$dz2>IrB3?oe4N<+S%L1ygtAjmwBjvLA%f&E^?2pR;^{r-e zJ0zU%d)bL#*CfvTT2PL1p_tr^)3=9D)=%K4o9pepS7B&d;`-7*jRU8)SH3p)57aGa zZ(qJTUa_Ko^F)Q=NsU|%tA@>V`CKyqNa>DRG}BAYu3EnHI=xmrm5#&l9@^BLE1#V_ zQT~>fuGs`$jw#-G`H+LT`Q^O<*1M%kaII`#IuKIZj!={pt>Oao*pRYyvCl=HEd+ne z*@wT2g%-J8ROJCYEbx8w``IJ@y6jpzdj2H47@}72YZs5=g)q)2B@d=a{`;Tn$yg-T zUtnL+$9>`Tmqg~Tn|KN<&XW3TBg7Xhn269f0HTNve-eCy9|0vWJYxqEvMCv9xAP;vi zRAp6(WS^@*m*j^!5&?a8pz|_rF=>q)P*T}6qsLh;Zr4kZ45I>f#7^Hj)-k%*CuBqO ztBLZon~^dL`OxKv8zSJ@$NU9-)eRVhUaFyix#*%+-y8aW%VPN7jKm(RbtgwYd6*He zEUL)@;0PWi>6Mnr|XPw4FTx8^!FN1b;j@)pqhkG zy^kL)wlE~XTPyLV=!24P%7lMg2dFr(v)sQBRh?AmVgz9KhB4LouviZ_O^LJ6K4Y(q^ly5S@(h z`kl5Lenovxh13#LoD04Rua*iWA1k>O>HLlPe>y)e=khOR_5X#Dwgi+H`5!0>|Ie;B zF{~IMq!guA-d}_&>z5-zjOB{3?MRy%8#dIWDMtxK+*jW3RbwamW${$W!suP0RN{6~ zdVuPV2hYn4pHh(&&qJsL03U%Gu=KD010V(ezB+shc+qVUNtm8?7N9`+P_-Kh`;P%1 z;P2jT0tf77WRdyqL$`TRmHhfrhX^vIt96{(o2Gx2 zq62r&p)`)x+q&!RxR;ykc?9Gx)x+hZaH zaCxVg`>SRtuTjFqsDOi44buxU+(>0qnf??%x50|E+QEyWMZVfEwpd9#RNf6o_Q_^( z7<`gjb5!@`W0afq#vR3aMRshPxRaX`akH0vM2fP*6YI8yy6}FMg&o`Z`o5za*89TA zbd>kbnW)b6(Ub}p{CnbuqJD_6u~iiW=hiB@XnH%R2ML|`w71&pNtr0R6T+kw2DNf0 z(rcU62WGG6c3RctH^Ds>ewXhj{Y`UG1f9%Jkwq0@QHm2XK9?~>Yd!`X$o)w}Ma$?e z@|SvZlIEq1k3B(`^#78#xe(9|XaoIdG4Np+Vt1qQQUJ{kMP&ccgU?-p7v&Z%NhGm+ z%sdCsBks%9Zhv@wLI1DJuPBS&mXt72$Xonnj-i9cPj|TVz?*t&WyAsR6+u-2D56^h zTZ}jq^%89#<+A?iH5f-Xv%F7#N&smG4`&jbAjrsXo~XvuO2Gmfbvj~jEG zO-ao-Z$f{2ERDP%cF-~in`C4Lifc+FS>d5Bw;^r3%zrHT(Kx11c?F*m$LHyABFuZ2 zm6XA$d-{u2+xRx^@VB$V$`!CMi}LH4SoqGk59NF4O`J^@dPJ3v>)!-izr2p`dGIW~MHt~0sGktZm!R%oXZ#~KaYgJ_ zM(8Q89qeiKu2TR^+$@L1x82F1c!jPomR4V@CB?Z`Yt`zhtA=2*w6QHg26=JtFKK%n zlbrUG6g11h?(}$bb0d;KfxefjeY7TlY3ONJExFnUWFUzNOHQ9))H^k%j$8*`A$16l zWnvuKWQsA~g&borB%^h{cMz`Q$%A*cTn^Pg8J1oJZ$D&QbC!yI*lw%Ixl?c6nY^MR z^8L$U7u6XZRtVS2ha;}P$QL*s@AcVCzBS_6yMvl3h6O3|JMoIrRTR~>@=%suuII;m z?yCMK_Facj@Ns}cRD3tpv_IEfivJ6w=b{cU)gx?TuvgPlUQ1FtaCrUr9=FpnXx9^D zkKn06(IIwU-xZ>zE11A~=^99#M~ZQf4^B>0b(;`pR@|s&zx7W1!8*i?V>x+LR4pz?yTy}IM58o=({B38A=eE2)=ZtYaknZuf(J}xpV1^O278K{{BN! zr3`*Y1*@9Ae9|VHhZenyITl$xsv`C_HcMCaYNYocWNd6S)9PF~N+pa2tq{*F;m1=M zbwE+NNDF<`YmdTzI@%r^1P4zl1>FxkIFh@#2eFHujSx_C2tVk$FR|O-hEssb?!P1w zkxETcn_7<@gQPExbFGY#B_>Eqf6(9N)OGv4t|?md!>*;i9K1-Qk70>*nA87R{59#Q z>t~1++%O0sMAl*dp14_wFyDJ{Wmk^vO2t@R9yp8++y?i|k)eXMe{&{jO?r4tb`4le zaU^dJJk`7j3F*|csDAH-Ij2Rh{zJ~!~bD^G00xNrv&NkkH`@G~7s%mfg6wDYf zs)BG+FeN_KYngRQaLk)M;@eQHe~(lW;kQbcz{w*M)5i&9OeXYKKoxFkGI$#?0VjCs z7HMv{0R8@=lCW$S10JVfnBNz3CYGV6?6l^kCKTgkkX0gpAWZq^#$@GD(}c)7oLO?5 zAnCn#xK1=rTofj8x=ZTLJv$n3z2Rmy$xWupTMbw~-{Sm{(lt+RQoz|wWR zgV>?x0HuR&*9It9`oYoO^bV@=_)Vb|{9p=T6|xKe8eM1N=TV0kB6VB_Ev!LlBMXLx zbbLLvKXe--+4-N@7rt%vk(7>l%(J9|)Kr{7KXQjdMoVNXll)4-%ky=fb8nS+1G{=H zc|u9QFT_4jHnD#6;$b(FK<})Q&RoZM>C&>|Y~VhHnv<=o;pf0k?DS|S4%OI+N;)-` z4CW;GJm6@UtQ2XLj{O^fRl&_>uDHN_tM`#wW3f8Xoc#1#(KdD2+d|?cZYBxLy!e8<~zZI%<_yx4sPIN95aS zjF?(JJ5_w%lLYkAHd(^uSdd(S@QTZ%!m4a%pi|2`x43K~BB*>U?^XYmCDmDBJ(kDn zD5YDfTZ0Yng60C2Swee0)>`g6PX}xBPZ*b>b~ET2@Qo$m8(?EEX54~+ zBMsA^Qlh>rJp%JreY2-Z+;MFlYnnuc`!r=fu7~vN9@eiGDS7V23W`+Fzwg6y+b$Y^ ztzAfYxCN6RSji+*h?zD`G0XNDLTe-I#{>OE*O#^D`je?EEM!u|Tg;-)Ru9y9ups15 zgP4qj@z=j%E5513FZ94*&x!m6&5N&n+6GeN#*0z6z3)15KM}P=@EsbIaaNQavOnvm z(>wj$nk}Rrm?tbCye3Q1#ZpUk(I4JMVF&LkbFHANY^?0$)f*VmKw4O|j;t(*-5y(p z7ksAKSP8N71iW~Y^_GSprZoL! zhSjx{$MNWs*%-cnx#R|L*{7+Wh5>W>>KjM4X!e4A@`HfKHb#DX2BO>O^(dJr`YlBc z&bVZP#}e>|FV+B6ShlaVY-PochDIv+2fLN+ZbCr{2;@b*wOK3+I(WAUo= zG9`Lf9!Ki!wk*!w+{OspM=Q4?G7H;})7@v!RCtr8x(RE#CBvWAI)3=_*e^PRis9@1 z<|`mW@fP5`SNK1kym&C4V$@62_O?%m&a#iHI;8V)KkL?6|8Uw@vISc` zn6>GoiJ+Bbyo~*DptAIfcfU6qUX0UL$U@ewfB|`~*~=RoCF5H4pM{iBFnSA}--3J0S z%Czp=i+gF^v+@p-{wi)_h3hBb%#pTv-{FutKGmsmuY(-^w<=G(7k?=2-OmbS;kcMlr+J*wHG>>w<6fXcM03fcuv)V++)P!)CI}Yz0rQgU z4z7ShYKLx`5CueTmi-~o0Q_Zq`>D^-K$bkAaYOtGb0KyLvw3c)0Xli1K4Y*s7IzIq znW*N@Xcc*+JCe|t+;e$wjq$)58Y*MnF{C+uzeRW!yf=3}W0*oaY*V8mBRBP?uTMIv zNrxi)@k5h)O*KF4L7FBB=+ZU^7nukKpJ-pjs_`uzyS_zrIoiZit3=s1%AqUL*0-f3 zBgPyVS@T%5W14JrY)H9Wpz~uFBpuS$IH*=7>c2715%wW$e00p+J4mRfzjTkId1%|! zRd~QmazJ!RFDud42dwB9c36v~>7ze}c@*|nsd0V7X@sAAXbKy%)e|$#XD1$^`$@e1 z3riNqEr9PX&#BFAW7+I1_V?rgIw+fDMo(Pl=Qa}&& zZsU(fCQ~v)czmB1*E-v?Zeq9S*v+)*39jF0wG#e}2C2Suh)hW}Wox7YLl&0m8<#Lf zgfR1pkL0f|9qsgV@Z7uPLLPDM#;w;It+&S6BvIl1@*eqvA|Y!n=BJ+u>mW)w2S4`+S(sE~^JLtAu@<@6rqfcG66IKT zT`w}r&s%s)sZXl5)AgtPWw~ivhXx4bW|n#@&M;iHD6nRdB0kRRf>uSiDp9B&lj)mP z<3`NkmTLT-r|pxhEa#~=@XencD9BA+Pdt!-?y2oS^`-%rsqNk4058DyW3mpY9!?~I zQGm--JbE0X>21}Pb*1?iq>#Ri!9tfU$gh?CGtQ4j;lnQ2ynh2>Qh!0Aka2;!)$=D! zmuvG2bS6`*`20AO1WSqJUFbe?3w?jl}9Zc=r+9gLZDWkIP5WqsTM{z(O z`|YmyE>pC9!*H=ollQ~^>;liM2=_$j}1+b_wDPH%Meu3h5*f8ym~=q2hC4oZ}R#50x{mg=GfoL)eXiY z*~9h{@8$eZhKS96FZo*TU%+ne>IA$jslC4`qJP6Gq2wToz3KOERCh#y&CTv*+P;>Z z<)MAoZSy!Ak4VMC9j)nYR%vPZ# zHyG53ecB}8R?Q_9n_$f!k8fq0YgLN$BlQi{j337^N~PVs%!~YA2G~&#f3eiccN+_! z9v27M!>qx0hTf(DWOg)(XkBDLDsK>zmU{6hp&3haYHDo0_msMm*Q2k^_0$7uyosgR zB*@*sb3g@}hXF1*Q5VK+!~`=fYP4i=FYA#w$z5T=T~u$kYV8EO{g&aNW&E$%`O_^L z-d?q5cDLD={(w|uh-SL`c9F?{_RBMyOhn?`?}w8^-5FB2XU-R#$s?_LYN%NvTUWTe zyf_l`*@`#75#po|{F%Ey-CTz>aIzX`L71y-gwA}JE9m>-n`U{F@wALOmn)-|;0gb4 zHistfH#vFn3!p%fNSF;cIE(5o=iF&=YQs9Ntrr!7FuUCE@1$>CZqv`YU-uKy9$Ng- zqxk20hcva;h+VHYJ_Sb(Lt=BrYtUc}1MpvC0_NnzYEgIj-R1r3EMKySU=!}Gdq%h- zDi;P9PdxZEmW8nO%{pXSOoGAxvwb z)C|6GU~9j9G^JG+YS{=gm`6u>Fy<;@VMR6ru!Bx{d?-Yo)Np@A3NdmM`@{MJSJGuG zlliSm+J2wn%B4PuwTR-*OnMS_?*>OwV^TaxFw~OZHE@>^2hf_T5R7yufXLVlU0vfG zkEVDtTgzD-;qms<2TM_&@9Eo3seGoUYN7LUBWn*Hh0V$%luaFPZZ8!(Cb^~s>SI_s z1Ei7GAu`fXAJ;2kQ=6NBRzF*dRD&eeSlmR`twcStL=AOc%|6(?-@4b)z4a=BJu@~$ zXXNUyKOYe1EnJ0OIm>?%MAQfr9Na8;gJ#WEuY<90+GYTLEPy*!gTWSJGr87Ai zpwn^HeZBS%M1tpr`7Tm^YgCRLI-imcybhr#8Zmb+`@aQBpOH|&mTjOB7^VFc%DCDtu zk$A)U=)b@n3(NcSQFfDbRS&X@pL4_76N-`_=LcbXUkNMI)>W7C)Z)k;fk}big?BQX zh~3L{R7@_Kvkg_UN`PU5tl%L{zxr-LKeW^~OjPDi@rm-u-Oz0-61g@ezk5jmajfCz z& zhDQ9v?PiYJpc-Ay4)hqTyCaiP`$U~By=Y4!;>=&h8WU+u$8k-nJg+`}8nYl{)d8p( zLEZ)^Xg8o_cktS}qE*{a{YXiXIO1hgN^d-T!RXJP)vk7!#uV>*LqZSO`c`&nk|QJ| z{lbm?I{n>&DhL(Vy-L`@>Hq4kRz?^Z+P}u&&)JBAtW~(Fev-Q~p6ILC9FEGhpG|X? z@}KHtD$q8jAzxC(Z;J|_b`k|9GH~A1p_-A4msCFyabE)PypwuX_>etDA2DSXA2yh% zANxO%tvCwVN}d|^@^a+ntFf6l>WMTWH#VSONwn(NI{g4qxt(L^RhZBj&EiZm(Nfikp!1qcM1Nw9s=D2 zF&3ssnix?q#U6~<|4HfIzKp0f)Mtk;T6kIA!S*)C%=W%i(KQEID1+B0$ZL>4aOc?e zh?E}9U;t{Y-+dAaBk!O)SS7vye$#J=V#Ca<#Ffb2u;k@($6|P3puFF9_0qHA z40jfUzOixv@X70ACS65b>Vnsy5zl%RmC&lk(sL8K{|Y;N{Ta#C=;F1G0?bi$8nZ5g zm!V(e=HrhRGrWEcFx_OK8#TG~YxQu4qMGNDiRF;I?=rCa`~rz|r}-95GxKdM8Z);A zMC$~aIcSHjUr~I!9CNu+8M|*IxG=gwpe8n*Umbd>6KTOtMjzCjEGt&^FvGnnrlvk_ zJKOr$m2bAi?h`k{qK~H>E!o!CF?AvuN7PTTchyiHyi>Eh6BgdEO`~^c^a_|QtM1fI zVBz1rlt;<|x=>uV@ha*~n|Mk0?&2ccq}-^DOmZG6TT`9l%QUnjn#dm4HZK8`O^p6; z!6Zy_{oP*T)bs&yY--lLpnkrip)H1@H8H6WqTkl{#HlJ*JIw?dlR9Zu_>=q2)Dvip z%Y{XL?&;5sm6lG3SaLA3wdUW?0L2mO#L5|Vuf&;!@ky854jtTFpkeR6AoaU`$Viv) z?X+9F4@(0{NoBMlZ3#udGv>k#lNkC*=uETKeZRQ|YGu=6;vgPY*E(3l$9ntbN?k}% zg4wG7m82x-F^gNgM3wgx)NY*C=*7o8f7Ra=GA{^Szg4$4H?r7rT8)Ix9t#Prf7Z%P zBhF1|VUFs)3F~5EHkseo7P6%eX^zzo*8b8d6I*}|ve6%C`WE|J&)8ch$c+n!8SY|s zdYh`9)S?P5Mw853$~jJSuyy94(o{GQMPZ!NPFt;}QSG^3ao(?KJoAdBJ&VWNhy2M7 z%_eq&t>2#!g+N1YG~$X;-EVlHe*6_=wbXp~#s1_kSw=~#t;H*l8X`c3ay*#PGGIpu zxfxF?yldGsX^-6@au#|MK62DTajARz=$qFwW}U=mLUJ#kem2SM4?!CuAnVjs9@ZOu z^%Gmoe?hONM&sKf3Y1(p>%ys@M_azJv|_K@TA$7+)zPJC&2O)V{UN{lICKn9>MZ@m zP=o8R(3N|074Gf$vJ}1aey_I=($ofBtTxBrN>K<8wq89aReT#^PWfI~d^7Bb5XOiW z_sAQ~UzuN6dVLx7K*w+5R!<~%wS|nV}=_jPdi+i5Gi&BWf(*&7D zY6o;337+VGJDyovc(P^#boap1vo$UifTcscLTlbp0= zb?^IDiL}yGayHHRvVx&0_{}t+N0Ez`X0id;2SZcV8R`Sq*F4T8tvd+>4Rc*Z!r$Nl z5{+qKq9wD%ifCaKKl@o%HGYj{l*8cf2Lz4S!Q<>~?M|2OKxyI6unCb>l_!D3V0(*z+v=e9dZo6PJyz|4AzzOj2zfPM)Mk>f}^ zl|NFn>C2?tE_c9A;H-H*DPp|9X%mdjW20*nZom+EUK?YWI=xEek<)YgpyFXMq$>yKF zU_6Aqu4s()2D$#*ox$`a!w&)X>omoPwX7A|<45LIOl8-p&)$P_0!?bsgm7x zGr`&<;SlS!b~Y%PO$T-LtSAZbP@G&!?P$=X$Rch6-ZE@hkZ7DdKjzv)8<))g5o*^9 z=|o?kPyhORn<1aEM#$G?BSCGDea_v4r6-&)Byv@E9Y@rV>H~2h=$BoB z2U>?{^u}0zO4=P6X)AsI^y7EehY|ifDB}(JI2cVmo<9|YE7-z5Z&iP0m#iZ8d%^A% zJHOG(Kt}eTOTV@rYkL&a4hf_e>*@0r@*D7hEoM9>m~#v4$olhby&8Q@Gvpl;$R*m! zJYx+^V)hk#V+!$O;ulku8E3(LOqv`05pDWD7C;Z0sFU-%pUUCIaQ69w`tMJb`<__y z5Psg|5aLG$c7}1V7y#!qg)Z2`WKqPq7yWLydBZIaaW-J40gXn<^tt89BYD}B^k6;{z1cjYSNpQ4XXYLW1L zvvbpSlN*l4%k$P>b6ClCS~H~vK!iFt}msVrM3);rR*HG{xHCQ&W zCGIdEnTR)KdtUwvX9*^_zjUgMB{vhw!+V~~$?wvNC0#oX_9sMC{mKR%+!RIS*|s?BGa^Gm68le=Lq9qJuZTb4M8R87{lEVA)6UzhE@D_7G#FAAkO{g{)ZJXu? z(H?O<3|)|7^Urtor(%VS-bBv7?rrx_u2mk!+?pCjy(*N{F!q;y1F$%km3(BQv%|n7 zM~)Ye6AJHO^=S^3JhkuUGgY%T%v_-c1>}Mp*2DL{yFQgSB&7w75Az z?-#~o7FNO5fmg;K=V6a-b34q|MRD5*5S}-NmAM%BbY8P9_6K&coqODHaxC(Yvo5;8 zPP4!UW&3kgA+^+t!y9IQBh8csOtCn!Rdd2)9=@@$_+r|4Fyku) zjb0_sRxd81BBi5H+d(TlaYERN39PY_R!V}r35&mmy542TaMLRCoC)+*D5X@7&~-pu~d zW-G)JUiJMuDd1}(4x1)U?Pub7E#jbNI=Pi4nWN?>v0!A#Z_y}BW%lyqsmbRt8t!<( zQS!a)kJTaRUJDCU3qiXjEYlb&^UIwLQ~tTJF#ETifZY_wmVIToHRQpYXCImppVR(% zCVkz?q)~r3lU3xH?O0-PS`H9wOzv#W*j)r^b>Yj;u$WgT3vb^^CV+{WqJspZiJ^@6 zahi=ZZX)G0=90SadzeYgzPvjlwl4p^Gll)6)k#|xa9YYwraDkBsMg$dXBKS!EQT-D zZza|q^Wni=-R5M4F}}J_zD3uFk1W$gp2hkUwNg=WEm;mHNrp$G52k{rZ*^Q*8s02w zY(%OI{t{WbPJS^gc6ba2$!h`v$HkwTM2U$-209Y%6JcMHv$XqyHL$lG#%lF$nK1X@ z*{I*uPg%ZxlssgvFdp3v?;mc_D9Bn(*v;*IJ8kT--`7r=;Xs%FL(5?Z=w1_hNBGN% z4oH29+0Dd@#B#4-ZRB-p*Y}+4P8d_-4srpswvf;6A1o6Y4B5XQJI99yI0|Wu78mDY zXI&&U4IEGiB^EnNV#!>MYqamBT>kD7tLP&Z>P}`UWc<;tE*m2$t->J3Je!NCY?kdd z1SlNU$ELOHBHF~yS;cT4&fTlNn;JN*kCF9<6f1Z*ZSl1ai~PLC)swbjZH{u3(@MR~ z6ijSm%odxf-~E}Ori8`l)=V|gkrO13zX}Z70i-&$?Y=$-w(S{5KC9tZ!gE+9&Qi|~ zKJP`26)+#TWmAP@wEsDVQIt#u;Ce^(Uh0GA8^qyl$uN-`1p>jiiD%7Q7|UBMT{I_P zz0;nMad7%rW~6TCKA46%UDwA%FGQ9W;;=A9s zu=U6xSMkMS<3p;9_B^9Q*MQS!S7X8kHE}v~X)7}pfnXEC1~1p@8fo&TCE?fwnJo*t zD$~2KA`C1f0h7=NYptg6B>%Sv3nSIxj7@M2d8P7zEbd-cyoNJL*0T50{=V@~;-MBz zO@2Pn&j7ql#4($e=K!4sM-qq{KcCn6;Zu@=fN|&pe-*^|Wx`$N1(EOm?3gTgBjw9z z$JaUBx%jxFMRLLfigpUnTJ(|p6MTc&c^?4?PC(h2tUq9KjHMs9i=V=*faff3asXZ6 zkYvwd4T^fQB?m=QvaCq~X3F5rcYv7^LtCl%4e3uoy;n8QQ7*{#FxBskR0{ zb!Oy{KmLM-!a@#!c~&)+59!-~LCFs2vbCll@jm~A>s9ipoxW8dEd4+6Q+fcWz1OC_ z+-P#xrG1QLOaTi4TKpTKko<t*=XXMGr!N%~ z8j@#|tj*y?zfuhKpeoEPl}7*QuuXDn#QnVZ(2+#+Rxn_8g%wqdg!Zuz1w8VmUR>Yp z0pr?U(r+5fAHWuUJ%1FL)gik*9NgbGdM=7q^k}nph0xL9d<)ik*z(ql=T;5vgla`p z88j#Tg5ipQnY_K!CS!{~mFzQ%#VI5#zbQO&$i3yoSe7P~Sy=@M@06@|J0RoW1Q`4~ z3&g=MXzglv8($uZsV*B%@hTfURfT!vz2(i{t@4?V&oMvN7Sr!ZILSAe^BlC)CTvL$ zx>bza2_W)rG(hflEt43{mA6S3@23_1Y)&J3XygD<*XsHD%uIxoZXyZt@buk@GcKL1 z=)VkE|1S`({_r~n5FbvhvZ;ZoCAC@k^H4jCH{?nQgRcZ6Ac>=75s-c;Nt@D!{NV`= zs;tvD?9w#s^3(LH-R&zHY6Rv~0xr7zR0w@27@a{#kLIs}qeZ)Cii69ZCfKSJrK_77 zdn^O745<9@!gqA4lw0Z1Mz$hwuZ7EiT!2El$G!bu(X_ccj;5yQh7n-^uc17>0cLdd z0fxt44=_b?BR}pGZrsUnHqt9bLEc{}n!MmKGZnLEZPW+lr3!M`FoDnMI~?ws0=QOe zZBRdtZ?d2i*$Dtdu zPP7bOScIuVv`yq`;XO(f894R zb`sH1nNS(H$WmlUejk8+$H{0Q`j*Kz=Cxj?kMd0d!SfWU+t{Q~Ns+w)2caggGM zN_7vNO!gawC`Y^8Wm#oLnK;7lgV-fMBs^z%F##5ZcD|Bd%b^J9)`t-SV*=Ut&D4?X zhOLwOTF8_br7v}+>;u(Vh(X!)x|Tj9NpjTBk6SOY?8bZK7voG&hPT2ff}X1tAOkJD z4T_WoG5~f%+oXXm6Oe$uM_;(znWbzfYRLEDFGz_xUd4?AW_3+1u4|~Q{fkBMU(LW* zoGb`mR!n!uqQd&K$dO)O^nXFgQqF8LFo>PQAwYY6XJu@sB18e+qG^!z&FJq>oX#7@%N$%5AENq6H2UikJ#?AFLB&S|%! zVAo>$BOPH$p5H?X)^;Hy&DD)l6#;`D&UvrD4t3}Rn6H1+L|Qc9vdjAEd1-rDi|o6p zo*b_#K-ax`G8HtC+nF6dHq;g8G^US9?P`bAr`@>L3q^1Wguj6Zz-zA%dQV&0)!~T-+^1spcmO*i}U%M|BAV`ql9^47;5FkN9 zaQ6h43>q{81QH-P3>G}NLvVNZ;4p)0@EI%v3_QD^Q)kzyecu21aCW`>LsKk5O5gxiCRvFCdW`^_o`X5C0m7vp z@Wh4p-5Z6HU!+%^@#XrxP9}rqDDD9Wj2l?|z z1P@$HDza4FCzLmtJ%I2;8tn3pO=w=&erWy}Rfti5NBDU->QMmo8mAwYYslvW6>~?}|OwD(shI8(M z;$S}9g)viKCgj}nzDxhoQbSm6{{0$6jG~zL4*aZ#__PT9C_RC2G13`jjZIdd(~;^c ztUpC#(x%NKSjyy{FPwY(_x>D7qgSFMsc?1KeKnnP4yMcp>mid|YQVxBGgW7nF9k%T zvop+_AtJF!bxy2JoJWfOIumF8k}f5@B@-KAyhmOxw=cOyJD zCwIG|V$!8YDml8X&$CX=j!J+!2Uv}tRqekYFrZKRn3tR)msXo>@fS77D+7SEVAEq8 zaH|60>uv(Spub8nX69C+H%Ye9B91hm0mxvF>nojh&(fZ?7CMY4002}DWPPI^_-x1} zx9QMql4d&3Yq}x!2|(yzlPoreuG= zHc056VSQkN8h70x!tdPTCe-EKa5-d)bPy%>h zXpgDoer79WX+p~mUK5u03)$|?`dn$(ioCr`>{nr^JV(V zWh|pmsN@WFTe<2qJtPRS>~)tJR$@P7A&^S-^c)~ImVhdHm|lMiFcpFxHr+JOn5!O_ zSsuj_B<6BU`|aD=-Oo0}Ketn`*Y0gj4giJ^cqpk3Wd4n~>0VWC7>b8s@!kiX5#FH9 zb&&`CS?KVd-9S2Oh`{fOt>#n5`P5-chRo7c(xtnNIvIz}^S*P6pm_$a1p&UZ0(eRP z{>sgoDOPLTj&_W2JU4kl9ktk(xPH&_T5WSd)4yR$9b8-Kd|=v=;S$FH!<)O-SX&1?F#)aH3c^LW^8z zdY?c>T4`E&`x0<<$1)TZ9R}OhMoE5~O}j+%L5r#Tf_EY--0sd=rftftF&;>=!tmB( zx2WRiPF{=c!CwQxXppP6^zppGjFz&EWMoc@x3+JbCRt8Nm`94mmDfUlXY3Hd^Aj%q z?dsm3wWWUcWEi&nYMh3dA8ksVU&+%f2@tZjZhTJ#oJatz;^$G*O%QWS3TRi%V^Cxo z|GoGRTsBG`D!wm42HFa}^eGG1U6B-#g;jlN3mg~UZ)I}QN2~qi zy;(t;;^nJtAYim1^dXSGF&X0a{#UrJ+5X`?M0HwI)4$RYC0_0lMDLTtoK}1!iC?VW5`Iwv;rdNasg7ba zhpkdFYYDsaWq}^~QE?H7Yf;KNDM|nAqB*7BU?-Pl?_4sZZ%?;lOH21WJIEPP%@+5< z#sTlfx`e?tPXXOH`MChvb~ZfoZ4&|~gx4FZu_b3oy~|OgJ<|G9>h=OPZsM$(+BjA3 zGgTibRW((GhZTBbOP3gcIx2~yn9|}fe*a00{XI)l=p2}&R z&)b5u@w3P7s+jH=c? zx-QWpMtCY%6<*bRb3LbI-|b|1GkDL?lIbA!Fg#ciz7;G$tw zV5uK$FwGRyc$;Xrq1j|sbGiU)!R>Fz+lp!XUf4P={sT2M8U-i%zW$;a5g!R;*&Y=STA?|5NUYDC@ z=NseZIs1+%wHM79Zz3LS${}T0iAk_LRK@Qi4Lw7ZGG)O6e1S|pC^D?Lf#O0W$C%cL z!!vFNKqlMf_>;Jj7pA1)P8_!|VLDfTCj99hU=#IbmM^bK*@>`U?b*(5tpa}5mYFAN zc{96%4j#zTJ?~n>w-fGUP!JZ4bCEV-?U6l!y*~^Z2uX^uox75}eZSFq$tG2aVKM2} zQo5{=P`7)KGb}6Haz2||qD0?oLZw_@c<|viUTe0_Hu^x?-22FHQevV4z`A#;NHE4< zm5eWY=2tn^>mDugd@f^+_q@#x!TNN?fVz!;=&H$n1%`th&~ro``n z_RZwj2O zCVrXW0d#^{R>5j!<3&;0FRp$=Y6oSb2M}lV@aE7517K8)|2Ti>)^5b|!JDyX~YvGA3%{dqp(Og?5mqYvp(4uTX#fO6-?H?g2aG3@N}s*||c6 zajX_RIgKCZsbvliex`+%kyo}hj5i8$_FyzhxR^Z6J-cV{pyh5yZ8%G73pB7@U? z9%gJq!+ru{JP7U?k?6D5pth#cni~mgzXFmGrdyW8v{2;mcw$UQNKXIHmJNGvJul0X z>q~l^i2c@jvm&c2!^97X{qrp7+rjmz;pyCXxR`+f;ROVDmn1Hj+pue2G=Hr%X95P# zjd7FV3U=@=SzR|?ce;3by8G?G23&|L+}tg{-rT{Fta<;CBPT3A5D#qKH(|(7>56Gm z4TO5H_Fy3EH1qfT#mW#`^)Hnk5STF?ocIFlc8|T+U{aG8>U0W1kkx8VEIpPT#Ku4Bg9WsYXngOmfbQvJcZ%cVy7nzV z>(aFf0hERM$3w#au~LqrA|Ng^RxbE%f334(`*O^8&}x+NdCpuH|D$TeBW@$$;C_Mh zy2l2ZXCyN6@aC=kh@Dfjt>OsTzmdE$(oSv_mTDfwu!eKCF=aNN#7KU1(gBFqzuvD` zk0V`-4~3$!a`&bPjzQUH7Ge~kBJh#{LYIE+G3w^$ygroLg;js1@X>oDs)hek!26YG zVBXE}Fk?d+3W%dpLNf8oQ65_n%NXqdN#iyB>mqX^8+52Qi=r?KWeXN<^#VtTGv)KD zvK#>^c|nZWNoub~iDr0nKO;qBE)-|`;FPcu#nJKGd*qQ3>ZjEkgx-O8mIcjeWlL`U zd*;gjGu1`ftzp%C5X)iGJ(?OQ%8qvzQE14DM?7+GME{OTB$yBAm}(&fc1~S@41q$v zf6(GxW!Sc5u>l9vp0z#_EP9sb_!~wn?BPMNpF;J9`qiw&wR|Ll2m&We=ESBFJ z>$bg@CqU*;zn$@>t@1Tpvi>d79^YW>X9`?+daWH_)k8JP8KfN#j4#S5xfNscGy>7z zkfHs**pmOgM!$?dZ3jeDJohYsdeWd1=vxX8EL?g2=MLDbCuBPR2M!~~dnK?!2T~o5 zIV{ILXWf{o4WEuUzjiS`GsvK33x%p_OXybW*P*?7?~z5a*ifWtz+N`&GkZUAG#@J= z1zBO=7(oHTnXe&0ZM;AU|AS_2QS+E)wbXZg1avYOPb0S7U=G`M@lhQHHhE5W;km^s zg~JR<1htAYto3i7lB%(|t_c%_W1@M?=W^B@W<}uWJx!bKH$>)FPVeO~595#t(4qUU60_Tg$c?iG`2oPlbnS*0 zynYN!6M`H{0MsOM=HC53buIq4P?I)*2~z)~G)?oUy?-1O<5O z7~|bhq6qTG#^9Y!y*^7}gzHa`=u>byudbQ2dMgdIPZ@GmZ`v0s$##}h36g)UMY{2(WaGJ_eyT_n8HUci}CGl+2xbchWhp%e1snOgfcdmQ2c!K;N*{ zdhiYWtQcvDRlYRshU<@OMd#9Ye=sA$MXGG3MapPwZ_vFdwmNDOV|X!R9nqHrU0=3T z1IaA;`fPi`*kc)1Rg3c2ns~V6ZDw9E;3M#N7-MT58IbapNjDA%hbU3LS8EU5{TD8B zbW$bZCdMk%J{d?;)q3`QcKmSeb_HEEgWHlm#$Q&0ZsEE001sQO5UHZrjED%86Ln^! z5SfmkplRT8DRJ^0{p=zZ>*m=DO7#wXbn2yLx*TVE9}yVzUFQ{}*yBJqo{T#n$CDHO z7bzOH(Gw6c`1C58P@rEA*gpSlbImR`9i{WMQ2%&gc6%}D<$2ExdGq>W=mJ)I0IU33!+%q{W}U6G2M zi6rX&?f(UR_%H7KU`a-DdEla@@v~O-ekzjgeJXZDo*_O*UlK{KI8IYGx+!UmONDku_Q)2r6vT`Cx_y31 zC@^4J9A-m6qC_4;la@lqjZt==Dbw4M6#8@+hsnMLK#WD`FE-vJY0qC1Dt)$Bb8}_Q zQKS3PzTBj0UH&zG%YeROYlmivOQYb{ut-vL{4+gH3>d${iGi!Ny?bsy6!-D8Z}2h3 z*6l}_-zDQZuV-P|FrB_25>ppJ_iL3_I-dQ{LpFys1q*kX#h2`g#9uKKk91e%8IyZe z#jy?^Hi070!I&-jpRw~~Xvwz8UOjzTVKM~nPsQw!A=^CXIO`^jHwFFk3M8Ra^zG4ZN*9SSf8_)olEH!%NETxRMecTV8OQ8ZDBNkW7QfhuLQFfRSHYUj z6Qcr6S&_lzAQ)eR#n14OP+T8D*j43^8)fF=;>nW1uRi=jWjZa3F2Pe}mF~IM7Qo{F zymiz4F^6`MKlY)&{>OIRWsVR_&w+e3n-3l%yLU!HQvNboqgV( z`gHe^=g4VXbW5*h>{d4#lna9L`$yLkH`Y1pxwB;P8&DG_KR_;|KKUdNgP&4}KoO_fUmhZjFQ(MPGzWL=6EuALg~A&7h+2MQ*EoTWw8$Oswtp-L-`q z=?YEr83VD6{eYZEkVpRGc+PWVn9Qt&iyZkPtx{uC$JdEC4LMcYoV7Z4C7y>$9-FgFj2Mlbw&f zk=ZA|)Lht7d^pKfSju_%nSp1qi*_A(JQyVQTSngLa}9rSZOKhQoy@EV8&#o40T&?y z{SVg{I+zKsMr#18?+K%eKSHpBaVt=1+;QPrQnQLc`a@$3xxeYY=%qHsOY&`f>4b1= zeerIjTc4@?$-$<72gg=+VBTWY)nQ)RpcDj~qNr{bw)|@9Y2bM44Jp~F#ncFv-A7xR zzuv97B~zMSRCczR)AqJNyvj`L4#laLqByU?nQ-py8BMMsYU0>383G+*hG z*7i1-$hrfg(Dkh8yU}RITg`?$1bz*z0)gTYQtL!OfS_a700s zOS|{(VLK+hC;p6n!;i3H&QGKA>fVX>w0LzU_KY>{v#>C!G2t+t8Y_AjCfa*~r`OV@ zO{-*yw&B0G4X6B>p8tcE=kpKRvA&ncWMG!LWX{91Oh>Nve4|=-L!%mN*W8VY1IAi| zm!@YWjlwgrp;;+Rh>>;dz!~cYXL-`bn(t7+lKH?sb*Yj8lF|mNGF_g6(lKy&7_NX# zm0zZ^CfyDJNzY7s*J1;R8}2;QTcJf)BO`XR^sdW{(mrc(o&;7~9OpE5IktwlbMJ@w}ciql=D%y?^g@7FLg$$sZT;FJX+wKK>_doIdj z5NBpVdJ3xCyqF&%cwGV zYA@p(=^vAs(tYd-9PQ!(Yq~|r@nq_@&V8s)HdAn+6HIEN|K1LphunWRoEiibywHye zG2q#(>k%y@Rp~w%uB}ji%R2HOo(8A5JUgQ;jxibJE;lp*vm0AUcpO} z7^0B}|Kh4KbCXfLk+YfV-67Qx>OTrS({r6>h_Dlg)14oTR*gBF7Y$Peu7bjyucA>V z+Zos8UJKs*p}`bb!>~BK{n|iuTeRF!2&g z$av(e908L5r(zKzOWOIzUPoGcGC-WSAgS>%1$?A>tmPRcM=hpjbK;??m!)$mg4Uni zsTrM&n|ke+D%z*xIFBCI&7E9bWUxaC62FfZx?cVJIL9`4#mD|Y?7O;{=^LEv62{=C zUsfTCOKgrX{l79D2J-%60ztV;c?VLhEt!Layi8qz!(I9P=W-M&!MXuBjvTDEulZ8e~^Sszd87tI^@Tcm9an)RVE?Id3>8?SnuIc zj0<`k_prg|B5;T;aKdG?Dy8lZw<=|cd9UBt+*e|8s~+gJ8B-zmsq zLx76ICvfC?KYKjZNnLR$eoGzXE=U;-Vh(4P=dEkPp}U?66h<8bfgTp6epEab7EYB> z1L8?uo$e&&#A9N=q`%Xu?HSW~SNk{?hwD&-##^#0X53p@eGZ|`H7E;@N+&VIWWbWT z|5x`e?V&w3veY9XG6yONxoe^QuFU7U1_9E=N*}7%21_(bQsQX+g8=zcd7V^jA(van zXhI>E1eOEZrNzO?!q)gSPS9#D28drL-$@QA197mO$F=(2;rDM3=#?eWma> z&7Ao8b_IFBfD^Y^I3!0?IAnEaBjc)O<$Sn*$f}v6hrY_CjxlnbI0iQl>bH0*oXR9P zgpI!GaHQGda__g|z%P0S+MGV!0at^j*h9hb%ktFjq$TM?4F-Xz)>% z-|9bTd+L3S&83I`pdkcuLmsQTQyhYpYyAOde{jgoV_8k?^rLL?l*DJ^?}bzgE{QO;%S{i?Xy~ zU85jU)iq|NCLhC9Z9%lVfGA|{BOc*SZd?x8L#fCRJo{icPzv?d z$*DYg2v`_0I!H(U@P)`(l-DOU<9Zo(Xx*#%W!_{V)8LF--M#10;!w@6Kyc3A4dQG0 z@5GKbl%`E(ADp4yRc-x*gRKowW|56VLLXs-p`=lXskg7>rt&+89-l5D{#_#<36N29 z!<2@mxiNC|e#544<%)7s(`aMww7D}VXAKrp;&XNMur;z%6a28gO(O75j400k`#=tF z#${H3f&OY}5$Zks2syW{5{&}fw0IS75TS=%FA(%oC4^O1cU`Sy-F8vR7gY=SjUgBV zQG$TEUlgE_pmo^nbYp`FA0WYw>fMp+atO*OIXU2Aq7eba|G6ZYBLnv5EShq3o)qT{ z^xA(cd`Y#wrZd*Ft;BxehqJi{QB&Z>w5-j>g<0r!x>Cf1H*< zw@4Gs@l8fGDv>X>z{8z-AtriX@vHMo>#8t$*Z(E^9cY^V_q?`B4u)GY#^wmYjZOs% zkzUFO-^i<6BIkAdh*PLVY8gP@IZyWvq|5op*~=J!7TZEsindE&^k?|gziQpjbwXZEOR0!x>Qh`V`1(?-wa4UtWY@5#!bz%xx15GCl{JF51j@7;N+T z4nV)XjCMS0bh&l2k7JtXdwC)JQhRFHQDXf0>e8Vl{q7@-g!g0R4k7QtwTsGyya0kn z>W0d3lQ|>#y3J=s)@Y&+sC_pD@L&HEG+DE(g=CFDkvTll@Z3{wF``~{F9Tm&D3YHD znE_yT0}D?sD0D+%Cl*ZeTH9SxY->fk1;48x=gdhs=B&Xs>zT|5XFoF(<^k zwRqcT7rT3cpFLQxRSr8Y7yjmW92w=`PlV;+eH3GcKm1Y9yPL2`O4 zv*1?CvnP+_+=vj3kFX+^qeA)2T5V>(PhAL_b1Rkc9Cq`6 z+K3l6=c3}}o|*iM74zRZ>-+dShyS4%j9-b&G)IQOIkwrq%)J2>S%F0;?P>ju{9B{` zOv|(+*khE6)rG=BR$K0vy9|I|^z?HxMn>-ZgBH0#-{^pJvX$dwPqK38#3$}6ZJ$D`kxn}+-V>uM~9wgQX_-6^d27G5$HEgyX>Khn>M#(oBf3?wo-`RmhU;sFs6 zBKJ69&#TkTr33ASfJ!^HP6~3`9OMqvVD% z9L1XfCiRN|QWD3L#X{M#U=?8vEs5DBB$Xb*!e2Ns5ApsZ`VkfcG^86Xqwh7$kdSrQ z8-&0&u*PPm!l!Oxq|1OIytmwOu7gX)@_o3;2fy9$*Pl|UZDT{2#0i{{uMMkvzraT-V##dq-Hy-8#iGgikNZVee7 zdztY5fb}c1)rwq6O33{7WBDJQnzK_qr;nrqS5saSGgi^k6p(4HaGq~5G0N+DXqs~=_dOrl zLyX-m`KH4^jk~xUewz&&5v-nfCqpCRC}jy1{Nln6d)pj!oqFLCS-4CGh8fhK8gp)V z4c~AB?Rw3O?hDk^l793l4JEW7T%GouGd5u&pD8K=`8g?+=#}71=AJN03egO6h*BlKvx;*`FY)(4(mh&3T#&T0d47*a7wJZwhFX z57lyl5!UV(C84CO-0y9F7CuCn*tcdJXa`@FqKeEs6~or^RH1(Fz%8tFSwxxg13pWS zJToFI>=zDk(3M2eu@}^s{vX;gaZ=^~E;;`g{Ifz<#^$8b|t;q1fLd zU8kRA;FZiy%!Sw2kOZ7pS^=(=?|+joN(N1{&tN4ERXWHA?;0{n^bFw+!<%NWx0l+g z1{ROqIMV7!ahB+Uvw$gihf?{QimJiJOBK(iAu^|6zg~=Q(4r7n*d|FRngnxm3dQXF zmGj=kr@7=tqooU3{F|VqQyF>0#Ud?jqGbsn-#C zXk`w@MjXVmAx*Y6PVW6>KyuAD(T*Q&BWI7E`eH|jL*Lj|6?f5K{1iXyxKF)%QV#C@(|TQD;;G z>c(QF8al_^OAcG9nbMM%GqtIu4SQW(X)Y}1^uWvSacr>&W zyXX1AZ^|u)KLq-^Nk&gXd~Lq1P8_2MU-&SE5kdwh{r6+x-R8(xL}ve@RCran39#nb zPkY-`y^%j}G)rQlq$CCsmQ2uYHK}krlD?Sj@0N&cpIu2JNcPLlDn6-?7k6hKO&cS% z5IjZ;*Cq`XiranC#(d#<-u`xNn?nqS7|SD-26j;42D?Pu+wA1lYxO{OT6JT*!@r3z zlZs5oUY&4guNm*FQEH}k?~lzaESX4O`1W*fo~tO|+hbkGLh*{1&qO}w)Z0=+Yiu<1 zI5NRrnUFZBTZ83cFUa*vxCl>^!Ro9S8WsgBk>)qstK%Fzi23^ApATi@X&XXSpu+C< zmaaM1573Gxb^nV<77UZ`(w?+Asv|fXKE_a1sk3gv;pIHTuX{pMGm-g>VpeEk3ea}V z%^>>`T20V&;b5*Z)jpCXbzg|=8vpVMezPR&>EYB%552Bz+Cnd9^r}(v3Kb8((VHT^b}R%F^w=ZOLjkty?akgX7Gx6*0DTVN5SEtCd}T&R*e96OGK7I#X1+4i>eE z*HW`5`oG7@OmnU_-U`9=#lqV*vqa!TddFJ3Jw0Zi38C3J8R9dgja*@mVQ$A-50|ke zEZ}yht$;X7rO(GU&vfO^Nj*HbY@9)%wKbJ7W8zUed|)p05~{^thzecCME{Pp!%=p@ zGfk<6i0+jWT4<0q`v(nwq%AAp!{yhaKLAi&SO`9|I})U+Jd;H_V5H%+C4VCD&9t~` zhkCzrUWA@=TBh=-X>qO+J)|)@3gM!aw>o zw7Bo=*zPPCB z#<%MPbzAMDm-nZj4d=Pw@hn86r5df7uF@OGb-3q!VYLA? zD3JOfWP%cz@djbjtB)?(|H|m)wRK3GvDc%0JLR<});FjmXx*-QYuUx(XY)mDfBf0A zXMn3qqQ*<$>z-%oL>3`ur+wFJamx78BT7nA+fm_kD?Ds9Nf`q4=?{h{)hj-P8%I+{ zvgp_4i)jpcSx;pCQsdm||5$@|QUolJ1{imPS=u2IsY;DqZds?)GQ0nv<-&_i56=m7 zUncx83+H~yFtBT2j41;%%IRkjT{T52zlJ9ZtGg$z@+7c)d@|&Y%k2+UyI&r1m!5xX zUV_b>UqDvVItg9YplwKOPH*&_z}`91k`1Ow(w)yhnF%+UlED(>^pcWB4mj=%f5J~6 zVmmvMdigpV=QVisS}wLzNTMio-Kns3Td7BBRb&j!V+nj2jIAez>n>VObE`E2%k|n{ z#v~U7PCl&!Esi?m99lc&Tk9Bk)nkTPs$%{e{eylMWcsbr11d5}Q`aA6cYrq_Cf6x_ zJ>2|kNr2Clw+fs@5K9n4zbF)Li8gwWIHVvU$(I;|SLNa$+;`EC9tT?1ANt)o%=GDS zZdv!REX(F>U2|h`!-Egu$9HO{mKXk~V8}YAO{#ikbjN9xDh4x)Yvk}p73KXaH4k^0 z$!@__U%I601CHAT<*;uhUQc45qBUb(&K;XxZoqo;{ax^K1yxP3zCk>NDqDuBMusFj zvgU%6A#G7Qtex(;MTs*{VaNlmWL@z5@dRp??iS{Et%ll#~3fP9c zY4?W`K6qlGy?#UrNqlFHmX%u#N0Dq)rnc#cAV7g&&DeHgPw%1j;Wfj=tp1F~O;v59 zdQ(yFx>=V6q8E}lIMx+;cQQ^8H`71y6Qg7a^Wwoda;`9P2Lw@b9o-*>@od1p_U0MC zAbVstH%m=1uApYsfsi?1(&co|0Xd(?q4^sV5ib6q1~ECCxvwxFF;Fv%^z}uG-DNa2 zy)U?e5hv+%Np7fMfhnEfZD!~w!yaD_8b@5Va+?Mn?B^ z%9J;INTOPEZj>&(c+;cz5mr|%s&$i-Cpy@0l)qqT?_c=ZCNv$7Dt6d|mHXn=l!U}d zx6O*-!OcGMDU(HAaNi8PBz=_;nZ2?Z-%l414}KOSfUr4={~WQyxli`|(GlFJa=iv@ z)#h@4M~rkgW(7H*$N*Kzmw(X8opypBB$2x9Rd0c9zJs|p;7r*VS)#qy9^MBwVme*8 z@pr|R67sit@S*^@A}7FWa~yxS54>f1|79njs$r1JJ~kf!c0kI2U6Q-ozWbi-Uv}7> zMd#=Bf}h66&#L@woIZ`bEOs~T$yX(qsP}mD!f|fYrlGu0ezj!Ka{@YKn z26$}c@ZjEWNb#N44xH_pP=Fc-uTHOUk$SaMbzLQN2%4N1UpyAASDluvq(t9Im4&?M z;U083f^uSxciR%J#KC|Lf)rRxv|<_n^c7bGZ_@{xw}-vRG+hv=wry=|5iA*{NoVCw ztX5S1-YV@GsQmxrNMP>g;s^Iiu)>E=zxpEz9w~E&#veZGG=Bw>uB!`wch^;mEb~{p z{+vsHu5J?6;b}1)Zt`tFj>fP@WaFC^T%8C5Xyts}3Vb^^utDr?g^!Y-ul5ec@#E2( z-^%RoB5luRj~REN>8Wo{qaQdVUDXm&tos$KhspR3P^}1W02c=j>S-xhyLFKkIDt_g zTZ}QUqDWAYI;1# zNFqXyzpZB`9|z_ta87{z$q{+^I-#sxIpjbC{)SBGA;VGT&yGQoJNZ>3po#l?N&*b4 zPv9HF-QUx4xM~q;18(7ML(M;jw*rH-u#XIHc((b3#8UE`T1|fb;EJBkV*Tx}mp`#( zi;4DGzCSo3K%h?`B7^7~O1z}M5b4`51*S1fw|NF`mo{cG&X4p;3VWk1q_*7zkO%(* zNv-055$5}-X>wA_uSaX{bMQyFTWAL`mhmz{_=bPV zB|ciWTK?3&ri=d>wjns-ajV^q6L^Nl|J$=C1R`jJ=b72?Jm{N#87|LcVaHTP6lW># ze?9Mi`YeE_7WPmS?8rxZV}j(W^1B?MXe-)}&+}(=jL?i_Z4H+4+q5V@i*lbWll;&l ztA+Ld=gl~PI3M}Y?p*`T+_8%02eO&806CZBQqb^C@UtVSufrd6-VtN3IYT;Fga>8` z93Zsehu2wr{p4P6$8=|tEo(Y6I*jb++T`S;CCcOU6#AqVAb*Vjk(At_sJ$0s^YH1D zf}r`0#@uKb|E+rq-D#D7PwnSTH7lKb@o_VxnOnMa{Pn+k0E_qTM-3# zWRFyZY59gKmpMGoW#0I2bA$C?EY0}9j&_3KWu+RI3|j9cuut^ zC#d3x0{0R8-w($HJ0gd7s?4_jMm!R(AmM)yFeIhMe3ct}UFxiKB7c$p%{!Qi*Qffv zFl&bi9~}zMfsiY=!N^L`BZXgvhMwxYj6!j7%BQ6Oi?G&~w)s5bu6w_HENa$7�fF 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 g+zNx}spK~^Ui>fC7XK{|@;};N{2$+g_P5=M^ literal 0 HcmV?d00001 diff --git a/_images/logging_arrayshape.png b/_images/logging_arrayshape.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b8535efe4e4d9261585565c08b5807aea39375 GIT binary patch literal 65288 zcmeFZ2T)UA7cUCZ6a`dNlxhV8U}NQG zrK6)`)4X&09vvMsj*gDL>mW1G;*P2G0zT+H?rGelEA8f;2fi@cfpkE0bf01l?>}M! zz8`YEW9mUicO3fjmwwDvz=w{Gnx=UhWaMMDL~(k<)s{3Dv)9rfKlin@Gm`Uq!;c@7 z#b@YlJg21M*3e7Xolh5Rb1;|Gk4x4c9e&gUJ8gRNne+6SLs1QlVJzbJE}XG{Mn`{h z(so!X?C8w{vJ$hl9&bOIIiB&5yvBUwz(smT&1I(TU{Y$p`n~x0c+>dJdMJ$>neh_R zR8}WunxvtG%IA1!or`a=J^ZPs-GyAq(IjQ{vCsy_-1J9{wn zZ$EH_=Ww2XQZx2nVZ{B7^yk+Py5|VY|LB_${YT$t4;C68totw2`e`>YY=7qj z=zifuDEPmzp8x*=){VMAhyug>-b5tnk{L3fcAKa+jDQGG+Gslh4&<%nG1UEd%}9gn z?h*UI{k1l!N1vZrq`L);dX2cOTl*{iQw35=P#s3x>txR{^@xq7tr1ARq+Pes-hWY6 z-!S*PnM-ADa2^&Rd6&!~XWLV}ku>56%RfczsB?}Wb8vFVNhz|#*#xc$6Rlu5_YcLv zGYz4bC@F93LK?NNhuW$snk!tDFISZlsouOgRH9N=7vt4jZ-8?&M?2b z8FHCwi54lt_;z_&pN&K$Ns&WcS5$0y@K^JzL{?iJCY(ID17P09m*6b_rLS;qey9h7 zi3le!o}BXW2Nj`UZN-n#8{9=AbhZa4yI-~%(U3lw@Hc| zeC9A=h&4_2#5Z?DeRH!gtL25byf+J)4F6EmQ7I`$drTK&`qKFOL+6K@FXKN6ew;n5 zyY^{cRwX>flLj_AF}|Lb!C<~6e=9`tet=#>Osz&jZ8eWbq;kH2fy#}4PT!J(aAw>C z!EawfE@bPU{XUj|8E$*P!0Q_N8{_7Z$C=uS*-qb^FuNpj`Sz3A`Hf{R*0?naxpan; zWiDX*(0>tdDIj2;Zi(&b?z@tp&p(=yu5z9hNm=$1oxSpp0bw@b$zkA?-q(Yno!WBN zm`DcODuA0*Yqg3A?S~u{qyIFi@QkcbFfXGYb$2fIq5e{5$wVGTTbA>6(RR@8jy}A6 z49@?L*Bq6CzDl3BJ#cT9Y%^%q)b*`VFI!z?*WR|G7GE>v^wZGC9`gS4vA(@RUti6C z%s+ssxc_Dqq5uDC{eQaA>>dC6UP`xc03`dj{grNk@t6^d={i2iURpyQ< zDJ#=UC#=tOkXpE`2_?UcGI;SMztHsBO@7(wwJ#5@4FEpML%HLQ+rmt^ohGyjS(|p? zcWM!(pwpZ_UmjebqYHMTug68Q*CjcO{YF8j&lq#zl&EyXCv~88@Oa)&k~MlIWA@*P zUR~1yKX3Y1FNd%zYAm>&Y}W_(&-_NC&m41M-RJSa*U|t3H3`?2t)Q6YFb)+Ajo~+{ z;K#38Ij{fZdfY5*M1~RY8~^9U#_~(kgPebQRLKWI!2nUI&{62`T}d(hdfGzoEa9i$ z`Jc<4_1Dui={Nqc1k8!#V^IGoN(Ocsp*UYEDoIEXP-aqlwGp`ywNYjO7YUC^HlQAH zhFf%f_s1mvz0as4b!-7axT*cQ9vt^TjO-*!!}UXGk7@$1M4Ve89NpH{Ya`5S}J z)tPbEn(zKi(-yU$=Y6P*pV6z*55YYe@g$IXDmV0nQ`dG0W2szF;WYfy)#r zQCN=Z=NB+N22)@2~;vMpEgC?Q6*?u?;WR%5h?tXMB2Lb^iP-jKUY)L zt?W?nzVbzUleAHWoOMxXd5xEm)~@8okUe9kv9JL<{V*_$wha?!aLHDW$|44B*H`+i zJzgLX;QlMM8sNEcb8G92F`oZ)2z*{kbuhv#17d4n7}O7(3@Bm!J~?b4I)iRvUbtW4 z%VCxQI$TFt##2QpDX;4DYy8N&o;S!o!wVv+2hMxO2YPVc)F<@uHR9a164|5IJTi?Qc8>Xt3^%*mdWb9V*! zlu#Mf=|g({sw>$+XM*c*3gNAnRjbR7LJxPa)rtPzM)7DI_D*YO8hs!x zupgI4n?CQkKh3abO!II1L98sk8Gb>rbJbpxJ0Aun|Jqm7ddpPNt)|kbB!KAEgk~2 zmw_Wjc0kv#6zQ(n_DBZPxq^|Q?vU14q8;L7fiTh|u7eS$t6z4c*f-&Q-;C$_Ssn$5qDJ(Wp!G+KAA}cB4;UyVc`E!qcFn2eAraD#j zES!bH>5&6&@m`9wncf!+?NX}Tr{(ngV`S~UX2^5OAdyEeP9D&&8duaScohmRPE&@) zb<9M1R0eJ3^4Xyl93bQ%R#+UW~-?zZub>S-)rvSCWyHs>(g(ys{5TM3x}VZ*?R zUQ2K269i~Wbe??Qy4^h(z{Rk!4RLpvO6hxb7TkNQg`GmV*&@D4*sE|B+#Y*Rosd9I z3bJ#KavbM671l>x$Q&(C2&_-f*Mzx+joN(`h{}$(16*eOzwEJnwgcyzv(e5a41?9*}Io-yB$5YD54owD=_x}tbm zw3=_hFLmYJZee$oXyP}Os+jJwV=yR}PV%;`t?iRHpWPMn*Ddl5t~`}Ql3pq85%wv_4~wJga-y}C(=ZV3XAT2GBfLG8}>+YQ%#xS1f!bwGq+Ftlb zmi5MtNl-+4yIpO=*G68qFtFS12V3b^fqQx{C5H4A<>}hU7RowWo4!xERfzeq<<4rg z6o_=4?CKhOU`c=F#&L*dS=Gj*Z{1~=Nmf4P0tOkymfEKWNI`SD7)@WW4VU1m#4gD< zzWeH2!S*N>s_JIZ*pZxUnyUTo{tM%=E9;G$H9aL*LTavoxa#A8%tYuc%{ zef9YHfsOq6;^Es|s>7GJ=5y5Yx3Hky~kq3++8n24Y*tm(OsXj74T{%Czu;Oj|^t`aaqv9{Xmhyhg)~ zax9xa|56#)aC^s7I?VOESD;r~-mwc{j00XB*TK&<$2CWdywvWV)@D5okJ8MS8!XZy ztQqiL;{nCWybpd`9#)3);f&f@w_g}{LhdOkU*7QLEzvrlGG~t)gd80K=d#Qd%n0-= zM?m+x3dfIUzUy_b*PiqHx=W34nKMaVm%wjl-nb^NLu5lJ-9bl^yu5WX>vzV8X}4o* zENtEPMgy=RjTU1?bOVF7$=5!0*bHwmvUK#4S5_AEcP}i#zi3~Mw~@FR*RCVwkW{1b zb`baCaP{I@_$Lls;=8t$cfJynJh+81y|S#HN$*l*#1jamcd*y#uZ49y+2(Q>SorcI z=G7-s83VA^`%_PrhOUYCBRc^lQU~*+r9np}$4f)++$Rv$5=km| zr>HZPy!)#D)i zHLPIB3w-t)WAi(B$F3&{zT?8Oc>3haigh)-hYUe*{h^IaCuug8w^PC)N;{0hS9zPC zqh|wwzaiVzndk78)8S2wrmcGW6aSX$*6la}%Q1CBxf}6u_0?g=rpgDG@ai!hH*{Ez zi$gNbug{42r8B6@NccQuVpRSbpVDsr$$Db_okF8^+D%Aa37maPVdnhGrdxM@zVVF` zVn55qocQ5~X~euX`8RWi0&Hx%=Ot=AGm~2qj{X>{Up3kL9`)I({Q0dBYQBp`VCBl9 zum9U^$1cI&R=*Bl_3P`56=qn16wEA(@pZGWS7^gaAc^Uq#Eb$@t(auXY-ZeC(b-gwPfi0WjLYOkDH<+>l>tpE71(I-oBxvtgH-p&Xbqd zkWo4FAX)Zw%5DZto{~=^WlZ5{Bi0(JIjYXbHDeEG&SW8N}`)53B-PRCa@ePi=nhgrkG zyL9&iAD&2J7FT%hfkh->QiR;S3o#DK`Q17V7AZ296Pfr?rQGaFKe&Ul~?Q*q3p@4puB9wuj34PdDA0zvjR(8)%f2 z^MUM>T0P(SorTpnDBN!LuTXv5>@hH>`|aw6LQc}~a!o+iR725!G|&j6%XAX@5dJcR zQ%cq&K=iwH{$?0huN^W!)XZ__+x{eP$=r@E{z)^MAZq(Smbd7MxQWY@Ul zLJ(UJj-mhMp$2J%@m48m!q%8Sv4;pmQtcoz4Id@j!!gjP-r3T*zPP)V{`bGWgn{cPQ_fV*ep&HQrc`SJx#(6~;XS zE@k-N^@AKzxNNiDawz%Pfm=z-Zyi=IC`fGdxyJ`s?xvMNl=K$IDxyD}7_Ze1oMZ1v zX*e?5a7D@?j*2vY#~kmCl+YozZ+A&+z$(OC_1?}19-MJWSh~(k`=o0+Ya2aJH&Ahm zzWC~S0XBA)DikHy9st8u+h8}*jjP8+ z%{N#nhHV}s>$caK{V73rm5N-NySHah`;CYQPNDD3l$nmiMaj4c4yzyEhICGbf!$}{ zoRs>;v?2^YaHT&KoCw>msS7g2=!j|jNQIj_bF9rVt|z`o3K3by3-#bI|m!If@zv;4{`v6qy|=fy(2x_Tj>N?2 zV9+SAbibKq>z2N`)$1jA=TSnR(N}ir{TAZM8WSne@q7Vpca4w`l(I%BlkNRFEvKA)Pv08j zLRjGcz-B#OVoEQYcLgM?IKHLF=i$)IK~0)%SUpaeOVRjFtY5o95!i1Q_;yLibf1i; zkzsm3V1luGIUy%HG2Q{|3i1|d+3@DOagYT^x)%!jbWuLf4TM;?VV&7-2-Q-wWzbOT z(5fE6D|v(pW;W^D`ak8)s_oSnp+(RGo0_>s`eIAHv?Ckn3 zB+#cpKdMMdE@=GpjBLKXjOOP;!Q37&F;KUI9JRKRG8jSH`j6I4rJK;G&P@w6<96k+ivA;E_PX7~i)nhx0w1K_j}H`Y1azKM*rhr;q52hW z*oTTK8o3f8RfhLUhcuS$kcNm&Wq*H9H$M*>(G773*STMjx-jdnB*u*Q%1g1Juk@=> zT7_NtjP%-$yfj(#H1u$1^ohZ533|#uy&}gp_6BK5VD2grW)IgWuctYXnx5eEU>o%) z4LoJVdqxF-mfq52QBIlOf-LSx^%zL6E;os#(Bify>%7kPu_=spOc;rD)&hw?EfV}J{EFY6~Nc%z5+yk5!sI7+t7a+Npng#h*HEm-Yv6rmKq8{&gu$uUbmx3?6f{vt|HywVCD4IVxdQkeIA?3i`2>9%*x1~5 ze`w+;g{gZjDi(tKd}S5xvFTBL*&SPl4XOnMIBkm1U+P!a-%XlL=`I9!&wV!O{#5dE zU3Tls`!hC*-aVk&T~^EeIJD<$U9!p#G?-BYbJ zroO6N0Q_QcNV)o1ojmHR^33+eQByFmi)3M)zH3aW4sbyY zZ6t3dc-*F2Y-CT`2Hu#O@X|fbf!1pIS@*VzJC%9qVsm|3;_K;#h7fhMg-~}p9_p0u z29vrO$2O?Etzra!gLOuq|IGzQzhS}USY_4Pb@#MfH8FaIn%YWvQfX`Lb&Abt6p64W zLW+rjfk8>SK_9;)_qR@Zr0b!0TFI&MwxZ$%+atToE|f}ZrkAIYE4iFwi|p~gQVjtW zF`b<^r8`812HEO9JzjU4^E0D;qKL|7g@kNWZeq0BRi?H!F(9%zI3S%$k&-pN>mF2X zG2^LgxSc8c?$RY7xmE0q&l=O3zq3x4KgV$LL3x;hOIu7&JAKDiPD#Vt6kmCxLMFWL zU7JuaKQr?l0WPFB9G!s)F=R@d>xLbh5n0ZQsb09G(Z+YoGbYSYo96%BYOlg`XQ_?Y zGa4rR21sc{cY2239Usog1j$;RU#D%rf^1w5fWxU%eab+7XCOpofK4_!PjjQXN51Ih zT`1kT{A1Rc<|wM1=ukSxC21)_D~T`5Q!fvM3UVde99a%HC;+q_3Zol5^%f#@vEE7dVZ6ZY7+xPo`36N@LFy^X}ad?1< zLbbJqZNRSki6~b*TpMQ`Exj6zEu`(`?!OC?AIrIt#p~X2S?1XfsUw2avjbI8HFL)@ zZSI-JA%-1pJKQ+_*P8~p57t#70fTAV6s!%Hn4A0HpWkQ+D~dvG#t)4R|5Pu0;lQ9E z8%G_EdjHAOahIj9kn`gvhXZj^^BuB1Irr0oU+r(bKWRT>Y~!i-#I9`+p-FVFUk0Ha_odjl^&C zIUy+nkMWL?f505!Ot{nRA1SKT44U{JjXELF4m;*r#6)Q!XaK;M^lpWLc^{@o+OhwNJNLQzzQub~y3B?_i#C@(v1C{z(zYH5!0tOi z*VC?3y^^=hy{e9;grA$+J5H(p>=<%p0>o-5bnr1XIBE?Zm5MTFlHJ3`h_lk z%UyP3X{fSrK zF#H8NHf=Kg4c-g>`E>B&p})P9{{I^91cG#x{ncB0lab;i@!#dR`eo{+Qu^rwPojX< z(o5R)TzpXSPm~!zjQ6;Hr`(xvyUnk6B;;@G+C|ce3!dW4=n)8C$u5FyBJ! zy_dKGu(WM%_Sj_v=E437{edTLoZ?rVcKa&v3wRzkYXbiSo=>a1x!?sL#*1QqW&QwC zpHz%U$w^LE$%n-N(AVU8v2Vqgz3#g0o-FLd3IGfT|54vTP&gICx$g6*=iR$=K<7Yh z37hQm;l|`Y1o?~Q;1hpJ@n_?Qzp3;8s4W(FNuro#R*Wk3{V#cshjIMD+rNrH2z5u> z+T?Uw*CbNDgAhohif>j&qBxezIv^AAJLdlL-LA`D)~-j#x;4C>vJJlrl2uu)K&xO; zKX3hh)dWq{Bpqr1;PlRS?|#4W>+k2LjuHw4+l073RQ2=L<<5JtJs?@?WnSdO@7VXx zdpB6y&~nkI0D}OAL8AR^`r~4b&IalP|7Zdp;`?*r{k(l6?SHrJ$X(3xoGMK|kVd{b zpJa#Bz~(6*V*6CXTccEC!QIXw+X%w%3}ln$5Hu#7&xWYJnj6xM>ZT&|D$pOPr~2OW zYF9F;UMlc2@y6hXnt!pRpFlc5JBk6q>^BYK#RW1o>0*Z&Qdj|HD`9 zXzl*u#CHfxON;7tKy+Umrqif}(#vSr;HPY@8hl{-YSV1<+2)JQSDU|FEI^j_Y{@_s zTX6Smm+BUC5PaZhB1fX3vTLyY-nOkTgQmXAK(3(gmD}k_GlzLWn-LpT z3BdOVQ2N^R;cqtvU)1b7SvtN;6rDCTLZ0R5^P4RHd@uI54l9{;4%e;8e(^bU%lZwA zqo|v_n{WRCSlH{t+a#ta_}dTV%K+4^Uyg=X(9jovv8g?WbOyw{$XBCl<%Gl?4dV5HT^t9M8y%+*QZ<+Jn1UZoPgE5>X@;=8~`q=3@Jm_v)_ z%s!#z(Qgs+RcbN4&rrx)L=Tpz-PUX=k1qbb8BII*R zL%V8_sLrA+QXQ=WOAYEJ$1WjniRhY(_VGeT$(KAzD_GB81wpFWEQU27@O&IPb zp4_@^PfXd8w#v^%2If-VAOq1pW_8nSb#In3`fKmcVS8DKIS|?>m03MvPINn3VNl<1 zTXCzFZ_N$w`$A21J>FB#jw8YKLM@ZyH0W5)b><)TN=-Uz@oR1I;tbJ z5p}0ji>lGSNkJ2gJK=Xz)6K@=Z^R!MmAYc0!XDz6{Ln)gCt2%oI(TPTvaQM@R;zOJ zj$gDj1$rt9Wtlp)h025Zcri6@wF(ZChFOz$V`#EC;nKhjypm2zG3A9>7#Gw(QFnh_ z-E9e1x%q4ZA-UnO_6(mt&zUwJZ-USJG+Dzl|AJ*{Sv505X;)EdiYPB9{Vr%niwrSS z*pc=JvovmCTh1(7ZOe(KiYL8p^%9t?)Q>%1PWxC$Y;*YYaY; zbdrRY(4j@Ar%;blT%RNlT%xTKpPFTumnR235s-YwT?ikEBMY3Q?kLiiz^D9RC!zJR z3CNELTcy=iug2cXgygxqF2x98^tO_n0I7Z@%99U^9;5Cf%f46YIlwOH#P*5pwO>Gd zLj{tN8V;1>o}CGE{JBKRyuhnv>)hz)^#s|0>%s(?bStm2iaj+mRo^3lDOfjhEcBa# z?PZ{J{kI8N0y@ls+Xt1xl#vm$>&2hWSdt>OZ0p(UJdr6wvN>62hYMDGC6+B!^$`8X zq2&HW;g2%go7tqK#z159)vK%baQQw&G!BMZ3wcg5iR|>@>4ft%Zpd{VZ7cn;WnxR7ahYn7Z|mi_@8;?v zLYaQ;`5L3>0eg`LTXfsYt%?fTwe*WdSGlQ_yx#$I)Y6~yB*&dzLPMYgZj3)ZS8a>G z1d<&f?e3m|0;yxHP(YNGuLcAWcepOF?rqS{vmY{R;Jpd)^4)Awf?PPmYw8Bu^KfCp zR^xMj3d}nv6iiDS`!P@R#}7lWC)|()6^MBk1;SR^pd>kWSdE7`^>8S=(m!L!D|kz1 zCU``&a)<0M86utASV=xaB@6UU8ne7x7A7QgfA{coL?-9j2}s8MQ3V-1@bCu{?ty@d z`DHZfVOyJ@`BW%alWut#YCZ!W(HL={XJO`Y$&VQgdZ5uuU z;VAPAb+!SFCOoP=-D9MJ6elOBO70rq>GQ0YE0gvp5g}}iWOOFZMKun7-0J1oo+nJnn)Pf+QKh3!TrrL8M5dSb*PTA`21F-Ll3rFq;3I(i1GdahZA`q zQ3FG=eVs}s$9?=^oL&7RtXP+b?ULttjJWFbOm8H*SxR=tSOc@X<3vJzt%am+aZ|o; zduPX*+bf{GI+*qg@1%y6U5aNOtM0xw0?vd`n-BrERt*qiN1!tE-5YEgT|9eX0i@O? zM}Qi(Q_r`v#x$wYA5t^FQ4j+|?Y`8;PDa_#OWfb{8xX0xczSDI{iJq^TtblhutMHl zjLyU+n1u)de$uVj4m!JFA9!AGj?92^B7 znw)|v-l!My_~?9qavKI&CM+qo5EUILGT!)NBQvU`CqyiObl&m#q{w(%%>?sAZkI8!v+jnf%>WteG=&0MdQy@JAjrl<`Nxfz&v6iSs zy~fFCi1+YFNbRG~x_GorlCb0Ns^|4IF-+uOo#n3mg~ke>93`}e#CU$SUFai?{2z9fmsNn9CO$WCCQZ-fm~eU3clz&djrLLFWrLQu;9Ch!kEBs(uEV|=QCR( zxiliST`qo+BIIoYH9i&AIi>xUK`sG_Dg$jrXNM~)e+=2phvwVV<#+kKtetJQCtB;SH%0RP z;MP>#x2fx5M7;}sR}^8L$JBA(Fhjatgy)EV;+S`0QZX--6V-*7Dz*`<7&Pbr*J;_8 z8@+kAU1fCcn#o1QRYBuN4w|O~3=i`*tucAs%?$$^uE30TrieRL&iBhvtaZ7dyi_U) zh3zOCk!)Y0i`TQxEr_+wnPTarHb5^xVh+~HSCE#5Y*hmNN!gV%Lg^cQIs1Xere^P2 z+I|x!-Mz;LAW`f!*8x#(9bTA(llbp$frav1yEffunlP=V(__aCT}1! z0qpD2f&|;%Hw3eTb%$&cla4UqR`MX#sydPxo}0&Al(W#A70;ziQtOonUT^C7PY!n=NB;<&3(MarR)=h@9V>9gbq zt`{Q&5!*p6D>3LP9x3tsA#iQmZG-B2Dq@s!2l zptn}(-?WU!ZlcA9)KG3QWmK} zv{eQgY!P!o2!sGo1Ng&e)$;1YGofG+|D<+q;8co4?9k%ZAZ(XM3HMAIsO{fxW{^WR=@xL4bYvwN zz~8;C^b#VBSVhE#u2y%<@VC^a4%tOf@g~0YQ!eX0=0v{2cO-?pcW&>8vO2*Uy^<4O zA;-6ghE6q&jLLS~B08GPL;Gvh zxQdQFiIKER7hRHag)%U6z&^CrS&^YilXeqF=^Y-(hjB3B0CDf`CwZ6v>kF_S_4^ht zuI{2b@P+j$K|h?(`&ygL2`}tr&={vCZCNDR7@{yDziKiu`bCx?_!MoWMYTE#<-BYA zO{a>%nZ+JJE!;rXameTF_gvZbi^teG4^FphBtd5gT|OSRGr(A!;H-F&j~_D`3- z#r0#!5CGl-8x*?t@gmvIvgm&A9-t!(Ff!z0Y5i7A0eH-45xe)Y8_KvHLD-Si)v#}}F+82q5MpJ5FL=iC?wlHA1UIhqi6EsVE^qOme=qTVoPA zB1b{#Nm3n@8mqy`fVZ?{;^9@cv_H(JZ*P?1{QX+; zX$7}B$43~ToIFD`XU&rtmlNHfQzq&cC%7*yF54c`plIJI%b&|s)~JIj_769IA}s|t z+~9)od*JH$0Y&%SmY<<7Nrp=0L9hFjhjlBGZAmiXrUiCiepUg$OS0QW^q;9QTkXq}IBDb%RzKHS^l&z*f!iq9k>(HX!=q78@nFz;T3Zvr=QBj5X8 zoA)fX9%8%w1Ylu_E@PK{RR0x`<+nxp8maOjhK-w*EeON*sz;v*2cIYdanyls3I36P zwr2y51LhosIswObnfrwqais!(3mkyt|HbEUb0=W?`(Sd8;6*w3$3AElmdAuk_nq~J z?ylQFg8!3w`<#KNa*;Z+X_IWHe`fRIHxR-Vf)p{z*1lfj&wSU>MAZ?HXeH%^YM$Ph zFPdjG#7xh^#KQKsAp~dUJAe>G80-?C0`v#ZbcmUjg|3BF)oK((`EFS;496oC3gAgs z=OD&G*A`6_)@Lim9v*=PzU;_YhMzh$>MyV?`72-e`De116SeC-20!-m?2$aIs2qaccIYow-DG|OU%X{Ob!Mqr-RfAhpMdQ; z{T!~co2gsU?@8V8bU+~Q`-^BvG7#z~Sqwu}cuhb}>cXRS#K^xArW*fFn8pKPTULqc zjydE8b6s`IO!tg6$IrZM;~oLg2;%?X`Sri$Ve8#zUL7H72nL8uGZ4QHOT`h zxgd5}j8lw9?1I?U-(+^f2?HD)4oMW7hJ-6`bZ*-NDb37trxF6ym5(ncf{(a?sYO&6*1DnV%B1FW{Gh}nmbOdZ+{-9ei6V< z!^k%ONLH>&0P4%3xS|J)&XnJslO-4e~D>) z=S*8q7T^-W_ODwa15E!o%J_@Ib^rC0eGdqsl_=K!JJ$3sDYHgrxbcequb``+G_I7- zZj#X%*MZ!|5dr&zIO5Nj2mjxD5Oq-MC(se2viDLqO+igaFZ~&_X(}<5X2EW}J2XPX z#1&u(r6^tFwO#gTLw1&Vv$ivg{#TL%R`OvY+T!Wc_ z`LiXCJZcvY`y>!P_~Zt4GK zeSOp~IpBqVNvM12x1{A|sR@1$F%E%<#f@Egh^QiS%h#coPY|1X5#wuJB?9NlJ)x^^ zW%y1~jmqaZHN)YJyrDy@y|6f$&ZXj~vShAuy_r1-d12uQuhrdH4SWGQSJw8j{o!PX zFyprl1sK}_+{*ia9@brCXdV3fwX(O_Z3ZKs!3u7g%XX5k0m`1*_W~< zT~OuzS^+}8YL~J~C>n#bSk*n;D%u{4j2=XJ)IB*7l_3!+BS5@Q9d1d>fR=cS zI<%Z6=Hpe5sgKK3;jIw@1$t&Rb$3x_p+ZrC#Ze}ke$2;9t>SF1nQR5Fsen*>3!f%8C0K|jjwKYN119{ zQnE8!+hbctw$z5N>rCq`QPit9Y$}cTx;AlB3--5GCYmhzIXbUAE&80U)_B@{!pwlu zb?P~Tk{vpIlQ|Tgc`Q*wdpwH4JcjxOF~8GpICl1jegyW*{5sRvQD}YZJZ;{H+gC$z z`HEvDci$S_t>>j&HfhPs@f#7P*P&BiwB~PrG^koEz&6J@9F*Pa0_Eo)+EgXOdm9!{ z3)pf9e{kZsX@$h=V2M-f&@yBFZes%eE|ypmtKh{YSK!A&S7tcr{Poj_ALaJyj_p9q zwIKzCoti<2mEZbr_G1uJ3xLk6a3j3U#y@!Yit%fJbJUE$@!0KX)TaNBdd|}%& zo8H*xp|pC|RG-V}%$S~GVrMRwi0Au|0`jSzWA);BVG;;snMj$XF|CyPVf%eP1m>Ge zkqc6|x8F+$=hG&aJlwCNeBLFcUa(`zM^*meJz^slUl~l#9zBLVT%kkfctjM@wH#X_ z6;GT9kuj^X5nrB9SBqH<+CJo8F?rPSBjFUcqD?_3ggn-DR{xh>ZBnAaDe;Z@yDm~7 z_HSdFhKP}wGL{QEXO^t=Hu$F~tf5yEze?L!cNXX|ZN=IIck>?g`hnGSN!_~Z;>NI0 zGm7p*g>JOy+hM;{qPzIj4im8sCLh8B}qF4J#Wb*j8^ZNo-20u*u=*AL1Mw@ z+Nj5u_6fw?sw9s?{^L&sz!lY>s(a6DKhSJh)Px4jmOgUQQFx!GI2Kc2-HN!Al9uD_ zHHLh3VM?cdR&7|xxbF+O6?11ob`V)6Yu+~^CX;9BazsKj$uGMDc@M|G7gUa*4L@}E z&2{-W3OQw9N3(zFwvp3Q9jHECbfop`DCDW4z^$wOJps5^McGj`ES}2&wChte(LD~4 ztV3BxvdT_k@y-T5S8k=7&K%yqaqFsBVio|O9@0oR7;vPQy`amLkG6C?4njDkQnYT* zRTh83LT+l~#`J)?=g9q%+$s8Sc1v;Q+|(8Z9dlF5Uh-)D%V>uHOpAmmL@8Mt=)JxwVXyiK1dD z@70d9>=mzkDl&=NXDx9giOreQyZ0~)iCK9(VCqX+QeqafpIwjrCu@xu?pkhw z1J@vr+J;|6wphUOTU(SLv5~NIK1B|p&M)h(IjqZTdi@o6I+11BG|&%qXi?|d^jT4q zY{uYhx;B6ReIeE*3zm=uU#Nq4*Hq;YgTV`~0tw#^VFvc_@K3V05F21+2n&0`hY0T{sfXAyql)KCynN8LvB) z%Ns~vW^yb)2J7Gf{7<007%!~S26M^>!V`~<*wZYCcwK0UO{Wh^tC?$z4EE z)ooPy3E}O~V#&-EX!5C*+{2BLq!Syf>n!d`jFN2kb_QTcqigg;VnT$w!KUvl?Lq`wsp~_AO6_1TsD7)4JoATC-f610wXOV@nb}7qLzlk z#?6g{B0oj9v4rd)7Qu98>j&Kj z@#_oHmo>U205Y8m5&zeW8KDJQrB~V{14%OtjM+ANb$%X&`vJB*j-_ zCDIDhFAmBEWqT{@%UgL3pTp);)$Xjmp@pL8&MHAhTwdupMt#4bppro3-GOAYx zzE+j=fSO!=Uf8@0Xc%-yQ^Qh-#Cj*-5egdCoXlGFBLZ>+8ipH5-8GtBi&*xl#f)lZ zLXxPqz33x#v|T=|zPE5IJ3}&-7-mnUxV5l`>9`I+-3&=!xx|!KNx&xNjp5(jlV1f_ z)Fa10o^}Jz^{>?ycl87|*CiJ6bxlHcFLqH*41xo!G>>mg*A)>)qX5S9R@;Txc55xn z&e1^Lz1cTaMy{EcX2AMG15-iX6+D^pzxYQM6JDN?q`F?;^6324mfL?%iMM1sVnE!z z;2)u?Wl-9~O=~E%%M7P!?JU9k`AGsZYQ2fuuqb&Xl$^`x3UaU)6D4u>Qf8~`Evi0{ zZx@<)>L1Y$M=8`iaK-RV?3K5Fj=nOS@7ya}!B7gxVkLR2$u-#`iN~MYm-~))@QAM! zoK+19W1!#Y`X3lnB2?u}W{mrLZvI)XVkE`g6yxM@tdlzsmZ+<_`?oZrVDPOh_Z7;8 zpw`j`=Dx8}r8wYdCaU^)l?7;h*#qNVN18nLnHx+yl?SN9Vux#M_#&FqIa#l=Orz8c zOgQa7`ohJh&{U|8q1xd{>Rj!}`W44}g{L*w?N5Bx(*72E&1)<>x)gAc#Y|2S9x5bh z^F+fUopCBtgF+uHAJBK`duCh<63GL2TIJcIK;R01nYZ=9-8QLpjkJSf(Ls#-q4$bp znr-D=%+{TVzOQ|J9hQMIvU^MoLU=ZZV`C!aj*_qX6kfUR1FRM^QH1Sut;n{1*JZa^ zk|Yy70;yXvCt(!U>pQT^UNaKX8x!K0F1~BDl#C_i^+RuZ2W(JmJYj^+JZWT%VJc`^ zli7*Erz=wepA}x`A2CxEi9syTBeU}=GKw~?3z_Od{KmW-Wr$MXKW~b#2vG@Al)D79 z&F$R;o`EFx;0|2tcIJ)6vBHu9-MBh@L!6ef$h|fTlIr$h_bbGj@yt)XHs~`@IM`Vi zqpQMm^OjfqIxG1X!b-64OVVA;B!L+l`EvqX#R=lv&wr|;8hf6K<|Kd6=mo#X2;-#Ym((Tor3~`O24^Jq za3I>P8so)n92^bkd#~X+55#8eMaJqr9#&+%n76n=e!A18m6r^zRq@Jhw5V7O{y~kr z#o4njPWG+FoY01N=`$h1d_c@Wv#Lz^*3)FNEfRP`&@m|BT(5MN@UhwBNP!}BYe|3R z*Oi`!m z#@dA~q}$70&XU|3USs~!#_xD=B%tly&=EN!$x4=Iq_`5eYHtv2z@bO%4p=ss<`HT0 zNUv_|aCAui(DbuF0hisfT6j6BwkIMb%6wtg^vM5sQ5_-hTy#IxXsa6-zjg(Eo#M5d z2(rb`Kh;QFff^y7o($6TBl*|ND38chqYq>!MN*(%X7SSGhbQlzRz`M={~G}}e4)Ep ze(zEaV-X`ywcE+AGe;%NKk|BjOE!-?uW_a=^Wc%`SRO6#V`iehV*6Bav0-OYyo4&8 zPyb7_(ta-$Z&q6AZVDRln%@1HHj% ztx^>Kv;ie7ExL#@SdJz~z`@%moP~&Lq4jNLvdCmZ%Jf}o*Wc=c=_tNYK@_u9;j>O% zH?DAJ%j;)}t{&zgGn>t;A*$6OZ~PH%=^`UN2vK8)h%Y~2&y7r z%Qo+e$O1%%n$tsBI&DMFi2?!cXJ$nWYl@9*j6G-ps7H*H7DwVS&ue58Oyr5ekcpHN zkE3;o^jXBNLTK=^CDLwTn#eJu!d||9Rp!~ItLMs#w?P;--$0SJUjctmdBL(Q%0npH z7^;lUtI)CXCyoFPWyo0VXMkCD|F({m28fp+OUlr6uF&$$h#+&s ztoj#15t{^DoLYyk{lli1z^y)N^uwrVfbq3Y_+$)J^9^3n=abN6**nBntdQ~`D5lm| zaOj6~&7Avb3h9+a2-OCCd(QFMUj5eI`QOX7=#_M}9ufjU(i6JX^8MhYh;{4PJ?lRL zlsC3as>A_Zi!&p_S36jFB*;G(UhK#+2jjRzuzsRoc#C-XJHB^U+9*I}%29!uSGIKg zaChaCWr!js67*U7zV*m6(tM2Lz>BP#W04#3EE7F_l(YbsvFCQ2xYlI9O{d5~5Iv-& zVb^7o|1ey;ZMH46ux0rfG3l?-u$5aDqNY~j7ZT}7lNT_~nMCHtX_TeQx+vq1Uckz1V}L+lAZ%b9nDA$&|m?#kg>_h*F*=+p~!FU^o1db=6(zO z6@KqN>2hU6hIkeVrK8k;+Ll(Wdv();bPHnh2GaI;wWrV5%;Fhlvm=~WC!iCQ)Ze%2 zw?aD92>=&(T%fW}NlX5LCDe!bF`N{~;WqLPnO+ygGBpUzF(kO~&x0)N() zkT7Nl;lL@>4wOI3RIB|9wPU|huN!x5I<=mw^)CEt*iL@6CaL!%k-R8%p(}aBcldhS zPhIIoQ73dQ$NSC;OCvS=>jPh5@Q7<4P9Ny5ewE|5^U1&YQEx;7RhW?>KPI(4Q*|fR z$36u6quIDH{@q1zCgJo0@C&9Nf#!N1yCKcS85r|~Rc73BC8T6s*7`L{CpkHGPdVym z2sn4=Z{ZWP%Mw0-j?OF2cxkmo3BN zN!OH}N|$z4I!0?8`NW_iN^1aq-p|dUu9zpab=O35^zD9M&5j4h@+^Z-QoO5X3PYV% zJkRzb+u~N?rIuBgn9_IZyQgd%;T7DfDwNCII@E9dly}x8Nxbgc7)FD?Q#snB{$D69 zO|;x98O;CvfTghVIR$$@m|w0WBBXsnZFSRgBrA1m3&OEWD0$n|2)*sDi+>Cv%#IQKe;qBx8cg}TIZ+Y{l}fi02v)n<9^w53gJ76;5f5TV6>Qpu*N)=^5e z;S9j&(0Jx-D?%a$;CQR^&TWB#-A|cP)S~{HXq^B%fu8^JBwgz~b-1pemaFJmYQrYp zRlBKh3KjB;(-oWMF!Qx!E>}(RQa3~p5Kn}zA1^j5loR@&bTdfQkc@!kIUk`B+eoqP z1}*Lh|CT4Ce0o?orAmMBZFtr7LxUl{_jvFv2e)H2+2=L|8YnM>4`bvlfQbn5S*a@^ z=?!qIwlJFw{E)8H__O3D`#eD~80RmfGB#`m!!hoVP6C>vnr%n?0JDVE_-Fd|53(eW zE@lz_Dh2KhJ@&Ge@duX(;9TM7Xv6(qKg1tr36Sm$q>eUbr>FIh?g^Q@Fie#rO(#oS zLpd1+-p(8Z)izQ}caPmaz8k`sGxsdK<1IE8-GFsmUPJ&^!cNnP8ww z*H@nr&L94%8^84o>G8OGJ{k<^N3`65PZl}g{Q(D;Q20^9LkW81%yE%i5W=x+y-2Tx zwsrYg)S)B3I`MAh#uXz>Tz&CdwT$Nb+-@1tSC|A}n2un)!p^Q3?+rnc@y`9ol!#O`X&=(ng&?I6jK{toFJ16x7qI)oK z!^mgk!v533ek~?r2-;%QH8wzA&i2IzdcLdZU@<|(G$ztu$7>qjg6I8#iPY2j1@ z^)oz-N_I0qVWdOltX-wk1N{l8`c@!HQ}KEjSNG#9zJ}W6%?Oll`{W~ zu1n77e!=^5(&~Sz04HEl!TIrIiSuc^tfRbr6cS<8E2h{CQcU$Lgoc-+#W;7~DQ813 zv7iIT_iE?WZ5o!rucIkTj+JME+lp(cy<^eePCvfgD-!gkS<`sx`L+5_e+-ZY-lk63 zz4}ZbcRD;gc)Vt6IJ74(!#DEL!QMHyQeRWf`f7o99m!EHq(uP3lYMc(iSo$>TSylKt8qzvoR)M^KjfxC=L&{q&p*@2y7 zy#U#v`LXz5MgjLI8z8_vEg44=5y2v>TDOs9XGq?`qLH6JNE;G`NMiM^k(tJ9^pzPw z$5zwd)o)%l z%^z_X@EioIQiIfqAJi}d=uIwqe(5-0{_Wh`i^q`lk$3w)P3!y}MBNY9T=@yri1UgE z(qNe2H^Zs!4d<`+Nq4bDJeHe6*P1LPq@wFY3I@U|Jdj>7#y$27V~VYvlF0KO`5(D@ z&bJ0j%g2d51*nJ+M^pdInr_z7-y4ZiYuv9LbY3sgc7ovc!YFEM^N6y~Pc=u?yJpmT zU!0lBR!1`!CXKX$sBM=#6~QhrrJ+oIOM0OPMT@$!J0i$KAHUk9vrL-~|PD*u^ zAj@8Q-a#9`BHf*->`5@E!(ucKc50I?OLY{yRXDepck;;`%gMz0D|TFkWTsD6QK~M< z7~>RG4nG|;*YB^7s@27$ZkQ^islfv}R@>%(-8_Dic&11(VXjxCTpE?& z(wCRgd}Dt8QROwgGc`4U5;AjI@(DF5vnhafY#&C--`9T@l3xxSaABUUJ)8ldtD=_) zp<{(8Y`u3-ivn!ElN^5EM7MWZ7c15#dHQwEiKgwLby4WzC5iDI}-* zZ2_`gdGm!>4d-KPEyaF|g_QxaRR;XWLHc5&+9(S*y3i=@GA1-e`m+|;xHvE*+(}pX z#LxA&m`ybnKL+rb?s`ZO?)$%+2b0XvBE-yk z(x2|49i30%xs3{5T^uPdrYGem^gav!`Po19<0WQ!Ag?&Di`XeRS8_IXsDGcRwK^z` zA^BU4Yw#Sa&A`f%AB^{foERLWGO@=0iuMZ?9eh(wzHv}Ky&yka_P(j?t%_ydJV-IE zMz$1J82Uda%(`1C3@&|F{OTAa`j&rC`Pt;Mmh?6Lk{59z8%gx3UJE@L$_yF3!5<$caHu`9GDzp0`4IrL8^YZ=hJ5NV zh+)BaX_6BA)I$-NwTwv?SeFA$tD$XbWng^t`oi1^U7!?o*3^0=DZotue0>Cfm(fV2 zS8(F3GMsQ)k-J=vCP&p@B2-x_LJypx}^)t7msBcmBVZ`$v8d#sX z?c4{VTF;x}n5>DRYMd_{wDzJIAf4f%fwwCxyTv7JW!pDfjpQZ7*{>ipqEsZ9BXLGBG{S8uk2$-lGsfXHIF{qo-L$56{UvHdU`trUx)+x^dlU?U1C*h?1oll=n+X(-xlQ~%dv-!`iZQZ6tzvVw&pFtjt2n?6m79;PpcIP?f4{nXac}}^Ls;vILr%>o) z1O_*Wu0Nahcy{FS&j-aYla{MSQ|-C0;#wZ|1C8SczHw@=?GKxyt`zf$}zwCC#erIc9%tSs$2 z|50xfNZkVtt{W*76UOJ1Mxe$rW!i9WKMM_H_D^!IOG|umyx;^b&T$}gEgT;9>g&kd zhHG;oOL^Xm%fHRAM)U#YYwynw)13>no*#GWt)?0kN^mJ{qW6c}tJcxU-ku>OuKZb73T{6BGzLGTKij6ZsRsW#9TJ89N9z(J;;A!#=%Jw z0pk`-6H{IO&>)KbmSZ(lVbG}8{<#dxP8~UUah!Bx2Mqf;HT#=~9m4+h>YsOi3kK)| zD&~ca)<*$&e`#$QVaW&`rW5k$M^=kYt>+Es*SZ9TyL2h3F7?b@9dqf(oNp<)LJXbl z%&-#>^2BPyhIoCG`{~o5rHGirZUoX0vXQxvn6tUSIB#xBK)69v~6)B(f}_z4sQE&*T*NYTXGSt--`hAM@d_8_%!($J3Q z-5PsqT9uR={R4-5L-3j3aVi=xCp1)xVY-d6I#kDPaU(6*i{Yv4^lTO?Ptd2U9Z;@C zG12ANR{2NC(Lb%>5nfLvq^#wAvm`mv_+a-2#ubuIzMzzAoy5-+xk^J~asJ}nW5ZMK z0)~#gSr7cZpRB;19OG`h#S9-X<^ucrzj=j|E*c%D3#n_1&&8#F*z|e+YlULzJT%~$ zkwJ8D2*&+0e|wC;I(ie@T?2k3}V%sV$)dnf;iK%#5KLI=YkF8~p5Br_?y# zcemDQmbHznc1n#NGe`ERWa zFO>C3q9a%(BlNh>#LnO~L|U{+O}^t+^3!s8 z@Am%iE%>R@r9_Ho+k@cpNR^4`!o{)Z;?~J7m7`u(ON;p^s`U-fz@dfrvmAh8J9W;2 zd-2+*zvORvEi5zh@3qn_J>?M+Xt?a5gWl>A%3=^5!9EM3#}3q3R_glf%D4d`=ak7m zTCJCMv-b{Vc)MSen5!Ai;wm{MM|5vKq(gXT=&mQ44-S5#u%)gbT>{^rkDjBqX^IK( zj@I0YqCE%tv!cn{GlbPQN0)I{MO#f>lXC$yEs%IQ!VwjV)kePP5L zbj%|P4eVRX$28|a20X1?rkh?H-t`)VUYzUHT&eNVfwj+4 z{^&p)^Qx8yXeO)25F__q@zPUCmhn3+@w`Y6iQ0s;FAVP-Jx%iw;wvH4n`78~svbUB zUt@lK^YwqtPF3RYEAsZSRh*{Nilp}*-}9tAO4?Uq>3NmAG6m7>&Fs#Me!Wx_?kYjcYiUJilbRPJ0oeVPu-!X2l?iFdP1AV5o2=&@Ry?JdRq-jZ>=c?gA>(41`GYfyVb|pM zokA9v9+AQI5g^O%)rXXHp*KP=rP;32$R2YKrNGZ69NUhOl)tk*yC(y<4V-FA_JcnyDDilMP}s=5H@!jAK>(TShD(K76-iJ1z7(0= zQS>SV_}YPr{4{Mf`LtmjnO`-I%>m6ech-^nS>X~@kSD}#tGuL(ZVss>s66#8I$Axh z<;>^Z4LCD4i)V-z9h;zOOccxkxPJFucV3LbNR&)2OMIkd;(LS8Dacicn^}KB zGc1}-2^d>*M{)3`@G`-y+AXB>&;xDTOCpu8Kd#=%kA27lm_9mKQ!lD1+ZUf4;sTho zyKvv`n&){$8U%g$yZSP2|2%^qD;=gXdGPCni>C#QbDfKZKXResI$!ggceW_fz zjSPk-zT8HE)7-PX_&!Bgb^NDPw^RGRUt@o(4U>BPfTU&T-q!DRbD!2#)*HMt`CD!! z&?e3@gguLWA0nu>8J?J&f@plw$%#tqegJTvVj0LW)HafC3i2PP-ifhd_WN|o*n=?o_&Ro^_88@_H4ST|2t>>n|ZE8f2}va^cJtv+-|MgXNN z_(}%;!uMn00Q`LQc?5=FAxfn@^h!_FjL_z9+gDF-CI_T7+g18rdy^P|anvgOM1PGx z@IzL(l#dKB<^_4oTm@`52&jX5l@Q}jU$HfxYB~8-Dj*|yyVvDyI6&Nn=;o5LoOS6{ zp2*CvhX;m^ragWSk2`_2HHVL~EYXCT0v_?f4~~G^xqwSZr9mhcA*aeP-c{SJuL=e6 zq1wEqK6zDC6bdqrJF;?*f~cILChTMV`vlQ&LF?z=d6gWK)5|Jk-Sjg|KK_cWViC-X zYmRp||Jam;==_rl+?$UuFc?%O4sR6kjgs(HDwy#G-8iLKiM_N<`&!6)&ZswES7Ns<{%PLu{yVzRB?CW1}toni zrsP{}+|mKZk)u+kcB?@%Zeto3Fb!@NYm0{SK2-{j(WCD!euMt$(A{pkC2_MIi$0{K z1Jqo)A1Z8~ZqG14;VwW!$cEwv&nNNj{Jwxg4I0pN)!cQAz-Hh|df_28pmzGMwnDFg}tyuUi+P;XLFPk4vvaozR`}g7 zPQ&pnhxKo%CKuiSvL4#j}(ucM1*Ljfe3}Y6HpExF1`%1iyCWq=AD#S$%lY^ zFwbiEE4?M?oadwaF|VHC%?)!`XhP$v$$A^*#72vHHFwm|wnd1=o6?=6DnYCsQ!0hq z-3EfP4u6O|HP%RviGism{IBevAo(U??BG3g>;}Myt}6@qxb&TIWb@kRD;xPwb}z)> zx`h^}-O}_R_q0vZKlS?f%y9XkaZ`@-o=Z*ZHb$o(o1S+e1S2KKj4|ISRSrp2-G)A2 zznavD!#$`bg@jbWw@UJ8Pui4Z=veJWtyW#_Y(SNn)I^KFV_ueIdMw{HnmG?|Zl0GCI70n3^^(kd)jG?f%`}~jarXX&;SIYE>l_r&oii9RfLU;WH^U5H*#hpR9tlw>0 zYxC#%3mpSkVSMy9&)66rZg4YQ)~?7~XkHjAF{BM$*O`v~6;Iv~hF$let~&X-Vgg*V z%Z&aLquk#xmKNgjT4wf}$e^wM3En&8*cq5uC*BJ3%44-y)2k%0#SxeIJ;j%9Se*WT;a15 z+pfs^3*>Ax(R26Q&0mBKF zfYQS#V@Ce&#|SQ~G_srEImtigKUoRe`)KQI%3f^-z_#}_WF9=rQe_4uTs=SAEYtdJ zMwa>7z`31aRXhjXTFY0g&sGg)|9o}v>LtGNlJ&gDlTT)>l_WHw;yM9?%j^rKKf?tg zHq=9on<{?O6tECMTw|eQ`?aA|x@}gul1OxOezcpytelfnsff#A!K+dr+n?XrftqsK znE8%?SADc=;73Td($~}Bg;9d+mCCEMRTf%Bx-b$wG&|>3^3`PBZ?HD%wa8P~!{Pn- zqSo_OIk$@m@7O-eD2GIjX3bF(lO`vGe7*W3_XP zhQd~@>i=oO1OC;9GbC%WaeA(eDbbqnAW(Um_Eme~S2Ubr z{*qB->$-fqOIGc%$V||rb(D^Y``v&3{fC{nIH~`AW!1m9$N%@mp#OjV?LzbFziY^b z)wqXgD>OG&dgAq1JHUP5ez0ZYzu%8`s%+%Wao@1l@<0P)>d|q=H67hc|F2hJ>9N10 zDhoZ9fF3P|(V~PJ%pZSw?1KBe2bZUnXI9sUsH5a|HoeZXoB8~u%4 z^ddW9v(aC7G>mV$3*#oy3>=A3yq0HqZFT-O(w0L?0E`6~c|e**@(qVZ6Lv%0Xh4dM zs?#0PCQiHXj31aXojRa)cv_OY#^jJjA1;nY1KKy_i~`Q)*k$t?tm7M{WgrYl~2bJSmUX_IrBcPclblBR(xhA$PyCt$^dyF&-J%{Eb6BIv6c}-q;F^U!qK`}Z}(`}^AVX)-c}ETGj@vw zo+UQCRMkKCPGlgYa%uGolDZk|qCwKvJ}DeCV+xn~bipC-l^^Rc7xmro<)H1ma?cIk zDahg)-q^&2Xb&(yW>H^vP-<*piaD9v&{?Ck41a-VG=SUc41iw~$EkNw&E(UQc-lw)VoFPvqnA3_ZQxSbqgME37SgkrBNqf%h%A(^X^D}VJc zoVw$b#|hg2(o@fg*8n#dT5a`D1$O0R3xA9T1&faD8=s&pFV94eZ#q+G6H8yjsRC<> zEwo{euN?*VGU&4HWthOnOf?2opPvz@+-A4l3Ly|DcC6G^j^o;DJ7}&e?KkZIPzH_o zv1!Lsmzu}F@5>viQKSSi*d9h2fxxg_zSkK8+#}b%MLYf<;yL{bL{p3akC+Gwt+jRj+FX zB5$vAp}~5t;rW+IbKg~z498z74BoZcJcQ&(ZFM|39E~(=arR>&wmO>Fnb?{^!4^JZOJC7@66rDRcafRmDRc?xInLyYzF%*p4LA3BrN-2Aslc zpmwOgHSp4~I(j602bA{9b#O0E!|=9#G*tNDC-j6CIH5>0-sLROG5819&V7e_Clu$E z7ukb=Zt1&u+=X6hmKZwCtWn&fBl-)zS59fToALY|3^FYMhF!K=f}}a&^6>m}s@AK9 z(|_8(&Z`0fFrwc(TgcrB`5H3rnpYc+b}`!VhOak;u39GTyj%F&R#$47oTt8SC){PF zduo44;aL-!2iW^^`r2-~pn$p6gtLvl%%-!;lxBL7`zc1lM+qLPvw?JYMjmU)D#Ux3 zEuC_k29CKZX*sksnV>}UCmaeVHz*NXiB|B4(YDq}K}N7kG?R1%BM<$#SqW^E-4sb= zWUa&>B{h=+ar0K#%QRPxo|V^W`?_k$%m2`os4(>g8mgXbPSPrgwC@Y3Y1;XN%Wdt5 zv{uka*G^5v^B=fwUu~X=gS?kZn@Lna9gT76&-bAdTt!Jq=C8jbqkbld(%`Gxz_mKI z(kYW4yEh5{*4e6)gb%$#Kn{6yK_DDozm;tvHaumLgXzvjS*{iBxQv2_G3mmWy>;OM zCIa#JlVNQ4QDw4I>g0Nuejg~Mp>Uuk-d4q`(Oy5tdf|99_n3#OKNE$=*ijs}1#6V_ zj;{Z?XZ48T)@yXg75hmPIb`gN>E1eC)b;U|{hIr=ljh<+48`j*m9Th#r&H)ZgHrx4 z?lss|`F*S377wXa*pFvm*J~(%=|F#WSX5;r!ZxF;_Z29&OPU|x?dQ!mSB9l5mFKP) z#0TD<%y@Kipf3mfr-@N-29$7Y8EkD5S3z`opiwvHQb2t&*5uOSdfQu}h@^XSGq9&_Y$WU^?sG9+k3rMSE~m!# z{8!K$v}#_L(;f~berQDI?eg4eV94G6+f+kEergVe?y*yCUfEdI@Rw71LE?u-5)0-N zQJ2L61g0E1k}{1N=*M1cs7GfqwT=r%`D{WjfXHrbGw!JTzUEN)Wk`>M6|V@kFMT~) z1t8mtcKJF})%fELHLAq1g=f?MEr$H40y>55auO!ou4C;CovU%9x!_l_KqI~vKc;~y zXVVBk5?+iz=~BpLe$dx-*l%qdoi=?3SfcqtEmpdYO16kM7M1NW zu{Y~4hkl1D1*T^oE>cEn4)PnRVzssR%>Qc{!g@sHHcvE-h8pm>Gq#D$?G7_ZBP>~D z`=Zf=*txD)=!3xdm985?$S1g;-BIoQn1?7nJOKZ_!Yh~Flb-z- zO&CoY=E7hEotcHwN%_yMOJ%CKHBSR*3f&Rp05~$rJ}T01?cS?C5IS(s3WETNcYtXD zXNg7|bJ7evZ>I@wJO5vt!p#4ZQ^<#>_GYMI70mX9D7#q)W=jN`dXMIPvz4kNEq_pX zs-L<2C;t_q=f2BzV3cW3M zj6lMAYF*3QSpSl8o$zaBvc^6&Zo=Hf64C10zkRWOxx&kz&=>*nTRl4eYXTvzngNn`-qL#7lHA2 zK57q&db|4EW5&3infW!osr0EQ??I0zGYVv{mH}fbM$1~SFxrH)TDe1+$6=3v?F_B= zw>tf1MBpaXShLNfIqb&ti*bts|1sKzhDyI zVr{Q-aGSmp?se!zUszuEB4bau1bQOx?iu%5g8k$j97x2wL;QbBlHMkr$gc#v@AZcjbT9nQ(AcG{}ZRs2V$eq42Qb{t( z4R1T+@^Zu~)xZ9ep+dy#V@}J##tPx`gIK7e&Y7=G#Lf-p!pvufiFK?F#^1Emz>t_a z3=`Ho<=_@&^5#}S)s9Y62YG`GIRJ4Rg$A5WRrT>rs&V!roJMh`*1I`6XIKAPg?1xs z`JGvPVP>!aPvEQf_w=ui2kbw;gf=p`EqmQDiO5VNF#QMC18<+A*o!1f${up>+@=a~ zLEQWJOF}f}??S}Eo_yM;!={7pa_7SIzz&D@c};?N4_LSL6TP!;8vMTm-ML1KoYQJ` z{510g0)5DmeeheRE6tqX$h9x1#LGLJd6R43wuCY9m@0Lb&w8f;QC6(Cer1SXuIs65 zeA(0)(Rcp>h0!{@kK2nC^xRexq@+6}#PQyy!eE)^a1XL=ppJSe=L zcOcNyATI_Dw;jJ{{bqTWg;|F3_Ee=nV_}Uaz9mpvTUKc{w68vr=gDhuuetR6*T^uJ zFCPCB%eb56x37KI5-I9aOkU_<=aQEK7gmvn3}j=$?+MulfD`t0Kvy;FOFsx&<>I%( z(Uc|M=yRJm6y1ovt0uyQ2KA?y3!>XRW?WCtFsHHb=`w!AzPki>4ulDzsdZ1@-OnQfN!Ilb>rW^Q~#0H4m$Q+EW$0> zNLypZ&6}{Kl+(|6U9x8ZAtKCjUXA>7%9(?snjtc(oK4NgG3mKYf4>xtXSE+~z7abV zhJ<$e<7Y!^=cZyQ`kwY`4#@6iAzGa*C};Sf20_`3)<6^HKyzhIqmpAn+~X&;TOw~y zQ|o#jw{?h?MU^S>b%#+}G2V2O9`9uVN<4jQ@w7F8xFkBFtI#g1>P#F`H#<4EW`<%h z4f#fFai^MkW*h+nl{*qNmoNzfW3#;7zgs?0K|S*=-Zb%sr)5=dR#@pY?(K~2q2mR? zHo!D^7*H}n>)YI_WhjSVNgtnMpCuN$iub7j49JXRegFy3tURrZE zXLgTA&zs_99th+`w4cqbr_RNs+s7O-=>r>t^EkM7`boA&rUWi=O(oRsBky@IxpNF0 z@93^LT=z8>S~ps-6$fFOmYU<~joXUSQk&PA#dYeik2gG9e}b=KqxZvJB+CnStgakM z@m4Y_pXw(@qyJaf2!u=$(SI2up51r3b#lZpHQDdK$+q9C7Ci8qf3L#(~x^c6& zJFO>3l$U;>BqgZN`z>>iOv`1uzn1ZP7<8|p*e}d^>h-B_da18@)9I^M# zQdL_ehr3mL^{!=JxVdtkc-EfIlSz$1or70lR13fx|G4Ip-NO9^=JD(}xFoketrK+dt2{WL-eI>02^91v)n9N}2_16$?RbT_?cQXB$#*)lDsnNijl;J9m9RF*`t*fAu@kMXxe z?t4x^8Af1py3A&CqOn#}&;(&$ph%@+i9h$_EXX~M>jvVjZ;uZ}hM{M<>x!^w6!Gkz z)iq65?bBy9!bW?2`Z%LYsqitM3tLrlhu?(c8c?b4Q&%GrcLm>>O^4k}A=IMAY(uSOQpmc!4d*3&Z>tqZg6^2&gb<9*Q~dlj5i!mz zybrw0-|0OjSB%>3=z-?N7cQ(SbEk?i&eRCPJj_R)6P@%Zxsf%FmR)(freZfqoNC~7 zz4h9rE|48~8h&#+Kmax35YR-kr11bb?L02MGI&?2)Or=my?KYL#@-GUsGtA0=?-&m zzzM0uAEMwbuNk;&khW*yl6C7mcdS86^xcn}G~YPP^NrPhE_7{%V|H7iL+<0+g>8F| zh!Ef3)6Q9`E`S$WgZ|=^-?Gk|T=$AUbaD7?)GDH&iOMf>f?Lo$wVybZW$Zg#?4qqd z`F?8U|McI}7~yXS4TEWa0~cdgrSzv}8Wsh5eqdl+&AUAcfd}8nVyqqw|5}Ym>W~PD?xs zm|`QLXF66l^;2}S>g?fR>^?paegzarTIeglS%vMj7p)u8;YC>e^TEueG((MM+_E-u zRnwv(hq|+2{y@;R$VHk#b*;ix$@R39+6#QDM+y?_n>68o3S<6!@#KVw=)D7s0xoUo z_$_{Z{wQW*8w8#IM?>jx^gi<=hN}4N%2=8{h7>Fx7jl-AxqcGL-0~EPycNJ*Fq(T9 zDH5M@vR@P;0MC)%98jQ@3o-CJYW)A6$**@~o z@ixacqXTC(&UfUmGZSD1tQ6nW=mLoq7ULMsrv%$Zl*>(O3XbG><}TR0eWU1Oy1aMAYF2=m!#oh+IN{$pUD7q!nY@oG{d zW^(UDp;jS7?c@8$_0=O=suau2!;5d|H?Q>lohp;H2BiNA323_Ga^WHo9nvYMzSNP*8E6$v@e=^;Tn= zyqS@<%7+qx1$xJz%_>E&fSIi2at7B6BPcw(+>e^Prs6=>APFQSAuz%3Ilq}X zXWqd4o3)a)@;qzbyWH1jU)RP1R#Q1%w1R=_fGH zyALGZDBkW)DSy6}xXS!*<{+_lrpfi<3+5Px9^9^nfq6YtyOykw7g@WAw66YW;&oe) zvqXRV(OD0Uo&`6u-KoDNa~AthnQ=h(QzPDB7sL?ON+bSb_+Fk_Rk`_d(H*)Zt|f9$ zUNZQ)hhsnChxX&uG!gNG%$||q+Ap$uTGbt2KMB^!ealHMHyj=cElL8Cr{>z%nwQ(kq?%Dlockw+hQF1;fkRE9(VFq8ertZ z6lNDD_LR$`&*QC=TX5i-`BG}xUW*Wte~@nXzv3)e`$@)?&IWKeOxsp@wn#P&p1Dd? z-Mx){tj&C!mpE3RE)_T$AmP2iWD{RrE=e_KJ>8Uh{BhJw!zWUo6L}HF=G%Yq)XXAm zBp$lNh$1q4pS)Uck$Sxq3`sg~IpjR|f$H_uJgei;=OvS>>=%gJ33^l5*#r&^7Uf2Qb$*OZIMTSkqz2(C*&?#*CN&%=PU}M8>Sq!4WjC4a z2i^C7Y4daaNwo(0GBTW{ttyKDGhsT+FBrIOlW8@BXXLk17DB@xhPlo0juNoYuXH0j ze}O=*5s{}?JoLeO`I1I{t@em@z=FKZ(%TL7)ba+cAu$_Mg%!T^&|fz5wPsOTMLX2} z;LosGCF)ZQEwD8~@=+ylE&WmPDu*r(@sL9kRfW z2t@&yfRcy#YRre4=z13TlzX8uzGHG$mlk=) zpiFYjueLsug5MM~G^fS=a{h30ybln@fo9pxM9r=96dh>Sh4)hydhc$qoV>panhZ2@ z!xppd<#9qjwHV#!8O~y;#f*k}^e#VIP=cLN^R;V|`T3Cl_A*=3>bfi~8SC<)A6QN` zlNA(tAv&Y-vvFg0VM~LDy>Z9JD%fSdo=d)16z%-#Xt${GVS#qppCR;H)xp^{$Ok>e z8omokhE8&B;O`p83F4*)cT%hQU*)mn^lw|Tln0+|;cH1rHbcER{YNssk(n3mFhn-# zt9=&i_ErHK3PrfGnn&t*75&tq#Op$mA(X_Ut}v5XyxKzQSlaQp6xO97RrfZ24@EfUnF!MhkG z@0yr{Sak#0hd)zO7x9?xmR&q<0Q-vbTr4!BpRha1_tsZkp`w6L72(yez};lN_~DBs zvg`2u+eubr!}O?2D+Y6+Fb5&xWDZVEqC&Hxgi#Ef(0(y@i~9 za%D`=EDByH3=m`3SX&g$B^bht%p)uDiK%3c4Y;cHWRh$GUaFAB(>k9)GFyU+h9Y68rTd;{K;MXl zcxU#esPA=WTx5AMn{$L8^0Yban{t8q-RUI zR*$LI^%A!nrz1&GeA69>ix@M1T28tYL!H7T4MvD17;Y3@_H+3(F6bN_G1 zFZhB0_{y5|QzB-@u4sa!kwv5M+IgdG50>u)VvqraA|nBD8K}8co_EbX@zFAhw8vNl zLC|aXSL2D$?i4^&)nSPb$Wwcp#t`HA{x)*-g53LiNn-EISZ-aC%PoVd6XvYvntWqZ zFzzXuX2}<>`CJ+Q@$gJr9P@NdiGQ&dNcOKSO8?>qN1q`rwE$Q|ASk9!W9+NjN2A|T zNbz;YFOh&a9!Jg87_S#dmaWydQd&!)s>9pyE5s$BQtM|$zGSas`xh+POtoMd#Am^& z(&^tCB5cZjen${fV|-gDuPbXiT#q<~$YlVymm+u+_{M|&uzjmN< z&@Jfgwq=&=^~fXX6CpmKX5nX9rBOZ*7qxc3T2t40mP$Ie2u_JO)eC%&gg^(fmLS{-V#WhJW*e~j4MmEb=@>-A(JdGnVSI{xi*X;1 z+7vXGBP*3`4W~+2^>%5um&@54ggd$S{%G`zCdqeE^dbmyg>lddh*3GNJshk7} zlPYo<+EBy4jQqf#yQmm+M|YL=d?Ls6PUi?EsCDNTQ?`8xh808foK0 zT`xb)ti96a24jK4o+jcBocqDeNp`OD{?`-PTQ*@D3XN5(`B%!{1I?fZcPh7}-%fp^ zP7Uporo{}cX}?#0NLu&OVoKfQ1@(=e;*B}7co;ITzH~B0R1{}y;~FUW$_^3)@5%yx z@+-Xi8hgro0;SEh?oV7$jc!vNnddc4VE8(YU5+v-R5ET4b`gDT$n8;mzntT&M~{-J zx~{ecGhxT~9in{xz69)&hsFUgn_-T_vTp0ta~KiqO{$;2#kiLWF&@&AIxRIT799_s z)KJ*R$A6Al*_7MN!T3-*I%FL3?}-x_TQ`5xTuh!t{RNUx22+#O*^3hXQsj-?0l|^0 zCXFd^vqFn=HVY5S6n=1iA@6p6>#jfFu}yq-lT0*Y(`L#EF!TK`e{V)Ts$|8;w<1SN zxHRq&m&}j`;4koQy6hMcI1eAw$vxwjJ`zMr@xC6={7LTOLcUNIYqJV)(^3bZ`KR;u z&G0bw7$u32Z2L7qvqD_MyF@qgR$k8lTX1u#oU=K6J`Lv?M%=RG%?GM#j*{~fqvL|uk4UK&_xal2C|nif`If-y1*osm+oA80YvWR zn9e$_ED!Zu-GxmTwxjcdIrF^AgEba_9x!_=zmPauKPKD%^0;?+Hv7hLAqze!;B-7) z60;OZYX>z zn(L|r`88tV>e8yodKq%sy08IAN)$>S8>Uvj05tH0|P^vvES} zZ2h~WtN%)YSsYHCyMOxjiI_ccSHMSSxoYV(hzA|g;P zP)VU<5y6tQ$KO?#RbHCVdsCrQf5Y3rNBK&LxFiyLE>R`svreE{q5$$fxm8ZE{FF#4mU{?RqD)FJ+NP$wJgPmgz!d|Vm2-Z4R!8DGSZl2LQ-_U5!2X&jpk^@C(?zb2bePf%+Q#9^M8~#oG4Xb=Z z!okht+sH1@qV`81y-P7t^$K}YUlryXWlhGW_Ndum28`zuvaU*_dvKCfeXu!NLqWlz zxU**XTrs9?XK#3GRhr6fsS1V5fp^Wnu?V?N?`UlG1ggUZ`M=5X+EJG6E-g=;Nd-en zYS!F|FY>LJ3z{; zqCRF;Y&n^6-p(B**ODQ+9S>qf-r#N z^p_rJDfX*J+mK@WfR-7Fx=mi14W*jd8(FwB> zt~{&H{t>V-PVXJ&)A-4eUlSMUvC5|Y4a(ndsiXQxF%zm>?f;54@ z2=^Ea|6Gd&awE+K+K1O}@_#AkEd9|5D~97 z^lS{|D}MSjO8=#8@XHWQM1|q-uus3o`A;2mAPOGqH*KXgB*ed+L4L*AT5*4!DbZGZ z$88PKk3H%Zj15S+lGg=jZ8BoP9(OvPs_G}*Zbc|f`TBbXx7?bBx4*zj7$nwbb#sY%0{-I3NG`;SxH;WMM z=8p_B^Q2guPIuZVne?p;>_i=pm-~u|>9Q8`9sL9@`3|io_CD?uOWr3gkO5m}<0SqR z{rK+1n1mX{WUBAsRj9M2Mmke?OZq`sLB%#}_0~AX1A5654ni`M&PxZ>7|Y+*%Gqv$ z7wyATU^9|w51ci^xdw8%h3zc(822xi=_L@BrWzNszbHZ8#*Me@{tR~({ok8G*{u>sr;EVvU8LW?3_Z@K<(P>r%6ZqK-*i-u)&zn$cp=4yR$pe zJkuQqZ`xQfR^w}iV{I6k9DTU|YPg}Wcm2R+$!u?tj=D42txGidO1anT*C+j@NH3rI zyO19Ea{m~aDt`?bJTC0wXSKd>I1C1Xp?w33-Y@I;EH3S;zt0@6a-Q818LW=d-?Fyc znYz}tkS*bG)O-Aalx(mieQE0G7=e!F`KEO#@h6AF zHRYLWT}GzASF0d&hL~)`)=8K8=Fb(y`JYDHm^B_XY$pBVKDWi^IywF4O^9IOOoIlW z@d=gqX(ws#yn=i>%2{K!Yq@bKW-79mMGt_tg5nMG9(|fw5o!GNn&(Ll{Z2HC4Sn=vz&p1-{55vJG9FrT_s@xpdcCP>Uzm{{MhDd{=82o6$u<8`Ljz$HV7 zV)hsKVzV!mb!9rVl;M!E<2_NtuzI9Q*SQuBdC$Jw4v=gYUb(K?_MmYu1$u3#{=(av zX=C?9(?dko2IPi^8o5e(&1t@)MdD@Wm5U$WNafRIja5%QPdhkfukeIzcCINSf$8HF zF|mr|=dFWJ+fR(-8{I40(gRNxF0gnik(!<^x&FajS0XBdY8-a@yTo4h-L(^=1s}8> z2WEX|tT@#JfzqXJoRk2ikjHR$3oD2W)s&FHl<)#?H5#2z-?ylf zb8zzaCcO#Ir$}uar-_yz^ttm&jPWc-X#HL zVHa&rb}-t#de$uLi8CuFw%zZ03H8lYby9)DB~kspuzIb z=!s6Rw?$drba6=s?%W>GQWH<~eLyVi$A+7b5<-TLK1 z^R-j6V)%8QXkEk!;arc*=6%kn9(YYLHtd>~jfof>Ia^HMzwiA2a#K2@IgVv$|Nrf? zwhG+;L^qgz`}Q*64fAsd>S*~&oblN3#^fNX#zE(ThU*lCd(ZO8|MYzS@MeJFCb_z^ zir)DX%EOSL@IVPY{JQ`3LktS!v(&6l@bX|_nxEfnG>$qS1_;TO#zWX30EiCoe?GFd ziqFAH43KBw9k2L_ zG+3S(%)YG0SQTyeC?^iQkxxutXP_K%Htj5G+|1_H6?Vd?qWaTB zbWK%02;7GP5Z;9}Y&C-S2sxwg_Zv!A#T=Azd{c*+v$YS?$-|jamN}lZoh{Q9IZcc) zLajPN=Tb6IHHB!0d*)!MO6g&)`J8?Z6R_ssN69#_V9NJ?ls$>wg$_(UYyWAg#L)zt z1eOOXmYV(bc#9D9fB4Ep3o4Zw(g;HV|9l^MxcY^EJb@%~655>?y*=!?b11Eo=9_e3 zH9aD08JFj`W-eA(2pkXY;!(MTW`8F-P-K_?n>ec9x1tqDF15WmR$tKOK%bTx#~fbo z_J{tk{Xzi$#Y>*reY6(6;y7Tr{}8|LS?WD}%%59Y6%00ie$F;0Km`~DmeYhHi9Y9_ zz5zT{Hh`H5&QloAsvH$SuMQXyTc4Klc;~pSoF>j?Di>pm2zc@j`*`c1z%x7ow`EFu zQ_k+w_%sc=bnb%kz!jYDJ*g#S9qCMHN`5)&go}AdUz?v8XXP;omKj@wnniK*OHb3_L825& zuNa)UvF?yegE}6DOTGlR?V&R`hquS=P&K*Wuc*?`=o*Je>G5!OhxW3}QZR$;0OqJp zZfuNVRAk6Q2WVgzhl{clE=aHg7rMH{F%M5ORl^Lf|HC|-529ei@00I$GNxho{+8{CbjuP5>nG!#38Xe<8kw%N3muZ zqr5mx{jSx%JuE>!6h^3Me2xI04h+fQdk1wXDn?4gB(J$EEYkIEMME=fQL%nrkpc1u z8S-cYT;&=PO;X8%d}e4PbZ9CZZi=4I+3`GlspobdB?m@YG`-FX_zC6{npyB_vMLx% zD^HntcFr2)!6)>p^{ba#y7M^SnrR1M?kk=sHN5&QcX*xuZdEBwjg ziyi??b>*iy`_OK%K%U(y#qlO%#!HACB-xJ$vCckj>WQbi#13DiFX&|8VR(Yo&Z*L} zQ`@cE3R6ePc*xu7CRRr60e(b$S6N5Gpxy2@^e=+~x%ts$R=)K~SMy~2Gpk0wKvC(J zQK`i{LfKLjYZlOTF81g#)7jo2A@gQwV2)q~OJ$pdvREaN~$E!c9Hv)D5_ z$A$9qFLpA_+Z4BhKb^ljM$k3VVK~7jhz3dxZ%;c-2FNyPQ`%0+6)BiYpj>EPk4m7A zm2p~P%!h5dKX?{}oe#qb3b!QpKU$PNaR5GMjEvI;iD2+GvX!u@m7r z-85+);K-rn|9nE_3Ugv;iOwUI9Tg5cs=c2|Nd7awkm0L-hF~V&iSg_cC+E`w%w?k1 zD{uj>76FrnJL^~y0}2Tk7flvMDp=v)wBC<&Byv2|5yOm_VnQG9(pq(sBM}FWw9G0} z-<@Ge#ngqzyKLmT*OuKn$#Y40o$|%DCP>m#q1{q7Mb`5y_C4nb*EIK8b`gJ4wHpZ3 zwq|+}%Zbe%cznKT7d`AOD5n1*UxD~Y2)dBCyWHa1*0r+2*=Jt|37%I^hG&G$0_}th z-*RU~-pbbMqu^h7?J-Q?Mir8nMbQ`2b(!jbIAN@KM8Jv!Q(obsjD;%Re`vvBsFjD<7qw%ICi97!t3f& zVa!K(JH{cm)&sS3O3=Z7Rr8*HNZH`FQ~kOEZoP@_*7M8;(A zQ%SGAH=;jDb%y-HWL@wY7^kW6dOL17E!@GuFi#-W>7Dj@!D4s1EZpUYA@>irZPvUrV%ym1VkoE}$es%Ds5LM)ysa zIIAMJWEtzsiv<>zV4~GcNvNvmziuK7N6EW~8@#CaM9uVPHJ30t>0?XXV`YS{9KE8I zIyQQ0!37=}p2dzkPt~JfhPDlH}RKNRCS7;4% zCv^|RuQEpsdyYXA?$B|GsKSeWsloGd4_L<3X<6gU^Ws}ax}pSGA#rdplqN?7zC6Q& z#~<3DudN05QJd}ky*z_gS}s|ywMvsC(M=Hpby~kY5pPok9lK64MBVy!FCdvcY-NM; za^K0s9U@@MB!1I4=xtz*h~yj6f(NpKN?4}w>Lo`W@J{n3a-4cH6=HE%LJ}0teu=PJ zn=Jbx%QNbIot`#%S12uLkf?A9|H;BulA)3yG2|5S;!@Rz)ll@KC7Tsu)HM6>(gI13 z^1aIMs?4&UbOv%G(By)pU2jDXsf0TGi=TP?@nX`m`2cqME5IdgJM2?ifQs|S%=zj@ zgw_ykMZ9`L{(%#s|J_}e5Bqa{y-4G&{B(7*%Ej&M-z%hvnrb2_mo87#*jK~_ZvZ1V z&QXZZC(G~4X`wEJZ3u){U2_FS>$n?lWi3!Pl^*=P)IO*x4rlc!*ygDF!q_2w(Vei^ zqxo3Cw7HQdpQyYhcU;iGrWjv%O+CNMr^XJ@X~Y|v&cRjgaK*mQ zYk`)TK72=O`dsB&ZKs^bA~kx_F+axthCy&9dc32q_ubbnPkB#B{K}`Vj(s+>(~%!t zv-k!Q$olMTP2Xs*D9ivvRhXpbLnf_+4V1%TNY4fl8V9bHnnFh#0gy<3eM?9>IxUx#uV=PJZ%N8(xY&W> z8Z2VQl66Cj6`P}+?#~P=FJQWD&)C@@9~)-nHv&#iRc#Myx31`rT9UsD25?`I%u2X< zjgt6C=7s=GTrquAvcY0P1eZI=Tv{dNsp}zdeUP07uhAR%^W9lCF-Z+=i~8uvF5E`dqn+m=sQP{rb(qhY!T&}t za#LHZSD&rV;=V;5K&(K$I47y&dB1G@M;rC!keFellu^;~qDBDft?NvqTjPs2jO81h zWS6!2DzF}3N|bgIMhv(>P|epcH%@hwbDkhv$B&cbvSZK{=`Dh`Q#brC+tevfvw+6y zT>|1rd@*@kSy?C7qqr-?cB6?;JsOr;2Yr$*R0=sN{tzUBoUhW7SWY@`sT`;abMcWV za4*RtMx#P&spsQvVZ^hg&yALGHb{mBWk0>zVlrCV`^R8_TvLkq+nR#mM55#YW&SL8*Y_nh^iU7r-cOP163Md(+H=+A+N)qlOmtZ!=0 zhCvdAsb)!be9*F-3w*v-8$tuHMl}9|%5Io_IcpN7FTK;4nMY*2Syph)__SGQ=kc7> zV_zNQHQOChvZ^h?hzsAWSt8J(suKQf4$`06U$8t~h>rL^qua=J>L0fuxKL@cQ$RpGVcN7-)yQ-2?P*&p~SY&3eHv~y4zYLs&eXy$Q_vljfcxKR^raIb~|5*zob!XE%!NPbzB9$e1 z@HfRS44;S2r2lqu1tqzZPY!Og*^sUo^80$RAqwyJ@>NUjwTYP8x>-SmRSD&Fe!vUUw`8St(3P^scHE>pq% zS1&_8c2m>Hsd=|7b5cn`hR#hPO>x<45*k!BH+6uX>NuAx^XVNX)4Hg|9K-L<3;-j9 zW&3xdTku(Ea}`~RN2Jc0(2XbWmc-(#oUi2K5x2*-p4VRaD)bdgl6t0ziQNnSj_7k? z%hO_;&E3VQNRbPD~nVV(8IK7_s*`wZ=fW-*)qog&5nw) z@zZL$i6-mrMzR=GNj-q3LaJZwcT?;^hl3y&3b5fFzbNTqoK)sPtz}i=yBYRpqmA8O z{d>8;;Gtz=u7hD?61!%UBzg3K@6S42ow27Sin4(H^a+45&b|6P0sd!*nk1f@fJ91b ziwVv=U>F-Whg$>XOYK&>D5*I}08}t$D-Q{i48{@po7ZL7Dw z!>GRhJkraGKVpr+Z`n9ypXT<78_a$ZbRW6U`*@M3HbM?MGU0bsY&mny`M~_aI}Xgf z(t$cVBHp44%zniV8&Q{VgP{xXOB*0Me-zelR`}|KXm~^_UzY8oCko1{bsi=J#umtn zdle4{@TMPLZwg$Y?x3k?uAWM&IR|JV=V$Eaq__pGnCC!%{HV&c(7h^ipjIo`WB4sn z5FV;hj>2TJe`0K+co`BDW$(ec;;8>FAY^ywJjUVFi(nVV4ZHsd&`*=Mo@&el(ee2Y zCZil443C=bb)8Q8`!!kpPbK5&YwYuIPJ#FnMV$*xtaTjKw{>n-eH-Yp-}x=%v}M3E!q$n)=e| zmrwX!T}!d9Uw2cVH5FOG_dwS@7HegE#)XPV$03so6Y=dvGYiOUgT%KpP9QCw9? zkN7n!EUUVI8`s0@-N+lm{@d%?5rZ+(nCAH(1|xV}Z1;5^Z}@b=jQ=a zaDM!i$yuKx2IFw#0)b7WXfwCE&{Ou?zP@s5_nbLKDr3E?=Nxn}u2-3Rp_0w6xr%qN zDo%EYAK!hFZbak^o}?DczO5}=2w+mrZ*72|yLHIbzwjR-L(f~`a$qG@4F%8%jFyz~ zP3jLvj<`yd+BPGJ-Br;+OqlCJBN;!$c)R#I;}?}a0HU-DUPs6A=i&ovun|uD;X=N) zy@?0koZ3AoOY2Fktr;`1&ad_8Ue2b6Ww{F8>rr>mgJl!tfA^}*895#n&$wErtpUbE)U2|R@OGbSwXW%l`b<=g*1hAa;vNuJOy(~5w4 z*=l~bAJa5UNM)}w;5H%?{~O5oQ57TUwBPPa6+zg?7PFgtL**PX8Pm&WcXwM#61u_% znD)NJj*%}t47@qY%v`X`QFlVCGF3UJE&~=wN~G7zlq2#!nmqynla%uUagpav&7js3 zCmmF9ro;rt@>?#ZE?iBU-OfsW*^f!{OSXOVN*xy9^~-nR;V(Vr6i=Ye z)k*vv7Cb{C(>}R50E0}4GA0tZRNv?xGsRr4V^XXl5e)&a#7TvO4aQ88F1%)nf5*Ek z3vRobO^~FA=pX6qh|3K934UW@0(bgI_v*HLI~bU-_2#ZS7d;R(Yc_635h9&?!S?+W zDtV@Ct5MCIe@WyZXmQT-@`%JYIp0&h_gLoj<&4Q7C-P*~4&7*PFiwaueyXTMa-=FA zcvutfUf8wGG5r=)o|=^Tnh|D;D1(<}##GwMj`F5CIHxR^@i=-8HlKz+QXT%P6aNau zD6Z1gyBXd!GUmi% zeJ84TCeLckDDvC*&S=6;Qz!iFJ?;bF=^lnwycr%4-QYALvU~@iu_Akha5D{o!36u} z1REI5#ifYr==Kw;u3)xh7vdBgYM!b=TG? zo4R-$XVq|M@G1oECp7GfC+d7e9lY*H3@xfxS<0j*!v_B!M8+gO7z{W9GQ9o)8JN7z zT9BXJPn&2Kr*ZVMQ$I58+9zVg4bPPFzyc@H_26*UbzH^=I_Ft*tPCh(LI&l%D9 zbI8Yy=~S6}e@Nv?hbuwBxhnqgS7}h2NvJIU{|dWZjE>g1rbcl1deHYQ zdBeIG?uBZi-*@m0{@@P`q!=>@L%c&U_yr9XPhNr?gIyx&9>{iEy=88P7C2t@CY&Xx z!PQe|!3^w?yoAs>Y^+-PzI#TJniBW%$59Yvr7Xgj=H=>O4x~GuSEsk5gNM{dy?3Urrwt0Iff#7&x!f}HZx|G zcmD)BIjI)97mN~3mnW?_0jhadW1jeKs<_b(}$3 zT=GDRJuGPvaz51Zjit_fi0anUg(rE0_72XL+4a`=4QY|84VxT!9($B={L%v7rXS#q z7rjA}rFBP~g*to-m(Z8cf=MS2$874egeP5R_slE$WIn^(U z+jj!KH=QYN9(iy@|0n>?EP8r?Y9RejM=@E1A+gFIjN9xqEz4L)`PT%y6rddkJ~p3h zuJL0AxJSDD(3o?5N@fIWkQRcB!*13U>Ukoc2-QD8Mw^XQ3b2Lgby29HMTnSPWxhi^ zjZHr>?-yYNH>j45BQir6q431v#=lq?9xoYT4q?p3##F_e@=yI`nIR}k@N&FzW3o%a zx;*Zqmm*Vdt#?1#7O35?-IZXO(PcCt z!Jx~~)Dq>+-8g*@7i=WJwJ!x*W|>KQP_*=W@um-b`EVzS=`3!*NsYZ&|>3aPb_ehSe(yi)v$FCuWd(Yu)v;f{) zHf<)w@|PltiI4{gN@|3Miy1Tcqr-*K`{|f@HM?{u1izJ`5iZTONA#Ov17`PGaruSm58F5dXi7;-Ysw)XZL0b&E4sbb1EGm|mCohMa^mp& z6Qa*1jjdB-xrVZAu)id2d--sKqJ76#kKfdhz1h@~W2JiFzUnqaoAOOp9=|4|cI_wu zCE_WO6)Z#rRl)8tKyPgDd#7>r>C(6UF&g@90sY?WV_X}35cZ!Fxvhw)PDbT7Yr|9E z31pXVC~CPvS%XLw?o4fdaPhD_Ef=eCZ-oWGvuq|~dqyEihvGE>jZv^y_2b}iQ1vSm zK5To+P5Ew*;x@#dk_EpsJGa*)>&#GCE*1C;%maV?BQUhO^R4St<0JgulUXjr(Pgou z%CD8VXZsopl|R0x{~&QMd0Ww3{zz&H0US0Ibpw(lGc#t+Px^gyL?7p5OW{YSvGm@< z43<@c?g(r?wV&PzA!LdNKBJtKn`ZzRYzZ)YeR%rrsLm0BA)omPR3KEJr7$JiN34#y-=IT zSAjW*J5t^=Z`KwP49`W#~>5kOOiPan7aFwgev}Dq;riQ#*uLmJ!fGP&zWDy6$fD2IU zLYze1-LX=h>r(-C;1SBWlm9Z7(5#=u@Xk@`ukU`wWvnsxdy=d>p}~N3!JNrQ!B&Y+ zcs7kNw{)K&?fCyPnlj|RPV?n?$eMPLSqCd&6P3J`_CNp{biil%CRr5cKD;Z)>&eYP zG|cvL8jHP6suy^JqHe?kvnrimCnl%8aTo9MG7XxtZCbjY5D*)bNB!txB12nigXvAn zK>CcTlCODaFhr=VC+BW*dpLFwemqGcvcP-dwVn>wenbBx+oPv5%u)+Fd)0gF2Lg@t9GvT1oq)p_+U z{fje#H$3RoWK8OhJNap0&r=n-p-VHu$s$nOvP;oC0fXWvE=4l@CP&*6(Yi~(H0~q7 z;=jtjX?zuW4t~x0=1UA5vhqBg1vc26I>R=qYMDRaV;JzsIE!bQ?(6qCay^QUthl+6 zk~*$o+w1!u+@gH-lMO+C8b*CZ)HL<}6v%SKUYIMqf9#2>rNJ$VqD@=E{Hv(RE>p}T zbl)v&k>LI5Hwzbv8`9{1sEeGSEMCDrF%b7!9Gna<1V|h2wnRb(@k|D@YPDBmV0qO_MAGRdi8db??dkqw*$!j zw+NXarvxV0$L~5lE>Vv-ugrN;ZGQcHfS{ww%wO}~>5YG8`QCqSQSY)(J;8|e@=4}5 z;tWQkDE0ADUzbh4d2GT=h)Ug=q8X0_B!2v-K~yP!gCtkQwo+*_V$y4EZ35^#Rt^jxrM7(l`sn28YHaiB0*BnY&C1*H*={>P|0pTp-79Kam!7P zzry#CJca%}h!h)YTOsx(Ii^mBdN9pVo~Na@sh$umChY=gl?A0>MI`6PC};vVN|pzA zWjBn~#TV98GvcTB;&uF+7|F-6(6G}NN|%UXJNOEB^kR&Uwz)H0;+9!RCbcYTaEEh# zJvz(hDAz*h-*2`}rL*`e^_{RKAeGlbIR459R3 zZDHjg(iZYt-v5hVH{7Z2+6M#QSJU-`yVRhtNq|2z%!XfEE*BCZwt&nr!&>Pjk0ctm z!Ouz8J97xlOlaVy?K0Kocti4|?43AYDG3c$$Rxj!D^+Hoz7J22wq?LllyQfA$;p>+ zNK2w1>**;BjKibapd!UKJJ`j|%cQEck>M*7^dJL&r&LF>1cz|BDy$$UJvpWQ*N7E3?ZKEIm;r1>GaZV`{0 zMeArt5g5>hBwVd}2}ojj#mL9qCa|i+Hw3bw>Ft}7jYNFQwz|QX6*}3^7O`FoG3$ee+>KLpXgw1PolD7OvhBIOtZ7=I zjcF~QP4173FFH3?U8gvDPHTpGPL(i}FT%Cjl$X9G5=tD-AX&wZy4R_TGaasO7j2L) zrtXy&u7%496m8t`w?J>F(dk+>=xJKGuXw_G%Iad0{AI~Pg1ww)(tgE;AaeHZ#?v{) z1Ywre@(t;J?TYyg?UgvlRdgedvx)3Wai2$L)sCwCIEM#iO_hmcZD9=^7H3IpDF9WUpsxOuqkm%4k1M{AlCR-lb)A$EHzTD@2r zlwha$`R1+f;7#UU!FmPB6~2NlMxz=Z^}anU=vdocbqDudx{JA7=9WW9g}ZAx zfm@Sev#C(0b&({*o#y6%CMQ?#o*d6(lH43))yh*Cr&WBTPI~SNo@|8Iut~KB?by|W z^61625yCS9=t27D-=ZMgaFh+@rkMd|ZPVX}DyI%}U=B3rDXlyEkGVL3(fNDsieH_P z!R+ZDZtk0~t>GfMN>8jY&io&Ku_W;k8l?sj*uoSIGAG%+1yv%F^ep290&`f(t~CFl zR_>X24wiI)8=-fIS#^gnh*RR{^ADBC9>5A5~igI(=|RSDjfB#rU11iIlFID?PL7pMjJJU z`dS)V=RBj4R!zG<0(b{-7u8$6saKM$b{h$JgJh}0L(=AfY8jz|eGgl8cnb01)2E87 zw-s;HKDlw{`lU}FjDEip)KH7JZ>>G;C9-UJ+prCu*)SzrsQ)o$eI6!o5__@E>)9Fh zHwGIwDt|rdWR3E0o7G%bRTwhu%P%?~UQehSCk{*_q3ZJaq9E`Muz`=IqP5KI{Fte*^ZFZfQ;$ zw;bPp*m9T+`8jBfZDk3#dB9jxRkQc!V3hVp^*8JQg$+#zB)w^fMGUK$$e9M5d2}$U zvk;Z3zqfpPLYez1wpd#B z3CSFG0<&iY&hgG36CA&bua80l&a}hV{DR9A8P=Rr#8N04Rab{YygC!gtZZ^?IUVv< z^U&r+DcG#^vrb?fWY|YC5Ng>qyG7UVAwThLgJzwcG3`ZFAAW~ym(7|DKLa)7$6lc8 z(7b=aoiX5!TDAmW$!u^l=yYhnuET1WdROgMK{Y|ONjL%U$ve>CEMP%_97ERNy%^dB zA+wjCCz-vN<-IxnU^_H!Z^_k1YawMukzTRGEHXfs=oz%0M};bu_v5)Sq_M3&5bhf- z)95geey0Tnxg@xzGq@VIm{73@5kzEk0E@QBwoY4r50Wm*HRExhPzd%XP;k3d zSdr|5*k6890E*kDqdgV*!MWSXINVH=lJ5bat`B6*`AyHXN8u_tX_J^YmRCm0_Yz8< z;@Vo!|MO%&VYg?2in#FT;IpH_#Zi_cY-EOZ50X#MQXK&a<^-++H;?bjNRW3&Zot8$H+$!RFF?aO9*;XbnjZ5AdKAtBQ(1%;kLqlBI)nPs_`SGSS~G?%^ri>(LFgN&2(;D+#i-p)Y^ zHqCQbH&adQ*XL zloF)`lqO<8!GxYbC{aR@8fpRwq4yA~BvJx50_WYg-n#eQw$@wkzrFTed-iHp zH}P?-@?Qev5ec6sY#p8Tz=f(Oz36vAtiL(TZK0fL(-sD|DsCklvPGZCM^6$zb%`M- z)tm?aYV%@+}aH=qN;npJj zPSjgNK`R0}gC0BoS#VR7FZx%CHq(o0II;!|LX%m4TZHb9 zUAzBo&9_%+xy<*YGgUzcl;bhiU+yv8iM)GS0rTm*afn*nRo;$l?$57ry2zqz5WpCy z{q;Ci<>l9}_6--95DikQCiXi$vcdi4-Pz?{R@N+Soi$y(^5GI6O?SmnYSqH!Ao2Zr zT^#&TGoiSisp~Guh}sd~j{ zCYo3%v{K_o8%POaf$s?a>BZh^wF}cF=Xtxk8&6yMha5=xo5igZ=U%-k9ke!`gZlpF zD*ouYDwBO-Yc*r*WP0c(K0;%25su32S$Q;aqs}X8(%eTZ+-*>AGCl-b^z^&(G9OKS zbv)DK0>9QK6@L)eD1QlSVmiiS?g0ox{T^YytPsdy~`RZCh3?^uc6} z_w>{q8CH}92i9`5h7U{!@P;N;8mvN6Eso-7hhHnoFO`050#_!G6W+;Wo+AHH)xrYt zY#rsF2CHj`7iWsSqff-9!C77)ZOHdH}%uic9X|1+VJ*_N1$C*lzxPc|fxF z#}n>j+BS900;4Q6rr@4M=&N9nbtUHFrx&0zFe6W6^VC>*Qjd_mm4FmREnQ1B3G)p= zY0o^h{j15hb0g45&$L3X$+n1MN8+VxvV{hAgRv=wMZi>;JUSHPJ3r{SmCR6)Eo3Z; z&KT(;4$`(?}d7d8bw)3z(zMXI9D&Q#YkL{~l2AbD*ZcXV-?ccdl z`@b~iW9f0CapNrFv*yhRS#$oy`Awt-2~4Unvk6AFWY$*41dFb#Fjqe@Ro5T&uH{f` zWDeNy;e+Sd9&=s9e;Rtla49a-!uCkouD4=g3~l11L3f%!{#FlaI2*5Ww7Mn3e=FkY zS~BA1JU@Is;6~~%SHNJg$E8_!Du<0yPMilNN#MQKdltt#8sdD^g{*B_GkUp{o}@e- zp$pW24F<&Afb%f`+pRCnt2@d-pO@db5xN-!zW1pmzg9itoL|&;lFi1rO{76vI+*V} z+T<03(<`0co{;oC{Ax!fFx+J+t3O8!6WxaX1C= zkkNHDbF~T1ZPXLNd55ck=fm05I0jZjD3%X1nMuXHERH#XgZ#y5ym>BvvSxkNZrh+} z4bR|0WtuJM^rselZ8edlAC8B_`TxHiw=9q4;)i5*>;Dkn$6Ax>{e>>;aRgkLOn z@c3NlQeA=J%<}CN{ftAAzG`n)?iATjHuIO$KFXXRG-ZknFIJ{4iq89a1S)&vDu^sr zO%~yvP?;0nG=JY-&nq_?WLDK9WF`qs-xs9E6?avl8< zRn=JTPNY^aE0PY7rRdAE!Wk0&rvt4<2h>jShn_EoWuY7Ed~3iJ;^L{Sq4xV!wX4S^8Sag7FvC^?V8J>~+Yh^2V>elijOK86>9asD24!xdTopUV%(| z5k@t3BuL<9h(FmjmU*?Jz@64z+9h7Es44&3n+&MIWS+{3_d z5CB&bv<_WQD_ELnww(-3XGC1UCp+|EosD^D&B^rwo70Aagc$6`i6$G_V z!`yjY!>w3hm;O^e;wmDh1L{Ya8ii8*MT|KGWzmy3sr7!ZNrLTG$7mS2?zA(p=}O4^ z&wPN98*X~mtae`;J?o^j(OBr8!{`Ub8KoDU=?qZKsGR7*^KtB0vHLUd;Np3dYKaBU zc_yMwu5YroL-?4qw-noZ2#PZje?mJx*N2hnXVykd%frPe=_|{zh6~=S#>$7+amw=} zsd4N%oi<0~1*J`#6|ZC5Xf@^<1`_B8NN3HrVaXOQ^B>$ZLY4W@nE8Qs)=7KaF+V|M z_U5`7fvppKK)pqR9LzTD^$P4{9`Fq(KdD;u8zinxW<%s)@TFU>9(r-3_=H*Ck*T)} zn`Sg(#;PsvACYAD@m4cJr(Iug$?~NC`=nHGW#sF?LXTB)MI0LwX_r0t=WkC?`oIBj z4D7ZY5cF6v?Age-!SI}Pw&yR-F=P8Y@+)cm?)Hsg#oy^qxUPRL#u+HSX?|Hdyjavw zd{D3gSh?To$yARJv^3HU3rk54DaP3Vr%d^JXQZf;9ibbHg>n`@Oq)*Z(N*>7;}gcA zB5YKTvo2{~h`i)!LBb~Nuu?QK8itQ=Nr(jiXfLBJP=p24|ZlN>K0%DQk z#wWlo;7kC(qj9V63XZN%jw}{9QV|m?Y}XsA36(wzK_X8a{s5KR*cAxw%{Y7SzV>Tc zwM^;-Z?gmj>z49{#pYkMPh^06$Im$bK&<+f{ulS<0<4c!k%tS=Z`yaUqs~JX7EpQw z>KNkZUN&t28@8Y&;roxzl^6RH$=Yql$@bF>mjk65TetlfH+>ZeP0BA(K@#J^W?~V( zH&T03W{a1dmb$hfykIe!kA|x$#hlLb{Ss-d^Qz(9vy#4sEnGecBRs(1(b)W4q9#6h zOO)@rit>G}Z#UpRDHc;q$3Y!Jq3&lThbf&YhvloY;UNFvs#IOo7Ux_MGwIA?@v^^FMeVOW2*Az&sRuCY^XAYvW{#0ni{go zyjnL#SuD7ABS*{~F@f3k-`-oKQ#j;z6K&zdeI^>0^#o7a*rdl6$xO{~(_V8yuBZTH ztR#mhTG-)D34HKbDnkGE4gV>n9)G=FlKImUI5FOat|RV957*pwNH?=1KZYTjLb!*G zxBVi71E9uIvK5os8BdfkS)!?9nN-P}HNe48v_(i?aL0iLxjng-KOR2(Q+_^TVU;d$ zek%jc%)LNP+6d)+n;v?eYK&ktb2O)9nY+1R51Cq6IWSxb#WmUEKh=6ykNkWik+>F= z`+Tx#ynxwvz}8{hNqK{96}Hlr%cl6~HBTx?tLo_rwUs6>;O-bjxFxEL-SNiD1&#MB^?HJXqZnSSu5L^Ih}CPN#i zKXT<*>mHie@CjJ?_DFuJJ3tGo@cdGQ?)V!Ss%f;W49Q7mMp)6ou6q6hdI_BO-C~lv z(!BEus>FS>%(KNa6C|E`eC2+KwNZ)U#bRFCrlbuVa^?8gpI{sxQ!nT3tR)_0>W-=n zXqq^Tizo>2^AyH0)Zb}T@W+R{$tPdh$2F>jeRAphh@!vwwM+Ih#yp>;dEGmw_cZLc zUWJBx9imBt2~J6JL5E|-`i}dkCLfSTez|g<$B$H_9a*GEelpl!v@+naC3v%DQ<8}M z-c_3k!l6NMy7I7h)Py+hxTNUvmy;R+D`;1jMpwHEUCyD_{fk3Y!;4nLaDrUzLoX{< z;{)cZnm2GH*y09!$*HoEZ6Ibq75EK~1xGl@cIQL4 z?Dzv`@+95?s_lNVaHJiduCv2Pl`okaufRPJ5Ollgo z@wAFk`gF#$*(tvg1+s~;H4rz%tdNxjt+7sjf%lcBVuXEo@LNja1b@m`rqQTrc+#5A&X^Zpucu(t*Df@I2;8l_X6BdF=5D59icg z6%%{$r^jt6;hCoNlEgwN%&qks05RPLUc$7gLV>zCB~VbMe|Ay}N;aa7P>P>3_#Hae zV50B{vicWziUp4&!p^)vN|ybs3}QcpVe0qd0evCsu?tc8OOsDe;;b&-?Epj)o6mG6 zb`6(m$4z3ZQuk^4fABO|!aY+sq7?_NHMh1A`o*gacOccgs_Ofkt)5B!1aVT4h*k6U zEdxlrQantQhshg-yY+2dCkKN9D@ZWaTFTnS%|uMd>O`zPzM|%ZKygE#rI&96J13Xb zG;Tx;M{%+|<_2zU;~NL|Jry;v3eZY6sbD!`vW9(K<1l|HLSyh!KDOGlKhcxpL-ggk z8{O_<2G;xnQpD?Rqs8N6tDoj=dJ{K-7>8u8ZdWXi_og^cAr1gJfimOG2lQ8ISi@NA z5Hpj7ii|C_uyXrL19 z#7gxqkD*=DN+*58BQZ~3)Ie^+jbFye9xIn|9W=8_fe_<;C%c3>i=$7%++H=$tDTtB znW-2rbgZGfH>{Le^WT(+hM>e*EpHDw1>%zd^M2s}iM3ayo4?d~YV~=kTsNYl(x8B-} zg}$x@`54c~<8<<=CI@M8*DU*zY$ml`Y%QbU)-k~uKU;m12(Lc5cVaZ+DmU9^EzvU2 zC`r?Vz3sGVJ^?OZuXT zeUj0n5I0>CPcs76AB7uX7PYWbBVQKQc&G%nf4k6L*iG?%P73djQvukX<+<}-3Vi}& zH=k#V6jE+cfku(^CHU44oU!5MvEe*A75zcrUt-I8R$nLd-`(9LU;JK3Qc;`XYX6 zn`^5ZulRG)buQa`S=PTxF5xpX0{>mwC^Z=b1Rln`MGn&##0b`TZsqL}42}FvKme02 zm%?s0Z2)K2{rGS}bSjA8J{X8soT6OVE;H^=`2cF2k7EZ;@o!uXO$Cz0EBu>JKYHD@ ze_(Mas`RiO;H#>Hw#W+ab!k@u%!3f&wi@8N;=Qa?qzqtSiUxkwHD@GJURuE^!W2Vl1V1#1lt1ilvP!l&D5pjsL zB#AeZRVx^;g3=1hCi;w6u(EUtGfPeTqaFnoGXy(GYlGPYZIu^jRMq=RO$LhH?<(`T zBpM5rRzH%%T~&Q|Vq50x=>NsFy?AqV>*%_=b=YORKWmUc(x3^c-v~r2F5pN~7@6M{ z7Dsf{*Sb=Q_+^wD^H#6_=W=E*WtwALL4tSvoS&<!9AiRJ8vA~{RN%CYBvatfM}x32ykiv1b_ zSlvHNgt7qI{n4rr@3)@dXaZ-ljRk%e42yEGsU|ZF<0(PA@)5*zRAxH z4lH%A@7Xb#hh(2ZCP=hTC=631{7vur2!!)V@Sq`QgIn9y$GM`PyMih za%%@v%=XDyi0_TE6zOBz6#!Rnt&$#RTPbXvA&J@VWHF?mH>Gr`WadzJ(+d-ffzWfs zW>+ojjK2dzLOjn8GO*c%F+8&CFAA=?Z{0lYK^CH`N(_qIXtuYgn}k{?Ah?=%59^G~ zB6DUzJtcp==DLir6Bdd6hbI)KaJqbq>=AVvBuW$7TQ*IG0-;|E5lvW!VH0g-laOGr z)hhkQF7G$WFuZJzWv9nk2z`mLyqWLSZD3BM29Tm%pa!e zB40%nkS{F!|Fi%+0Dk(z)cG9fA?tH6g@9Wu!qJB<2j&cwlNn}WR%U#Zdc1V7>@InE zkAtm{r%OZN0)&O3Wsc+5dGb|&olx$$gXaxdZ1lOj+4uy^H}{W+FAMBxMxtn83+@kc z&-9=6qxW9kZ9@3JOS}G$;%J9c{_haNre4ojn&#$CGQeT`Y9dr0hO8Sec(1W%+e05O zbAskmCJ1;B6W}K=D=P~d=UEjLoXOoz?YW+}YE7A5yL1j@@LYOV{N9alH$_RUBA#Yj zPflE;#^70rNQy#2ol{v)L1iy9=xlz@22M9^nmMigkM}}caqWzl>jT{DOa^ePLOe@5 zJB=5}!(+SXQLV_=pF3>6w${1k9$FmIk!Lkv`glDLyTfL$rZI}b8m|BLq``mWy;%P;4k({M)HH4k8+Vc~hev#zwKfCneE-oMP3*gyClQ_B zT-7*>9w>E$yu^l=JChdAV>nO*lmQZsxX`(2rUG(GHc6!NwufH3uO8RLS~Tv(D(j*Y zRY5rxq-}1N>st^vZQ`-dZHr)>&aPU{W>Q6e3eY%%$;evHmmSy}nmLMV{eC|u{C*DV z5pVN}rQH$6Rap~zgERNyy80PQtFPEWSyyAu&BMFa={FB|^C}oT6e0RW|=ZLcKc34OV=HM*F+3O_{sVl}4)vVKMa^5!y76MhLH{9B{_3T6aPAOl@jW77dGapIX(iyYY~-EqyYbP_O?!HVuT+Sz6tUrX zS?n$8sv!!9+nS4C&3YuB%okS1(VcxVS-c4)Zk?LD>lXL*r>?mn&qKMrS?}A{}2(wmiDs!^a`vZp%gK ziZqe;U>wruf$7~M6V^%CGh~McX&1ASWiqL%V`t0;AG)YJVE@tz zb*AAS+^SNVXJ`i8_|E!YyLIjDKC9rpSoBKorOqSZ@dqgX*}LBmcxkbN2e#Y()^d>c z^fXWT2zM#wDlfkCp=;XrN8;(;37qwvkxpTqrjC$|wI924dr!6hGjn_YQJDUhCJO(Z z##-AOX*{;JnHQpLS2e3`MPz_TmAIncA)*&CnZjtv)+jjuHv)Ml(#*EC|g_G zi>Y5#)35q9hZP|cH$2Vxnxj^30Rr9Z|2E}JbUiaaR=H#QmY8;kG-sH)`&sd}vT1zm zW2#lHPr2%qQ~N#p3Sn?M1)c(X4#=9=G0&92l+ty`?f!snO^ibLso2lpd)Rz0mu+T- zi;KkZ-t#Nd(qzE46FIE#lU;y7|35*vP*u18(~F7``8i4{AM(nxK`Q$k9L|BfGi)!I z9oqOhy)%z^#Y_`h-bg4|6M+~x?ROQ^rB^#-f|eDpo$}wY=dG}INVkcmMV7eB2`=G&;MDmOh)wO;+TmqY3o25lfct_~iJ+X9KG4$7h$bW;1%T=OO9 z96xjYk`;i?qQvF(bdg{`e<6cA4V?`*TdLA8v7BWI7!uV&rUHmUXZZSq&;hA+1wm&9 zuS5iZiVYd~dUXKgBDgB{mCF`_0=hv)fOiakUvJ7Ze!qB3J4)(tV^|dWRvu93GxSff zJHnmZ50(}_{|plP9L<0K?wnEhs}(yW5uWS&5~Hvt9jI5DPf5?yp`R3Ub6{Ig&THg9 zeBm^zsMBm$cHI>y&P=R9*Mp^pT8WKM&2*3H$rm z^N!=VJ6Ra-sY5D7lXiYRoTI$M3S^_Fd6VwK43J0cA+~z#Uk-vLQhbORZw1XVTQFxiq z_D!Br`)-G4;K`S#yMYiL|_hfZ1g2%_y zM!GT1v|Si|8T&MtP;f4gbG$Y{vL@~!sFLi+H)YGyim)P&A^fM*{0(XY4Uxm`EuZwiDBN;7CzuNOw(ogz}rQ|n?n^7s2 za@+bWxSVnc8u+tAn-I*RCBfXN7zj|p0DO3C4BoI$CWhT0d;%JMsHW+K5zYpz=5O9Z z$#ywSQZ_weov58Oe*2-2u^tJ5IsEx^#j{ymJc*hnk%tWKQE^!}rhrV`9ZeL+WHM|z z5HntNrKzgWW@bJH0!{g7$(1bPCB~EZCf`5WlXZ5xCs)e#IGh|OJ9LtX`lG)@azNw^ z7&Yr|Tgq;*AAf=Cth-X{g`%D>=Jsma=B|qmK zcwSei%esB!f&LL(>8TF%ZM+H5bHm#eg9k*5`tI5_{&$YdsnUQJR45=Z z9<_IT&6i1N3`?}+B3Xor95o@@bz%hQsUHFUKjIVp`X3G(+P)0dC1>TaQNiv0V$}6Q z0M~uES*nrhTymh@Rj~%S^!$v5bL7G-s}*)VEoxh`Z(~Y5_c$3kN;WnqW&R#kR`{gY zIg=P3mG`#|$?_D!`;ObjMX1tf{#Y0!9St}x82tojsbMljDl;v|sL#!@&|V^gh&h#< z4wCPx7r@)GK^6_F8Y9ayve0Dyj10Z?Z9IAI#7XWSJ|b`m0Zxj~J2X+9P5p5TO)LA5 z1yBH&T8@lFddBnP+&2_3Yao_xj`!&S?}WzBLQb`EA=Wi2lUgF#e<|C<`ErbuQYf0cwEhag2}?K`>bYo~HN zZ);z>b@AA+sWpW;Ei6Vtvdv3JOZEc=OOPaY6eO1g0pcCbbQhHL82t&ca0a`pU2uKN zqJ2n5c;#Y{{(KEq>)oG6yRkjGWN!w}v3}*B+JTdR_#_@wQ7Y!7a<8YQ^A&d;m6ISb zE(tRUuez8FaHI-0u~Vf literal 0 HcmV?d00001 diff --git a/_images/welcome.png b/_images/welcome.png new file mode 100644 index 0000000000000000000000000000000000000000..84951d060af1d8b323509c619fc0457983efce23 GIT binary patch literal 92455 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 ZSIR!Tih}+U#5mwb`H7lB;bW6`{|}&iuCxFE literal 0 HcmV?d00001 diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..e0c8fdc02 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,149 @@ + + + + + + + Overview: module code — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/openeo/api/logs.html b/_modules/openeo/api/logs.html new file mode 100644 index 000000000..e0bc213cf --- /dev/null +++ b/_modules/openeo/api/logs.html @@ -0,0 +1,229 @@ + + + + + + + openeo.api.logs — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.api.logs

+import logging
+from typing import Optional, Union
+
+
+
+[docs] +class LogEntry(dict): + """ + Log message and info for jobs and services + + Fields: + - ``id``: Unique ID for the log, string, REQUIRED + - ``code``: Error code, string, optional + - ``level``: Severity level, string (error, warning, info or debug), REQUIRED + - ``message``: Error message, string, REQUIRED + - ``time``: Date and time of the error event as RFC3339 date-time, string, available since API 1.1.0 + - ``path``: A "stack trace" for the process, array of dicts + - ``links``: Related links, array of dicts + - ``usage``: Usage metrics available as property 'usage', dict, available since API 1.1.0 + May contain the following metrics: cpu, memory, duration, network, disk, storage and other custom ones + Each of the metrics is also a dict with the following parts: value (numeric) and unit (string) + - ``data``: Arbitrary data the user wants to "log" for debugging purposes. + Please note that this property may not exist as there's a difference + between None and non-existing. None for example refers to no-data in + many cases while the absence of the property means that the user did + not provide any data for debugging. + """ + + _required = {"id", "level", "message"} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Check required fields + missing = self._required.difference(self.keys()) + if missing: + raise ValueError("Missing required fields: {m}".format(m=sorted(missing))) + + @property + def id(self): + return self["id"] + + # Legacy alias + log_id = id + + @property + def message(self): + return self["message"] + + @property + def level(self): + return self["level"]
+ + + # TODO: add properties for "code", "time", "path", "links" and "data" with sensible defaults? + + +
+[docs] +def normalize_log_level( + log_level: Union[int, str, None], default: int = logging.DEBUG +) -> int: + """ + Helper function to convert a openEO API log level (e.g. string "error") + to the integer constants defined in Python's standard library ``logging`` module (e.g. ``logging.ERROR``). + + :param log_level: log level to normalize: a log level string in the style of + the openEO API ("error", "warning", "info", or "debug"), + an integer value (e.g. a ``logging`` constant), or ``None``. + + :param default: fallback log level to return on unknown log level strings or ``None`` input. + + :raises TypeError: when log_level is any other type than str, an int or None. + :return: One of the following log level constants from the standard module ``logging``: + ``logging.ERROR``, ``logging.WARNING``, ``logging.INFO``, or ``logging.DEBUG`` . + """ + if isinstance(log_level, str): + log_level = log_level.upper() + if log_level in ["CRITICAL", "ERROR", "FATAL"]: + return logging.ERROR + elif log_level in ["WARNING", "WARN"]: + return logging.WARNING + elif log_level == "INFO": + return logging.INFO + elif log_level == "DEBUG": + return logging.DEBUG + else: + return default + elif isinstance(log_level, int): + return log_level + elif log_level is None: + return default + else: + raise TypeError( + f"Value for log_level is not an int or str: type={type(log_level)}, value={log_level!r}" + )
+ + + +def log_level_name(log_level: Union[int, str, None]) -> str: + """ + Get the name of a normalized log level. + This value conforms to log level names used in the openEO API. + """ + return logging.getLevelName(normalize_log_level(log_level)).lower() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/api/process.html b/_modules/openeo/api/process.html new file mode 100644 index 000000000..76ec5c25c --- /dev/null +++ b/_modules/openeo/api/process.html @@ -0,0 +1,615 @@ + + + + + + + openeo.api.process — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.api.process

+from __future__ import annotations
+
+import warnings
+from typing import List, Optional, Union
+
+
+
+[docs] +class Parameter: + """ + A (process) parameter to build parameterized + :ref:`user-defined processes<user-defined-processes>`. + + Parameter objects can be :ref:`defined <udp-declaring-parameters>` + with at least a name and expected schema + (e.g. is the parameter a placeholder for a string, a bounding box, a date, ...) + and can then be :ref:`used <build_and_store_udp>` + with various functions and classes, + like :py:class:`~openeo.rest.datacube.DataCube`, + to build parameterized user-defined processes. + + Apart from the generic :py:class:`Parameter` constructor, + this class also provides various helpers (class methods) + to easily create parameters for common parameter types. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param schema: JSON schema describing the expected data type and structure of the parameter. + :param default: default value for the parameter when it's optional. + :param optional: toggle to indicate whether the parameter is optional or required. + """ + # TODO unify with openeo.internal.processes.parse.Parameter? + __slots__ = ("name", "description", "schema", "default", "optional") + + _DEFAULT_UNDEFINED = object() + + def __init__( + self, + name: str, + description: Optional[str] = None, + schema: Union[list, dict, str, None] = None, + default=_DEFAULT_UNDEFINED, + optional: Optional[bool] = None, + ): + self.name = name + if description is None: + # Description is required in openEO API, we are a bit more permissive here. + warnings.warn("Parameter without description: using name as description.") + description = name + self.description = description + self.schema = {"type": schema} if isinstance(schema, str) else (schema or {}) + # TODO: automatically set `optional` when `default` is set? + self.default = default + self.optional = optional + +
+[docs] + def to_dict(self) -> dict: + """ + Convert to dictionary for JSON-serialization. + """ + d = {"name": self.name, "description": self.description, "schema": self.schema} + if self.optional is not None: + d["optional"] = self.optional + if self.default is not self._DEFAULT_UNDEFINED: + d["default"] = self.default + d["optional"] = True + return d
+ + +
+[docs] + @classmethod + def raster_cube(cls, name: str = "data", description: str = "A data cube.", **kwargs) -> Parameter: + """ + Helper to easily create a 'raster-cube' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + schema = {"type": "object", "subtype": "raster-cube"} + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def datacube(cls, name: str = "data", description: str = "A data cube.", **kwargs) -> Parameter: + """ + Helper to easily create a 'datacube' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.22.0 + """ + schema = {"type": "object", "subtype": "datacube"} + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def string( + cls, + name: str, + description: Optional[str] = None, + *, + values: Optional[List[str]] = None, + subtype: Optional[str] = None, + format: Optional[str] = None, + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'string' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param values: Optional list of allowed string values to make this an "enum". + :param subtype: Optional subtype of the 'string' schema. + :param format: Optional format of the 'string' schema. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + schema = {"type": "string"} + if values is not None: + schema["enum"] = values + if subtype: + schema["subtype"] = subtype + if format: + schema["format"] = format + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def integer(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to create an 'integer' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "integer"}, **kwargs)
+ + +
+[docs] + @classmethod + def number(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to easily create a 'number' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "number"}, **kwargs)
+ + +
+[docs] + @classmethod + def boolean(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to easily create a 'boolean' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "boolean"}, **kwargs)
+ + +
+[docs] + @classmethod + def array( + cls, + name: str, + description: Optional[str] = None, + *, + item_schema: Optional[Union[str, dict]] = None, + **kwargs, + ) -> Parameter: + """ + Helper to easily create parameter with an 'array' schema. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param item_schema: Schema of the array items given in JSON Schema style, e.g. ``{"type": "string"}``. + Simple schemas can also be specified as single string: + e.g. ``"string"`` will be expanded to ``{"type": "string"}``. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionchanged:: 0.23.0 + Added ``item_schema`` argument. + """ + schema = {"type": "array"} + if item_schema: + if isinstance(item_schema, str): + item_schema = {"type": item_schema} + schema["items"] = item_schema + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def object( + cls, name: str, description: Optional[str] = None, *, subtype: Optional[str] = None, **kwargs + ) -> Parameter: + """ + Helper to create an 'object' type parameter + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param subtype: subtype of the 'object' schema + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.26.0 + """ + schema = {"type": "object"} + if subtype: + schema["subtype"] = subtype + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def bounding_box( + cls, + name: str, + description: str = "Spatial extent specified as a bounding box with 'west', 'south', 'east' and 'north' fields.", + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'bounding box' parameter, which allows to specify a spatial extent + with "west", "south", "east" and "north" bounds (and optionally a CRS identifier). + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = { + "type": "object", + "subtype": "bounding-box", + "required": ["west", "south", "east", "north"], + "properties": { + "west": { + "type": "number", + "description": "West (lower left corner, coordinate axis 1).", + }, + "south": { + "type": "number", + "description": "South (lower left corner, coordinate axis 2).", + }, + "east": { + "type": "number", + "description": "East (upper right corner, coordinate axis 1).", + }, + "north": { + "type": "number", + "description": "North (upper right corner, coordinate axis 2).", + }, + "crs": { + "description": "Coordinate reference system of the extent, specified as as [EPSG code](http://www.epsg-registry.org/) or [WKT2 CRS string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). Defaults to `4326` (EPSG code 4326) unless the client explicitly requests a different coordinate reference system.", + "anyOf": [ + { + "type": "integer", + "subtype": "epsg-code", + "title": "EPSG Code", + "minimum": 1000, + }, + { + "type": "string", + "subtype": "wkt2-definition", + "title": "WKT2 definition", + }, + ], + "default": 4326, + }, + # TODO: support base and height? + }, + } + return cls(name=name, description=description, schema=schema, **kwargs)
+ + + _spatial_extent_description = """Limits the data to process to the specified bounding box or polygons. + +For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +For vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided. + +Empty geometries are ignored. +Set this parameter to null to set no limit for the spatial extent. """ + +
+[docs] + @classmethod + def spatial_extent( + cls, + name: str = "spatial_extent", + description: str = _spatial_extent_description, + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'spatial_extent' parameter, which is compatible with the 'load_collection' argument of + the same name. This allows to conveniently create user-defined processes that can be applied to a bounding box and vector data + for spatial filtering. It is also possible for users to set to null, and define spatial filtering using other processes. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.32.0 + """ + schema = [ + { + "title": "Bounding Box", + "type": "object", + "subtype": "bounding-box", + "required": ["west", "south", "east", "north"], + "properties": { + "west": {"description": "West (lower left corner, coordinate axis 1).", "type": "number"}, + "south": {"description": "South (lower left corner, coordinate axis 2).", "type": "number"}, + "east": {"description": "East (upper right corner, coordinate axis 1).", "type": "number"}, + "north": {"description": "North (upper right corner, coordinate axis 2).", "type": "number"}, + "base": { + "description": "Base (optional, lower left corner, coordinate axis 3).", + "type": ["number", "null"], + "default": None, + }, + "height": { + "description": "Height (optional, upper right corner, coordinate axis 3).", + "type": ["number", "null"], + "default": None, + }, + "crs": { + "description": "Coordinate reference system of the extent, specified as as [EPSG code](http://www.epsg-registry.org/) or [WKT2 CRS string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). Defaults to `4326` (EPSG code 4326) unless the client explicitly requests a different coordinate reference system.", + "anyOf": [ + { + "title": "EPSG Code", + "type": "integer", + "subtype": "epsg-code", + "minimum": 1000, + "examples": [3857], + }, + {"title": "WKT2", "type": "string", "subtype": "wkt2-definition"}, + ], + "default": 4326, + }, + }, + }, + { + "title": "Vector data cube", + "description": "Limits the data cube to the bounding box of the given geometries in the vector data cube. For raster data, all pixels inside the bounding box that do not intersect with any of the polygons will be set to no data (`null`). Empty geometries are ignored.", + "type": "object", + "subtype": "datacube", + "dimensions": [{"type": "geometry"}], + }, + { + "title": "No filter", + "description": "Don't filter spatially. All data is included in the data cube.", + "type": "null", + }, + ] + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def date(cls, name: str, description: str = "A date.", **kwargs) -> Parameter: + """ + Helper to easily create a 'date' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "string", "subtype": "date", "format": "date"} + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def date_time(cls, name: str, description: str = "A date with time.", **kwargs) -> Parameter: + """ + Helper to easily create a 'date-time' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "string", "subtype": "date-time", "format": "date-time"} + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def geojson(cls, name: str, description: str = "Geometries specified as GeoJSON object.", **kwargs) -> Parameter: + """ + Helper to easily create a 'geojson' parameter, which allows to specify geometries as an inline GeoJSON object. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "object", "subtype": "geojson"} + return cls(name=name, description=description, schema=schema, **kwargs)
+ + +
+[docs] + @classmethod + def temporal_interval( + cls, + name: str, + description: str = "Temporal extent specified as two-element array with start and end date/date-time.", + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'temporal-interval' parameter, which allows to specify a temporal extent + as a two-element array with start and end date/date-time. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = { + "type": "array", + "subtype": "temporal-interval", + "uniqueItems": True, + "minItems": 2, + "maxItems": 2, + "items": { + "anyOf": [ + {"type": "string", "subtype": "date-time", "format": "date-time"}, + {"type": "string", "subtype": "date", "format": "date"}, + {"type": "null"}, + ] + }, + } + return cls(name=name, description=description, schema=schema, **kwargs)
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/extra/job_management.html b/_modules/openeo/extra/job_management.html new file mode 100644 index 000000000..d8502a77f --- /dev/null +++ b/_modules/openeo/extra/job_management.html @@ -0,0 +1,1309 @@ + + + + + + + openeo.extra.job_management — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.extra.job_management

+import abc
+import collections
+import contextlib
+import datetime
+import json
+import logging
+import re
+import time
+import warnings
+from pathlib import Path
+from threading import Thread
+from typing import Callable, Dict, List, NamedTuple, Optional, Union
+
+import numpy
+import pandas as pd
+import requests
+import shapely.errors
+import shapely.geometry.base
+import shapely.wkt
+from requests.adapters import HTTPAdapter, Retry
+
+from openeo import BatchJob, Connection
+from openeo.internal.processes.parse import (
+    Parameter,
+    Process,
+    parse_remote_process_definition,
+)
+from openeo.rest import OpenEoApiError
+from openeo.util import LazyLoadCache, deep_get, repr_truncate, rfc3339
+
+_log = logging.getLogger(__name__)
+
+class _Backend(NamedTuple):
+    """Container for backend info/settings"""
+
+    # callable to create a backend connection
+    get_connection: Callable[[], Connection]
+    # Maximum number of jobs to allow in parallel on a backend
+    parallel_jobs: int
+
+
+MAX_RETRIES = 5
+
+# Sentinel value to indicate that a parameter was not set
+_UNSET = object()
+
+
+
+[docs] +class JobDatabaseInterface(metaclass=abc.ABCMeta): + """ + Interface for a database of job metadata to use with the :py:class:`MultiBackendJobManager`, + allowing to regularly persist the job metadata while polling the job statuses + and resume/restart the job tracking after it was interrupted. + + .. versionadded:: 0.31.0 + """ + +
+[docs] + @abc.abstractmethod + def exists(self) -> bool: + """Does the job database already exist, to read job data from?""" + ...
+ + +
+[docs] + @abc.abstractmethod + def read(self) -> pd.DataFrame: + """ + Read job data from the database as pandas DataFrame. + + :return: loaded job data. + """ + ...
+ + +
+[docs] + @abc.abstractmethod + def persist(self, df: pd.DataFrame): + """ + Store job data to the database. + The provided dataframe may contain partial information, which is merged into the larger database. + + :param df: job data to store. + """ + ...
+ + +
+[docs] + @abc.abstractmethod + def count_by_status(self, statuses: List[str]) -> dict: + """ + Retrieve the number of jobs per status. + + :return: dictionary with status as key and the count as value. + """ + ...
+ + +
+[docs] + @abc.abstractmethod + def get_by_status(self, statuses: List[str], max=None) -> pd.DataFrame: + """ + Returns a dataframe with jobs, filtered by status. + + :param statuses: List of statuses to include. + :param max: Maximum number of jobs to return. + + :return: DataFrame with jobs filtered by status. + """ + ...
+
+ + + +def _start_job_default(row: pd.Series, connection: Connection, *args, **kwargs): + raise NotImplementedError("No 'start_job' callable provided") + + +
+[docs] +class MultiBackendJobManager: + """ + Tracker for multiple jobs on multiple backends. + + Usage example: + + .. code-block:: python + + import logging + import pandas as pd + import openeo + from openeo.extra.job_management import MultiBackendJobManager + + logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO + ) + + manager = MultiBackendJobManager() + manager.add_backend("foo", connection=openeo.connect("http://foo.test")) + manager.add_backend("bar", connection=openeo.connect("http://bar.test")) + + jobs_df = pd.DataFrame(...) + output_file = "jobs.csv" + + def start_job( + row: pd.Series, + connection: openeo.Connection, + **kwargs + ) -> openeo.BatchJob: + year = row["year"] + cube = connection.load_collection( + ..., + temporal_extent=[f"{year}-01-01", f"{year+1}-01-01"], + ) + ... + return cube.create_job(...) + + manager.run_jobs(df=jobs_df, start_job=start_job, output_file=output_file) + + See :py:meth:`.run_jobs` for more information on the ``start_job`` callable. + + :param poll_sleep: + How many seconds to sleep between polls. + + :param root_dir: + Root directory to save files for the jobs, e.g. metadata and error logs. + This defaults to "." the current directory. + + Each job gets its own subfolder in this root directory. + You can use the following methods to find the relevant paths, + based on the job ID: + + - get_job_dir + - get_error_log_path + - get_job_metadata_path + + :param cancel_running_job_after: + Optional temporal limit (in seconds) after which running jobs should be canceled + by the job manager. + + .. versionadded:: 0.14.0 + + .. versionchanged:: 0.32.0 + Added ``cancel_running_job_after`` parameter. + """ + + def __init__( + self, + poll_sleep: int = 60, + root_dir: Optional[Union[str, Path]] = ".", + *, + cancel_running_job_after: Optional[int] = None, + ): + """Create a MultiBackendJobManager.""" + self._stop_thread = None + self.backends: Dict[str, _Backend] = {} + self.poll_sleep = poll_sleep + self._connections: Dict[str, _Backend] = {} + + # An explicit None or "" should also default to "." + self._root_dir = Path(root_dir or ".") + + self._cancel_running_job_after = ( + datetime.timedelta(seconds=cancel_running_job_after) if cancel_running_job_after is not None else None + ) + self._thread = None + +
+[docs] + def add_backend( + self, + name: str, + connection: Union[Connection, Callable[[], Connection]], + parallel_jobs: int = 2, + ): + """ + Register a backend with a name and a Connection getter. + + :param name: + Name of the backend. + :param connection: + Either a Connection to the backend, or a callable to create a backend connection. + :param parallel_jobs: + Maximum number of jobs to allow in parallel on a backend. + """ + + # TODO: Code might become simpler if we turn _Backend into class move this logic there. + # We would need to keep add_backend here as part of the public API though. + # But the amount of unrelated "stuff to manage" would be less (better cohesion) + if isinstance(connection, Connection): + c = connection + connection = lambda: c + assert callable(connection) + self.backends[name] = _Backend(get_connection=connection, parallel_jobs=parallel_jobs)
+ + + def _get_connection(self, backend_name: str, resilient: bool = True) -> Connection: + """Get a connection for the backend and optionally make it resilient (adds retry behavior) + + The default is to get a resilient connection, but if necessary you can turn it off with + resilient=False + """ + + # TODO: Code could be simplified if _Backend is a class and this method is moved there. + # TODO: Is it better to make this a public method? + + # Reuse the connection if we can, in order to avoid modifying the same connection several times. + # This is to avoid adding the retry HTTPAdapter multiple times. + # Remember that the get_connection attribute on _Backend can be a Connection object instead + # of a callable, so we don't want to assume it is a fresh connection that doesn't have the + # retry adapter yet. + if backend_name in self._connections: + return self._connections[backend_name] + + connection = self.backends[backend_name].get_connection() + # If we really need it we can skip making it resilient, but by default it should be resilient. + if resilient: + self._make_resilient(connection) + + self._connections[backend_name] = connection + return connection + + @staticmethod + def _make_resilient(connection): + """Add an HTTPAdapter that retries the request if it fails. + + Retry for the following HTTP 50x statuses: + 502 Bad Gateway + 503 Service Unavailable + 504 Gateway Timeout + """ + # TODO: refactor this helper out of this class and unify with `openeo_driver.util.http.requests_with_retry` + status_forcelist = [500, 502, 503, 504] + retries = Retry( + total=MAX_RETRIES, + read=MAX_RETRIES, + other=MAX_RETRIES, + status=MAX_RETRIES, + backoff_factor=0.1, + status_forcelist=status_forcelist, + allowed_methods=["HEAD", "GET", "OPTIONS", "POST"], + ) + connection.session.mount("https://", HTTPAdapter(max_retries=retries)) + connection.session.mount("http://", HTTPAdapter(max_retries=retries)) + + @staticmethod + def _normalize_df(df: pd.DataFrame) -> pd.DataFrame: + """ + Normalize given pandas dataframe (creating a new one): + ensure we have the required columns. + + :param df: The dataframe to normalize. + :return: a new dataframe that is normalized. + """ + # check for some required columns. + required_with_default = [ + ("status", "not_started"), + ("id", None), + ("start_time", None), + ("running_start_time", None), + # TODO: columns "cpu", "memory", "duration" are not referenced directly + # within MultiBackendJobManager making it confusing to claim they are required. + # However, they are through assumptions about job "usage" metadata in `_track_statuses`. + # => proposed solution: allow to configure usage columns when adding a backend + ("cpu", None), + ("memory", None), + ("duration", None), + ("backend_name", None), + ] + new_columns = {col: val for (col, val) in required_with_default if col not in df.columns} + df = df.assign(**new_columns) + + return df + +
+[docs] + def start_job_thread(self, start_job: Callable[[], BatchJob], job_db: JobDatabaseInterface): + """ + Start running the jobs in a separate thread, returns afterwards. + + :param start_job: + A callback which will be invoked with, amongst others, + the row of the dataframe for which a job should be created and/or started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + The following parameters will be passed to ``start_job``: + + ``row`` (:py:class:`pandas.Series`): + The row in the pandas dataframe that stores the jobs state and other tracked data. + + ``connection_provider``: + A getter to get a connection by backend name. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``connection`` (:py:class:`Connection`): + The :py:class:`Connection` itself, that has already been created. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``provider`` (``str``): + The name of the backend that will run the job. + + You do not have to define all the parameters described below, but if you leave + any of them out, then remember to include the ``*args`` and ``**kwargs`` parameters. + Otherwise you will have an exception because :py:meth:`run_jobs` passes unknown parameters to ``start_job``. + :param job_db: + Job database to load/store existing job status data and other metadata from/to. + Can be specified as a path to CSV or Parquet file, + or as a custom database object following the :py:class:`JobDatabaseInterface` interface. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency <installation-optional-dependencies>`. + + .. versionadded:: 0.32.0 + """ + + # Resume from existing db + _log.info(f"Resuming `run_jobs` from existing {job_db}") + df = job_db.read() + + self._stop_thread = False + def run_loop(): + while ( + sum(job_db.count_by_status(statuses=["not_started", "created", "queued", "running"]).values()) > 0 + and not self._stop_thread + ): + self._job_update_loop(job_db=job_db, start_job=start_job) + + # Do sequence of micro-sleeps to allow for quick thread exit + for _ in range(int(max(1, self.poll_sleep))): + time.sleep(1) + if self._stop_thread: + break + + self._thread = Thread(target=run_loop) + self._thread.start()
+ + +
+[docs] + def stop_job_thread(self, timeout_seconds: Optional[float] = _UNSET): + """ + Stop the job polling thread. + + :param timeout_seconds: The time to wait for the thread to stop. + By default, it will wait for 2 times the poll_sleep time. + Set to None to wait indefinitely. + + .. versionadded:: 0.32.0 + """ + if self._thread is not None: + self._stop_thread = True + if timeout_seconds is _UNSET: + timeout_seconds = 2 * self.poll_sleep + self._thread.join(timeout_seconds) + if self._thread.is_alive(): + _log.warning("Job thread did not stop after timeout") + else: + _log.error("No job thread to stop")
+ + +
+[docs] + def run_jobs( + self, + df: Optional[pd.DataFrame] = None, + start_job: Callable[[], BatchJob] = _start_job_default, + job_db: Union[str, Path, JobDatabaseInterface, None] = None, + **kwargs, + ) -> dict: + """Runs jobs, specified in a dataframe, and tracks parameters. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. If None, the job_db has to be specified and will be used. + + :param start_job: + A callback which will be invoked with, amongst others, + the row of the dataframe for which a job should be created and/or started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + The following parameters will be passed to ``start_job``: + + ``row`` (:py:class:`pandas.Series`): + The row in the pandas dataframe that stores the jobs state and other tracked data. + + ``connection_provider``: + A getter to get a connection by backend name. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``connection`` (:py:class:`Connection`): + The :py:class:`Connection` itself, that has already been created. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``provider`` (``str``): + The name of the backend that will run the job. + + You do not have to define all the parameters described below, but if you leave + any of them out, then remember to include the ``*args`` and ``**kwargs`` parameters. + Otherwise you will have an exception because :py:meth:`run_jobs` passes unknown parameters to ``start_job``. + + :param job_db: + Job database to load/store existing job status data and other metadata from/to. + Can be specified as a path to CSV or Parquet file, + or as a custom database object following the :py:class:`JobDatabaseInterface` interface. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency <installation-optional-dependencies>`. + + :return: dictionary with stats collected during the job running loop. + Note that the set of fields in this dictionary is experimental + and subject to change + + .. versionchanged:: 0.31.0 + Added support for persisting the job metadata in Parquet format. + + .. versionchanged:: 0.31.0 + Replace ``output_file`` argument with ``job_db`` argument, + which can be a path to a CSV or Parquet file, + or a user-defined :py:class:`JobDatabaseInterface` object. + The deprecated ``output_file`` argument is still supported for now. + + .. versionchanged:: 0.33.0 + return a stats dictionary + """ + # TODO Defining start_jobs as a Protocol might make its usage more clear, and avoid complicated docstrings, + + # Backwards compatibility for deprecated `output_file` argument + if "output_file" in kwargs: + if job_db is not None: + raise ValueError("Only one of `output_file` and `job_db` should be provided") + warnings.warn( + "The `output_file` argument is deprecated. Use `job_db` instead.", DeprecationWarning, stacklevel=2 + ) + job_db = kwargs.pop("output_file") + assert not kwargs, f"Unexpected keyword arguments: {kwargs!r}" + + if isinstance(job_db, (str, Path)): + job_db = get_job_db(path=job_db) + + if not isinstance(job_db, JobDatabaseInterface): + raise ValueError(f"Unsupported job_db {job_db!r}") + + if job_db.exists(): + # Resume from existing db + _log.info(f"Resuming `run_jobs` from existing {job_db}") + elif df is not None: + # TODO: start showing deprecation warnings for this usage pattern? + job_db.initialize_from_df(df) + + stats = collections.defaultdict(int) + while sum(job_db.count_by_status(statuses=["not_started", "created", "queued", "running"]).values()) > 0: + self._job_update_loop(job_db=job_db, start_job=start_job, stats=stats) + stats["run_jobs loop"] += 1 + + time.sleep(self.poll_sleep) + stats["sleep"] += 1 + + return stats
+ + + def _job_update_loop( + self, job_db: JobDatabaseInterface, start_job: Callable[[], BatchJob], stats: Optional[dict] = None + ): + """ + Inner loop logic of job management: + go through the necessary jobs to check for status updates, + trigger status events, start new jobs when there is room for them, etc. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + with ignore_connection_errors(context="get statuses"): + self._track_statuses(job_db, stats=stats) + stats["track_statuses"] += 1 + + not_started = job_db.get_by_status(statuses=["not_started"], max=200).copy() + if len(not_started) > 0: + # Check number of jobs running at each backend + running = job_db.get_by_status(statuses=["created", "queued", "running"]) + stats["job_db get_by_status"] += 1 + per_backend = running.groupby("backend_name").size().to_dict() + _log.info(f"Running per backend: {per_backend}") + total_added = 0 + for backend_name in self.backends: + backend_load = per_backend.get(backend_name, 0) + if backend_load < self.backends[backend_name].parallel_jobs: + to_add = self.backends[backend_name].parallel_jobs - backend_load + for i in not_started.index[total_added : total_added + to_add]: + self._launch_job(start_job, df=not_started, i=i, backend_name=backend_name, stats=stats) + stats["job launch"] += 1 + + job_db.persist(not_started.loc[i : i + 1]) + stats["job_db persist"] += 1 + total_added += 1 + + def _launch_job(self, start_job, df, i, backend_name, stats: Optional[dict] = None): + """Helper method for launching jobs + + :param start_job: + A callback which will be invoked with the row of the dataframe for which a job should be started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + See also: + `MultiBackendJobManager.run_jobs` for the parameters and return type of this callable + + Even though it is called here in `_launch_job` and that is where the constraints + really come from, the public method `run_jobs` needs to document `start_job` anyway, + so let's avoid duplication in the docstrings. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. + + :param i: + index of the job's row in dataframe df + + :param backend_name: + name of the backend that will execute the job. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + df.loc[i, "backend_name"] = backend_name + row = df.loc[i] + try: + _log.info(f"Starting job on backend {backend_name} for {row.to_dict()}") + connection = self._get_connection(backend_name, resilient=True) + + stats["start_job call"] += 1 + job = start_job( + row=row, + connection_provider=self._get_connection, + connection=connection, + provider=backend_name, + ) + except requests.exceptions.ConnectionError as e: + _log.warning(f"Failed to start job for {row.to_dict()}", exc_info=True) + df.loc[i, "status"] = "start_failed" + stats["start_job error"] += 1 + else: + df.loc[i, "start_time"] = rfc3339.utcnow() + if job: + df.loc[i, "id"] = job.job_id + with ignore_connection_errors(context="get status"): + status = job.status() + stats["job get status"] += 1 + df.loc[i, "status"] = status + if status == "created": + # start job if not yet done by callback + try: + job.start() + stats["job start"] += 1 + df.loc[i, "status"] = job.status() + stats["job get status"] += 1 + except OpenEoApiError as e: + _log.error(e) + df.loc[i, "status"] = "start_failed" + stats["job start error"] += 1 + else: + # TODO: what is this "skipping" about actually? + df.loc[i, "status"] = "skipped" + stats["start_job skipped"] += 1 + +
+[docs] + def on_job_done(self, job: BatchJob, row): + """ + Handles jobs that have finished. Can be overridden to provide custom behaviour. + + Default implementation downloads the results into a folder containing the title. + + :param job: The job that has finished. + :param row: DataFrame row containing the job's metadata. + """ + # TODO: param `row` is never accessed in this method. Remove it? Is this intended for future use? + + job_metadata = job.describe() + job_dir = self.get_job_dir(job.job_id) + metadata_path = self.get_job_metadata_path(job.job_id) + + self.ensure_job_dir_exists(job.job_id) + job.get_results().download_files(target=job_dir) + + with metadata_path.open("w", encoding="utf-8") as f: + json.dump(job_metadata, f, ensure_ascii=False)
+ + +
+[docs] + def on_job_error(self, job: BatchJob, row): + """ + Handles jobs that stopped with errors. Can be overridden to provide custom behaviour. + + Default implementation writes the error logs to a JSON file. + + :param job: The job that has finished. + :param row: DataFrame row containing the job's metadata. + """ + # TODO: param `row` is never accessed in this method. Remove it? Is this intended for future use? + + error_logs = job.logs(level="error") + error_log_path = self.get_error_log_path(job.job_id) + + if len(error_logs) > 0: + self.ensure_job_dir_exists(job.job_id) + error_log_path.write_text(json.dumps(error_logs, indent=2))
+ + +
+[docs] + def on_job_cancel(self, job: BatchJob, row): + """ + Handle a job that was cancelled. Can be overridden to provide custom behaviour. + + Default implementation does not do anything. + + :param job: The job that was canceled. + :param row: DataFrame row containing the job's metadata. + """ + pass
+ + + def _cancel_prolonged_job(self, job: BatchJob, row): + """Cancel the job if it has been running for too long.""" + job_running_start_time = rfc3339.parse_datetime(row["running_start_time"], with_timezone=True) + elapsed = datetime.datetime.now(tz=datetime.timezone.utc) - job_running_start_time + if elapsed > self._cancel_running_job_after: + try: + _log.info( + f"Cancelling long-running job {job.job_id} (after {elapsed}, running since {job_running_start_time})" + ) + job.stop() + except OpenEoApiError as e: + _log.error(f"Failed to cancel long-running job {job.job_id}: {e}") + +
+[docs] + def get_job_dir(self, job_id: str) -> Path: + """Path to directory where job metadata, results and error logs are be saved.""" + return self._root_dir / f"job_{job_id}"
+ + +
+[docs] + def get_error_log_path(self, job_id: str) -> Path: + """Path where error log file for the job is saved.""" + return self.get_job_dir(job_id) / f"job_{job_id}_errors.json"
+ + +
+[docs] + def get_job_metadata_path(self, job_id: str) -> Path: + """Path where job metadata file is saved.""" + return self.get_job_dir(job_id) / f"job_{job_id}.json"
+ + +
+[docs] + def ensure_job_dir_exists(self, job_id: str) -> Path: + """Create the job folder if it does not exist yet.""" + job_dir = self.get_job_dir(job_id) + if not job_dir.exists(): + job_dir.mkdir(parents=True)
+ + + def _track_statuses(self, job_db: JobDatabaseInterface, stats: Optional[dict] = None): + """ + Tracks status (and stats) of running jobs (in place). + Optionally cancels jobs when running too long. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + active = job_db.get_by_status(statuses=["created", "queued", "running"]).copy() + for i in active.index: + job_id = active.loc[i, "id"] + backend_name = active.loc[i, "backend_name"] + previous_status = active.loc[i, "status"] + + try: + con = self._get_connection(backend_name) + the_job = con.job(job_id) + job_metadata = the_job.describe() + stats["job describe"] += 1 + new_status = job_metadata["status"] + + _log.info( + f"Status of job {job_id!r} (on backend {backend_name}) is {new_status!r} (previously {previous_status!r})" + ) + + if new_status == "finished": + stats["job finished"] += 1 + self.on_job_done(the_job, active.loc[i]) + + if previous_status != "error" and new_status == "error": + stats["job failed"] += 1 + self.on_job_error(the_job, active.loc[i]) + + if previous_status in {"created", "queued"} and new_status == "running": + stats["job started running"] += 1 + active.loc[i, "running_start_time"] = rfc3339.utcnow() + + if new_status == "canceled": + stats["job canceled"] += 1 + self.on_job_cancel(the_job, active.loc[i]) + + if self._cancel_running_job_after and new_status == "running": + self._cancel_prolonged_job(the_job, active.loc[i]) + + active.loc[i, "status"] = new_status + + # TODO: there is well hidden coupling here with "cpu", "memory" and "duration" from `_normalize_df` + for key in job_metadata.get("usage", {}).keys(): + if key in active.columns: + active.loc[i, key] = _format_usage_stat(job_metadata, key) + + except OpenEoApiError as e: + # TODO: inspect status code and e.g. differentiate between 4xx/5xx + stats["job tracking error"] += 1 + _log.warning(f"Error while tracking status of job {job_id!r} on backend {backend_name}: {e!r}") + + stats["job_db persist"] += 1 + job_db.persist(active)
+ + + +def _format_usage_stat(job_metadata: dict, field: str) -> str: + value = deep_get(job_metadata, "usage", field, "value", default=0) + unit = deep_get(job_metadata, "usage", field, "unit", default="") + return f"{value} {unit}".strip() + + +@contextlib.contextmanager +def ignore_connection_errors(context: Optional[str] = None, sleep: int = 5): + """Context manager to ignore connection errors.""" + # TODO: move this out of this module and make it a more public utility? + try: + yield + except requests.exceptions.ConnectionError as e: + _log.warning(f"Ignoring connection error (context {context or 'n/a'}): {e}") + # Back off a bit + time.sleep(sleep) + + +class FullDataFrameJobDatabase(JobDatabaseInterface): + + def __init__(self): + super().__init__() + self._df = None + + def initialize_from_df(self, df: pd.DataFrame, *, on_exists: str = "error"): + """ + Initialize the job database from a given dataframe, + which will be first normalized to be compatible + with :py:class:`MultiBackendJobManager` usage. + + :param df: dataframe with some columns your ``start_job`` callable expects + :param on_exists: what to do when the job database already exists (persisted on disk): + - "error": (default) raise an exception + - "skip": work with existing database, ignore given dataframe and skip any initialization + + :return: initialized job database. + + .. versionadded:: 0.33.0 + """ + # TODO: option to provide custom MultiBackendJobManager subclass with custom normalize? + if self.exists(): + if on_exists == "skip": + return self + elif on_exists == "error": + raise FileExistsError(f"Job database {self!r} already exists.") + else: + # TODO handle other on_exists modes: e.g. overwrite, merge, ... + raise ValueError(f"Invalid on_exists={on_exists!r}") + df = MultiBackendJobManager._normalize_df(df) + self.persist(df) + # Return self to allow chaining with constructor. + return self + + @property + def df(self) -> pd.DataFrame: + if self._df is None: + self._df = self.read() + return self._df + + def count_by_status(self, statuses: List[str]) -> dict: + status_histogram = self.df.groupby("status").size().to_dict() + return {k:v for k,v in status_histogram.items() if k in statuses} + + def get_by_status(self, statuses, max=None) -> pd.DataFrame: + """ + Returns a dataframe with jobs, filtered by status. + + :param statuses: List of statuses to include. + :param max: Maximum number of jobs to return. + + :return: DataFrame with jobs filtered by status. + """ + df = self.df + filtered = df[df.status.isin(statuses)] + return filtered.head(max) if max is not None else filtered + + def _merge_into_df(self, df: pd.DataFrame): + if self._df is not None: + self._df.update(df, overwrite=True) + else: + self._df = df + + +
+[docs] +class CsvJobDatabase(FullDataFrameJobDatabase): + """ + Persist/load job metadata with a CSV file. + + :implements: :py:class:`JobDatabaseInterface` + :param path: Path to local CSV file. + + .. note:: + Support for GeoPandas dataframes depends on the ``geopandas`` package + as :ref:`optional dependency <installation-optional-dependencies>`. + + .. versionadded:: 0.31.0 + """ + def __init__(self, path: Union[str, Path]): + super().__init__() + self.path = Path(path) + + def __repr__(self): + return f"{self.__class__.__name__}({str(self.path)!r})" + + def exists(self) -> bool: + return self.path.exists() + + def _is_valid_wkt(self, wkt: str) -> bool: + try: + shapely.wkt.loads(wkt) + return True + except shapely.errors.WKTReadingError: + return False + + def read(self) -> pd.DataFrame: + df = pd.read_csv(self.path) + if ( + "geometry" in df.columns + and df["geometry"].dtype.name != "geometry" + and self._is_valid_wkt(df["geometry"].iloc[0]) + ): + import geopandas + + # `df.to_csv()` in `persist()` has encoded geometries as WKT, so we decode that here. + df = geopandas.GeoDataFrame(df, geometry=geopandas.GeoSeries.from_wkt(df["geometry"])) + return df + + def persist(self, df: pd.DataFrame): + self._merge_into_df(df) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.df.to_csv(self.path, index=False)
+ + + +
+[docs] +class ParquetJobDatabase(FullDataFrameJobDatabase): + """ + Persist/load job metadata with a Parquet file. + + :implements: :py:class:`JobDatabaseInterface` + :param path: Path to the Parquet file. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency <installation-optional-dependencies>`. + + Support for GeoPandas dataframes depends on the ``geopandas`` package + as :ref:`optional dependency <installation-optional-dependencies>`. + + .. versionadded:: 0.31.0 + """ + def __init__(self, path: Union[str, Path]): + super().__init__() + self.path = Path(path) + + def __repr__(self): + return f"{self.__class__.__name__}({str(self.path)!r})" + + def exists(self) -> bool: + return self.path.exists() + + def read(self) -> pd.DataFrame: + # Unfortunately, a naive `pandas.read_parquet()` does not easily allow + # reconstructing geometries from a GeoPandas Parquet file. + # And vice-versa, `geopandas.read_parquet()` does not support reading + # Parquet file without geometries. + # So we have to guess which case we have. + # TODO is there a cleaner way to do this? + import pyarrow.parquet + + metadata = pyarrow.parquet.read_metadata(self.path) + if b"geo" in metadata.metadata: + import geopandas + return geopandas.read_parquet(self.path) + else: + return pd.read_parquet(self.path) + + def persist(self, df: pd.DataFrame): + self._merge_into_df(df) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.df.to_parquet(self.path, index=False)
+ + + +def get_job_db(path: Union[str, Path]) -> JobDatabaseInterface: + """ + Factory to get a job database at a given path, + guessing the database type from filename extension. + + :param path: path to job database file. + + .. versionadded:: 0.33.0 + """ + path = Path(path) + if path.suffix.lower() in {".csv"}: + job_db = CsvJobDatabase(path=path) + elif path.suffix.lower() in {".parquet", ".geoparquet"}: + job_db = ParquetJobDatabase(path=path) + else: + raise ValueError(f"Could not guess job database type from {path!r}") + return job_db + + +def create_job_db(path: Union[str, Path], df: pd.DataFrame, *, on_exists: str = "error"): + """ + Factory to create a job database at given path, + initialized from a given dataframe, + and its database type guessed from filename extension. + + :param path: Path to the job database file. + :param df: DataFrame to store in the job database. + :param on_exists: What to do when the job database already exists: + - "error": (default) raise an exception + - "skip": work with existing database, ignore given dataframe and skip any initialization + + .. versionadded:: 0.33.0 + """ + job_db = get_job_db(path) + if isinstance(job_db, FullDataFrameJobDatabase): + job_db.initialize_from_df(df=df, on_exists=on_exists) + else: + raise NotImplementedError(f"Initialization of {type(job_db)} is not supported.") + return job_db + + +
+[docs] +class ProcessBasedJobCreator: + """ + Batch job creator + (to be used together with :py:class:`MultiBackendJobManager`) + that takes a parameterized openEO process definition + (e.g a user-defined process (UDP) or a remote openEO process definition), + and creates a batch job + for each row of the dataframe managed by the :py:class:`MultiBackendJobManager` + by filling in the process parameters with corresponding row values. + + .. seealso:: + See :ref:`job-management-with-process-based-job-creator` + for more information and examples. + + Process parameters are linked to dataframe columns by name. + While this intuitive name-based matching should cover most use cases, + there are additional options for overrides or fallbacks: + + - When provided, ``parameter_column_map`` will be consulted + for resolving a process parameter name (key in the dictionary) + to a desired dataframe column name (corresponding value). + - One common case is handled automatically as convenience functionality. + + When: + + - ``parameter_column_map`` is not provided (or set to ``None``), + - and there is a *single parameter* that accepts inline GeoJSON geometries, + - and the dataframe is a GeoPandas dataframe with a *single geometry* column, + + then this parameter and this geometries column will be linked automatically. + + - If a parameter can not be matched with a column by name as described above, + a default value will be picked, + first by looking in ``parameter_defaults`` (if provided), + and then by looking up the default value from the parameter schema in the process definition. + - Finally if no (default) value can be determined and the parameter + is not flagged as optional, an error will be raised. + + + :param process_id: (optional) openEO process identifier. + Can be omitted when working with a remote process definition + that is fully defined with a URL in the ``namespace`` parameter. + :param namespace: (optional) openEO process namespace. + Typically used to provide a URL to a remote process definition. + :param parameter_defaults: (optional) default values for process parameters, + to be used when not available in the dataframe managed by + :py:class:`MultiBackendJobManager`. + :param parameter_column_map: Optional overrides + for linking process parameters to dataframe columns: + mapping of process parameter names as key + to dataframe column names as value. + + .. versionadded:: 0.33.0 + + .. warning:: + This is an experimental API subject to change, + and we greatly welcome + `feedback and suggestions for improvement <https://github.com/Open-EO/openeo-python-client/issues>`_. + + """ + def __init__( + self, + *, + process_id: Optional[str] = None, + namespace: Union[str, None] = None, + parameter_defaults: Optional[dict] = None, + parameter_column_map: Optional[dict] = None, + ): + if process_id is None and namespace is None: + raise ValueError("At least one of `process_id` and `namespace` should be provided.") + self._process_id = process_id + self._namespace = namespace + self._parameter_defaults = parameter_defaults or {} + self._parameter_column_map = parameter_column_map + self._cache = LazyLoadCache() + + def _get_process_definition(self, connection: Connection) -> Process: + if isinstance(self._namespace, str) and re.match("https?://", self._namespace): + # Remote process definition handling + return self._cache.get( + key=("remote_process_definition", self._namespace, self._process_id), + load=lambda: parse_remote_process_definition(namespace=self._namespace, process_id=self._process_id), + ) + elif self._namespace is None: + # Handling of a user-specific UDP + udp_raw = connection.user_defined_process(self._process_id).describe() + return Process.from_dict(udp_raw) + else: + raise NotImplementedError( + f"Unsupported process definition source udp_id={self._process_id!r} namespace={self._namespace!r}" + ) + + +
+[docs] + def start_job(self, row: pd.Series, connection: Connection, **_) -> BatchJob: + """ + Implementation of the ``start_job`` callable interface + of :py:meth:`MultiBackendJobManager.run_jobs` + to create a job based on given dataframe row + + :param row: The row in the pandas dataframe that stores the jobs state and other tracked data. + :param connection: The connection to the backend. + """ + # TODO: refactor out some methods, for better reuse and decoupling: + # `get_arguments()` (to build the arguments dictionary), `get_cube()` (to create the cube), + + process_definition = self._get_process_definition(connection=connection) + process_id = process_definition.id + parameters = process_definition.parameters or [] + + if self._parameter_column_map is None: + self._parameter_column_map = self._guess_parameter_column_map(parameters=parameters, row=row) + + arguments = {} + for parameter in parameters: + param_name = parameter.name + column_name = self._parameter_column_map.get(param_name, param_name) + if column_name in row.index: + # Get value from dataframe row + value = row.loc[column_name] + elif param_name in self._parameter_defaults: + # Fallback on default values from constructor + value = self._parameter_defaults[param_name] + elif parameter.has_default(): + # Explicitly use default value from parameter schema + value = parameter.default + elif parameter.optional: + # Skip optional parameters without any fallback default value + continue + else: + raise ValueError(f"Missing required parameter {param_name !r} for process {process_id!r}") + + # Prepare some values/dtypes for JSON encoding + if isinstance(value, numpy.integer): + value = int(value) + elif isinstance(value, numpy.number): + value = float(value) + elif isinstance(value, shapely.geometry.base.BaseGeometry): + value = shapely.geometry.mapping(value) + + arguments[param_name] = value + + cube = connection.datacube_from_process(process_id=process_id, namespace=self._namespace, **arguments) + + title = row.get("title", f"Process {process_id!r} with {repr_truncate(arguments)}") + description = row.get("description", f"Process {process_id!r} (namespace {self._namespace}) with {arguments}") + job = connection.create_job(cube, title=title, description=description) + + return job
+ + +
+[docs] + def __call__(self, *arg, **kwargs) -> BatchJob: + """Syntactic sugar for calling :py:meth:`start_job`.""" + return self.start_job(*arg, **kwargs)
+ + + @staticmethod + def _guess_parameter_column_map(parameters: List[Parameter], row: pd.Series) -> dict: + """ + Guess parameter-column mapping from given parameter list and dataframe row + """ + parameter_column_map = {} + # Geometry based mapping: try to automatically map geometry columns to geojson parameters + geojson_parameters = [p.name for p in parameters if p.schema.accepts_geojson()] + geometry_columns = [i for (i, v) in row.items() if isinstance(v, shapely.geometry.base.BaseGeometry)] + if geojson_parameters and geometry_columns: + if len(geojson_parameters) == 1 and len(geometry_columns) == 1: + # Most common case: one geometry parameter and one geometry column: can be mapped naively + parameter_column_map[geojson_parameters[0]] = geometry_columns[0] + elif all(p in geometry_columns for p in geojson_parameters): + # Each geometry param has geometry column with same name: easy to map + parameter_column_map.update((p, p) for p in geojson_parameters) + else: + raise RuntimeError( + f"Problem with mapping geometry columns ({geometry_columns}) to process parameters ({geojson_parameters})" + ) + _log.debug(f"Guessed parameter-column map: {parameter_column_map}") + return parameter_column_map
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/extra/spectral_indices/spectral_indices.html b/_modules/openeo/extra/spectral_indices/spectral_indices.html new file mode 100644 index 000000000..1a7296241 --- /dev/null +++ b/_modules/openeo/extra/spectral_indices/spectral_indices.html @@ -0,0 +1,620 @@ + + + + + + + openeo.extra.spectral_indices.spectral_indices — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.extra.spectral_indices.spectral_indices

+import functools
+import json
+import re
+from typing import Dict, List, Optional, Set
+
+from openeo import BaseOpenEoException
+from openeo.processes import ProcessBuilder, array_create, array_modify
+from openeo.rest.datacube import DataCube
+
+try:
+    import importlib_resources
+except ImportError:
+    import importlib.resources as importlib_resources
+
+
+@functools.lru_cache(maxsize=1)
+def load_indices() -> Dict[str, dict]:
+    """Load set of supported spectral indices."""
+    # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class?
+    specs = {}
+
+    for path in [
+        "resources/awesome-spectral-indices/spectral-indices-dict.json",
+        # TODO #506 Deprecate extra-indices-dict.json as a whole
+        #      and provide an alternative mechanism to work with custom indices
+        "resources/extra-indices-dict.json",
+    ]:
+        with importlib_resources.files("openeo.extra.spectral_indices") / path as resource_path:
+            data = json.loads(resource_path.read_text(encoding="utf8"))
+            overwrites = set(specs.keys()).intersection(data["SpectralIndices"].keys())
+            if overwrites:
+                raise RuntimeError(f"Duplicate spectral indices: {overwrites} from {path}")
+            specs.update(data["SpectralIndices"])
+
+    return specs
+
+
+@functools.lru_cache(maxsize=1)
+def load_constants() -> Dict[str, float]:
+    """Load constants defined by Awesome Spectral Indices."""
+    # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class?
+    with importlib_resources.files(
+        "openeo.extra.spectral_indices"
+    ) / "resources/awesome-spectral-indices/constants.json" as resource_path:
+        data = json.loads(resource_path.read_text(encoding="utf8"))
+
+    return {k: v["default"] for k, v in data.items() if isinstance(v["default"], (int, float))}
+
+
+@functools.lru_cache(maxsize=1)
+def _load_bands() -> Dict[str, dict]:
+    """Load band name mapping defined by Awesome Spectral Indices."""
+    # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class?
+    with importlib_resources.files(
+        "openeo.extra.spectral_indices"
+    ) / "resources/awesome-spectral-indices/bands.json" as resource_path:
+        data = json.loads(resource_path.read_text(encoding="utf8"))
+    return data
+
+
+class BandMappingException(BaseOpenEoException):
+    """Failure to determine band-variable mapping."""
+
+
+class _BandMapping:
+    """
+    Helper class to extract mappings between band names and variable names used in Awesome Spectral Indices formulas.
+    """
+
+    _EXTRA = {
+        "sentinel1": {"HH": "HH", "HV": "HV", "VH": "VH", "VV": "VV"},
+    }
+
+    def __init__(self):
+        # Load bands.json from Awesome Spectral Indices
+        self._band_data = _load_bands()
+
+    @staticmethod
+    def _normalize_platform(platform: str) -> str:
+        platform = platform.lower().replace("-", "").replace(" ", "")
+        if platform in {"sentinel2a", "sentinel2b"}:
+            platform = "sentinel2"
+        return platform
+
+    @staticmethod
+    def _normalize_band_name(band_name: str) -> str:
+        band_name = band_name.upper()
+        # Normalize band names like "B01" to "B1"
+        band_name = re.sub(r"^B0+(\d+)$", r"B\1", band_name)
+        return band_name
+
+    @functools.lru_cache(maxsize=1)
+    def get_platforms(self) -> Set[str]:
+        """Get list of supported (normalized) satellite platforms."""
+        platforms = {p for var_data in self._band_data.values() for p in var_data.get("platforms", {}).keys()}
+        platforms.update(self._EXTRA.keys())
+        platforms.update({self._normalize_platform(p) for p in platforms})
+        return platforms
+
+    def guess_platform(self, name: str) -> str:
+        """Guess platform from given collection id or name."""
+        # First check original id, then retry with removed separators as last resort.
+        for haystack in [name.lower(), re.sub("[_ -]", "", name.lower())]:
+            for platform in sorted(self.get_platforms(), key=len, reverse=True):
+                if platform in haystack:
+                    return platform
+        raise BandMappingException(f"Unable to guess satellite platform from id {name!r}.")
+
+    def variable_to_band_name_map(self, platform: str) -> Dict[str, str]:
+        """
+        Build mapping from Awesome Spectral Indices variable names to (normalized) band names for given satellite platform.
+        """
+        platform_normalized = self._normalize_platform(platform)
+        if platform_normalized in self._EXTRA:
+            return self._EXTRA[platform_normalized]
+
+        var_to_band = {
+            var: pf_data["band"]
+            for var, var_data in self._band_data.items()
+            for pf, pf_data in var_data.get("platforms", {}).items()
+            if self._normalize_platform(pf) == platform_normalized
+        }
+        if not var_to_band:
+            raise BandMappingException(f"Empty band mapping derived for satellite platform {platform!r}")
+        return var_to_band
+
+    def actual_band_name_to_variable_map(self, platform: str, band_names: List[str]) -> Dict[str, str]:
+        """Build mapping from actual band names (as given) to Awesome Spectral Indices variable names."""
+        var_to_band = self.variable_to_band_name_map(platform=platform)
+        band_to_var = {
+            band_name: var
+            for var, normalized_band_name in var_to_band.items()
+            for band_name in band_names
+            if self._normalize_band_name(band_name) == normalized_band_name
+        }
+        return band_to_var
+
+
+
+[docs] +def list_indices() -> List[str]: + """List names of supported spectral indices""" + specs = load_indices() + return list(specs.keys())
+ + + +def _check_params(item, params): + range_vals = ["input_range", "output_range"] + if set(params) != set(range_vals): + raise ValueError( + f"You have set the parameters {params} on {item}, while the following are required {range_vals}" + ) + for rng in range_vals: + if params[rng] is None: + continue + if len(params[rng]) != 2: + raise ValueError( + f"The list of provided values {params[rng]} for parameter {rng} for {item} is not of length 2" + ) + # TODO: allow float too? + if not all(isinstance(val, int) for val in params[rng]): + raise ValueError("The ranges you supplied are not all of type int") + if (params["input_range"] is None) != (params["output_range"] is None): + raise ValueError(f"The index_range and output_range of {item} should either be both supplied, or both None") + + +def _check_validity_index_dict(index_dict: dict, index_specs: dict): + # TODO: this `index_dict` API needs some more rethinking: + # - the dictionary has no explicit order of indices, which can be important for end user + # - allow "collection" to be missing (e.g. if no rescaling is desired, or input data is not kept)? + # - option to define default output range, instead of having it to specify it for each index? + # - keep "rescaling" feature separate/orthogonal from "spectral indices" feature. It could be useful as + # a more generic machine learning data preparation feature + input_vals = ["collection", "indices"] + if set(index_dict.keys()) != set(input_vals): + raise ValueError( + f"The first level of the dictionary should contain the keys 'collection' and 'indices', but they contain {index_dict.keys()}" + ) + _check_params("collection", index_dict["collection"]) + for index, params in index_dict["indices"].items(): + if index not in index_specs.keys(): + raise NotImplementedError("Index " + index + " is not supported.") + _check_params(index, params) + + +def _callback( + x: ProcessBuilder, + index_dict: dict, + index_specs: dict, + append: bool, + band_names: List[str], + band_to_var: Dict[str, str], +) -> ProcessBuilder: + index_values = [] + x_res = x + + # TODO: use `label` parameter of `array_element` to avoid index based band references + variables = {band_to_var[bn]: x.array_element(i) for i, bn in enumerate(band_names) if bn in band_to_var} + eval_globals = { + **load_constants(), + **variables, + } + # TODO: user might want to control order of indices, which is tricky through a dictionary. + for index, params in index_dict["indices"].items(): + index_result = eval(index_specs[index]["formula"], eval_globals) + if params["input_range"] is not None: + index_result = index_result.linear_scale_range(*params["input_range"], *params["output_range"]) + index_values.append(index_result) + if index_dict["collection"]["input_range"] is not None: + x_res = x_res.linear_scale_range( + *index_dict["collection"]["input_range"], *index_dict["collection"]["output_range"] + ) + if append: + return array_modify(data=x_res, values=index_values, index=len(band_names)) + else: + return array_create(data=index_values) + + +
+[docs] +def compute_and_rescale_indices( + datacube: DataCube, + index_dict: dict, + *, + append: bool = False, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Computes a list of indices from a data cube + + :param datacube: input data cube + :param index_dict: a dictionary that contains the input- and output range of the collection on which you calculate the indices + as well as the indices that you want to calculate with their responding input- and output ranges + It follows the following format:: + + { + "collection": { + "input_range": [0,8000], + "output_range": [0,250] + }, + "indices": { + "NDVI": { + "input_range": [-1,1], + "output_range": [0,250] + }, + } + } + + If you don't want to rescale your data, you can fill the input-, index- and output-range with ``None``. + + See `list_indices()` for supported indices. + + :param append: append the indices as bands to the given data cube + instead of creating a new cube with only the calculated indices + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: the datacube with the indices attached as bands + + .. warning:: this "rescaled" index helper uses an experimental API (e.g. `index_dict` argument) that is subject to change. + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + + """ + index_specs = load_indices() + + _check_validity_index_dict(index_dict, index_specs) + + if variable_map is None: + # Automatic band mapping + band_mapping = _BandMapping() + if platform is None: + if datacube.metadata and datacube.metadata.get("id"): + platform = band_mapping.guess_platform(name=datacube.metadata.get("id")) + else: + raise BandMappingException("Unable to determine satellite platform from data cube metadata") + band_to_var = band_mapping.actual_band_name_to_variable_map( + platform=platform, band_names=datacube.metadata.band_names + ) + else: + band_to_var = {b: v for v, b in variable_map.items()} + + res = datacube.apply_dimension( + dimension="bands", + process=lambda x: _callback( + x, + index_dict=index_dict, + index_specs=index_specs, + append=append, + band_names=datacube.metadata.band_names, + band_to_var=band_to_var, + ), + ) + if append: + return res.rename_labels("bands", target=datacube.metadata.band_names + list(index_dict["indices"].keys())) + else: + return res.rename_labels("bands", target=list(index_dict["indices"].keys()))
+ + + +
+[docs] +def append_and_rescale_indices( + datacube: DataCube, + index_dict: dict, + *, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Computes a list of indices from a datacube and appends them to the existing datacube + + :param datacube: input data cube + :param index_dict: a dictionary that contains the input- and output range of the collection on which you calculate the indices + as well as the indices that you want to calculate with their responding input- and output ranges + It follows the following format:: + + { + "collection": { + "input_range": [0,8000], + "output_range": [0,250] + }, + "indices": { + "NDVI": { + "input_range": [-1,1], + "output_range": [0,250] + }, + } + } + + See `list_indices()` for supported indices. + + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended indices + + .. warning:: this "rescaled" index helper uses an experimental API (e.g. `index_dict` argument) that is subject to change. + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + return compute_and_rescale_indices( + datacube=datacube, index_dict=index_dict, append=True, variable_map=variable_map, platform=platform + )
+ + + +
+[docs] +def compute_indices( + datacube: DataCube, + indices: List[str], + *, + append: bool = False, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Compute multiple spectral indices from the given data cube. + + :param datacube: input data cube + :param indices: list of names of the indices to compute and append. See `list_indices()` for supported indices. + :param append: append the indices as bands to the given data cube + instead of creating a new cube with only the calculated indices + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube containing the indices as bands + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + # TODO: it's bit weird to have to specify all these None's in this structure + index_dict = { + "collection": { + "input_range": None, + "output_range": None, + }, + "indices": {index: {"input_range": None, "output_range": None} for index in indices}, + } + return compute_and_rescale_indices( + datacube=datacube, index_dict=index_dict, append=append, variable_map=variable_map, platform=platform + )
+ + + +
+[docs] +def append_indices( + datacube: DataCube, + indices: List[str], + *, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Compute multiple spectral indices and append them to the given data cube. + + :param datacube: input data cube + :param indices: list of names of the indices to compute and append. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended indices + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + + return compute_indices( + datacube=datacube, indices=indices, append=True, variable_map=variable_map, platform=platform + )
+ + + +
+[docs] +def compute_index( + datacube: DataCube, index: str, *, variable_map: Optional[Dict[str, str]] = None, platform: Optional[str] = None +) -> DataCube: + """ + Compute a single spectral index from a data cube. + + :param datacube: input data cube + :param index: name of the index to compute. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube containing the index as band + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + # TODO: option to compute the index with `reduce_dimension` instead of `apply_dimension`? + return compute_indices( + datacube=datacube, indices=[index], append=False, variable_map=variable_map, platform=platform + )
+ + + +
+[docs] +def append_index( + datacube: DataCube, index: str, *, variable_map: Optional[Dict[str, str]] = None, platform: Optional[str] = None +) -> DataCube: + """ + Compute a single spectral index and append it to the given data cube. + + :param cube: input data cube + :param index: name of the index to compute and append. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended index + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + return compute_indices( + datacube=datacube, indices=[index], append=True, variable_map=variable_map, platform=platform + )
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/internal/graph_building.html b/_modules/openeo/internal/graph_building.html new file mode 100644 index 000000000..6837228f4 --- /dev/null +++ b/_modules/openeo/internal/graph_building.html @@ -0,0 +1,630 @@ + + + + + + + openeo.internal.graph_building — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.internal.graph_building

+"""
+Internal openEO process graph building utilities
+''''''''''''''''''''''''''''''''''''''''''''''''''
+
+Internal functionality for abstracting, building, manipulating and processing openEO process graphs.
+
+"""
+
+from __future__ import annotations
+
+import abc
+import collections
+import copy
+import json
+import sys
+from contextlib import nullcontext
+from pathlib import Path
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
+
+from openeo.api.process import Parameter
+from openeo.internal.process_graph_visitor import (
+    ProcessGraphUnflattener,
+    ProcessGraphVisitException,
+    ProcessGraphVisitor,
+)
+from openeo.util import dict_no_none, load_json_resource
+
+
+
+[docs] +class FlatGraphableMixin(metaclass=abc.ABCMeta): + """ + Mixin for classes that can be exported/converted to + a "flat graph" representation of an openEO process graph. + """ + + @abc.abstractmethod + def flat_graph(self) -> Dict[str, dict]: + ... + +
+[docs] + def to_json(self, *, indent: Union[int, None] = 2, separators: Optional[Tuple[str, str]] = None) -> str: + """ + Get interoperable JSON representation of the process graph. + + See :py:meth:`DataCube.print_json` to directly print the JSON representation + and :ref:`process_graph_export` for more usage information. + + Also see ``json.dumps`` docs for more information on the JSON formatting options. + + :param indent: JSON indentation level. + :param separators: (optional) tuple of item/key separators. + :return: JSON string + """ + pg = {"process_graph": self.flat_graph()} + return json.dumps(pg, indent=indent, separators=separators)
+ + +
+[docs] + def print_json( + self, + *, + file=None, + indent: Union[int, None] = 2, + separators: Optional[Tuple[str, str]] = None, + end: str = "\n", + ): + """ + Print interoperable JSON representation of the process graph. + + See :py:meth:`DataCube.to_json` to get the JSON representation as a string + and :ref:`process_graph_export` for more usage information. + + Also see ``json.dumps`` docs for more information on the JSON formatting options. + + :param file: file-like object (stream) to print to (current ``sys.stdout`` by default). + Or a path (string or pathlib.Path) to a file to write to. + :param indent: JSON indentation level. + :param separators: (optional) tuple of item/key separators. + :param end: additional string to be printed at the end (newline by default). + + .. versionadded:: 0.12.0 + + .. versionadded:: 0.23.0 + added the ``end`` argument. + """ + pg = {"process_graph": self.flat_graph()} + if isinstance(file, (str, Path)): + # Create (new) file and automatically close it + file_ctx = Path(file).open("w", encoding="utf8") + else: + # Just use file as-is, but don't close it automatically. + file_ctx = nullcontext(enter_result=file or sys.stdout) + with file_ctx as f: + json.dump(pg, f, indent=indent, separators=separators) + if end: + f.write(end)
+
+ + + +class _FromNodeMixin(abc.ABC): + """Mixin for classes that want to hook into the generation of a "from_node" reference.""" + + @abc.abstractmethod + def from_node(self) -> PGNode: + # TODO: "from_node" is a bit a confusing name: + # it refers to the "from_node" node reference in openEO process graphs, + # but as a method name here it reads like "construct from PGNode", + # while it is actually meant as "export as PGNode" (that can be used in a "from_node" reference). + pass + + +
+[docs] +class PGNode(_FromNodeMixin, FlatGraphableMixin): + """ + A process node in a process graph: has at least a process_id and arguments. + + Note that a full openEO "process graph" is essentially a directed acyclic graph of nodes + pointing to each other. A full process graph is practically equivalent with its "result" node, + as it points (directly or indirectly) to all the other nodes it depends on. + + .. warning:: + This class is an implementation detail meant for internal use. + It is not recommended for general use in normal user code. + Instead, use process graph abstraction builders like + :py:meth:`Connection.load_collection() <openeo.rest.connection.Connection.load_collection>`, + :py:meth:`Connection.datacube_from_process() <openeo.rest.connection.Connection.datacube_from_process>`, + :py:meth:`Connection.datacube_from_flat_graph() <openeo.rest.connection.Connection.datacube_from_flat_graph>`, + :py:meth:`Connection.datacube_from_json() <openeo.rest.connection.Connection.datacube_from_json>`, + :py:meth:`Connection.load_ml_model() <openeo.rest.connection.Connection.load_ml_model>`, + :py:func:`openeo.processes.process()`, + + """ + + __slots__ = ["_process_id", "_arguments", "_namespace"] + + def __init__(self, process_id: str, arguments: dict = None, namespace: Union[str, None] = None, **kwargs): + self._process_id = process_id + # Merge arguments dict and kwargs + arguments = dict(**(arguments or {}), **kwargs) + # Make sure direct PGNode arguments are properly wrapped in a "from_node" dict + for arg, value in arguments.items(): + if isinstance(value, _FromNodeMixin): + arguments[arg] = {"from_node": value.from_node()} + elif isinstance(value, list): + for index, arrayelement in enumerate(value): + if isinstance(arrayelement, _FromNodeMixin): + value[index] = {"from_node": arrayelement.from_node()} + # TODO: use a frozendict of some sort to ensure immutability? + self._arguments = arguments + self._namespace = namespace + + def from_node(self): + return self + + def __repr__(self): + return "<{c} {p!r} at 0x{m:x}>".format(c=self.__class__.__name__, p=self.process_id, m=id(self)) + + @property + def process_id(self) -> str: + return self._process_id + + @property + def arguments(self) -> dict: + return self._arguments + + @property + def namespace(self) -> Union[str, None]: + return self._namespace + +
+[docs] + def update_arguments(self, **kwargs): + """ + Add/Update arguments of the process node. + + .. versionadded:: 0.10.1 + """ + self._arguments = {**self._arguments, **kwargs}
+ + + def _as_tuple(self): + return (self._process_id, self._arguments, self._namespace) + + def __eq__(self, other): + return isinstance(other, type(self)) and self._as_tuple() == other._as_tuple() + +
+[docs] + def to_dict(self) -> dict: + """ + Convert process graph to a nested dictionary structure. + Uses deep copy style: nodes that are reused in graph will be deduplicated + """ + + def _deep_copy(x): + """PGNode aware deep copy helper""" + if isinstance(x, PGNode): + return dict_no_none(process_id=x.process_id, arguments=_deep_copy(x.arguments), namespace=x.namespace) + if isinstance(x, Parameter): + return {"from_parameter": x.name} + elif isinstance(x, dict): + return {str(k): _deep_copy(v) for k, v in x.items()} + elif isinstance(x, (list, tuple)): + return type(x)(_deep_copy(v) for v in x) + elif isinstance(x, (str, int, float)) or x is None: + return x + else: + raise ValueError(repr(x)) + + return _deep_copy(self)
+ + +
+[docs] + def flat_graph(self) -> Dict[str, dict]: + """Get the process graph in internal flat dict representation.""" + return GraphFlattener().flatten(node=self)
+ + +
+[docs] + @staticmethod + def to_process_graph_argument(value: Union["PGNode", str, dict]) -> dict: + """ + Normalize given argument properly to a "process_graph" argument + to be used as reducer/subprocess for processes like + ``reduce_dimension``, ``aggregate_spatial``, ``apply``, ``merge_cubes``, ``resample_cube_temporal`` + """ + if isinstance(value, str): + # assume string with predefined reduce/apply process ("mean", "sum", ...) + # TODO: is this case still used? It's invalid anyway for 1.0 openEO spec I think? + return value + elif isinstance(value, PGNode): + return {"process_graph": value} + elif isinstance(value, dict) and isinstance(value.get("process_graph"), PGNode): + return value + else: + raise ValueError(value)
+ + +
+[docs] + @staticmethod + def from_flat_graph(flat_graph: dict, parameters: Optional[dict] = None) -> PGNode: + """Unflatten a given flat dict representation of a process graph and return result node.""" + return PGNodeGraphUnflattener.unflatten(flat_graph=flat_graph, parameters=parameters)
+ + + +
+[docs] + def walk_nodes(self) -> Iterator[PGNode]: + """Walk this node and all it's parents""" + # TODO: option to do deep walk (walk through child graphs too)? + yield self + + def walk(x) -> Iterator[PGNode]: + if isinstance(x, PGNode): + yield from x.walk_nodes() + elif isinstance(x, dict): + for v in x.values(): + yield from walk(v) + elif isinstance(x, (list, tuple)): + for v in x: + yield from walk(v) + + yield from walk(self.arguments)
+
+ + + +def as_flat_graph(x: Union[dict, FlatGraphableMixin, Path, List[FlatGraphableMixin], Any]) -> Dict[str, dict]: + """ + Convert given object to a internal flat dict graph representation. + """ + # TODO: document or verify which process graph flavor this is: + # including `{"process": {"process_graph": {nodes}}` ("process graph with metadata") + # including `{"process_graph": {nodes}}` ("process graph") + # or just the raw process graph nodes? + if isinstance(x, dict): + # Assume given dict is already a flat graph representation + return x + elif isinstance(x, FlatGraphableMixin): + return x.flat_graph() + elif isinstance(x, (str, Path)): + # Assume a JSON resource (raw JSON, path to local file, JSON url, ...) + return load_json_resource(x) + elif isinstance(x, (list, tuple)) and all(isinstance(i, FlatGraphableMixin) for i in x): + return MultiLeafGraph(x).flat_graph() + raise ValueError(x) + + +class ReduceNode(PGNode): + """ + A process graph node for "reduce" processes (has a reducer sub-process-graph) + """ + + def __init__( + self, + data: _FromNodeMixin, + reducer: Union[PGNode, str, dict], + dimension: str, + context=None, + process_id="reduce_dimension", + band_math_mode: bool = False, + ): + assert process_id in ("reduce_dimension", "reduce_dimension_binary") + arguments = { + "data": data, + "reducer": self.to_process_graph_argument(reducer), + "dimension": dimension, + } + if context is not None: + arguments["context"] = context + super().__init__(process_id=process_id, arguments=arguments) + # TODO #123 is it (still) necessary to make "band" math a special case? + self.band_math_mode = band_math_mode + + @property + def dimension(self): + return self.arguments["dimension"] + + def reducer_process_graph(self) -> PGNode: + return self.arguments["reducer"]["process_graph"] + + def clone_with_new_reducer(self, reducer: PGNode) -> ReduceNode: + """Copy/clone this reduce node: keep input reference, but use new reducer""" + return ReduceNode( + data=self.arguments["data"]["from_node"], + reducer=reducer, + dimension=self.arguments["dimension"], + band_math_mode=self.band_math_mode, + context=self.arguments.get("context"), + ) + + +class FlatGraphNodeIdGenerator: + """ + Helper class to generate unique node ids (e.g. autoincrement style) + for processes in a flat process graph. + """ + + def __init__(self): + self._counters = collections.defaultdict(int) + + def generate(self, process_id: str): + """Generate new key for given process id.""" + self._counters[process_id] += 1 + return "{p}{c}".format(p=process_id.replace("_", ""), c=self._counters[process_id]) + + +class GraphFlattener(ProcessGraphVisitor): + + def __init__(self, node_id_generator: FlatGraphNodeIdGenerator = None, multi_input_mode: bool = False): + super().__init__() + self._node_id_generator = node_id_generator or FlatGraphNodeIdGenerator() + self._last_node_id = None + self._flattened: Dict[str, dict] = {} + self._argument_stack = [] + self._node_cache = {} + self._multi_input_mode = multi_input_mode + + def flatten(self, node: PGNode) -> Dict[str, dict]: + """Consume given nested process graph and return flat dict representation""" + if self._flattened and not self._multi_input_mode: + raise RuntimeError("Flattening multiple graphs, but not in multi-input mode") + self.accept_node(node) + assert len(self._argument_stack) == 0 + return self.flattened(set_result_flag=not self._multi_input_mode) + + def flattened(self, set_result_flag: bool = True) -> Dict[str, dict]: + flat_graph = copy.deepcopy(self._flattened) + if set_result_flag: + # TODO #583 an "end" node is not necessarily a "result" node + flat_graph[self._last_node_id]["result"] = True + return flat_graph + + def accept_node(self, node: PGNode): + # Process reused nodes only first time and remember node id. + node_id = id(node) + if node_id not in self._node_cache: + super()._accept_process(process_id=node.process_id, arguments=node.arguments, namespace=node.namespace) + self._node_cache[node_id] = self._last_node_id + else: + self._last_node_id = self._node_cache[node_id] + + def enterProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + self._argument_stack.append({}) + + def leaveProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + node_id = self._node_id_generator.generate(process_id) + self._flattened[node_id] = dict_no_none( + process_id=process_id, + arguments=self._argument_stack.pop(), + namespace=namespace, + ) + self._last_node_id = node_id + + def _store_argument(self, argument_id: str, value): + if isinstance(value, Parameter): + value = {"from_parameter": value.name} + self._argument_stack[-1][argument_id] = value + + def _store_array_element(self, value): + if isinstance(value, Parameter): + value = {"from_parameter": value.name} + self._argument_stack[-1].append(value) + + def enterArray(self, argument_id: str): + array = [] + self._store_argument(argument_id, array) + self._argument_stack.append(array) + + def leaveArray(self, argument_id: str): + self._argument_stack.pop() + + def arrayElementDone(self, value): + self._store_array_element(self._flatten_argument(value)) + + def constantArrayElement(self, value): + self._store_array_element(self._flatten_argument(value)) + + def _flatten_argument(self, value): + if isinstance(value, dict): + if "from_node" in value: + value = {"from_node": self._last_node_id} + elif "process_graph" in value: + pg = value["process_graph"] + if isinstance(pg, PGNode): + value = {"process_graph": GraphFlattener(node_id_generator=self._node_id_generator).flatten(pg)} + elif isinstance(pg, dict): + # Assume it is already a valid flat graph representation of a subprocess + value = {"process_graph": pg} + else: + raise ValueError(pg) + else: + value = {k: self._flatten_argument(v) for k, v in value.items()} + elif isinstance(value, Parameter): + value = {"from_parameter": value.name} + return value + + def leaveArgument(self, argument_id: str, value): + self._store_argument(argument_id, self._flatten_argument(value)) + + def constantArgument(self, argument_id: str, value): + self._store_argument(argument_id, value) + + +class PGNodeGraphUnflattener(ProcessGraphUnflattener): + """ + Unflatten a flat process graph to a graph of :py:class:`PGNode` objects + + Parameter substitution can also be performed, but is optional: + if the ``parameters=None`` is given, no parameter substitution is done, + if it is a dictionary (even an empty one) is given, every parameter encountered in the process + graph must have an entry for substitution. + """ + + def __init__(self, flat_graph: dict, parameters: Optional[dict] = None): + super().__init__(flat_graph=flat_graph) + self._parameters = parameters + + def _process_node(self, node: dict) -> PGNode: + return PGNode( + process_id=node["process_id"], + arguments=self._process_value(value=node["arguments"]), + namespace=node.get("namespace"), + ) + + def _process_from_node(self, key: str, node: dict) -> PGNode: + return self.get_node(key=key) + + def _process_from_parameter(self, name: str) -> Any: + if self._parameters is None: + return super()._process_from_parameter(name=name) + if name not in self._parameters: + raise ProcessGraphVisitException("No substitution value for parameter {p!r}.".format(p=name)) + return self._parameters[name] + + +class MultiLeafGraph(FlatGraphableMixin): + """ + Container for process graphs with multiple leaf/result nodes. + """ + + __slots__ = ["_leaves"] + + def __init__(self, leaves: Iterable[FlatGraphableMixin]): + self._leaves = list(leaves) + + def flat_graph(self) -> Dict[str, dict]: + flattener = GraphFlattener(multi_input_mode=True) + for leaf in self._leaves: + if isinstance(leaf, PGNode): + flattener.flatten(leaf) + elif isinstance(leaf, _FromNodeMixin): + flattener.flatten(leaf.from_node()) + else: + raise ValueError(f"Unsupported type {type(leaf)}") + + return flattener.flattened(set_result_flag=True) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/metadata.html b/_modules/openeo/metadata.html new file mode 100644 index 000000000..739968c6c --- /dev/null +++ b/_modules/openeo/metadata.html @@ -0,0 +1,869 @@ + + + + + + + openeo.metadata — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.metadata

+from __future__ import annotations
+
+import functools
+import logging
+import warnings
+from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Union
+
+import pystac
+import pystac.extensions.datacube
+import pystac.extensions.eo
+import pystac.extensions.item_assets
+
+from openeo.internal.jupyter import render_component
+from openeo.util import deep_get
+
+_log = logging.getLogger(__name__)
+
+
+class MetadataException(Exception):
+    pass
+
+
+class DimensionAlreadyExistsException(MetadataException):
+    pass
+
+
+# TODO: make these dimension classes immutable data classes
+class Dimension:
+    """Base class for dimensions."""
+
+    def __init__(self, type: str, name: str):
+        self.type = type
+        self.name = name
+
+    def __repr__(self):
+        return "{c}({f})".format(
+            c=self.__class__.__name__,
+            f=", ".join("{k!s}={v!r}".format(k=k, v=v) for (k, v) in self.__dict__.items())
+        )
+
+    def __eq__(self, other):
+        return self.__class__ == other.__class__ and self.__dict__ == other.__dict__
+
+    def rename(self, name) -> Dimension:
+        """Create new dimension with new name."""
+        return Dimension(type=self.type, name=name)
+
+    def rename_labels(self, target, source) -> Dimension:
+        """
+        Rename labels, if the type of dimension allows it.
+
+        :param target: List of target labels
+        :param source: Source labels, or empty list
+        :return: A new dimension with modified labels, or the same if no change is applied.
+        """
+        # In general, we don't have/manage label info here, so do nothing.
+        return Dimension(type=self.type, name=self.name)
+
+
+
+[docs] +class SpatialDimension(Dimension): + DEFAULT_CRS = 4326 + + def __init__( + self, + name: str, + extent: Union[Tuple[float, float], List[float]], + crs: Union[str, int, dict] = DEFAULT_CRS, + step=None, + ): + """ + + @param name: + @param extent: + @param crs: + @param step: The space between the values. Use null for irregularly spaced steps. + """ + super().__init__(type="spatial", name=name) + self.extent = extent + self.crs = crs + self.step = step + +
+[docs] + def rename(self, name) -> Dimension: + return SpatialDimension(name=name, extent=self.extent, crs=self.crs, step=self.step)
+
+ + + +
+[docs] +class TemporalDimension(Dimension): + def __init__(self, name: str, extent: Union[Tuple[str, str], List[str]]): + super().__init__(type="temporal", name=name) + self.extent = extent + +
+[docs] + def rename(self, name) -> Dimension: + return TemporalDimension(name=name, extent=self.extent)
+ + +
+[docs] + def rename_labels(self, target, source) -> Dimension: + # TODO should we check if the extent has changed with the new labels? + return TemporalDimension(name=self.name, extent=self.extent)
+
+ + + +class Band(NamedTuple): + """ + Simple container class for band metadata. + Based on https://github.com/stac-extensions/eo#band-object + """ + + name: str + common_name: Optional[str] = None + # wavelength in micrometer + wavelength_um: Optional[float] = None + aliases: Optional[List[str]] = None + # "openeo:gsd" field (https://github.com/Open-EO/openeo-stac-extensions#GSD-Object) + gsd: Optional[dict] = None + + +
+[docs] +class BandDimension(Dimension): + # TODO #575 support unordered bands and avoid assumption that band order is known. + def __init__(self, name: str, bands: List[Band]): + super().__init__(type="bands", name=name) + self.bands = bands + + @property + def band_names(self) -> List[str]: + return [b.name for b in self.bands] + + @property + def band_aliases(self) -> List[List[str]]: + return [b.aliases for b in self.bands] + + @property + def common_names(self) -> List[str]: + return [b.common_name for b in self.bands] + +
+[docs] + def band_index(self, band: Union[int, str]) -> int: + """ + Resolve a given band (common) name/index to band index + + :param band: band name, common name or index + :return int: band index + """ + band_names = self.band_names + if isinstance(band, int) and 0 <= band < len(band_names): + return band + elif isinstance(band, str): + common_names = self.common_names + # First try common names if possible + if band in common_names: + return common_names.index(band) + if band in band_names: + return band_names.index(band) + # Check band aliases to still support old band names + aliases = [True if aliases and band in aliases else False for aliases in self.band_aliases] + if any(aliases): + return aliases.index(True) + raise ValueError("Invalid band name/index {b!r}. Valid names: {n!r}".format(b=band, n=band_names))
+ + +
+[docs] + def band_name(self, band: Union[str, int], allow_common=True) -> str: + """Resolve (common) name or index to a valid (common) name""" + if isinstance(band, str): + if band in self.band_names: + return band + elif band in self.common_names: + if allow_common: + return band + else: + return self.band_names[self.common_names.index(band)] + elif any([True if aliases and band in aliases else False for aliases in self.band_aliases]): + return self.band_names[self.band_index(band)] + elif isinstance(band, int) and 0 <= band < len(self.bands): + return self.band_names[band] + raise ValueError("Invalid band name/index {b!r}. Valid names: {n!r}".format(b=band, n=self.band_names))
+ + +
+[docs] + def filter_bands(self, bands: List[Union[int, str]]) -> BandDimension: + """ + Construct new BandDimension with subset of bands, + based on given band indices or (common) names + """ + return BandDimension( + name=self.name, + bands=[self.bands[self.band_index(b)] for b in bands] + )
+ + +
+[docs] + def append_band(self, band: Band) -> BandDimension: + """Create new BandDimension with appended band.""" + if band.name in self.band_names: + raise ValueError("Duplicate band {b!r}".format(b=band)) + + return BandDimension( + name=self.name, + bands=self.bands + [band] + )
+ + +
+[docs] + def rename_labels(self, target, source) -> Dimension: + if source: + if len(target) != len(source): + raise ValueError( + "In rename_labels, `target` and `source` should have same number of labels, " + "but got: `target` {t} and `source` {s}".format(t=target, s=source) + ) + new_bands = self.bands.copy() + for old_name, new_name in zip(source, target): + band_index = self.band_index(old_name) + the_band = new_bands[band_index] + new_bands[band_index] = Band( + name=new_name, + common_name=the_band.common_name, + wavelength_um=the_band.wavelength_um, + aliases=the_band.aliases, + gsd=the_band.gsd, + ) + else: + new_bands = [Band(name=n) for n in target] + return BandDimension(name=self.name, bands=new_bands)
+ + +
+[docs] + def rename(self, name) -> Dimension: + return BandDimension(name=name, bands=self.bands)
+
+ + +class CubeMetadata: + """ + Interface for metadata of a data cube. + + Allows interaction with the cube dimensions and their labels (if available). + """ + + def __init__(self, dimensions: Optional[List[Dimension]] = None): + # Original collection metadata (actual cube metadata might be altered through processes) + self._dimensions = dimensions + self._band_dimension = None + self._temporal_dimension = None + + if dimensions is not None: + for dim in self._dimensions: + # TODO: here we blindly pick last bands or temporal dimension if multiple. Let user choose? + # TODO: add spacial dimension handling? + if dim.type == "bands": + if isinstance(dim, BandDimension): + self._band_dimension = dim + else: + raise MetadataException("Invalid band dimension {d!r}".format(d=dim)) + if dim.type == "temporal": + if isinstance(dim, TemporalDimension): + self._temporal_dimension = dim + else: + raise MetadataException("Invalid temporal dimension {d!r}".format(d=dim)) + + def __eq__(self, o: Any) -> bool: + return isinstance(o, type(self)) and self._dimensions == o._dimensions + + def _clone_and_update(self, dimensions: Optional[List[Dimension]] = None, **kwargs) -> CubeMetadata: + """Create a new instance (of same class) with copied/updated fields.""" + cls = type(self) + if dimensions is None: + dimensions = self._dimensions + return cls(dimensions=dimensions, **kwargs) + + def dimension_names(self) -> List[str]: + return list(d.name for d in self._dimensions) + + def assert_valid_dimension(self, dimension: str) -> str: + """Make sure given dimension name is valid.""" + names = self.dimension_names() + if dimension not in names: + raise ValueError(f"Invalid dimension {dimension!r}. Should be one of {names}") + return dimension + + def has_band_dimension(self) -> bool: + return isinstance(self._band_dimension, BandDimension) + + @property + def band_dimension(self) -> BandDimension: + """Dimension corresponding to spectral/logic/thematic "bands".""" + if not self.has_band_dimension(): + raise MetadataException("No band dimension") + return self._band_dimension + + def has_temporal_dimension(self) -> bool: + return isinstance(self._temporal_dimension, TemporalDimension) + + @property + def temporal_dimension(self) -> TemporalDimension: + if not self.has_temporal_dimension(): + raise MetadataException("No temporal dimension") + return self._temporal_dimension + + @property + def spatial_dimensions(self) -> List[SpatialDimension]: + return [d for d in self._dimensions if isinstance(d, SpatialDimension)] + + @property + def bands(self) -> List[Band]: + """Get band metadata as list of Band metadata tuples""" + return self.band_dimension.bands + + @property + def band_names(self) -> List[str]: + """Get band names of band dimension""" + return self.band_dimension.band_names + + @property + def band_common_names(self) -> List[str]: + return self.band_dimension.common_names + + def get_band_index(self, band: Union[int, str]) -> int: + # TODO: eliminate this shortcut for smaller API surface + return self.band_dimension.band_index(band) + + def filter_bands(self, band_names: List[Union[int, str]]) -> CubeMetadata: + """ + Create new `CubeMetadata` with filtered band dimension + :param band_names: list of band names/indices to keep + :return: + """ + assert self.band_dimension + return self._clone_and_update( + dimensions=[d.filter_bands(band_names) if isinstance(d, BandDimension) else d for d in self._dimensions] + ) + + def append_band(self, band: Band) -> CubeMetadata: + """ + Create new `CubeMetadata` with given band added to band dimension. + """ + assert self.band_dimension + return self._clone_and_update( + dimensions=[d.append_band(band) if isinstance(d, BandDimension) else d for d in self._dimensions] + ) + + def rename_labels(self, dimension: str, target: list, source: list = None) -> CubeMetadata: + """ + Renames the labels of the specified dimension from source to target. + + :param dimension: Dimension name + :param target: The new names for the labels. + :param source: The names of the labels as they are currently in the data cube. + + :return: Updated metadata + """ + self.assert_valid_dimension(dimension) + loc = self.dimension_names().index(dimension) + new_dimensions = self._dimensions.copy() + new_dimensions[loc] = new_dimensions[loc].rename_labels(target, source) + + return self._clone_and_update(dimensions=new_dimensions) + + def rename_dimension(self, source: str, target: str) -> CubeMetadata: + """ + Rename source dimension into target, preserving other properties + """ + self.assert_valid_dimension(source) + loc = self.dimension_names().index(source) + new_dimensions = self._dimensions.copy() + new_dimensions[loc] = new_dimensions[loc].rename(target) + + return self._clone_and_update(dimensions=new_dimensions) + + def reduce_dimension(self, dimension_name: str) -> CubeMetadata: + """Create new CubeMetadata object by collapsing/reducing a dimension.""" + # TODO: option to keep reduced dimension (with a single value)? + # TODO: rename argument to `name` for more internal consistency + # TODO: merge with drop_dimension (which does the same). + self.assert_valid_dimension(dimension_name) + loc = self.dimension_names().index(dimension_name) + dimensions = self._dimensions[:loc] + self._dimensions[loc + 1 :] + return self._clone_and_update(dimensions=dimensions) + + def reduce_spatial(self) -> CubeMetadata: + """Create new CubeMetadata object by reducing the spatial dimensions.""" + dimensions = [d for d in self._dimensions if not isinstance(d, SpatialDimension)] + return self._clone_and_update(dimensions=dimensions) + + def add_dimension(self, name: str, label: Union[str, float], type: str = None) -> CubeMetadata: + """Create new CubeMetadata object with added dimension""" + if any(d.name == name for d in self._dimensions): + raise DimensionAlreadyExistsException(f"Dimension with name {name!r} already exists") + if type == "bands": + dim = BandDimension(name=name, bands=[Band(name=label)]) + elif type == "spatial": + dim = SpatialDimension(name=name, extent=[label, label]) + elif type == "temporal": + dim = TemporalDimension(name=name, extent=[label, label]) + else: + dim = Dimension(type=type or "other", name=name) + return self._clone_and_update(dimensions=self._dimensions + [dim]) + + def drop_dimension(self, name: str = None) -> CubeMetadata: + """Create new CubeMetadata object without dropped dimension with given name""" + dimension_names = self.dimension_names() + if name not in dimension_names: + raise ValueError("No dimension named {n!r} (valid names: {ns!r})".format(n=name, ns=dimension_names)) + return self._clone_and_update(dimensions=[d for d in self._dimensions if not d.name == name]) + + def __str__(self) -> str: + bands = self.band_names if self.has_band_dimension() else "no bands dimension" + return f"CubeMetadata({bands} - {self.dimension_names()})" + + +
+[docs] +class CollectionMetadata(CubeMetadata): + """ + Wrapper for EO Data Collection metadata. + + Simplifies getting values from deeply nested mappings, + allows additional parsing and normalizing compatibility issues. + + Metadata is expected to follow format defined by + https://openeo.org/documentation/1.0/developers/api/reference.html#operation/describe-collection + (with partial support for older versions) + + """ + + def __init__(self, metadata: dict, dimensions: List[Dimension] = None): + self._orig_metadata = metadata + if dimensions is None: + dimensions = self._parse_dimensions(self._orig_metadata) + + super().__init__(dimensions=dimensions) + + @classmethod + def _parse_dimensions(cls, spec: dict, complain: Callable[[str], None] = warnings.warn) -> List[Dimension]: + """ + Extract data cube dimension metadata from STAC-like description of a collection. + + Dimension metadata comes from different places in spec: + - 'cube:dimensions' has dimension names (e.g. 'x', 'y', 't'), dimension extent info + and band names for band dimensions + - 'eo:bands' has more detailed band information like "common" name and wavelength info + + This helper tries to normalize/combine these sources. + + :param spec: STAC like collection metadata dict + :param complain: handler for warnings + :return list: list of `Dimension` objects + + """ + + # Dimension info is in `cube:dimensions` (or 0.4-style `properties/cube:dimensions`) + cube_dimensions = ( + deep_get(spec, "cube:dimensions", default=None) + or deep_get(spec, "properties", "cube:dimensions", default=None) + or {} + ) + if not cube_dimensions: + complain("No cube:dimensions metadata") + dimensions = [] + for name, info in cube_dimensions.items(): + dim_type = info.get("type") + if dim_type == "spatial": + dimensions.append( + SpatialDimension( + name=name, + extent=info.get("extent"), + crs=info.get("reference_system", SpatialDimension.DEFAULT_CRS), + step=info.get("step", None), + ) + ) + elif dim_type == "temporal": + dimensions.append(TemporalDimension(name=name, extent=info.get("extent"))) + elif dim_type == "bands": + bands = [Band(name=b) for b in info.get("values", [])] + if not bands: + complain("No band names in dimension {d!r}".format(d=name)) + dimensions.append(BandDimension(name=name, bands=bands)) + else: + complain("Unknown dimension type {t!r}".format(t=dim_type)) + dimensions.append(Dimension(name=name, type=dim_type)) + + # Detailed band information: `summaries/[eo|raster]:bands` (and 0.4 style `properties/eo:bands`) + eo_bands = ( + deep_get(spec, "summaries", "eo:bands", default=None) + or deep_get(spec, "summaries", "raster:bands", default=None) + or deep_get(spec, "properties", "eo:bands", default=None) + ) + if eo_bands: + # center_wavelength is in micrometer according to spec + bands_detailed = [ + Band( + name=b["name"], + common_name=b.get("common_name"), + wavelength_um=b.get("center_wavelength"), + aliases=b.get("aliases"), + gsd=b.get("openeo:gsd"), + ) + for b in eo_bands + ] + # Update band dimension with more detailed info + band_dimensions = [d for d in dimensions if d.type == "bands"] + if len(band_dimensions) == 1: + dim = band_dimensions[0] + # Update band values from 'cube:dimensions' with more detailed 'eo:bands' info + eo_band_names = [b.name for b in bands_detailed] + cube_dimension_band_names = [b.name for b in dim.bands] + if eo_band_names == cube_dimension_band_names: + dim.bands = bands_detailed + else: + complain("Band name mismatch: {a} != {b}".format(a=cube_dimension_band_names, b=eo_band_names)) + elif len(band_dimensions) == 0: + if len(dimensions) == 0: + complain("Assuming name 'bands' for anonymous band dimension.") + dimensions.append(BandDimension(name="bands", bands=bands_detailed)) + else: + complain("No 'bands' dimension in 'cube:dimensions' while having 'eo:bands' or 'raster:bands'") + else: + complain("Multiple dimensions of type 'bands'") + + return dimensions + + def _clone_and_update( + self, metadata: dict = None, dimensions: List[Dimension] = None, **kwargs + ) -> CollectionMetadata: + """ + Create a new instance (of same class) with copied/updated fields. + + This overrides the method in `CubeMetadata` to keep the original metadata. + """ + cls = type(self) + if metadata is None: + metadata = self._orig_metadata + if dimensions is None: + dimensions = self._dimensions + return cls(metadata=metadata, dimensions=dimensions, **kwargs) + + def get(self, *args, default=None): + return deep_get(self._orig_metadata, *args, default=default) + + @property + def extent(self) -> dict: + # TODO: is this currently used and relevant? + # TODO: check against extent metadata in dimensions + return self._orig_metadata.get("extent") + + def _repr_html_(self): + return render_component("collection", data=self._orig_metadata) + + def __str__(self) -> str: + bands = self.band_names if self.has_band_dimension() else "no bands dimension" + return f"CollectionMetadata({self.extent} - {bands} - {self.dimension_names()})"
+ + + +def metadata_from_stac(url: str) -> CubeMetadata: + """ + Reads the band metadata a static STAC catalog or a STAC API Collection and returns it as a :py:class:`CubeMetadata` + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a specific STAC API Collection + :return: A :py:class:`CubeMetadata` containing the DataCube band metadata from the url. + """ + + # TODO move these nested functions and other logic to _StacMetadataParser + + def get_band_metadata(eo_bands_location: dict) -> List[Band]: + # TODO: return None iso empty list when no metadata? + return [ + Band(name=band["name"], common_name=band.get("common_name"), wavelength_um=band.get("center_wavelength")) + for band in eo_bands_location.get("eo:bands", []) + ] + + def get_band_names(bands: List[Band]) -> List[str]: + return [band.name for band in bands] + + def is_band_asset(asset: pystac.Asset) -> bool: + return "eo:bands" in asset.extra_fields + + stac_object = pystac.read_file(href=url) + + if isinstance(stac_object, pystac.Item): + item = stac_object + if "eo:bands" in item.properties: + eo_bands_location = item.properties + elif item.get_collection() is not None: + # TODO: Also do asset based band detection (like below)? + eo_bands_location = item.get_collection().summaries.lists + else: + eo_bands_location = {} + bands = get_band_metadata(eo_bands_location) + + elif isinstance(stac_object, pystac.Collection): + collection = stac_object + bands = get_band_metadata(collection.summaries.lists) + + # Summaries is not a required field in a STAC collection, so also check the assets + for itm in collection.get_items(): + band_assets = {asset_id: asset for asset_id, asset in itm.get_assets().items() if is_band_asset(asset)} + + for asset in band_assets.values(): + asset_bands = get_band_metadata(asset.extra_fields) + for asset_band in asset_bands: + if asset_band.name not in get_band_names(bands): + bands.append(asset_band) + if _PYSTAC_1_9_EXTENSION_INTERFACE and collection.ext.has("item_assets"): + # TODO #575 support unordered band names and avoid conversion to a list. + bands = list(_StacMetadataParser().get_bands_from_item_assets(collection.ext.item_assets)) + + elif isinstance(stac_object, pystac.Catalog): + catalog = stac_object + bands = get_band_metadata(catalog.extra_fields.get("summaries", {})) + else: + raise ValueError(stac_object) + + # TODO: conditionally include band dimension when there was actual indication of band metadata? + band_dimension = BandDimension(name="bands", bands=bands) + dimensions = [band_dimension] + + # TODO: is it possible to derive the actual name of temporal dimension that the backend will use? + temporal_dimension = _StacMetadataParser().get_temporal_dimension(stac_object) + if temporal_dimension: + dimensions.append(temporal_dimension) + + metadata = CubeMetadata(dimensions=dimensions) + return metadata + +# Sniff for PySTAC extension API since version 1.9.0 (which is not available below Python 3.9) +# TODO: remove this once support for Python 3.7 and 3.8 is dropped +_PYSTAC_1_9_EXTENSION_INTERFACE = hasattr(pystac.Item, "ext") + + +class _StacMetadataParser: + """ + Helper to extract openEO metadata from STAC metadata resource + """ + + def __init__(self): + # TODO: toggles for how to handle strictness, warnings, logging, etc + pass + + def _get_band_from_eo_bands_item(self, eo_band: Union[dict, pystac.extensions.eo.Band]) -> Band: + if isinstance(eo_band, pystac.extensions.eo.Band): + return Band( + name=eo_band.name, + common_name=eo_band.common_name, + wavelength_um=eo_band.center_wavelength, + ) + elif isinstance(eo_band, dict) and "name" in eo_band: + return Band( + name=eo_band["name"], + common_name=eo_band.get("common_name"), + wavelength_um=eo_band.get("center_wavelength"), + ) + else: + raise ValueError(eo_band) + + def get_bands_from_eo_bands(self, eo_bands: List[Union[dict, pystac.extensions.eo.Band]]) -> List[Band]: + """ + Extract bands from STAC `eo:bands` array + + :param eo_bands: List of band objects, as dict or `pystac.extensions.eo.Band` instances + """ + # TODO: option to skip bands that failed to parse in some way? + return [self._get_band_from_eo_bands_item(band) for band in eo_bands] + + def _get_bands_from_item_asset( + self, item_asset: pystac.extensions.item_assets.AssetDefinition, *, _warn: Callable[[str], None] = _log.warning + ) -> Union[List[Band], None]: + """Get bands from a STAC 'item_assets' asset definition.""" + if _PYSTAC_1_9_EXTENSION_INTERFACE and item_asset.ext.has("eo"): + if item_asset.ext.eo.bands is not None: + return self.get_bands_from_eo_bands(item_asset.ext.eo.bands) + elif "eo:bands" in item_asset.properties: + # TODO: skip this in strict mode? + if _PYSTAC_1_9_EXTENSION_INTERFACE: + _warn("Extracting band info from 'eo:bands' metadata, but 'eo' STAC extension was not declared.") + return self.get_bands_from_eo_bands(item_asset.properties["eo:bands"]) + + def get_bands_from_item_assets( + self, item_assets: Dict[str, pystac.extensions.item_assets.AssetDefinition] + ) -> Set[Band]: + """ + Get bands extracted from "item_assets" objects (defined by "item-assets" extension, + in combination with "eo" extension) at STAC Collection top-level, + + Note that "item_assets" in STAC is a mapping, so the band order is undefined, + which is why we return a set of bands here. + + :param item_assets: a STAC `item_assets` mapping + """ + bands = set() + # Trick to just warn once per collection + _warn = functools.lru_cache()(_log.warning) + for item_asset in item_assets.values(): + asset_bands = self._get_bands_from_item_asset(item_asset, _warn=_warn) + if asset_bands: + bands.update(asset_bands) + return bands + + def get_temporal_dimension(self, stac_obj: pystac.STACObject) -> Union[TemporalDimension, None]: + """ + Extract the temporal dimension from a STAC Collection/Item (if any) + """ + # TODO: also extract temporal dimension from assets? + if _PYSTAC_1_9_EXTENSION_INTERFACE: + if stac_obj.ext.has("cube") and hasattr(stac_obj.ext, "cube"): + temporal_dims = [ + (n, d.extent or [None, None]) + for (n, d) in stac_obj.ext.cube.dimensions.items() + if d.dim_type == pystac.extensions.datacube.DimensionType.TEMPORAL + ] + if len(temporal_dims) == 1: + name, extent = temporal_dims[0] + return TemporalDimension(name=name, extent=extent) + else: + if isinstance(stac_obj, pystac.Item): + cube_dimensions = stac_obj.properties.get("cube:dimensions", {}) + elif isinstance(stac_obj, pystac.Collection): + cube_dimensions = stac_obj.extra_fields.get("cube:dimensions", {}) + else: + cube_dimensions = {} + temporal_dims = [ + (n, d.get("extent", [None, None])) for (n, d) in cube_dimensions.items() if d.get("type") == "temporal" + ] + if len(temporal_dims) == 1: + name, extent = temporal_dims[0] + return TemporalDimension(name=name, extent=extent) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/processes.html b/_modules/openeo/processes.html new file mode 100644 index 000000000..985891e64 --- /dev/null +++ b/_modules/openeo/processes.html @@ -0,0 +1,6173 @@ + + + + + + + openeo.processes — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.processes

+
+# Do not edit this file directly.
+# It is automatically generated.
+# Used command line arguments:
+#    openeo/internal/processes/generator.py specs/openeo-processes specs/openeo-processes/proposals specs/openeo-processes-legacy --output openeo/processes.py
+# Generated on 2024-01-09
+
+from __future__ import annotations
+
+import builtins
+
+from openeo.internal.documentation import openeo_process
+from openeo.internal.processes.builder import UNSET, ProcessBuilderBase
+from openeo.rest._datacube import build_child_callback
+
+
+
+[docs] +class ProcessBuilder(ProcessBuilderBase): + """ + .. include:: api-processbuilder.rst + """ + + _ITERATION_LIMIT = 100 + + @openeo_process(process_id="add", mode="operator") + def __add__(self, other) -> ProcessBuilder: + return self.add(other) + + @openeo_process(process_id="add", mode="operator") + def __radd__(self, other) -> ProcessBuilder: + return add(other, self) + + @openeo_process(process_id="subtract", mode="operator") + def __sub__(self, other) -> ProcessBuilder: + return self.subtract(other) + + @openeo_process(process_id="subtract", mode="operator") + def __rsub__(self, other) -> ProcessBuilder: + return subtract(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __mul__(self, other) -> ProcessBuilder: + return self.multiply(other) + + @openeo_process(process_id="multiply", mode="operator") + def __rmul__(self, other) -> ProcessBuilder: + return multiply(other, self) + + @openeo_process(process_id="divide", mode="operator") + def __truediv__(self, other) -> ProcessBuilder: + return self.divide(other) + + @openeo_process(process_id="divide", mode="operator") + def __rtruediv__(self, other) -> ProcessBuilder: + return divide(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __neg__(self) -> ProcessBuilder: + return self.multiply(-1) + + @openeo_process(process_id="power", mode="operator") + def __pow__(self, other) -> ProcessBuilder: + return self.power(other) + + @openeo_process(process_id="array_element", mode="operator") + def __getitem__(self, key) -> ProcessBuilder: + if isinstance(key, builtins.int): + if key > self._ITERATION_LIMIT: + raise RuntimeError( + "Exceeded ProcessBuilder iteration limit. " + "Are you mistakenly using a Python builtin like `sum()` or `all()` in a callback " + "instead of the appropriate helpers from the `openeo.processes` module?" + ) + return self.array_element(index=key) + else: + return self.array_element(label=key) + + @openeo_process(process_id="eq", mode="operator") + def __eq__(self, other) -> ProcessBuilder: + return eq(self, other) + + @openeo_process(process_id="neq", mode="operator") + def __ne__(self, other) -> ProcessBuilder: + return neq(self, other) + + @openeo_process(process_id="lt", mode="operator") + def __lt__(self, other) -> ProcessBuilder: + return lt(self, other) + + @openeo_process(process_id="lte", mode="operator") + def __le__(self, other) -> ProcessBuilder: + return lte(self, other) + + @openeo_process(process_id="ge", mode="operator") + def __ge__(self, other) -> ProcessBuilder: + return gte(self, other) + + @openeo_process(process_id="gt", mode="operator") + def __gt__(self, other) -> ProcessBuilder: + return gt(self, other) + + @openeo_process + def absolute(self) -> ProcessBuilder: + """ + Absolute value + + :param self: A number. + + :return: The computed absolute value. + """ + return absolute(x=self) + + @openeo_process + def add(self, y) -> ProcessBuilder: + """ + Addition of two numbers + + :param self: The first summand. + :param y: The second summand. + + :return: The computed sum of the two numbers. + """ + return add(x=self, y=y) + + @openeo_process + def add_dimension(self, name, label, type=UNSET) -> ProcessBuilder: + """ + Add a new dimension + + :param self: A data cube to add the dimension to. + :param name: Name for the dimension. + :param label: A dimension label. + :param type: The type of dimension, defaults to `other`. + + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. + All other dimensions remain unchanged. + """ + return add_dimension(data=self, name=name, label=label, type=type) + + @openeo_process + def aggregate_spatial(self, geometries, reducer, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for geometries + + :param self: A raster data cube with at least two spatial dimensions. The data cube implicitly gets + restricted to the bounds of the geometries as if ``filter_spatial()`` would have been used with the + same values for the corresponding parameters immediately before this process. + :param geometries: Geometries for which the aggregation will be computed. Feature properties are + preserved for vector data cubes and all GeoJSON Features. One value will be computed per label in the + dimension of type `geometries`, GeoJSON `Feature` or `Geometry`. For a `FeatureCollection` multiple + values will be computed, one value per contained `Feature`. No values will be computed for empty + geometries. For example, a single value will be computed for a `MultiPolygon`, but two values will be + computed for a `FeatureCollection` containing two polygons. - For **polygons**, the process considers + all pixels for which the point at the pixel center intersects with the corresponding polygon (as + defined in the Simple Features standard by the OGC). - For **points**, the process considers the + closest pixel center. - For **lines** (line strings), the process considers all the pixels whose + centers are closest to at least one point on the line. Thus, pixels may be part of multiple geometries + and be part of multiple aggregations. No operation is applied to geometries that are outside of the + bounds of the data. + :param reducer: A reducer to be applied on all values of each geometry. A reducer is a single process + such as ``mean()`` or a set of processes, which computes a single value for a list of values, see the + category 'reducer' for such processes. + :param target_dimension: By default (which is `null`), the process only computes the results and + doesn't add a new dimension. If this parameter contains a new dimension name, the computation also + stores information about the total count of pixels (valid + invalid pixels) and the number of valid + pixels (see ``is_valid()``) for each computed value. These values are added as a new dimension. The new + dimension of type `other` has the dimension labels `value`, `total_count` and `valid_count`. Fails + with a `TargetDimensionExists` exception if a dimension with the specified name exists. + :param context: Additional data to be passed to the reducer. + + :return: A vector data cube with the computed results. Empty geometries still exist but without any + aggregated values (i.e. no-data). The spatial dimensions are replaced by a dimension of type + 'geometries' and if `target_dimension` is not `null`, a new dimension is added. + """ + return aggregate_spatial( + data=self, + geometries=geometries, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + target_dimension=target_dimension, + context=context + ) + + @openeo_process + def aggregate_spatial_window(self, reducer, size, boundary=UNSET, align=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for rectangular windows + + :param self: A raster data cube with exactly two horizontal spatial dimensions and an arbitrary number + of additional dimensions. The process is applied to all additional dimensions individually. + :param reducer: A reducer to be applied on the list of values, which contain all pixels covered by the + window. A reducer is a single process such as ``mean()`` or a set of processes, which computes a single + value for a list of values, see the category 'reducer' for such processes. + :param size: Window size in pixels along the horizontal spatial dimensions. The first value + corresponds to the `x` axis, the second value corresponds to the `y` axis. + :param boundary: Behavior to apply if the number of values for the axes `x` and `y` is not a multiple + of the corresponding value in the `size` parameter. Options are: - `pad` (default): pad the data cube + with the no-data value `null` to fit the required window size. - `trim`: trim the data cube to fit the + required window size. Set the parameter `align` to specifies to which corner the data is aligned to. + :param align: If the data requires padding or trimming (see parameter `boundary`), specifies to which + corner of the spatial extent the data is aligned to. For example, if the data is aligned to the upper + left, the process pads/trims at the lower-right. + :param context: Additional data to be passed to the reducer. + + :return: A raster data cube with the newly computed values and the same dimensions. The resolution + will change depending on the chosen values for the `size` and `boundary` parameter. It usually + decreases for the dimensions which have the corresponding parameter `size` set to values greater than + 1. The dimension labels will be set to the coordinate at the center of the window. The other dimension + properties (name, type and reference system) remain unchanged. + """ + return aggregate_spatial_window( + data=self, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + size=size, + boundary=boundary, + align=align, + context=context + ) + + @openeo_process + def aggregate_temporal(self, intervals, reducer, labels=UNSET, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations + + :param self: A data cube. + :param intervals: Left-closed temporal intervals, which are allowed to overlap. Each temporal interval + in the array has exactly two elements: 1. The first element is the start of the temporal interval. The + specified time instant is **included** in the interval. 2. The second element is the end of the + temporal interval. The specified time instant is **excluded** from the interval. The second element + must always be greater/later than the first element, except when using time without date. Otherwise, a + `TemporalExtentEmpty` exception is thrown. + :param reducer: A reducer to be applied for the values contained in each interval. A reducer is a + single process such as ``mean()`` or a set of processes, which computes a single value for a list of + values, see the category 'reducer' for such processes. Intervals may not contain any values, which for + most reducers leads to no-data (`null`) values by default. + :param labels: Distinct labels for the intervals, which can contain dates and/or times. Is only + required to be specified if the values for the start of the temporal intervals are not distinct and + thus the default labels would not be unique. The number of labels and the number of groups need to be + equal. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the given temporal dimension. + """ + return aggregate_temporal( + data=self, + intervals=intervals, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + labels=labels, + dimension=dimension, + context=context + ) + + @openeo_process + def aggregate_temporal_period(self, period, reducer, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations based on calendar hierarchies + + :param self: The source data cube. + :param period: The time intervals to aggregate. The following pre-defined values are available: * + `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten day periods, + counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third + dekad of the month can range from 8 to 11 days. For example, the third dekad of a year spans from + January 21 till January 31 (11 days), the fourth dekad spans from February 1 till February 10 (10 days) + and the sixth dekad spans from February 21 till February 28 or February 29 in a leap year (8 or 9 days + respectively). * `month`: Month of the year * `season`: Three month periods of the calendar seasons + (December - February, March - May, June - August, September - November). * `tropical-season`: Six month + periods of the tropical seasons (November - April, May - October). * `year`: Proleptic years * + `decade`: Ten year periods ([0-to-9 decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from + a year ending in a 0 to the next year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. + :param reducer: A reducer to be applied for the values contained in each period. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, + see the category 'reducer' for such processes. Periods may not contain any values, which for most + reducers leads to no-data (`null`) values by default. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the source data + cube is expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it + has more dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not + exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the given temporal dimension. The specified temporal dimension has the following dimension labels + (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: `YYYY-MM- + DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * `dekad`: + `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - February), + `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - November). * `tropical- + season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * `decade`: + `YYY0` * `decade-ad`: `YYY1` The dimension labels in the new data cube are complete for the whole + extent of the source data cube. For example, if `period` is set to `day` and the source data cube has + two dimension labels at the beginning of the year (`2020-01-01`) and the end of a year (`2020-12-31`), + the process returns a data cube with 365 dimension labels (`2020-001`, `2020-002`, ..., `2020-365`). In + contrast, if `period` is set to `day` and the source data cube has just one dimension label + `2020-01-05`, the process returns a data cube with just a single dimension label (`2020-005`). + """ + return aggregate_temporal_period( + data=self, + period=period, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def all(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Are all of the values true? + + :param self: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return all(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def and_(self, y) -> ProcessBuilder: + """ + Logical AND + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical AND. + """ + return and_(x=self, y=y) + + @openeo_process + def anomaly(self, normals, period) -> ProcessBuilder: + """ + Compute anomalies + + :param self: A data cube with exactly one temporal dimension and the following dimension labels for the + given period (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * + `hour`: `YYYY-MM-DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - + `YYYY-52` * `dekad`: `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` + (December - February), `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - + November). * `tropical-season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * + `year`: `YYYY` * `decade`: `YYY0` * `decade-ad`: `YYY1` * `single-period` / `climatology-period`: Any + ``aggregate_temporal_period()`` can compute such a data cube. + :param normals: A data cube with normals, e.g. daily, monthly or yearly values computed from a process + such as ``climatological_normal()``. Must contain exactly one temporal dimension with the following + dimension labels for the given period: * `hour`: `00` - `23` * `day`: `001` - `365` * `week`: `01` - + `52` * `dekad`: `00` - `36` * `month`: `01` - `12` * `season`: `djf` (December - February), `mam` + (March - May), `jja` (June - August), `son` (September - November) * `tropical-season`: `ndjfma` + (November - April), `mjjaso` (May - October) * `year`: Four-digit year numbers * `decade`: Four-digit + year numbers, the last digit being a `0` * `decade-ad`: Four-digit year numbers, the last digit being a + `1` * `single-period` / `climatology-period`: A single dimension label with any name is expected. + :param period: Specifies the time intervals available in the normals data cube. The following options + are available: * `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * + `dekad`: Ten day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - + end of month). The third dekad of the month can range from 8 to 11 days. For example, the fourth dekad + is Feb, 1 - Feb, 10 each year. * `month`: Month of the year * `season`: Three month periods of the + calendar seasons (December - February, March - May, June - August, September - November). * `tropical- + season`: Six month periods of the tropical seasons (November - April, May - October). * `year`: + Proleptic years * `decade`: Ten year periods ([0-to-9 + decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the next + year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. * `single-period` / + `climatology-period`: A single period of arbitrary length + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged. + """ + return anomaly(data=self, normals=normals, period=period) + + @openeo_process + def any(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Is at least one value true? + + :param self: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return any(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def apply(self, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each value + + :param self: A data cube. + :param process: A process that accepts and returns a single value and is applied on each individual + value in the data cube. The process may consist of multiple sub-processes and could, for example, + consist of processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply(data=self, process=build_child_callback(process, parent_parameters=['x', 'context']), context=context) + + @openeo_process + def apply_dimension(self, process, dimension, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to all values along a dimension + + :param self: A data cube. + :param process: Process to be applied on all values along the given dimension. The specified process + needs to accept an array and must return an array with at least one element. A process may consist of + multiple sub-processes. + :param dimension: The name of the source dimension to apply the process on. Fails with a + `DimensionNotAvailable` exception if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or `null` (the default) to use the source + dimension specified in the parameter `dimension`. By specifying a target dimension, the source + dimension is removed. The target dimension with the specified name and the type `other` (see + ``add_dimension()``) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. All dimensions stay the same, except for the + dimensions specified in corresponding parameters. There are three cases how the dimensions can change: + 1. The source dimension is the target dimension: - The (number of) dimensions remain unchanged as + the source dimension is the target dimension. - The source dimension properties name and type remain + unchanged. - The dimension labels, the reference system and the resolution are preserved only if the + number of values in the source dimension is equal to the number of values computed by the process. + Otherwise, all other dimension properties change as defined in the list below. 2. The source dimension + is not the target dimension. The target dimension exists with a single label only: - The number of + dimensions decreases by one as the source dimension is 'dropped' and the target dimension is filled + with the processed data that originates from the source dimension. - The target dimension properties + name and type remain unchanged. All other dimension properties change as defined in the list below. 3. + The source dimension is not the target dimension and the latter does not exist: - The number of + dimensions remain unchanged, but the source dimension is replaced with the target dimension. - The + target dimension has the specified name and the type other. All other dimension properties are set as + defined in the list below. Unless otherwise stated above, for the given (target) dimension the + following applies: - the number of dimension labels is equal to the number of values computed by the + process, - the dimension labels are incrementing integers starting from zero, - the resolution changes, + and - the reference system is undefined. + """ + return apply_dimension( + data=self, + process=build_child_callback(process, parent_parameters=['data', 'context']), + dimension=dimension, + target_dimension=target_dimension, + context=context + ) + + @openeo_process + def apply_kernel(self, kernel, factor=UNSET, border=UNSET, replace_invalid=UNSET) -> ProcessBuilder: + """ + Apply a spatial convolution with a kernel + + :param self: A raster data cube. + :param kernel: Kernel as a two-dimensional array of weights. The inner level of the nested array aligns + with the `x` axis and the outer level aligns with the `y` axis. Each level of the kernel must have an + uneven number of elements, otherwise the process throws a `KernelDimensionsUneven` exception. + :param factor: A factor that is multiplied to each value after the kernel has been applied. This is + basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often + required for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults + to fill the border with zeroes. The following options are available: * *numeric value* - fill with a + user-defined constant number `n`: `nnnnnn|abcdefgh|nnnnnn` (default, with `n` = 0) * `replicate` - + repeat the value from the pixel at the border: `aaaaaa|abcdefgh|hhhhhh` * `reflect` - mirror/reflect + from the border: `fedcba|abcdefgh|hgfedc` * `reflect_pixel` - mirror/reflect from the center of the + pixel at the border: `gfedcb|abcdefgh|gfedcb` * `wrap` - repeat/wrap the image: + `cdefgh|abcdefgh|abcdef` + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite + numerical values with. By default, those values are replaced with zeroes. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_kernel(data=self, kernel=kernel, factor=factor, border=border, replace_invalid=replace_invalid) + + @openeo_process + def apply_neighborhood(self, process, size, overlap=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to pixels in a n-dimensional neighborhood + + :param self: A raster data cube. + :param process: Process to be applied on all neighborhoods. + :param size: Neighborhood sizes along each dimension. This object maps dimension names to either a + physical measure (e.g. 100 m, 10 days) or pixels (e.g. 32 pixels). For dimensions not specified, the + default is to provide all values. Be aware that including all values from overly large dimensions may + not be processed at once. + :param overlap: Overlap of neighborhoods along each dimension to avoid border effects. By default no + overlap is provided. For instance a temporal dimension can add 1 month before and after a + neighborhood. In the spatial dimensions, this is often a number of pixels. The overlap specified is + added before and after, so an overlap of 8 pixels will add 8 pixels on both sides of the window, so 16 + in total. Be aware that large overlaps increase the need for computational resources and modifying + overlapping data in subsequent operations have no effect. + :param context: Additional data to be passed to the process. + + :return: A raster data cube with the newly computed values and the same dimensions. The dimension + properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_neighborhood( + data=self, + process=build_child_callback(process, parent_parameters=['data', 'context']), + size=size, + overlap=overlap, + context=context + ) + + @openeo_process + def apply_polygon(self, polygons, process, mask_value=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to segments of the data cube + + :param self: A data cube. + :param polygons: A vector data cube containing at least one polygon. The provided vector data can be + one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or + `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or + `MultiPolygon` geometries. * Empty geometries are ignored. + :param process: A process that accepts and returns a single data cube and is applied on each individual + sub data cube. The process may consist of multiple sub-processes. + :param mask_value: All pixels for which the point at the pixel center **does not** intersect with the + polygon are replaced with the given value, which defaults to `null` (no data). It can provide a + distinction between no data values within the polygon and masked pixels outside of it. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_polygon( + data=self, + polygons=polygons, + process=build_child_callback(process, parent_parameters=['data', 'context']), + mask_value=mask_value, + context=context + ) + + @openeo_process + def arccos(self) -> ProcessBuilder: + """ + Inverse cosine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arccos(x=self) + + @openeo_process + def arcosh(self) -> ProcessBuilder: + """ + Inverse hyperbolic cosine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arcosh(x=self) + + @openeo_process + def arcsin(self) -> ProcessBuilder: + """ + Inverse sine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arcsin(x=self) + + @openeo_process + def arctan(self) -> ProcessBuilder: + """ + Inverse tangent + + :param self: A number. + + :return: The computed angle in radians. + """ + return arctan(x=self) + + @openeo_process + def arctan2(self, x) -> ProcessBuilder: + """ + Inverse tangent of two numbers + + :param self: A number to be used as the dividend. + :param x: A number to be used as the divisor. + + :return: The computed angle in radians. + """ + return arctan2(y=self, x=x) + + @openeo_process + def ard_normalized_radar_backscatter(self, elevation_model=UNSET, contributing_area=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant SAR NRB generation + + :param self: The source data cube containing SAR input. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options + will reduce portability. + + :return: Backscatter values expressed as gamma0 in linear scale. In addition to the bands + `contributing_area` and `ellipsoid_incidence_angle` that can optionally be added with corresponding + parameters, the following bands are always added to the data cube: - `mask`: A data mask that + indicates which values are valid (1), invalid (0) or contain no-data (null). - `local_incidence_angle`: + A band with DEM-based local incidence angles in degrees. The data returned is CARD4L compliant with + corresponding metadata. + """ + return ard_normalized_radar_backscatter( + data=self, + elevation_model=elevation_model, + contributing_area=contributing_area, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + @openeo_process + def ard_surface_reflectance(self, atmospheric_correction_method, cloud_detection_method, elevation_model=UNSET, atmospheric_correction_options=UNSET, cloud_detection_options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant Surface Reflectance generation + + :param self: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances. There must be a single dimension of type `bands` available. + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. Each method supports detecting + different atmospheric disturbances such as clouds, cloud shadows, aerosols, haze, ozone and/or water + vapour in optical imagery. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + Specifying proprietary options will reduce portability. + :param cloud_detection_options: Proprietary options for the cloud detection method. Specifying + proprietary options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances for each spectral band in the source + data cube, with atmospheric disturbances like clouds and cloud shadows removed. No-data values (null) + are directly set in the bands. Depending on the methods used, several additional bands will be added to + the data cube: Data cube containing bottom of atmosphere reflectances for each spectral band in the + source data cube, with atmospheric disturbances like clouds and cloud shadows removed. Depending on the + methods used, several additional bands will be added to the data cube: - `date` (optional): Specifies + per-pixel acquisition timestamps. - `incomplete-testing` (required): Identifies pixels with a value of + 1 for which the per-pixel tests (at least saturation, cloud and cloud shadows, see CARD4L specification + for details) have not all been successfully completed. Otherwise, the value is 0. - `saturation` + (required) / `saturation_{band}` (optional): Indicates where pixels in the input spectral bands are + saturated (1) or not (0). If the saturation is given per band, the band names are `saturation_{band}` + with `{band}` being the band name from the source data cube. - `cloud`, `shadow` (both + required),`aerosol`, `haze`, `ozone`, `water_vapor` (all optional): Indicates the probability of pixels + being an atmospheric disturbance such as clouds. All bands have values between 0 (clear) and 1, which + describes the probability that it is an atmospheric disturbance. - `snow-ice` (optional): Points to a + file that indicates whether a pixel is assessed as being snow/ice (1) or not (0). All values describe + the probability and must be between 0 and 1. - `land-water` (optional): Indicates whether a pixel is + assessed as being land (1) or water (0). All values describe the probability and must be between 0 and + 1. - `incidence-angle` (optional): Specifies per-pixel incidence angles in degrees. - `azimuth` + (optional): Specifies per-pixel azimuth angles in degrees. - `sun-azimuth:` (optional): Specifies per- + pixel sun azimuth angles in degrees. - `sun-elevation` (optional): Specifies per-pixel sun elevation + angles in degrees. - `terrain-shadow` (optional): Indicates with a value of 1 whether a pixel is not + directly illuminated due to terrain shadowing. Otherwise, the value is 0. - `terrain-occlusion` + (optional): Indicates with a value of 1 whether a pixel is not visible to the sensor due to terrain + occlusion during off-nadir viewing. Otherwise, the value is 0. - `terrain-illumination` (optional): + Contains coefficients used for terrain illumination correction are provided for each pixel. The data + returned is CARD4L compliant with corresponding metadata. + """ + return ard_surface_reflectance( + data=self, + atmospheric_correction_method=atmospheric_correction_method, + cloud_detection_method=cloud_detection_method, + elevation_model=elevation_model, + atmospheric_correction_options=atmospheric_correction_options, + cloud_detection_options=cloud_detection_options + ) + + @openeo_process + def array_append(self, value, label=UNSET) -> ProcessBuilder: + """ + Append a value to an array + + :param self: An array. + :param value: Value to append to the array. + :param label: If the given array is a labeled array, a new label for the new value should be given. If + not given or `null`, the array index as string is used as the label. If in any case the label exists, a + `LabelExists` exception is thrown. + + :return: The new array with the value being appended. + """ + return array_append(data=self, value=value, label=label) + + @openeo_process + def array_apply(self, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each array element + + :param self: An array. + :param process: A process that accepts and returns a single value and is applied on each individual + value in the array. The process may consist of multiple sub-processes and could, for example, consist + of processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: An array with the newly computed values. The number of elements are the same as for the + original array. + """ + return array_apply( + data=self, + process=build_child_callback(process, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + @openeo_process + def array_concat(self, array2) -> ProcessBuilder: + """ + Merge two arrays + + :param self: The first array. + :param array2: The second array. + + :return: The merged array. + """ + return array_concat(array1=self, array2=array2) + + @openeo_process + def array_contains(self, value) -> ProcessBuilder: + """ + Check whether the array contains a given value + + :param self: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `false`. + + :return: `true` if the list contains the value, false` otherwise. + """ + return array_contains(data=self, value=value) + + @openeo_process + def array_create(self=UNSET, repeat=UNSET) -> ProcessBuilder: + """ + Create an array + + :param self: A (native) array to fill the newly created array with. Defaults to an empty array. + :param repeat: The number of times the (native) array specified in `data` is repeatedly added after + each other to the new array being created. Defaults to `1`. + + :return: The newly created array. + """ + return array_create(data=self, repeat=repeat) + + @openeo_process + def array_create_labeled(self, labels) -> ProcessBuilder: + """ + Create a labeled array + + :param self: An array of values to be used. + :param labels: An array of labels to be used. + + :return: The newly created labeled array. + """ + return array_create_labeled(data=self, labels=labels) + + @openeo_process + def array_element(self, index=UNSET, label=UNSET, return_nodata=UNSET) -> ProcessBuilder: + """ + Get an element from an array + + :param self: An array. + :param index: The zero-based index of the element to retrieve. + :param label: The label of the element to retrieve. Throws an `ArrayNotLabeled` exception, if the given + array is not a labeled array and this parameter is set. + :param return_nodata: By default this process throws an `ArrayElementNotAvailable` exception if the + index or label is invalid. If you want to return `null` instead, set this flag to `true`. + + :return: The value of the requested element. + """ + return array_element(data=self, index=index, label=label, return_nodata=return_nodata) + + @openeo_process + def array_filter(self, condition, context=UNSET) -> ProcessBuilder: + """ + Filter an array based on a condition + + :param self: An array. + :param condition: A condition that is evaluated against each value, index and/or label in the array. + Only the array elements for which the condition returns `true` are preserved. + :param context: Additional data to be passed to the condition. + + :return: An array filtered by the specified condition. The number of elements are less than or equal + compared to the original array. + """ + return array_filter( + data=self, + condition=build_child_callback(condition, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + @openeo_process + def array_find(self, value, reverse=UNSET) -> ProcessBuilder: + """ + Get the index for a value in an array + + :param self: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `null`. + :param reverse: By default, this process finds the index of the first match. To return the index of the + last match instead, set this flag to `true`. + + :return: The index of the first element with the specified value. If no element was found, `null` is + returned. + """ + return array_find(data=self, value=value, reverse=reverse) + + @openeo_process + def array_find_label(self, label) -> ProcessBuilder: + """ + Get the index for a label in a labeled array + + :param self: List to find the label in. + :param label: Label to find in `data`. + + :return: The index of the element with the specified label assigned. If no such label was found, `null` + is returned. + """ + return array_find_label(data=self, label=label) + + @openeo_process + def array_interpolate_linear(self) -> ProcessBuilder: + """ + One-dimensional linear interpolation for arrays + + :param self: An array of numbers and no-data values. If the given array is a labeled array, the labels + must have a natural/inherent label order and the process expects the labels to be sorted accordingly. + This is the default behavior in openEO for spatial and temporal dimensions. + + :return: An array with no-data values being replaced with interpolated values. If not at least 2 + numerical values are available in the array, the array stays the same. + """ + return array_interpolate_linear(data=self) + + @openeo_process + def array_labels(self) -> ProcessBuilder: + """ + Get the labels for an array + + :param self: An array. + + :return: The labels or indices as array. + """ + return array_labels(data=self) + + @openeo_process + def array_modify(self, values, index, length=UNSET) -> ProcessBuilder: + """ + Change the content of an array (remove, insert, update) + + :param self: The array to modify. + :param values: The values to insert into the `data` array. + :param index: The index in the `data` array of the element to insert the value(s) before. If the index + is greater than the number of elements in the `data` array, the process throws an + `ArrayElementNotAvailable` exception. To insert after the last element, there are two options: 1. Use + the simpler processes ``array_append()`` to append a single value or ``array_concat()`` to append + multiple values. 2. Specify the number of elements in the array. You can retrieve the number of + elements with the process ``count()``, having the parameter `condition` set to `true`. + :param length: The number of elements in the `data` array to remove (or replace) starting from the + given index. If the array contains fewer elements, the process simply removes all elements up to the + end. + + :return: An array with values added, updated or removed. + """ + return array_modify(data=self, values=values, index=index, length=length) + + @openeo_process + def arsinh(self) -> ProcessBuilder: + """ + Inverse hyperbolic sine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arsinh(x=self) + + @openeo_process + def artanh(self) -> ProcessBuilder: + """ + Inverse hyperbolic tangent + + :param self: A number. + + :return: The computed angle in radians. + """ + return artanh(x=self) + + @openeo_process + def atmospheric_correction(self, method, elevation_model=UNSET, options=UNSET) -> ProcessBuilder: + """ + Apply atmospheric correction + + :param self: Data cube containing multi-spectral optical top of atmosphere reflectances to be + corrected. + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but + reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param options: Proprietary options for the atmospheric correction method. Specifying proprietary + options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances. + """ + return atmospheric_correction(data=self, method=method, elevation_model=elevation_model, options=options) + + @openeo_process + def between(self, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison + + :param self: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return between(x=self, min=min, max=max, exclude_max=exclude_max) + + @openeo_process + def ceil(self) -> ProcessBuilder: + """ + Round fractions up + + :param self: A number to round up. + + :return: The number rounded up. + """ + return ceil(x=self) + + @openeo_process + def climatological_normal(self, period, climatology_period=UNSET) -> ProcessBuilder: + """ + Compute climatology normals + + :param self: A data cube with exactly one temporal dimension. The data cube must span at least the + temporal interval specified in the parameter `climatology-period`. Seasonal periods may span two + consecutive years, e.g. temporal winter that includes months December, January and February. If the + required months before the actual climate period are available, the season is taken into account. If + not available, the first season is not taken into account and the seasonal mean is based on one year + less than the other seasonal normals. The incomplete season at the end of the last year is never taken + into account. + :param period: The time intervals to aggregate the average value for. The following pre-defined + frequencies are supported: * `day`: Day of the year * `month`: Month of the year * `climatology- + period`: The period specified in the `climatology-period`. * `season`: Three month periods of the + calendar seasons (December - February, March - May, June - August, September - November). * `tropical- + season`: Six month periods of the tropical seasons (November - April, May - October). + :param climatology_period: The climatology period as a closed temporal interval. The first element of + the array is the first year to be fully included in the temporal interval. The second element is the + last year to be fully included in the temporal interval. The default climatology period is from 1981 + until 2010 (both inclusive) right now, but this might be updated over time to what is commonly used in + climatology. If you don't want to keep your research to be reproducible, please explicitly specify a + period. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the temporal + dimension. The temporal dimension has the following dimension labels: * `day`: `001` - `365` * + `month`: `01` - `12` * `climatology-period`: `climatology-period` * `season`: `djf` (December - + February), `mam` (March - May), `jja` (June - August), `son` (September - November) * `tropical- + season`: `ndjfma` (November - April), `mjjaso` (May - October) + """ + return climatological_normal(data=self, period=period, climatology_period=climatology_period) + + @openeo_process + def clip(self, min, max) -> ProcessBuilder: + """ + Clip a value between a minimum and a maximum + + :param self: A number. + :param min: Minimum value. If the value is lower than this value, the process will return the value of + this parameter. + :param max: Maximum value. If the value is greater than this value, the process will return the value + of this parameter. + + :return: The value clipped to the specified range. + """ + return clip(x=self, min=min, max=max) + + @openeo_process + def cloud_detection(self, method, options=UNSET) -> ProcessBuilder: + """ + Create cloud masks + + :param self: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances on which to perform cloud detection. + :param method: The cloud detection method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but + reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param options: Proprietary options for the cloud detection method. Specifying proprietary options will + reduce portability. + + :return: A data cube with bands for the atmospheric disturbances. Each of the masks contains values + between 0 and 1. The data cube has the same spatial and temporal dimensions as the source data cube and + a dimension that contains a dimension label for each of the supported/considered atmospheric + disturbance. + """ + return cloud_detection(data=self, method=method, options=options) + + @openeo_process + def constant(self) -> ProcessBuilder: + """ + Define a constant value + + :param self: The value of the constant. + + :return: The value of the constant. + """ + return constant(x=self) + + @openeo_process + def cos(self) -> ProcessBuilder: + """ + Cosine + + :param self: An angle in radians. + + :return: The computed cosine of `x`. + """ + return cos(x=self) + + @openeo_process + def cosh(self) -> ProcessBuilder: + """ + Hyperbolic cosine + + :param self: An angle in radians. + + :return: The computed hyperbolic cosine of `x`. + """ + return cosh(x=self) + + @openeo_process + def count(self, condition=UNSET, context=UNSET) -> ProcessBuilder: + """ + Count the number of elements + + :param self: An array with elements of any data type. + :param condition: A condition consists of one or more processes, which in the end return a boolean + value. It is evaluated against each element in the array. An element is counted only if the condition + returns `true`. Defaults to count valid elements in a list (see ``is_valid()``). Setting this parameter + to boolean `true` counts all elements in the list. `false` is not a valid value for this parameter. + :param context: Additional data to be passed to the condition. + + :return: The counted number of elements. + """ + return count(data=self, condition=condition, context=context) + + @openeo_process + def create_data_cube(self) -> ProcessBuilder: + """ + Create an empty data cube + + :return: An empty data cube with no dimensions. + """ + return create_data_cube() + + @openeo_process + def cummax(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative maxima + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative maxima. + """ + return cummax(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cummin(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative minima + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative minima. + """ + return cummin(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cumproduct(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative products + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative products. + """ + return cumproduct(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cumsum(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative sums + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative sums. + """ + return cumsum(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def date_between(self, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison for dates and times + + :param self: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return date_between(x=self, min=min, max=max, exclude_max=exclude_max) + + @openeo_process + def date_difference(self, date2, unit=UNSET) -> ProcessBuilder: + """ + Computes the difference between two time instants + + :param self: The base date, optionally with a time component. + :param date2: The other date, optionally with a time component. + :param unit: The unit for the returned value. The following units are available: - millisecond - + second - leap seconds are ignored in computations. - minute - hour - day - month - year + + :return: Returns the difference between date1 and date2 in the given unit (seconds by default), + including a fractional part if required. For comparison purposes this means: - If `date1` < `date2`, + the returned value is positive. - If `date1` = `date2`, the returned value is 0. - If `date1` > + `date2`, the returned value is negative. + """ + return date_difference(date1=self, date2=date2, unit=unit) + + @openeo_process + def date_shift(self, value, unit) -> ProcessBuilder: + """ + Manipulates dates and times by addition or subtraction + + :param self: The date (and optionally time) to manipulate. If the given date doesn't include the time, + the process assumes that the time component is `00:00:00Z` (i.e. midnight, in UTC). The millisecond + part of the time is optional and defaults to `0` if not given. + :param value: The period of time in the unit given that is added (positive numbers) or subtracted + (negative numbers). The value `0` doesn't have any effect. + :param unit: The unit for the value given. The following pre-defined units are available: - + millisecond: Milliseconds - second: Seconds - leap seconds are ignored in computations. - minute: + Minutes - hour: Hours - day: Days - changes only the the day part of a date - week: Weeks (equivalent + to 7 days) - month: Months - year: Years Manipulations with the unit `year`, `month`, `week` or `day` + do never change the time. If any of the manipulations result in an invalid date or time, the + corresponding part is rounded down to the next valid date or time respectively. For example, adding a + month to `2020-01-31` would result in `2020-02-29`. + + :return: The manipulated date. If a time component was given in the parameter `date`, the time + component is returned with the date. + """ + return date_shift(date=self, value=value, unit=unit) + + @openeo_process + def dimension_labels(self, dimension) -> ProcessBuilder: + """ + Get the dimension labels + + :param self: The data cube. + :param dimension: The name of the dimension to get the labels for. + + :return: The labels as an array. + """ + return dimension_labels(data=self, dimension=dimension) + + @openeo_process + def divide(self, y) -> ProcessBuilder: + """ + Division of two numbers + + :param self: The dividend. + :param y: The divisor. + + :return: The computed result. + """ + return divide(x=self, y=y) + + @openeo_process + def drop_dimension(self, name) -> ProcessBuilder: + """ + Remove a dimension + + :param self: The data cube to drop a dimension from. + :param name: Name of the dimension to drop. + + :return: A data cube without the specified dimension. The number of dimensions decreases by one, but + the dimension properties (name, type, labels, reference system and resolution) for all other dimensions + remain unchanged. + """ + return drop_dimension(data=self, name=name) + + @openeo_process + def e(self) -> ProcessBuilder: + """ + Euler's number (e) + + :return: The numerical value of Euler's number. + """ + return e() + + @openeo_process + def eq(self, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Equal to comparison + + :param self: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a + positive non-zero number the equality of two numbers is checked against a delta value. This is + especially useful to circumvent problems with floating-point inaccuracy in machine-based computation. + This option is basically an alias for the following computation: `lte(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be + disabled by setting this parameter to `false`. + + :return: `true` if `x` is equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return eq(x=self, y=y, delta=delta, case_sensitive=case_sensitive) + + @openeo_process + def exp(self) -> ProcessBuilder: + """ + Exponentiation to the base e + + :param self: The numerical exponent. + + :return: The computed value for *e* raised to the power of `p`. + """ + return exp(p=self) + + @openeo_process + def extrema(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum and maximum values + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that an array with two `null` values is + returned if any value is such a value. + + :return: An array containing the minimum and maximum values for the specified numbers. The first + element is the minimum, the second element is the maximum. If the input array is empty both elements + are set to `null`. + """ + return extrema(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def filter_bands(self, bands=UNSET, wavelengths=UNSET) -> ProcessBuilder: + """ + Filter the bands by names + + :param self: A data cube with bands. + :param bands: A list of band names. Either the unique band name (metadata field `name` in bands) or one + of the common band names (metadata field `common_name` in bands). If the unique band name and the + common name conflict, the unique band name has a higher priority. The order of the specified array + defines the order of the bands in the data cube. If multiple bands match a common name, all matched + bands are included in the original order. + :param wavelengths: A list of sub-lists with each sub-list consisting of two elements. The first + element is the minimum wavelength and the second element is the maximum wavelength. Wavelengths are + specified in micrometers (μm). The order of the specified array defines the order of the bands in the + data cube. If multiple bands match the wavelengths, all matched bands are included in the original + order. + + :return: A data cube limited to a subset of its original bands. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the dimension of + type `bands` has less (or the same) dimension labels. + """ + return filter_bands(data=self, bands=bands, wavelengths=wavelengths) + + @openeo_process + def filter_bbox(self, extent) -> ProcessBuilder: + """ + Spatial filter using a bounding box + + :param self: A data cube. + :param extent: A bounding box, which may include a vertical axis (see `base` and `height`). + + :return: A data cube restricted to the bounding box. The dimensions and dimension properties (name, + type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions + have less (or the same) dimension labels. + """ + return filter_bbox(data=self, extent=extent) + + @openeo_process + def filter_labels(self, condition, dimension, context=UNSET) -> ProcessBuilder: + """ + Filter dimension labels based on a condition + + :param self: A data cube. + :param condition: A condition that is evaluated against each dimension label in the specified + dimension. A dimension label and the corresponding data is preserved for the given dimension, if the + condition returns `true`. + :param dimension: The name of the dimension to filter on. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the condition. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that the given dimension has less (or the same) + dimension labels. + """ + return filter_labels( + data=self, + condition=build_child_callback(condition, parent_parameters=['value', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def filter_spatial(self, geometries) -> ProcessBuilder: + """ + Spatial filter raster data cubes using geometries + + :param self: A raster data cube. + :param geometries: One or more geometries used for filtering, given as GeoJSON or vector data cube. If + multiple geometries are provided, the union of them is used. Empty geometries are ignored. Limits the + data cube to the bounding box of the given geometries. No implicit masking gets applied. To mask the + pixels of the data cube use ``mask_polygon()``. + + :return: A raster data cube restricted to the specified geometries. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + spatial dimensions have less (or the same) dimension labels. + """ + return filter_spatial(data=self, geometries=geometries) + + @openeo_process + def filter_temporal(self, extent, dimension=UNSET) -> ProcessBuilder: + """ + Temporal filter based on temporal intervals + + :param self: A data cube. + :param extent: Left-closed temporal interval, i.e. an array with exactly two elements: 1. The first + element is the start of the temporal interval. The specified time instant is **included** in the + interval. 2. The second element is the end of the temporal interval. The specified time instant is + **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by + setting one of the boundaries to `null`, but never both. + :param dimension: The name of the temporal dimension to filter on. If no specific dimension is + specified, the filter applies to all temporal dimensions. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + + :return: A data cube restricted to the specified temporal extent. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + temporal dimensions (determined by `dimensions` parameter) may have less dimension labels. + """ + return filter_temporal(data=self, extent=extent, dimension=dimension) + + @openeo_process + def filter_vector(self, geometries, relation=UNSET) -> ProcessBuilder: + """ + Spatial vector filter using geometries + + :param self: A vector data cube with the candidate geometries. + :param geometries: One or more base geometries used for filtering, given as vector data cube. If + multiple base geometries are provided, the union of them is used. + :param relation: The spatial filter predicate for comparing the geometries provided through (a) + `geometries` (base geometries) and (b) `data` (candidate geometries). + + :return: A vector data cube restricted to the specified geometries. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + geometries dimension has less (or the same) dimension labels. + """ + return filter_vector(data=self, geometries=geometries, relation=relation) + + @openeo_process + def first(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + First element + + :param self: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if the first value is + such a value. + + :return: The first element of the input array. + """ + return first(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def fit_curve(self, parameters, function, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Curve fitting + + :param self: A labeled array, the labels correspond to the variable `y` and the values correspond to + the variable `x`. + :param parameters: Defined the number of parameters for the model function and provides an initial + guess for them. At least one parameter is required. + :param function: The model function. It must take the parameters to fit as array through the first + argument and the independent variable `x` as the second argument. It is recommended to store the model + function as a user-defined process on the back-end to be able to re-use the model function with the + computed optimal values for the parameters afterwards. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is passed to the model function. + + :return: An array with the optimal values for the parameters. + """ + return fit_curve( + data=self, + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + ignore_nodata=ignore_nodata + ) + + @openeo_process + def flatten_dimensions(self, dimensions, target_dimension, label_separator=UNSET) -> ProcessBuilder: + """ + Combine multiple dimensions into a single dimension + + :param self: A data cube. + :param dimensions: The names of the dimension to combine. The order of the array defines the order in + which the dimension labels and values are combined (see the example in the process description). Fails + with a `DimensionNotAvailable` exception if at least one of the specified dimensions does not exist. + :param target_dimension: The name of the new target dimension. A new dimensions will be created with + the given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` + exception if a dimension with the specified name exists. + :param label_separator: The string that will be used as a separator for the concatenated dimension + labels. To unambiguously revert the dimension labels with the process ``unflatten_dimension()``, the + given string must not be contained in any of the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system + and resolution) for all other dimensions remain unchanged. + """ + return flatten_dimensions(data=self, dimensions=dimensions, target_dimension=target_dimension, label_separator=label_separator) + + @openeo_process + def floor(self) -> ProcessBuilder: + """ + Round fractions down + + :param self: A number to round down. + + :return: The number rounded down. + """ + return floor(x=self) + + @openeo_process + def gt(self, y) -> ProcessBuilder: + """ + Greater than comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly greater than `y` or `null` if any operand is `null`, otherwise + `false`. + """ + return gt(x=self, y=y) + + @openeo_process + def gte(self, y) -> ProcessBuilder: + """ + Greater than or equal to comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is greater than or equal to `y`, `null` if any operand is `null`, otherwise + `false`. + """ + return gte(x=self, y=y) + + @openeo_process + def if_(self, accept, reject=UNSET) -> ProcessBuilder: + """ + If-Then-Else conditional + + :param self: A boolean value. + :param accept: A value that is returned if the boolean value is `true`. + :param reject: A value that is returned if the boolean value is **not** `true`. Defaults to `null`. + + :return: Either the `accept` or `reject` argument depending on the given boolean value. + """ + return if_(value=self, accept=accept, reject=reject) + + @openeo_process + def inspect(self, message=UNSET, code=UNSET, level=UNSET) -> ProcessBuilder: + """ + Add information to the logs + + :param self: Data to log. + :param message: A message to send in addition to the data. + :param code: A label to help identify one or more log entries originating from this process in the list + of all log entries. It can help to group or filter log entries and is usually not unique. + :param level: The severity level of this message, defaults to `info`. + + :return: The data as passed to the `data` parameter without any modification. + """ + return inspect(data=self, message=message, code=code, level=level) + + @openeo_process + def int(self) -> ProcessBuilder: + """ + Integer part of a number + + :param self: A number. + + :return: Integer part of the number. + """ + return int(x=self) + + @openeo_process + def is_infinite(self) -> ProcessBuilder: + """ + Value is an infinite number + + :param self: The data to check. + + :return: `true` if the data is an infinite number, otherwise `false`. + """ + return is_infinite(x=self) + + @openeo_process + def is_nan(self) -> ProcessBuilder: + """ + Value is not a number + + :param self: The data to check. + + :return: Returns `true` for `NaN` and all non-numeric data types, otherwise returns `false`. + """ + return is_nan(x=self) + + @openeo_process + def is_nodata(self) -> ProcessBuilder: + """ + Value is a no-data value + + :param self: The data to check. + + :return: `true` if the data is a no-data value, otherwise `false`. + """ + return is_nodata(x=self) + + @openeo_process + def is_valid(self) -> ProcessBuilder: + """ + Value is valid data + + :param self: The data to check. + + :return: `true` if the data is valid, otherwise `false`. + """ + return is_valid(x=self) + + @openeo_process + def last(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Last element + + :param self: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if the last value is + such a value. + + :return: The last element of the input array. + """ + return last(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def linear_scale_range(self, inputMin, inputMax, outputMin=UNSET, outputMax=UNSET) -> ProcessBuilder: + """ + Linear transformation between two ranges + + :param self: A number to transform. The number gets clipped to the bounds specified in `inputMin` and + `inputMax`. + :param inputMin: Minimum value the input can obtain. + :param inputMax: Maximum value the input can obtain. + :param outputMin: Minimum value of the desired output range. + :param outputMax: Maximum value of the desired output range. + + :return: The transformed number. + """ + return linear_scale_range(x=self, inputMin=inputMin, inputMax=inputMax, outputMin=outputMin, outputMax=outputMax) + + @openeo_process + def ln(self) -> ProcessBuilder: + """ + Natural logarithm + + :param self: A number to compute the natural logarithm for. + + :return: The computed natural logarithm. + """ + return ln(x=self) + + @openeo_process + def load_collection(self, spatial_extent, temporal_extent, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Load a collection + + :param self: The collection id. + :param spatial_extent: Limits the data to load from the collection to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). * For vector data, the process loads the geometry into the data cube if the + geometry is fully *within* the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been + provided. The GeoJSON can be one of the following feature types: * A `Polygon` or `MultiPolygon` + geometry, * a `Feature` with a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` + containing at least one `Feature` with `Polygon` or `MultiPolygon` geometries. * Empty geometries are + ignored. Set this parameter to `null` to set no limit for the spatial extent. Be careful with this + when loading large datasets! It is recommended to use this parameter instead of using ``filter_bbox()`` + or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load from the collection to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array + with exactly two elements: 1. The first element is the start of the temporal interval. The specified + time instant is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified time instant is **excluded** from the interval. The second element must always + be greater/later than the first element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also + supports unbounded intervals by setting one of the boundaries to `null`, but never both. Set this + parameter to `null` to set no limit for the temporal extent. Be careful with this when loading large + datasets! It is recommended to use this parameter instead of using ``filter_temporal()`` directly after + loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + :param properties: Limits the data by metadata properties to include only data in the data cube which + all given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the + name of the metadata property, which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against the collection metadata, + see the example. + + :return: A data cube for further processing. The dimensions and dimension properties (name, type, + labels, reference system and resolution) correspond to the collection's metadata, but the dimension + labels are restricted as specified in the parameters. + """ + return load_collection(id=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + @openeo_process + def load_geojson(self, properties=UNSET) -> ProcessBuilder: + """ + Converts GeoJSON into a vector data cube + + :param self: A GeoJSON object to convert into a vector data cube. The GeoJSON type `GeometryCollection` + is not supported. Each geometry in the GeoJSON data results in a dimension label in the `geometries` + dimension. + :param properties: A list of properties from the GeoJSON file to construct an additional dimension + from. A new dimension with the name `properties` and type `other` is created if at least one property + is provided. Only applies for GeoJSON Features and FeatureCollections. Missing values are generally set + to no-data (`null`). Depending on the number of properties provided, the process creates the dimension + differently: - Single property with scalar values: A single dimension label with the name of the + property and a single value per geometry. - Single property of type array: The dimension labels + correspond to the array indices. There are as many values and labels per geometry as there are for the + largest array. - Multiple properties with scalar values: The dimension labels correspond to the + property names. There are as many values and labels per geometry as there are properties provided here. + + :return: A vector data cube containing the geometries, either one or two dimensional. + """ + return load_geojson(data=self, properties=properties) + + @openeo_process + def load_ml_model(self) -> ProcessBuilder: + """ + Load a ML model + + :param self: The STAC Item to load the machine learning model from. The STAC Item must implement the + `ml-model` extension. + + :return: A machine learning model to be used with machine learning processes such as + ``predict_random_forest()``. + """ + return load_ml_model(id=self) + + @openeo_process + def load_result(self, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET) -> ProcessBuilder: + """ + Load batch job results + + :param self: The id of a batch job with results. + :param spatial_extent: Limits the data to load from the batch job result to the specified bounding box + or polygons. * For raster data, the process loads the pixel into the data cube if the point at the + pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). * For vector data, the process loads the geometry into the data cube of the + geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been + provided. The GeoJSON can be one of the following feature types: * A `Polygon` or `MultiPolygon` + geometry, * a `Feature` with a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` + containing at least one `Feature` with `Polygon` or `MultiPolygon` geometries. Set this parameter to + `null` to set no limit for the spatial extent. Be careful with this when loading large datasets! It is + recommended to use this parameter instead of using ``filter_bbox()`` or ``filter_spatial()`` directly + after loading unbounded data. + :param temporal_extent: Limits the data to load from the batch job result to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array + with exactly two elements: 1. The first element is the start of the temporal interval. The specified + instance in time is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified instance in time is **excluded** from the interval. The specified temporal + strings follow [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html). Also supports open intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit + for the temporal extent. Be careful with this when loading large datasets! It is recommended to use + this parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + + :return: A data cube for further processing. + """ + return load_result(id=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands) + + @openeo_process + def load_stac(self, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Loads data from STAC + + :param self: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a + specific STAC API Collection that allows to filter items and to download assets. This includes batch + job results, which itself are compliant to STAC. For external URLs, authentication details such as API + keys or tokens may need to be included in the URL. Batch job results can be specified in two ways: - + For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with `/jobs/{id}/results` and `{id}` is the + corresponding batch job ID. - For external results, a signed URL must be provided. Not all back-ends + support signed URLs, which are provided as a link with the link relation `canonical` in the batch job + result metadata. + :param spatial_extent: Limits the data to load to the specified bounding box or polygons. * For raster + data, the process loads the pixel into the data cube if the point at the pixel center intersects with + the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). * For + vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty + geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be one + of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. Set this parameter to `null` to set no limit for the spatial + extent. Be careful with this when loading large datasets! It is recommended to use this parameter + instead of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load to the specified left-closed temporal interval. Applies + to all temporal dimensions. The interval has to be specified as an array with exactly two elements: 1. + The first element is the start of the temporal interval. The specified instance in time is **included** + in the interval. 2. The second element is the end of the temporal interval. The specified instance in + time is **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports open intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit + for the temporal extent. Be careful with this when loading large datasets! It is recommended to use + this parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + :param properties: Limits the data by metadata properties to include only data in the data cube which + all given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the + name of the metadata property, which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. This parameter + is not supported for static STAC. + + :return: A data cube for further processing. + """ + return load_stac(url=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + @openeo_process + def load_uploaded_files(self, format, options=UNSET) -> ProcessBuilder: + """ + Load files from the user workspace + + :param self: The files to read. Folders can't be specified, specify all files instead. An exception is + thrown if a file can't be read. + :param format: The file format to read from. It must be one of the values that the server reports as + supported input file formats, which usually correspond to the short GDAL/OGR codes. If the format is + not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter is + *case insensitive*. + :param options: The file format parameters to be used to read the files. Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return load_uploaded_files(paths=self, format=format, options=options) + + @openeo_process + def load_url(self, format, options=UNSET) -> ProcessBuilder: + """ + Load data from a URL + + :param self: The URL to read from. Authentication details such as API keys or tokens may need to be + included in the URL. + :param format: The file format to use when loading the data. It must be one of the values that the + server reports as supported input file formats, which usually correspond to the short GDAL/OGR codes. + If the format is not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This + parameter is *case insensitive*. + :param options: The file format parameters to use when reading the data. Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return load_url(url=self, format=format, options=options) + + @openeo_process + def log(self, base) -> ProcessBuilder: + """ + Logarithm to a base + + :param self: A number to compute the logarithm for. + :param base: The numerical base. + + :return: The computed logarithm. + """ + return log(x=self, base=base) + + @openeo_process + def lt(self, y) -> ProcessBuilder: + """ + Less than comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly less than `y`, `null` if any operand is `null`, otherwise `false`. + """ + return lt(x=self, y=y) + + @openeo_process + def lte(self, y) -> ProcessBuilder: + """ + Less than or equal to comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is less than or equal to `y`, `null` if any operand is `null`, otherwise + `false`. + """ + return lte(x=self, y=y) + + @openeo_process + def mask(self, mask, replacement=UNSET) -> ProcessBuilder: + """ + Apply a raster mask + + :param self: A raster data cube. + :param mask: A mask as a raster data cube. Every pixel in `data` must have a corresponding element in + `mask`. + :param replacement: The value used to replace masked values with. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, + labels, reference system and resolution) remain unchanged. + """ + return mask(data=self, mask=mask, replacement=replacement) + + @openeo_process + def mask_polygon(self, mask, replacement=UNSET, inside=UNSET) -> ProcessBuilder: + """ + Apply a polygon mask + + :param self: A raster data cube. + :param mask: A GeoJSON object or a vector data cube containing at least one polygon. The provided + vector data can be one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with + a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` + with `Polygon` or `MultiPolygon` geometries. * Empty geometries are ignored. + :param replacement: The value used to replace masked values with. + :param inside: If set to `true` all pixels for which the point at the pixel center **does** intersect + with any polygon are replaced. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, + labels, reference system and resolution) remain unchanged. + """ + return mask_polygon(data=self, mask=mask, replacement=replacement, inside=inside) + + @openeo_process + def max(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Maximum value + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The maximum value. + """ + return max(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def mean(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Arithmetic mean (average) + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed arithmetic mean. + """ + return mean(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def median(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Statistical median + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed statistical median. + """ + return median(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def merge_cubes(self, cube2, overlap_resolver=UNSET, context=UNSET) -> ProcessBuilder: + """ + Merge two data cubes + + :param self: The base data cube. + :param cube2: The other data cube to be merged with the base data cube. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The + reducer must return a value of the same data type as the input values are. The reduction operator may + be a single process such as ``multiply()`` or consist of multiple sub-processes. `null` (the default) + can be specified if no overlap resolver is required. + :param context: Additional data to be passed to the overlap resolver. + + :return: The merged data cube. See the process description for details regarding the dimensions and + dimension properties (name, type, labels, reference system and resolution). + """ + return merge_cubes( + cube1=self, + cube2=cube2, + overlap_resolver=(build_child_callback(overlap_resolver, parent_parameters=['x', 'y', 'context']) if overlap_resolver not in [None, UNSET] else overlap_resolver), + context=context + ) + + @openeo_process + def min(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum value + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The minimum value. + """ + return min(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def mod(self, y) -> ProcessBuilder: + """ + Modulo + + :param self: A number to be used as the dividend. + :param y: A number to be used as the divisor. + + :return: The remainder after division. + """ + return mod(x=self, y=y) + + @openeo_process + def multiply(self, y) -> ProcessBuilder: + """ + Multiplication of two numbers + + :param self: The multiplier. + :param y: The multiplicand. + + :return: The computed product of the two numbers. + """ + return multiply(x=self, y=y) + + @openeo_process + def nan(self) -> ProcessBuilder: + """ + Not a Number (NaN) + + :return: Returns `NaN`. + """ + return nan() + + @openeo_process + def ndvi(self, nir=UNSET, red=UNSET, target_band=UNSET) -> ProcessBuilder: + """ + Normalized Difference Vegetation Index + + :param self: A raster data cube with two bands that have the common names `red` and `nir` assigned. + :param nir: The name of the NIR band. Defaults to the band that has the common name `nir` assigned. + Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata + field `common_name` in bands) can be specified. If the unique band name and the common name conflict, + the unique band name has a higher priority. + :param red: The name of the red band. Defaults to the band that has the common name `red` assigned. + Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata + field `common_name` in bands) can be specified. If the unique band name and the common name conflict, + the unique band name has a higher priority. + :param target_band: By default, the dimension of type `bands` is dropped. To keep the dimension specify + a new band name in this parameter so that a new dimension label with the specified name will be added + for the computed values. + + :return: A raster data cube containing the computed NDVI values. The structure of the data cube differs + depending on the value passed to `target_band`: * `target_band` is `null`: The data cube does not + contain the dimension of type `bands`, the number of dimensions decreases by one. The dimension + properties (name, type, labels, reference system and resolution) for all other dimensions remain + unchanged. * `target_band` is a string: The data cube keeps the same dimensions. The dimension + properties remain unchanged, but the number of dimension labels for the dimension of type `bands` + increases by one. The additional label is named as specified in `target_band`. + """ + return ndvi(data=self, nir=nir, red=red, target_band=target_band) + + @openeo_process + def neq(self, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Not equal to comparison + + :param self: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a + positive non-zero number the non-equality of two numbers is checked against a delta value. This is + especially useful to circumvent problems with floating-point inaccuracy in machine-based computation. + This option is basically an alias for the following computation: `gt(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be + disabled by setting this parameter to `false`. + + :return: `true` if `x` is *not* equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return neq(x=self, y=y, delta=delta, case_sensitive=case_sensitive) + + @openeo_process + def normalized_difference(self, y) -> ProcessBuilder: + """ + Normalized difference + + :param self: The value for the first band. + :param y: The value for the second band. + + :return: The computed normalized difference. + """ + return normalized_difference(x=self, y=y) + + @openeo_process + def not_(self) -> ProcessBuilder: + """ + Inverting a boolean + + :param self: Boolean value to invert. + + :return: Inverted boolean value. + """ + return not_(x=self) + + @openeo_process + def or_(self, y) -> ProcessBuilder: + """ + Logical OR + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical OR. + """ + return or_(x=self, y=y) + + @openeo_process + def order(self, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Get the order of array elements + + :param self: An array to compute the order for. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set + to `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The computed permutation. + """ + return order(data=self, asc=asc, nodata=nodata) + + @openeo_process + def pi(self) -> ProcessBuilder: + """ + Pi (π) + + :return: The numerical value of Pi. + """ + return pi() + + @openeo_process + def power(self, p) -> ProcessBuilder: + """ + Exponentiation + + :param self: The numerical base. + :param p: The numerical exponent. + + :return: The computed value for `base` raised to the power of `p`. + """ + return power(base=self, p=p) + + @openeo_process + def predict_curve(self, function, dimension, labels=UNSET) -> ProcessBuilder: + """ + Predict values + + :param self: A data cube with optimal values, e.g. computed by the process ``fit_curve()``. + :param function: The model function. It must take the parameters to fit as array through the first + argument and the independent variable `x` as the second argument. It is recommended to store the model + function as a user-defined process on the back-end. + :param dimension: The name of the dimension for predictions. + :param labels: The labels to predict values for. If no labels are given, predicts values only for no- + data (`null`) values in the data cube. + + :return: A data cube with the predicted values with the provided dimension `dimension` having as many + labels as provided through `labels`. + """ + return predict_curve( + parameters=self, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + dimension=dimension, + labels=labels + ) + + @openeo_process + def predict_random_forest(self, model) -> ProcessBuilder: + """ + Predict values based on a Random Forest model + + :param self: An array of numbers. + :param model: A model object that can be trained with the processes ``fit_regr_random_forest()`` + (regression) and ``fit_class_random_forest()`` (classification). + + :return: The predicted value. Returns `null` if any of the given values in the array is a no-data + value. + """ + return predict_random_forest(data=self, model=model) + + @openeo_process + def product(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the product by multiplying numbers + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed product of the sequence of numbers. + """ + return product(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def quantiles(self, probabilities=UNSET, q=UNSET, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Quantiles + + :param self: An array of numbers. + :param probabilities: Quantiles to calculate. Either a list of probabilities or the number of + intervals: * Provide an array with a sorted list of probabilities in ascending order to calculate + quantiles for. The probabilities must be between 0 and 1 (inclusive). If not sorted in ascending order, + an `AscendingProbabilitiesRequired` exception is thrown. * Provide an integer to specify the number of + intervals to calculate quantiles for. Calculates q-quantiles with equal-sized intervals. + :param q: Number of intervals to calculate quantiles for. Calculates q-quantiles with equal-sized + intervals. This parameter has been **deprecated**. Please use the parameter `probabilities` instead. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that an array with `null` values is returned + if any element is such a value. + + :return: An array with the computed quantiles. The list has either * as many elements as the given + list of `probabilities` had or * *`q`-1* elements. If the input array is empty the resulting array is + filled with as many `null` values as required according to the list above. See the 'Empty array' + example for an example. + """ + return quantiles(data=self, probabilities=probabilities, q=q, ignore_nodata=ignore_nodata) + + @openeo_process + def rearrange(self, order) -> ProcessBuilder: + """ + Sort an array based on a permutation + + :param self: The array to rearrange. + :param order: The permutation used for rearranging. + + :return: The rearranged array. + """ + return rearrange(data=self, order=order) + + @openeo_process + def reduce_dimension(self, reducer, dimension, context=UNSET) -> ProcessBuilder: + """ + Reduce dimensions + + :param self: A data cube. + :param reducer: A reducer to apply on the specified dimension. A reducer is a single process such as + ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param dimension: The name of the dimension over which to reduce. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the given dimension, the number of + dimensions decreases by one. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return reduce_dimension( + data=self, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def reduce_spatial(self, reducer, context=UNSET) -> ProcessBuilder: + """ + Reduce spatial dimensions 'x' and 'y' + + :param self: A raster data cube. + :param reducer: A reducer to apply on the horizontal spatial dimensions. A reducer is a single process + such as ``mean()`` or a set of processes, which computes a single value for a list of values, see the + category 'reducer' for such processes. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the horizontal spatial dimensions, + the number of dimensions decreases by two. The dimension properties (name, type, labels, reference + system and resolution) for all other dimensions remain unchanged. + """ + return reduce_spatial(data=self, reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), context=context) + + @openeo_process + def rename_dimension(self, source, target) -> ProcessBuilder: + """ + Rename a dimension + + :param self: The data cube. + :param source: The current name of the dimension. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a `DimensionExists` exception if a dimension + with the specified name exists. + + :return: A data cube with the same dimensions, but the name of one of the dimensions changes. The old + name can not be referred to any longer. The dimension properties (name, type, labels, reference system + and resolution) remain unchanged. + """ + return rename_dimension(data=self, source=source, target=target) + + @openeo_process + def rename_labels(self, dimension, target, source=UNSET) -> ProcessBuilder: + """ + Rename dimension labels + + :param self: The data cube. + :param dimension: The name of the dimension to rename the labels for. + :param target: The new names for the labels. If a target dimension label already exists in the data + cube, a `LabelExists` exception is thrown. + :param source: The original names of the labels to be renamed to corresponding array elements in the + parameter `target`. It is allowed to only specify a subset of labels to rename, as long as the `target` + and `source` parameter have the same length. The order of the labels doesn't need to match the order of + the dimension labels in the data cube. By default, the array is empty so that the dimension labels in + the data cube are expected to be enumerated. If the dimension labels are not enumerated and the given + array is empty, the `LabelsNotEnumerated` exception is thrown. If one of the source dimension labels + doesn't exist, the `LabelNotAvailable` exception is thrown. + + :return: The data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except that for the given dimension the labels + change. The old labels can not be referred to any longer. The number of labels remains the same. + """ + return rename_labels(data=self, dimension=dimension, target=target, source=source) + + @openeo_process + def resample_cube_spatial(self, target, method=UNSET) -> ProcessBuilder: + """ + Resample the spatial dimensions to match a target data cube + + :param self: A raster data cube. + :param target: A raster data cube that describes the spatial target resolution. + :param method: Resampling method to use. The following options are available and are meant to align + with [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average + (mean) resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling + * `cubic`: cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc + resampling * `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median + resampling, selects the median value of all valid pixels * `min`: minimum resampling, selects the + minimum value from all valid pixels * `mode`: mode resampling, selects the value which appears most + often of all the sampled points * `near`: nearest neighbour resampling (default) * `q1`: first quartile + resampling, selects the first quartile value of all valid pixels * `q3`: third quartile resampling, + selects the third quartile value of all valid pixels * `rms` root mean square (quadratic mean) of all + valid pixels * `sum`: compute the weighted sum of all valid pixels Valid pixels are determined based + on the function ``is_valid()``. + + :return: A raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the spatial dimensions. + """ + return resample_cube_spatial(data=self, target=target, method=method) + + @openeo_process + def resample_cube_temporal(self, target, dimension=UNSET, valid_within=UNSET) -> ProcessBuilder: + """ + Resample temporal dimensions to match a target data cube + + :param self: A data cube with one or more temporal dimensions. + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample, which must exist with this name in + both data cubes. If the dimension is not set or is set to `null`, the process resamples all temporal + dimensions that exist with the same names in both data cubes. The following exceptions may occur: * A + dimension is given, but it does not exist in any of the data cubes: `DimensionNotAvailable` * A + dimension is given, but one of them is not temporal: `DimensionMismatch` * No specific dimension name + is given and there are no temporal dimensions with the same name in the data: `DimensionMismatch` + :param valid_within: Setting this parameter to a numerical value enables that the process searches for + valid values within the given period of days before and after the target timestamps. Valid values are + determined based on the function ``is_valid()``. For example, the limit of `7` for the target + timestamps `2020-01-15 12:00:00` looks for a nearest neighbor after `2020-01-08 12:00:00` and before + `2020-01-22 12:00:00`. If no valid value is found within the given period, the value will be set to no- + data (`null`). + + :return: A data cube with the same dimensions and the same dimension properties (name, type, labels, + reference system and resolution) for all non-temporal dimensions. For the temporal dimension, the name + and type remain unchanged, but the dimension labels, resolution and reference system may change. + """ + return resample_cube_temporal(data=self, target=target, dimension=dimension, valid_within=valid_within) + + @openeo_process + def resample_spatial(self, resolution=UNSET, projection=UNSET, method=UNSET, align=UNSET) -> ProcessBuilder: + """ + Resample and warp the spatial dimensions + + :param self: A raster data cube. + :param resolution: Resamples the data cube to the target resolution, which can be specified either as + separate values for x and y or as a single value for both axes. Specified in the units of the target + projection. Doesn't change the resolution by default (`0`). + :param projection: Warps the data cube to the target projection, specified as as [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). By default (`null`), the projection + is not changed. + :param method: Resampling method to use. The following options are available and are meant to align + with [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average + (mean) resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling + * `cubic`: cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc + resampling * `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median + resampling, selects the median value of all valid pixels * `min`: minimum resampling, selects the + minimum value from all valid pixels * `mode`: mode resampling, selects the value which appears most + often of all the sampled points * `near`: nearest neighbour resampling (default) * `q1`: first quartile + resampling, selects the first quartile value of all valid pixels * `q3`: third quartile resampling, + selects the third quartile value of all valid pixels * `rms` root mean square (quadratic mean) of all + valid pixels * `sum`: compute the weighted sum of all valid pixels Valid pixels are determined based + on the function ``is_valid()``. + :param align: Specifies to which corner of the spatial extent the new resampled data is aligned to. + + :return: A raster data cube with values warped onto the new projection. It has the same dimensions and + the same dimension properties (name, type, labels, reference system and resolution) for all non-spatial + or vertical spatial dimensions. For the horizontal spatial dimensions the name and type remain + unchanged, but reference system, labels and resolution may change depending on the given parameters. + """ + return resample_spatial(data=self, resolution=resolution, projection=projection, method=method, align=align) + + @openeo_process + def round(self, p=UNSET) -> ProcessBuilder: + """ + Round to a specified precision + + :param self: A number to round. + :param p: A positive number specifies the number of digits after the decimal point to round to. A + negative number means rounding to a power of ten, so for example *-2* rounds to the nearest hundred. + Defaults to *0*. + + :return: The rounded number. + """ + return round(x=self, p=p) + + @openeo_process + def run_udf(self, udf, runtime, version=UNSET, context=UNSET) -> ProcessBuilder: + """ + Run a UDF + + :param self: The data to be passed to the UDF. + :param udf: Either source code, an absolute URL or a path to a UDF script. + :param runtime: A UDF runtime identifier available at the back-end. + :param version: An UDF runtime version. If set to `null`, the default runtime version specified for + each runtime is used. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can be of any data type and is exactly what + the UDF code returns. + """ + return run_udf(data=self, udf=udf, runtime=runtime, version=version, context=context) + + @openeo_process + def run_udf_externally(self, url, context=UNSET) -> ProcessBuilder: + """ + Run an externally hosted UDF container + + :param self: The data to be passed to the UDF. + :param url: Absolute URL to a remote UDF service. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can in principle be of any data type, but it + depends on what is returned by the UDF code. Please see the implemented UDF interface for details. + """ + return run_udf_externally(data=self, url=url, context=context) + + @openeo_process + def sar_backscatter(self, coefficient=UNSET, elevation_model=UNSET, mask=UNSET, contributing_area=UNSET, local_incidence_angle=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + Computes backscatter from SAR input + + :param self: The source data cube containing SAR input. + :param coefficient: Select the radiometric correction coefficient. The following options are available: + * `beta0`: radar brightness * `sigma0-ellipsoid`: ground area computed with ellipsoid earth model * + `sigma0-terrain`: ground area computed with terrain earth model * `gamma0-ellipsoid`: ground area + computed with ellipsoid earth model in sensor line of sight * `gamma0-terrain`: ground area computed + with terrain earth model in sensor line of sight (default) * `null`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. It indicates + which values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options + will reduce portability. + + :return: Backscatter values corresponding to the chosen parametrization. The values are given in linear + scale. + """ + return sar_backscatter( + data=self, + coefficient=coefficient, + elevation_model=elevation_model, + mask=mask, + contributing_area=contributing_area, + local_incidence_angle=local_incidence_angle, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + @openeo_process + def save_result(self, format, options=UNSET) -> ProcessBuilder: + """ + Save processed data + + :param self: The data to deliver in the given file format. + :param format: The file format to use. It must be one of the values that the server reports as + supported output file formats, which usually correspond to the short GDAL/OGR codes. This parameter is + *case insensitive*. * If the data cube is empty and the file format can't store empty data cubes, a + `DataCubeEmpty` exception is thrown. * If the file format is otherwise not suitable for storing the + underlying data structure, a `FormatUnsuitable` exception is thrown. + :param options: The file format parameters to be used to create the file(s). Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: Always returns `true` as in case of an error an exception is thrown which aborts the execution + of the process. + """ + return save_result(data=self, format=format, options=options) + + @openeo_process + def sd(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Standard deviation + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sample standard deviation. + """ + return sd(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def sgn(self) -> ProcessBuilder: + """ + Signum + + :param self: A number. + + :return: The computed signum value of `x`. + """ + return sgn(x=self) + + @openeo_process + def sin(self) -> ProcessBuilder: + """ + Sine + + :param self: An angle in radians. + + :return: The computed sine of `x`. + """ + return sin(x=self) + + @openeo_process + def sinh(self) -> ProcessBuilder: + """ + Hyperbolic sine + + :param self: An angle in radians. + + :return: The computed hyperbolic sine of `x`. + """ + return sinh(x=self) + + @openeo_process + def sort(self, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Sort data + + :param self: An array with data to sort. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set + to `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The sorted array. + """ + return sort(data=self, asc=asc, nodata=nodata) + + @openeo_process + def sqrt(self) -> ProcessBuilder: + """ + Square root + + :param self: A number. + + :return: The computed square root. + """ + return sqrt(x=self) + + @openeo_process + def subtract(self, y) -> ProcessBuilder: + """ + Subtraction of two numbers + + :param self: The minuend. + :param y: The subtrahend. + + :return: The computed result. + """ + return subtract(x=self, y=y) + + @openeo_process + def sum(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the sum by adding up numbers + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sum of the sequence of numbers. + """ + return sum(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def tan(self) -> ProcessBuilder: + """ + Tangent + + :param self: An angle in radians. + + :return: The computed tangent of `x`. + """ + return tan(x=self) + + @openeo_process + def tanh(self) -> ProcessBuilder: + """ + Hyperbolic tangent + + :param self: An angle in radians. + + :return: The computed hyperbolic tangent of `x`. + """ + return tanh(x=self) + + @openeo_process + def text_begins(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text begins with another text + + :param self: Text in which to find something at the beginning. + :param pattern: Text to find at the beginning of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` begins with `pattern`, false` otherwise. + """ + return text_begins(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def text_concat(self, separator=UNSET) -> ProcessBuilder: + """ + Concatenate elements to a single text + + :param self: A set of elements. Numbers, boolean values and null values get converted to their (lower + case) string representation. For example: `1` (integer), `-1.5` (number), `true` / `false` (boolean + values) + :param separator: A separator to put between each of the individual texts. Defaults to an empty string. + + :return: A string containing a string representation of all the array elements in the same order, with + the separator between each element. + """ + return text_concat(data=self, separator=separator) + + @openeo_process + def text_contains(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text contains another text + + :param self: Text in which to find something in. + :param pattern: Text to find in `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` contains the `pattern`, false` otherwise. + """ + return text_contains(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def text_ends(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text ends with another text + + :param self: Text in which to find something at the end. + :param pattern: Text to find at the end of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` ends with `pattern`, false` otherwise. + """ + return text_ends(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def trim_cube(self) -> ProcessBuilder: + """ + Remove dimension labels with no-data values + + :param self: A data cube to trim. + + :return: A trimmed data cube with the same dimensions. The dimension properties name, type, reference + system and resolution remain unchanged. The number of dimension labels may decrease. + """ + return trim_cube(data=self) + + @openeo_process + def unflatten_dimension(self, dimension, target_dimensions, label_separator=UNSET) -> ProcessBuilder: + """ + Split a single dimensions into multiple dimensions + + :param self: A data cube that is consistently structured so that operation can execute flawlessly (e.g. + the dimension labels need to contain the `label_separator` exactly 1 time for two target dimensions, 2 + times for three target dimensions etc.). + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the new target dimensions. New dimensions will be created with + the given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` + exception if any of the dimensions exists. The order of the array defines the order in which the + dimensions and dimension labels are added to the data cube (see the example in the process + description). + :param label_separator: The string that will be used as a separator to split the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system + and resolution) for all other dimensions remain unchanged. + """ + return unflatten_dimension(data=self, dimension=dimension, target_dimensions=target_dimensions, label_separator=label_separator) + + @openeo_process + def variance(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Variance + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sample variance. + """ + return variance(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def vector_buffer(self, distance) -> ProcessBuilder: + """ + Buffer geometries by distance + + :param self: Geometries to apply the buffer on. Feature properties are preserved. + :param distance: The distance of the buffer in meters. A positive distance expands the geometries, + resulting in outward buffering (dilation), while a negative distance shrinks the geometries, resulting + in inward buffering (erosion). If the unit of the spatial reference system is not meters, a + `UnitMismatch` error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable + spatial reference system. + + :return: Returns a vector data cube with the computed new geometries of which some may be empty. + """ + return vector_buffer(geometries=self, distance=distance) + + @openeo_process + def vector_reproject(self, projection, dimension=UNSET) -> ProcessBuilder: + """ + Reprojects the geometry dimension + + :param self: A vector data cube. + :param projection: Coordinate reference system to reproject to. Specified as an [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). + :param dimension: The name of the geometry dimension to reproject. If no specific dimension is + specified, the filter applies to all geometry dimensions. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + + :return: A vector data cube with geometries projected to the new coordinate reference system. The + reference system of the geometry dimension changes, all other dimensions and properties remain + unchanged. + """ + return vector_reproject(data=self, projection=projection, dimension=dimension) + + @openeo_process + def vector_to_random_points(self, geometry_count=UNSET, total_count=UNSET, group=UNSET, seed=UNSET) -> ProcessBuilder: + """ + Sample random points from geometries + + :param self: Input geometries for sample extraction. + :param geometry_count: The maximum number of points to compute per geometry. Points in the input + geometries can be selected only once by the sampling. + :param total_count: The maximum number of points to compute overall. Throws a `CountMismatch` + exception if the specified value is less than the provided number of geometries. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a + `MultiPoint` per geometry given which keeps the original identifier if present. * Otherwise, each + sampled point is generated as a distinct `Point` geometry without identifier. + :param seed: A randomization seed to use for random sampling. If not given or `null`, no seed is used + and results may differ on subsequent use. + + :return: Returns a vector data cube with the sampled points. + """ + return vector_to_random_points(data=self, geometry_count=geometry_count, total_count=total_count, group=group, seed=seed) + + @openeo_process + def vector_to_regular_points(self, distance, group=UNSET) -> ProcessBuilder: + """ + Sample regular points from geometries + + :param self: Input geometries for sample extraction. + :param distance: Defines the minimum distance in meters that is required between two samples generated + *inside* a single geometry. If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. - For **polygons**, the distance defines the cell sizes of a regular grid that starts at the + upper-left bound of each polygon. The centroid of each cell is then a sample point. If the centroid is + not enclosed in the polygon, no point is sampled. If no point can be sampled for the geometry at all, + the first coordinate of the geometry is returned as point. - For **lines** (line strings), the sampling + starts with a point at the first coordinate of the line and then walks along the line and samples a new + point each time the distance to the previous point has been reached again. - For **points**, the point + is returned as given. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a + `MultiPoint` per geometry given which keeps the original identifier if present. * Otherwise, each + sampled point is generated as a distinct `Point` geometry without identifier. + + :return: Returns a vector data cube with the sampled points. + """ + return vector_to_regular_points(data=self, distance=distance, group=group) + + @openeo_process + def xor(self, y) -> ProcessBuilder: + """ + Logical XOR (exclusive or) + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical XOR. + """ + return xor(x=self, y=y)
+ + + +# Public shortcut +process = ProcessBuilder.process +# Private shortcut that has lower chance to collide with a process argument named `process` +_process = ProcessBuilder.process + + +
+[docs] +@openeo_process +def absolute(x) -> ProcessBuilder: + """ + Absolute value + + :param x: A number. + + :return: The computed absolute value. + """ + return _process('absolute', x=x)
+ + + +
+[docs] +@openeo_process +def add(x, y) -> ProcessBuilder: + """ + Addition of two numbers + + :param x: The first summand. + :param y: The second summand. + + :return: The computed sum of the two numbers. + """ + return _process('add', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def add_dimension(data, name, label, type=UNSET) -> ProcessBuilder: + """ + Add a new dimension + + :param data: A data cube to add the dimension to. + :param name: Name for the dimension. + :param label: A dimension label. + :param type: The type of dimension, defaults to `other`. + + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. All + other dimensions remain unchanged. + """ + return _process('add_dimension', data=data, name=name, label=label, type=type)
+ + + +
+[docs] +@openeo_process +def aggregate_spatial(data, geometries, reducer, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for geometries + + :param data: A raster data cube with at least two spatial dimensions. The data cube implicitly gets + restricted to the bounds of the geometries as if ``filter_spatial()`` would have been used with the same + values for the corresponding parameters immediately before this process. + :param geometries: Geometries for which the aggregation will be computed. Feature properties are preserved + for vector data cubes and all GeoJSON Features. One value will be computed per label in the dimension of + type `geometries`, GeoJSON `Feature` or `Geometry`. For a `FeatureCollection` multiple values will be + computed, one value per contained `Feature`. No values will be computed for empty geometries. For example, + a single value will be computed for a `MultiPolygon`, but two values will be computed for a + `FeatureCollection` containing two polygons. - For **polygons**, the process considers all pixels for + which the point at the pixel center intersects with the corresponding polygon (as defined in the Simple + Features standard by the OGC). - For **points**, the process considers the closest pixel center. - For + **lines** (line strings), the process considers all the pixels whose centers are closest to at least one + point on the line. Thus, pixels may be part of multiple geometries and be part of multiple aggregations. + No operation is applied to geometries that are outside of the bounds of the data. + :param reducer: A reducer to be applied on all values of each geometry. A reducer is a single process such + as ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param target_dimension: By default (which is `null`), the process only computes the results and doesn't + add a new dimension. If this parameter contains a new dimension name, the computation also stores + information about the total count of pixels (valid + invalid pixels) and the number of valid pixels (see + ``is_valid()``) for each computed value. These values are added as a new dimension. The new dimension of + type `other` has the dimension labels `value`, `total_count` and `valid_count`. Fails with a + `TargetDimensionExists` exception if a dimension with the specified name exists. + :param context: Additional data to be passed to the reducer. + + :return: A vector data cube with the computed results. Empty geometries still exist but without any + aggregated values (i.e. no-data). The spatial dimensions are replaced by a dimension of type 'geometries' + and if `target_dimension` is not `null`, a new dimension is added. + """ + return _process('aggregate_spatial', + data=data, + geometries=geometries, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + target_dimension=target_dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def aggregate_spatial_window(data, reducer, size, boundary=UNSET, align=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for rectangular windows + + :param data: A raster data cube with exactly two horizontal spatial dimensions and an arbitrary number of + additional dimensions. The process is applied to all additional dimensions individually. + :param reducer: A reducer to be applied on the list of values, which contain all pixels covered by the + window. A reducer is a single process such as ``mean()`` or a set of processes, which computes a single + value for a list of values, see the category 'reducer' for such processes. + :param size: Window size in pixels along the horizontal spatial dimensions. The first value corresponds to + the `x` axis, the second value corresponds to the `y` axis. + :param boundary: Behavior to apply if the number of values for the axes `x` and `y` is not a multiple of + the corresponding value in the `size` parameter. Options are: - `pad` (default): pad the data cube with + the no-data value `null` to fit the required window size. - `trim`: trim the data cube to fit the required + window size. Set the parameter `align` to specifies to which corner the data is aligned to. + :param align: If the data requires padding or trimming (see parameter `boundary`), specifies to which + corner of the spatial extent the data is aligned to. For example, if the data is aligned to the upper left, + the process pads/trims at the lower-right. + :param context: Additional data to be passed to the reducer. + + :return: A raster data cube with the newly computed values and the same dimensions. The resolution will + change depending on the chosen values for the `size` and `boundary` parameter. It usually decreases for the + dimensions which have the corresponding parameter `size` set to values greater than 1. The dimension + labels will be set to the coordinate at the center of the window. The other dimension properties (name, + type and reference system) remain unchanged. + """ + return _process('aggregate_spatial_window', + data=data, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + size=size, + boundary=boundary, + align=align, + context=context + )
+ + + +
+[docs] +@openeo_process +def aggregate_temporal(data, intervals, reducer, labels=UNSET, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations + + :param data: A data cube. + :param intervals: Left-closed temporal intervals, which are allowed to overlap. Each temporal interval in + the array has exactly two elements: 1. The first element is the start of the temporal interval. The + specified time instant is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified time instant is **excluded** from the interval. The second element must always be + greater/later than the first element, except when using time without date. Otherwise, a + `TemporalExtentEmpty` exception is thrown. + :param reducer: A reducer to be applied for the values contained in each interval. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, see + the category 'reducer' for such processes. Intervals may not contain any values, which for most reducers + leads to no-data (`null`) values by default. + :param labels: Distinct labels for the intervals, which can contain dates and/or times. Is only required to + be specified if the values for the start of the temporal intervals are not distinct and thus the default + labels would not be unique. The number of labels and the number of groups need to be equal. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the given + temporal dimension. + """ + return _process('aggregate_temporal', + data=data, + intervals=intervals, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + labels=labels, + dimension=dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def aggregate_temporal_period(data, period, reducer, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations based on calendar hierarchies + + :param data: The source data cube. + :param period: The time intervals to aggregate. The following pre-defined values are available: * `hour`: + Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten day periods, counted per + year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month + can range from 8 to 11 days. For example, the third dekad of a year spans from January 21 till January 31 + (11 days), the fourth dekad spans from February 1 till February 10 (10 days) and the sixth dekad spans from + February 21 till February 28 or February 29 in a leap year (8 or 9 days respectively). * `month`: Month of + the year * `season`: Three month periods of the calendar seasons (December - February, March - May, June - + August, September - November). * `tropical-season`: Six month periods of the tropical seasons (November - + April, May - October). * `year`: Proleptic years * `decade`: Ten year periods ([0-to-9 + decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the next year + ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. + :param reducer: A reducer to be applied for the values contained in each period. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, see + the category 'reducer' for such processes. Periods may not contain any values, which for most reducers + leads to no-data (`null`) values by default. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the source data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the given + temporal dimension. The specified temporal dimension has the following dimension labels (`YYYY` = four- + digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: `YYYY-MM-DD-00` - `YYYY-MM- + DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * `dekad`: `YYYY-00` - `YYYY-36` * + `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - February), `YYYY-mam` (March - May), + `YYYY-jja` (June - August), `YYYY-son` (September - November). * `tropical-season`: `YYYY-ndjfma` (November + - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * `decade`: `YYY0` * `decade-ad`: `YYY1` The + dimension labels in the new data cube are complete for the whole extent of the source data cube. For + example, if `period` is set to `day` and the source data cube has two dimension labels at the beginning of + the year (`2020-01-01`) and the end of a year (`2020-12-31`), the process returns a data cube with 365 + dimension labels (`2020-001`, `2020-002`, ..., `2020-365`). In contrast, if `period` is set to `day` and + the source data cube has just one dimension label `2020-01-05`, the process returns a data cube with just a + single dimension label (`2020-005`). + """ + return _process('aggregate_temporal_period', + data=data, + period=period, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def all(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Are all of the values true? + + :param data: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return _process('all', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def and_(x, y) -> ProcessBuilder: + """ + Logical AND + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical AND. + """ + return _process('and', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def anomaly(data, normals, period) -> ProcessBuilder: + """ + Compute anomalies + + :param data: A data cube with exactly one temporal dimension and the following dimension labels for the + given period (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: + `YYYY-MM-DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * + `dekad`: `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - + February), `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - November). * + `tropical-season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * + `decade`: `YYY0` * `decade-ad`: `YYY1` * `single-period` / `climatology-period`: Any + ``aggregate_temporal_period()`` can compute such a data cube. + :param normals: A data cube with normals, e.g. daily, monthly or yearly values computed from a process such + as ``climatological_normal()``. Must contain exactly one temporal dimension with the following dimension + labels for the given period: * `hour`: `00` - `23` * `day`: `001` - `365` * `week`: `01` - `52` * `dekad`: + `00` - `36` * `month`: `01` - `12` * `season`: `djf` (December - February), `mam` (March - May), `jja` + (June - August), `son` (September - November) * `tropical-season`: `ndjfma` (November - April), `mjjaso` + (May - October) * `year`: Four-digit year numbers * `decade`: Four-digit year numbers, the last digit being + a `0` * `decade-ad`: Four-digit year numbers, the last digit being a `1` * `single-period` / `climatology- + period`: A single dimension label with any name is expected. + :param period: Specifies the time intervals available in the normals data cube. The following options are + available: * `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten + day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The + third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 + each year. * `month`: Month of the year * `season`: Three month periods of the calendar seasons (December - + February, March - May, June - August, September - November). * `tropical-season`: Six month periods of the + tropical seasons (November - April, May - October). * `year`: Proleptic years * `decade`: Ten year periods + ([0-to-9 decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the + next year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. * `single-period` / `climatology- + period`: A single period of arbitrary length + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged. + """ + return _process('anomaly', data=data, normals=normals, period=period)
+ + + +
+[docs] +@openeo_process +def any(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Is at least one value true? + + :param data: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return _process('any', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def apply(data, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each value + + :param data: A data cube. + :param process: A process that accepts and returns a single value and is applied on each individual value + in the data cube. The process may consist of multiple sub-processes and could, for example, consist of + processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply', data=data, process=build_child_callback(process, parent_parameters=['x', 'context']), context=context)
+ + + +
+[docs] +@openeo_process +def apply_dimension(data, process, dimension, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to all values along a dimension + + :param data: A data cube. + :param process: Process to be applied on all values along the given dimension. The specified process needs + to accept an array and must return an array with at least one element. A process may consist of multiple + sub-processes. + :param dimension: The name of the source dimension to apply the process on. Fails with a + `DimensionNotAvailable` exception if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or `null` (the default) to use the source + dimension specified in the parameter `dimension`. By specifying a target dimension, the source dimension + is removed. The target dimension with the specified name and the type `other` (see ``add_dimension()``) is + created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. All dimensions stay the same, except for the + dimensions specified in corresponding parameters. There are three cases how the dimensions can change: 1. + The source dimension is the target dimension: - The (number of) dimensions remain unchanged as the + source dimension is the target dimension. - The source dimension properties name and type remain + unchanged. - The dimension labels, the reference system and the resolution are preserved only if the + number of values in the source dimension is equal to the number of values computed by the process. + Otherwise, all other dimension properties change as defined in the list below. 2. The source dimension is + not the target dimension. The target dimension exists with a single label only: - The number of + dimensions decreases by one as the source dimension is 'dropped' and the target dimension is filled with + the processed data that originates from the source dimension. - The target dimension properties name and + type remain unchanged. All other dimension properties change as defined in the list below. 3. The source + dimension is not the target dimension and the latter does not exist: - The number of dimensions remain + unchanged, but the source dimension is replaced with the target dimension. - The target dimension has + the specified name and the type other. All other dimension properties are set as defined in the list below. + Unless otherwise stated above, for the given (target) dimension the following applies: - the number of + dimension labels is equal to the number of values computed by the process, - the dimension labels are + incrementing integers starting from zero, - the resolution changes, and - the reference system is + undefined. + """ + return _process('apply_dimension', + data=data, + process=build_child_callback(process, parent_parameters=['data', 'context']), + dimension=dimension, + target_dimension=target_dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def apply_kernel(data, kernel, factor=UNSET, border=UNSET, replace_invalid=UNSET) -> ProcessBuilder: + """ + Apply a spatial convolution with a kernel + + :param data: A raster data cube. + :param kernel: Kernel as a two-dimensional array of weights. The inner level of the nested array aligns + with the `x` axis and the outer level aligns with the `y` axis. Each level of the kernel must have an + uneven number of elements, otherwise the process throws a `KernelDimensionsUneven` exception. + :param factor: A factor that is multiplied to each value after the kernel has been applied. This is + basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required + for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults to + fill the border with zeroes. The following options are available: * *numeric value* - fill with a user- + defined constant number `n`: `nnnnnn|abcdefgh|nnnnnn` (default, with `n` = 0) * `replicate` - repeat the + value from the pixel at the border: `aaaaaa|abcdefgh|hhhhhh` * `reflect` - mirror/reflect from the border: + `fedcba|abcdefgh|hgfedc` * `reflect_pixel` - mirror/reflect from the center of the pixel at the border: + `gfedcb|abcdefgh|gfedcb` * `wrap` - repeat/wrap the image: `cdefgh|abcdefgh|abcdef` + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite numerical + values with. By default, those values are replaced with zeroes. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_kernel', data=data, kernel=kernel, factor=factor, border=border, replace_invalid=replace_invalid)
+ + + +
+[docs] +@openeo_process +def apply_neighborhood(data, process, size, overlap=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to pixels in a n-dimensional neighborhood + + :param data: A raster data cube. + :param process: Process to be applied on all neighborhoods. + :param size: Neighborhood sizes along each dimension. This object maps dimension names to either a + physical measure (e.g. 100 m, 10 days) or pixels (e.g. 32 pixels). For dimensions not specified, the + default is to provide all values. Be aware that including all values from overly large dimensions may not + be processed at once. + :param overlap: Overlap of neighborhoods along each dimension to avoid border effects. By default no + overlap is provided. For instance a temporal dimension can add 1 month before and after a neighborhood. In + the spatial dimensions, this is often a number of pixels. The overlap specified is added before and after, + so an overlap of 8 pixels will add 8 pixels on both sides of the window, so 16 in total. Be aware that + large overlaps increase the need for computational resources and modifying overlapping data in subsequent + operations have no effect. + :param context: Additional data to be passed to the process. + + :return: A raster data cube with the newly computed values and the same dimensions. The dimension + properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_neighborhood', + data=data, + process=build_child_callback(process, parent_parameters=['data', 'context']), + size=size, + overlap=overlap, + context=context + )
+ + + +
+[docs] +@openeo_process +def apply_polygon(data, polygons, process, mask_value=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to segments of the data cube + + :param data: A data cube. + :param polygons: A vector data cube containing at least one polygon. The provided vector data can be one of + the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or `MultiPolygon` + geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or `MultiPolygon` + geometries. * Empty geometries are ignored. + :param process: A process that accepts and returns a single data cube and is applied on each individual sub + data cube. The process may consist of multiple sub-processes. + :param mask_value: All pixels for which the point at the pixel center **does not** intersect with the + polygon are replaced with the given value, which defaults to `null` (no data). It can provide a + distinction between no data values within the polygon and masked pixels outside of it. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_polygon', + data=data, + polygons=polygons, + process=build_child_callback(process, parent_parameters=['data', 'context']), + mask_value=mask_value, + context=context + )
+ + + +
+[docs] +@openeo_process +def arccos(x) -> ProcessBuilder: + """ + Inverse cosine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arccos', x=x)
+ + + +
+[docs] +@openeo_process +def arcosh(x) -> ProcessBuilder: + """ + Inverse hyperbolic cosine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arcosh', x=x)
+ + + +
+[docs] +@openeo_process +def arcsin(x) -> ProcessBuilder: + """ + Inverse sine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arcsin', x=x)
+ + + +
+[docs] +@openeo_process +def arctan(x) -> ProcessBuilder: + """ + Inverse tangent + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arctan', x=x)
+ + + +
+[docs] +@openeo_process +def arctan2(y, x) -> ProcessBuilder: + """ + Inverse tangent of two numbers + + :param y: A number to be used as the dividend. + :param x: A number to be used as the divisor. + + :return: The computed angle in radians. + """ + return _process('arctan2', y=y, x=x)
+ + + +
+[docs] +@openeo_process +def ard_normalized_radar_backscatter(data, elevation_model=UNSET, contributing_area=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant SAR NRB generation + + :param data: The source data cube containing SAR input. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options will + reduce portability. + + :return: Backscatter values expressed as gamma0 in linear scale. In addition to the bands + `contributing_area` and `ellipsoid_incidence_angle` that can optionally be added with corresponding + parameters, the following bands are always added to the data cube: - `mask`: A data mask that indicates + which values are valid (1), invalid (0) or contain no-data (null). - `local_incidence_angle`: A band with + DEM-based local incidence angles in degrees. The data returned is CARD4L compliant with corresponding + metadata. + """ + return _process('ard_normalized_radar_backscatter', + data=data, + elevation_model=elevation_model, + contributing_area=contributing_area, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + )
+ + + +
+[docs] +@openeo_process +def ard_surface_reflectance(data, atmospheric_correction_method, cloud_detection_method, elevation_model=UNSET, atmospheric_correction_options=UNSET, cloud_detection_options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant Surface Reflectance generation + + :param data: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances. There must be a single dimension of type `bands` available. + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. Each method supports detecting different + atmospheric disturbances such as clouds, cloud shadows, aerosols, haze, ozone and/or water vapour in + optical imagery. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + Specifying proprietary options will reduce portability. + :param cloud_detection_options: Proprietary options for the cloud detection method. Specifying proprietary + options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances for each spectral band in the source data + cube, with atmospheric disturbances like clouds and cloud shadows removed. No-data values (null) are + directly set in the bands. Depending on the methods used, several additional bands will be added to the + data cube: Data cube containing bottom of atmosphere reflectances for each spectral band in the source + data cube, with atmospheric disturbances like clouds and cloud shadows removed. Depending on the methods + used, several additional bands will be added to the data cube: - `date` (optional): Specifies per-pixel + acquisition timestamps. - `incomplete-testing` (required): Identifies pixels with a value of 1 for which + the per-pixel tests (at least saturation, cloud and cloud shadows, see CARD4L specification for details) + have not all been successfully completed. Otherwise, the value is 0. - `saturation` (required) / + `saturation_{band}` (optional): Indicates where pixels in the input spectral bands are saturated (1) or not + (0). If the saturation is given per band, the band names are `saturation_{band}` with `{band}` being the + band name from the source data cube. - `cloud`, `shadow` (both required),`aerosol`, `haze`, `ozone`, + `water_vapor` (all optional): Indicates the probability of pixels being an atmospheric disturbance such as + clouds. All bands have values between 0 (clear) and 1, which describes the probability that it is an + atmospheric disturbance. - `snow-ice` (optional): Points to a file that indicates whether a pixel is + assessed as being snow/ice (1) or not (0). All values describe the probability and must be between 0 and 1. + - `land-water` (optional): Indicates whether a pixel is assessed as being land (1) or water (0). All values + describe the probability and must be between 0 and 1. - `incidence-angle` (optional): Specifies per-pixel + incidence angles in degrees. - `azimuth` (optional): Specifies per-pixel azimuth angles in degrees. - `sun- + azimuth:` (optional): Specifies per-pixel sun azimuth angles in degrees. - `sun-elevation` (optional): + Specifies per-pixel sun elevation angles in degrees. - `terrain-shadow` (optional): Indicates with a value + of 1 whether a pixel is not directly illuminated due to terrain shadowing. Otherwise, the value is 0. - + `terrain-occlusion` (optional): Indicates with a value of 1 whether a pixel is not visible to the sensor + due to terrain occlusion during off-nadir viewing. Otherwise, the value is 0. - `terrain-illumination` + (optional): Contains coefficients used for terrain illumination correction are provided for each pixel. + The data returned is CARD4L compliant with corresponding metadata. + """ + return _process('ard_surface_reflectance', + data=data, + atmospheric_correction_method=atmospheric_correction_method, + cloud_detection_method=cloud_detection_method, + elevation_model=elevation_model, + atmospheric_correction_options=atmospheric_correction_options, + cloud_detection_options=cloud_detection_options + )
+ + + +
+[docs] +@openeo_process +def array_append(data, value, label=UNSET) -> ProcessBuilder: + """ + Append a value to an array + + :param data: An array. + :param value: Value to append to the array. + :param label: If the given array is a labeled array, a new label for the new value should be given. If not + given or `null`, the array index as string is used as the label. If in any case the label exists, a + `LabelExists` exception is thrown. + + :return: The new array with the value being appended. + """ + return _process('array_append', data=data, value=value, label=label)
+ + + +
+[docs] +@openeo_process +def array_apply(data, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each array element + + :param data: An array. + :param process: A process that accepts and returns a single value and is applied on each individual value + in the array. The process may consist of multiple sub-processes and could, for example, consist of + processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: An array with the newly computed values. The number of elements are the same as for the original + array. + """ + return _process('array_apply', + data=data, + process=build_child_callback(process, parent_parameters=['x', 'index', 'label', 'context']), + context=context + )
+ + + +
+[docs] +@openeo_process +def array_concat(array1, array2) -> ProcessBuilder: + """ + Merge two arrays + + :param array1: The first array. + :param array2: The second array. + + :return: The merged array. + """ + return _process('array_concat', array1=array1, array2=array2)
+ + + +
+[docs] +@openeo_process +def array_contains(data, value) -> ProcessBuilder: + """ + Check whether the array contains a given value + + :param data: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `false`. + + :return: `true` if the list contains the value, false` otherwise. + """ + return _process('array_contains', data=data, value=value)
+ + + +
+[docs] +@openeo_process +def array_create(data=UNSET, repeat=UNSET) -> ProcessBuilder: + """ + Create an array + + :param data: A (native) array to fill the newly created array with. Defaults to an empty array. + :param repeat: The number of times the (native) array specified in `data` is repeatedly added after each + other to the new array being created. Defaults to `1`. + + :return: The newly created array. + """ + return _process('array_create', data=data, repeat=repeat)
+ + + +
+[docs] +@openeo_process +def array_create_labeled(data, labels) -> ProcessBuilder: + """ + Create a labeled array + + :param data: An array of values to be used. + :param labels: An array of labels to be used. + + :return: The newly created labeled array. + """ + return _process('array_create_labeled', data=data, labels=labels)
+ + + +
+[docs] +@openeo_process +def array_element(data, index=UNSET, label=UNSET, return_nodata=UNSET) -> ProcessBuilder: + """ + Get an element from an array + + :param data: An array. + :param index: The zero-based index of the element to retrieve. + :param label: The label of the element to retrieve. Throws an `ArrayNotLabeled` exception, if the given + array is not a labeled array and this parameter is set. + :param return_nodata: By default this process throws an `ArrayElementNotAvailable` exception if the index + or label is invalid. If you want to return `null` instead, set this flag to `true`. + + :return: The value of the requested element. + """ + return _process('array_element', data=data, index=index, label=label, return_nodata=return_nodata)
+ + + +
+[docs] +@openeo_process +def array_filter(data, condition, context=UNSET) -> ProcessBuilder: + """ + Filter an array based on a condition + + :param data: An array. + :param condition: A condition that is evaluated against each value, index and/or label in the array. Only + the array elements for which the condition returns `true` are preserved. + :param context: Additional data to be passed to the condition. + + :return: An array filtered by the specified condition. The number of elements are less than or equal + compared to the original array. + """ + return _process('array_filter', + data=data, + condition=build_child_callback(condition, parent_parameters=['x', 'index', 'label', 'context']), + context=context + )
+ + + +
+[docs] +@openeo_process +def array_find(data, value, reverse=UNSET) -> ProcessBuilder: + """ + Get the index for a value in an array + + :param data: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `null`. + :param reverse: By default, this process finds the index of the first match. To return the index of the + last match instead, set this flag to `true`. + + :return: The index of the first element with the specified value. If no element was found, `null` is + returned. + """ + return _process('array_find', data=data, value=value, reverse=reverse)
+ + + +
+[docs] +@openeo_process +def array_find_label(data, label) -> ProcessBuilder: + """ + Get the index for a label in a labeled array + + :param data: List to find the label in. + :param label: Label to find in `data`. + + :return: The index of the element with the specified label assigned. If no such label was found, `null` is + returned. + """ + return _process('array_find_label', data=data, label=label)
+ + + +
+[docs] +@openeo_process +def array_interpolate_linear(data) -> ProcessBuilder: + """ + One-dimensional linear interpolation for arrays + + :param data: An array of numbers and no-data values. If the given array is a labeled array, the labels + must have a natural/inherent label order and the process expects the labels to be sorted accordingly. This + is the default behavior in openEO for spatial and temporal dimensions. + + :return: An array with no-data values being replaced with interpolated values. If not at least 2 numerical + values are available in the array, the array stays the same. + """ + return _process('array_interpolate_linear', data=data)
+ + + +
+[docs] +@openeo_process +def array_labels(data) -> ProcessBuilder: + """ + Get the labels for an array + + :param data: An array. + + :return: The labels or indices as array. + """ + return _process('array_labels', data=data)
+ + + +
+[docs] +@openeo_process +def array_modify(data, values, index, length=UNSET) -> ProcessBuilder: + """ + Change the content of an array (remove, insert, update) + + :param data: The array to modify. + :param values: The values to insert into the `data` array. + :param index: The index in the `data` array of the element to insert the value(s) before. If the index is + greater than the number of elements in the `data` array, the process throws an `ArrayElementNotAvailable` + exception. To insert after the last element, there are two options: 1. Use the simpler processes + ``array_append()`` to append a single value or ``array_concat()`` to append multiple values. 2. Specify the + number of elements in the array. You can retrieve the number of elements with the process ``count()``, + having the parameter `condition` set to `true`. + :param length: The number of elements in the `data` array to remove (or replace) starting from the given + index. If the array contains fewer elements, the process simply removes all elements up to the end. + + :return: An array with values added, updated or removed. + """ + return _process('array_modify', data=data, values=values, index=index, length=length)
+ + + +
+[docs] +@openeo_process +def arsinh(x) -> ProcessBuilder: + """ + Inverse hyperbolic sine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arsinh', x=x)
+ + + +
+[docs] +@openeo_process +def artanh(x) -> ProcessBuilder: + """ + Inverse hyperbolic tangent + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('artanh', x=x)
+ + + +
+[docs] +@openeo_process +def atmospheric_correction(data, method, elevation_model=UNSET, options=UNSET) -> ProcessBuilder: + """ + Apply atmospheric correction + + :param data: Data cube containing multi-spectral optical top of atmosphere reflectances to be corrected. + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce + reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param options: Proprietary options for the atmospheric correction method. Specifying proprietary options + will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances. + """ + return _process('atmospheric_correction', data=data, method=method, elevation_model=elevation_model, options=options)
+ + + +
+[docs] +@openeo_process +def between(x, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison + + :param x: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return _process('between', x=x, min=min, max=max, exclude_max=exclude_max)
+ + + +
+[docs] +@openeo_process +def ceil(x) -> ProcessBuilder: + """ + Round fractions up + + :param x: A number to round up. + + :return: The number rounded up. + """ + return _process('ceil', x=x)
+ + + +
+[docs] +@openeo_process +def climatological_normal(data, period, climatology_period=UNSET) -> ProcessBuilder: + """ + Compute climatology normals + + :param data: A data cube with exactly one temporal dimension. The data cube must span at least the temporal + interval specified in the parameter `climatology-period`. Seasonal periods may span two consecutive years, + e.g. temporal winter that includes months December, January and February. If the required months before the + actual climate period are available, the season is taken into account. If not available, the first season + is not taken into account and the seasonal mean is based on one year less than the other seasonal normals. + The incomplete season at the end of the last year is never taken into account. + :param period: The time intervals to aggregate the average value for. The following pre-defined frequencies + are supported: * `day`: Day of the year * `month`: Month of the year * `climatology-period`: The period + specified in the `climatology-period`. * `season`: Three month periods of the calendar seasons (December - + February, March - May, June - August, September - November). * `tropical-season`: Six month periods of the + tropical seasons (November - April, May - October). + :param climatology_period: The climatology period as a closed temporal interval. The first element of the + array is the first year to be fully included in the temporal interval. The second element is the last year + to be fully included in the temporal interval. The default climatology period is from 1981 until 2010 + (both inclusive) right now, but this might be updated over time to what is commonly used in climatology. If + you don't want to keep your research to be reproducible, please explicitly specify a period. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the temporal + dimension. The temporal dimension has the following dimension labels: * `day`: `001` - `365` * `month`: + `01` - `12` * `climatology-period`: `climatology-period` * `season`: `djf` (December - February), `mam` + (March - May), `jja` (June - August), `son` (September - November) * `tropical-season`: `ndjfma` (November + - April), `mjjaso` (May - October) + """ + return _process('climatological_normal', data=data, period=period, climatology_period=climatology_period)
+ + + +
+[docs] +@openeo_process +def clip(x, min, max) -> ProcessBuilder: + """ + Clip a value between a minimum and a maximum + + :param x: A number. + :param min: Minimum value. If the value is lower than this value, the process will return the value of this + parameter. + :param max: Maximum value. If the value is greater than this value, the process will return the value of + this parameter. + + :return: The value clipped to the specified range. + """ + return _process('clip', x=x, min=min, max=max)
+ + + +
+[docs] +@openeo_process +def cloud_detection(data, method, options=UNSET) -> ProcessBuilder: + """ + Create cloud masks + + :param data: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances on which to perform cloud detection. + :param method: The cloud detection method to use. To get reproducible results, you have to set a specific + method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce + reproducibility as you *may* get different results if you run the processes multiple times. + :param options: Proprietary options for the cloud detection method. Specifying proprietary options will + reduce portability. + + :return: A data cube with bands for the atmospheric disturbances. Each of the masks contains values between + 0 and 1. The data cube has the same spatial and temporal dimensions as the source data cube and a dimension + that contains a dimension label for each of the supported/considered atmospheric disturbance. + """ + return _process('cloud_detection', data=data, method=method, options=options)
+ + + +
+[docs] +@openeo_process +def constant(x) -> ProcessBuilder: + """ + Define a constant value + + :param x: The value of the constant. + + :return: The value of the constant. + """ + return _process('constant', x=x)
+ + + +
+[docs] +@openeo_process +def cos(x) -> ProcessBuilder: + """ + Cosine + + :param x: An angle in radians. + + :return: The computed cosine of `x`. + """ + return _process('cos', x=x)
+ + + +
+[docs] +@openeo_process +def cosh(x) -> ProcessBuilder: + """ + Hyperbolic cosine + + :param x: An angle in radians. + + :return: The computed hyperbolic cosine of `x`. + """ + return _process('cosh', x=x)
+ + + +
+[docs] +@openeo_process +def count(data, condition=UNSET, context=UNSET) -> ProcessBuilder: + """ + Count the number of elements + + :param data: An array with elements of any data type. + :param condition: A condition consists of one or more processes, which in the end return a boolean value. + It is evaluated against each element in the array. An element is counted only if the condition returns + `true`. Defaults to count valid elements in a list (see ``is_valid()``). Setting this parameter to boolean + `true` counts all elements in the list. `false` is not a valid value for this parameter. + :param context: Additional data to be passed to the condition. + + :return: The counted number of elements. + """ + return _process('count', data=data, condition=condition, context=context)
+ + + +
+[docs] +@openeo_process +def create_data_cube() -> ProcessBuilder: + """ + Create an empty data cube + + :return: An empty data cube with no dimensions. + """ + return _process('create_data_cube', )
+ + + +
+[docs] +@openeo_process +def cummax(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative maxima + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative maxima. + """ + return _process('cummax', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def cummin(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative minima + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative minima. + """ + return _process('cummin', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def cumproduct(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative products + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative products. + """ + return _process('cumproduct', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def cumsum(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative sums + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative sums. + """ + return _process('cumsum', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def date_between(x, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison for dates and times + + :param x: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return _process('date_between', x=x, min=min, max=max, exclude_max=exclude_max)
+ + + +
+[docs] +@openeo_process +def date_difference(date1, date2, unit=UNSET) -> ProcessBuilder: + """ + Computes the difference between two time instants + + :param date1: The base date, optionally with a time component. + :param date2: The other date, optionally with a time component. + :param unit: The unit for the returned value. The following units are available: - millisecond - second - + leap seconds are ignored in computations. - minute - hour - day - month - year + + :return: Returns the difference between date1 and date2 in the given unit (seconds by default), including a + fractional part if required. For comparison purposes this means: - If `date1` < `date2`, the returned + value is positive. - If `date1` = `date2`, the returned value is 0. - If `date1` > `date2`, the returned + value is negative. + """ + return _process('date_difference', date1=date1, date2=date2, unit=unit)
+ + + +
+[docs] +@openeo_process +def date_shift(date, value, unit) -> ProcessBuilder: + """ + Manipulates dates and times by addition or subtraction + + :param date: The date (and optionally time) to manipulate. If the given date doesn't include the time, the + process assumes that the time component is `00:00:00Z` (i.e. midnight, in UTC). The millisecond part of the + time is optional and defaults to `0` if not given. + :param value: The period of time in the unit given that is added (positive numbers) or subtracted (negative + numbers). The value `0` doesn't have any effect. + :param unit: The unit for the value given. The following pre-defined units are available: - millisecond: + Milliseconds - second: Seconds - leap seconds are ignored in computations. - minute: Minutes - hour: Hours + - day: Days - changes only the the day part of a date - week: Weeks (equivalent to 7 days) - month: Months + - year: Years Manipulations with the unit `year`, `month`, `week` or `day` do never change the time. If + any of the manipulations result in an invalid date or time, the corresponding part is rounded down to the + next valid date or time respectively. For example, adding a month to `2020-01-31` would result in + `2020-02-29`. + + :return: The manipulated date. If a time component was given in the parameter `date`, the time component is + returned with the date. + """ + return _process('date_shift', date=date, value=value, unit=unit)
+ + + +
+[docs] +@openeo_process +def dimension_labels(data, dimension) -> ProcessBuilder: + """ + Get the dimension labels + + :param data: The data cube. + :param dimension: The name of the dimension to get the labels for. + + :return: The labels as an array. + """ + return _process('dimension_labels', data=data, dimension=dimension)
+ + + +
+[docs] +@openeo_process +def divide(x, y) -> ProcessBuilder: + """ + Division of two numbers + + :param x: The dividend. + :param y: The divisor. + + :return: The computed result. + """ + return _process('divide', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def drop_dimension(data, name) -> ProcessBuilder: + """ + Remove a dimension + + :param data: The data cube to drop a dimension from. + :param name: Name of the dimension to drop. + + :return: A data cube without the specified dimension. The number of dimensions decreases by one, but the + dimension properties (name, type, labels, reference system and resolution) for all other dimensions remain + unchanged. + """ + return _process('drop_dimension', data=data, name=name)
+ + + +
+[docs] +@openeo_process +def e() -> ProcessBuilder: + """ + Euler's number (e) + + :return: The numerical value of Euler's number. + """ + return _process('e', )
+ + + +
+[docs] +@openeo_process +def eq(x, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Equal to comparison + + :param x: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a positive + non-zero number the equality of two numbers is checked against a delta value. This is especially useful to + circumvent problems with floating-point inaccuracy in machine-based computation. This option is basically + an alias for the following computation: `lte(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be disabled + by setting this parameter to `false`. + + :return: `true` if `x` is equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('eq', x=x, y=y, delta=delta, case_sensitive=case_sensitive)
+ + + +
+[docs] +@openeo_process +def exp(p) -> ProcessBuilder: + """ + Exponentiation to the base e + + :param p: The numerical exponent. + + :return: The computed value for *e* raised to the power of `p`. + """ + return _process('exp', p=p)
+ + + +
+[docs] +@openeo_process +def extrema(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum and maximum values + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that an array with two `null` values is returned if any + value is such a value. + + :return: An array containing the minimum and maximum values for the specified numbers. The first element is + the minimum, the second element is the maximum. If the input array is empty both elements are set to + `null`. + """ + return _process('extrema', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def filter_bands(data, bands=UNSET, wavelengths=UNSET) -> ProcessBuilder: + """ + Filter the bands by names + + :param data: A data cube with bands. + :param bands: A list of band names. Either the unique band name (metadata field `name` in bands) or one of + the common band names (metadata field `common_name` in bands). If the unique band name and the common name + conflict, the unique band name has a higher priority. The order of the specified array defines the order + of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the + original order. + :param wavelengths: A list of sub-lists with each sub-list consisting of two elements. The first element is + the minimum wavelength and the second element is the maximum wavelength. Wavelengths are specified in + micrometers (μm). The order of the specified array defines the order of the bands in the data cube. If + multiple bands match the wavelengths, all matched bands are included in the original order. + + :return: A data cube limited to a subset of its original bands. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the dimension of type + `bands` has less (or the same) dimension labels. + """ + return _process('filter_bands', data=data, bands=bands, wavelengths=wavelengths)
+ + + +
+[docs] +@openeo_process +def filter_bbox(data, extent) -> ProcessBuilder: + """ + Spatial filter using a bounding box + + :param data: A data cube. + :param extent: A bounding box, which may include a vertical axis (see `base` and `height`). + + :return: A data cube restricted to the bounding box. The dimensions and dimension properties (name, type, + labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less (or + the same) dimension labels. + """ + return _process('filter_bbox', data=data, extent=extent)
+ + + +
+[docs] +@openeo_process +def filter_labels(data, condition, dimension, context=UNSET) -> ProcessBuilder: + """ + Filter dimension labels based on a condition + + :param data: A data cube. + :param condition: A condition that is evaluated against each dimension label in the specified dimension. A + dimension label and the corresponding data is preserved for the given dimension, if the condition returns + `true`. + :param dimension: The name of the dimension to filter on. Fails with a `DimensionNotAvailable` exception if + the specified dimension does not exist. + :param context: Additional data to be passed to the condition. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that the given dimension has less (or the same) dimension + labels. + """ + return _process('filter_labels', + data=data, + condition=build_child_callback(condition, parent_parameters=['value', 'context']), + dimension=dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def filter_spatial(data, geometries) -> ProcessBuilder: + """ + Spatial filter raster data cubes using geometries + + :param data: A raster data cube. + :param geometries: One or more geometries used for filtering, given as GeoJSON or vector data cube. If + multiple geometries are provided, the union of them is used. Empty geometries are ignored. Limits the data + cube to the bounding box of the given geometries. No implicit masking gets applied. To mask the pixels of + the data cube use ``mask_polygon()``. + + :return: A raster data cube restricted to the specified geometries. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions + have less (or the same) dimension labels. + """ + return _process('filter_spatial', data=data, geometries=geometries)
+ + + +
+[docs] +@openeo_process +def filter_temporal(data, extent, dimension=UNSET) -> ProcessBuilder: + """ + Temporal filter based on temporal intervals + + :param data: A data cube. + :param extent: Left-closed temporal interval, i.e. an array with exactly two elements: 1. The first + element is the start of the temporal interval. The specified time instant is **included** in the interval. + 2. The second element is the end of the temporal interval. The specified time instant is **excluded** from + the interval. The second element must always be greater/later than the first element. Otherwise, a + `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by setting one of the + boundaries to `null`, but never both. + :param dimension: The name of the temporal dimension to filter on. If no specific dimension is specified, + the filter applies to all temporal dimensions. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + + :return: A data cube restricted to the specified temporal extent. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the temporal dimensions + (determined by `dimensions` parameter) may have less dimension labels. + """ + return _process('filter_temporal', data=data, extent=extent, dimension=dimension)
+ + + +
+[docs] +@openeo_process +def filter_vector(data, geometries, relation=UNSET) -> ProcessBuilder: + """ + Spatial vector filter using geometries + + :param data: A vector data cube with the candidate geometries. + :param geometries: One or more base geometries used for filtering, given as vector data cube. If multiple + base geometries are provided, the union of them is used. + :param relation: The spatial filter predicate for comparing the geometries provided through (a) + `geometries` (base geometries) and (b) `data` (candidate geometries). + + :return: A vector data cube restricted to the specified geometries. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the geometries + dimension has less (or the same) dimension labels. + """ + return _process('filter_vector', data=data, geometries=geometries, relation=relation)
+ + + +
+[docs] +@openeo_process +def first(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + First element + + :param data: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if the first value is such a + value. + + :return: The first element of the input array. + """ + return _process('first', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def fit_curve(data, parameters, function, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Curve fitting + + :param data: A labeled array, the labels correspond to the variable `y` and the values correspond to the + variable `x`. + :param parameters: Defined the number of parameters for the model function and provides an initial guess + for them. At least one parameter is required. + :param function: The model function. It must take the parameters to fit as array through the first argument + and the independent variable `x` as the second argument. It is recommended to store the model function as + a user-defined process on the back-end to be able to re-use the model function with the computed optimal + values for the parameters afterwards. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is passed to the model function. + + :return: An array with the optimal values for the parameters. + """ + return _process('fit_curve', + data=data, + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + ignore_nodata=ignore_nodata + )
+ + + +
+[docs] +@openeo_process +def flatten_dimensions(data, dimensions, target_dimension, label_separator=UNSET) -> ProcessBuilder: + """ + Combine multiple dimensions into a single dimension + + :param data: A data cube. + :param dimensions: The names of the dimension to combine. The order of the array defines the order in which + the dimension labels and values are combined (see the example in the process description). Fails with a + `DimensionNotAvailable` exception if at least one of the specified dimensions does not exist. + :param target_dimension: The name of the new target dimension. A new dimensions will be created with the + given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` exception if a + dimension with the specified name exists. + :param label_separator: The string that will be used as a separator for the concatenated dimension labels. + To unambiguously revert the dimension labels with the process ``unflatten_dimension()``, the given string + must not be contained in any of the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('flatten_dimensions', data=data, dimensions=dimensions, target_dimension=target_dimension, label_separator=label_separator)
+ + + +
+[docs] +@openeo_process +def floor(x) -> ProcessBuilder: + """ + Round fractions down + + :param x: A number to round down. + + :return: The number rounded down. + """ + return _process('floor', x=x)
+ + + +
+[docs] +@openeo_process +def gt(x, y) -> ProcessBuilder: + """ + Greater than comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly greater than `y` or `null` if any operand is `null`, otherwise `false`. + """ + return _process('gt', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def gte(x, y) -> ProcessBuilder: + """ + Greater than or equal to comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is greater than or equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('gte', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def if_(value, accept, reject=UNSET) -> ProcessBuilder: + """ + If-Then-Else conditional + + :param value: A boolean value. + :param accept: A value that is returned if the boolean value is `true`. + :param reject: A value that is returned if the boolean value is **not** `true`. Defaults to `null`. + + :return: Either the `accept` or `reject` argument depending on the given boolean value. + """ + return _process('if', value=value, accept=accept, reject=reject)
+ + + +
+[docs] +@openeo_process +def inspect(data, message=UNSET, code=UNSET, level=UNSET) -> ProcessBuilder: + """ + Add information to the logs + + :param data: Data to log. + :param message: A message to send in addition to the data. + :param code: A label to help identify one or more log entries originating from this process in the list of + all log entries. It can help to group or filter log entries and is usually not unique. + :param level: The severity level of this message, defaults to `info`. + + :return: The data as passed to the `data` parameter without any modification. + """ + return _process('inspect', data=data, message=message, code=code, level=level)
+ + + +
+[docs] +@openeo_process +def int(x) -> ProcessBuilder: + """ + Integer part of a number + + :param x: A number. + + :return: Integer part of the number. + """ + return _process('int', x=x)
+ + + +
+[docs] +@openeo_process +def is_infinite(x) -> ProcessBuilder: + """ + Value is an infinite number + + :param x: The data to check. + + :return: `true` if the data is an infinite number, otherwise `false`. + """ + return _process('is_infinite', x=x)
+ + + +
+[docs] +@openeo_process +def is_nan(x) -> ProcessBuilder: + """ + Value is not a number + + :param x: The data to check. + + :return: Returns `true` for `NaN` and all non-numeric data types, otherwise returns `false`. + """ + return _process('is_nan', x=x)
+ + + +
+[docs] +@openeo_process +def is_nodata(x) -> ProcessBuilder: + """ + Value is a no-data value + + :param x: The data to check. + + :return: `true` if the data is a no-data value, otherwise `false`. + """ + return _process('is_nodata', x=x)
+ + + +
+[docs] +@openeo_process +def is_valid(x) -> ProcessBuilder: + """ + Value is valid data + + :param x: The data to check. + + :return: `true` if the data is valid, otherwise `false`. + """ + return _process('is_valid', x=x)
+ + + +
+[docs] +@openeo_process +def last(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Last element + + :param data: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if the last value is such a value. + + :return: The last element of the input array. + """ + return _process('last', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def linear_scale_range(x, inputMin, inputMax, outputMin=UNSET, outputMax=UNSET) -> ProcessBuilder: + """ + Linear transformation between two ranges + + :param x: A number to transform. The number gets clipped to the bounds specified in `inputMin` and + `inputMax`. + :param inputMin: Minimum value the input can obtain. + :param inputMax: Maximum value the input can obtain. + :param outputMin: Minimum value of the desired output range. + :param outputMax: Maximum value of the desired output range. + + :return: The transformed number. + """ + return _process('linear_scale_range', x=x, inputMin=inputMin, inputMax=inputMax, outputMin=outputMin, outputMax=outputMax)
+ + + +
+[docs] +@openeo_process +def ln(x) -> ProcessBuilder: + """ + Natural logarithm + + :param x: A number to compute the natural logarithm for. + + :return: The computed natural logarithm. + """ + return _process('ln', x=x)
+ + + +
+[docs] +@openeo_process +def load_collection(id, spatial_extent, temporal_extent, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Load a collection + + :param id: The collection id. + :param spatial_extent: Limits the data to load from the collection to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard + by the OGC). * For vector data, the process loads the geometry into the data cube if the geometry is fully + *within* the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be + one of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. * Empty geometries are ignored. Set this parameter to `null` to + set no limit for the spatial extent. Be careful with this when loading large datasets! It is recommended to + use this parameter instead of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading + unbounded data. + :param temporal_extent: Limits the data to load from the collection to the specified left-closed temporal + interval. Applies to all temporal dimensions. The interval has to be specified as an array with exactly two + elements: 1. The first element is the start of the temporal interval. The specified time instant is + **included** in the interval. 2. The second element is the end of the temporal interval. The specified time + instant is **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit for + the temporal extent. Be careful with this when loading large datasets! It is recommended to use this + parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + :param properties: Limits the data by metadata properties to include only data in the data cube which all + given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the name of + the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value + must be a condition (user-defined process) to be evaluated against the collection metadata, see the + example. + + :return: A data cube for further processing. The dimensions and dimension properties (name, type, labels, + reference system and resolution) correspond to the collection's metadata, but the dimension labels are + restricted as specified in the parameters. + """ + return _process('load_collection', id=id, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties)
+ + + +
+[docs] +@openeo_process +def load_geojson(data, properties=UNSET) -> ProcessBuilder: + """ + Converts GeoJSON into a vector data cube + + :param data: A GeoJSON object to convert into a vector data cube. The GeoJSON type `GeometryCollection` is + not supported. Each geometry in the GeoJSON data results in a dimension label in the `geometries` + dimension. + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. A + new dimension with the name `properties` and type `other` is created if at least one property is provided. + Only applies for GeoJSON Features and FeatureCollections. Missing values are generally set to no-data + (`null`). Depending on the number of properties provided, the process creates the dimension differently: + - Single property with scalar values: A single dimension label with the name of the property and a single + value per geometry. - Single property of type array: The dimension labels correspond to the array indices. + There are as many values and labels per geometry as there are for the largest array. - Multiple properties + with scalar values: The dimension labels correspond to the property names. There are as many values and + labels per geometry as there are properties provided here. + + :return: A vector data cube containing the geometries, either one or two dimensional. + """ + return _process('load_geojson', data=data, properties=properties)
+ + + +
+[docs] +@openeo_process +def load_ml_model(id) -> ProcessBuilder: + """ + Load a ML model + + :param id: The STAC Item to load the machine learning model from. The STAC Item must implement the `ml- + model` extension. + + :return: A machine learning model to be used with machine learning processes such as + ``predict_random_forest()``. + """ + return _process('load_ml_model', id=id)
+ + + +
+[docs] +@openeo_process +def load_result(id, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET) -> ProcessBuilder: + """ + Load batch job results + + :param id: The id of a batch job with results. + :param spatial_extent: Limits the data to load from the batch job result to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard + by the OGC). * For vector data, the process loads the geometry into the data cube of the geometry is fully + within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be + one of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. Set this parameter to `null` to set no limit for the spatial + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load from the batch job result to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array with + exactly two elements: 1. The first element is the start of the temporal interval. The specified instance + in time is **included** in the interval. 2. The second element is the end of the temporal interval. The + specified instance in time is **excluded** from the interval. The specified temporal strings follow [RFC + 3339](https://www.rfc-editor.org/rfc/rfc3339.html). Also supports open intervals by setting one of the + boundaries to `null`, but never both. Set this parameter to `null` to set no limit for the temporal + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :return: A data cube for further processing. + """ + return _process('load_result', id=id, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands)
+ + + +
+[docs] +@openeo_process +def load_stac(url, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Loads data from STAC + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a specific + STAC API Collection that allows to filter items and to download assets. This includes batch job results, + which itself are compliant to STAC. For external URLs, authentication details such as API keys or tokens + may need to be included in the URL. Batch job results can be specified in two ways: - For Batch job + results at the same back-end, a URL pointing to the corresponding batch job results endpoint should be + provided. The URL usually ends with `/jobs/{id}/results` and `{id}` is the corresponding batch job ID. - + For external results, a signed URL must be provided. Not all back-ends support signed URLs, which are + provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: Limits the data to load to the specified bounding box or polygons. * For raster + data, the process loads the pixel into the data cube if the point at the pixel center intersects with the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). * For vector + data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or + any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be + in the data cube if no spatial extent has been provided. The GeoJSON can be one of the following feature + types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or `MultiPolygon` + geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or `MultiPolygon` + geometries. Set this parameter to `null` to set no limit for the spatial extent. Be careful with this when + loading large datasets! It is recommended to use this parameter instead of using ``filter_bbox()`` or + ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load to the specified left-closed temporal interval. Applies to + all temporal dimensions. The interval has to be specified as an array with exactly two elements: 1. The + first element is the start of the temporal interval. The specified instance in time is **included** in the + interval. 2. The second element is the end of the temporal interval. The specified instance in time is + **excluded** from the interval. The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports open intervals by setting one of the + boundaries to `null`, but never both. Set this parameter to `null` to set no limit for the temporal + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + :param properties: Limits the data by metadata properties to include only data in the data cube which all + given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the name of + the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value + must be a condition (user-defined process) to be evaluated against a STAC API. This parameter is not + supported for static STAC. + + :return: A data cube for further processing. + """ + return _process('load_stac', url=url, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties)
+ + + +
+[docs] +@openeo_process +def load_uploaded_files(paths, format, options=UNSET) -> ProcessBuilder: + """ + Load files from the user workspace + + :param paths: The files to read. Folders can't be specified, specify all files instead. An exception is + thrown if a file can't be read. + :param format: The file format to read from. It must be one of the values that the server reports as + supported input file formats, which usually correspond to the short GDAL/OGR codes. If the format is not + suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter is *case + insensitive*. + :param options: The file format parameters to be used to read the files. Must correspond to the parameters + that the server reports as supported parameters for the chosen `format`. The parameter names and valid + values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return _process('load_uploaded_files', paths=paths, format=format, options=options)
+ + + +
+[docs] +@openeo_process +def load_url(url, format, options=UNSET) -> ProcessBuilder: + """ + Load data from a URL + + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included + in the URL. + :param format: The file format to use when loading the data. It must be one of the values that the server + reports as supported input file formats, which usually correspond to the short GDAL/OGR codes. If the + format is not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter + is *case insensitive*. + :param options: The file format parameters to use when reading the data. Must correspond to the parameters + that the server reports as supported parameters for the chosen `format`. The parameter names and valid + values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return _process('load_url', url=url, format=format, options=options)
+ + + +
+[docs] +@openeo_process +def log(x, base) -> ProcessBuilder: + """ + Logarithm to a base + + :param x: A number to compute the logarithm for. + :param base: The numerical base. + + :return: The computed logarithm. + """ + return _process('log', x=x, base=base)
+ + + +
+[docs] +@openeo_process +def lt(x, y) -> ProcessBuilder: + """ + Less than comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly less than `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('lt', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def lte(x, y) -> ProcessBuilder: + """ + Less than or equal to comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is less than or equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('lte', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def mask(data, mask, replacement=UNSET) -> ProcessBuilder: + """ + Apply a raster mask + + :param data: A raster data cube. + :param mask: A mask as a raster data cube. Every pixel in `data` must have a corresponding element in + `mask`. + :param replacement: The value used to replace masked values with. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged. + """ + return _process('mask', data=data, mask=mask, replacement=replacement)
+ + + +
+[docs] +@openeo_process +def mask_polygon(data, mask, replacement=UNSET, inside=UNSET) -> ProcessBuilder: + """ + Apply a polygon mask + + :param data: A raster data cube. + :param mask: A GeoJSON object or a vector data cube containing at least one polygon. The provided vector + data can be one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` + or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or + `MultiPolygon` geometries. * Empty geometries are ignored. + :param replacement: The value used to replace masked values with. + :param inside: If set to `true` all pixels for which the point at the pixel center **does** intersect with + any polygon are replaced. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged. + """ + return _process('mask_polygon', data=data, mask=mask, replacement=replacement, inside=inside)
+ + + +
+[docs] +@openeo_process +def max(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Maximum value + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The maximum value. + """ + return _process('max', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def mean(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Arithmetic mean (average) + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed arithmetic mean. + """ + return _process('mean', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def median(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Statistical median + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed statistical median. + """ + return _process('median', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def merge_cubes(cube1, cube2, overlap_resolver=UNSET, context=UNSET) -> ProcessBuilder: + """ + Merge two data cubes + + :param cube1: The base data cube. + :param cube2: The other data cube to be merged with the base data cube. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The reducer + must return a value of the same data type as the input values are. The reduction operator may be a single + process such as ``multiply()`` or consist of multiple sub-processes. `null` (the default) can be specified + if no overlap resolver is required. + :param context: Additional data to be passed to the overlap resolver. + + :return: The merged data cube. See the process description for details regarding the dimensions and + dimension properties (name, type, labels, reference system and resolution). + """ + return _process('merge_cubes', + cube1=cube1, + cube2=cube2, + overlap_resolver=(build_child_callback(overlap_resolver, parent_parameters=['x', 'y', 'context']) if overlap_resolver not in [None, UNSET] else overlap_resolver), + context=context + )
+ + + +
+[docs] +@openeo_process +def min(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum value + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The minimum value. + """ + return _process('min', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def mod(x, y) -> ProcessBuilder: + """ + Modulo + + :param x: A number to be used as the dividend. + :param y: A number to be used as the divisor. + + :return: The remainder after division. + """ + return _process('mod', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def multiply(x, y) -> ProcessBuilder: + """ + Multiplication of two numbers + + :param x: The multiplier. + :param y: The multiplicand. + + :return: The computed product of the two numbers. + """ + return _process('multiply', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def nan() -> ProcessBuilder: + """ + Not a Number (NaN) + + :return: Returns `NaN`. + """ + return _process('nan', )
+ + + +
+[docs] +@openeo_process +def ndvi(data, nir=UNSET, red=UNSET, target_band=UNSET) -> ProcessBuilder: + """ + Normalized Difference Vegetation Index + + :param data: A raster data cube with two bands that have the common names `red` and `nir` assigned. + :param nir: The name of the NIR band. Defaults to the band that has the common name `nir` assigned. Either + the unique band name (metadata field `name` in bands) or one of the common band names (metadata field + `common_name` in bands) can be specified. If the unique band name and the common name conflict, the unique + band name has a higher priority. + :param red: The name of the red band. Defaults to the band that has the common name `red` assigned. Either + the unique band name (metadata field `name` in bands) or one of the common band names (metadata field + `common_name` in bands) can be specified. If the unique band name and the common name conflict, the unique + band name has a higher priority. + :param target_band: By default, the dimension of type `bands` is dropped. To keep the dimension specify a + new band name in this parameter so that a new dimension label with the specified name will be added for the + computed values. + + :return: A raster data cube containing the computed NDVI values. The structure of the data cube differs + depending on the value passed to `target_band`: * `target_band` is `null`: The data cube does not contain + the dimension of type `bands`, the number of dimensions decreases by one. The dimension properties (name, + type, labels, reference system and resolution) for all other dimensions remain unchanged. * `target_band` + is a string: The data cube keeps the same dimensions. The dimension properties remain unchanged, but the + number of dimension labels for the dimension of type `bands` increases by one. The additional label is + named as specified in `target_band`. + """ + return _process('ndvi', data=data, nir=nir, red=red, target_band=target_band)
+ + + +
+[docs] +@openeo_process +def neq(x, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Not equal to comparison + + :param x: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a positive + non-zero number the non-equality of two numbers is checked against a delta value. This is especially useful + to circumvent problems with floating-point inaccuracy in machine-based computation. This option is + basically an alias for the following computation: `gt(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be disabled + by setting this parameter to `false`. + + :return: `true` if `x` is *not* equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('neq', x=x, y=y, delta=delta, case_sensitive=case_sensitive)
+ + + +
+[docs] +@openeo_process +def normalized_difference(x, y) -> ProcessBuilder: + """ + Normalized difference + + :param x: The value for the first band. + :param y: The value for the second band. + + :return: The computed normalized difference. + """ + return _process('normalized_difference', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def not_(x) -> ProcessBuilder: + """ + Inverting a boolean + + :param x: Boolean value to invert. + + :return: Inverted boolean value. + """ + return _process('not', x=x)
+ + + +
+[docs] +@openeo_process +def or_(x, y) -> ProcessBuilder: + """ + Logical OR + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical OR. + """ + return _process('or', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def order(data, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Get the order of array elements + + :param data: An array to compute the order for. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set to + `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The computed permutation. + """ + return _process('order', data=data, asc=asc, nodata=nodata)
+ + + +
+[docs] +@openeo_process +def pi() -> ProcessBuilder: + """ + Pi (π) + + :return: The numerical value of Pi. + """ + return _process('pi', )
+ + + +
+[docs] +@openeo_process +def power(base, p) -> ProcessBuilder: + """ + Exponentiation + + :param base: The numerical base. + :param p: The numerical exponent. + + :return: The computed value for `base` raised to the power of `p`. + """ + return _process('power', base=base, p=p)
+ + + +
+[docs] +@openeo_process +def predict_curve(parameters, function, dimension, labels=UNSET) -> ProcessBuilder: + """ + Predict values + + :param parameters: A data cube with optimal values, e.g. computed by the process ``fit_curve()``. + :param function: The model function. It must take the parameters to fit as array through the first argument + and the independent variable `x` as the second argument. It is recommended to store the model function as + a user-defined process on the back-end. + :param dimension: The name of the dimension for predictions. + :param labels: The labels to predict values for. If no labels are given, predicts values only for no-data + (`null`) values in the data cube. + + :return: A data cube with the predicted values with the provided dimension `dimension` having as many + labels as provided through `labels`. + """ + return _process('predict_curve', + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + dimension=dimension, + labels=labels + )
+ + + +
+[docs] +@openeo_process +def predict_random_forest(data, model) -> ProcessBuilder: + """ + Predict values based on a Random Forest model + + :param data: An array of numbers. + :param model: A model object that can be trained with the processes ``fit_regr_random_forest()`` + (regression) and ``fit_class_random_forest()`` (classification). + + :return: The predicted value. Returns `null` if any of the given values in the array is a no-data value. + """ + return _process('predict_random_forest', data=data, model=model)
+ + + +
+[docs] +@openeo_process +def product(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the product by multiplying numbers + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed product of the sequence of numbers. + """ + return _process('product', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def quantiles(data, probabilities=UNSET, q=UNSET, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Quantiles + + :param data: An array of numbers. + :param probabilities: Quantiles to calculate. Either a list of probabilities or the number of intervals: * + Provide an array with a sorted list of probabilities in ascending order to calculate quantiles for. The + probabilities must be between 0 and 1 (inclusive). If not sorted in ascending order, an + `AscendingProbabilitiesRequired` exception is thrown. * Provide an integer to specify the number of + intervals to calculate quantiles for. Calculates q-quantiles with equal-sized intervals. + :param q: Number of intervals to calculate quantiles for. Calculates q-quantiles with equal-sized + intervals. This parameter has been **deprecated**. Please use the parameter `probabilities` instead. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that an array with `null` values is returned if any + element is such a value. + + :return: An array with the computed quantiles. The list has either * as many elements as the given list of + `probabilities` had or * *`q`-1* elements. If the input array is empty the resulting array is filled with + as many `null` values as required according to the list above. See the 'Empty array' example for an + example. + """ + return _process('quantiles', data=data, probabilities=probabilities, q=q, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def rearrange(data, order) -> ProcessBuilder: + """ + Sort an array based on a permutation + + :param data: The array to rearrange. + :param order: The permutation used for rearranging. + + :return: The rearranged array. + """ + return _process('rearrange', data=data, order=order)
+ + + +
+[docs] +@openeo_process +def reduce_dimension(data, reducer, dimension, context=UNSET) -> ProcessBuilder: + """ + Reduce dimensions + + :param data: A data cube. + :param reducer: A reducer to apply on the specified dimension. A reducer is a single process such as + ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param dimension: The name of the dimension over which to reduce. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the given dimension, the number of + dimensions decreases by one. The dimension properties (name, type, labels, reference system and resolution) + for all other dimensions remain unchanged. + """ + return _process('reduce_dimension', + data=data, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + )
+ + + +
+[docs] +@openeo_process +def reduce_spatial(data, reducer, context=UNSET) -> ProcessBuilder: + """ + Reduce spatial dimensions 'x' and 'y' + + :param data: A raster data cube. + :param reducer: A reducer to apply on the horizontal spatial dimensions. A reducer is a single process such + as ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the horizontal spatial dimensions, the + number of dimensions decreases by two. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('reduce_spatial', data=data, reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), context=context)
+ + + +
+[docs] +@openeo_process +def rename_dimension(data, source, target) -> ProcessBuilder: + """ + Rename a dimension + + :param data: The data cube. + :param source: The current name of the dimension. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a `DimensionExists` exception if a dimension with + the specified name exists. + + :return: A data cube with the same dimensions, but the name of one of the dimensions changes. The old name + can not be referred to any longer. The dimension properties (name, type, labels, reference system and + resolution) remain unchanged. + """ + return _process('rename_dimension', data=data, source=source, target=target)
+ + + +
+[docs] +@openeo_process +def rename_labels(data, dimension, target, source=UNSET) -> ProcessBuilder: + """ + Rename dimension labels + + :param data: The data cube. + :param dimension: The name of the dimension to rename the labels for. + :param target: The new names for the labels. If a target dimension label already exists in the data cube, + a `LabelExists` exception is thrown. + :param source: The original names of the labels to be renamed to corresponding array elements in the + parameter `target`. It is allowed to only specify a subset of labels to rename, as long as the `target` and + `source` parameter have the same length. The order of the labels doesn't need to match the order of the + dimension labels in the data cube. By default, the array is empty so that the dimension labels in the data + cube are expected to be enumerated. If the dimension labels are not enumerated and the given array is + empty, the `LabelsNotEnumerated` exception is thrown. If one of the source dimension labels doesn't exist, + the `LabelNotAvailable` exception is thrown. + + :return: The data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that for the given dimension the labels change. The old + labels can not be referred to any longer. The number of labels remains the same. + """ + return _process('rename_labels', data=data, dimension=dimension, target=target, source=source)
+ + + +
+[docs] +@openeo_process +def resample_cube_spatial(data, target, method=UNSET) -> ProcessBuilder: + """ + Resample the spatial dimensions to match a target data cube + + :param data: A raster data cube. + :param target: A raster data cube that describes the spatial target resolution. + :param method: Resampling method to use. The following options are available and are meant to align with + [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average (mean) + resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling * `cubic`: + cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc resampling * + `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median resampling, + selects the median value of all valid pixels * `min`: minimum resampling, selects the minimum value from + all valid pixels * `mode`: mode resampling, selects the value which appears most often of all the sampled + points * `near`: nearest neighbour resampling (default) * `q1`: first quartile resampling, selects the + first quartile value of all valid pixels * `q3`: third quartile resampling, selects the third quartile + value of all valid pixels * `rms` root mean square (quadratic mean) of all valid pixels * `sum`: compute + the weighted sum of all valid pixels Valid pixels are determined based on the function ``is_valid()``. + + :return: A raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of the + spatial dimensions. + """ + return _process('resample_cube_spatial', data=data, target=target, method=method)
+ + + +
+[docs] +@openeo_process +def resample_cube_temporal(data, target, dimension=UNSET, valid_within=UNSET) -> ProcessBuilder: + """ + Resample temporal dimensions to match a target data cube + + :param data: A data cube with one or more temporal dimensions. + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample, which must exist with this name in both + data cubes. If the dimension is not set or is set to `null`, the process resamples all temporal dimensions + that exist with the same names in both data cubes. The following exceptions may occur: * A dimension is + given, but it does not exist in any of the data cubes: `DimensionNotAvailable` * A dimension is given, but + one of them is not temporal: `DimensionMismatch` * No specific dimension name is given and there are no + temporal dimensions with the same name in the data: `DimensionMismatch` + :param valid_within: Setting this parameter to a numerical value enables that the process searches for + valid values within the given period of days before and after the target timestamps. Valid values are + determined based on the function ``is_valid()``. For example, the limit of `7` for the target timestamps + `2020-01-15 12:00:00` looks for a nearest neighbor after `2020-01-08 12:00:00` and before `2020-01-22 + 12:00:00`. If no valid value is found within the given period, the value will be set to no-data (`null`). + + :return: A data cube with the same dimensions and the same dimension properties (name, type, labels, + reference system and resolution) for all non-temporal dimensions. For the temporal dimension, the name and + type remain unchanged, but the dimension labels, resolution and reference system may change. + """ + return _process('resample_cube_temporal', data=data, target=target, dimension=dimension, valid_within=valid_within)
+ + + +
+[docs] +@openeo_process +def resample_spatial(data, resolution=UNSET, projection=UNSET, method=UNSET, align=UNSET) -> ProcessBuilder: + """ + Resample and warp the spatial dimensions + + :param data: A raster data cube. + :param resolution: Resamples the data cube to the target resolution, which can be specified either as + separate values for x and y or as a single value for both axes. Specified in the units of the target + projection. Doesn't change the resolution by default (`0`). + :param projection: Warps the data cube to the target projection, specified as as [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). By default (`null`), the projection is + not changed. + :param method: Resampling method to use. The following options are available and are meant to align with + [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average (mean) + resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling * `cubic`: + cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc resampling * + `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median resampling, + selects the median value of all valid pixels * `min`: minimum resampling, selects the minimum value from + all valid pixels * `mode`: mode resampling, selects the value which appears most often of all the sampled + points * `near`: nearest neighbour resampling (default) * `q1`: first quartile resampling, selects the + first quartile value of all valid pixels * `q3`: third quartile resampling, selects the third quartile + value of all valid pixels * `rms` root mean square (quadratic mean) of all valid pixels * `sum`: compute + the weighted sum of all valid pixels Valid pixels are determined based on the function ``is_valid()``. + :param align: Specifies to which corner of the spatial extent the new resampled data is aligned to. + + :return: A raster data cube with values warped onto the new projection. It has the same dimensions and the + same dimension properties (name, type, labels, reference system and resolution) for all non-spatial or + vertical spatial dimensions. For the horizontal spatial dimensions the name and type remain unchanged, but + reference system, labels and resolution may change depending on the given parameters. + """ + return _process('resample_spatial', data=data, resolution=resolution, projection=projection, method=method, align=align)
+ + + +
+[docs] +@openeo_process +def round(x, p=UNSET) -> ProcessBuilder: + """ + Round to a specified precision + + :param x: A number to round. + :param p: A positive number specifies the number of digits after the decimal point to round to. A negative + number means rounding to a power of ten, so for example *-2* rounds to the nearest hundred. Defaults to + *0*. + + :return: The rounded number. + """ + return _process('round', x=x, p=p)
+ + + +
+[docs] +@openeo_process +def run_udf(data, udf, runtime, version=UNSET, context=UNSET) -> ProcessBuilder: + """ + Run a UDF + + :param data: The data to be passed to the UDF. + :param udf: Either source code, an absolute URL or a path to a UDF script. + :param runtime: A UDF runtime identifier available at the back-end. + :param version: An UDF runtime version. If set to `null`, the default runtime version specified for each + runtime is used. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can be of any data type and is exactly what the + UDF code returns. + """ + return _process('run_udf', data=data, udf=udf, runtime=runtime, version=version, context=context)
+ + + +
+[docs] +@openeo_process +def run_udf_externally(data, url, context=UNSET) -> ProcessBuilder: + """ + Run an externally hosted UDF container + + :param data: The data to be passed to the UDF. + :param url: Absolute URL to a remote UDF service. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can in principle be of any data type, but it + depends on what is returned by the UDF code. Please see the implemented UDF interface for details. + """ + return _process('run_udf_externally', data=data, url=url, context=context)
+ + + +
+[docs] +@openeo_process +def sar_backscatter(data, coefficient=UNSET, elevation_model=UNSET, mask=UNSET, contributing_area=UNSET, local_incidence_angle=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + Computes backscatter from SAR input + + :param data: The source data cube containing SAR input. + :param coefficient: Select the radiometric correction coefficient. The following options are available: * + `beta0`: radar brightness * `sigma0-ellipsoid`: ground area computed with ellipsoid earth model * + `sigma0-terrain`: ground area computed with terrain earth model * `gamma0-ellipsoid`: ground area computed + with ellipsoid earth model in sensor line of sight * `gamma0-terrain`: ground area computed with terrain + earth model in sensor line of sight (default) * `null`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. It indicates which + values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options will + reduce portability. + + :return: Backscatter values corresponding to the chosen parametrization. The values are given in linear + scale. + """ + return _process('sar_backscatter', + data=data, + coefficient=coefficient, + elevation_model=elevation_model, + mask=mask, + contributing_area=contributing_area, + local_incidence_angle=local_incidence_angle, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + )
+ + + +
+[docs] +@openeo_process +def save_result(data, format, options=UNSET) -> ProcessBuilder: + """ + Save processed data + + :param data: The data to deliver in the given file format. + :param format: The file format to use. It must be one of the values that the server reports as supported + output file formats, which usually correspond to the short GDAL/OGR codes. This parameter is *case + insensitive*. * If the data cube is empty and the file format can't store empty data cubes, a + `DataCubeEmpty` exception is thrown. * If the file format is otherwise not suitable for storing the + underlying data structure, a `FormatUnsuitable` exception is thrown. + :param options: The file format parameters to be used to create the file(s). Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names and + valid values usually correspond to the GDAL/OGR format options. + + :return: Always returns `true` as in case of an error an exception is thrown which aborts the execution of + the process. + """ + return _process('save_result', data=data, format=format, options=options)
+ + + +
+[docs] +@openeo_process +def sd(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Standard deviation + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sample standard deviation. + """ + return _process('sd', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def sgn(x) -> ProcessBuilder: + """ + Signum + + :param x: A number. + + :return: The computed signum value of `x`. + """ + return _process('sgn', x=x)
+ + + +
+[docs] +@openeo_process +def sin(x) -> ProcessBuilder: + """ + Sine + + :param x: An angle in radians. + + :return: The computed sine of `x`. + """ + return _process('sin', x=x)
+ + + +
+[docs] +@openeo_process +def sinh(x) -> ProcessBuilder: + """ + Hyperbolic sine + + :param x: An angle in radians. + + :return: The computed hyperbolic sine of `x`. + """ + return _process('sinh', x=x)
+ + + +
+[docs] +@openeo_process +def sort(data, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Sort data + + :param data: An array with data to sort. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set to + `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The sorted array. + """ + return _process('sort', data=data, asc=asc, nodata=nodata)
+ + + +
+[docs] +@openeo_process +def sqrt(x) -> ProcessBuilder: + """ + Square root + + :param x: A number. + + :return: The computed square root. + """ + return _process('sqrt', x=x)
+ + + +
+[docs] +@openeo_process +def subtract(x, y) -> ProcessBuilder: + """ + Subtraction of two numbers + + :param x: The minuend. + :param y: The subtrahend. + + :return: The computed result. + """ + return _process('subtract', x=x, y=y)
+ + + +
+[docs] +@openeo_process +def sum(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the sum by adding up numbers + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sum of the sequence of numbers. + """ + return _process('sum', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def tan(x) -> ProcessBuilder: + """ + Tangent + + :param x: An angle in radians. + + :return: The computed tangent of `x`. + """ + return _process('tan', x=x)
+ + + +
+[docs] +@openeo_process +def tanh(x) -> ProcessBuilder: + """ + Hyperbolic tangent + + :param x: An angle in radians. + + :return: The computed hyperbolic tangent of `x`. + """ + return _process('tanh', x=x)
+ + + +
+[docs] +@openeo_process +def text_begins(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text begins with another text + + :param data: Text in which to find something at the beginning. + :param pattern: Text to find at the beginning of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` begins with `pattern`, false` otherwise. + """ + return _process('text_begins', data=data, pattern=pattern, case_sensitive=case_sensitive)
+ + + +
+[docs] +@openeo_process +def text_concat(data, separator=UNSET) -> ProcessBuilder: + """ + Concatenate elements to a single text + + :param data: A set of elements. Numbers, boolean values and null values get converted to their (lower case) + string representation. For example: `1` (integer), `-1.5` (number), `true` / `false` (boolean values) + :param separator: A separator to put between each of the individual texts. Defaults to an empty string. + + :return: A string containing a string representation of all the array elements in the same order, with the + separator between each element. + """ + return _process('text_concat', data=data, separator=separator)
+ + + +
+[docs] +@openeo_process +def text_contains(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text contains another text + + :param data: Text in which to find something in. + :param pattern: Text to find in `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` contains the `pattern`, false` otherwise. + """ + return _process('text_contains', data=data, pattern=pattern, case_sensitive=case_sensitive)
+ + + +
+[docs] +@openeo_process +def text_ends(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text ends with another text + + :param data: Text in which to find something at the end. + :param pattern: Text to find at the end of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` ends with `pattern`, false` otherwise. + """ + return _process('text_ends', data=data, pattern=pattern, case_sensitive=case_sensitive)
+ + + +
+[docs] +@openeo_process +def trim_cube(data) -> ProcessBuilder: + """ + Remove dimension labels with no-data values + + :param data: A data cube to trim. + + :return: A trimmed data cube with the same dimensions. The dimension properties name, type, reference + system and resolution remain unchanged. The number of dimension labels may decrease. + """ + return _process('trim_cube', data=data)
+ + + +
+[docs] +@openeo_process +def unflatten_dimension(data, dimension, target_dimensions, label_separator=UNSET) -> ProcessBuilder: + """ + Split a single dimensions into multiple dimensions + + :param data: A data cube that is consistently structured so that operation can execute flawlessly (e.g. the + dimension labels need to contain the `label_separator` exactly 1 time for two target dimensions, 2 times + for three target dimensions etc.). + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the new target dimensions. New dimensions will be created with the + given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` exception if + any of the dimensions exists. The order of the array defines the order in which the dimensions and + dimension labels are added to the data cube (see the example in the process description). + :param label_separator: The string that will be used as a separator to split the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('unflatten_dimension', data=data, dimension=dimension, target_dimensions=target_dimensions, label_separator=label_separator)
+ + + +
+[docs] +@openeo_process +def variance(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Variance + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sample variance. + """ + return _process('variance', data=data, ignore_nodata=ignore_nodata)
+ + + +
+[docs] +@openeo_process +def vector_buffer(geometries, distance) -> ProcessBuilder: + """ + Buffer geometries by distance + + :param geometries: Geometries to apply the buffer on. Feature properties are preserved. + :param distance: The distance of the buffer in meters. A positive distance expands the geometries, + resulting in outward buffering (dilation), while a negative distance shrinks the geometries, resulting in + inward buffering (erosion). If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. + + :return: Returns a vector data cube with the computed new geometries of which some may be empty. + """ + return _process('vector_buffer', geometries=geometries, distance=distance)
+ + + +
+[docs] +@openeo_process +def vector_reproject(data, projection, dimension=UNSET) -> ProcessBuilder: + """ + Reprojects the geometry dimension + + :param data: A vector data cube. + :param projection: Coordinate reference system to reproject to. Specified as an [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). + :param dimension: The name of the geometry dimension to reproject. If no specific dimension is specified, + the filter applies to all geometry dimensions. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + + :return: A vector data cube with geometries projected to the new coordinate reference system. The reference + system of the geometry dimension changes, all other dimensions and properties remain unchanged. + """ + return _process('vector_reproject', data=data, projection=projection, dimension=dimension)
+ + + +
+[docs] +@openeo_process +def vector_to_random_points(data, geometry_count=UNSET, total_count=UNSET, group=UNSET, seed=UNSET) -> ProcessBuilder: + """ + Sample random points from geometries + + :param data: Input geometries for sample extraction. + :param geometry_count: The maximum number of points to compute per geometry. Points in the input + geometries can be selected only once by the sampling. + :param total_count: The maximum number of points to compute overall. Throws a `CountMismatch` exception if + the specified value is less than the provided number of geometries. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a `MultiPoint` + per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is + generated as a distinct `Point` geometry without identifier. + :param seed: A randomization seed to use for random sampling. If not given or `null`, no seed is used and + results may differ on subsequent use. + + :return: Returns a vector data cube with the sampled points. + """ + return _process('vector_to_random_points', data=data, geometry_count=geometry_count, total_count=total_count, group=group, seed=seed)
+ + + +
+[docs] +@openeo_process +def vector_to_regular_points(data, distance, group=UNSET) -> ProcessBuilder: + """ + Sample regular points from geometries + + :param data: Input geometries for sample extraction. + :param distance: Defines the minimum distance in meters that is required between two samples generated + *inside* a single geometry. If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. - For **polygons**, the distance defines the cell sizes of a regular grid that starts at the + upper-left bound of each polygon. The centroid of each cell is then a sample point. If the centroid is not + enclosed in the polygon, no point is sampled. If no point can be sampled for the geometry at all, the first + coordinate of the geometry is returned as point. - For **lines** (line strings), the sampling starts with a + point at the first coordinate of the line and then walks along the line and samples a new point each time + the distance to the previous point has been reached again. - For **points**, the point is returned as + given. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a `MultiPoint` + per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is + generated as a distinct `Point` geometry without identifier. + + :return: Returns a vector data cube with the sampled points. + """ + return _process('vector_to_regular_points', data=data, distance=distance, group=group)
+ + + +
+[docs] +@openeo_process +def xor(x, y) -> ProcessBuilder: + """ + Logical XOR (exclusive or) + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical XOR. + """ + return _process('xor', x=x, y=y)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/_datacube.html b/_modules/openeo/rest/_datacube.html new file mode 100644 index 000000000..991902568 --- /dev/null +++ b/_modules/openeo/rest/_datacube.html @@ -0,0 +1,494 @@ + + + + + + + openeo.rest._datacube — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest._datacube

+from __future__ import annotations
+
+import logging
+import pathlib
+import re
+import typing
+import uuid
+import warnings
+from typing import Dict, List, Optional, Tuple, Union
+
+import requests
+
+from openeo.internal.graph_building import FlatGraphableMixin, PGNode, _FromNodeMixin
+from openeo.internal.jupyter import render_component
+from openeo.internal.processes.builder import (
+    convert_callable_to_pgnode,
+    get_parameter_names,
+)
+from openeo.internal.warnings import UserDeprecationWarning
+from openeo.rest import OpenEoClientException
+from openeo.util import dict_no_none, str_truncate
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo.rest.connection import Connection
+
+log = logging.getLogger(__name__)
+
+# Sentinel object to refer to "current" cube in chained cube processing expressions.
+THIS = object()
+
+
+class _ProcessGraphAbstraction(_FromNodeMixin, FlatGraphableMixin):
+    """
+    Base class for client-side abstractions/wrappers
+    for structures that are represented by a openEO process graph:
+    raster data cubes, vector cubes, ML models, ...
+    """
+
+    def __init__(self, pgnode: PGNode, connection: Union[Connection, None]):
+        self._pg = pgnode
+        # TODO: now that connection can officially be None:
+        #       improve exceptions in cases where is it still assumed to be a real connection (download, create_job, ...)
+        self._connection = connection
+
+    def __str__(self):
+        return "{t}({pg})".format(t=self.__class__.__name__, pg=self._pg)
+
+    def flat_graph(self) -> Dict[str, dict]:
+        """
+        Get the process graph in internal flat dict representation.
+
+        .. warning:: This method is mainly intended for internal use.
+            It is not recommended for general use and is *subject to change*.
+
+            Instead, it is recommended to use
+            :py:meth:`to_json()` or :py:meth:`print_json()`
+            to obtain a standardized, interoperable JSON representation of the process graph.
+            See :ref:`process_graph_export` for more information.
+        """
+        # TODO: wrap in {"process_graph":...} by default/optionally?
+        return self._pg.flat_graph()
+
+    @property
+    def _api_version(self):
+        return self._connection.capabilities().api_version_check
+
+    @property
+    def connection(self) -> Connection:
+        return self._connection
+
+    def result_node(self) -> PGNode:
+        """
+        Get the current result node (:py:class:`PGNode`) of the process graph.
+
+        .. versionadded:: 0.10.1
+        """
+        return self._pg
+
+    def from_node(self):
+        # _FromNodeMixin API
+        return self._pg
+
+    def _build_pgnode(
+        self,
+        process_id: str,
+        arguments: Optional[dict] = None,
+        namespace: Optional[str] = None,
+        **kwargs
+    ) -> PGNode:
+        """
+        Helper to build a PGNode from given argument dict and/or kwargs,
+        and possibly resolving the `THIS` reference.
+        """
+        arguments = {**(arguments or {}), **kwargs}
+        for k, v in arguments.items():
+            if v is THIS:
+                arguments[k] = self
+            # TODO: also necessary to traverse lists/dictionaries?
+        return PGNode(process_id=process_id, arguments=arguments, namespace=namespace)
+
+    # TODO #278 also move process graph "execution" methods here: `download`, `execute`, `execute_batch`, `create_job`, `save_udf`,  ...
+
+    def _repr_html_(self):
+        process = {"process_graph": self.flat_graph()}
+        parameters = {
+            "id": uuid.uuid4().hex,
+            "explicit-zoom": True,
+            "height": "400px",
+        }
+        return render_component("model-builder", data=process, parameters=parameters)
+
+
+
+[docs] +class UDF: + """ + Helper class to load UDF code (e.g. from file) and embed them as "callback" or child process in a process graph. + + Usage example: + + .. code-block:: python + + udf = UDF.from_file("my-udf-code.py") + cube = cube.apply(process=udf) + + + .. versionchanged:: 0.13.0 + Added auto-detection of ``runtime``. + Specifying the ``data`` argument is not necessary anymore, and actually deprecated. + Added :py:meth:`from_file` to simplify loading UDF code from a file. + See :ref:`old_udf_api` for more background about the changes. + """ + + # TODO: eliminate dependency on `openeo.rest.connection` and move to somewhere under `openeo.internal`? + + __slots__ = ["code", "_runtime", "version", "context", "_source"] + + def __init__( + self, + code: str, + runtime: Optional[str] = None, + data=None, # TODO #181 remove `data` argument + version: Optional[str] = None, + context: Optional[dict] = None, + _source=None, + ): + """ + Construct a UDF object from given code string and other argument related to the ``run_udf`` process. + + :param code: UDF source code string (Python, R, ...) + :param runtime: optional UDF runtime identifier, will be autodetected from source code if omitted. + :param data: unused leftover from old API. Don't use this argument, it will be removed in a future release. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + :param _source: (for internal use) source identifier + """ + # TODO: automatically dedent code (when literal string) ? + self.code = code + self._runtime = runtime + self.version = version + self.context = context + self._source = _source + if data is not None: + # TODO #181 remove `data` argument + warnings.warn( + f"The `data` argument of `{self.__class__.__name__}` is deprecated, unused and will be removed in a future release.", + category=UserDeprecationWarning, + stacklevel=2, + ) + + def __repr__(self): + return f"<{type(self).__name__} runtime={self._runtime!r} code={str_truncate(self.code, width=200)!r}>" + + def get_runtime(self, connection: Optional[Connection] = None) -> str: + return self._runtime or self._guess_runtime(connection=connection) + +
+[docs] + @classmethod + def from_file( + cls, + path: Union[str, pathlib.Path], + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> UDF: + """ + Load a UDF from a local file. + + .. seealso:: + :py:meth:`from_url` for loading from a URL. + + :param path: path to the local file with UDF source code + :param runtime: optional UDF runtime identifier, will be auto-detected from source code if omitted. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + """ + path = pathlib.Path(path) + code = path.read_text(encoding="utf-8") + return cls( + code=code, runtime=runtime, version=version, context=context, _source=path + )
+ + +
+[docs] + @classmethod + def from_url( + cls, + url: str, + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> UDF: + """ + Load a UDF from a URL. + + .. seealso:: + :py:meth:`from_file` for loading from a local file. + + :param url: URL path to load the UDF source code from + :param runtime: optional UDF runtime identifier, will be auto-detected from source code if omitted. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + """ + resp = requests.get(url) + resp.raise_for_status() + code = resp.text + return cls( + code=code, runtime=runtime, version=version, context=context, _source=url + )
+ + + def _guess_runtime(self, connection: Optional[Connection] = None) -> str: + """Guess UDF runtime from UDF source (path) or source code.""" + # First, guess UDF language + language = None + if isinstance(self._source, pathlib.Path): + language = self._guess_runtime_from_suffix(self._source.suffix) + elif isinstance(self._source, str): + url_match = re.match( + r"https?://.*?(?P<suffix>\.\w+)([&#].*)?$", self._source + ) + if url_match: + language = self._guess_runtime_from_suffix(url_match.group("suffix")) + if not language: + # Guess language from UDF code + if re.search(r"^def [\w0-9_]+\(", self.code, flags=re.MULTILINE): + language = "Python" + # TODO: detection heuristics for R and other languages? + if not language: + raise OpenEoClientException("Failed to detect language of UDF code.") + runtime = language + if connection: + # Some additional best-effort validation/normalization of the runtime + # TODO: this just does some case-normalization, just drop that all together to eliminate + # the dependency on a connection object. See https://github.com/Open-EO/openeo-api/issues/510 + runtimes = {k.lower(): k for k in connection.list_udf_runtimes().keys()} + runtime = runtimes.get(runtime.lower(), runtime) + return runtime + + def _guess_runtime_from_suffix(self, suffix: str) -> Union[str]: + return { + ".py": "Python", + ".r": "R", + }.get(suffix.lower()) + +
+[docs] + def get_run_udf_callback(self, connection: Optional[Connection] = None, data_parameter: str = "data") -> PGNode: + """ + For internal use: construct `run_udf` node to be used as callback in `apply`, `reduce_dimension`, ... + """ + arguments = dict_no_none( + data={"from_parameter": data_parameter}, + udf=self.code, + runtime=self.get_runtime(connection=connection), + version=self.version, + context=self.context, + ) + return PGNode(process_id="run_udf", arguments=arguments)
+
+ + + +def build_child_callback( + process: Union[str, PGNode, typing.Callable, UDF], + parent_parameters: List[str], + connection: Optional[Connection] = None, +) -> dict: + """ + Build a "callback" process: a user defined process that is used by another process (such + as `apply`, `apply_dimension`, `reduce`, ....) + + :param process: process id string, PGNode or callable that uses the ProcessBuilder mechanism to build a process + :param parent_parameters: list of parameter names defined for child process + :param connection: optional connection object to improve runtime validation for UDFs + :return: + """ + # TODO: move this to more generic process graph building utility module + # TODO: autodetect the parameters defined by parent process? + # TODO: eliminate need for connection object (also see `UDF._guess_runtime`) + # TODO: when `openeo.rest` deps are gone: move this helper to somewhere under `openeo.internal` + if isinstance(process, PGNode): + # Assume this is already a valid callback process + pg = process + elif isinstance(process, str): + # Assume given reducer is a simple predefined reduce process_id + # TODO: avoid local import (workaround for circular import issue) + import openeo.processes + if process in openeo.processes.__dict__: + process_params = get_parameter_names(openeo.processes.__dict__[process]) + # TODO: switch to "Callable" handling here + else: + # Best effort guess + process_params = parent_parameters + if parent_parameters == ["x", "y"] and (len(process_params) == 1 or process_params[:1] == ["data"]): + # Special case: wrap all parent parameters in an array + arguments = {process_params[0]: [{"from_parameter": p} for p in parent_parameters]} + else: + # Only pass parameters that correspond with an arg name + common = set(process_params).intersection(parent_parameters) + arguments = {p: {"from_parameter": p} for p in common} + pg = PGNode(process_id=process, arguments=arguments) + elif isinstance(process, typing.Callable): + pg = convert_callable_to_pgnode(process, parent_parameters=parent_parameters) + elif isinstance(process, UDF): + pg = process.get_run_udf_callback(connection=connection, data_parameter=parent_parameters[0]) + elif isinstance(process, dict) and isinstance(process.get("process_graph"), PGNode): + pg = process["process_graph"] + else: + raise ValueError(process) + + return PGNode.to_process_graph_argument(pg) + + +def _ensure_save_result( + cube: _ProcessGraphAbstraction, + *, + format: Optional[str] = None, + options: Optional[dict] = None, + weak_format: Optional[str] = None, + default_format: str, + method: str, +) -> _ProcessGraphAbstraction: + """ + Make sure there is a`save_result` node in the process graph. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :param weak_format: (optional) weak format indicator guessed from file name + :param default_format: default format for data type to use when no format is specified by user + :return: + """ + # TODO #278 instead of standalone helper function, move this to common base class for raster cubes, vector cubes, ... + save_result_nodes = [n for n in cube.result_node().walk_nodes() if n.process_id == "save_result"] + + if not save_result_nodes: + # No `save_result` node yet: automatically add it. + # TODO: the `save_result` method is not defined on _ProcessGraphAbstraction, but it is on DataCube and VectorCube + cube = cube.save_result(format=format or weak_format or default_format, options=options) + elif format or options: + raise OpenEoClientException( + f"{method} with explicit output {'format' if format else 'options'} {format or options!r}," + f" but the process graph already has `save_result` node(s)" + f" which is ambiguous and should not be combined." + ) + + return cube +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/connection.html b/_modules/openeo/rest/connection.html new file mode 100644 index 000000000..dd72f0409 --- /dev/null +++ b/_modules/openeo/rest/connection.html @@ -0,0 +1,2307 @@ + + + + + + + openeo.rest.connection — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.connection

+"""
+This module provides a Connection object to manage and persist settings when interacting with the OpenEO API.
+"""
+from __future__ import annotations
+
+import datetime
+import json
+import logging
+import os
+import shlex
+import sys
+import warnings
+from collections import OrderedDict
+from pathlib import Path, PurePosixPath
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Sequence,
+    Set,
+    Tuple,
+    Union,
+)
+
+import requests
+import shapely.geometry.base
+from requests import Response
+from requests.auth import AuthBase, HTTPBasicAuth
+
+import openeo
+from openeo.capabilities import ApiVersionException, ComparableVersion
+from openeo.config import config_log, get_config_option
+from openeo.internal.documentation import openeo_process
+from openeo.internal.graph_building import FlatGraphableMixin, PGNode, as_flat_graph
+from openeo.internal.jupyter import VisualDict, VisualList
+from openeo.internal.processes.builder import ProcessBuilderBase
+from openeo.internal.warnings import deprecated, legacy_alias
+from openeo.metadata import (
+    Band,
+    BandDimension,
+    CollectionMetadata,
+    SpatialDimension,
+    TemporalDimension,
+)
+from openeo.rest import (
+    DEFAULT_DOWNLOAD_CHUNK_SIZE,
+    CapabilitiesException,
+    OpenEoApiError,
+    OpenEoApiPlainError,
+    OpenEoClientException,
+    OpenEoRestError,
+)
+from openeo.rest._datacube import _ProcessGraphAbstraction, build_child_callback
+from openeo.rest.auth.auth import BasicBearerAuth, BearerAuth, NullAuth, OidcBearerAuth
+from openeo.rest.auth.config import AuthConfig, RefreshTokenStore
+from openeo.rest.auth.oidc import (
+    DefaultOidcClientGrant,
+    GrantsChecker,
+    OidcAuthCodePkceAuthenticator,
+    OidcAuthenticator,
+    OidcClientCredentialsAuthenticator,
+    OidcClientInfo,
+    OidcDeviceAuthenticator,
+    OidcException,
+    OidcProviderInfo,
+    OidcRefreshTokenAuthenticator,
+    OidcResourceOwnerPasswordAuthenticator,
+)
+from openeo.rest.datacube import DataCube, InputDate
+from openeo.rest.graph_building import CollectionProperty
+from openeo.rest.job import BatchJob, RESTJob
+from openeo.rest.mlmodel import MlModel
+from openeo.rest.rest_capabilities import RESTCapabilities
+from openeo.rest.service import Service
+from openeo.rest.udp import Parameter, RESTUserDefinedProcess
+from openeo.rest.userfile import UserFile
+from openeo.rest.vectorcube import VectorCube
+from openeo.util import (
+    ContextTimer,
+    LazyLoadCache,
+    dict_no_none,
+    ensure_list,
+    load_json_resource,
+    repr_truncate,
+    rfc3339,
+    str_truncate,
+    url_join,
+)
+
+_log = logging.getLogger(__name__)
+
+# Default timeouts for requests
+# TODO: get default_timeout from config?
+DEFAULT_TIMEOUT = 20 * 60
+DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE = 30 * 60
+
+
+class RestApiConnection:
+    """Base connection class implementing generic REST API request functionality"""
+
+    def __init__(
+        self,
+        root_url: str,
+        auth: Optional[AuthBase] = None,
+        session: Optional[requests.Session] = None,
+        default_timeout: Optional[int] = None,
+        slow_response_threshold: Optional[float] = None,
+    ):
+        self._root_url = root_url
+        self.auth = auth or NullAuth()
+        self.session = session or requests.Session()
+        self.default_timeout = default_timeout or DEFAULT_TIMEOUT
+        self.default_headers = {
+            "User-Agent": "openeo-python-client/{cv} {py}/{pv} {pl}".format(
+                cv=openeo.client_version(),
+                py=sys.implementation.name, pv=".".join(map(str, sys.version_info[:3])),
+                pl=sys.platform
+            )
+        }
+        self.slow_response_threshold = slow_response_threshold
+
+    @property
+    def root_url(self):
+        return self._root_url
+
+    def build_url(self, path: str):
+        return url_join(self._root_url, path)
+
+    def _merged_headers(self, headers: dict) -> dict:
+        """Merge default headers with given headers"""
+        result = self.default_headers.copy()
+        if headers:
+            result.update(headers)
+        return result
+
+    def _is_external(self, url: str) -> bool:
+        """Check if given url is external (not under root url)"""
+        root = self.root_url.rstrip("/")
+        return not (url == root or url.startswith(root + '/'))
+
+    def request(
+        self,
+        method: str,
+        path: str,
+        *,
+        headers: Optional[dict] = None,
+        auth: Optional[AuthBase] = None,
+        check_error: bool = True,
+        expected_status: Optional[Union[int, Iterable[int]]] = None,
+        **kwargs,
+    ):
+        """Generic request send"""
+        url = self.build_url(path)
+        # Don't send default auth headers to external domains.
+        auth = auth or (self.auth if not self._is_external(url) else None)
+        slow_response_threshold = kwargs.pop("slow_response_threshold", self.slow_response_threshold)
+        if _log.isEnabledFor(logging.DEBUG):
+            _log.debug("Request `{m} {u}` with headers {h}, auth {a}, kwargs {k}".format(
+                m=method.upper(), u=url, h=headers and headers.keys(), a=type(auth).__name__, k=list(kwargs.keys()))
+            )
+        with ContextTimer() as timer:
+            resp = self.session.request(
+                method=method,
+                url=url,
+                headers=self._merged_headers(headers),
+                auth=auth,
+                timeout=kwargs.pop("timeout", self.default_timeout),
+                **kwargs
+            )
+        if slow_response_threshold and timer.elapsed() > slow_response_threshold:
+            _log.warning("Slow response: `{m} {u}` took {e:.2f}s (>{t:.2f}s)".format(
+                m=method.upper(), u=str_truncate(url, width=64),
+                e=timer.elapsed(), t=slow_response_threshold
+            ))
+        if _log.isEnabledFor(logging.DEBUG):
+            _log.debug(
+                f"openEO request `{resp.request.method} {resp.request.path_url}` -> response {resp.status_code} headers {resp.headers!r}"
+            )
+        # Check for API errors and unexpected HTTP status codes as desired.
+        status = resp.status_code
+        expected_status = ensure_list(expected_status) if expected_status else []
+        if check_error and status >= 400 and status not in expected_status:
+            self._raise_api_error(resp)
+        if expected_status and status not in expected_status:
+            raise OpenEoRestError("Got status code {s!r} for `{m} {p}` (expected {e!r}) with body {body}".format(
+                m=method.upper(), p=path, s=status, e=expected_status, body=resp.text)
+            )
+        return resp
+
+    def _raise_api_error(self, response: requests.Response):
+        """Convert API error response to Python exception"""
+        status_code = response.status_code
+        try:
+            info = response.json()
+        except Exception:
+            info = None
+
+        # Valid JSON object with "code" and "message" fields indicates a proper openEO API error.
+        if isinstance(info, dict):
+            error_code = info.get("code")
+            error_message = info.get("message")
+            if error_code and isinstance(error_code, str) and error_message and isinstance(error_message, str):
+                raise OpenEoApiError(
+                    http_status_code=status_code,
+                    code=error_code,
+                    message=error_message,
+                    id=info.get("id"),
+                    url=info.get("url"),
+                )
+
+        # Failed to parse it as a compliant openEO API error: show body as-is in the exception.
+        text = response.text
+        error_message = None
+        _log.warning(f"Failed to parse API error response: [{status_code}] {text!r} (headers: {response.headers})")
+
+        # TODO: eliminate this VITO-backend specific error massaging?
+        if status_code == 502 and "Proxy Error" in text:
+            error_message = (
+                "Received 502 Proxy Error."
+                " This typically happens when a synchronous openEO processing request takes too long and is aborted."
+                " Consider using a batch job instead."
+            )
+
+        raise OpenEoApiPlainError(message=text, http_status_code=status_code, error_message=error_message)
+
+    def get(self, path: str, stream: bool = False, auth: Optional[AuthBase] = None, **kwargs) -> Response:
+        """
+        Do GET request to REST API.
+
+        :param path: API path (without root url)
+        :param stream: True if the get request should be streamed, else False
+        :param auth: optional custom authentication to use instead of the default one
+        :return: response: Response
+        """
+        return self.request("get", path=path, stream=stream, auth=auth, **kwargs)
+
+    def post(self, path: str, json: Optional[dict] = None, **kwargs) -> Response:
+        """
+        Do POST request to REST API.
+
+        :param path: API path (without root url)
+        :param json: Data (as dictionary) to be posted with JSON encoding)
+        :return: response: Response
+        """
+        return self.request("post", path=path, json=json, allow_redirects=False, **kwargs)
+
+    def delete(self, path: str, **kwargs) -> Response:
+        """
+        Do DELETE request to REST API.
+
+        :param path: API path (without root url)
+        :return: response: Response
+        """
+        return self.request("delete", path=path, allow_redirects=False, **kwargs)
+
+    def patch(self, path: str, **kwargs) -> Response:
+        """
+        Do PATCH request to REST API.
+
+        :param path: API path (without root url)
+        :return: response: Response
+        """
+        return self.request("patch", path=path, allow_redirects=False, **kwargs)
+
+    def put(self, path: str, headers: Optional[dict] = None, data: Optional[dict] = None, **kwargs) -> Response:
+        """
+        Do PUT request to REST API.
+
+        :param path: API path (without root url)
+        :param headers: headers that gets added to the request.
+        :param data: data that gets added to the request.
+        :return: response: Response
+        """
+        return self.request("put", path=path, data=data, headers=headers, allow_redirects=False, **kwargs)
+
+    def __repr__(self):
+        return "<{c} to {r!r} with {a}>".format(c=type(self).__name__, r=self._root_url, a=type(self.auth).__name__)
+
+
+
+[docs] +class Connection(RestApiConnection): + """ + Connection to an openEO backend. + + :param url: Backend root url + :param session: Optional ``requests.Session`` object to use for requests. + :param default_timeout: Default timeout for requests in seconds. + :param auto_validate: toggle to automatically validate process graphs before execution + :param slow_response_threshold: Optional threshold in seconds + to consider a response as slow and log a warning. + :param auth_config: Optional :class:`AuthConfig` object + to fetch authentication related configuration from. + :param refresh_token_store: For advanced usage: + custom :class:`RefreshTokenStore` object + to use for storing/loading refresh tokens. + :param oidc_auth_renewer: For advanced usage: + optional :class:`OidcAuthenticator` object to use for renewing OIDC tokens. + :param auth: Optional ``requests.auth.AuthBase`` object to use for requests. + Usage of this parameter is deprecated, use the specific authentication methods instead. + """ + + _MINIMUM_API_VERSION = ComparableVersion("1.0.0") + + def __init__( + self, + url: str, + *, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auto_validate: bool = True, + slow_response_threshold: Optional[float] = None, + auth_config: Optional[AuthConfig] = None, + refresh_token_store: Optional[RefreshTokenStore] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + auth: Optional[AuthBase] = None, + ): + if "://" not in url: + url = "https://" + url + self._orig_url = url + super().__init__( + root_url=self.version_discovery(url, session=session, timeout=default_timeout), + auth=auth, session=session, default_timeout=default_timeout, + slow_response_threshold=slow_response_threshold, + ) + self._capabilities_cache = LazyLoadCache() + + # Initial API version check. + self._api_version.require_at_least(self._MINIMUM_API_VERSION) + + self._auth_config = auth_config + self._refresh_token_store = refresh_token_store + self._oidc_auth_renewer = oidc_auth_renewer + self._auto_validate = auto_validate + +
+[docs] + @classmethod + def version_discovery( + cls, url: str, session: Optional[requests.Session] = None, timeout: Optional[int] = None + ) -> str: + """ + Do automatic openEO API version discovery from given url, using a "well-known URI" strategy. + + :param url: initial backend url (not including "/.well-known/openeo") + :return: root url of highest supported backend version + """ + try: + connection = RestApiConnection(url, session=session) + well_known_url_response = connection.get("/.well-known/openeo", timeout=timeout) + assert well_known_url_response.status_code == 200 + versions = well_known_url_response.json()["versions"] + supported_versions = [v for v in versions if cls._MINIMUM_API_VERSION <= v["api_version"]] + assert supported_versions + production_versions = [v for v in supported_versions if v.get("production", True)] + highest_version = max(production_versions or supported_versions, key=lambda v: v["api_version"]) + _log.debug("Highest supported version available in backend: %s" % highest_version) + return highest_version['url'] + except Exception: + # Be very lenient about failing on the well-known URI strategy. + return url
+ + + def _get_auth_config(self) -> AuthConfig: + if self._auth_config is None: + self._auth_config = AuthConfig() + return self._auth_config + + def _get_refresh_token_store(self) -> RefreshTokenStore: + if self._refresh_token_store is None: + self._refresh_token_store = RefreshTokenStore() + return self._refresh_token_store + +
+[docs] + def authenticate_basic(self, username: Optional[str] = None, password: Optional[str] = None) -> Connection: + """ + Authenticate a user to the backend using basic username and password. + + :param username: User name + :param password: User passphrase + """ + if not self.capabilities().supports_endpoint("/credentials/basic", method="GET"): + raise OpenEoClientException("This openEO back-end does not support basic authentication.") + if username is None: + username, password = self._get_auth_config().get_basic_auth(backend=self._orig_url) + if username is None: + raise OpenEoClientException("No username/password given or found.") + + resp = self.get( + '/credentials/basic', + # /credentials/basic is the only endpoint that expects a Basic HTTP auth + auth=HTTPBasicAuth(username, password) + ).json() + # Switch to bearer based authentication in further requests. + self.auth = BasicBearerAuth(access_token=resp["access_token"]) + return self
+ + + def _get_oidc_provider( + self, provider_id: Union[str, None] = None, parse_info: bool = True + ) -> Tuple[str, Union[OidcProviderInfo, None]]: + """ + Get provider id and info, based on context. + If provider_id is given, verify it against backend's list of providers. + If not given, find a suitable provider based on env vars, config or backend's default. + + :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). + Can be None if there is just one provider. + :param parse_info: whether to parse the provider info into an :py:class:`OidcProviderInfo` object + (which involves a ".well-known/openid-configuration" request) + :return: resolved/verified provider_id and provider info object (unless ``parse_info`` is False) + """ + oidc_info = self.get("/credentials/oidc", expected_status=200).json() + providers = OrderedDict((p["id"], p) for p in oidc_info["providers"]) + if len(providers) < 1: + raise OpenEoClientException("Backend lists no OIDC providers.") + _log.info("Found OIDC providers: {p}".format(p=list(providers.keys()))) + + # TODO: also support specifying provider through issuer URL? + provider_id_from_env = os.environ.get("OPENEO_AUTH_PROVIDER_ID") + + if provider_id: + if provider_id not in providers: + raise OpenEoClientException( + "Requested OIDC provider {r!r} not available. Should be one of {p}.".format( + r=provider_id, p=list(providers.keys()) + ) + ) + provider = providers[provider_id] + elif provider_id_from_env and provider_id_from_env in providers: + _log.info(f"Using provider_id {provider_id_from_env!r} from OPENEO_AUTH_PROVIDER_ID env var") + provider_id = provider_id_from_env + provider = providers[provider_id] + elif len(providers) == 1: + provider_id, provider = providers.popitem() + _log.info( + f"No OIDC provider given, but only one available: {provider_id!r}. Using that one." + ) + else: + # Check if there is a single provider in the config to use. + backend = self._orig_url + provider_configs = self._get_auth_config().get_oidc_provider_configs( + backend=backend + ) + intersection = set(provider_configs.keys()).intersection(providers.keys()) + if len(intersection) == 1: + provider_id = intersection.pop() + provider = providers[provider_id] + _log.info( + f"No OIDC provider given, but only one in config (for backend {backend!r}): {provider_id!r}. Using that one." + ) + else: + provider_id, provider = providers.popitem(last=False) + _log.info( + f"No OIDC provider given. Using first provider {provider_id!r} as advertised by backend." + ) + + provider_info = OidcProviderInfo.from_dict(provider) if parse_info else None + + return provider_id, provider_info + + def _get_oidc_provider_and_client_info( + self, + provider_id: str, + client_id: Union[str, None] = None, + client_secret: Union[str, None] = None, + default_client_grant_check: Union[None, GrantsChecker] = None, + ) -> Tuple[str, OidcClientInfo]: + """ + Resolve provider_id and client info (as given or from config) + + :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). + Can be None if there is just one provider. + + :return: OIDC provider id and client info + """ + provider_id, provider = self._get_oidc_provider(provider_id) + + if client_id is None: + _log.debug("No client_id: checking config for preferred client_id") + client_id, client_secret = self._get_auth_config().get_oidc_client_configs( + backend=self._orig_url, provider_id=provider_id + ) + if client_id: + _log.info("Using client_id {c!r} from config (provider {p!r})".format(c=client_id, p=provider_id)) + if client_id is None and default_client_grant_check: + # Try "default_clients" from backend's provider info. + _log.debug("No client_id given: checking default clients in backend's provider info") + client_id = provider.get_default_client_id(grant_check=default_client_grant_check) + if client_id: + _log.info("Using default client_id {c!r} from OIDC provider {p!r} info.".format( + c=client_id, p=provider_id + )) + if client_id is None: + raise OpenEoClientException("No client_id found.") + + client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider) + + return provider_id, client_info + + def _authenticate_oidc( + self, + authenticator: OidcAuthenticator, + *, + provider_id: str, + store_refresh_token: bool = False, + fallback_refresh_token_to_store: Optional[str] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + ) -> Connection: + """ + Authenticate through OIDC and set up bearer token (based on OIDC access_token) for further requests. + """ + tokens = authenticator.get_tokens(request_refresh_token=store_refresh_token) + _log.info("Obtained tokens: {t}".format(t=[k for k, v in tokens._asdict().items() if v])) + if store_refresh_token: + refresh_token = tokens.refresh_token or fallback_refresh_token_to_store + if refresh_token: + self._get_refresh_token_store().set_refresh_token( + issuer=authenticator.provider_info.issuer, + client_id=authenticator.client_id, + refresh_token=refresh_token + ) + if not oidc_auth_renewer: + oidc_auth_renewer = OidcRefreshTokenAuthenticator( + client_info=authenticator.client_info, refresh_token=refresh_token + ) + else: + _log.warning("No OIDC refresh token to store.") + token = tokens.access_token + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token) + self._oidc_auth_renewer = oidc_auth_renewer + return self + +
+[docs] + def authenticate_oidc_authorization_code( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + timeout: Optional[int] = None, + server_address: Optional[Tuple[str, int]] = None, + webbrowser_open: Optional[Callable] = None, + store_refresh_token=False, + ) -> Connection: + """ + OpenID Connect Authorization Code Flow (with PKCE). + + .. deprecated:: 0.19.0 + Usage of the Authorization Code flow is deprecated (because of its complexity) and will be removed. + It is recommended to use the Device Code flow with :py:meth:`authenticate_oidc_device` + or Client Credentials flow with :py:meth:`authenticate_oidc_client_credentials`. + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=[DefaultOidcClientGrant.AUTH_CODE_PKCE], + ) + authenticator = OidcAuthCodePkceAuthenticator( + client_info=client_info, + webbrowser_open=webbrowser_open, timeout=timeout, server_address=server_address + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token)
+ + +
+[docs] + def authenticate_oidc_client_credentials( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + ) -> Connection: + """ + Authenticate with :ref:`OIDC Client Credentials flow <authenticate_oidc_client_credentials>` + + Client id, secret and provider id can be specified directly through the available arguments. + It is also possible to leave these arguments empty and specify them through + environment variables ``OPENEO_AUTH_CLIENT_ID``, + ``OPENEO_AUTH_CLIENT_SECRET`` and ``OPENEO_AUTH_PROVIDER_ID`` respectively + as discussed in :ref:`authenticate_oidc_client_credentials_env_vars`. + + :param client_id: client id to use + :param client_secret: client secret to use + :param provider_id: provider id to use + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + + .. versionchanged:: 0.18.0 Allow specifying client id, secret and provider id through environment variables. + """ + # TODO: option to get client id/secret from a config file too? + if client_id is None and "OPENEO_AUTH_CLIENT_ID" in os.environ and "OPENEO_AUTH_CLIENT_SECRET" in os.environ: + client_id = os.environ.get("OPENEO_AUTH_CLIENT_ID") + client_secret = os.environ.get("OPENEO_AUTH_CLIENT_SECRET") + _log.debug(f"Getting client id ({client_id}) and secret from environment") + + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret + ) + authenticator = OidcClientCredentialsAuthenticator(client_info=client_info) + return self._authenticate_oidc( + authenticator, provider_id=provider_id, store_refresh_token=False, oidc_auth_renewer=authenticator + )
+ + +
+[docs] + def authenticate_oidc_resource_owner_password_credentials( + self, + username: str, + password: str, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + store_refresh_token: bool = False, + ) -> Connection: + """ + OpenId Connect Resource Owner Password Credentials + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret + ) + # TODO: also get username and password from config? + authenticator = OidcResourceOwnerPasswordAuthenticator( + client_info=client_info, username=username, password=password + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token)
+ + +
+[docs] + def authenticate_oidc_refresh_token( + self, + client_id: Optional[str] = None, + refresh_token: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + *, + store_refresh_token: bool = False, + ) -> Connection: + """ + Authenticate with :ref:`OIDC Refresh Token flow <authenticate_oidc_client_credentials>` + + :param client_id: client id to use + :param refresh_token: refresh token to use + :param client_secret: client secret to use + :param provider_id: provider id to use. + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + :param store_refresh_token: whether to store the received refresh token automatically + + .. versionchanged:: 0.19.0 Support fallback provider id through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=[DefaultOidcClientGrant.REFRESH_TOKEN], + ) + + if refresh_token is None: + refresh_token = self._get_refresh_token_store().get_refresh_token( + issuer=client_info.provider.issuer, + client_id=client_info.client_id + ) + if refresh_token is None: + raise OpenEoClientException("No refresh token given or found") + + authenticator = OidcRefreshTokenAuthenticator(client_info=client_info, refresh_token=refresh_token) + return self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + fallback_refresh_token_to_store=refresh_token, + oidc_auth_renewer=authenticator, + )
+ + +
+[docs] + def authenticate_oidc_device( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + *, + store_refresh_token: bool = False, + use_pkce: Optional[bool] = None, + max_poll_time: float = OidcDeviceAuthenticator.DEFAULT_MAX_POLL_TIME, + **kwargs, + ) -> Connection: + """ + Authenticate with the :ref:`OIDC Device Code flow <authenticate_oidc_device>` + + :param client_id: client id to use instead of the default one + :param client_secret: client secret to use instead of the default one + :param provider_id: provider id to use. + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + :param store_refresh_token: whether to store the received refresh token automatically + :param use_pkce: Use PKCE instead of client secret. + If not set explicitly to `True` (use PKCE) or `False` (use client secret), + it will be attempted to detect the best mode automatically. + Note that PKCE for device code is not widely supported among OIDC providers. + :param max_poll_time: maximum time in seconds to keep polling for successful authentication. + + .. versionchanged:: 0.5.1 Add :py:obj:`use_pkce` argument + .. versionchanged:: 0.17.0 Add :py:obj:`max_poll_time` argument + .. versionchanged:: 0.19.0 Support fallback provider id through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + """ + _g = DefaultOidcClientGrant # alias for compactness + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=(lambda grants: _g.DEVICE_CODE in grants or _g.DEVICE_CODE_PKCE in grants), + ) + authenticator = OidcDeviceAuthenticator( + client_info=client_info, use_pkce=use_pkce, max_poll_time=max_poll_time, **kwargs + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token)
+ + +
+[docs] + def authenticate_oidc( + self, + provider_id: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + *, + store_refresh_token: bool = True, + use_pkce: Optional[bool] = None, + display: Callable[[str], None] = print, + max_poll_time: float = OidcDeviceAuthenticator.DEFAULT_MAX_POLL_TIME, + ): + """ + Generic method to do OpenID Connect authentication. + + In the context of interactive usage, this method first tries to use refresh tokens + and falls back on device code flow. + + For non-interactive, machine-to-machine contexts, it is also possible to trigger + the usage of the "client_credentials" flow through environment variables. + Assuming you have set up a OIDC client (with a secret): + set ``OPENEO_AUTH_METHOD`` to ``client_credentials``, + set ``OPENEO_AUTH_CLIENT_ID`` to the client id, + and set ``OPENEO_AUTH_CLIENT_SECRET`` to the client secret. + + See :ref:`authenticate_oidc_automatic` for more details. + + :param provider_id: provider id to use + :param client_id: client id to use + :param client_secret: client secret to use + :param max_poll_time: maximum time in seconds to keep polling for successful authentication. + + .. versionadded:: 0.6.0 + .. versionchanged:: 0.17.0 Add :py:obj:`max_poll_time` argument + .. versionchanged:: 0.18.0 Add support for client credentials flow. + """ + # TODO: unify `os.environ.get` with `get_config_option`? + # TODO also support OPENEO_AUTH_CLIENT_ID, ... env vars for refresh token and device code auth? + + auth_method = os.environ.get("OPENEO_AUTH_METHOD") + if auth_method == "client_credentials": + _log.debug("authenticate_oidc: going for 'client_credentials' authentication") + return self.authenticate_oidc_client_credentials( + client_id=client_id, client_secret=client_secret, provider_id=provider_id + ) + elif auth_method: + raise ValueError(f"Unhandled auth method {auth_method}") + + _g = DefaultOidcClientGrant # alias for compactness + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=lambda grants: ( + _g.REFRESH_TOKEN in grants and (_g.DEVICE_CODE in grants or _g.DEVICE_CODE_PKCE in grants) + ) + ) + + # Try refresh token first. + refresh_token = self._get_refresh_token_store().get_refresh_token( + issuer=client_info.provider.issuer, + client_id=client_info.client_id + ) + if refresh_token: + try: + _log.info("Found refresh token: trying refresh token based authentication.") + authenticator = OidcRefreshTokenAuthenticator(client_info=client_info, refresh_token=refresh_token) + con = self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + fallback_refresh_token_to_store=refresh_token, + ) + # TODO: pluggable/jupyter-aware display function? + print("Authenticated using refresh token.") + return con + except OidcException as e: + _log.info("Refresh token based authentication failed: {e}.".format(e=e)) + + # Fall back on device code flow + # TODO: make it possible to do other fallback flows too? + _log.info("Trying device code flow.") + authenticator = OidcDeviceAuthenticator( + client_info=client_info, use_pkce=use_pkce, display=display, max_poll_time=max_poll_time + ) + con = self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + ) + print("Authenticated using device code flow.") + return con
+ + +
+[docs] + def authenticate_oidc_access_token(self, access_token: str, provider_id: Optional[str] = None) -> Connection: + """ + Set up authorization headers directly with an OIDC access token. + + :py:class:`Connection` provides multiple methods to handle various OIDC authentication flows end-to-end. + If you already obtained a valid OIDC access token in another "out-of-band" way, you can use this method to + set up the authorization headers appropriately. + + :param access_token: OIDC access token + :param provider_id: id of the OIDC provider as listed by the openEO backend (``/credentials/oidc``). + If not specified, the first (default) OIDC provider will be used. + :param skip_verification: Skip clients-side verification of the provider_id + against the backend's list of providers to avoid and related OIDC configuration + + .. versionadded:: 0.31.0 + + .. versionchanged:: 0.33.0 + Return connection object to support chaining. + """ + provider_id, _ = self._get_oidc_provider(provider_id=provider_id, parse_info=False) + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=access_token) + self._oidc_auth_renewer = None + return self
+ + +
+[docs] + def request( + self, + method: str, + path: str, + headers: Optional[dict] = None, + auth: Optional[AuthBase] = None, + check_error: bool = True, + expected_status: Optional[Union[int, Iterable[int]]] = None, + **kwargs, + ): + # Do request, but with retry when access token has expired and refresh token is available. + def _request(): + return super(Connection, self).request( + method=method, path=path, headers=headers, auth=auth, + check_error=check_error, expected_status=expected_status, **kwargs, + ) + + try: + # Initial request attempt + return _request() + except OpenEoApiError as api_exc: + if api_exc.http_status_code in {401, 403} and api_exc.code == "TokenInvalid": + # Auth token expired: can we refresh? + if isinstance(self.auth, OidcBearerAuth) and self._oidc_auth_renewer: + msg = f"OIDC access token expired ({api_exc.http_status_code} {api_exc.code})." + try: + self._authenticate_oidc( + authenticator=self._oidc_auth_renewer, + provider_id=self._oidc_auth_renewer.provider_info.id, + store_refresh_token=False, + oidc_auth_renewer=self._oidc_auth_renewer, + ) + _log.info(f"{msg} Obtained new access token (grant {self._oidc_auth_renewer.grant_type!r}).") + except OpenEoClientException as auth_exc: + _log.error( + f"{msg} Failed to obtain new access token (grant {self._oidc_auth_renewer.grant_type!r}): {auth_exc!r}." + ) + else: + # Retry request. + return _request() + raise
+ + +
+[docs] + def describe_account(self) -> dict: + """ + Describes the currently authenticated user account. + """ + return self.get('/me', expected_status=200).json()
+ + +
+[docs] + @deprecated("use :py:meth:`list_jobs` instead", version="0.4.10") + def user_jobs(self) -> List[dict]: + return self.list_jobs()
+ + +
+[docs] + def list_collections(self) -> List[dict]: + """ + List basic metadata of all collections provided by the back-end. + + .. caution:: + + Only the basic collection metadata will be returned. + To obtain full metadata of a particular collection, + it is recommended to use :py:meth:`~openeo.rest.connection.Connection.describe_collection` instead. + + :return: list of dictionaries with basic collection metadata. + """ + # TODO: add caching #383 + data = self.get('/collections', expected_status=200).json()["collections"] + return VisualList("collections", data=data)
+ + +
+[docs] + def list_collection_ids(self) -> List[str]: + """ + List all collection ids provided by the back-end. + + .. seealso:: + + :py:meth:`~openeo.rest.connection.Connection.describe_collection` + to get the metadata of a particular collection. + + :return: list of collection ids + """ + return [collection['id'] for collection in self.list_collections() if 'id' in collection]
+ + +
+[docs] + def capabilities(self) -> RESTCapabilities: + """ + Loads all available capabilities. + """ + return self._capabilities_cache.get( + "capabilities", + load=lambda: RESTCapabilities(data=self.get('/', expected_status=200).json(), url=self._orig_url) + )
+ + + def list_input_formats(self) -> dict: + return self.list_file_formats().get("input", {}) + + def list_output_formats(self) -> dict: + return self.list_file_formats().get("output", {}) + + list_file_types = legacy_alias( + list_output_formats, "list_file_types", since="0.4.6" + ) + +
+[docs] + def list_file_formats(self) -> dict: + """ + Get available input and output formats + """ + formats = self._capabilities_cache.get( + key="file_formats", + load=lambda: self.get('/file_formats', expected_status=200).json() + ) + return VisualDict("file-formats", data=formats)
+ + +
+[docs] + def list_service_types(self) -> dict: + """ + Loads all available service types. + + :return: data_dict: Dict All available service types + """ + types = self._capabilities_cache.get( + key="service_types", + load=lambda: self.get('/service_types', expected_status=200).json() + ) + return VisualDict("service-types", data=types)
+ + +
+[docs] + def list_udf_runtimes(self) -> dict: + """ + List information about the available UDF runtimes. + + :return: A dictionary with metadata about each available UDF runtime. + """ + runtimes = self._capabilities_cache.get( + key="udf_runtimes", + load=lambda: self.get('/udf_runtimes', expected_status=200).json() + ) + return VisualDict("udf-runtimes", data=runtimes)
+ + +
+[docs] + def list_services(self) -> dict: + """ + Loads all available services of the authenticated user. + + :return: data_dict: Dict All available services + """ + # TODO return parsed service objects + services = self.get('/services', expected_status=200).json()["services"] + return VisualList("data-table", data=services, parameters={'columns': 'services'})
+ + +
+[docs] + def describe_collection(self, collection_id: str) -> dict: + """ + Get full collection metadata for given collection id. + + .. seealso:: + + :py:meth:`~openeo.rest.connection.Connection.list_collection_ids` + to list all collection ids provided by the back-end. + + :param collection_id: collection id + :return: collection metadata. + """ + # TODO: duplication with `Connection.collection_metadata`: deprecate one or the other? + # TODO: add caching #383 + data = self.get(f"/collections/{collection_id}", expected_status=200).json() + return VisualDict("collection", data=data)
+ + +
+[docs] + def collection_items( + self, + name, + spatial_extent: Optional[List[float]] = None, + temporal_extent: Optional[List[Union[str, datetime.datetime]]] = None, + limit: Optional[int] = None, + ) -> Iterator[dict]: + """ + Loads items for a specific image collection. + May not be available for all collections. + + This is an experimental API and is subject to change. + + :param name: String Id of the collection + :param spatial_extent: Limits the items to the given bounding box in WGS84: + 1. Lower left corner, coordinate axis 1 + 2. Lower left corner, coordinate axis 2 + 3. Upper right corner, coordinate axis 1 + 4. Upper right corner, coordinate axis 2 + + :param temporal_extent: Limits the items to the specified temporal interval. + :param limit: The amount of items per request/page. If None, the back-end decides. + The interval has to be specified as an array with exactly two elements (start, end). + Also supports open intervals by setting one of the boundaries to None, but never both. + + :return: data_list: List A list of items + """ + url = '/collections/{}/items'.format(name) + params = {} + if spatial_extent: + params["bbox"] = ",".join(str(c) for c in spatial_extent) + if temporal_extent: + params["datetime"] = "/".join(".." if t is None else rfc3339.normalize(t) for t in temporal_extent) + if limit is not None and limit > 0: + params['limit'] = limit + + return paginate(self, url, params, lambda response, page: VisualDict("items", data = response, parameters = {'show-map': True, 'heading': 'Page {} - Items'.format(page)}))
+ + + def collection_metadata(self, name) -> CollectionMetadata: + # TODO: duplication with `Connection.describe_collection`: deprecate one or the other? + return CollectionMetadata(metadata=self.describe_collection(name)) + +
+[docs] + def list_processes(self, namespace: Optional[str] = None) -> List[dict]: + # TODO: Maybe format the result dictionary so that the process_id is the key of the dictionary. + """ + Loads all available processes of the back end. + + :param namespace: The namespace for which to list processes. + + :return: processes_dict: Dict All available processes of the back end. + """ + if namespace is None: + processes = self._capabilities_cache.get( + key=("processes", "backend"), + load=lambda: self.get('/processes', expected_status=200).json()["processes"] + ) + else: + processes = self.get('/processes/' + namespace, expected_status=200).json()["processes"] + return VisualList("processes", data=processes, parameters={'show-graph': True, 'provide-download': False})
+ + +
+[docs] + def describe_process(self, id: str, namespace: Optional[str] = None) -> dict: + """ + Returns a single process from the back end. + + :param id: The id of the process. + :param namespace: The namespace of the process. + + :return: The process definition. + """ + + processes = self.list_processes(namespace) + for process in processes: + if process["id"] == id: + return VisualDict("process", data=process, parameters={'show-graph': True, 'provide-download': False}) + + raise OpenEoClientException("Process does not exist.")
+ + +
+[docs] + def list_jobs(self) -> List[dict]: + """ + Lists all jobs of the authenticated user. + + :return: job_list: Dict of all jobs of the user. + """ + # TODO: Parse the result so that there get Job classes returned? + resp = self.get('/jobs', expected_status=200).json() + if resp.get("federation:missing"): + _log.warning("Partial user job listing due to missing federation components: {c}".format( + c=",".join(resp["federation:missing"]) + )) + jobs = resp["jobs"] + return VisualList("data-table", data=jobs, parameters={'columns': 'jobs'})
+ + +
+[docs] + def assert_user_defined_process_support(self): + """ + Capabilities document based verification that back-end supports user-defined processes. + + .. versionadded:: 0.23.0 + """ + if not self.capabilities().supports_endpoint("/process_graphs"): + raise CapabilitiesException("Backend does not support user-defined processes.")
+ + +
+[docs] + def save_user_defined_process( + self, user_defined_process_id: str, + process_graph: Union[dict, ProcessBuilderBase], + parameters: List[Union[dict, Parameter]] = None, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ) -> RESTUserDefinedProcess: + """ + Store a process graph and its metadata on the backend as a user-defined process for the authenticated user. + + :param user_defined_process_id: unique identifier for the user-defined process + :param process_graph: a process graph + :param parameters: a list of parameters + :param public: visible to other users? + :param summary: A short summary of what the process does. + :param description: Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation. + :param returns: Description and schema of the return value. + :param categories: A list of categories. + :param examples: A list of examples. + :param links: A list of links. + :return: a RESTUserDefinedProcess instance + """ + self.assert_user_defined_process_support() + if user_defined_process_id in set(p["id"] for p in self.list_processes()): + warnings.warn("Defining user-defined process {u!r} with same id as a pre-defined process".format( + u=user_defined_process_id)) + if not parameters: + warnings.warn("Defining user-defined process {u!r} without parameters".format(u=user_defined_process_id)) + udp = RESTUserDefinedProcess(user_defined_process_id=user_defined_process_id, connection=self) + udp.store( + process_graph=process_graph, parameters=parameters, public=public, + summary=summary, description=description, + returns=returns, categories=categories, examples=examples, links=links + ) + return udp
+ + +
+[docs] + def list_user_defined_processes(self) -> List[dict]: + """ + Lists all user-defined processes of the authenticated user. + """ + self.assert_user_defined_process_support() + data = self.get("/process_graphs", expected_status=200).json()["processes"] + return VisualList("processes", data=data, parameters={'show-graph': True, 'provide-download': False})
+ + +
+[docs] + def user_defined_process(self, user_defined_process_id: str) -> RESTUserDefinedProcess: + """ + Get the user-defined process based on its id. The process with the given id should already exist. + + :param user_defined_process_id: the id of the user-defined process + :return: a RESTUserDefinedProcess instance + """ + return RESTUserDefinedProcess(user_defined_process_id=user_defined_process_id, connection=self)
+ + +
+[docs] + def validate_process_graph( + self, process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]] + ) -> List[dict]: + """ + Validate a process graph without executing it. + + :param process_graph: openEO-style (flat) process graph representation, + or an object that can be converted to such a representation: + a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object, + a string with a JSON representation, + a local file path or URL to a JSON representation, + a :py:class:`~openeo.rest.multiresult.MultiResult` object, ... + + :return: list of errors (dictionaries with "code" and "message" fields) + """ + pg_with_metadata = self._build_request_with_process_graph(process_graph)["process"] + return self.post(path="/validation", json=pg_with_metadata, expected_status=200).json()["errors"]
+ + + @property + def _api_version(self) -> ComparableVersion: + # TODO make this a public property (it's also useful outside the Connection class) + return self.capabilities().api_version_check + +
+[docs] + def vectorcube_from_paths( + self, paths: List[str], format: str, options: dict = {} + ) -> VectorCube: + """ + Loads one or more files referenced by url or path that is accessible by the backend. + + :param paths: The files to read. + :param format: The file format to read from. It must be one of the values that the server reports as supported input file formats. + :param options: The file format parameters to be used to read the files. Must correspond to the parameters that the server reports as supported parameters for the chosen format. + + :return: A :py:class:`VectorCube`. + + .. versionadded:: 0.14.0 + """ + # TODO #457 deprecate this in favor of `load_url` and standard support for `load_uploaded_files` + graph = PGNode( + "load_uploaded_files", + arguments=dict(paths=paths, format=format, options=options), + ) + # TODO: load_uploaded_files might also return a raster data cube. Determine this based on format? + return VectorCube(graph=graph, connection=self)
+ + +
+[docs] + def datacube_from_process(self, process_id: str, namespace: Optional[str] = None, **kwargs) -> DataCube: + """ + Load a data cube from a (custom) process. + + :param process_id: The process id. + :param namespace: optional: process namespace + :param kwargs: The arguments of the custom process + :return: A :py:class:`DataCube`, without valid metadata, as the client is not aware of this custom process. + """ + graph = PGNode(process_id, namespace=namespace, arguments=kwargs) + return DataCube(graph=graph, connection=self)
+ + +
+[docs] + def datacube_from_flat_graph(self, flat_graph: dict, parameters: Optional[dict] = None) -> DataCube: + """ + Construct a :py:class:`DataCube` from a flat dictionary representation of a process graph. + + .. seealso:: :ref:`datacube_from_json`, :py:meth:`~openeo.rest.connection.Connection.datacube_from_json` + + :param flat_graph: flat dictionary representation of a process graph + or a process dictionary with such a flat process graph under a "process_graph" field + (and optionally parameter metadata under a "parameters" field). + :param parameters: Optional dictionary mapping parameter names to parameter values + to use for parameters occurring in the process graph (e.g. as used in user-defined processes) + :return: A :py:class:`DataCube` corresponding with the operations encoded in the process graph + """ + parameters = parameters or {} + + if "process_graph" in flat_graph: + # `flat_graph` is a "process" structure + # Extract defaults from declared parameters. + for param in flat_graph.get("parameters") or []: + if "default" in param: + parameters.setdefault(param["name"], param["default"]) + + flat_graph = flat_graph["process_graph"] + + pgnode = PGNode.from_flat_graph(flat_graph=flat_graph, parameters=parameters or {}) + return DataCube(graph=pgnode, connection=self)
+ + +
+[docs] + def datacube_from_json(self, src: Union[str, Path], parameters: Optional[dict] = None) -> DataCube: + """ + Construct a :py:class:`DataCube` from JSON resource containing (flat) process graph representation. + + .. seealso:: :ref:`datacube_from_json`, :py:meth:`~openeo.rest.connection.Connection.datacube_from_flat_graph` + + :param src: raw JSON string, URL to JSON resource or path to local JSON file + :param parameters: Optional dictionary mapping parameter names to parameter values + to use for parameters occurring in the process graph (e.g. as used in user-defined processes) + :return: A :py:class:`DataCube` corresponding with the operations encoded in the process graph + """ + return self.datacube_from_flat_graph(load_json_resource(src), parameters=parameters)
+ + +
+[docs] + @openeo_process + def load_collection( + self, + collection_id: Union[str, Parameter], + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Union[None, List[str], Parameter] = None, + properties: Union[ + None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty + ] = None, + max_cloud_cover: Optional[float] = None, + fetch_metadata: bool = True, + ) -> DataCube: + """ + Load a DataCube by collection id. + + :param collection_id: image collection identifier + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands. + :param properties: limit data by collection metadata property predicates. + See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of such predicates. + :param max_cloud_cover: shortcut to set maximum cloud cover ("eo:cloud_cover" collection property) + :return: a datacube containing the requested data + + .. versionadded:: 0.13.0 + added the ``max_cloud_cover`` argument. + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + + .. versionchanged:: 0.26.0 + Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument. + """ + return DataCube.load_collection( + collection_id=collection_id, + connection=self, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + max_cloud_cover=max_cloud_cover, + fetch_metadata=fetch_metadata, + )
+ + + # TODO: remove this #100 #134 0.4.10 + imagecollection = legacy_alias( + load_collection, name="imagecollection", since="0.4.10" + ) + +
+[docs] + @openeo_process + def load_result( + self, + id: str, + spatial_extent: Optional[Dict[str, float]] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + ) -> DataCube: + """ + Loads batch job results by job id from the server-side user workspace. + The job must have been stored by the authenticated user on the back-end currently connected to. + + :param id: The id of a batch job with results. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands + + :return: a :py:class:`DataCube` + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + """ + # TODO: add check that back-end supports `load_result` process? + cube = self.datacube_from_process( + process_id="load_result", + id=id, + **dict_no_none( + spatial_extent=spatial_extent, + temporal_extent=temporal_extent and DataCube._get_temporal_extent(extent=temporal_extent), + bands=bands, + ), + ) + return cube
+ + +
+[docs] + @openeo_process + def load_stac( + self, + url: str, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + ) -> DataCube: + """ + Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`. + A batch job result can be loaded by providing a reference to it. + + If supported by the underlying metadata and file format, the data that is added to the data cube can be + restricted with the parameters ``spatial_extent``, ``temporal_extent`` and ``bands``. + If no data is available for the given extents, a ``NoDataAvailable`` error is thrown. + + Remarks: + + * The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as + specified in the metadata if the ``bands`` parameter is set to ``null``. + * If no additional parameter is specified this would imply that the whole data set is expected to be loaded. + Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only + load the data that is actually required after evaluating subsequent processes such as filters. + This means that the values should be processed only after the data has been limited to the required extent + and as a consequence also to a manageable size. + + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) + or a specific STAC API Collection that allows to filter items and to download assets. + This includes batch job results, which itself are compliant to STAC. + For external URLs, authentication details such as API keys or tokens may need to be included in the URL. + + Batch job results can be specified in two ways: + + - For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with ``/jobs/{id}/results`` and ``{id}`` + is the corresponding batch job ID. + - For external results, a signed URL must be provided. Not all back-ends support signed URLs, + which are provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: + Limits the data to load to the specified bounding box or polygons. + + For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects + with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + + For vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. + + The GeoJSON can be one of the following feature types: + + * A ``Polygon`` or ``MultiPolygon`` geometry, + * a ``Feature`` with a ``Polygon`` or ``MultiPolygon`` geometry, or + * a ``FeatureCollection`` containing at least one ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometries. + + Set this parameter to ``None`` to set no limit for the spatial extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + + :param temporal_extent: + Limits the data to load to the specified left-closed temporal interval. + Applies to all temporal dimensions. + The interval has to be specified as an array with exactly two elements: + + 1. The first element is the start of the temporal interval. + The specified instance in time is **included** in the interval. + 2. The second element is the end of the temporal interval. + The specified instance in time is **excluded** from the interval. + + The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. + + Also supports open intervals by setting one of the boundaries to ``None``, but never both. + + Set this parameter to ``None`` to set no limit for the temporal extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_temporal()`` directly after loading unbounded data. + + :param bands: + Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. + + Either the unique band name (metadata field ``name`` in bands) or one of the common band names + (metadata field ``common_name`` in bands) can be specified. + If the unique band name and the common name conflict, the unique band name has a higher priority. + + The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. + + It is recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :param properties: + Limits the data by metadata properties to include only data in the data cube which + all given conditions return ``True`` for (AND operation). + + Specify key-value-pairs with the key being the name of the metadata property, + which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. + This parameter is not supported for static STAC. + + .. versionadded:: 0.17.0 + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + """ + return DataCube.load_stac( + url=url, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + connection=self, + )
+ + +
+[docs] + def load_stac_from_job( + self, + job: Union[BatchJob, str], + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + ) -> DataCube: + """ + Convenience function to directly load the results of a finished openEO job + (as a STAC collection) with :py:meth:`load_stac` in a new openEO process graph. + + When available, the "canonical" link (signed URL) of the job results will be used. + + :param job: a :py:class:`~openeo.rest.job.BatchJob` or job id pointing to a finished job. + Note that the :py:class:`~openeo.rest.job.BatchJob` approach allows to point + to a batch job on a different back-end. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + :param bands: limit data to the specified bands + + .. versionadded:: 0.30.0 + """ + # TODO #634 add option to require or avoid the canonical link + if isinstance(job, str): + job = BatchJob(job_id=job, connection=self) + elif not isinstance(job, BatchJob): + raise ValueError("job must be a BatchJob or job id") + + try: + job_results = job.get_results() + + canonical_links = [ + link["href"] + for link in job_results.get_metadata().get("links", []) + if link.get("rel") == "canonical" and "href" in link + ] + if len(canonical_links) == 0: + _log.warning("No canonical link found in job results metadata. Using job results URL instead.") + stac_link = job.get_results_metadata_url(full=True) + else: + if len(canonical_links) > 1: + _log.warning( + f"Multiple canonical links found in job results metadata: {canonical_links}. Picking first one." + ) + stac_link = canonical_links[0] + except OpenEoApiError as e: + _log.warning(f"Failed to get the canonical job results: {e!r}. Using job results URL instead.") + stac_link = job.get_results_metadata_url(full=True) + + return self.load_stac( + url=stac_link, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + )
+ + +
+[docs] + def load_ml_model(self, id: Union[str, BatchJob]) -> MlModel: + """ + Loads a machine learning model from a STAC Item. + + :param id: STAC item reference, as URL, batch job (id) or user-uploaded file + :return: + + .. versionadded:: 0.10.0 + """ + return MlModel.load_ml_model(connection=self, id=id)
+ + +
+[docs] + @openeo_process + def load_geojson( + self, + data: Union[dict, str, Path, shapely.geometry.base.BaseGeometry, Parameter], + properties: Optional[List[str]] = None, + ): + """ + Converts GeoJSON data as defined by RFC 7946 into a vector data cube. + + :param data: the geometry to load. One of: + + - GeoJSON-style data structure: e.g. a dictionary with ``"type": "Polygon"`` and ``"coordinates"`` fields + - a path to a local GeoJSON file + - a GeoJSON string + - a shapely geometry object + + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + return VectorCube.load_geojson(connection=self, data=data, properties=properties)
+ + +
+[docs] + @openeo_process + def load_url(self, url: str, format: str, options: Optional[dict] = None): + """ + Loads a file from a URL + + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL. + :param format: The file format to use when loading the data. + :param options: The file format parameters to use when reading the data. + Must correspond to the parameters that the server reports as supported parameters for the chosen ``format`` + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + if format not in self.list_input_formats(): + # TODO: make this an error? + _log.warning(f"Format {format!r} not listed in back-end input formats") + # TODO: Inspect format's gis_data_type to see if we need to load a VectorCube or classic raster DataCube + return VectorCube.load_url(connection=self, url=url, format=format, options=options)
+ + + def create_service(self, graph: dict, type: str, **kwargs) -> Service: + # TODO: type hint for graph: is it a nested or a flat one? + pg_with_metadata = self._build_request_with_process_graph(process_graph=graph, type=type, **kwargs) + self._preflight_validation(pg_with_metadata=pg_with_metadata) + response = self.post(path="/services", json=pg_with_metadata, expected_status=201) + service_id = response.headers.get("OpenEO-Identifier") + return Service(service_id, self) + +
+[docs] + @deprecated("Use :py:meth:`openeo.rest.service.Service.delete_service` instead.", version="0.8.0") + def remove_service(self, service_id: str): + """ + Stop and remove a secondary web service. + + :param service_id: service identifier + :return: + """ + Service(service_id, self).delete_service()
+ + +
+[docs] + @deprecated("Use :py:meth:`openeo.rest.job.BatchJob.get_results` instead.", version="0.4.10") + def job_results(self, job_id) -> dict: + """Get batch job results metadata.""" + return BatchJob(job_id=job_id, connection=self).list_results()
+ + +
+[docs] + @deprecated("Use :py:meth:`openeo.rest.job.BatchJob.logs` instead.", version="0.4.10") + def job_logs(self, job_id, offset) -> list: + """Get batch job logs.""" + return BatchJob(job_id=job_id, connection=self).logs(offset=offset)
+ + +
+[docs] + def list_files(self) -> List[UserFile]: + """ + Lists all user-uploaded files in the user workspace on the back-end. + + :return: List of the user-uploaded files. + """ + files = self.get('/files', expected_status=200).json()['files'] + files = [UserFile.from_metadata(metadata=f, connection=self) for f in files] + return VisualList("data-table", data=files, parameters={'columns': 'files'})
+ + +
+[docs] + def get_file( + self, path: Union[str, PurePosixPath], metadata: Optional[dict] = None + ) -> UserFile: + """ + Gets a handle to a user-uploaded file in the user workspace on the back-end. + + :param path: The path on the user workspace. + """ + return UserFile(path=path, connection=self, metadata=metadata)
+ + +
+[docs] + def upload_file( + self, + source: Union[Path, str], + target: Optional[Union[str, PurePosixPath]] = None, + ) -> UserFile: + """ + Uploads a file to the given target location in the user workspace on the back-end. + + If a file at the target path exists in the user workspace it will be replaced. + + :param source: A path to a file on the local file system to upload. + :param target: The desired path (which can contain a folder structure if desired) on the user workspace. + If not set: defaults to the original filename (without any folder structure) of the local file . + """ + source = Path(source) + target = target or source.name + # TODO: support other non-path sources too: bytes, open file, url, ... + with source.open("rb") as f: + resp = self.put(f"/files/{target!s}", expected_status=200, data=f) + metadata = resp.json() + return UserFile.from_metadata(metadata=metadata, connection=self)
+ + + def _build_request_with_process_graph( + self, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + **kwargs, + ) -> dict: + """ + Prepare a json payload with a process graph to submit to /result, /services, /jobs, ... + :param process_graph: flat dict representing a "process graph with metadata" ({"process": {"process_graph": ...}, ...}) + """ + # TODO: make this a more general helper (like `as_flat_graph`) + connections = extract_connections(process_graph) + if any(c != self for c in connections): + raise OpenEoClientException(f"Mixing different connections: {self} and {connections}.") + result = kwargs + process_graph = as_flat_graph(process_graph) + if "process_graph" not in process_graph: + process_graph = {"process_graph": process_graph} + # TODO: also check if `process_graph` already has "process" key (i.e. is a "process graph with metadata" already) + result["process"] = process_graph + return result + + def _preflight_validation(self, pg_with_metadata: dict, *, validate: Optional[bool] = None): + """ + Preflight validation of process graph to execute. + + :param pg_with_metadata: flat dict representation of process graph with metadata, + e.g. as produced by `_build_request_with_process_graph` + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + + :return: + """ + if validate is None: + validate = self._auto_validate + if validate and self.capabilities().supports_endpoint("/validation", "POST"): + # At present, the intention is that a failed validation does not block + # the job from running, it is only reported as a warning. + # Therefor we also want to continue when something *else* goes wrong + # *during* the validation. + try: + resp = self.post(path="/validation", json=pg_with_metadata["process"], expected_status=200) + validation_errors = resp.json()["errors"] + if validation_errors: + _log.warning( + "Preflight process graph validation raised: " + + (" ".join(f"[{e.get('code')}] {e.get('message')}" for e in validation_errors)) + ) + except Exception as e: + _log.error(f"Preflight process graph validation failed: {e}") + + # TODO: additional validation and sanity checks: e.g. is there a result node, are all process_ids valid, ...? + + # TODO: unify `download` and `execute` better: e.g. `download` always writes to disk, `execute` returns result (raw or as JSON decoded dict) +
+[docs] + def download( + self, + graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + outputfile: Union[Path, str, None] = None, + *, + timeout: Optional[int] = None, + validate: Optional[bool] = None, + chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE, + ) -> Union[None, bytes]: + """ + Downloads the result of a process graph synchronously, + and save the result to the given file or return bytes object if no outputfile is specified. + This method is useful to export binary content such as images. For json content, the execute method is recommended. + + :param graph: (flat) dict representing a process graph, or process graph as raw JSON string, + or as local file path or URL + :param outputfile: output file + :param timeout: timeout to wait for response + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param chunk_size: chunk size for streaming response. + """ + pg_with_metadata = self._build_request_with_process_graph(process_graph=graph) + self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate) + response = self.post( + path="/result", + json=pg_with_metadata, + expected_status=200, + stream=True, + timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, + ) + + if outputfile is not None: + with Path(outputfile).open(mode="wb") as f: + for chunk in response.iter_content(chunk_size=chunk_size): + f.write(chunk) + else: + return response.content
+ + +
+[docs] + def execute( + self, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + *, + 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. 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 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) + response = self.post( + path="/result", + json=pg_with_metadata, + expected_status=200, + timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, + ) + 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
+ + +
+[docs] + def create_job( + self, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + additional: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + """ + Create a new job from given process graph on the back-end. + + :param process_graph: openEO-style (flat) process graph representation, + or an object that can be converted to such a representation: + a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object, + a string with a JSON representation, + a local file path or URL to a JSON representation, + a :py:class:`~openeo.rest.multiresult.MultiResult` object, ... + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param additional: additional job options to pass to the backend + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :return: Created job + + .. versionchanged:: 0.35.0 + Add :ref:`multi-result support <multi-result-process-graphs>`. + """ + # TODO move all this (BatchJob factory) logic to BatchJob? + + pg_with_metadata = self._build_request_with_process_graph( + process_graph=process_graph, + **dict_no_none(title=title, description=description, plan=plan, budget=budget) + ) + if additional: + # TODO: get rid of this non-standard field? https://github.com/Open-EO/openeo-api/issues/276 + pg_with_metadata["job_options"] = additional + + self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate) + response = self.post("/jobs", json=pg_with_metadata, expected_status=201) + + job_id = None + if "openeo-identifier" in response.headers: + job_id = response.headers['openeo-identifier'].strip() + elif "location" in response.headers: + _log.warning("Backend did not explicitly respond with job id, will guess it from redirect URL.") + job_id = response.headers['location'].split("/")[-1] + if not job_id: + raise OpenEoClientException("Job creation response did not contain a valid job id") + return BatchJob(job_id=job_id, connection=self)
+ + +
+[docs] + def job(self, job_id: str) -> BatchJob: + """ + Get the job based on the id. The job with the given id should already exist. + + Use :py:meth:`openeo.rest.connection.Connection.create_job` to create new jobs + + :param job_id: the job id of an existing job + :return: A job object. + """ + return BatchJob(job_id=job_id, connection=self)
+ + +
+[docs] + def service(self, service_id: str) -> Service: + """ + Get the secondary web service based on the id. The service with the given id should already exist. + + Use :py:meth:`openeo.rest.connection.Connection.create_service` to create new services + + :param job_id: the service id of an existing secondary web service + :return: A service object. + """ + return Service(service_id, connection=self)
+ + +
+[docs] + @deprecated( + reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.", + version="0.25.0") + def load_disk_collection( + self, format: str, glob_pattern: str, options: Optional[dict] = None + ) -> DataCube: + """ + Loads image data from disk as a :py:class:`DataCube`. + + This is backed by a non-standard process ('load_disk_data'). This will eventually be replaced by standard options such as + :py:meth:`openeo.rest.connection.Connection.load_stac` or https://processes.openeo.org/#load_uploaded_files + + :param format: the file format, e.g. 'GTiff' + :param glob_pattern: a glob pattern that matches the files to load from disk + :param options: options specific to the file format + """ + return DataCube.load_disk_collection( + self, format, glob_pattern, **(options or {}) + )
+ + +
+[docs] + def as_curl( + self, + data: Union[dict, DataCube, FlatGraphableMixin], + path="/result", + method="POST", + obfuscate_auth: bool = False, + ) -> str: + """ + Build curl command to evaluate given process graph or data cube + (including authorization and content-type headers). + + >>> print(connection.as_curl(cube)) + curl -i -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer ...' \\ + --data '{"process":{"process_graph":{...}}' \\ + https://openeo.example/openeo/1.1/result + + :param data: something that is convertable to an openEO process graph: a dictionary, + a :py:class:`~openeo.rest.datacube.DataCube` object, + a :py:class:`~openeo.processes.ProcessBuilder`, ... + :param path: endpoint to send request to: typically ``"/result"`` (default) for synchronous requests + or ``"/jobs"`` for batch jobs + :param method: HTTP method to use (typically ``"POST"``) + :param obfuscate_auth: don't show actual bearer token + + :return: curl command as a string + """ + cmd = ["curl", "-i", "-X", method] + cmd += ["-H", "Content-Type: application/json"] + if isinstance(self.auth, BearerAuth): + cmd += ["-H", f"Authorization: Bearer {'...' if obfuscate_auth else self.auth.bearer}"] + pg_with_metadata = self._build_request_with_process_graph(data) + if path == "/validation": + pg_with_metadata = pg_with_metadata["process"] + post_json = json.dumps(pg_with_metadata, separators=(",", ":")) + cmd += ["--data", post_json] + cmd += [self.build_url(path)] + return " ".join(shlex.quote(c) for c in cmd)
+ + +
+[docs] + def version_info(self): + """List version of the openEO client, API, back-end, etc.""" + capabilities = self.capabilities() + return { + "client": openeo.client_version(), + "api": capabilities.api_version(), + "backend": dict_no_none({ + "root_url": self.root_url, + "version": capabilities.get("backend_version"), + "processing:software": capabilities.get("processing:software"), + }), + }
+
+ + + +
+[docs] +def connect( + url: Optional[str] = None, + *, + auth_type: Optional[str] = None, + auth_options: Optional[dict] = None, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auto_validate: bool = True, +) -> Connection: + """ + This method is the entry point to OpenEO. + You typically create one connection object in your script or application + and re-use it for all calls to that backend. + + If the backend requires authentication, you can pass authentication data directly to this function, + but it could be easier to authenticate as follows: + + >>> # For basic authentication + >>> conn = connect(url).authenticate_basic(username="john", password="foo") + >>> # For OpenID Connect authentication + >>> conn = connect(url).authenticate_oidc(client_id="myclient") + + :param url: The http url of the OpenEO back-end. + :param auth_type: Which authentication to use: None, "basic" or "oidc" (for OpenID Connect) + :param auth_options: Options/arguments specific to the authentication type + :param default_timeout: default timeout (in seconds) for requests + :param auto_validate: toggle to automatically validate process graphs before execution + + .. versionadded:: 0.24.0 + added ``auto_validate`` argument + """ + + def _config_log(message): + _log.info(message) + config_log(message) + + if url is None: + default_backend = get_config_option("connection.default_backend") + if default_backend: + url = default_backend + _config_log(f"Using default back-end URL {url!r} (from config)") + default_backend_auto_auth = get_config_option("connection.default_backend.auto_authenticate") + if default_backend_auto_auth and default_backend_auto_auth.lower() in {"basic", "oidc"}: + auth_type = default_backend_auto_auth.lower() + _config_log(f"Doing auto-authentication {auth_type!r} (from config)") + + if auth_type is None: + auto_authenticate = get_config_option("connection.auto_authenticate") + if auto_authenticate and auto_authenticate.lower() in {"basic", "oidc"}: + auth_type = auto_authenticate.lower() + _config_log(f"Doing auto-authentication {auth_type!r} (from config)") + + if not url: + raise OpenEoClientException("No openEO back-end URL given or known to connect to.") + connection = Connection(url, session=session, default_timeout=default_timeout, auto_validate=auto_validate) + + auth_type = auth_type.lower() if isinstance(auth_type, str) else auth_type + if auth_type in {None, False, 'null', 'none'}: + pass + elif auth_type == "basic": + connection.authenticate_basic(**(auth_options or {})) + elif auth_type in {"oidc", "openid"}: + connection.authenticate_oidc(**(auth_options or {})) + else: + raise ValueError("Unknown auth type {a!r}".format(a=auth_type)) + return connection
+ + + +@deprecated("Use :py:func:`openeo.connect` instead", version="0.0.9") +def session(userid=None, endpoint: str = "https://openeo.org/openeo") -> Connection: + """ + This method is the entry point to OpenEO. You typically create one session object in your script or application, per back-end. + and re-use it for all calls to that backend. + If the backend requires authentication, you should set pass your credentials. + + :param endpoint: The http url of an OpenEO endpoint. + :rtype: openeo.sessions.Session + """ + return connect(url=endpoint) + + +def paginate(con: Connection, url: str, params: Optional[dict] = None, callback: Callable = lambda resp, page: resp): + # TODO: make this a method `get_paginated` on `RestApiConnection`? + # TODO: is it necessary to have `callback`? It's only used just before yielding, + # so it's probably cleaner (even for the caller) to to move it outside. + page = 1 + while True: + response = con.get(url, params=params).json() + yield callback(response, page) + next_links = [link for link in response.get("links", []) if link.get("rel") == "next" and "href" in link] + if not next_links: + break + url = next_links[0]["href"] + page += 1 + params = {} + + +def extract_connections( + data: Union[_ProcessGraphAbstraction, Sequence[_ProcessGraphAbstraction], Any] +) -> Set[Connection]: + """ + Extract the :py:class:`Connection` object(s) linked from a given data construct. + Typical use case is to get the connection from a :py:class:`DataCube`, + but can also extract multiple connections from a list of data cubes. + """ + connections = set() + # TODO: define some kind of "Connected" interface/mixin/protocol + # for objects that contain a connection instead of just checking for _ProcessGraphAbstraction + # TODO: also support extracting connections from other objects like BatchJob, ... + if isinstance(data, _ProcessGraphAbstraction) and data.connection: + connections.add(data.connection) + elif isinstance(data, (list, tuple, set)): + for item in data: + if isinstance(item, _ProcessGraphAbstraction) and item.connection: + connections.add(item.connection) + + return connections +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/conversions.html b/_modules/openeo/rest/conversions.html new file mode 100644 index 000000000..98374eefa --- /dev/null +++ b/_modules/openeo/rest/conversions.html @@ -0,0 +1,263 @@ + + + + + + + openeo.rest.conversions — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.conversions

+"""
+Helpers for data conversions between Python ecosystem data types and openEO data structures.
+"""
+
+from __future__ import annotations
+
+import typing
+
+import numpy as np
+import pandas
+
+from openeo.internal.warnings import deprecated
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    import xarray
+
+    from openeo.udf import XarrayDataCube
+
+
+
+[docs] +class InvalidTimeSeriesException(ValueError): + pass
+ + + +
+[docs] +def timeseries_json_to_pandas(timeseries: dict, index: str = "date", auto_collapse=True) -> pandas.DataFrame: + """ + Convert a timeseries JSON object as returned by the `aggregate_spatial` process to a pandas DataFrame object + + This timeseries data has three dimensions in general: date, polygon index and band index. + One of these will be used as index of the resulting dataframe (as specified by the `index` argument), + and the other two will be used as multilevel columns. + When there is just a single polygon or band in play, the dataframe will be simplified + by removing the corresponding dimension if `auto_collapse` is enabled (on by default). + + :param timeseries: dictionary as returned by `aggregate_spatial` + :param index: which dimension should be used for the DataFrame index: 'date' or 'polygon' + :param auto_collapse: whether single band or single polygon cases should be simplified automatically + + :return: pandas DataFrame or Series + """ + # The input timeseries dictionary is assumed to have this structure: + # {dict mapping date -> [list with one item per polygon: [list with one float/None per band or empty list]]} + # TODO is this format of `aggregate_spatial` standardized across backends? Or can we detect the structure? + # TODO: option to pass a path to a JSON file as input? + + # Some quick checks + if len(timeseries) == 0: + raise InvalidTimeSeriesException("Empty data set") + polygon_counts = set(len(polygon_data) for polygon_data in timeseries.values()) + if polygon_counts == {0}: + raise InvalidTimeSeriesException("No polygon data for each date") + elif 0 in polygon_counts: + # TODO: still support this use case? + raise InvalidTimeSeriesException("No polygon data for some dates ({p})".format(p=polygon_counts)) + elif len(polygon_counts) > 1: + raise InvalidTimeSeriesException("Inconsistent polygon counts: {p}".format(p=polygon_counts)) + # Count the number of bands in the timeseries, so we can provide a fallback array for missing data + band_counts = set(len(band_data) for polygon_data in timeseries.values() for band_data in polygon_data) + if band_counts == {0}: + raise InvalidTimeSeriesException("Zero bands everywhere") + band_counts.discard(0) + if len(band_counts) != 1: + raise InvalidTimeSeriesException("Inconsistent band counts: {b}".format(b=band_counts)) + band_count = band_counts.pop() + band_data_fallback = [np.nan] * band_count + # Load the timeseries data in a pandas Series with multi-index ["date", "polygon", "band"] + s = pandas.DataFrame.from_records( + ( + (date, polygon_index, band_index, value) + for (date, polygon_data) in timeseries.items() + for polygon_index, band_data in enumerate(polygon_data) + for band_index, value in enumerate(band_data or band_data_fallback) + ), + columns=["date", "polygon", "band", "value"], + index=["date", "polygon", "band"] + )["value"].rename(None) + # TODO convert date to real date index? + + if auto_collapse: + if s.index.levshape[2] == 1: + # Single band case + s.index = s.index.droplevel("band") + if s.index.levshape[1] == 1: + # Single polygon case + s.index = s.index.droplevel("polygon") + + # Reshape as desired + if index == "date": + if len(s.index.names) > 1: + return s.unstack("date").T + else: + return s + elif index == "polygon": + return s.unstack("polygon").T + else: + raise ValueError(index)
+ + + +
+[docs] +@deprecated("Use :py:meth:`XarrayDataCube.from_file` instead.", version="0.7.0") +def datacube_from_file(filename, fmt="netcdf") -> XarrayDataCube: + from openeo.udf.xarraydatacube import XarrayDataCube + return XarrayDataCube.from_file(path=filename, fmt=fmt)
+ + + +
+[docs] +@deprecated("Use :py:meth:`XarrayDataCube.save_to_file` instead.", version="0.7.0") +def datacube_to_file(datacube: XarrayDataCube, filename, fmt="netcdf"): + return datacube.save_to_file(path=filename, fmt=fmt)
+ + + +@deprecated("Use :py:meth:`XarrayIO.to_json_file` instead", version="0.7.0") +def _save_DataArray_to_JSON(filename, array: xarray.DataArray): + from openeo.udf.xarraydatacube import XarrayIO + return XarrayIO.to_json_file(array=array, path=filename) + + +@deprecated("Use :py:meth:`XarrayIO.to_netcdf_file` instead", version="0.7.0") +def _save_DataArray_to_NetCDF(filename, array: xarray.DataArray): + from openeo.udf.xarraydatacube import XarrayIO + return XarrayIO.to_netcdf_file(array=array, path=filename) + + +
+[docs] +@deprecated("Use :py:meth:`XarrayDataCube.plot` instead.", version="0.7.0") +def datacube_plot(datacube: XarrayDataCube, *args, **kwargs): + datacube.plot(*args, **kwargs)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/datacube.html b/_modules/openeo/rest/datacube.html new file mode 100644 index 000000000..93fd14037 --- /dev/null +++ b/_modules/openeo/rest/datacube.html @@ -0,0 +1,3142 @@ + + + + + + + openeo.rest.datacube — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.datacube

+"""
+The main module for creating earth observation processes. It aims to easily build complex process chains, that can
+be evaluated by an openEO backend.
+
+.. data:: THIS
+
+    Symbolic reference to the current data cube, to be used as argument in :py:meth:`DataCube.process()` calls
+
+"""
+from __future__ import annotations
+
+import datetime
+import logging
+import pathlib
+import typing
+import warnings
+from builtins import staticmethod
+from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
+
+import numpy as np
+import requests
+import shapely.geometry
+import shapely.geometry.base
+from shapely.geometry import MultiPolygon, Polygon, mapping
+
+from openeo.api.process import Parameter
+from openeo.dates import get_temporal_extent
+from openeo.internal.documentation import openeo_process
+from openeo.internal.graph_building import PGNode, ReduceNode, _FromNodeMixin
+from openeo.internal.jupyter import in_jupyter_context
+from openeo.internal.processes.builder import (
+    ProcessBuilderBase,
+    convert_callable_to_pgnode,
+    get_parameter_names,
+)
+from openeo.internal.warnings import UserDeprecationWarning, deprecated, legacy_alias
+from openeo.metadata import (
+    Band,
+    BandDimension,
+    CollectionMetadata,
+    SpatialDimension,
+    TemporalDimension,
+    metadata_from_stac,
+)
+from openeo.processes import ProcessBuilder
+from openeo.rest import BandMathException, OpenEoClientException, OperatorException
+from openeo.rest._datacube import (
+    THIS,
+    UDF,
+    _ensure_save_result,
+    _ProcessGraphAbstraction,
+    build_child_callback,
+)
+from openeo.rest.graph_building import CollectionProperty
+from openeo.rest.job import BatchJob, RESTJob
+from openeo.rest.mlmodel import MlModel
+from openeo.rest.service import Service
+from openeo.rest.udp import RESTUserDefinedProcess
+from openeo.rest.vectorcube import VectorCube
+from openeo.util import dict_no_none, guess_format, normalize_crs, rfc3339
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    import xarray
+
+    from openeo.rest.connection import Connection
+    from openeo.udf import XarrayDataCube
+
+
+log = logging.getLogger(__name__)
+
+
+# Type annotation aliases
+InputDate = Union[str, datetime.date, Parameter, PGNode, ProcessBuilderBase, None]
+
+
+
+[docs] +class DataCube(_ProcessGraphAbstraction): + """ + Class representing a openEO (raster) data cube. + + The data cube is represented by its corresponding openeo "process graph" + and this process graph can be "grown" to a desired workflow by calling the appropriate methods. + """ + + # TODO: set this based on back-end or user preference? + _DEFAULT_RASTER_FORMAT = "GTiff" + +
+[docs] + def __init__( + self, graph: PGNode, connection: Optional[Connection] = None, metadata: Optional[CollectionMetadata] = None + ): + super().__init__(pgnode=graph, connection=connection) + self.metadata: Optional[CollectionMetadata] = metadata
+ + +
+[docs] + def process( + self, + process_id: str, + arguments: Optional[dict] = None, + metadata: Optional[CollectionMetadata] = None, + namespace: Optional[str] = None, + **kwargs, + ) -> DataCube: + """ + Generic helper to create a new DataCube by applying a process. + + :param process_id: process id of the process. + :param arguments: argument dictionary for the process. + :param metadata: optional: metadata to override original cube metadata (e.g. when reducing dimensions) + :param namespace: optional: process namespace + :return: new DataCube instance + """ + pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs) + return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
+ + + graph_add_node = legacy_alias(process, "graph_add_node", since="0.1.1") + +
+[docs] + def process_with_node(self, pg: PGNode, metadata: Optional[CollectionMetadata] = None) -> DataCube: + """ + Generic helper to create a new DataCube by applying a process (given as process graph node) + + :param pg: process graph node (containing process id and arguments) + :param metadata: optional: metadata to override original cube metadata (e.g. when reducing dimensions) + :return: new DataCube instance + """ + # TODO: deep copy `self.metadata` instead of using same instance? + # TODO: cover more cases where metadata has to be altered + # TODO: deprecate `process_with_node``: little added value over just calling DataCube() directly + return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
+ + + def _do_metadata_normalization(self) -> bool: + """Do metadata-based normalization/validation of dimension names, band names, ...""" + return isinstance(self.metadata, CollectionMetadata) + + def _assert_valid_dimension_name(self, name: str) -> str: + if self._do_metadata_normalization(): + self.metadata.assert_valid_dimension(name) + return name + +
+[docs] + @classmethod + @openeo_process + def load_collection( + cls, + collection_id: Union[str, Parameter], + connection: Optional[Connection] = None, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Union[None, List[str], Parameter] = None, + fetch_metadata: bool = True, + properties: Union[ + None, Dict[str, Union[str, PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty + ] = None, + max_cloud_cover: Optional[float] = None, + ) -> DataCube: + """ + Create a new Raster Data cube. + + :param collection_id: image collection identifier + :param connection: The backend connection to use. + Can be ``None`` to work without connection and collection metadata. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands. + :param properties: limit data by metadata property predicates. + See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of such predicates. + :param max_cloud_cover: shortcut to set maximum cloud cover ("eo:cloud_cover" collection property) + :return: new DataCube containing the collection + + .. versionchanged:: 0.13.0 + added the ``max_cloud_cover`` argument. + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + + .. versionchanged:: 0.26.0 + Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument. + """ + if temporal_extent: + temporal_extent = cls._get_temporal_extent(extent=temporal_extent) + + if isinstance(spatial_extent, Parameter): + if spatial_extent.schema.get("type") != "object": + warnings.warn( + "Unexpected parameterized `spatial_extent` in `load_collection`:" + f" expected schema with type 'object' but got {spatial_extent.schema!r}." + ) + arguments = { + 'id': collection_id, + # TODO: spatial_extent could also be a "geojson" subtype object, so we might want to allow (and convert) shapely shapes as well here. + 'spatial_extent': spatial_extent, + 'temporal_extent': temporal_extent, + } + if isinstance(collection_id, Parameter): + fetch_metadata = False + metadata: Optional[CollectionMetadata] = ( + connection.collection_metadata(collection_id) if connection and fetch_metadata else None + ) + if bands: + if isinstance(bands, str): + bands = [bands] + elif isinstance(bands, Parameter): + metadata = None + if metadata: + bands = [b if isinstance(b, str) else metadata.band_dimension.band_name(b) for b in bands] + metadata = metadata.filter_bands(bands) + arguments['bands'] = bands + + if isinstance(properties, list): + # TODO: warn about items that are not CollectionProperty objects instead of silently dropping them. + properties = {p.name: p.from_node() for p in properties if isinstance(p, CollectionProperty)} + if isinstance(properties, CollectionProperty): + properties = {properties.name: properties.from_node()} + elif properties is None: + properties = {} + if max_cloud_cover: + properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover + if properties: + summaries = metadata and metadata.get("summaries") or {} + undefined_properties = set(properties.keys()).difference(summaries.keys()) + if undefined_properties: + warnings.warn( + f"{collection_id} property filtering with properties that are undefined " + f"in the collection metadata (summaries): {', '.join(undefined_properties)}.", + stacklevel=2, + ) + arguments["properties"] = { + prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items() + } + + pg = PGNode( + process_id='load_collection', + arguments=arguments + ) + return cls(graph=pg, connection=connection, metadata=metadata)
+ + + create_collection = legacy_alias( + load_collection, name="create_collection", since="0.4.6" + ) + +
+[docs] + @classmethod + @deprecated(reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.",version="0.25.0") + def load_disk_collection(cls, connection: Connection, file_format: str, glob_pattern: str, **options) -> DataCube: + """ + Loads image data from disk as a DataCube. + This is backed by a non-standard process ('load_disk_data'). This will eventually be replaced by standard options such as + :py:meth:`openeo.rest.connection.Connection.load_stac` or https://processes.openeo.org/#load_uploaded_files + + + :param connection: The connection to use to connect with the backend. + :param file_format: the file format, e.g. 'GTiff' + :param glob_pattern: a glob pattern that matches the files to load from disk + :param options: options specific to the file format + :return: the data as a DataCube + """ + pg = PGNode( + process_id='load_disk_data', + arguments={ + 'format': file_format, + 'glob_pattern': glob_pattern, + 'options': options + } + ) + return cls(graph=pg, connection=connection)
+ + +
+[docs] + @classmethod + def load_stac( + cls, + url: str, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + connection: Optional[Connection] = None, + ) -> DataCube: + """ + Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`. + A batch job result can be loaded by providing a reference to it. + + If supported by the underlying metadata and file format, the data that is added to the data cube can be + restricted with the parameters ``spatial_extent``, ``temporal_extent`` and ``bands``. + If no data is available for the given extents, a ``NoDataAvailable`` error is thrown. + + Remarks: + + * The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as + specified in the metadata if the ``bands`` parameter is set to ``null``. + * If no additional parameter is specified this would imply that the whole data set is expected to be loaded. + Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only + load the data that is actually required after evaluating subsequent processes such as filters. + This means that the values should be processed only after the data has been limited to the required extent + and as a consequence also to a manageable size. + + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) + or a specific STAC API Collection that allows to filter items and to download assets. + This includes batch job results, which itself are compliant to STAC. + For external URLs, authentication details such as API keys or tokens may need to be included in the URL. + + Batch job results can be specified in two ways: + + - For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with ``/jobs/{id}/results`` and ``{id}`` + is the corresponding batch job ID. + - For external results, a signed URL must be provided. Not all back-ends support signed URLs, + which are provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: + Limits the data to load to the specified bounding box or polygons. + + For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects + with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + + For vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. + + The GeoJSON can be one of the following feature types: + + * A ``Polygon`` or ``MultiPolygon`` geometry, + * a ``Feature`` with a ``Polygon`` or ``MultiPolygon`` geometry, or + * a ``FeatureCollection`` containing at least one ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometries. + + Set this parameter to ``None`` to set no limit for the spatial extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + + :param temporal_extent: + Limits the data to load to the specified left-closed temporal interval. + Applies to all temporal dimensions. + The interval has to be specified as an array with exactly two elements: + + 1. The first element is the start of the temporal interval. + The specified instance in time is **included** in the interval. + 2. The second element is the end of the temporal interval. + The specified instance in time is **excluded** from the interval. + + The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. + + Also supports open intervals by setting one of the boundaries to ``None``, but never both. + + Set this parameter to ``None`` to set no limit for the temporal extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_temporal()`` directly after loading unbounded data. + + :param bands: + Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. + + Either the unique band name (metadata field ``name`` in bands) or one of the common band names + (metadata field ``common_name`` in bands) can be specified. + If the unique band name and the common name conflict, the unique band name has a higher priority. + + The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. + + It is recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :param properties: + Limits the data by metadata properties to include only data in the data cube which + all given conditions return ``True`` for (AND operation). + + Specify key-value-pairs with the key being the name of the metadata property, + which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. + This parameter is not supported for static STAC. + + :param connection: The connection to use to connect with the backend. + + .. versionadded:: 0.33.0 + + """ + arguments = {"url": url} + # TODO #425 more normalization/validation of extent/band parameters + if spatial_extent: + arguments["spatial_extent"] = spatial_extent + if temporal_extent: + arguments["temporal_extent"] = DataCube._get_temporal_extent(extent=temporal_extent) + if bands: + arguments["bands"] = bands + if properties: + arguments["properties"] = { + prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items() + } + graph = PGNode("load_stac", arguments=arguments) + try: + metadata = metadata_from_stac(url) + except Exception: + log.warning(f"Failed to extract cube metadata from STAC URL {url}", exc_info=True) + metadata = None + return cls(graph=graph, connection=connection, metadata=metadata)
+ + + @classmethod + def _get_temporal_extent( + cls, + *args, + start_date: InputDate = None, + end_date: InputDate = None, + extent: Union[Sequence[InputDate], Parameter, str, None] = None, + ) -> Union[List[Union[str, Parameter, PGNode, None]], Parameter]: + """Parameter aware temporal_extent normalizer""" + # TODO: move this outside of DataCube class + # TODO: return extent as tuple instead of list + if len(args) == 1 and isinstance(args[0], Parameter): + assert start_date is None and end_date is None and extent is None + return args[0] + elif len(args) == 0 and isinstance(extent, Parameter): + assert start_date is None and end_date is None + # TODO: warn about unexpected parameter schema + return extent + else: + def convertor(d: Any) -> Any: + # TODO: can this be generalized through _FromNodeMixin? + if isinstance(d, Parameter) or isinstance(d, PGNode): + # TODO: warn about unexpected parameter schema + return d + elif isinstance(d, ProcessBuilderBase): + return d.pgnode + else: + return rfc3339.normalize(d) + + return list( + get_temporal_extent(*args, start_date=start_date, end_date=end_date, extent=extent, convertor=convertor) + ) + +
+[docs] + @openeo_process + def filter_temporal( + self, + *args, + start_date: InputDate = None, + end_date: InputDate = None, + extent: Union[Sequence[InputDate], Parameter, str, None] = None, + ) -> DataCube: + """ + Limit the DataCube to a certain date range, which can be specified in several ways: + + >>> cube.filter_temporal("2019-07-01", "2019-08-01") + >>> cube.filter_temporal(["2019-07-01", "2019-08-01"]) + >>> cube.filter_temporal(extent=["2019-07-01", "2019-08-01"]) + >>> cube.filter_temporal(start_date="2019-07-01", end_date="2019-08-01"]) + + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + + :param start_date: start date of the filter (inclusive), as a string or date object + :param end_date: end date of the filter (exclusive), as a string or date object + :param extent: temporal extent. + Typically, specified as a two-item list or tuple containing start and end date. + + .. versionchanged:: 0.23.0 + Arguments ``start_date``, ``end_date`` and ``extent``: + add support for year/month shorthand notation as discussed at :ref:`date-shorthand-handling`. + """ + if len(args) == 1 and isinstance(args[0], (str)): + raise OpenEoClientException( + f"filter_temporal() with a single string argument ({args[0]!r}) is ambiguous." + f" If you want a half-unbounded interval, use something like filter_temporal({args[0]!r}, None) or use explicit keyword arguments." + f" If you want the full interval covering all of {args[0]!r}, use something like filter_temporal(extent={args[0]!r})." + ) + return self.process( + process_id='filter_temporal', + arguments={ + 'data': THIS, + 'extent': self._get_temporal_extent(*args, start_date=start_date, end_date=end_date, extent=extent) + } + )
+ + +
+[docs] + @openeo_process + def filter_bbox( + self, + *args, + west: Optional[float] = None, + south: Optional[float] = None, + east: Optional[float] = None, + north: Optional[float] = None, + crs: Optional[Union[int, str]] = None, + base: Optional[float] = None, + height: Optional[float] = None, + bbox: Optional[Sequence[float]] = None, + ) -> DataCube: + """ + Limits the data cube to the specified bounding box. + + The bounding box can be specified in multiple ways. + + - With keyword arguments:: + + >>> cube.filter_bbox(west=3, south=51, east=4, north=52, crs=4326) + + - With a (west, south, east, north) list or tuple + (note that EPSG:4326 is the default CRS, so it's not necessary to specify it explicitly):: + + >>> cube.filter_bbox([3, 51, 4, 52]) + >>> cube.filter_bbox(bbox=[3, 51, 4, 52]) + + - With a bbox dictionary:: + + >>> bbox = {"west": 3, "south": 51, "east": 4, "north": 52, "crs": 4326} + >>> cube.filter_bbox(bbox) + >>> cube.filter_bbox(bbox=bbox) + >>> cube.filter_bbox(**bbox) + + - With a shapely geometry (of which the bounding box will be used):: + + >>> cube.filter_bbox(geometry) + >>> cube.filter_bbox(bbox=geometry) + + - Passing a parameter:: + + >>> bbox_param = Parameter(name="my_bbox", schema="object") + >>> cube.filter_bbox(bbox_param) + >>> cube.filter_bbox(bbox=bbox_param) + + - With a CRS other than EPSG 4326:: + + >>> cube.filter_bbox( + ... west=652000, east=672000, north=5161000, south=5181000, + ... crs=32632 + ... ) + + - Deprecated: positional arguments are also supported, + but follow a non-standard order for legacy reasons:: + + >>> west, east, north, south = 3, 4, 52, 51 + >>> cube.filter_bbox(west, east, north, south) + + :param crs: value describing the coordinate reference system. + Typically just an int (interpreted as EPSG code, e.g. ``4326``) + or a string (handled as authority string, e.g. ``"EPSG:4326"``). + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + """ + if args and any(k is not None for k in (west, south, east, north, bbox)): + raise ValueError("Don't mix positional arguments with keyword arguments.") + if bbox and any(k is not None for k in (west, south, east, north)): + raise ValueError("Don't mix `bbox` with `west`/`south`/`east`/`north` keyword arguments.") + + if args: + if 4 <= len(args) <= 5: + # Handle old-style west-east-north-south order + # TODO remove handling of this legacy order? + warnings.warn("Deprecated argument order usage: `filter_bbox(west, east, north, south)`." + " Use keyword arguments or tuple/list argument instead.") + west, east, north, south = args[:4] + if len(args) > 4: + crs = normalize_crs(args[4]) + elif len(args) == 1 and (isinstance(args[0], (list, tuple)) and len(args[0]) == 4 + or isinstance(args[0], (dict, shapely.geometry.base.BaseGeometry, Parameter))): + bbox = args[0] + else: + raise ValueError(args) + + if isinstance(bbox, Parameter): + if bbox.schema.get("type") != "object": + warnings.warn( + "Unexpected parameterized `extent` in `filter_bbox`:" + f" expected schema with type 'object' but got {bbox.schema!r}." + ) + extent = bbox + else: + if bbox: + if isinstance(bbox, shapely.geometry.base.BaseGeometry): + west, south, east, north = bbox.bounds + elif isinstance(bbox, (list, tuple)) and len(bbox) == 4: + west, south, east, north = bbox[:4] + elif isinstance(bbox, dict): + west, south, east, north = (bbox[k] for k in ["west", "south", "east", "north"]) + if "crs" in bbox: + crs = bbox["crs"] + else: + raise ValueError(bbox) + + extent = {'west': west, 'east': east, 'north': north, 'south': south} + extent.update(dict_no_none(crs=crs, base=base, height=height)) + + return self.process( + process_id='filter_bbox', + arguments={ + 'data': THIS, + 'extent': extent + } + )
+ + +
+[docs] + @openeo_process + def filter_spatial(self, geometries) -> DataCube: + """ + Limits the data cube over the spatial dimensions to the specified geometries. + + - For polygons, the filter retains a pixel in the data cube if the point at the pixel center intersects with + at least one of the polygons (as defined in the Simple Features standard by the OGC). + - For points, the process considers the closest pixel center. + - For lines (line strings), the process considers all the pixels whose centers are closest to at least one + point on the line. + + More specifically, pixels outside of the bounding box of the given geometry will not be available after filtering. + All pixels inside the bounding box that are not retained will be set to null (no data). + + :param geometries: One or more geometries used for filtering, specified as GeoJSON in EPSG:4326. + :return: A data cube restricted to the specified geometries. The dimensions and dimension properties (name, + type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less + (or the same) dimension labels. + """ + valid_geojson_types = [ + "Point", "MultiPoint", "LineString", "MultiLineString", + "Polygon", "MultiPolygon", "GeometryCollection", "FeatureCollection" + ] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types, crs=None) + return self.process( + process_id='filter_spatial', + arguments={ + 'data': THIS, + 'geometries': geometries + } + )
+ + +
+[docs] + @openeo_process + def filter_bands(self, bands: Union[List[Union[str, int]], str]) -> DataCube: + """ + Filter the data cube by the given bands + + :param bands: list of band names, common names or band indices. Single band name can also be given as string. + :return: a DataCube instance + """ + if isinstance(bands, str): + bands = [bands] + if self._do_metadata_normalization(): + bands = [self.metadata.band_dimension.band_name(b) for b in bands] + cube = self.process( + process_id="filter_bands", + arguments={"data": THIS, "bands": bands}, + metadata=self.metadata.filter_bands(bands) if self.metadata else None, + ) + return cube
+ + +
+[docs] + @openeo_process + def filter_labels( + self, condition: Union[PGNode, Callable], dimension: str, context: Optional[dict] = None + ) -> DataCube: + """ + Filters the dimension labels in the data cube for the given dimension. + Only the dimension labels that match the specified condition are preserved, + all other labels with their corresponding data get removed. + + :param condition: the "child callback" which will be given a single label value (number or string) + and returns a boolean expressing if the label should be preserved. + Also see :ref:`callbackfunctions`. + :param dimension: The name of the dimension to filter on. + + .. versionadded:: 0.27.0 + """ + condition = build_child_callback(condition, parent_parameters=["value"]) + return self.process( + process_id="filter_labels", + arguments=dict_no_none(data=THIS, condition=condition, dimension=dimension, context=context), + )
+ + + band_filter = legacy_alias(filter_bands, "band_filter", since="0.1.0") + +
+[docs] + def band(self, band: Union[str, int]) -> DataCube: + """ + Filter out a single band + + :param band: band name, band common name or band index. + :return: a DataCube instance + """ + if self._do_metadata_normalization(): + band = self.metadata.band_dimension.band_index(band) + arguments = {"data": {"from_parameter": "data"}} + if isinstance(band, int): + arguments["index"] = band + else: + arguments["label"] = band + return self.reduce_bands(reducer=PGNode(process_id="array_element", arguments=arguments))
+ + +
+[docs] + @openeo_process + def resample_spatial( + self, resolution: Union[float, Tuple[float, float]], projection: Union[int, str] = None, + method: str = 'near', align: str = 'upper-left' + ) -> DataCube: + return self.process('resample_spatial', { + 'data': THIS, + 'resolution': resolution, + 'projection': projection, + 'method': method, + 'align': align + })
+ + +
+[docs] + def resample_cube_spatial(self, target: DataCube, method: str = "near") -> DataCube: + """ + Resamples the spatial dimensions (x,y) from a source data cube to align with the corresponding + dimensions of the given target data cube. + Returns a new data cube with the resampled dimensions. + + To resample a data cube to a specific resolution or projection regardless of an existing target + data cube, refer to :py:meth:`resample_spatial`. + + :param target: A data cube that describes the spatial target resolution. + :param method: Resampling method to use. + :return: + """ + return self.process("resample_cube_spatial", {"data": self, "target": target, "method": method})
+ + +
+[docs] + @openeo_process + def resample_cube_temporal( + self, target: DataCube, dimension: Optional[str] = None, valid_within: Optional[int] = None + ) -> DataCube: + """ + Resamples one or more given temporal dimensions from a source data cube to align with the corresponding + dimensions of the given target data cube using the nearest neighbor method. + Returns a new data cube with the resampled dimensions. + + By default, this process simply takes the nearest neighbor independent of the value (including values such as + no-data / ``null``). Depending on the data cubes this may lead to values being assigned to two target timestamps. + To only consider valid values in a specific range around the target timestamps, use the parameter ``valid_within``. + + The rare case of ties is resolved by choosing the earlier timestamps. + + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample. + :param valid_within: + :return: + + .. versionadded:: 0.10.0 + """ + return self.process( + "resample_cube_temporal", + dict_no_none({"data": self, "target": target, "dimension": dimension, "valid_within": valid_within}) + )
+ + + def _operator_binary(self, operator: str, other: Union[DataCube, int, float], reverse=False) -> DataCube: + """Generic handling of (mathematical) binary operator""" + band_math_mode = self._in_bandmath_mode() + if band_math_mode: + if isinstance(other, (int, float)): + return self._bandmath_operator_binary_scalar(operator, other, reverse=reverse) + elif isinstance(other, DataCube): + return self._bandmath_operator_binary_cubes(operator, other) + else: + if isinstance(other, DataCube): + return self._merge_operator_binary_cubes(operator, other) + elif isinstance(other, (int, float)): + # "`apply` math" mode + return self._apply_operator( + operator=operator, other=other, reverse=reverse + ) + raise OperatorException( + f"Unsupported operator {operator!r} with `other` type {type(other)!r} (band math mode={band_math_mode})" + ) + + def _operator_unary(self, operator: str, **kwargs) -> DataCube: + band_math_mode = self._in_bandmath_mode() + if band_math_mode: + return self._bandmath_operator_unary(operator, **kwargs) + else: + return self._apply_operator(operator=operator, extra_arguments=kwargs) + + def _apply_operator( + self, + operator: str, + other: Optional[Union[int, float]] = None, + reverse: Optional[bool] = None, + extra_arguments: Optional[dict] = None, + ) -> DataCube: + """ + Apply a unary or binary operator/process, + by appending to existing `apply` node, or starting a new one. + + :param operator: process id of operator + :param other: for binary operators: "other" argument + :param reverse: for binary operators: "self" and "other" should be swapped (reflected operator mode) + """ + if self.result_node().process_id == "apply": + # Append to existing `apply` node + orig_apply = self.result_node() + data = orig_apply.arguments["data"] + x = {"from_node": orig_apply.arguments["process"]["process_graph"]} + context = orig_apply.arguments.get("context") + else: + # Start new `apply` node. + data = self + x = {"from_parameter": "x"} + context = None + # Build args for child callback. + args = {"x": x, **(extra_arguments or {})} + if other is not None: + # Binary operator mode + args["y"] = other + if reverse: + args["x"], args["y"] = args["y"], args["x"] + child_pg = PGNode(process_id=operator, arguments=args) + return self.process_with_node( + PGNode( + process_id="apply", + arguments=dict_no_none( + data=data, + process={"process_graph": child_pg}, + context=context, + ), + ) + ) + +
+[docs] + @openeo_process(mode="operator") + def add(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("add", other, reverse=reverse)
+ + +
+[docs] + @openeo_process(mode="operator") + def subtract(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("subtract", other, reverse=reverse)
+ + +
+[docs] + @openeo_process(mode="operator") + def divide(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("divide", other, reverse=reverse)
+ + +
+[docs] + @openeo_process(mode="operator") + def multiply(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("multiply", other, reverse=reverse)
+ + +
+[docs] + @openeo_process + def normalized_difference(self, other: DataCube) -> DataCube: + # This DataCube method is only a convenience function when in band math mode + assert self._in_bandmath_mode() + assert other._in_bandmath_mode() + return self._operator_binary("normalized_difference", other)
+ + +
+[docs] + @openeo_process(process_id="or", mode="operator") + def logical_or(self, other: DataCube) -> DataCube: + """ + Apply element-wise logical `or` operation + + :param other: + :return: logical_or(this, other) + """ + return self._operator_binary("or", other)
+ + +
+[docs] + @openeo_process(process_id="and", mode="operator") + def logical_and(self, other: DataCube) -> DataCube: + """ + Apply element-wise logical `and` operation + + :param other: + :return: logical_and(this, other) + """ + return self._operator_binary("and", other)
+ + + @openeo_process(process_id="not", mode="operator") + def __invert__(self) -> DataCube: + return self._operator_unary("not") + + @openeo_process(process_id="neq", mode="operator") + def __ne__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("neq", other) + + @openeo_process(process_id="eq", mode="operator") + def __eq__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pixelwise comparison of this data cube with another cube or constant. + + :param other: Another data cube, or a constant + :return: + """ + return self._operator_binary("eq", other) + + @openeo_process(process_id="gt", mode="operator") + def __gt__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pairwise comparison of the bands in this data cube with the bands in the 'other' data cube. + + :param other: + :return: this > other + """ + return self._operator_binary("gt", other) + + @openeo_process(process_id="ge", mode="operator") + def __ge__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("gte", other) + + @openeo_process(process_id="lt", mode="operator") + def __lt__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pairwise comparison of the bands in this data cube with the bands in the 'other' data cube. + The number of bands in both data cubes has to be the same. + + :param other: + :return: this < other + """ + return self._operator_binary("lt", other) + + @openeo_process(process_id="le", mode="operator") + def __le__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("lte", other) + + @openeo_process(process_id="add", mode="operator") + def __add__(self, other) -> DataCube: + return self.add(other) + + @openeo_process(process_id="add", mode="operator") + def __radd__(self, other) -> DataCube: + return self.add(other, reverse=True) + + @openeo_process(process_id="subtract", mode="operator") + def __sub__(self, other) -> DataCube: + return self.subtract(other) + + @openeo_process(process_id="subtract", mode="operator") + def __rsub__(self, other) -> DataCube: + return self.subtract(other, reverse=True) + + @openeo_process(process_id="multiply", mode="operator") + def __neg__(self) -> DataCube: + return self.multiply(-1) + + @openeo_process(process_id="multiply", mode="operator") + def __mul__(self, other) -> DataCube: + return self.multiply(other) + + @openeo_process(process_id="multiply", mode="operator") + def __rmul__(self, other) -> DataCube: + return self.multiply(other, reverse=True) + + @openeo_process(process_id="divide", mode="operator") + def __truediv__(self, other) -> DataCube: + return self.divide(other) + + @openeo_process(process_id="divide", mode="operator") + def __rtruediv__(self, other) -> DataCube: + return self.divide(other, reverse=True) + + @openeo_process(process_id="power", mode="operator") + def __rpow__(self, other) -> DataCube: + return self._power(other, reverse=True) + + @openeo_process(process_id="power", mode="operator") + def __pow__(self, other) -> DataCube: + return self._power(other, reverse=False) + + def _power(self, other, reverse=False): + node = self._get_bandmath_node() + x = node.reducer_process_graph() + y = other + if reverse: + x, y = y, x + return self.process_with_node(node.clone_with_new_reducer( + PGNode(process_id="power", base=x, p=y) + )) + +
+[docs] + @openeo_process(process_id="power", mode="operator") + def power(self, p: float): + return self._power(other=p, reverse=False)
+ + +
+[docs] + @openeo_process(process_id="ln", mode="operator") + def ln(self) -> DataCube: + return self._operator_unary("ln")
+ + +
+[docs] + @openeo_process(process_id="log", mode="operator") + def logarithm(self, base: float) -> DataCube: + return self._operator_unary("log", base=base)
+ + +
+[docs] + @openeo_process(process_id="log", mode="operator") + def log2(self) -> DataCube: + return self.logarithm(base=2)
+ + +
+[docs] + @openeo_process(process_id="log", mode="operator") + def log10(self) -> DataCube: + return self.logarithm(base=10)
+ + + @openeo_process(process_id="or", mode="operator") + def __or__(self, other) -> DataCube: + return self.logical_or(other) + + @openeo_process(process_id="and", mode="operator") + def __and__(self, other): + return self.logical_and(other) + + def _bandmath_operator_binary_cubes( + self, operator, other: DataCube, left_arg_name="x", right_arg_name="y" + ) -> DataCube: + """Band math binary operator with cube as right hand side argument""" + left = self._get_bandmath_node() + right = other._get_bandmath_node() + if left.arguments["data"] != right.arguments["data"]: + raise BandMathException("'Band math' between bands of different data cubes is not supported yet.") + + # Build reducer's sub-processgraph + merged = PGNode( + process_id=operator, + arguments={ + left_arg_name: {"from_node": left.reducer_process_graph()}, + right_arg_name: {"from_node": right.reducer_process_graph()}, + }, + ) + return self.process_with_node(left.clone_with_new_reducer(merged)) + + def _bandmath_operator_binary_scalar(self, operator: str, other: Union[int, float], reverse=False) -> DataCube: + """Band math binary operator with scalar value (int or float) as right hand side argument""" + node = self._get_bandmath_node() + x = {'from_node': node.reducer_process_graph()} + y = other + if reverse: + x, y = y, x + return self.process_with_node(node.clone_with_new_reducer( + PGNode(operator, x=x, y=y) + )) + + def _bandmath_operator_unary(self, operator: str, **kwargs) -> DataCube: + node = self._get_bandmath_node() + return self.process_with_node(node.clone_with_new_reducer( + PGNode(operator, x={'from_node': node.reducer_process_graph()}, **kwargs) + )) + + def _in_bandmath_mode(self) -> bool: + """So-called "band math" mode: current result node is reduce_dimension along "bands" dimension.""" + # TODO #123 is it (still) necessary to make "band" math a special case? + return isinstance(self._pg, ReduceNode) and self._pg.band_math_mode + + def _get_bandmath_node(self) -> ReduceNode: + """Check we are in bandmath mode and return the node""" + if not self._in_bandmath_mode(): + raise BandMathException("Must be in band math mode already") + return self._pg + + def _merge_operator_binary_cubes( + self, operator: str, other: DataCube, left_arg_name="x", right_arg_name="y" + ) -> DataCube: + """Merge two cubes with given operator as overlap_resolver.""" + # TODO #123 reuse an existing merge_cubes process graph if it already exists? + return self.merge_cubes(other, overlap_resolver=PGNode( + process_id=operator, + arguments={ + left_arg_name: {"from_parameter": "x"}, + right_arg_name: {"from_parameter": "y"}, + } + )) + + def _get_geometry_argument( + self, + geometry: Union[ + shapely.geometry.base.BaseGeometry, + dict, + str, + pathlib.Path, + Parameter, + _FromNodeMixin, + ], + valid_geojson_types: List[str], + crs: Optional[str] = None, + ) -> Union[dict, Parameter, PGNode]: + """ + Convert input to a geometry as "geojson" subtype object. + + :param crs: value that encodes a coordinate reference system. + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + """ + if isinstance(geometry, (str, pathlib.Path)): + # Assumption: `geometry` is path to polygon is a path to vector file at backend. + # TODO #104: `read_vector` is non-standard process. + # TODO: If path exists client side: load it client side? + return PGNode(process_id="read_vector", arguments={"filename": str(geometry)}) + elif isinstance(geometry, Parameter): + return geometry + elif isinstance(geometry, _FromNodeMixin): + return geometry.from_node() + + if isinstance(geometry, shapely.geometry.base.BaseGeometry): + geometry = mapping(geometry) + if not isinstance(geometry, dict): + raise OpenEoClientException("Invalid geometry argument: {g!r}".format(g=geometry)) + + if geometry.get("type") not in valid_geojson_types: + raise OpenEoClientException("Invalid geometry type {t!r}, must be one of {s}".format( + t=geometry.get("type"), s=valid_geojson_types + )) + if crs: + # TODO: don't warn when the crs is Lon-Lat like EPSG:4326? + warnings.warn(f"Geometry with non-Lon-Lat CRS {crs!r} is only supported by specific back-ends.") + # TODO #204 alternative for non-standard CRS in GeoJSON object? + epsg_code = normalize_crs(crs) + if epsg_code is not None: + # proj did recognize the CRS + crs_name = f"EPSG:{epsg_code}" + else: + # proj did not recognise this CRS + warnings.warn(f"non-Lon-Lat CRS {crs!r} is not known to the proj library and might not be supported.") + crs_name = crs + geometry["crs"] = {"type": "name", "properties": {"name": crs_name}} + return geometry + +
+[docs] + @openeo_process + def aggregate_spatial( + self, + geometries: Union[ + shapely.geometry.base.BaseGeometry, + dict, + str, + pathlib.Path, + Parameter, + VectorCube, + ], + reducer: Union[str, typing.Callable, PGNode], + target_dimension: Optional[str] = None, + crs: Optional[Union[int, str]] = None, + context: Optional[dict] = None, + # TODO arguments: target dimension, context + ) -> VectorCube: + """ + Aggregates statistics for one or more geometries (e.g. zonal statistics for polygons) + over the spatial dimensions. + + :param geometries: a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute <openeo.processes.max>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.min()`` (function or lambda) + + :param target_dimension: The new dimension name to be used for storing the results. + :param crs: The spatial reference system of the provided polygon. + By default, longitude-latitude (EPSG:4326) is assumed. + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + + :param context: Additional data to be passed to the reducer process. + + .. note:: this ``crs`` argument is a non-standard/experimental feature, only supported by specific back-ends. + See https://github.com/Open-EO/openeo-processes/issues/235 for details. + """ + valid_geojson_types = [ + "Point", "MultiPoint", "LineString", "MultiLineString", + "Polygon", "MultiPolygon", "GeometryCollection", "Feature", "FeatureCollection" + ] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types, crs=crs) + reducer = build_child_callback(reducer, parent_parameters=["data"]) + return VectorCube( + graph=self._build_pgnode( + process_id="aggregate_spatial", + data=THIS, + geometries=geometries, + reducer=reducer, + arguments=dict_no_none( + target_dimension=target_dimension, context=context + ), + ), + connection=self._connection, + # TODO: also add new "geometry" dimension #457 + metadata=None if self.metadata is None else self.metadata.reduce_spatial(), + )
+ + +
+[docs] + @openeo_process + def aggregate_spatial_window( + self, + reducer: Union[str, typing.Callable, PGNode], + size: List[int], + boundary: str = "pad", + align: str = "upper-left", + context: Optional[dict] = None, + # TODO arguments: target dimension, context + ) -> DataCube: + """ + Aggregates statistics over the horizontal spatial dimensions (axes x and y) of the data cube. + + The pixel grid for the axes x and y is divided into non-overlapping windows with the size + specified in the parameter size. If the number of values for the axes x and y is not a multiple + of the corresponding window size, the behavior specified in the parameters boundary and align + is applied. For each of these windows, the reducer process computes the result. + + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + :param size: Window size in pixels along the horizontal spatial dimensions. + The first value corresponds to the x axis, the second value corresponds to the y axis. + :param boundary: Behavior to apply if the number of values for the axes x and y is not a + multiple of the corresponding value in the size parameter. + Options are: + + - ``pad`` (default): pad the data cube with the no-data value null to fit the required window size. + - ``trim``: trim the data cube to fit the required window size. + + Use the parameter ``align`` to align the data to the desired corner. + + :param align: If the data requires padding or trimming (see parameter ``boundary``), specifies + to which corner of the spatial extent the data is aligned to. For example, if the data is + aligned to the upper left, the process pads/trims at the lower-right. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. + """ + valid_boundary_types = ["pad", "trim"] + valid_align_types = ["lower-left", "upper-left", "lower-right", "upper-right"] + if boundary not in valid_boundary_types: + raise ValueError(f"Provided boundary type not supported. Please use one of {valid_boundary_types} .") + if align not in valid_align_types: + raise ValueError(f"Provided align type not supported. Please use one of {valid_align_types} .") + if len(size) != 2: + raise ValueError(f"Provided size not supported. Please provide a list of 2 integer values.") + + reducer = build_child_callback(reducer, parent_parameters=["data"]) + arguments = { + "data": THIS, + "boundary": boundary, + "align": align, + "size": size, + "reducer": reducer, + "context": context, + } + return self.process(process_id="aggregate_spatial_window", arguments=arguments)
+ + +
+[docs] + @openeo_process + def apply_dimension( + self, + code: Optional[str] = None, + runtime=None, + # TODO: drop None default of process (when `code` and `runtime` args can be dropped) + process: Union[str, typing.Callable, UDF, PGNode] = None, + version: Optional[str] = None, + # TODO: dimension has no default (per spec)? + dimension: str = "t", + target_dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a process to all pixel values along a dimension of a raster data cube. For example, + if the temporal dimension is specified the process will work on a time series of pixel values. + + The process to apply is specified by either `code` and `runtime` in case of a UDF, or by providing a callback function + in the `process` argument. + + The process reduce_dimension also applies a process to pixel values along a dimension, but drops + the dimension afterwards. The process apply applies a process to each pixel value in the data cube. + + The target dimension is the source dimension if not specified otherwise in the target_dimension parameter. + The pixel values in the target dimension get replaced by the computed pixel values. The name, type and + reference system are preserved. + + The dimension labels are preserved when the target dimension is the source dimension and the number of + pixel values in the source dimension is equal to the number of values computed by the process. Otherwise, + the dimension labels will be incrementing integers starting from zero, which can be changed using + rename_labels afterwards. The number of labels will equal to the number of values computed by the process. + + :param code: [**deprecated**] UDF code or process identifier (optional) + :param runtime: [**deprecated**] UDF runtime to use (optional) + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns an array of numerical values. + For example: + + - ``"sort"`` (string) + - :py:func:`sort <openeo.processes.sort>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.concat([42, -3])`` (function or lambda) + + + :param version: [**deprecated**] Version of the UDF runtime to use + :param dimension: The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or null (the default) to use the source dimension + specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. + The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A datacube with the UDF applied to the given dimension. + :raises: DimensionNotAvailable + + .. versionchanged:: 0.13.0 + arguments ``code``, ``runtime`` and ``version`` are deprecated if favor of the standard approach + of using an :py:class:`UDF <openeo.rest._datacube.UDF>` object in the ``process`` argument. + See :ref:`old_udf_api` for more background about the changes. + + """ + # TODO #137 #181 #312 remove support for code/runtime/version + if runtime or (isinstance(code, str) and "\n" in code) or version: + if process: + raise ValueError( + "Cannot specify `process` argument together with deprecated `code`/`runtime`/`version` arguments." + ) + else: + warnings.warn( + "Specifying UDF code through `code`, `runtime` and `version` arguments is deprecated. " + "Instead create an `openeo.UDF` object and pass that to the `process` argument.", + category=UserDeprecationWarning, + stacklevel=2, + ) + process = UDF(code=code, runtime=runtime, version=version, context=context) + else: + process = process or code + process = build_child_callback( + process=process, parent_parameters=["data", "context"], connection=self.connection + ) + arguments = { + "data": THIS, + "process": process, + "dimension": self._assert_valid_dimension_name(dimension), + } + + metadata = self.metadata + if target_dimension is not None: + arguments["target_dimension"] = target_dimension + metadata = self.metadata.reduce_dimension(dimension_name=dimension) if self.metadata else None + if(not target_dimension in self.metadata.dimension_names()): + metadata = self.metadata.add_dimension(target_dimension, label="unknown") + if context is not None: + arguments["context"] = context + result_cube = self.process(process_id="apply_dimension", arguments=arguments, metadata = metadata) + + return result_cube
+ + +
+[docs] + @openeo_process + def reduce_dimension( + self, + dimension: str, + reducer: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + process_id="reduce_dimension", + band_math_mode: bool = False, + ) -> DataCube: + """ + Add a reduce process with given reducer callback along given dimension + + :param dimension: the label of the dimension to reduce + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute <openeo.processes.max>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.min()`` (function or lambda) + + :param context: Additional data to be passed to the process. + """ + # TODO: check if dimension is valid according to metadata? #116 + # TODO: #125 use/test case for `reduce_dimension_binary`? + reducer = build_child_callback( + process=reducer, parent_parameters=["data", "context"], connection=self.connection + ) + + return self.process_with_node( + ReduceNode( + process_id=process_id, + data=self, + reducer=reducer, + dimension=self._assert_valid_dimension_name(dimension), + context=context, + # TODO #123 is it (still) necessary to make "band" math a special case? + band_math_mode=band_math_mode, + ), + metadata=self.metadata.reduce_dimension(dimension_name=dimension) if self.metadata else None, + )
+ + +
+[docs] + @openeo_process + def reduce_spatial( + self, + reducer: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + ) -> "DataCube": + """ + Add a reduce process with given reducer callback along the spatial dimensions + + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute <openeo.processes.max>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.min()`` (function or lambda) + + :param context: Additional data to be passed to the process. + """ + reducer = build_child_callback( + process=reducer, parent_parameters=["data", "context"], connection=self.connection + ) + return self.process( + process_id="reduce_spatial", + data=self, + reducer=reducer, + context=context, + metadata=self.metadata.reduce_spatial(), + )
+ + +
+[docs] + @deprecated("Use :py:meth:`apply_polygon`.", version="0.26.0") + def chunk_polygon( + self, + chunks: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + process: Union[str, PGNode, typing.Callable, UDF], + mask_value: float = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Apply a process to spatial chunks of a data cube. + + .. warning:: experimental process: not generally supported, API subject to change. + + :param chunks: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param process: "child callback" function, see :ref:`callbackfunctions` + :param mask_value: The value used for cells outside the polygon. + This provides a distinction between NoData cells within the polygon (due to e.g. clouds) + and masked cells outside it. If no value is provided, NoData cells are used outside the polygon. + :param context: Additional data to be passed to the process. + """ + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = [ + "Polygon", + "MultiPolygon", + "GeometryCollection", + "Feature", + "FeatureCollection", + ] + chunks = self._get_geometry_argument( + chunks, valid_geojson_types=valid_geojson_types + ) + mask_value = float(mask_value) if mask_value is not None else None + return self.process( + process_id="chunk_polygon", + data=THIS, + chunks=chunks, + process=process, + arguments=dict_no_none( + mask_value=mask_value, + context=context, + ), + )
+ + +
+[docs] + @openeo_process + def apply_polygon( + self, + geometries: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube] = None, + process: Union[str, PGNode, typing.Callable, UDF] = None, + mask_value: Optional[float] = None, + context: Optional[dict] = None, + **kwargs, + ) -> DataCube: + """ + Apply a process to segments of the data cube that are defined by the given polygons. + For each polygon provided, all pixels for which the point at the pixel center intersects + with the polygon (as defined in the Simple Features standard by the OGC) are collected into sub data cubes. + If a pixel is part of multiple of the provided polygons (e.g., when the polygons overlap), + the GeometriesOverlap exception is thrown. + Each sub data cube is passed individually to the given process. + + :param geometries: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param process: "child callback" function, see :ref:`callbackfunctions` + :param mask_value: The value used for pixels outside the polygon. + :param context: Additional data to be passed to the process. + + .. warning:: experimental process: not generally supported, API subject to change. + + .. versionchanged:: 0.32.0 + Argument ``polygons`` was renamed to ``geometries``. + While deprecated, the old name ``polygons`` is still supported + as keyword argument for backwards compatibility. + """ + # TODO drop support for legacy `polygons` argument: + # remove `kwargs, remove default `None` value for `geometries` and `process` + # and the related backwards compatibility code + geometries_parameter = "geometries" + if geometries is None and "polygons" in kwargs: + geometries = kwargs.pop("polygons") + geometries_parameter = "polygons" + warnings.warn( + "In `apply_polygon` use argument `geometries` instead of deprecated 'polygons'.", + category=UserDeprecationWarning, + stacklevel=2, + ) + if kwargs: + raise ValueError(f"Unexpected keyword arguments: {kwargs!r}") + if not geometries: + raise ValueError("No geometries provided.") + + # Note: the `process` argument was given a default value `None` (with the `polygons`/`geometries` argument rename) + # to keep support for legacy `cube.apply_polygon(polygons=..., process=...)` usage: + # `geometries` had to be given a default value, and so did `process` as it comes after it. + # TODO: remove default value for `process` when dropping support for legacy `polygons` argument + assert process is not None + + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = ["Polygon", "MultiPolygon", "Feature", "FeatureCollection"] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types) + mask_value = float(mask_value) if mask_value is not None else None + return self.process( + process_id="apply_polygon", + data=THIS, + **{geometries_parameter: geometries}, + process=process, + arguments=dict_no_none( + mask_value=mask_value, + context=context, + ), + )
+ + +
+[docs] + def reduce_bands(self, reducer: Union[str, PGNode, typing.Callable, UDF]) -> DataCube: + """ + Shortcut for :py:meth:`reduce_dimension` along the band dimension + + :param reducer: "child callback" function, see :ref:`callbackfunctions` + """ + return self.reduce_dimension( + dimension=self.metadata.band_dimension.name if self.metadata else "bands", + reducer=reducer, + band_math_mode=True, + )
+ + +
+[docs] + def reduce_temporal(self, reducer: Union[str, PGNode, typing.Callable, UDF]) -> DataCube: + """ + Shortcut for :py:meth:`reduce_dimension` along the temporal dimension + + :param reducer: "child callback" function, see :ref:`callbackfunctions` + """ + return self.reduce_dimension( + dimension=self.metadata.temporal_dimension.name if self.metadata else "t", + reducer=reducer, + )
+ + +
+[docs] + @deprecated( + "Use :py:meth:`reduce_bands` with :py:class:`UDF <openeo.rest._datacube.UDF>` as reducer.", + version="0.13.0", + ) + def reduce_bands_udf(self, code: str, runtime: Optional[str] = None, version: Optional[str] = None) -> DataCube: + """ + Use `reduce_dimension` process with given UDF along band/spectral dimension. + """ + # TODO #181 #312 drop this deprecated pattern + return self.reduce_bands(reducer=UDF(code=code, runtime=runtime, version=version))
+ + +
+[docs] + @openeo_process + def add_dimension(self, name: str, label: str, type: Optional[str] = None): + """ + Adds a new named dimension to the data cube. + Afterwards, the dimension can be referenced with the specified name. If a dimension with the specified name exists, + the process fails with a DimensionExists error. The dimension label of the dimension is set to the specified label. + + This call does not modify the datacube in place, but returns a new datacube with the additional dimension. + + :param name: The name of the dimension to add + :param label: The dimension label. + :param type: Dimension type, allowed values: 'spatial', 'temporal', 'bands', 'other', default value is 'other' + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. All other dimensions remain unchanged. + """ + return self.process( + process_id="add_dimension", + arguments=dict_no_none({"data": self, "name": name, "label": label, "type": type}), + metadata=self.metadata.add_dimension(name=name, label=label, type=type) if self.metadata else None, + )
+ + +
+[docs] + @openeo_process + def drop_dimension(self, name: str): + """ + Drops a dimension from the data cube. + Dropping a dimension only works on dimensions with a single dimension label left, otherwise the process fails + with a DimensionLabelCountMismatch exception. Dimension values can be reduced to a single value with a filter + such as filter_bands or the reduce_dimension process. If a dimension with the specified name does not exist, + the process fails with a DimensionNotAvailable exception. + + :param name: The name of the dimension to drop + :return: The data cube with the given dimension dropped. + """ + return self.process( + process_id="drop_dimension", + arguments={"data": self, "name": name}, + metadata=self.metadata.drop_dimension(name=name) if self.metadata else None, + )
+ + +
+[docs] + @deprecated( + "Use :py:meth:`reduce_temporal` with :py:class:`UDF <openeo.rest._datacube.UDF>` as reducer", + version="0.13.0", + ) + def reduce_temporal_udf(self, code: str, runtime="Python", version="latest"): + """ + Apply reduce (`reduce_dimension`) process with given UDF along temporal dimension. + + :param code: The UDF code, compatible with the given runtime and version + :param runtime: The UDF runtime + :param version: The UDF runtime version + """ + # TODO #181 #312 drop this deprecated pattern + return self.reduce_temporal(reducer=UDF(code=code, runtime=runtime, version=version))
+ + + reduce_tiles_over_time = legacy_alias( + reduce_temporal_udf, name="reduce_tiles_over_time", since="0.1.1" + ) + +
+[docs] + @openeo_process + def apply_neighborhood( + self, + process: Union[str, PGNode, typing.Callable, UDF], + size: List[Dict], + overlap: List[dict] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a focal process to a data cube. + + A focal process is a process that works on a 'neighbourhood' of pixels. The neighbourhood can extend into multiple dimensions, this extent is specified by the `size` argument. It is not only (part of) the size of the input window, but also the size of the output for a given position of the sliding window. The sliding window moves with multiples of `size`. + + An overlap can be specified so that neighbourhoods can have overlapping boundaries. This allows for continuity of the output. The values included in the data cube as overlap can't be modified by the given `process`. + + The neighbourhood size should be kept small enough, to avoid running beyond computational resources, but a too small size will result in a larger number of process invocations, which may slow down processing. Window sizes for spatial dimensions typically are in the range of 64 to 512 pixels, while overlaps of 8 to 32 pixels are common. + + The process must not add new dimensions, or remove entire dimensions, but the result can have different dimension labels. + + For the special case of 2D convolution, it is recommended to use ``apply_kernel()``. + + :param size: + :param overlap: + :param process: a callback function that creates a process graph, see :ref:`callbackfunctions` + :param context: Additional data to be passed to the process. + + :return: + """ + return self.process( + process_id="apply_neighborhood", + arguments=dict_no_none( + data=THIS, + process=build_child_callback(process=process, parent_parameters=["data"], connection=self.connection), + size=size, + overlap=overlap, + context=context, + ) + )
+ + +
+[docs] + @openeo_process + def apply( + self, + process: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a unary process (a local operation) to each value of the specified or all dimensions in the data cube. + + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives a single numerical value + and returns a single numerical value. + For example: + + - ``"absolute"`` (string) + - :py:func:`absolute <openeo.processes.absolute>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda x: x * 2 + 3`` (function or lambda) + + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube. + """ + return self.process( + process_id="apply", + arguments=dict_no_none( + { + "data": THIS, + "process": build_child_callback(process, parent_parameters=["x"], connection=self.connection), + "context": context, + } + ), + )
+ + + reduce_temporal_simple = legacy_alias( + reduce_temporal, "reduce_temporal_simple", since="0.13.0" + ) + +
+[docs] + @openeo_process(process_id="min", mode="reduce_dimension") + def min_time(self) -> DataCube: + """ + Finds the minimum value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("min")
+ + +
+[docs] + @openeo_process(process_id="max", mode="reduce_dimension") + def max_time(self) -> DataCube: + """ + Finds the maximum value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("max")
+ + +
+[docs] + @openeo_process(process_id="mean", mode="reduce_dimension") + def mean_time(self) -> DataCube: + """ + Finds the mean value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("mean")
+ + +
+[docs] + @openeo_process(process_id="median", mode="reduce_dimension") + def median_time(self) -> DataCube: + """ + Finds the median value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("median")
+ + +
+[docs] + @openeo_process(process_id="count", mode="reduce_dimension") + def count_time(self) -> DataCube: + """ + Counts the number of images with a valid mask in a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("count")
+ + +
+[docs] + @openeo_process + def aggregate_temporal( + self, + intervals: List[list], + reducer: Union[str, typing.Callable, PGNode], + labels: Optional[List[str]] = None, + dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Computes a temporal aggregation based on an array of date and/or time intervals. + + Calendar hierarchies such as year, month, week etc. must be transformed into specific intervals by the clients. For each interval, all data along the dimension will be passed through the reducer. The computed values will be projected to the labels, so the number of labels and the number of intervals need to be equal. + + If the dimension is not set, the data cube is expected to only have one temporal dimension. + + :param intervals: Temporal left-closed intervals so that the start time is contained, but not the end time. + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute <openeo.processes.max>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.min()`` (function or lambda) + + :param labels: Labels for the intervals. The number of labels and the number of groups need to be equal. + :param dimension: The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension. + :param context: Additional data to be passed to the reducer. Not set by default. + + :return: A :py:class:`DataCube` containing a result for each time window + """ + return self.process( + process_id="aggregate_temporal", + arguments=dict_no_none( + data=THIS, + intervals=intervals, + labels=labels, + dimension=dimension, + reducer=build_child_callback(reducer, parent_parameters=["data"]), + context=context, + ), + )
+ + +
+[docs] + @openeo_process + def aggregate_temporal_period( + self, + period: str, + reducer: Union[str, PGNode, typing.Callable], + dimension: Optional[str] = None, + context: Optional[Dict] = None, + ) -> DataCube: + """ + Computes a temporal aggregation based on calendar hierarchies such as years, months or seasons. For other calendar hierarchies aggregate_temporal can be used. + + For each interval, all data along the dimension will be passed through the reducer. + + If the dimension is not set or is set to null, the data cube is expected to only have one temporal dimension. + + The period argument specifies the time intervals to aggregate. The following pre-defined values are available: + + - hour: Hour of the day + - day: Day of the year + - week: Week of the year + - dekad: Ten day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 each year. + - month: Month of the year + - season: Three month periods of the calendar seasons (December - February, March - May, June - August, September - November). + - tropical-season: Six month periods of the tropical seasons (November - April, May - October). + - year: Proleptic years + - decade: Ten year periods (0-to-9 decade), from a year ending in a 0 to the next year ending in a 9. + - decade-ad: Ten year periods (1-to-0 decade) better aligned with the Anno Domini (AD) calendar era, from a year ending in a 1 to the next year ending in a 0. + + + :param period: The period of the time intervals to aggregate. + :param reducer: A reducer to be applied on all values along the specified dimension. The reducer must be a callable process (or a set processes) that accepts an array and computes a single return value of the same type as the input values, for example median. + :param dimension: The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return self.process( + process_id="aggregate_temporal_period", + arguments=dict_no_none( + data=THIS, + period=period, + dimension=dimension, + reducer=build_child_callback(reducer, parent_parameters=["data"]), + context=context, + ), + )
+ + +
+[docs] + @openeo_process + def ndvi(self, nir: str = None, red: str = None, target_band: str = None) -> DataCube: + """ + Normalized Difference Vegetation Index (NDVI) + + :param nir: (optional) name of NIR band + :param red: (optional) name of red band + :param target_band: (optional) name of the newly created band + + :return: a DataCube instance + """ + if self.metadata is None: + metadata = None + elif target_band is None: + metadata = self.metadata.reduce_dimension(self.metadata.band_dimension.name) + else: + # TODO: first drop "bands" dim and re-add it with single "ndvi" band + metadata = self.metadata.append_band(Band(name=target_band, common_name="ndvi")) + return self.process( + process_id="ndvi", + arguments=dict_no_none( + data=THIS, nir=nir, red=red, target_band=target_band + ), + metadata=metadata, + )
+ + +
+[docs] + @openeo_process + def rename_dimension(self, source: str, target: str): + """ + Renames a dimension in the data cube while preserving all other properties. + + :param source: The current name of the dimension. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a DimensionExists error if a dimension with the specified name exists. + + :return: A new datacube with the dimension renamed. + """ + if self._do_metadata_normalization() and target in self.metadata.dimension_names(): + raise ValueError('Target dimension name conflicts with existing dimension: %s.' % target) + return self.process( + process_id="rename_dimension", + arguments=dict_no_none( + data=THIS, + source=self._assert_valid_dimension_name(source), + target=target, + ), + metadata=self.metadata.rename_dimension(source, target) if self.metadata else None, + )
+ + +
+[docs] + @openeo_process + def rename_labels(self, dimension: str, target: list, source: list = None) -> DataCube: + """ + Renames the labels of the specified dimension in the data cube from source to target. + + :param dimension: Dimension name + :param target: The new names for the labels. + :param source: The names of the labels as they are currently in the data cube. + + :return: An DataCube instance + """ + return self.process( + process_id="rename_labels", + arguments=dict_no_none( + data=THIS, + dimension=self._assert_valid_dimension_name(dimension), + target=target, + source=source, + ), + metadata=self.metadata.rename_labels(dimension, target, source) if self.metadata else None, + )
+ + +
+[docs] + @openeo_process(mode="apply") + def linear_scale_range(self, input_min, input_max, output_min, output_max) -> DataCube: + """ + Performs a linear transformation between the input and output range. + + The given number in x is clipped to the bounds specified in inputMin and inputMax so that the underlying formula + + ((x - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin) + outputMin + + never returns any value lower than outputMin or greater than outputMax. + + Potential use case include scaling values to the 8-bit range (0 - 255) often used for numeric representation of + values in one of the channels of the RGB colour model or calculating percentages (0 - 100). + + The no-data value null is passed through and therefore gets propagated. + + :param input_min: Minimum input value + :param input_max: Maximum input value + :param output_min: Minimum value of the desired output range. + :param output_max: Maximum value of the desired output range. + :return: a DataCube instance + """ + + return self.apply(lambda x: x.linear_scale_range(input_min, input_max, output_min, output_max))
+ + +
+[docs] + @openeo_process + def mask(self, mask: DataCube = None, replacement=None) -> DataCube: + """ + Applies a mask to a raster data cube. To apply a vector mask use `mask_polygon`. + + A mask is a raster data cube for which corresponding pixels among `data` and `mask` + are compared and those pixels in `data` are replaced whose pixels in `mask` are non-zero + (for numbers) or true (for boolean values). + The pixel values are replaced with the value specified for `replacement`, + which defaults to null (no data). + + :param mask: the raster mask + :param replacement: the value to replace the masked pixels with + """ + return self.process( + process_id="mask", + arguments=dict_no_none(data=self, mask=mask, replacement=replacement), + )
+ + +
+[docs] + @openeo_process + def mask_polygon( + self, + mask: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + srs: str = None, + replacement=None, + inside: bool = None, + ) -> DataCube: + """ + Applies a polygon mask to a raster data cube. To apply a raster mask use `mask`. + + All pixels for which the point at the pixel center does not intersect with any + polygon (as defined in the Simple Features standard by the OGC) are replaced. + This behaviour can be inverted by setting the parameter `inside` to true. + + The pixel values are replaced with the value specified for `replacement`, + which defaults to `no data`. + + :param mask: The geometry to mask with: a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param srs: The spatial reference system of the provided polygon. + By default longitude-latitude (EPSG:4326) is assumed. + + .. note:: this ``srs`` argument is a non-standard/experimental feature, only supported by specific back-ends. + See https://github.com/Open-EO/openeo-processes/issues/235 for details. + :param replacement: the value to replace the masked pixels with + """ + valid_geojson_types = ["Polygon", "MultiPolygon", "GeometryCollection", "Feature", "FeatureCollection"] + mask = self._get_geometry_argument(mask, valid_geojson_types=valid_geojson_types, crs=srs) + return self.process( + process_id="mask_polygon", + arguments=dict_no_none( + data=THIS, + mask=mask, + replacement=replacement, + inside=inside + ) + )
+ + +
+[docs] + @openeo_process + def merge_cubes( + self, + other: DataCube, + overlap_resolver: Union[str, PGNode, typing.Callable] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Merging two data cubes + + The data cubes have to be compatible. A merge operation without overlap should be reversible with (a set of) filter operations for each of the two cubes. The process performs the join on overlapping dimensions, with the same name and type. + An overlapping dimension has the same name, type, reference system and resolution in both dimensions, but can have different labels. One of the dimensions can have different labels, for all other dimensions the labels must be equal. If data overlaps, the parameter overlap_resolver must be specified to resolve the overlap. + + Examples for merging two data cubes: + + #. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first cube and B3 and B4. An overlap resolver is not needed. The merged data cube has the dimensions x, y, t and bands and the dimension bands has four dimension labels: B1, B2, B3, B4. + #. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first data cube and B2 and B3 for the second. An overlap resolver is required to resolve overlap in band B2. The merged data cube has the dimensions x, y, t and bands and the dimension bands has three dimension labels: B1, B2, B3. + #. Data cubes with the dimensions x, y and t have the same dimension labels in x,y and t. There are two options: + * Keep the overlapping values separately in the merged data cube: An overlap resolver is not needed, but for each data cube you need to add a new dimension using add_dimension. The new dimensions must be equal, except that the labels for the new dimensions must differ by name. The merged data cube has the same dimensions and labels as the original data cubes, plus the dimension added with add_dimension, which has the two dimension labels after the merge. + * Combine the overlapping values into a single value: An overlap resolver is required to resolve the overlap for all pixels. The merged data cube has the same dimensions and labels as the original data cubes, but all pixel values have been processed by the overlap resolver. + #. Merging a data cube with dimensions x, y, t with another cube with dimensions x, y will join on the x, y dimension, so the lower dimension cube is merged with each time step in the higher dimensional cube. This can for instance be used to apply a digital elevation model to a spatiotemporal data cube. + + :param other: The data cube to merge with. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The reducer must return a value of the same data type as the input values are. The reduction operator may be a single process such as multiply or consist of multiple sub-processes. null (the default) can be specified if no overlap resolver is required. + :param context: Additional data to be passed to the process. + + :return: The merged data cube. + """ + arguments = {"cube1": self, "cube2": other} + if overlap_resolver: + arguments["overlap_resolver"] = build_child_callback(overlap_resolver, parent_parameters=["x", "y"]) + if ( + self.metadata + and self.metadata.has_band_dimension() + and isinstance(other, DataCube) + and other.metadata + and other.metadata.has_band_dimension() + ): + # Minimal client side metadata merging + merged_metadata = self.metadata + for b in other.metadata.band_dimension.bands: + if b not in merged_metadata.bands: + merged_metadata = merged_metadata.append_band(b) + else: + merged_metadata = None + # Overlapping bands without overlap resolver will give an error in the backend + if context: + arguments["context"] = context + return self.process(process_id="merge_cubes", arguments=arguments, metadata=merged_metadata)
+ + + merge = legacy_alias(merge_cubes, name="merge", since="0.4.6") + +
+[docs] + @openeo_process + def apply_kernel( + self, kernel: Union[np.ndarray, List[List[float]]], factor=1.0, border=0, + replace_invalid=0 + ) -> DataCube: + """ + Applies a focal operation based on a weighted kernel to each value of the specified dimensions in the data cube. + + The border parameter determines how the data is extended when the kernel overlaps with the borders. + The following options are available: + + * numeric value - fill with a user-defined constant number n: nnnnnn|abcdefgh|nnnnnn (default, with n = 0) + * replicate - repeat the value from the pixel at the border: aaaaaa|abcdefgh|hhhhhh + * reflect - mirror/reflect from the border: fedcba|abcdefgh|hgfedc + * reflect_pixel - mirror/reflect from the center of the pixel at the border: gfedcb|abcdefgh|gfedcb + * wrap - repeat/wrap the image: cdefgh|abcdefgh|abcdef + + + :param kernel: The kernel to be applied on the data cube. The kernel has to be as many dimensions as the data cube has dimensions. + :param factor: A factor that is multiplied to each value computed by the focal operation. This is basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults to fill the border with zeroes. + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite numerical values with. By default, those values are replaced with zeroes. + :return: A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube. + """ + return self.process('apply_kernel', { + 'data': THIS, + 'kernel': kernel.tolist() if isinstance(kernel, np.ndarray) else kernel, + 'factor': factor, + 'border': border, + 'replace_invalid': replace_invalid + })
+ + +
+[docs] + @openeo_process + def resolution_merge( + self, high_resolution_bands: List[str], low_resolution_bands: List[str], method: str = None + ) -> DataCube: + """ + Resolution merging algorithms try to improve the spatial resolution of lower resolution bands + (e.g. Sentinel-2 20M) based on higher resolution bands. (e.g. Sentinel-2 10M). + + External references: + + `Pansharpening explained <https://bok.eo4geo.eu/IP2-1-3>`_ + + `Example publication: 'Improving the Spatial Resolution of Land Surface Phenology by Fusing Medium- and + Coarse-Resolution Inputs' <https://doi.org/10.1109/TGRS.2016.2537929>`_ + + .. warning:: experimental process: not generally supported, API subject to change. + + :param high_resolution_bands: A list of band names to use as 'high-resolution' band. Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will remain unmodified. + :param low_resolution_bands: A list of band names for which the spatial resolution should be increased. Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will be modified by the process. + :param method: The method to use. The supported algorithms can vary between back-ends. Set to `null` (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility.. + :return: A datacube with the same bands and metadata as the input, but algorithmically increased spatial resolution for the selected bands. + """ + return self.process('resolution_merge', { + 'data': THIS, + 'high_resolution_bands': high_resolution_bands, + 'low_resolution_bands': low_resolution_bands, + 'method': method, + + })
+ + +
+[docs] + def raster_to_vector(self) -> VectorCube: + """ + Converts this raster data cube into a :py:class:`~openeo.rest.vectorcube.VectorCube`. + The bounding polygon of homogenous areas of pixels is constructed. + + .. warning:: experimental process: not generally supported, API subject to change. + + :return: a :py:class:`~openeo.rest.vectorcube.VectorCube` + """ + pg_node = PGNode(process_id="raster_to_vector", arguments={"data": self}) + return VectorCube(pg_node, connection=self._connection)
+ + + ####VIEW methods ####### + +
+[docs] + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'mean'``.", version="0.10.0" + ) + def polygonal_mean_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a mean time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="mean")
+ + +
+[docs] + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'histogram'``.", + version="0.10.0", + ) + def polygonal_histogram_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a histogram time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="histogram")
+ + +
+[docs] + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'median'``.", version="0.10.0" + ) + def polygonal_median_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a median time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="median")
+ + +
+[docs] + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'sd'``.", version="0.10.0" + ) + def polygonal_standarddeviation_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a time series of standard deviations for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="sd")
+ + +
+[docs] + @openeo_process + def ard_surface_reflectance( + self, atmospheric_correction_method: str, cloud_detection_method: str, elevation_model: str = None, + atmospheric_correction_options: dict = None, cloud_detection_options: dict = None, + ) -> DataCube: + """ + Computes CARD4L compliant surface reflectance values from optical input. + + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. + :param elevation_model: The digital elevation model to use, leave empty to allow the back-end to make a suitable choice. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + :param cloud_detection_options: Proprietary options for the cloud detection method. + :return: Data cube containing bottom of atmosphere reflectances with atmospheric disturbances like clouds and cloud shadows removed. The data returned is CARD4L compliant and contains metadata. + """ + return self.process('ard_surface_reflectance', { + 'data': THIS, + 'atmospheric_correction_method': atmospheric_correction_method, + 'cloud_detection_method': cloud_detection_method, + 'elevation_model': elevation_model, + 'atmospheric_correction_options': atmospheric_correction_options or {}, + 'cloud_detection_options': cloud_detection_options or {}, + })
+ + +
+[docs] + @openeo_process + def atmospheric_correction(self, method: str = None, elevation_model: str = None, options: dict = None) -> DataCube: + """ + Applies an atmospheric correction that converts top of atmosphere reflectance values into bottom of atmosphere/top of canopy reflectance values. + + Note that multiple atmospheric methods exist, but may not be supported by all backends. The method parameter gives + you the option of requiring a specific method, but this may result in an error if the backend does not support it. + + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a specific method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use, leave empty to allow the back-end to make a suitable choice. + :param options: Proprietary options for the atmospheric correction method. + :return: datacube with bottom of atmosphere reflectances + """ + return self.process('atmospheric_correction', { + 'data': THIS, + 'method': method, + 'elevation_model': elevation_model, + 'options': options or {}, + })
+ + +
+[docs] + @openeo_process + def save_result( + self, + format: str = _DEFAULT_RASTER_FORMAT, + options: Optional[dict] = None, + ) -> DataCube: + if self._connection: + formats = set(self._connection.list_output_formats().keys()) + # TODO: map format to correct casing too? + if format.lower() not in {f.lower() for f in formats}: + raise ValueError("Invalid format {f!r}. Should be one of {s}".format(f=format, s=formats)) + return self.process( + process_id="save_result", + arguments={ + "data": THIS, + "format": format, + # TODO: leave out options if unset? + "options": options or {} + } + )
+ + +
+[docs] + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + ) -> Union[None, bytes]: + """ + Execute synchronously and download the raster data cube, e.g. as GeoTIFF. + + If outputfile is provided, the result is stored on disk locally, otherwise, a bytes object is returned. + The bytes object can be passed on to a suitable decoder for decoding. + + :param outputfile: Optional, an output file if the result needs to be stored on disk. + :param format: Optional, an output format supported by the backend. + :param options: Optional, file format options + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: None if the result is stored to disk, or a bytes object returned by the backend. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=format, + options=options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.download()", + ) + return self._connection.download(cube.flat_graph(), outputfile, validate=validate)
+ + +
+[docs] + def validate(self) -> List[dict]: + """ + Validate a process graph without executing it. + + :return: list of errors (dictionaries with "code" and "message" fields) + """ + return self._connection.validate_process_graph(self.flat_graph())
+ + + def tiled_viewing_service(self, type: str, **kwargs) -> Service: + return self._connection.create_service(self.flat_graph(), type=type, **kwargs) + + def _get_spatial_extent_from_load_collection(self): + pg = self.flat_graph() + for node in pg: + if pg[node]["process_id"] == "load_collection": + if "spatial_extent" in pg[node]["arguments"] and all( + cd in pg[node]["arguments"]["spatial_extent"] for cd in ["east", "west", "south", "north"] + ): + return pg[node]["arguments"]["spatial_extent"] + return None + +
+[docs] + def preview( + self, + center: Union[Iterable, None] = None, + zoom: Union[int, None] = None, + ): + """ + Creates a service with the process graph and displays a map widget. Only supports XYZ. + + :param center: (optional) Map center. Default is (0,0). + :param zoom: (optional) Zoom level of the map. Default is 1. + + :return: ipyleaflet Map object and the displayed Service + + .. warning:: experimental feature, subject to change. + .. versionadded:: 0.19.0 + """ + if "XYZ" not in self.connection.list_service_types(): + raise OpenEoClientException("Backend does not support service type 'XYZ'.") + + if not in_jupyter_context(): + raise Exception("On-demand preview only supported in Jupyter notebooks!") + try: + import ipyleaflet + except ImportError: + raise Exception( + "Additional modules must be installed for on-demand preview. Run `pip install openeo[jupyter]` or refer to the documentation." + ) + + service = self.tiled_viewing_service("XYZ") + service_metadata = service.describe_service() + + m = ipyleaflet.Map( + center=center or (0, 0), + zoom=zoom or 1, + scroll_wheel_zoom=True, + basemap=ipyleaflet.basemaps.OpenStreetMap.Mapnik, + ) + service_layer = ipyleaflet.TileLayer(url=service_metadata["url"]) + m.add(service_layer) + + if center is None and zoom is None: + spatial_extent = self._get_spatial_extent_from_load_collection() + if spatial_extent is not None: + m.fit_bounds( + [ + [spatial_extent["south"], spatial_extent["west"]], + [spatial_extent["north"], spatial_extent["east"]], + ] + ) + + class Preview: + """ + On-demand preview instance holding the associated XYZ service and ipyleaflet Map + """ + + def __init__(self, service: Service, ipyleaflet_map: ipyleaflet.Map): + self.service = service + self.map = ipyleaflet_map + + def _repr_html_(self): + from IPython.display import display + + display(self.map) + + def delete_service(self): + self.service.delete_service() + + return Preview(service, m)
+ + +
+[docs] + def execute_batch( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print: typing.Callable[[str], None] = print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: deprecate `format_options` as keyword arguments + **format_options, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long-running jobs, you probably do not want to keep the client running. + + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) File format to use for the job result. + :param job_options: + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: start showing deprecation warnings about these inconsistent argument names + if "format" in format_options and not out_format: + out_format = format_options["format"] # align with 'download' call arg name + + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.execute_batch()", + ) + + job = cube.create_job( + title=title, + description=description, + plan=plan, + budget=budget, + job_options=job_options, + validate=validate, + auto_add_save_result=False, + ) + return job.run_synchronous( + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + )
+ + +
+[docs] + def create_job( + self, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: avoid `format_options` as keyword arguments + **format_options, + ) -> BatchJob: + """ + Sends the datacube's process graph as a batch job to the back-end + and return a :py:class:`~openeo.rest.job.BatchJob` instance. + + Note that the batch job will just be created at the back-end, + it still needs to be started and tracked explicitly. + Use :py:meth:`execute_batch` instead to have the openEO Python client take care of that job management. + + :param out_format: output file format. + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: custom job options. + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: Created job. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: add option to also automatically start the job? + # TODO: avoid using all kwargs as format_options + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options or None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.create_job()", + ) + return self._connection.create_job( + process_graph=cube.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + validate=validate, + additional=job_options, + )
+ + + send_job = legacy_alias(create_job, name="send_job", since="0.10.0") + +
+[docs] + def save_user_defined_process( + self, + user_defined_process_id: str, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ) -> RESTUserDefinedProcess: + """ + Saves this process graph in the backend as a user-defined process for the authenticated user. + + :param user_defined_process_id: unique identifier for the process + :param public: visible to other users? + :param summary: A short summary of what the process does. + :param description: Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation. + :param returns: Description and schema of the return value. + :param categories: A list of categories. + :param examples: A list of examples. + :param links: A list of links. + :return: a RESTUserDefinedProcess instance + """ + return self._connection.save_user_defined_process( + user_defined_process_id=user_defined_process_id, + process_graph=self.flat_graph(), public=public, summary=summary, description=description, + returns=returns, categories=categories, examples=examples, links=links, + )
+ + +
+[docs] + 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)
+ + +
+[docs] + @staticmethod + @deprecated(reason="Use :py:func:`openeo.udf.run_code.execute_local_udf` instead", version="0.7.0") + def execute_local_udf(udf: str, datacube: Union[str, 'xarray.DataArray', 'XarrayDataCube'] = None, fmt='netcdf'): + import openeo.udf.run_code + return openeo.udf.run_code.execute_local_udf(udf=udf, datacube=datacube, fmt=fmt)
+ + +
+[docs] + @openeo_process + def ard_normalized_radar_backscatter( + self, elevation_model: str = None, contributing_area=False, + ellipsoid_incidence_angle: bool = False, noise_removal: bool = True + ) -> DataCube: + """ + Computes CARD4L compliant backscatter (gamma0) from SAR input. + This method is a variant of :py:meth:`~openeo.rest.datacube.DataCube.sar_backscatter`, + with restricted parameters to generate backscatter according to CARD4L specifications. + + Note that backscatter computation may require instrument specific metadata that is tightly coupled to the original SAR products. + As a result, this process may only work in combination with loading data from specific collections, not with general data cubes. + + :param elevation_model: The digital elevation model to use. Set to None (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named `contributing_area` + is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `True`, an ellipsoidal incidence angle band named `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `True`, which removes noise. + + :return: Backscatter values expressed as gamma0. The data returned is CARD4L compliant and contains metadata. By default, the backscatter values are given in linear scale. + """ + return self.process(process_id="ard_normalized_radar_backscatter", arguments={ + "data": THIS, + "elevation_model": elevation_model, + "contributing_area": contributing_area, + "ellipsoid_incidence_angle": ellipsoid_incidence_angle, + "noise_removal": noise_removal + })
+ + +
+[docs] + @openeo_process + def sar_backscatter( + self, + coefficient: Union[str, None] = "gamma0-terrain", + elevation_model: Union[str, None] = None, + mask: bool = False, + contributing_area: bool = False, + local_incidence_angle: bool = False, + ellipsoid_incidence_angle: bool = False, + noise_removal: bool = True, + options: Optional[dict] = None + ) -> DataCube: + """ + Computes backscatter from SAR input. + + Note that backscatter computation may require instrument specific metadata that is tightly coupled to the + original SAR products. As a result, this process may only work in combination with loading data from + specific collections, not with general data cubes. + + :param coefficient: Select the radiometric correction coefficient. + The following options are available: + + - `"beta0"`: radar brightness + - `"sigma0-ellipsoid"`: ground area computed with ellipsoid earth model + - `"sigma0-terrain"`: ground area computed with terrain earth model + - `"gamma0-ellipsoid"`: ground area computed with ellipsoid earth model in sensor line of sight + - `"gamma0-terrain"`: ground area computed with terrain earth model in sensor line of sight (default) + - `None`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `None` (the default) to allow + the back-end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. + It indicates which values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named `contributing_area` + is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes noise. + :param options: dictionary with additional (backend-specific) options. + :return: + + .. versionadded:: 0.4.9 + .. versionchanged:: 0.4.10 replace `orthorectify` and `rtc` arguments with `coefficient`. + """ + coefficient_options = [ + "beta0", "sigma0-ellipsoid", "sigma0-terrain", "gamma0-ellipsoid", "gamma0-terrain", None + ] + if coefficient not in coefficient_options: + raise OpenEoClientException("Invalid `sar_backscatter` coefficient {c!r}. Should be one of {o}".format( + c=coefficient, o=coefficient_options + )) + arguments = { + "data": THIS, + "coefficient": coefficient, + "elevation_model": elevation_model, + "mask": mask, + "contributing_area": contributing_area, + "local_incidence_angle": local_incidence_angle, + "ellipsoid_incidence_angle": ellipsoid_incidence_angle, + "noise_removal": noise_removal, + } + if options: + arguments["options"] = options + return self.process(process_id="sar_backscatter", arguments=arguments)
+ + +
+[docs] + @openeo_process + def fit_curve(self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str): + """ + Use non-linear least squares to fit a model function `y = f(x, parameters)` to data. + + The process throws an `InvalidValues` exception if invalid values are encountered. + Invalid values are finite numbers (see also ``is_valid()``). + + .. warning:: experimental process: not generally supported, API subject to change. + https://github.com/Open-EO/openeo-processes/pull/240 + + :param parameters: + :param function: "child callback" function, see :ref:`callbackfunctions` + :param dimension: + """ + # TODO: does this return a `DataCube`? Shouldn't it just return an array (wrapper)? + return self.process( + process_id="fit_curve", + arguments={ + "data": THIS, + "parameters": parameters, + "function": build_child_callback(function, parent_parameters=["x", "parameters"]), + "dimension": dimension, + }, + )
+ + +
+[docs] + @openeo_process + def predict_curve( + self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str, + labels=None + ): + """ + Predict values using a model function and pre-computed parameters. + + .. warning:: experimental process: not generally supported, API subject to change. + https://github.com/Open-EO/openeo-processes/pull/240 + + :param parameters: + :param function: "child callback" function, see :ref:`callbackfunctions` + :param dimension: + """ + return self.process( + process_id="predict_curve", + arguments={ + "data": THIS, + "parameters": parameters, + "function": build_child_callback(function, parent_parameters=["x", "parameters"]), + "dimension": dimension, + "labels": labels, + }, + )
+ + +
+[docs] + @openeo_process(mode="reduce_dimension") + def predict_random_forest(self, model: Union[str, BatchJob, MlModel], dimension: str = "bands"): + """ + Apply ``reduce_dimension`` process with a ``predict_random_forest`` reducer. + + :param model: a reference to a trained model, one of + + - a :py:class:`~openeo.rest.mlmodel.MlModel` instance (e.g. loaded from :py:meth:`Connection.load_ml_model`) + - a :py:class:`~openeo.rest.job.BatchJob` instance of a batch job that saved a single random forest model + - a job id (``str``) of a batch job that saved a single random forest model + - a STAC item URL (``str``) to load the random forest from. + (The STAC Item must implement the `ml-model` extension.) + :param dimension: dimension along which to apply the ``reduce_dimension`` process. + + .. versionadded:: 0.10.0 + """ + if not isinstance(model, MlModel): + model = MlModel.load_ml_model(connection=self.connection, id=model) + reducer = PGNode( + process_id="predict_random_forest", data={"from_parameter": "data"}, model={"from_parameter": "context"} + ) + return self.reduce_dimension(dimension=dimension, reducer=reducer, context=model)
+ + +
+[docs] + @openeo_process + def dimension_labels(self, dimension: str) -> DataCube: + """ + Gives all labels for a dimension in the data cube. The labels have the same order as in the data cube. + + :param dimension: The name of the dimension to get the labels for. + """ + if self._do_metadata_normalization(): + dimension_names = self.metadata.dimension_names() + if dimension_names and dimension not in dimension_names: + raise ValueError(f"Invalid dimension name {dimension!r}, should be one of {dimension_names}") + return self.process(process_id="dimension_labels", arguments={"data": THIS, "dimension": dimension})
+ + +
+[docs] + @openeo_process + def flatten_dimensions(self, dimensions: List[str], target_dimension: str, label_separator: Optional[str] = None): + """ + Combines multiple given dimensions into a single dimension by flattening the values + and merging the dimension labels with the given `label_separator`. Non-string dimension labels will + be converted to strings. This process is the opposite of the process :py:meth:`unflatten_dimension()` + but executing both processes subsequently doesn't necessarily create a data cube that + is equal to the original data cube. + + :param dimensions: The names of the dimension to combine. + :param target_dimension: The name of a target dimension with a single dimension label to replace. + :param label_separator: The string that will be used as a separator for the concatenated dimension labels. + :return: A data cube with the new shape. + + .. warning:: experimental process: not generally supported, API subject to change. + .. versionadded:: 0.10.0 + """ + return self.process( + process_id="flatten_dimensions", + arguments=dict_no_none( + data=THIS, + dimensions=dimensions, + target_dimension=target_dimension, + label_separator=label_separator, + ), + )
+ + +
+[docs] + @openeo_process + def unflatten_dimension(self, dimension: str, target_dimensions: List[str], label_separator: Optional[str] = None): + """ + Splits a single dimension into multiple dimensions by systematically extracting values and splitting + the dimension labels by the given `label_separator`. + This process is the opposite of the process :py:meth:`flatten_dimensions()` but executing both processes + subsequently doesn't necessarily create a data cube that is equal to the original data cube. + + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the target dimensions. + :param label_separator: The string that will be used as a separator to split the dimension labels. + :return: A data cube with the new shape. + + .. warning:: experimental process: not generally supported, API subject to change. + .. versionadded:: 0.10.0 + """ + return self.process( + process_id="unflatten_dimension", + arguments=dict_no_none( + data=THIS, + dimension=dimension, + target_dimensions=target_dimensions, + label_separator=label_separator, + ), + )
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/graph_building.html b/_modules/openeo/rest/graph_building.html new file mode 100644 index 000000000..ba870fc58 --- /dev/null +++ b/_modules/openeo/rest/graph_building.html @@ -0,0 +1,208 @@ + + + + + + + openeo.rest.graph_building — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.graph_building

+"""
+Public openEO process graph building utilities
+'''''''''''''''''''''''''''''''''''''''''''''''
+
+"""
+from __future__ import annotations
+
+from typing import Optional
+
+from openeo.internal.graph_building import PGNode, _FromNodeMixin
+from openeo.processes import ProcessBuilder
+
+
+
+[docs] +class CollectionProperty(_FromNodeMixin): + """ + Helper object to easily create simple collection metadata property filters + to be used with :py:meth:`Connection.load_collection() <openeo.rest.connection.Connection.load_collection>`. + + .. note:: This class should not be used directly by end user code. + Use the :py:func:`~openeo.rest.graph_building.collection_property` factory instead. + + .. warning:: this is an experimental feature, naming might change. + """ + + def __init__(self, name: str, _builder: Optional[ProcessBuilder] = None): + self.name = name + self._builder = _builder or ProcessBuilder(pgnode={"from_parameter": "value"}) + + def from_node(self) -> PGNode: + return self._builder.from_node() + + def __eq__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder == other) + + def __ne__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder != other) + + def __gt__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder > other) + + def __ge__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder >= other) + + def __lt__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder < other) + + def __le__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder <= other)
+ + + +
+[docs] +def collection_property(name: str) -> CollectionProperty: + """ + Helper to easily create simple collection metadata property filters + to be used with :py:meth:`Connection.load_collection() <openeo.rest.connection.Connection.load_collection>`. + + Usage example: + + .. code-block:: python + + from openeo import collection_property + ... + + connection.load_collection( + ... + properties=[ + collection_property("eo:cloud_cover") <= 75, + collection_property("platform") == "Sentinel-2B", + ] + ) + + .. warning:: this is an experimental feature, naming might change. + + .. versionadded:: 0.26.0 + + :param name: name of the collection property to filter on + :return: an object that supports operators like ``<=``, ``==`` to easily build simple property filters. + """ + return CollectionProperty(name=name)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/job.html b/_modules/openeo/rest/job.html new file mode 100644 index 000000000..055810161 --- /dev/null +++ b/_modules/openeo/rest/job.html @@ -0,0 +1,751 @@ + + + + + + + openeo.rest.job — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.job

+from __future__ import annotations
+
+import datetime
+import json
+import logging
+import time
+import typing
+from pathlib import Path
+from typing import Dict, List, Optional, Union
+
+import requests
+
+from openeo.api.logs import LogEntry, log_level_name, normalize_log_level
+from openeo.internal.documentation import openeo_endpoint
+from openeo.internal.jupyter import (
+    VisualDict,
+    VisualList,
+    render_component,
+    render_error,
+)
+from openeo.internal.warnings import deprecated, legacy_alias
+from openeo.rest import (
+    DEFAULT_DOWNLOAD_CHUNK_SIZE,
+    JobFailedException,
+    OpenEoApiError,
+    OpenEoApiPlainError,
+    OpenEoClientException,
+)
+from openeo.util import ensure_dir
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo.rest.connection import Connection
+
+logger = logging.getLogger(__name__)
+
+
+DEFAULT_JOB_RESULTS_FILENAME = "job-results.json"
+
+
+
+[docs] +class BatchJob: + """ + Handle for an openEO batch job, allowing it to describe, start, cancel, inspect results, etc. + + .. versionadded:: 0.11.0 + This class originally had the more cryptic name :py:class:`RESTJob`, + which is still available as legacy alias, + but :py:class:`BatchJob` is recommended since version 0.11.0. + + """ + + # TODO #425 method to bootstrap `load_stac` directly from a BatchJob object + + def __init__(self, job_id: str, connection: Connection): + self.job_id = job_id + """Unique identifier of the batch job (string).""" + + self.connection = connection + + def __repr__(self): + return '<{c} job_id={i!r}>'.format(c=self.__class__.__name__, i=self.job_id) + + def _repr_html_(self): + data = self.describe() + currency = self.connection.capabilities().currency() + return render_component('job', data=data, parameters={'currency': currency}) + +
+[docs] + @openeo_endpoint("GET /jobs/{job_id}") + def describe(self) -> dict: + """ + Get detailed metadata about a submitted batch job + (title, process graph, status, progress, ...). + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`describe_job`. + """ + return self.connection.get(f"/jobs/{self.job_id}", expected_status=200).json()
+ + + describe_job = legacy_alias(describe, name="describe_job", since="0.20.0", mode="soft") + +
+[docs] + def status(self) -> str: + """ + Get the status of the batch job + + :return: batch job status, one of "created", "queued", "running", "canceled", "finished" or "error". + """ + return self.describe().get("status", "N/A")
+ + +
+[docs] + @openeo_endpoint("DELETE /jobs/{job_id}") + def delete(self): + """ + Delete this batch job. + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`delete_job`. + """ + self.connection.delete(f"/jobs/{self.job_id}", expected_status=204)
+ + + delete_job = legacy_alias(delete, name="delete_job", since="0.20.0", mode="soft") + +
+[docs] + @openeo_endpoint("GET /jobs/{job_id}/estimate") + def estimate(self): + """Calculate time/cost estimate for a job.""" + data = self.connection.get( + f"/jobs/{self.job_id}/estimate", expected_status=200 + ).json() + currency = self.connection.capabilities().currency() + return VisualDict('job-estimate', data=data, parameters={'currency': currency})
+ + + estimate_job = legacy_alias(estimate, name="estimate_job", since="0.20.0", mode="soft") + +
+[docs] + @openeo_endpoint("POST /jobs/{job_id}/results") + def start(self) -> BatchJob: + """ + Start this batch job. + + :return: Started batch job + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`start_job`. + """ + self.connection.post(f"/jobs/{self.job_id}/results", expected_status=202) + return self
+ + + start_job = legacy_alias(start, name="start_job", since="0.20.0", mode="soft") + +
+[docs] + @openeo_endpoint("DELETE /jobs/{job_id}/results") + def stop(self): + """ + Stop this batch job. + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`stop_job`. + """ + self.connection.delete(f"/jobs/{self.job_id}/results", expected_status=204)
+ + + stop_job = legacy_alias(stop, name="stop_job", since="0.20.0", mode="soft") + +
+[docs] + def get_results_metadata_url(self, *, full: bool = False) -> str: + """Get results metadata URL""" + url = f"/jobs/{self.job_id}/results" + if full: + url = self.connection.build_url(url) + return url
+ + +
+[docs] + @deprecated("Use :py:meth:`~BatchJob.get_results` instead.", version="0.4.10") + def list_results(self) -> dict: + """Get batch job results metadata.""" + return self.get_results().get_metadata()
+ + +
+[docs] + def download_result(self, target: Union[str, Path] = None) -> Path: + """ + Download single job result to the target file path or into folder (current working dir by default). + + Fails if there are multiple result files. + + :param target: String or path where the file should be downloaded to. + """ + return self.get_results().download_file(target=target)
+ + +
+[docs] + @deprecated( + "Instead use :py:meth:`BatchJob.get_results` and the more flexible download functionality of :py:class:`JobResults`", + version="0.4.10") + def download_results(self, target: Union[str, Path] = None) -> Dict[Path, dict]: + """ + Download all job result files into given folder (current working dir by default). + + The names of the files are taken directly from the backend. + + :param target: String/path, folder where to put the result files. + :return: file_list: Dict containing the downloaded file path as value and asset metadata + """ + return self.get_result().download_files(target)
+ + +
+[docs] + @deprecated("Use :py:meth:`BatchJob.get_results` instead.", version="0.4.10") + def get_result(self): + return _Result(self)
+ + +
+[docs] + def get_results(self) -> JobResults: + """ + Get handle to batch job results for result metadata inspection or downloading resulting assets. + + .. versionadded:: 0.4.10 + """ + return JobResults(job=self)
+ + +
+[docs] + def logs( + self, offset: Optional[str] = None, level: Optional[Union[str, int]] = None + ) -> List[LogEntry]: + """Retrieve job logs. + + :param offset: The last identifier (property ``id`` of a LogEntry) the client has received. + + If provided, the back-ends only sends the entries that occurred after the specified identifier. + If not provided or empty, start with the first entry. + + Defaults to None. + + :param level: Minimum log level to retrieve. + + You can use either constants from Python's standard module ``logging`` + or their names (case-insensitive). + + For example: + ``logging.INFO``, ``"info"`` or ``"INFO"`` can all be used to show the messages + for level ``logging.INFO`` and above, i.e. also ``logging.WARNING`` and + ``logging.ERROR`` will be included. + + Default is to show all log levels, in other words ``logging.DEBUG``. + This is also the result when you explicitly pass log_level=None or log_level="". + + :return: A list containing the log entries for the batch job. + """ + url = f"/jobs/{self.job_id}/logs" + params = {} + if offset is not None: + params["offset"] = offset + if level is not None: + params["level"] = log_level_name(level) + response = self.connection.get(url, params=params, expected_status=200) + logs = response.json()["logs"] + + # Only filter logs when specified. + # We should still support client-side log_level filtering because not all backends + # support the minimum log level parameter. + if level is not None: + log_level = normalize_log_level(level) + logs = ( + log + for log in logs + if normalize_log_level(log.get("level")) >= log_level + ) + + entries = [LogEntry(log) for log in logs] + return VisualList("logs", data=entries)
+ + +
+[docs] + def run_synchronous( + self, outputfile: Union[str, Path, None] = None, + print=print, max_poll_interval=60, connection_retry_interval=30 + ) -> BatchJob: + """Start the job, wait for it to finish and download result""" + self.start_and_wait( + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + ) + # TODO #135 support multi file result sets too? + if outputfile is not None: + self.download_result(outputfile) + return self
+ + +
+[docs] + def start_and_wait( + self, print=print, max_poll_interval: int = 60, connection_retry_interval: int = 30, soft_error_max=10 + ) -> BatchJob: + """ + Start the batch job, poll its status and wait till it finishes (or fails) + + :param print: print/logging function to show progress/status + :param max_poll_interval: maximum number of seconds to sleep between status polls + :param connection_retry_interval: how long to wait when status poll failed due to connection issue + :param soft_error_max: maximum number of soft errors (e.g. temporary connection glitches) to allow + :return: + """ + # TODO rename `connection_retry_interval` to something more generic? + start_time = time.time() + + def elapsed() -> str: + return str(datetime.timedelta(seconds=time.time() - start_time)).rsplit(".")[0] + + def print_status(msg: str): + print("{t} Job {i!r}: {m}".format(t=elapsed(), i=self.job_id, m=msg)) + + # TODO: make `max_poll_interval`, `connection_retry_interval` class constants or instance properties? + print_status("send 'start'") + self.start() + + # TODO: also add `wait` method so you can track a job that already has started explicitly + # or just rename this method to `wait` and automatically do start if not started yet? + + # Start with fast polling. + poll_interval = min(5, max_poll_interval) + status = None + _soft_error_count = 0 + + def soft_error(message: str): + """Non breaking error (unless we had too much of them)""" + nonlocal _soft_error_count + _soft_error_count += 1 + if _soft_error_count > soft_error_max: + raise OpenEoClientException("Excessive soft errors") + print_status(message) + time.sleep(connection_retry_interval) + + while True: + # TODO: also allow a hard time limit on this infinite poll loop? + try: + job_info = self.describe() + except requests.ConnectionError as e: + soft_error("Connection error while polling job status: {e}".format(e=e)) + continue + except OpenEoApiPlainError as e: + if e.http_status_code in [502, 503]: + soft_error("Service availability error while polling job status: {e}".format(e=e)) + continue + else: + raise + + status = job_info.get("status", "N/A") + progress = '{p}%'.format(p=job_info["progress"]) if "progress" in job_info else "N/A" + print_status("{s} (progress {p})".format(s=status, p=progress)) + if status not in ('submitted', 'created', 'queued', 'running'): + break + + # Sleep for next poll (and adaptively make polling less frequent) + time.sleep(poll_interval) + poll_interval = min(1.25 * poll_interval, max_poll_interval) + + if status != "finished": + # TODO: allow to disable this printing logs (e.g. in non-interactive contexts)? + # TODO: render logs jupyter-aware in a notebook context? + print(f"Your batch job {self.job_id!r} failed. Error logs:") + print(self.logs(level=logging.ERROR)) + print( + f"Full logs can be inspected in an openEO (web) editor or with `connection.job({self.job_id!r}).logs()`." + ) + raise JobFailedException( + f"Batch job {self.job_id!r} didn't finish successfully. Status: {status} (after {elapsed()}).", + job=self, + ) + + return self
+
+ + + +
+[docs] +@deprecated(reason="Use :py:class:`BatchJob` instead", version="0.11.0") +class RESTJob(BatchJob): + """ + Legacy alias for :py:class:`BatchJob`. + """
+ + + +
+[docs] +class ResultAsset: + """ + Result asset of a batch job (e.g. a GeoTIFF or JSON file) + + .. versionadded:: 0.4.10 + """ + + def __init__(self, job: BatchJob, name: str, href: str, metadata: dict): + self.job = job + + self.name = name + """Asset name as advertised by the backend.""" + + self.href = href + """Download URL of the asset.""" + + self.metadata = metadata + """Asset metadata provided by the backend, possibly containing keys "type" (for media type), "roles", "title", "description".""" + + def __repr__(self): + return "<ResultAsset {n!r} (type {t}) at {h!r}>".format( + n=self.name, t=self.metadata.get("type", "unknown"), h=self.href + ) + +
+[docs] + def download( + self, target: Optional[Union[Path, str]] = None, *, chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE + ) -> Path: + """ + Download asset to given location + + :param target: download target path. Can be an existing folder + (in which case the filename advertised by backend will be used) + or full file name. By default, the working directory will be used. + :param chunk_size: chunk size for streaming response. + """ + target = Path(target or Path.cwd()) + if target.is_dir(): + target = target / self.name + ensure_dir(target.parent) + logger.info("Downloading Job result asset {n!r} from {h!s} to {t!s}".format(n=self.name, h=self.href, t=target)) + response = self._get_response(stream=True) + with target.open("wb") as f: + for block in response.iter_content(chunk_size=chunk_size): + f.write(block) + return target
+ + + def _get_response(self, stream=True) -> requests.Response: + return self.job.connection.get(self.href, stream=stream) + +
+[docs] + def load_json(self) -> dict: + """Load asset in memory and parse as JSON.""" + if not (self.name.lower().endswith(".json") or self.metadata.get("type") == "application/json"): + logger.warning("Asset might not be JSON") + return self._get_response().json()
+ + +
+[docs] + def load_bytes(self) -> bytes: + """Load asset in memory as raw bytes.""" + return self._get_response().content
+
+ + + # TODO: more `load` methods e.g.: load GTiff asset directly as numpy array + + +class MultipleAssetException(OpenEoClientException): + pass + + +
+[docs] +class JobResults: + """ + Results of a batch job: listing of one or more output files (assets) + and some metadata. + + .. versionadded:: 0.4.10 + """ + + def __init__(self, job: BatchJob): + self._job = job + self._results = None + + def __repr__(self): + return "<JobResults for job {j!r}>".format(j=self._job.job_id) + + def get_job_id(self) -> str: + return self._job.job_id + + def _repr_html_(self): + try: + response = self.get_metadata() + return render_component("batch-job-result", data = response) + except OpenEoApiError as error: + return render_error(error) + +
+[docs] + def get_metadata(self, force=False) -> dict: + """Get batch job results metadata (parsed JSON)""" + if self._results is None or force: + self._results = self._job.connection.get( + self._job.get_results_metadata_url(), expected_status=200 + ).json() + return self._results
+ + + # TODO: provide methods for `stac_version`, `id`, `geometry`, `properties`, `links`, ...? + +
+[docs] + def get_assets(self) -> List[ResultAsset]: + """ + Get all assets from the job results. + """ + # TODO: add arguments to filter on metadata, e.g. to only get assets of type "image/tiff" + metadata = self.get_metadata() + # API 1.0 style: dictionary mapping filenames to metadata dict (with at least a "href" field) + assets = metadata.get("assets", {}) + if not assets: + logger.warning("No assets found in job result metadata.") + return [ + ResultAsset(job=self._job, name=name, href=asset["href"], metadata=asset) + for name, asset in assets.items() + ]
+ + +
+[docs] + def get_asset(self, name: str = None) -> ResultAsset: + """ + Get single asset by name or without name if there is only one. + """ + # TODO: also support getting a single asset by type or role? + assets = self.get_assets() + if len(assets) == 0: + raise OpenEoClientException("No assets in result.") + if name is None: + if len(assets) == 1: + return assets[0] + else: + raise MultipleAssetException("Multiple result assets for job {j}: {a}".format( + j=self._job.job_id, a=[a.name for a in assets] + )) + else: + try: + return next(a for a in assets if a.name == name) + except StopIteration: + raise OpenEoClientException( + "No asset {n!r} in: {a}".format(n=name, a=[a.name for a in assets]) + )
+ + +
+[docs] + def download_file(self, target: Union[Path, str] = None, name: str = None) -> Path: + """ + Download single asset. Can be used when there is only one asset in the + :py:class:`JobResults`, or when the desired asset name is given explicitly. + + :param target: path to download to. Can be an existing directory + (in which case the filename advertised by backend will be used) + or full file name. By default, the working directory will be used. + :param name: asset name to download (not required when there is only one asset) + :return: path of downloaded asset + """ + try: + return self.get_asset(name=name).download(target=target) + except MultipleAssetException: + raise OpenEoClientException( + "Can not use `download_file` with multiple assets. Use `download_files` instead.")
+ + +
+[docs] + def download_files(self, target: Union[Path, str] = None, include_stac_metadata: bool = True) -> List[Path]: + """ + Download all assets to given folder. + + :param target: path to folder to download to (must be a folder if it already exists) + :param include_stac_metadata: whether to download the job result metadata as a STAC (JSON) file. + :return: list of paths to the downloaded assets. + """ + target = Path(target or Path.cwd()) + if target.exists() and not target.is_dir(): + raise OpenEoClientException(f"Target argument {target} exists but isn't a folder.") + ensure_dir(target) + + downloaded = [a.download(target) for a in self.get_assets()] + + if include_stac_metadata: + # TODO #184: convention for metadata file name? + metadata_file = target / DEFAULT_JOB_RESULTS_FILENAME + # TODO #184: rewrite references to locally downloaded assets? + metadata_file.write_text(json.dumps(self.get_metadata())) + downloaded.append(metadata_file) + + return downloaded
+
+ + + +@deprecated(reason="Use :py:class:`JobResults` instead", version="0.4.10") +class _Result: + """ + Wrapper around `JobResults` to adapt old deprecated "Result" API. + + .. deprecated:: 0.4.10 + """ + + # TODO: deprecated: remove this + + def __init__(self, job): + self.results = JobResults(job=job) + + def download_file(self, target: Union[str, Path] = None) -> Path: + return self.results.download_file(target=target) + + def download_files(self, target: Union[str, Path] = None) -> Dict[Path, dict]: + target = Path(target or Path.cwd()) + if target.exists() and not target.is_dir(): + raise OpenEoClientException(f"Target argument {target} exists but isn't a folder.") + return {a.download(target): a.metadata for a in self.results.get_assets()} + + def load_json(self) -> dict: + return self.results.get_asset().load_json() + + def load_bytes(self) -> bytes: + return self.results.get_asset().load_bytes() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/mlmodel.html b/_modules/openeo/rest/mlmodel.html new file mode 100644 index 000000000..bf759f5df --- /dev/null +++ b/_modules/openeo/rest/mlmodel.html @@ -0,0 +1,264 @@ + + + + + + + openeo.rest.mlmodel — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.mlmodel

+from __future__ import annotations
+
+import logging
+import pathlib
+import typing
+from typing import Optional, Union
+
+from openeo.internal.documentation import openeo_process
+from openeo.internal.graph_building import PGNode
+from openeo.rest._datacube import _ProcessGraphAbstraction
+from openeo.rest.job import BatchJob
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo import Connection
+
+_log = logging.getLogger(__name__)
+
+
+
+[docs] +class MlModel(_ProcessGraphAbstraction): + """ + A machine learning model. + + It is the result of a training procedure, e.g. output of a ``fit_...`` process, + and can be used for prediction (classification or regression) with the corresponding ``predict_...`` process. + + .. versionadded:: 0.10.0 + """ + + def __init__(self, graph: PGNode, connection: Union[Connection, None]): + super().__init__(pgnode=graph, connection=connection) + +
+[docs] + def save_ml_model(self, options: Optional[dict] = None): + """ + Saves a machine learning model as part of a batch job. + + :param options: Additional parameters to create the file(s). + """ + pgnode = PGNode( + process_id="save_ml_model", + arguments={"data": self, "options": options or {}} + ) + return MlModel(graph=pgnode, connection=self._connection)
+ + +
+[docs] + @staticmethod + @openeo_process + def load_ml_model(connection: Connection, id: Union[str, BatchJob]) -> MlModel: + """ + Loads a machine learning model from a STAC Item. + + :param connection: connection object + :param id: STAC item reference, as URL, batch job (id) or user-uploaded file + :return: + + .. versionadded:: 0.10.0 + """ + if isinstance(id, BatchJob): + id = id.job_id + return MlModel(graph=PGNode(process_id="load_ml_model", id=id), connection=connection)
+ + +
+[docs] + def execute_batch( + self, + outputfile: Union[str, pathlib.Path], + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print=print, + max_poll_interval=60, + connection_retry_interval=30, + job_options=None, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long running jobs, you probably do not want to keep the client running. + + :param job_options: + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) Format of the job result. + :param format_options: String Parameters for the job result format + """ + job = self.create_job(title=title, description=description, plan=plan, budget=budget, job_options=job_options) + return job.run_synchronous( + # TODO #135 support multi file result sets too + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + )
+ + +
+[docs] + def create_job( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + ) -> BatchJob: + """ + Sends a job to the backend and returns a ClientJob instance. + + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: A dictionary containing (custom) job options + :param format_options: String Parameters for the job result format + :return: Created job. + """ + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + pg = self + if pg.result_node().process_id not in {"save_ml_model"}: + _log.warning("Process graph has no final `save_ml_model`. Adding it automatically.") + pg = pg.save_ml_model() + return self._connection.create_job( + process_graph=pg.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + additional=job_options, + )
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/multiresult.html b/_modules/openeo/rest/multiresult.html new file mode 100644 index 000000000..f18d522a9 --- /dev/null +++ b/_modules/openeo/rest/multiresult.html @@ -0,0 +1,232 @@ + + + + + + + openeo.rest.multiresult — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.multiresult

+from __future__ import annotations
+
+from typing import Dict, List, Optional
+
+from openeo import BatchJob
+from openeo.internal.graph_building import FlatGraphableMixin, MultiLeafGraph
+from openeo.rest import OpenEoClientException
+from openeo.rest.connection import Connection, extract_connections
+
+
+
+[docs] +class MultiResult(FlatGraphableMixin): + """ + Helper to create and run batch jobs with process graphs + that contain multiple result nodes + or, more generally speaking, multiple process graph "leaf" nodes. + + Provide multiple + :py:class:`~openeo.rest.datacube.DataCube`/:py:class:`~openeo.rest.vectorcube.VectorCube` + instances to the constructor, + and start a batch job from that, + for example as follows: + + .. code-block:: python + + from openeo import MultiResult + + cube1 = ... + cube2 = ... + multi_result = MultiResult([cube1, cube2]) + job = multi_result.create_job() + + .. seealso:: + + :ref:`multi-result-process-graphs` + + .. versionadded:: 0.35.0 + """ + + __slots__ = ("_multi_leaf_graph", "_connection") + +
+[docs] + def __init__(self, leaves: List[FlatGraphableMixin], connection: Optional[Connection] = None): + """ + Build a :py:class:`MultiResult` instance from multiple leaf nodes + + :param leaves: list of objects that can be + converted to an openEO-style (flat) process graph representation, + typically :py:class:`~openeo.rest.datacube.DataCube` + or :py:class:`~openeo.rest.vectorcube.VectorCube` instances. + :param connection: Optional connection to use for creating/starting batch jobs, + for special use cases where the provided leaf instances + are not already associated with a connection. + """ + self._multi_leaf_graph = MultiLeafGraph(leaves=leaves) + self._connection = self._extract_connection(leaves=leaves, connection=connection)
+ + + @staticmethod + def _extract_connection(leaves: List[FlatGraphableMixin], connection: Optional[Connection] = None) -> Connection: + """ + Extract common connection from leaves and/or explicitly provided connection. + Fails if there are multiple or none. + """ + connections = set() + if connection: + connections.add(connection) + connections.update(extract_connections(leaves)) + + if len(connections) == 1: + return connections.pop() + elif len(connections) == 0: + raise OpenEoClientException("No connection in any of the MultiResult leaves") + else: + raise OpenEoClientException("MultiResult with multiple different connections") + + def flat_graph(self) -> Dict[str, dict]: + return self._multi_leaf_graph.flat_graph() + + def create_job( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + return self._connection.create_job( + process_graph=self._multi_leaf_graph, + title=title, + description=description, + additional=job_options, + validate=validate, + ) + + def execute_batch( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + job = self.create_job(title=title, description=description, job_options=job_options, validate=validate) + return job.run_synchronous()
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/udp.html b/_modules/openeo/rest/udp.html new file mode 100644 index 000000000..c592d917b --- /dev/null +++ b/_modules/openeo/rest/udp.html @@ -0,0 +1,266 @@ + + + + + + + openeo.rest.udp — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.udp

+from __future__ import annotations
+
+import typing
+from pathlib import Path
+from typing import List, Optional, Union
+
+from openeo.api.process import Parameter
+from openeo.internal.graph_building import FlatGraphableMixin, as_flat_graph
+from openeo.internal.jupyter import render_component
+from openeo.internal.processes.builder import ProcessBuilderBase
+from openeo.internal.warnings import deprecated
+from openeo.util import dict_no_none
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo.rest.connection import Connection
+
+
+
+[docs] +def build_process_dict( + process_graph: Union[dict, FlatGraphableMixin, Path, List[FlatGraphableMixin]], + process_id: Optional[str] = None, + summary: Optional[str] = None, + description: Optional[str] = None, + parameters: Optional[List[Union[Parameter, dict]]] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, +) -> dict: + """ + Build a dictionary describing a process with metadaa (`process_graph`, `parameters`, `description`, ...) + + :param process_graph: dict or builder representing a process graph + :param process_id: identifier of the process + :param summary: short summary of what the process does + :param description: detailed description + :param parameters: list of process parameters (which have name, schema, default value, ...) + :param returns: description and schema of what the process returns + :param categories: list of categories + :param examples: list of examples, may be used for unit tests + :param links: list of links related to the process + :return: dictionary in openEO "process graph with metadata" format + """ + process = dict_no_none( + process_graph=as_flat_graph(process_graph), + id=process_id, + summary=summary, + description=description, + returns=returns, + categories=categories, + examples=examples, + links=links + ) + if parameters is not None: + process["parameters"] = [ + (p if isinstance(p, Parameter) else Parameter(**p)).to_dict() + for p in parameters + ] + return process
+ + + +
+[docs] +class RESTUserDefinedProcess: + """ + Wrapper for a user-defined process stored (or to be stored) on an openEO back-end + """ + + def __init__(self, user_defined_process_id: str, connection: Connection): + self.user_defined_process_id = user_defined_process_id + self._connection = connection + self._connection.assert_user_defined_process_support() + + def _repr_html_(self): + process = self.describe() + return render_component('process', data=process, parameters = {'show-graph': True, 'provide-download': False}) + +
+[docs] + def store( + self, + process_graph: Union[dict, FlatGraphableMixin], + parameters: Optional[List[Union[Parameter, dict]]] = None, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ): + """Store a process graph and its metadata on the backend as a user-defined process""" + process = build_process_dict( + process_graph=process_graph, parameters=parameters, + summary=summary, description=description, returns=returns, + categories=categories, examples=examples, links=links, + ) + + # TODO: this "public" flag is not standardized yet EP-3609, https://github.com/Open-EO/openeo-api/issues/310 + process["public"] = public + + self._connection._preflight_validation(pg_with_metadata={"process": process}) + self._connection.put( + path="/process_graphs/{}".format(self.user_defined_process_id), json=process, expected_status=200 + )
+ + +
+[docs] + @deprecated( + "Use `store` instead. Method `update` is misleading: OpenEO API does not provide (partial) updates" + " of user-defined processes, only fully overwriting 'store' operations.", + version="0.4.11") + def update( + self, process_graph: Union[dict, ProcessBuilderBase], parameters: List[Union[Parameter, dict]] = None, + public: bool = False, summary: str = None, description: str = None + ): + self.store(process_graph=process_graph, parameters=parameters, public=public, summary=summary, + description=description)
+ + +
+[docs] + def describe(self) -> dict: + """Get metadata of this user-defined process.""" + # TODO: parse the "parameters" to Parameter objects? + return self._connection.get(path="/process_graphs/{}".format(self.user_defined_process_id)).json()
+ + +
+[docs] + def delete(self) -> None: + """Remove user-defined process from back-end""" + self._connection.delete(path="/process_graphs/{}".format(self.user_defined_process_id), expected_status=204)
+ + + def validate(self) -> None: + raise NotImplementedError
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/userfile.html b/_modules/openeo/rest/userfile.html new file mode 100644 index 000000000..c8bbd106f --- /dev/null +++ b/_modules/openeo/rest/userfile.html @@ -0,0 +1,242 @@ + + + + + + + openeo.rest.userfile — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.userfile

+from __future__ import annotations
+
+import typing
+from pathlib import Path, PurePosixPath
+from typing import Any, Dict, Optional, Union
+
+from openeo.rest import DEFAULT_DOWNLOAD_CHUNK_SIZE
+from openeo.util import ensure_dir
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo.rest.connection import Connection
+
+
+
+[docs] +class UserFile: + """ + Handle to a (user-uploaded) file in the user workspace on a openEO back-end. + """ + + def __init__( + self, + path: Union[str, PurePosixPath, None], + *, + connection: Connection, + metadata: Optional[dict] = None, + ): + if path: + pass + elif metadata and metadata.get("path"): + path = metadata.get("path") + else: + raise ValueError( + "File path should be specified through `path` or `metadata` argument." + ) + + self.path = PurePosixPath(path) + self.metadata = metadata or {"path": path} + self.connection = connection + +
+[docs] + @classmethod + def from_metadata(cls, metadata: dict, connection: Connection) -> UserFile: + """Build :py:class:`UserFile` from a workspace file metadata dictionary.""" + return cls(path=None, connection=connection, metadata=metadata)
+ + + def __repr__(self): + return "<{c} file={i!r}>".format(c=self.__class__.__name__, i=self.path) + + def _get_endpoint(self) -> str: + return f"/files/{self.path!s}" + +
+[docs] + def download(self, target: Union[Path, str] = None) -> Path: + """ + Downloads a user-uploaded file from the user workspace on the back-end + locally to the given location. + + :param target: local download target path. Can be an existing folder + (in which case the file name advertised by backend will be used) + or full file name. By default, the working directory will be used. + """ + response = self.connection.get( + self._get_endpoint(), expected_status=200, stream=True + ) + + target = Path(target or Path.cwd()) + if target.is_dir(): + target = target / self.path.name + ensure_dir(target.parent) + + with target.open(mode="wb") as f: + for chunk in response.iter_content(chunk_size=DEFAULT_DOWNLOAD_CHUNK_SIZE): + f.write(chunk) + + return target
+ + +
+[docs] + def upload(self, source: Union[Path, str]) -> UserFile: + """ + Uploads a local file to the path corresponding to this :py:class:`UserFile` in the user workspace + and returns new :py:class:`UserFile` of newly uploaded file. + + .. tip:: + Usually you'll just need + :py:meth:`Connection.upload_file() <openeo.rest.connection.Connection.upload_file>` + instead of this :py:class:`UserFile` method. + + If the file exists in the user workspace it will be replaced. + + :param source: A path to a file on the local file system to upload. + :return: new :py:class:`UserFile` instance of the newly uploaded file + """ + return self.connection.upload_file(source, target=self.path)
+ + +
+[docs] + def delete(self): + """Delete the user-uploaded file from the user workspace on the back-end.""" + self.connection.delete(self._get_endpoint(), expected_status=204)
+ + +
+[docs] + def to_dict(self) -> Dict[str, Any]: + """Returns the provided metadata as dict.""" + # This is used in internal/jupyter.py to detect and get the original metadata. + # TODO: make this more explicit with an internal API? + return self.metadata
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/rest/vectorcube.html b/_modules/openeo/rest/vectorcube.html new file mode 100644 index 000000000..c900bf246 --- /dev/null +++ b/_modules/openeo/rest/vectorcube.html @@ -0,0 +1,788 @@ + + + + + + + openeo.rest.vectorcube — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.vectorcube

+from __future__ import annotations
+
+import json
+import pathlib
+import typing
+from typing import Callable, List, Optional, Tuple, Union
+
+import shapely.geometry.base
+
+import openeo.rest.datacube
+from openeo.api.process import Parameter
+from openeo.internal.documentation import openeo_process
+from openeo.internal.graph_building import PGNode
+from openeo.internal.warnings import legacy_alias
+from openeo.metadata import CollectionMetadata, CubeMetadata, Dimension
+from openeo.rest._datacube import (
+    THIS,
+    UDF,
+    _ensure_save_result,
+    _ProcessGraphAbstraction,
+    build_child_callback,
+)
+from openeo.rest.job import BatchJob
+from openeo.rest.mlmodel import MlModel
+from openeo.util import InvalidBBoxException, dict_no_none, guess_format, to_bbox_dict
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    from openeo import Connection
+
+
+
+[docs] +class VectorCube(_ProcessGraphAbstraction): + """ + A Vector Cube, or 'Vector Collection' is a data structure containing 'Features': + https://www.w3.org/TR/sdw-bp/#dfn-feature + + The features in this cube are restricted to have a geometry. Geometries can be points, lines, polygons etcetera. + A geometry is specified in a 'coordinate reference system'. https://www.w3.org/TR/sdw-bp/#dfn-coordinate-reference-system-(crs) + """ + + _DEFAULT_VECTOR_FORMAT = "GeoJSON" + + def __init__(self, graph: PGNode, connection: Union[Connection, None], metadata: Optional[CubeMetadata] = None): + super().__init__(pgnode=graph, connection=connection) + self.metadata = metadata + + @classmethod + def _build_metadata(cls, add_properties: bool = False) -> CollectionMetadata: + """Helper to build a (minimal) `CollectionMetadata` object.""" + # Vector cubes have at least a "geometry" dimension + dimensions = [Dimension(name="geometry", type="geometry")] + if add_properties: + dimensions.append(Dimension(name="properties", type="other")) + # TODO #464: use a more generic metadata container than "collection" metadata + return CollectionMetadata(metadata={}, dimensions=dimensions) + +
+[docs] + def process( + self, + process_id: str, + arguments: dict = None, + metadata: Optional[CollectionMetadata] = None, + namespace: Optional[str] = None, + **kwargs, + ) -> VectorCube: + """ + Generic helper to create a new VectorCube by applying a process. + + :param process_id: process id of the process. + :param args: argument dictionary for the process. + :return: new VectorCube instance + """ + pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs) + return VectorCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata)
+ + +
+[docs] + @classmethod + @openeo_process + def load_geojson( + cls, + connection: Connection, + data: Union[dict, str, pathlib.Path, shapely.geometry.base.BaseGeometry, Parameter], + properties: Optional[List[str]] = None, + ) -> VectorCube: + """ + Converts GeoJSON data as defined by RFC 7946 into a vector data cube. + + :param connection: the connection to use to connect with the openEO back-end. + :param data: the geometry to load. One of: + + - GeoJSON-style data structure: e.g. a dictionary with ``"type": "Polygon"`` and ``"coordinates"`` fields + - a path to a local GeoJSON file + - a GeoJSON string + - a shapely geometry object + + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + # TODO: unify with `DataCube._get_geometry_argument` + # TODO #457 also support client side fetching of GeoJSON from URL? + if isinstance(data, str) and data.strip().startswith("{"): + # Assume JSON dump + geometry = json.loads(data) + elif isinstance(data, (str, pathlib.Path)): + # Assume local file + with pathlib.Path(data).open(mode="r", encoding="utf-8") as f: + geometry = json.load(f) + assert isinstance(geometry, dict) + elif isinstance(data, shapely.geometry.base.BaseGeometry): + geometry = shapely.geometry.mapping(data) + elif isinstance(data, Parameter): + geometry = data + elif isinstance(data, dict): + geometry = data + else: + raise ValueError(data) + # TODO #457 client side verification of GeoJSON construct: valid type, valid structure, presence of CRS, ...? + + pg = PGNode(process_id="load_geojson", data=geometry, properties=properties or []) + # TODO #457 always a "properties" dimension? https://github.com/Open-EO/openeo-processes/issues/448 + metadata = cls._build_metadata(add_properties=True) + return cls(graph=pg, connection=connection, metadata=metadata)
+ + +
+[docs] + @classmethod + @openeo_process + def load_url(cls, connection: Connection, url: str, format: str, options: Optional[dict] = None) -> VectorCube: + """ + Loads a file from a URL + + :param connection: the connection to use to connect with the openEO back-end. + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL. + :param format: The file format to use when loading the data. + :param options: The file format parameters to use when reading the data. + Must correspond to the parameters that the server reports as supported parameters for the chosen ``format`` + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + pg = PGNode(process_id="load_url", arguments=dict_no_none(url=url, format=format, options=options)) + # TODO #457 always a "properties" dimension? https://github.com/Open-EO/openeo-processes/issues/448 + metadata = cls._build_metadata(add_properties=True) + return cls(graph=pg, connection=connection, metadata=metadata)
+ + +
+[docs] + @openeo_process + def run_udf( + self, + udf: Union[str, UDF], + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> VectorCube: + """ + Run a UDF on the vector cube. + + It is recommended to provide the UDF just as :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + (the other arguments could be used to override UDF parameters if necessary). + + :param udf: UDF code as a string or :py:class:`UDF <openeo.rest._datacube.UDF>` instance + :param runtime: UDF runtime + :param version: UDF version + :param context: UDF context + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + .. versionadded:: 0.10.0 + + .. versionchanged:: 0.16.0 + Added support to pass self-contained :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + """ + if isinstance(udf, UDF): + # `UDF` instance is preferred usage pattern, but allow overriding. + version = version or udf.version + context = context or udf.context + runtime = runtime or udf.get_runtime(connection=self.connection) + udf = udf.code + else: + if not runtime: + raise ValueError("Argument `runtime` must be specified") + return self.process( + process_id="run_udf", + data=self, udf=udf, runtime=runtime, + arguments=dict_no_none({"version": version, "context": context}), + )
+ + +
+[docs] + @openeo_process + def save_result(self, format: Union[str, None] = "GeoJSON", options: dict = None): + # TODO #401: guard against duplicate save_result nodes? + return self.process( + process_id="save_result", + arguments={ + "data": self, + "format": format or "GeoJSON", + "options": options or {}, + }, + )
+ + +
+[docs] + def execute(self, *, validate: Optional[bool] = None) -> dict: + """Executes the process graph.""" + return self._connection.execute(self.flat_graph(), validate=validate)
+ + +
+[docs] + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + ) -> Union[None, bytes]: + """ + Execute synchronously and download the vector cube. + + The result will be stored to the output path, when specified. + If no output path (or ``None``) is given, the raw download content will be returned as ``bytes`` object. + + :param outputfile: (optional) output file to store the result to + :param format: (optional) output format to use. + :param options: (optional) additional output format options. + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=format, + options=options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.download()", + ) + return self._connection.download(cube.flat_graph(), outputfile=outputfile, validate=validate)
+ + +
+[docs] + def execute_batch( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print=print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: avoid using kwargs as format options + **format_options, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long running jobs, you probably do not want to keep the client running. + + :param job_options: + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) output format to use. + :param format_options: (optional) additional output format options + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.execute_batch()", + ) + job = cube.create_job( + title=title, + description=description, + plan=plan, + budget=budget, + job_options=job_options, + validate=validate, + auto_add_save_result=False, + ) + return job.run_synchronous( + # TODO #135 support multi file result sets too + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + )
+ + +
+[docs] + def create_job( + self, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + **format_options, + ) -> BatchJob: + """ + Sends a job to the backend and returns a ClientJob instance. + + :param out_format: String Format of the job result. + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: A dictionary containing (custom) job options + :param format_options: String Parameters for the job result format + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: Created job. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: avoid using all kwargs as format_options + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options or None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.create_job()", + ) + return self._connection.create_job( + process_graph=cube.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + additional=job_options, + validate=validate, + )
+ + + send_job = legacy_alias(create_job, name="send_job", since="0.10.0") + +
+[docs] + @openeo_process + def filter_bands(self, bands: List[str]) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + return self.process( + process_id="filter_bands", + arguments={"data": THIS, "bands": bands}, + )
+ + +
+[docs] + @openeo_process + def filter_bbox( + self, + *, + west: Optional[float] = None, + south: Optional[float] = None, + east: Optional[float] = None, + north: Optional[float] = None, + extent: Optional[Union[dict, List[float], Tuple[float, float, float, float], Parameter]] = None, + crs: Optional[int] = None, + ) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + if any(c is not None for c in [west, south, east, north]): + if extent is not None: + raise InvalidBBoxException("Don't specify both west/south/east/north and extent") + extent = dict_no_none(west=west, south=south, east=east, north=north) + + if isinstance(extent, Parameter): + pass + else: + extent = to_bbox_dict(extent, crs=crs) + return self.process( + process_id="filter_bbox", + arguments={"data": THIS, "extent": extent}, + )
+ + +
+[docs] + @openeo_process + def filter_labels( + self, condition: Union[PGNode, Callable], dimension: str, context: Optional[dict] = None + ) -> VectorCube: + """ + Filters the dimension labels in the data cube for the given dimension. + Only the dimension labels that match the specified condition are preserved, + all other labels with their corresponding data get removed. + + :param condition: the "child callback" which will be given a single label value (number or string) + and returns a boolean expressing if the label should be preserved. + Also see :ref:`callbackfunctions`. + :param dimension: The name of the dimension to filter on. + + .. versionadded:: 0.22.0 + """ + condition = build_child_callback(condition, parent_parameters=["value"]) + return self.process( + process_id="filter_labels", + arguments=dict_no_none(data=THIS, condition=condition, dimension=dimension, context=context), + )
+ + +
+[docs] + @openeo_process + def filter_vector( + self, geometries: Union["VectorCube", shapely.geometry.base.BaseGeometry, dict], relation: str = "intersects" + ) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + if not isinstance(geometries, (VectorCube, Parameter)): + geometries = self.load_geojson(connection=self.connection, data=geometries) + return self.process( + process_id="filter_vector", + arguments={"data": THIS, "geometries": geometries, "relation": relation}, + )
+ + +
+[docs] + @openeo_process + def fit_class_random_forest( + self, + # TODO #279 #293: target type should be `VectorCube` (with adapters for GeoJSON FeatureCollection, GeoPandas, ...) + target: dict, + # TODO #293 max_variables officially has no default + max_variables: Optional[int] = None, + num_trees: int = 100, + seed: Optional[int] = None, + ) -> MlModel: + """ + Executes the fit of a random forest classification based on the user input of target and predictors. + The Random Forest classification model is based on the approach by Breiman (2001). + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + :param target: The training sites for the classification model as a vector data cube. This is associated with the target + variable for the Random Forest model. The geometry has to be associated with a value to predict (e.g. fractional + forest canopy cover). + :param max_variables: Specifies how many split variables will be used at a node. Default value is `null`, which corresponds to the + number of predictors divided by 3. + :param num_trees: The number of trees build within the Random Forest classification. + :param seed: A randomization seed to use for the random sampling in training. + + .. versionadded:: 0.16.0 + Originally added in version 0.10.0 as :py:class:`DataCube <openeo.rest.datacube.DataCube>` method, + but moved to :py:class:`VectorCube` in version 0.16.0. + """ + pgnode = PGNode( + process_id="fit_class_random_forest", + arguments=dict_no_none( + predictors=self, + # TODO #279 strictly per-spec, target should be a `vector-cube`, but due to lack of proper support we are limited to inline GeoJSON for now + target=target, + max_variables=max_variables, + num_trees=num_trees, + seed=seed, + ), + ) + model = MlModel(graph=pgnode, connection=self._connection) + return model
+ + +
+[docs] + @openeo_process + def fit_regr_random_forest( + self, + # TODO #279 #293: target type should be `VectorCube` (with adapters for GeoJSON FeatureCollection, GeoPandas, ...) + target: dict, + # TODO #293 max_variables officially has no default + max_variables: Optional[int] = None, + num_trees: int = 100, + seed: Optional[int] = None, + ) -> MlModel: + """ + Executes the fit of a random forest regression based on training data. + The Random Forest regression model is based on the approach by Breiman (2001). + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + :param target: The training sites for the regression model as a vector data cube. + This is associated with the target variable for the Random Forest model. + The geometry has to associated with a value to predict (e.g. fractional forest canopy cover). + :param max_variables: Specifies how many split variables will be used at a node. Default value is `null`, which corresponds to the + number of predictors divided by 3. + :param num_trees: The number of trees build within the Random Forest classification. + :param seed: A randomization seed to use for the random sampling in training. + + .. versionadded:: 0.16.0 + Originally added in version 0.10.0 as :py:class:`DataCube <openeo.rest.datacube.DataCube>` method, + but moved to :py:class:`VectorCube` in version 0.16.0. + """ + # TODO #279 #293: `fit_class_random_forest` should be defined on VectorCube instead of DataCube + pgnode = PGNode( + process_id="fit_regr_random_forest", + arguments=dict_no_none( + predictors=self, + # TODO #279 strictly per-spec, target should be a `vector-cube`, but due to lack of proper support we are limited to inline GeoJSON for now + target=target, + max_variables=max_variables, + num_trees=num_trees, + seed=seed, + ), + ) + model = MlModel(graph=pgnode, connection=self._connection) + return model
+ + +
+[docs] + @openeo_process + def apply_dimension( + self, + process: Union[str, typing.Callable, UDF, PGNode], + dimension: str, + target_dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> VectorCube: + """ + Applies a process to all values along a dimension of a data cube. + For example, if the temporal dimension is specified the process will work on the values of a time series. + + The process to apply is specified by providing a callback function in the `process` argument. + + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF <openeo.rest._datacube.UDF>` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns an array of numerical values. + For example: + + - ``"sort"`` (string) + - :py:func:`sort <openeo.processes.sort>` (:ref:`predefined openEO process function <openeo_processes_functions>`) + - ``lambda data: data.concat([42, -3])`` (function or lambda) + + + :param dimension: The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or null (the default) to use the source dimension + specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. + The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A datacube with the UDF applied to the given dimension. + :raises: DimensionNotAvailable + + .. versionadded:: 0.22.0 + """ + process = build_child_callback( + process=process, parent_parameters=["data", "context"], connection=self.connection + ) + arguments = dict_no_none( + { + "data": THIS, + "process": process, + "dimension": dimension, + "target_dimension": target_dimension, + "context": context, + } + ) + return self.process(process_id="apply_dimension", arguments=arguments)
+ + +
+[docs] + def vector_to_raster(self, target: openeo.rest.datacube.DataCube) -> openeo.rest.datacube.DataCube: + """ + Converts this vector cube (:py:class:`VectorCube`) into a raster data cube (:py:class:`~openeo.rest.datacube.DataCube`). + The bounding polygon of homogenous areas of pixels is constructed. + + :param target: a reference raster data cube to adopt the CRS/projection/resolution from. + + .. warning:: ``vector_to_raster`` is an experimental, non-standard process. It is not widely supported, and its API is subject to change. + + .. versionadded:: 0.28.0 + + """ + # TODO: this parameter sniffing is a temporary workaround until + # the `target` parameter name rename has fully settled + # https://github.com/Open-EO/openeo-python-driver/issues/274 + # After that has settled, it is still useful to verify assumptions about this non-standard process. + try: + process_spec = self.connection.describe_process("vector_to_raster") + target_parameter = process_spec["parameters"][1]["name"] + assert "target" in target_parameter + except Exception: + target_parameter = "target" + + pg_node = PGNode( + process_id="vector_to_raster", + arguments={"data": self, target_parameter: target}, + ) + # TODO: the correct metadata has to be passed here: + # replace "geometry" dimension with spatial dimensions of the target cube + return openeo.rest.datacube.DataCube(pg_node, connection=self._connection, metadata=self.metadata)
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/testing.html b/_modules/openeo/testing.html new file mode 100644 index 000000000..4351a8a5f --- /dev/null +++ b/_modules/openeo/testing.html @@ -0,0 +1,170 @@ + + + + + + + openeo.testing — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.testing

+"""
+Utilities for testing of openEO client workflows.
+"""
+
+import json
+from pathlib import Path
+from typing import Callable, Optional, Union
+
+
+
+[docs] +class TestDataLoader: + """ + Helper to resolve paths to test data files, load them as JSON, optionally preprocess them, etc. + + It's intended to be used as a pytest fixture, e.g. from ``conftest.py``: + + .. code-block:: python + + @pytest.fixture + def test_data() -> TestDataLoader: + return TestDataLoader(root=Path(__file__).parent / "data") + + .. versionadded:: 0.30.0 + """ + + def __init__(self, root: Union[str, Path]): + self.data_root = Path(root) + +
+[docs] + def get_path(self, filename: Union[str, Path]) -> Path: + """Get absolute path to a test data file""" + return self.data_root / filename
+ + +
+[docs] + def load_json(self, filename: Union[str, Path], preprocess: Optional[Callable[[str], str]] = None) -> dict: + """Parse data from a test JSON file""" + data = self.get_path(filename).read_text(encoding="utf8") + if preprocess: + data = preprocess(data) + return json.loads(data)
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/testing/results.html b/_modules/openeo/testing/results.html new file mode 100644 index 000000000..57d8d2ade --- /dev/null +++ b/_modules/openeo/testing/results.html @@ -0,0 +1,524 @@ + + + + + + + openeo.testing.results — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.testing.results

+"""
+Assert functions for comparing actual (batch job) results against expected reference data.
+"""
+
+import json
+import logging
+import tempfile
+from pathlib import Path
+from typing import List, Optional, Union
+
+import xarray
+import xarray.testing
+
+from openeo.rest.job import DEFAULT_JOB_RESULTS_FILENAME, BatchJob, JobResults
+from openeo.util import repr_truncate
+
+_log = logging.getLogger(__name__)
+
+
+_DEFAULT_RTOL = 1e-6
+_DEFAULT_ATOL = 1e-6
+
+
+def _load_xarray_netcdf(path: Union[str, Path], **kwargs) -> xarray.Dataset:
+    """
+    Load a netCDF file as Xarray Dataset
+    """
+    _log.debug(f"_load_xarray_netcdf: {path!r}")
+    return xarray.load_dataset(path, **kwargs)
+
+
+def _load_rioxarray_geotiff(path: Union[str, Path], **kwargs) -> xarray.DataArray:
+    """
+    Load a GeoTIFF file as Xarray DataArray (using `rioxarray` extension).
+    """
+    _log.debug(f"_load_rioxarray_geotiff: {path!r}")
+    try:
+        import rioxarray
+    except ImportError as e:
+        raise ImportError("This feature requires 'rioxarray` as optional dependency.") from e
+    return rioxarray.open_rasterio(path, **kwargs)
+
+
+def _load_xarray(path: Union[str, Path], **kwargs) -> Union[xarray.Dataset, xarray.DataArray]:
+    """
+    Generically load a netCDF/GeoTIFF file as Xarray Dataset/DataArray.
+    """
+    path = Path(path)
+    if path.suffix.lower() in {".nc", ".netcdf"}:
+        return _load_xarray_netcdf(path, **kwargs)
+    elif path.suffix.lower() in {".tif", ".tiff", ".gtiff", ".geotiff"}:
+        return _load_rioxarray_geotiff(path, **kwargs)
+    raise ValueError(f"Unsupported file type: {path}")
+
+
+def _load_json(path: Union[str, Path]) -> dict:
+    """
+    Load a JSON file.
+    """
+    with Path(path).open("r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def _as_xarray_dataset(data: Union[str, Path, xarray.Dataset]) -> xarray.Dataset:
+    """
+    Get data as Xarray Dataset (loading from file if needed).
+    """
+    if isinstance(data, (str, Path)):
+        data = _load_xarray(data)
+    # TODO auto-convert DataArray to Dataset?
+    if not isinstance(data, xarray.Dataset):
+        raise ValueError(f"Unsupported type: {type(data)}")
+    return data
+
+
+def _as_xarray_dataarray(data: Union[str, Path, xarray.DataArray]) -> xarray.DataArray:
+    """
+    Convert a path to a NetCDF/GeoTIFF file to an Xarray DataArray.
+
+    :param data: path to a NetCDF/GeoTIFF file or Xarray DataArray
+    :return: Xarray DataArray
+    """
+    if isinstance(data, (str, Path)):
+        data = _load_xarray(data)
+    # TODO: auto-convert Dataset to DataArray?
+    if not isinstance(data, xarray.DataArray):
+        raise ValueError(f"Unsupported type: {type(data)}")
+    return data
+
+
+def _compare_xarray_dataarray(
+    actual: Union[xarray.DataArray, str, Path],
+    expected: Union[xarray.DataArray, str, Path],
+    *,
+    rtol: float = _DEFAULT_RTOL,
+    atol: float = _DEFAULT_ATOL,
+) -> List[str]:
+    """
+    Compare two xarray DataArrays with tolerance and report mismatch issues (as strings)
+
+    Checks that are done (with tolerance):
+    - (optional) Check fraction of mismatching pixels (difference exceeding some tolerance).
+      If fraction is below a given threshold, ignore these mismatches in subsequent comparisons.
+      If fraction is above the threshold, report this issue.
+    - Compare actual and expected data with `xarray.testing.assert_allclose` and specified tolerances.
+
+    :return: list of issues (empty if no issues)
+    """
+    # TODO: make this a public function?
+    # TODO: option for nodata fill value?
+    # TODO: option to include data type check?
+    # TODO: option to cast to some data type (or even rescale) before comparison?
+    # TODO: also compare attributes of the DataArray?
+    actual = _as_xarray_dataarray(actual)
+    expected = _as_xarray_dataarray(expected)
+    issues = []
+
+    # `xarray.testing.assert_allclose` currently does not always
+    # provides detailed information about shape/dimension mismatches
+    # so we enrich the issue listing with some more details
+    if actual.dims != expected.dims:
+        issues.append(f"Dimension mismatch: {actual.dims} != {expected.dims}")
+    for dim in sorted(set(expected.dims).intersection(actual.dims)):
+        acs = actual.coords[dim].values
+        ecs = expected.coords[dim].values
+        if not (acs.shape == ecs.shape and (acs == ecs).all()):
+            issues.append(f"Coordinates mismatch for dimension {dim!r}: {acs} != {ecs}")
+    if actual.shape != expected.shape:
+        issues.append(f"Shape mismatch: {actual.shape} != {expected.shape}")
+
+    try:
+        xarray.testing.assert_allclose(a=actual, b=expected, rtol=rtol, atol=atol)
+    except AssertionError as e:
+        # TODO: message of `assert_allclose` is typically multiline, split it again or make it one line?
+        issues.append(str(e).strip())
+
+    return issues
+
+
+
+[docs] +def assert_xarray_dataarray_allclose( + actual: Union[xarray.DataArray, str, Path], + expected: Union[xarray.DataArray, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataArray`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file. + :param expected: expected or reference data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_xarray_dataarray(actual=actual, expected=expected, rtol=rtol, atol=atol) + if issues: + raise AssertionError("\n".join(issues))
+ + + +def _compare_xarray_datasets( + actual: Union[xarray.Dataset, str, Path], + expected: Union[xarray.Dataset, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +) -> List[str]: + """ + Compare two xarray ``DataSet``s with tolerance and report mismatch issues (as strings) + + :return: list of issues (empty if no issues) + """ + # TODO: make this a public function? + actual = _as_xarray_dataset(actual) + expected = _as_xarray_dataset(expected) + + all_issues = [] + # TODO: just leverage DataSet support in xarray.testing.assert_allclose for all this? + actual_vars = set(actual.data_vars) + expected_vars = set(expected.data_vars) + _log.debug(f"_compare_xarray_datasets: actual_vars={actual_vars!r} expected_vars={expected_vars!r}") + if actual_vars != expected_vars: + all_issues.append(f"Xarray DataSet variables mismatch: {actual_vars} != {expected_vars}") + for var in expected_vars.intersection(actual_vars): + _log.debug(f"_compare_xarray_datasets: comparing variable {var!r}") + issues = _compare_xarray_dataarray(actual[var], expected[var], rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for variable {var!r}:") + all_issues.extend(issues) + return all_issues + + +
+[docs] +def assert_xarray_dataset_allclose( + actual: Union[xarray.Dataset, str, Path], + expected: Union[xarray.Dataset, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataSet`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file + :param expected: expected or reference data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_xarray_datasets(actual=actual, expected=expected, rtol=rtol, atol=atol) + if issues: + raise AssertionError("\n".join(issues))
+ + + +
+[docs] +def assert_xarray_allclose( + actual: Union[xarray.Dataset, xarray.DataArray, str, Path], + expected: Union[xarray.Dataset, xarray.DataArray, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataSet`` or ``DataArray`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray object or path to NetCDF/GeoTIFF file. + :param expected: expected or reference data, provided as Xarray object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + if isinstance(actual, (str, Path)): + actual = _load_xarray(actual) + if isinstance(expected, (str, Path)): + expected = _load_xarray(expected) + + if isinstance(actual, xarray.Dataset) and isinstance(expected, xarray.Dataset): + assert_xarray_dataset_allclose(actual, expected, rtol=rtol, atol=atol) + elif isinstance(actual, xarray.DataArray) and isinstance(expected, xarray.DataArray): + assert_xarray_dataarray_allclose(actual, expected, rtol=rtol, atol=atol) + else: + raise ValueError(f"Unsupported types: {type(actual)} and {type(expected)}")
+ + + +def _as_job_results_download( + job_results: Union[BatchJob, JobResults, str, Path], tmp_path: Optional[Path] = None +) -> Path: + """ + Produce a directory with downloaded job results assets and metadata. + + :param job_results: a batch job, job results metadata object or a path + :param tmp_path: root temp path to download results if needed + :return: + """ + # TODO: support download/copy from other sources (e.g. S3, ...) + if isinstance(job_results, BatchJob): + job_results = job_results.get_results() + if isinstance(job_results, JobResults): + download_dir = tempfile.mkdtemp(dir=tmp_path, prefix=job_results.get_job_id() + "-") + _log.info(f"Downloading results from job {job_results.get_job_id()} to {download_dir}") + job_results.download_files(target=download_dir) + job_results = download_dir + if isinstance(job_results, (str, Path)): + return Path(job_results) + else: + raise ValueError(f"Unsupported type: {type(job_results)}") + + +def _compare_job_results( + actual: Union[BatchJob, JobResults, str, Path], + expected: Union[BatchJob, JobResults, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, + tmp_path: Optional[Path] = None, +) -> List[str]: + """ + Compare two job results sets (directories with downloaded assets and metadata, + e.g. as produced by ``JobResults.download_files()``) + + :return: list of issues (empty if no issues) + """ + actual_dir = _as_job_results_download(actual, tmp_path=tmp_path) + expected_dir = _as_job_results_download(expected, tmp_path=tmp_path) + _log.info(f"Comparing job results: {actual_dir!r} vs {expected_dir!r}") + + all_issues = [] + + actual_filenames = set(p.name for p in actual_dir.glob("*") if p.is_file()) + expected_filenames = set(p.name for p in expected_dir.glob("*") if p.is_file()) + if actual_filenames != expected_filenames: + all_issues.append(f"File set mismatch: {actual_filenames} != {expected_filenames}") + + for filename in expected_filenames.intersection(actual_filenames): + actual_path = actual_dir / filename + expected_path = expected_dir / filename + if filename == DEFAULT_JOB_RESULTS_FILENAME: + issues = _compare_job_result_metadata(actual=actual_path, expected=expected_path) + if issues: + all_issues.append(f"Issues for metadata file {filename!r}:") + all_issues.extend(issues) + elif expected_path.suffix.lower() in {".nc", ".netcdf"}: + issues = _compare_xarray_datasets(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for file {filename!r}:") + all_issues.extend(issues) + elif expected_path.suffix.lower() in {".tif", ".tiff", ".gtiff", ".geotiff"}: + issues = _compare_xarray_dataarray(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for file {filename!r}:") + all_issues.extend(issues) + else: + _log.warning(f"Unhandled job result asset {filename!r}") + + return all_issues + + +def _compare_job_result_metadata( + actual: Union[str, Path], + expected: Union[str, Path], +) -> List[str]: + issues = [] + actual_metadata = _load_json(actual) + expected_metadata = _load_json(expected) + + # Check "derived_from" links + actual_derived_from = set(k["href"] for k in actual_metadata.get("links", []) if k["rel"] == "derived_from") + expected_derived_from = set(k["href"] for k in expected_metadata.get("links", []) if k["rel"] == "derived_from") + + if actual_derived_from != expected_derived_from: + actual_only = actual_derived_from - expected_derived_from + expected_only = expected_derived_from - actual_derived_from + common = actual_derived_from.intersection(expected_derived_from) + issues.append( + f"Differing 'derived_from' links ({len(common)} common, {len(actual_only)} only in actual, {len(expected_only)} only in expected):\n" + f" only in actual: {repr_truncate(actual_only, width=1000)}\n" + f" only in expected: {repr_truncate(expected_only, width=1000)}." + ) + + # TODO: more metadata checks (e.g. spatial and temporal extents)? + + return issues + + +
+[docs] +def assert_job_results_allclose( + actual: Union[BatchJob, JobResults, str, Path], + expected: Union[BatchJob, JobResults, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, + tmp_path: Optional[Path] = None, +): + """ + Assert that two job results sets are equal (with tolerance). + + :param actual: actual job results, provided as :py:class:`~openeo.rest.job.BatchJob` object, + :py:meth:`~openeo.rest.job.JobResults` object or path to directory with downloaded assets. + :param expected: expected job results, provided as :py:class:`~openeo.rest.job.BatchJob` object, + :py:meth:`~openeo.rest.job.JobResults` object or path to directory with downloaded assets. + :param rtol: relative tolerance + :param atol: absolute tolerance + :param tmp_path: root temp path to download results if needed. + It's recommended to pass pytest's `tmp_path` fixture here + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_job_results(actual, expected, rtol=rtol, atol=atol, tmp_path=tmp_path) + if issues: + raise AssertionError("\n".join(issues))
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/debug.html b/_modules/openeo/udf/debug.html new file mode 100644 index 000000000..29edac138 --- /dev/null +++ b/_modules/openeo/udf/debug.html @@ -0,0 +1,157 @@ + + + + + + + openeo.udf.debug — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.debug

+"""
+Debug utilities for UDFs
+"""
+import logging
+import os
+import sys
+
+_log = logging.getLogger(__name__)
+_user_log = logging.getLogger(os.environ.get("OPENEO_UDF_USER_LOGGER", f"{__name__}.user"))
+
+
+
+[docs] +def inspect(data=None, message: str = "", code: str = "User", level: str = "info"): + """ + Implementation of the openEO `inspect` process for UDF contexts. + + Note that it is up to the back-end implementation to properly capture this logging + and include it in the batch job logs. + + :param data: data to log + :param message: message to send in addition to the data + :param code: A label to help identify one or more log entries + :param level: The severity level of this message. Allowed values: "error", "warning", "info", "debug" + + .. versionadded:: 0.10.1 + + .. seealso:: :ref:`udf_logging_with_inspect` + """ + extra = {"data": data, "code": code} + kwargs = {"stacklevel": 2} if sys.version_info >= (3, 8) else {} + _user_log.log(level=logging.getLevelName(level.upper()), msg=message, extra=extra, **kwargs)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/run_code.html b/_modules/openeo/udf/run_code.html new file mode 100644 index 000000000..3b619cd08 --- /dev/null +++ b/_modules/openeo/udf/run_code.html @@ -0,0 +1,458 @@ + + + + + + + openeo.udf.run_code — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.run_code

+"""
+
+Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf)
+"""
+
+import functools
+import inspect
+import logging
+import math
+import pathlib
+import re
+from typing import Callable, List, Union
+
+import numpy
+import pandas
+import shapely
+import xarray
+from pandas import Series
+
+import openeo
+from openeo import UDF
+from openeo.udf import OpenEoUdfException
+from openeo.udf._compat import tomllib
+from openeo.udf.feature_collection import FeatureCollection
+from openeo.udf.structured_data import StructuredData
+from openeo.udf.udf_data import UdfData
+from openeo.udf.xarraydatacube import XarrayDataCube
+
+_log = logging.getLogger(__name__)
+
+
+def _build_default_execution_context():
+    # TODO: is it really necessary to "pre-load" these modules? Isn't user going to import them explicitly in their script anyway?
+    context = {
+        "numpy": numpy, "np": numpy,
+        "xarray": xarray,
+        "pandas": pandas, "pd": pandas,
+        "shapely": shapely,
+        "math": math,
+        "UdfData": UdfData,
+        "XarrayDataCube": XarrayDataCube,
+        "DataCube": XarrayDataCube,  # Legacy alias
+        "StructuredData": StructuredData,
+        "FeatureCollection": FeatureCollection,
+        # "SpatialExtent": SpatialExtent,  # TODO?
+        # "MachineLearnModel": MachineLearnModelConfig, # TODO?
+    }
+
+
+    return context
+
+
+@functools.lru_cache(maxsize=100)
+def load_module_from_string(code: str) -> dict:
+    """
+    Experimental: avoid loading same UDF module more than once, to make caching inside the udf work.
+    @param code:
+    @return:
+    """
+    globals = _build_default_execution_context()
+    exec(code, globals)
+    return globals
+
+
+def _get_annotation_str(annotation: Union[str, type]) -> str:
+    """Get parameter annotation as a string"""
+    if isinstance(annotation, str):
+        return annotation
+    elif isinstance(annotation, type):
+        mod = annotation.__module__
+        return (mod + "." if mod != str.__module__ else "") + annotation.__name__
+    else:
+        return str(annotation)
+
+
+def _annotation_is_pandas_series(annotation) -> bool:
+    return annotation in {pandas.Series, _get_annotation_str(pandas.Series)}
+
+
+def _annotation_is_udf_datacube(annotation) -> bool:
+    return annotation is XarrayDataCube or _get_annotation_str(annotation) in {
+        _get_annotation_str(XarrayDataCube),
+        'openeo_udf.api.datacube.DataCube',  # Legacy `openeo_udf` annotation
+    }
+
+def _annotation_is_data_array(annotation) -> bool:
+    return annotation is xarray.DataArray or _get_annotation_str(annotation) in {
+        _get_annotation_str(xarray.DataArray)
+    }
+
+def _annotation_is_udf_data(annotation) -> bool:
+    return annotation is UdfData or _get_annotation_str(annotation) in {
+        _get_annotation_str(UdfData),
+        'openeo_udf.api.udf_data.UdfData'  # Legacy `openeo_udf` annotation
+    }
+
+
+def _apply_timeseries_xarray(array: xarray.DataArray, callback: Callable[[Series], Series]) -> xarray.DataArray:
+    """
+    Apply timeseries callback to given xarray data array
+    along its time dimension (named "t" or "time")
+
+    :param array: array to transform
+    :param callback: function that transforms a timeseries in another (same size)
+    :return: transformed array
+    """
+    # Make time dimension the last one, and flatten the rest
+    # to create a 1D sequence of input time series (also 1D).
+    [time_position] = [i for (i, d) in enumerate(array.dims) if d in ["t", "time"]]
+    input_series = numpy.moveaxis(array.values, time_position, -1)
+    orig_shape = input_series.shape
+    input_series = input_series.reshape((-1, input_series.shape[-1]))
+
+    applied = numpy.asarray([callback(s) for s in input_series])
+
+    # Reshape to original shape
+    applied = applied.reshape(orig_shape)
+    applied = numpy.moveaxis(applied, -1, time_position)
+    assert applied.shape == array.shape
+
+    return xarray.DataArray(applied, coords=array.coords, dims=array.dims, name=array.name)
+
+
+def apply_timeseries_generic(
+        udf_data: UdfData,
+        callback: Callable[[Series, dict], Series]
+) -> UdfData:
+    """
+    Implements the UDF contract by calling a user provided time series transformation function.
+
+    :param udf_data:
+    :param callback: callable that takes a pandas Series and context dict and returns a pandas Series.
+        See template :py:func:`openeo.udf.udf_signatures.apply_timeseries`
+    :return:
+    """
+    callback = functools.partial(callback, context=udf_data.user_context)
+    datacubes = [
+        XarrayDataCube(_apply_timeseries_xarray(array=cube.array, callback=callback))
+        for cube in udf_data.get_datacube_list()
+    ]
+    # Insert the new tiles as list of raster collection tiles in the input object. The new tiles will
+    # replace the original input tiles.
+    udf_data.set_datacube_list(datacubes)
+    return udf_data
+
+
+def run_udf_code(code: str, data: UdfData) -> UdfData:
+    # TODO: current implementation uses first match directly, first check for multiple matches?
+    module = load_module_from_string(code)
+    functions = ((k, v) for (k, v) in module.items() if callable(v))
+
+    for (fn_name, func) in functions:
+        try:
+            sig = inspect.signature(func)
+        except ValueError:
+            continue
+        params = sig.parameters
+        first_param = next(iter(params.values()), None)
+
+        if (
+                fn_name == 'apply_timeseries'
+                and 'series' in params and 'context' in params
+                and _annotation_is_pandas_series(params["series"].annotation)
+                and _annotation_is_pandas_series(sig.return_annotation)
+        ):
+            _log.info("Found timeseries mapping UDF `{n}` {f!r}".format(n=fn_name, f=func))
+            return apply_timeseries_generic(data, func)
+        elif (
+                fn_name in ['apply_hypercube', 'apply_datacube']
+                and 'cube' in params and 'context' in params
+                and _annotation_is_udf_datacube(params["cube"].annotation)
+                and _annotation_is_udf_datacube(sig.return_annotation)
+        ):
+            _log.info("Found datacube mapping UDF `{n}` {f!r}".format(n=fn_name, f=func))
+            if len(data.get_datacube_list()) != 1:
+                raise ValueError("The provided UDF expects exactly one datacube, but {c} were provided.".format(
+                    c=len(data.get_datacube_list())
+                ))
+            # TODO: also support calls without user context?
+            result_cube = func(cube=data.get_datacube_list()[0], context=data.user_context)
+            data.set_datacube_list([result_cube])
+            return data
+        elif (
+                fn_name in ['apply_datacube']
+                and 'cube' in params and 'context' in params
+                and _annotation_is_data_array(params["cube"].annotation)
+                and _annotation_is_data_array(sig.return_annotation)
+        ):
+            _log.info("Found datacube mapping UDF `{n}` {f!r}".format(n=fn_name, f=func))
+            if len(data.get_datacube_list()) != 1:
+                raise ValueError("The provided UDF expects exactly one datacube, but {c} were provided.".format(
+                    c=len(data.get_datacube_list())
+                ))
+            # TODO: also support calls without user context?
+            result_cube: xarray.DataArray = func(cube=data.get_datacube_list()[0].get_array(), context=data.user_context)
+            data.set_datacube_list([XarrayDataCube(result_cube)])
+            return data
+        elif (
+            fn_name in ["apply_vectorcube"]
+            and "geometries" in params
+            and _get_annotation_str(params["geometries"].annotation) == "geopandas.geodataframe.GeoDataFrame"
+            and "cube" in params
+            and _annotation_is_data_array(params["cube"].annotation)
+        ):
+            if data.get_feature_collection_list is None or data.get_datacube_list() is None:
+                raise ValueError(
+                    "The provided UDF expects a FeatureCollection and a datacube, but received {f} and {c}".format(
+                        f=data.get_feature_collection_list(), c=data.get_datacube_list()
+                    )
+                )
+            if len(data.get_feature_collection_list()) != 1:
+                raise ValueError(
+                    "The provided UDF expects exactly one FeatureCollection, but {c} were provided.".format(
+                        c=len(data.get_feature_collection_list())
+                    )
+                )
+            if len(data.get_datacube_list()) != 1:
+                raise ValueError(
+                    "The provided UDF expects exactly one datacube, but {c} were provided.".format(
+                        c=len(data.get_datacube_list())
+                    )
+                )
+            # TODO: geopandas is optional dependency.
+            input_geoms = data.get_feature_collection_list()[0].data
+            input_cube = data.get_datacube_list()[0].get_array()
+            result_geoms, result_cube = func(geometries=input_geoms, cube=input_cube, context=data.user_context)
+            data.set_datacube_list([XarrayDataCube(result_cube)])
+            data.set_feature_collection_list([FeatureCollection(id="udf_result", data=result_geoms)])
+            return data
+        elif len(params) == 1 and _annotation_is_udf_data(first_param.annotation):
+            _log.info("Found generic UDF `{n}` {f!r}".format(n=fn_name, f=func))
+            func(data)
+            return data
+
+    raise OpenEoUdfException("No UDF found.")
+
+
+
+[docs] +def execute_local_udf(udf: Union[str, openeo.UDF], datacube: Union[str, xarray.DataArray, XarrayDataCube], fmt='netcdf'): + """ + Locally executes an user defined function on a previously downloaded datacube. + + :param udf: the code of the user defined function + :param datacube: the path to the downloaded data in disk or a DataCube + :param fmt: format of the file if datacube is string + :return: the resulting DataCube + """ + if isinstance(udf, openeo.UDF): + udf = udf.code + + if isinstance(datacube, (str, pathlib.Path)): + d = XarrayDataCube.from_file(path=datacube, fmt=fmt) + elif isinstance(datacube, XarrayDataCube): + d = datacube + elif isinstance(datacube, xarray.DataArray): + d = XarrayDataCube(datacube) + else: + raise ValueError(datacube) + d_array = d.get_array() + expected_order = ("t", "bands", "y", "x") + dims = [d for d in expected_order if d in d_array.dims] + + # TODO #472: skip going through XarrayDataCube above, we only need xarray.DataArray here anyway. + d = XarrayDataCube( + d_array.transpose(*dims) + # TODO: this float conversion was in original implementation (0962e00e03) but is that actually necessary? + .astype(numpy.float64) + ) + # wrap to udf_data + udf_data = UdfData(datacube_list=[d]) + + # TODO: enrich to other types like time series, vector data,... probalby by adding named arguments + # signature: UdfData(proj, datacube_list, feature_collection_list, structured_data_list, ml_model_list, metadata) + + # run the udf through the same routine as it would have been parsed in the backend + result = run_udf_code(udf, udf_data) + return result
+ + + +
+[docs] +def extract_udf_dependencies(udf: Union[str, UDF]) -> Union[List[str], None]: + """ + Extract dependencies from UDF code declared in a top-level comment block + following the `inline script metadata specification (PEP 508) <https://packaging.python.org/en/latest/specifications/inline-script-metadata>`_. + + Basic example UDF snippet declaring expected dependencies as embedded metadata + in a comment block: + + .. code-block:: python + + # /// script + # dependencies = [ + # "geojson", + # ] + # /// + + import geojson + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + ... + + .. seealso:: :ref:`python-udf-dependency-declaration` for more in-depth information. + + :param udf: UDF code as a string or :py:class:`~openeo.rest._datacube.UDF` object + :return: List of extracted dependencies or ``None`` when no valid metadata block with dependencies was found. + + .. versionadded:: 0.30.0 + """ + udf_code = udf.code if isinstance(udf, UDF) else udf + + # Extract "script" blocks + script_type = "script" + block_regex = re.compile( + r"^# /// (?P<type>[a-zA-Z0-9-]+)\s*$\s(?P<content>(^#(| .*)$\s)+)^# ///$", flags=re.MULTILINE + ) + script_blocks = [ + match.group("content") for match in block_regex.finditer(udf_code) if match.group("type") == script_type + ] + + if len(script_blocks) > 1: + raise ValueError(f"Multiple {script_type!r} blocks found in top-level comment") + elif len(script_blocks) == 0: + return None + + # Extract dependencies from "script" block + content = "".join( + line[2:] if line.startswith("# ") else line[1:] for line in script_blocks[0].splitlines(keepends=True) + ) + + return tomllib.loads(content).get("dependencies")
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/structured_data.html b/_modules/openeo/udf/structured_data.html new file mode 100644 index 000000000..f487b351e --- /dev/null +++ b/_modules/openeo/udf/structured_data.html @@ -0,0 +1,174 @@ + + + + + + + openeo.udf.structured_data — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.structured_data

+"""
+
+"""
+
+# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf)
+
+from __future__ import annotations
+
+import builtins
+from typing import Union
+
+
+
+[docs] +class StructuredData: + """ + This class represents structured data that is produced by an UDF and can not be represented + as a raster or vector data cube. For example: the result of a statistical + computation. + + Usage example:: + + >>> StructuredData([3, 5, 8, 13]) + >>> StructuredData({"mean": 5, "median": 8}) + >>> StructuredData([('col_1', 'col_2'), (1, 2), (2, 3)], type="table") + """ + + def __init__(self, data: Union[list, dict], description: str = None, type: str = None): + self.data = data + self.type = type or builtins.type(data).__name__ + self.description = description or self.type + + def __repr__(self): + return f"<{type(self).__name__} with {self.type}>" + + def to_dict(self) -> dict: + return dict( + data=self.data, + description=self.description, + type=self.type, + ) + + @classmethod + def from_dict(cls, data: dict) -> StructuredData: + return cls( + data=data["data"], + description=data.get("description"), + type=data.get("type") + )
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/udf_data.html b/_modules/openeo/udf/udf_data.html new file mode 100644 index 000000000..6feb2349f --- /dev/null +++ b/_modules/openeo/udf/udf_data.html @@ -0,0 +1,283 @@ + + + + + + + openeo.udf.udf_data — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.udf_data

+"""
+
+"""
+
+# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf)
+
+from __future__ import annotations
+
+from typing import List, Optional, Union
+
+from openeo.udf.feature_collection import FeatureCollection
+from openeo.udf.structured_data import StructuredData
+from openeo.udf.xarraydatacube import XarrayDataCube
+
+
+
+[docs] +class UdfData: + """ + Container for data passed to a user defined function (UDF) + """ + + # TODO: original implementation in `openeo_udf` project had `get_datacube_by_id`, `get_feature_collection_by_id`: is it still useful to provide this? + # TODO: original implementation in `openeo_udf` project had `server_context`: is it still useful to provide this? + + def __init__( + self, + proj: dict = None, + datacube_list: Optional[List[XarrayDataCube]] = None, + feature_collection_list: Optional[List[FeatureCollection]] = None, + structured_data_list: Optional[List[StructuredData]] = None, + user_context: Optional[dict] = None, + ): + """ + The constructor of the UDF argument class that stores all data required by the + user defined function. + + :param proj: A dictionary of form {"proj type string": "projection description"} e.g. {"EPSG": 4326} + :param datacube_list: A list of data cube objects + :param feature_collection_list: A list of VectorTile objects + :param structured_data_list: A list of structured data objects + """ + self.datacube_list = datacube_list + self.feature_collection_list = feature_collection_list + self.structured_data_list = structured_data_list + self.proj = proj + self._user_context = user_context or {} + + def __repr__(self) -> str: + fields = " ".join( + f"{f}:{getattr(self, f)!r}" for f in + ["datacube_list", "feature_collection_list", "structured_data_list"] + ) + return f"<{type(self).__name__} {fields}>" + + @property + def user_context(self) -> dict: + """Return the user context that was passed to the run_udf function""" + return self._user_context + +
+[docs] + def get_datacube_list(self) -> Union[List[XarrayDataCube], None]: + """Get the data cube list""" + return self._datacube_list
+ + +
+[docs] + def set_datacube_list(self, datacube_list: Union[List[XarrayDataCube], None]): + """ + Set the data cube list + + :param datacube_list: A list of data cubes + """ + self._datacube_list = datacube_list
+ + + datacube_list = property(fget=get_datacube_list, fset=set_datacube_list) + +
+[docs] + def get_feature_collection_list(self) -> Union[List[FeatureCollection], None]: + """get all feature collections as list""" + return self._feature_collection_list
+ + + def set_feature_collection_list(self, feature_collection_list: Union[List[FeatureCollection], None]): + self._feature_collection_list = feature_collection_list + + feature_collection_list = property(fget=get_feature_collection_list, fset=set_feature_collection_list) + +
+[docs] + def get_structured_data_list(self) -> Union[List[StructuredData], None]: + """ + Get all structured data entries + + :return: A list of StructuredData objects + """ + return self._structured_data_list
+ + +
+[docs] + def set_structured_data_list(self, structured_data_list: Union[List[StructuredData], None]): + """ + Set the list of structured data + + :param structured_data_list: A list of StructuredData objects + """ + self._structured_data_list = structured_data_list
+ + + structured_data_list = property(fget=get_structured_data_list, fset=set_structured_data_list) + +
+[docs] + def to_dict(self) -> dict: + """ + Convert this UdfData object into a dictionary that can be converted into + a valid JSON representation + """ + return { + "datacubes": [x.to_dict() for x in self.datacube_list] \ + if self.datacube_list else None, + "feature_collection_list": [x.to_dict() for x in self.feature_collection_list] \ + if self.feature_collection_list else None, + "structured_data_list": [x.to_dict() for x in self.structured_data_list] \ + if self.structured_data_list else None, + "proj": self.proj, + "user_context": self.user_context, + }
+ + +
+[docs] + @classmethod + def from_dict(cls, udf_dict: dict) -> UdfData: + """ + Create a udf data object from a python dictionary that was created from + the JSON definition of the UdfData class + + :param udf_dict: The dictionary that contains the udf data definition + """ + + datacubes = [XarrayDataCube.from_dict(x) for x in udf_dict.get("datacubes", [])] + feature_collection_list = [FeatureCollection.from_dict(x) for x in udf_dict.get("feature_collection_list", [])] + structured_data_list = [StructuredData.from_dict(x) for x in udf_dict.get("structured_data_list", [])] + udf_data = cls( + proj=udf_dict.get("proj"), + datacube_list=datacubes, + feature_collection_list=feature_collection_list, + structured_data_list=structured_data_list, + user_context=udf_dict.get("user_context") + ) + return udf_data
+
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/udf_signatures.html b/_modules/openeo/udf/udf_signatures.html new file mode 100644 index 000000000..014c83360 --- /dev/null +++ b/_modules/openeo/udf/udf_signatures.html @@ -0,0 +1,248 @@ + + + + + + + openeo.udf.udf_signatures — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.udf_signatures

+"""
+This module defines a number of function signatures that can be implemented by UDF's.
+Both the name of the function and the argument types are/can be used by the backend to validate if the provided UDF
+is compatible with the calling context of the process graph in which it is used.
+
+"""
+# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf)
+
+import xarray
+from pandas import Series
+
+from openeo.metadata import CollectionMetadata
+from openeo.udf.udf_data import UdfData
+from openeo.udf.xarraydatacube import XarrayDataCube
+
+try:
+    # Geopandas is an optional dependency, but one of the signatures uses it as type annotation
+    import geopandas
+except ImportError:
+    pass
+
+
+
+[docs] +def apply_timeseries(series: Series, context: dict) -> Series: + """ + Process a timeseries of values, without changing the time instants. + + This can for instance be used for smoothing or gap-filling. + + :param series: A Pandas Series object with a date-time index. + :param context: A dictionary containing user context. + :return: A Pandas Series object with the same datetime index. + """ + # TODO: do we need geospatial coordinates for the series? + return series
+ + + +
+[docs] +def apply_datacube(cube: XarrayDataCube, context: dict) -> XarrayDataCube: + """ + Map a :py:class:`XarrayDataCube` to another :py:class:`XarrayDataCube`. + + Depending on the context in which this function is used, the :py:class:`XarrayDataCube` dimensions + have to be retained or can be chained. + For instance, in the context of a reducing operation along a dimension, + that dimension will have to be reduced to a single value. + In the context of a 1 to 1 mapping operation, all dimensions have to be retained. + + :param cube: input data cube + :param context: A dictionary containing user context. + :return: output data cube + """ + return cube
+ + + +
+[docs] +def apply_udf_data(data: UdfData): + """ + Generic UDF function that directly manipulates a :py:class:`UdfData` object + + :param data: :py:class:`UdfData` object to manipulate in-place + """ + pass
+ + + +
+[docs] +def apply_metadata(metadata: CollectionMetadata, context: dict) -> CollectionMetadata: + """ + .. warning:: + This signature is not yet fully standardized and subject to change. + + Returns the expected cube metadata, after applying this UDF, based on input metadata. + The provided metadata represents the whole raster or vector cube. This function does not need to be called for every data chunk. + + When this function is not implemented by the UDF, the backend may still be able to infer correct metadata by running the + UDF, but this can result in reduced performance or errors. + + This function does not need to be provided when using the UDF in combination with processes that by design have a clear + effect on cube metadata, such as :py:meth:`~openeo.rest.datacube.DataCube.reduce_dimension()` + + :param metadata: the collection metadata of the input data cube + :param context: A dictionary containing user context. + + :return: output metadata: the expected metadata of the cube, after applying the udf + + Examples + -------- + + An example for a UDF that is applied on the 'bands' dimension, and returns a new set of bands with different labels. + + >>> def apply_metadata(metadata: CollectionMetadata, context: dict) -> CollectionMetadata: + ... return metadata.rename_labels( + ... dimension="bands", + ... target=["computed_band_1", "computed_band_2"] + ... ) + + """ + pass
+ + + +
+[docs] +def apply_vectorcube( + geometries: "geopandas.geodataframe.GeoDataFrame", cube: xarray.DataArray, context: dict +) -> ("geopandas.geodataframe.GeoDataFrame", xarray.DataArray): + """ + Map a vector cube to another vector cube. + + :param geometries: input geometries as a geopandas.GeoDataFrame. This contains the actual shapely geometries and optional properties. + :param cube: a data cube with dimensions (geometries, time, bands) where time and bands are optional. + The coordinates for the geometry dimension are integers and match the index of the geometries in the geometries parameter. + :param context: A dictionary containing user context. + :return: output geometries, output data cube + """ + pass
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/udf/xarraydatacube.html b/_modules/openeo/udf/xarraydatacube.html new file mode 100644 index 000000000..0add527e9 --- /dev/null +++ b/_modules/openeo/udf/xarraydatacube.html @@ -0,0 +1,526 @@ + + + + + + + openeo.udf.xarraydatacube — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.udf.xarraydatacube

+"""
+
+"""
+
+# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf)
+
+from __future__ import annotations
+
+import collections
+import json
+import typing
+from pathlib import Path
+from typing import Optional, Union
+
+import numpy
+import xarray
+
+from openeo.udf import OpenEoUdfException
+from openeo.util import deep_get, dict_no_none
+
+if typing.TYPE_CHECKING:
+    # Imports for type checking only (circular import issue at runtime).
+    import matplotlib.colors
+
+
+
+[docs] +class XarrayDataCube: + """ + This is a thin wrapper around :py:class:`xarray.DataArray` + providing a basic "DataCube" interface for openEO UDF usage around multi-dimensional data. + """ + + # TODO #472 This class, just wrapping an array.DataArray, seems to make things more complicated/confusing than necessary. + + def __init__(self, array: xarray.DataArray): + if not isinstance(array, xarray.DataArray): + raise OpenEoUdfException("Argument data must be of type xarray.DataArray") + self._array = array + + def __repr__(self): + return f"<{type(self).__name__} shape:{self._array.shape}>" + +
+[docs] + def get_array(self) -> xarray.DataArray: + """ + Get the :py:class:`xarray.DataArray` that contains the data and dimension definition + """ + return self._array
+ + + array = property(fget=get_array) + + @property + def id(self): + return self._array.name + +
+[docs] + def to_dict(self) -> dict: + """ + Convert this hypercube into a dictionary that can be converted into + a valid JSON representation + + >>> example = { + ... "id": "test_data", + ... "data": [ + ... [[0.0, 0.1], [0.2, 0.3]], + ... [[0.0, 0.1], [0.2, 0.3]], + ... ], + ... "dimension": [ + ... {"name": "time", "coordinates": ["2001-01-01", "2001-01-02"]}, + ... {"name": "X", "coordinates": [50.0, 60.0]}, + ... {"name": "Y"}, + ... ], + ... } + """ + xd = self._array.to_dict() + return dict_no_none({ + "id": xd.get("name"), + "data": xd.get("data"), + "description": deep_get(xd, "attrs", "description", default=None), + "dimensions": [ + dict_no_none( + name=dim, + coordinates=deep_get(xd, "coords", dim, "data", default=None) + ) + for dim in xd.get("dims", []) + ] + })
+ + +
+[docs] + @classmethod + def from_dict(cls, xdc_dict: dict) -> XarrayDataCube: + """ + Create a :py:class:`XarrayDataCube` from a Python dictionary that was created from + the JSON definition of the data cube + + :param data: The dictionary that contains the data cube definition + """ + + if "data" not in xdc_dict: + raise OpenEoUdfException("Missing data in dictionary") + + data = numpy.asarray(xdc_dict["data"]) + + if "dimensions" in xdc_dict: + dims = [dim["name"] for dim in xdc_dict["dimensions"]] + coords = {dim["name"]: dim["coordinates"] for dim in xdc_dict["dimensions"] if "coordinates" in dim} + else: + dims = None + coords = None + + x = xarray.DataArray(data, dims=dims, coords=coords, name=xdc_dict.get("id")) + + if "description" in xdc_dict: + x.attrs["description"] = xdc_dict["description"] + + return cls(array=x)
+ + + @staticmethod + def _guess_format(path: Union[str, Path]) -> str: + """Guess file format from file name.""" + suffix = Path(path).suffix.lower() + if suffix in [".nc", ".netcdf"]: + return "netcdf" + elif suffix in [".json"]: + return "json" + else: + raise ValueError("Can not guess format of {p}".format(p=path)) + +
+[docs] + @classmethod + def from_file(cls, path: Union[str, Path], fmt=None, **kwargs) -> XarrayDataCube: + """ + Load data file as :py:class:`XarrayDataCube` in memory + + :param path: the file on disk + :param fmt: format to load from, e.g. "netcdf" or "json" + (will be auto-detected when not specified) + + :return: loaded data cube + """ + fmt = fmt or cls._guess_format(path) + if fmt.lower() == 'netcdf': + return cls(array=XarrayIO.from_netcdf_file(path=path, **kwargs)) + elif fmt.lower() == 'json': + return cls(array=XarrayIO.from_json_file(path=path)) + else: + raise ValueError("invalid format {f}".format(f=fmt))
+ + +
+[docs] + def save_to_file(self, path: Union[str, Path], fmt=None, **kwargs): + """ + Store :py:class:`XarrayDataCube` to file + + :param path: destination file on disk + :param fmt: format to save as, e.g. "netcdf" or "json" + (will be auto-detected when not specified) + """ + fmt = fmt or self._guess_format(path) + if fmt.lower() == 'netcdf': + XarrayIO.to_netcdf_file(array=self.get_array(), path=path, **kwargs) + elif fmt.lower() == 'json': + XarrayIO.to_json_file(array=self.get_array(), path=path) + else: + raise ValueError(fmt)
+ + +
+[docs] + def plot( + self, + title: str = None, + limits=None, + show_bandnames: bool = True, + show_dates: bool = True, + show_axeslabels: bool = False, + fontsize: float = 10., + oversample: float = 1, + cmap: Union[str, 'matplotlib.colors.Colormap'] = 'RdYlBu_r', + cbartext: str = None, + to_file: str = None, + to_show: bool = True + ): + """ + Visualize a :py:class:`XarrayDataCube` with matplotlib + + :param datacube: data to plot + :param title: title text drawn in the top left corner (default: nothing) + :param limits: range of the contour plot as a tuple(min,max) (default: None, in which case the min/max is computed from the data) + :param show_bandnames: whether to plot the column names (default: True) + :param show_dates: whether to show the dates for each row (default: True) + :param show_axeslabels: whether to show the labels on the axes (default: False) + :param fontsize: font size in pixels (default: 10) + :param oversample: one value is plotted into oversample x oversample number of pixels (default: 1 which means each value is plotted as a single pixel) + :param cmap: built-in matplotlib color map name or ColorMap object (default: RdYlBu_r which is a blue-yellow-red rainbow) + :param cbartext: text on top of the legend (default: nothing) + :param to_file: filename to save the image to (default: None, which means no file is generated) + :param to_show: whether to show the image in a matplotlib window (default: True) + + :return: None + """ + from matplotlib import pyplot + + data = self.get_array() + if limits is None: + vmin = data.min() + vmax = data.max() + else: + vmin = limits[0] + vmax = limits[1] + + # fill bands and t if missing + if 'bands' not in data.dims: + data = data.expand_dims(dim={'bands': ['band0']}) + if 't' not in data.dims: + data = data.expand_dims(dim={'t': [numpy.datetime64('today')]}) + if 'bands' not in data.coords: + data['bands'] = ['band0'] + if 't' not in data.coords: + data['t'] = [numpy.datetime64('today')] + + # align with plot + data = data.transpose('t', 'bands', 'y', 'x') + dpi = 100 + xres = len(data.x) / dpi + yres = len(data.y) / dpi + fs = fontsize / oversample + frame = 0.33 + + nrow = data.shape[0] + ncol = data.shape[1] + + fig = pyplot.figure(figsize=((ncol + frame) * xres * 1.1, (nrow + frame) * yres), dpi=int(dpi * oversample)) + gs = pyplot.GridSpec(nrow, ncol, wspace=0., hspace=0., top=nrow / (nrow + frame), bottom=0., + left=frame / (ncol + frame), right=1.) + + xmin = data.x.min() + xmax = data.x.max() + ymin = data.y.min() + ymax = data.y.max() + + # flip around if incorrect, this is in harmony with origin='lower' + if (data.x[0] > data.x[-1]): + data = data.reindex(x=list(reversed(data.x))) + if (data.y[0] > data.y[-1]): + data = data.reindex(y=list(reversed(data.y))) + + extent = (data.x[0], data.x[-1], data.y[0], data.y[-1]) + + for i in range(nrow): + for j in range(ncol): + im = data[i, j] + ax = pyplot.subplot(gs[i, j]) + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) + img = ax.imshow(im, vmin=vmin, vmax=vmax, cmap=cmap, origin='lower', extent=extent) + ax.xaxis.set_tick_params(labelsize=fs) + ax.yaxis.set_tick_params(labelsize=fs) + if not show_axeslabels: + ax.set_axis_off() + ax.set_xticklabels([]) + ax.set_yticklabels([]) + if show_bandnames: + if i == 0: ax.text(0.5, 1.08, data.bands.values[j] + " (" + str(data.dtype) + ")", size=fs, + va="center", + ha="center", transform=ax.transAxes) + if show_dates: + if j == 0: ax.text(-0.08, 0.5, data.t.dt.strftime("%Y-%m-%d").values[i], size=fs, va="center", + ha="center", rotation=90, transform=ax.transAxes) + + if title is not None: + fig.text(0., 1., title.split('/')[-1], size=fs, va="top", ha="left", weight='bold') + + cbar_ax = fig.add_axes([0.01, 0.1, 0.04, 0.5]) + if cbartext is not None: + fig.text(0.06, 0.62, cbartext, size=fs, va="bottom", ha="center") + cbar = fig.colorbar(img, cax=cbar_ax) + cbar.ax.tick_params(labelsize=fs) + cbar.outline.set_visible(False) + cbar.ax.tick_params(size=0) + cbar.ax.yaxis.set_tick_params(pad=0) + + if to_file is not None: + pyplot.savefig(str(to_file)) + if to_show: + pyplot.show() + + pyplot.close()
+
+ + + +class XarrayIO: + """ + Helpers to load/store :py:cass:`xarray.DataArray` objects, + with some conventions about expected dimensions/bands + """ + + @classmethod + def from_json_file(cls, path: Union[str, Path]) -> xarray.DataArray: + with Path(path).open() as f: + return cls.from_json(json.load(f)) + + @classmethod + def from_json(cls, d: dict) -> xarray.DataArray: + d['data'] = numpy.array(d['data'], dtype=numpy.dtype(d['attrs']['dtype'])) + for k, v in d['coords'].items(): + # prepare coordinate + d['coords'][k]['data'] = numpy.array(v['data'], dtype=v['attrs']['dtype']) + # remove dtype and shape, because that is included for helping the user + if d['coords'][k].get('attrs', None) is not None: + d['coords'][k]['attrs'].pop('dtype', None) + d['coords'][k]['attrs'].pop('shape', None) + + # remove dtype and shape, because that is included for helping the user + if d.get('attrs', None) is not None: + d['attrs'].pop('dtype', None) + d['attrs'].pop('shape', None) + # convert to xarray + r = xarray.DataArray.from_dict(d) + + # build dimension list in proper order + dims = list(filter(lambda i: i != 't' and i != 'bands' and i != 'x' and i != 'y', r.dims)) + if 't' in r.dims: dims += ['t'] + if 'bands' in r.dims: dims += ['bands'] + if 'x' in r.dims: dims += ['x'] + if 'y' in r.dims: dims += ['y'] + # return the resulting data array + return r.transpose(*dims) + + @classmethod + def from_netcdf_file(cls, path: Union[str, Path], engine: Optional[str] = None) -> xarray.DataArray: + # load the dataset and convert to data array + ds = xarray.open_dataset(path, engine=engine) + + # Skip non-numerical variables (like "crs") + band_vars = [k for k, v in ds.data_vars.items() if v.dtype.kind in {"b", "i", "u", "f"} and len(v.dims) > 0] + ds = ds[band_vars] + + r = ds.to_array(dim='bands') + + # Reorder dims to proper order (t-bands-x-y at the end) + expected_order = ("t", "bands", "x", "y") + dims = [d for d in r.dims if d not in expected_order] + [d for d in expected_order if d in r.dims] + + return r.transpose(*dims) + + @classmethod + def to_json_file(cls, array: xarray.DataArray, path: Union[str, Path]): + # to deserialized json + jsonarray = array.to_dict() + # add attributes that needed for re-creating xarray from json + jsonarray['attrs']['dtype'] = str(array.values.dtype) + jsonarray['attrs']['shape'] = list(array.values.shape) + for i in array.coords.values(): + jsonarray['coords'][i.name]['attrs']['dtype'] = str(i.dtype) + jsonarray['coords'][i.name]['attrs']['shape'] = list(i.shape) + # custom print so resulting json file is humanly easy to read + # TODO: make this human friendly JSON format optional and allow compact JSON too. + with Path(path).open("w", encoding="utf-8") as f: + def custom_print(data_structure, indent=1): + f.write("{\n") + needs_comma = False + for key, value in data_structure.items(): + if needs_comma: + f.write(',\n') + needs_comma = True + f.write(' ' * indent + json.dumps(key) + ':') + if isinstance(value, dict): + custom_print(value, indent + 1) + else: + json.dump(value, f, default=str, separators=(',', ':')) + f.write('\n' + ' ' * (indent - 1) + "}") + + custom_print(jsonarray) + + @classmethod + def to_netcdf_file(cls, array: xarray.DataArray, path: Union[str, Path], engine: Optional[str] = None): + # temp reference to avoid modifying the original array + result = array + # rearrange in a basic way because older xarray versions have a bug and ellipsis don't work in xarray.transpose() + if result.dims[-2] == 'x' and result.dims[-1] == 'y': + l = list(result.dims[:-2]) + result = result.transpose(*(l + ['y', 'x'])) + # turn it into a dataset where each band becomes a variable + if not 'bands' in result.dims: + result = result.expand_dims(dim=collections.OrderedDict({'bands': ['band_0']})) + else: + if not 'bands' in result.coords: + labels = ['band_' + str(i) for i in range(result.shape[result.dims.index('bands')])] + result = result.assign_coords(bands=labels) + result = result.to_dataset('bands') + result.to_netcdf(path, engine=engine) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/util.html b/_modules/openeo/util.html new file mode 100644 index 000000000..2ec1f4180 --- /dev/null +++ b/_modules/openeo/util.html @@ -0,0 +1,831 @@ + + + + + + + openeo.util — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.util

+"""
+Various utilities and helpers.
+"""
+
+# TODO #465 split this kitchen-sink in thematic submodules
+
+from __future__ import annotations
+
+import datetime as dt
+import functools
+import json
+import logging
+import re
+import sys
+import time
+from collections import OrderedDict
+from enum import Enum
+from pathlib import Path
+from typing import Any, Callable, List, Optional, Tuple, Union
+from urllib.parse import urljoin
+
+import requests
+import shapely.geometry.base
+from deprecated import deprecated
+
+try:
+    # pyproj is an optional dependency
+    import pyproj
+except ImportError:
+    pyproj = None
+
+
+logger = logging.getLogger(__name__)
+
+
+class Rfc3339:
+    """
+    Formatter for dates according to RFC-3339.
+
+    Parses date(time)-like input and formats according to RFC-3339. Some examples:
+
+        >>> rfc3339.date("2020:03:17")
+        "2020-03-17"
+        >>> rfc3339.date(2020, 3, 17)
+        "2020-03-17"
+        >>> rfc3339.datetime("2020/03/17/12/34/56")
+        "2020-03-17T12:34:56Z"
+        >>> rfc3339.datetime([2020, 3, 17, 12, 34, 56])
+        "2020-03-17T12:34:56Z"
+        >>> rfc3339.datetime(2020, 3, 17)
+        "2020-03-17T00:00:00Z"
+        >>> rfc3339.datetime(datetime(2020, 3, 17, 12, 34, 56))
+        "2020-03-17T12:34:56Z"
+
+    Or just normalize (automatically preserve date/datetime resolution):
+
+        >>> rfc3339.normalize("2020/03/17")
+        "2020-03-17"
+        >>> rfc3339.normalize("2020-03-17-12-34-56")
+        "2020-03-17T12:34:56Z"
+
+    Also see https://tools.ietf.org/html/rfc3339#section-5.6
+    """
+    # TODO: currently we hard code timezone 'Z' for simplicity. Add real time zone support?
+    _FMT_DATE = '%Y-%m-%d'
+    _FMT_TIME = '%H:%M:%SZ'
+    _FMT_DATETIME = _FMT_DATE + "T" + _FMT_TIME
+
+    _regex_datetime = re.compile(r"""
+        ^(?P<Y>\d{4})[:/_-](?P<m>\d{2})[:/_-](?P<d>\d{2})[T :/_-]?
+        (?:(?P<H>\d{2})[:/_-](?P<M>\d{2})(?:[:/_-](?P<S>\d{2}))?)?""", re.VERBOSE)
+
+    def __init__(self, propagate_none: bool = False):
+        self._propagate_none = propagate_none
+
+    def datetime(self, x: Any, *args) -> Union[str, None]:
+        """
+        Format given date(time)-like object as RFC-3339 datetime string.
+        """
+        if args:
+            return self.datetime((x,) + args)
+        elif isinstance(x, dt.datetime):
+            return self._format_datetime(x)
+        elif isinstance(x, dt.date):
+            return self._format_datetime(dt.datetime.combine(x, dt.time()))
+        elif isinstance(x, str):
+            return self._format_datetime(dt.datetime(*self._parse_datetime(x)))
+        elif isinstance(x, (tuple, list)):
+            return self._format_datetime(dt.datetime(*(int(v) for v in x)))
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    def date(self, x: Any, *args) -> Union[str, None]:
+        """
+        Format given date-like object as RFC-3339 date string.
+        """
+        if args:
+            return self.date((x,) + args)
+        elif isinstance(x, (dt.date, dt.datetime)):
+            return self._format_date(x)
+        elif isinstance(x, str):
+            return self._format_date(dt.datetime(*self._parse_datetime(x)))
+        elif isinstance(x, (tuple, list)):
+            return self._format_date(dt.datetime(*(int(v) for v in x)))
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    def normalize(self, x: Any, *args) -> Union[str, None]:
+        """
+        Format given date(time)-like object as RFC-3339 date or date-time string depending on given resolution
+
+            >>> rfc3339.normalize("2020/03/17")
+            "2020-03-17"
+            >>> rfc3339.normalize("2020/03/17/12/34/56")
+            "2020-03-17T12:34:56Z"
+        """
+        if args:
+            return self.normalize((x,) + args)
+        elif isinstance(x, dt.datetime):
+            return self.datetime(x)
+        elif isinstance(x, dt.date):
+            return self.date(x)
+        elif isinstance(x, str):
+            x = self._parse_datetime(x)
+            return self.date(x) if len(x) <= 3 else self.datetime(x)
+        elif isinstance(x, (tuple, list)):
+            return self.date(x) if len(x) <= 3 else self.datetime(x)
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    def parse_date(self, x: Union[str, None]) -> Union[dt.date, None]:
+        """Parse given string as RFC3339 date."""
+        if isinstance(x, str):
+            return dt.datetime.strptime(x, "%Y-%m-%d").date()
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    def parse_datetime(
+        self, x: Union[str, None], with_timezone: bool = False
+    ) -> Union[dt.datetime, None]:
+        """Parse given string as RFC3339 date-time."""
+        if isinstance(x, str):
+            # TODO: Also support parsing other timezones than UTC (Z)
+            if re.search(r":\d+\.\d+", x):
+                res = dt.datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%fZ")
+            else:
+                res = dt.datetime.strptime(x, "%Y-%m-%dT%H:%M:%SZ")
+            if with_timezone:
+                res = res.replace(tzinfo=dt.timezone.utc)
+            return res
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    def parse_date_or_datetime(
+        self, x: Union[str, None], with_timezone: bool = False
+    ) -> Union[dt.date, dt.datetime, None]:
+        """Parse given string as RFC3339 date or date-time."""
+        if isinstance(x, str):
+            if len(x) > 10:
+                return self.parse_datetime(x, with_timezone=with_timezone)
+            else:
+                return self.parse_date(x)
+        elif x is None and self._propagate_none:
+            return None
+        raise ValueError(x)
+
+    @classmethod
+    def _format_datetime(cls, d: dt.datetime) -> str:
+        """Format given datetime as RFC-3339 date-time string."""
+        if d.tzinfo not in {None, dt.timezone.utc}:
+            # TODO: add support for non-UTC timezones?
+            raise ValueError(f"No support for non-UTC timezone {d.tzinfo}")
+        return d.strftime(cls._FMT_DATETIME)
+
+    @classmethod
+    def _format_date(cls, d: dt.date) -> str:
+        """Format given datetime as RFC-3339 date-time string."""
+        return d.strftime(cls._FMT_DATE)
+
+    @classmethod
+    def _parse_datetime(cls, s: str) -> Tuple[int]:
+        """Try to parse string to a date(time) tuple"""
+        try:
+            return tuple(int(v) for v in cls._regex_datetime.match(s).groups() if v is not None)
+        except Exception:
+            raise ValueError("Can not parse as date: {s}".format(s=s))
+
+    def today(self) -> str:
+        """Today (date) in RFC3339 format"""
+        return self.date(dt.date.today())
+
+    def utcnow(self) -> str:
+        """Current UTC datetime in RFC3339 format."""
+        # Current time in UTC timezone (instead of naive `datetime.datetime.utcnow()`, per `datetime` documentation)
+        now = dt.datetime.now(tz=dt.timezone.utc)
+        return self.datetime(now)
+
+
+# Default RFC3339 date-time formatter
+rfc3339 = Rfc3339()
+
+
+@deprecated("Use `rfc3339.normalize`, `rfc3339.date` or `rfc3339.datetime` instead")
+def date_to_rfc3339(d: Any) -> str:
+    """
+    Convert date-like object to a RFC 3339 formatted date string
+
+    see https://tools.ietf.org/html/rfc3339#section-5.6
+    """
+    return rfc3339.normalize(d)
+
+
+def dict_no_none(*args, **kwargs) -> dict:
+    """
+    Helper to build a dict containing given key-value pairs where the value is not None.
+    """
+    return {
+        k: v
+        for k, v in dict(*args, **kwargs).items()
+        if v is not None
+    }
+
+
+def first_not_none(*args):
+    """Return first item from given arguments that is not None."""
+    for item in args:
+        if item is not None:
+            return item
+    raise ValueError("No not-None values given.")
+
+
+def ensure_dir(path: Union[str, Path]) -> Path:
+    """Create directory if it doesn't exist."""
+    path = Path(path)
+    if not path.exists():
+        path.mkdir(parents=True, exist_ok=True)
+    assert path.is_dir()
+    return path
+
+
+def ensure_list(x):
+    """Convert given data structure to a list."""
+    try:
+        return list(x)
+    except TypeError:
+        return [x]
+
+
+class ContextTimer:
+    """
+    Context manager to measure the "wall clock" time (in seconds) inside/for a block of code.
+
+    Usage example:
+
+        with ContextTimer() as timer:
+            # Inside code block: currently elapsed time
+            print(timer.elapsed())
+
+        # Outside code block: elapsed time when block ended
+        print(timer.elapsed())
+
+    """
+
+    __slots__ = ["start", "end"]
+
+    # Function that returns current time in seconds (overridable for unit tests)
+    _clock = time.time
+
+    def __init__(self):
+        self.start = None
+        self.end = None
+
+    def elapsed(self) -> float:
+        """Elapsed time (in seconds) inside or at the end of wrapped context."""
+        if self.start is None:
+            raise RuntimeError("Timer not started.")
+        if self.end is not None:
+            # Elapsed time when exiting context.
+            return self.end - self.start
+        else:
+            # Currently elapsed inside context.
+            return self._clock() - self.start
+
+    def __enter__(self) -> ContextTimer:
+        self.start = self._clock()
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.end = self._clock()
+
+
+class TimingLogger:
+    """
+    Context manager for quick and easy logging of start time, end time and elapsed time of some block of code
+
+    Usage example:
+
+    >>> with TimingLogger("Doing batch job"):
+    ...     do_batch_job()
+
+    At start of the code block the current time will be logged
+    and at end of the code block the end time and elapsed time will be logged.
+
+    Can also be used as a function/method decorator, for example:
+
+    >>> @TimingLogger("Calculation going on")
+    ... def add(x, y):
+    ...     return x + y
+    """
+
+    # Function that returns current datetime (overridable for unit tests)
+    _now = dt.datetime.now
+
+    def __init__(self, title: str = "Timing", logger: Union[logging.Logger, str, Callable] = logger):
+        """
+        :param title: the title to use in the logging
+        :param logger: how the timing should be logged.
+            Can be specified as a logging.Logger object (in which case the INFO log level will be used),
+            as a string (name of the logging.Logger object to construct),
+            or as callable (e.g. to use the `print` function, or the `.debug` method of an existing logger)
+        """
+        self.title = title
+        if isinstance(logger, str):
+            logger = logging.getLogger(logger)
+        if isinstance(logger, (logging.Logger, logging.LoggerAdapter)):
+            self._log = logger.info
+        elif callable(logger):
+            self._log = logger
+        else:
+            raise ValueError("Invalid logger {l!r}".format(l=logger))
+
+        self.start_time = self.end_time = self.elapsed = None
+
+    def __enter__(self):
+        self.start_time = self._now()
+        self._log("{t}: start {s}".format(t=self.title, s=self.start_time))
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.end_time = self._now()
+        self.elapsed = self.end_time - self.start_time
+        self._log("{t}: {s} {e}, elapsed {d}".format(
+            t=self.title,
+            s="fail" if exc_type else "end",
+            e=self.end_time, d=self.elapsed
+        ))
+
+    def __call__(self, f: Callable):
+        """
+        Use TimingLogger as function/method decorator
+        """
+
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            with self:
+                return f(*args, **kwargs)
+
+        return wrapper
+
+
+class DeepKeyError(LookupError):
+    def __init__(self, key, keys):
+        super(DeepKeyError, self).__init__("{k!r} (from deep key {s!r})".format(k=key, s=keys))
+
+
+# Sentinel object for `default` argument of `deep_get`
+_deep_get_default_undefined = object()
+
+
+def deep_get(data: dict, *keys, default=_deep_get_default_undefined):
+    """
+    Get value deeply from nested dictionaries/lists/tuples
+
+    :param data: nested data structure of dicts, lists, tuples
+    :param keys: sequence of keys/indexes to traverse
+    :param default: default value when a key is missing.
+        By default a DeepKeyError will be raised.
+    :return:
+    """
+    for key in keys:
+        if isinstance(data, dict) and key in data:
+            data = data[key]
+        elif isinstance(data, (list, tuple)) and isinstance(key, int) and 0 <= key < len(data):
+            data = data[key]
+        else:
+            if default is _deep_get_default_undefined:
+                raise DeepKeyError(key, keys)
+            else:
+                return default
+    return data
+
+
+def deep_set(data: dict, *keys, value):
+    """
+    Set a value deeply in nested dictionary
+
+    :param data: nested data structure of dicts, lists, tuples
+    :param keys: sequence of keys/indexes to traverse
+    :param value: value to set
+    """
+    if len(keys) == 1:
+        data[keys[0]] = value
+    elif len(keys) > 1:
+        if isinstance(data, dict):
+            deep_set(data.setdefault(keys[0], OrderedDict()), *keys[1:], value=value)
+        elif isinstance(data, (list, tuple)):
+            deep_set(data[keys[0]], *keys[1:], value=value)
+        else:
+            ValueError(data)
+    else:
+        raise ValueError("No keys given")
+
+
+def guess_format(filename: Union[str, Path]) -> Union[str, None]:
+    """
+    Guess the output format from a given filename and return the corrected format.
+    Any names not in the dict get passed through.
+    """
+    extension = Path(filename).suffix
+    if not extension:
+        return None
+    extension = extension[1:].lower()
+
+    format_map = {
+        "gtiff": "GTiff",
+        "geotiff": "GTiff",
+        "geotif": "GTiff",
+        "tiff": "GTiff",
+        "tif": "GTiff",
+        "nc": "netCDF",
+        "netcdf": "netCDF",
+        "geojson": "GeoJSON",
+    }
+
+    return format_map.get(extension, extension.upper())
+
+
+def load_json(path: Union[Path, str]) -> dict:
+    with Path(path).open("r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+
+[docs] +def load_json_resource(src: Union[str, Path]) -> dict: + """ + Helper to load some kind of JSON resource + + :param src: a JSON resource: a raw JSON string, + a path to (local) JSON file, or a URL to a remote JSON resource + :return: data structured parsed from JSON + """ + if isinstance(src, str) and src.strip().startswith("{"): + # Assume source is a raw JSON string + return json.loads(src) + elif isinstance(src, str) and re.match(r"^https?://", src, flags=re.I): + # URL to remote JSON resource + return requests.get(src).json() + elif isinstance(src, Path) or (isinstance(src, str) and src.endswith(".json")): + # Assume source is a local JSON file path + return load_json(src) + raise ValueError(src)
+ + + +class LazyLoadCache: + """Simple cache that allows to (lazy) load on cache miss.""" + + def __init__(self): + self._cache = {} + + def get(self, key: Union[str, tuple], load: Callable[[], Any]): + if key not in self._cache: + self._cache[key] = load() + return self._cache[key] + + +def str_truncate(text: str, width: int = 64, ellipsis: str = "...") -> str: + """Shorten a string (with an ellipsis) if it is longer than certain length.""" + width = max(0, int(width)) + if len(text) <= width: + return text + if len(ellipsis) > width: + ellipsis = ellipsis[:width] + return text[:max(0, (width - len(ellipsis)))] + ellipsis + + +def repr_truncate(obj: Any, width: int = 64, ellipsis: str = "...") -> str: + """Do `repr` rendering of an object, but truncate string if it is too long .""" + if isinstance(obj, str) and width > len(ellipsis) + 2: + # Special case: put ellipsis inside quotes + return repr(str_truncate(text=obj, width=width - 2, ellipsis=ellipsis)) + else: + # General case: just put ellipsis at end + return str_truncate(text=repr(obj), width=width, ellipsis=ellipsis) + + +def in_interactive_mode() -> bool: + """Detect if we are running in interactive mode (Jupyter/IPython/repl)""" + # Based on https://stackoverflow.com/a/64523765 + return hasattr(sys, "ps1") + + +class InvalidBBoxException(ValueError): + pass + + +
+[docs] +class BBoxDict(dict): + """ + Dictionary based helper to easily create/work with bounding box dictionaries + (having keys "west", "south", "east", "north", and optionally "crs"). + + :param crs: value describing the coordinate reference system. + Typically just an int (interpreted as EPSG code, e.g. ``4326``) + or a string (handled as authority string, e.g. ``"EPSG:4326"``). + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + + .. versionadded:: 0.10.1 + """ + + def __init__(self, *, west: float, south: float, east: float, north: float, crs: Optional[Union[str, int]] = None): + super().__init__(west=west, south=south, east=east, north=north) + if crs is not None: + self.update(crs=normalize_crs(crs)) + + # TODO: provide west, south, east, north, crs as @properties? Read-only or read-write? + + @classmethod + def from_any(cls, x: Any, *, crs: Optional[str] = None) -> BBoxDict: + if isinstance(x, dict): + if crs and "crs" in x and crs != x["crs"]: + raise InvalidBBoxException(f"Two CRS values specified: {crs} and {x['crs']}") + return cls.from_dict({"crs": crs, **x}) + elif isinstance(x, (list, tuple)): + return cls.from_sequence(x, crs=crs) + elif isinstance(x, shapely.geometry.base.BaseGeometry): + return cls.from_sequence(x.bounds, crs=crs) + # TODO: support other input? E.g.: WKT string, GeoJson-style dictionary (Polygon, FeatureCollection, ...) + else: + raise InvalidBBoxException(f"Can not construct BBoxDict from {x!r}") + +
+[docs] + @classmethod + def from_dict(cls, data: dict) -> BBoxDict: + """Build from dictionary with at least keys "west", "south", "east", and "north".""" + expected_fields = {"west", "south", "east", "north"} + # TODO: also support upper case fields? + # TODO: optional support for parameterized bbox fields? + missing = expected_fields.difference(data.keys()) + if missing: + raise InvalidBBoxException(f"Missing bbox fields {sorted(missing)}") + invalid = {k: data[k] for k in expected_fields if not isinstance(data[k], (int, float))} + if invalid: + raise InvalidBBoxException(f"Non-numerical bbox fields {invalid}.") + return cls(west=data["west"], south=data["south"], east=data["east"], north=data["north"], crs=data.get("crs"))
+ + +
+[docs] + @classmethod + def from_sequence(cls, seq: Union[list, tuple], crs: Optional[str] = None) -> BBoxDict: + """Build from sequence of 4 bounds (west, south, east and north).""" + if len(seq) != 4: + raise InvalidBBoxException(f"Expected sequence with 4 items, but got {len(seq)}.") + return cls(west=seq[0], south=seq[1], east=seq[2], north=seq[3], crs=crs)
+
+ + + +
+[docs] +def to_bbox_dict(x: Any, *, crs: Optional[Union[str, int]] = None) -> BBoxDict: + """ + Convert given data or object to a bounding box dictionary + (having keys "west", "south", "east", "north", and optionally "crs"). + + Supports various input types/formats: + + - list/tuple (assumed to be in west-south-east-north order) + + >>> to_bbox_dict([3, 50, 4, 51]) + {'west': 3, 'south': 50, 'east': 4, 'north': 51} + + - dictionary (unnecessary items will be stripped) + + >>> to_bbox_dict({ + ... "color": "red", "shape": "triangle", + ... "west": 1, "south": 2, "east": 3, "north": 4, "crs": "EPSG:4326", + ... }) + {'west': 1, 'south': 2, 'east': 3, 'north': 4, 'crs': 'EPSG:4326'} + + - a shapely geometry + + .. versionadded:: 0.10.1 + + :param x: input data that describes west-south-east-north bounds in some way, e.g. as a dictionary, + a list, a tuple, ashapely geometry, ... + :param crs: (optional) CRS field + :return: dictionary (subclass) with keys "west", "south", "east", "north", and optionally "crs". + """ + return BBoxDict.from_any(x=x, crs=crs)
+ + + +def url_join(root_url: str, path: str): + """Join a base url and sub path properly.""" + return urljoin(root_url.rstrip("/") + "/", path.lstrip("/")) + + +def clip(x: float, min: float, max: float) -> float: + """Clip given value between minimum and maximum value""" + return min if x < min else (x if x < max else max) + + +class SimpleProgressBar: + """Simple ASCII-based progress bar helper.""" + + __slots__ = ["width", "bar", "fill", "left", "right"] + + def __init__(self, width: int = 40, *, bar: str = "#", fill: str = "-", left: str = "[", right: str = "]"): + self.width = int(width) + self.bar = bar[0] + self.fill = fill[0] + self.left = left + self.right = right + + def get(self, fraction: float) -> str: + width = self.width - len(self.left) - len(self.right) + bar = self.bar * int(round(width * clip(fraction, min=0, max=1))) + return f"{self.left}{bar:{self.fill}<{width}s}{self.right}" + + +
+[docs] +def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]: + """ + Normalize the given value (describing a CRS or Coordinate Reference System) + to an openEO compatible EPSG code (int) or WKT2 CRS string. + + At minimum, the following input values are handled: + + - an integer value (e.g. ``4326``) is interpreted as an EPSG code + - a string that just contains an integer (e.g. ``"4326"``) + or with and additional ``"EPSG:"`` prefix (e.g. ``"EPSG:4326"``) + will also be interpreted as an EPSG value + + Additional support and behavior depends on the availability of the ``pyproj`` library: + + - When available, it will be used for parsing and validation: + everything supported by `pyproj.CRS.from_user_input <https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input>`_ is allowed. + See the ``pyproj`` docs for more details. + - Otherwise, some best effort validation is done: + EPSG looking integer or string values will be parsed as such as discussed above. + Other strings will be assumed to be WKT2 already. + Other data structures will not be accepted. + + :param crs: value that encodes a coordinate reference system, typically just an int (EPSG code) or string (authority string). + If the ``pyproj`` library is available, everything supported by it is allowed. + + :param use_pyproj: whether ``pyproj`` should be leveraged at all + (mainly useful for testing the "no pyproj available" code path) + + :return: EPSG code as int, or WKT2 string. Or None if input was empty. + + :raises ValueError: + When the given CRS data can not be parsed/converted/normalized. + + """ + if crs in (None, "", {}): + return None + + if pyproj and use_pyproj: + try: + # (if available:) let pyproj do the validation/parsing + crs_obj = pyproj.CRS.from_user_input(crs) + # Convert back to EPSG int or WKT2 string + crs = crs_obj.to_epsg() or crs_obj.to_wkt() + except pyproj.ProjError as e: + raise ValueError(f"Failed to normalize CRS data with pyproj: {crs!r}") from e + else: + # Best effort simple validation/normalization + if isinstance(crs, int) and crs > 0: + # Assume int is already valid EPSG code + pass + elif isinstance(crs, str): + # Parse as EPSG int code if it looks like that, + # otherwise: leave it as-is, assuming it is a valid WKT2 CRS string + if re.match(r"^(epsg:)?\d+$", crs.strip(), flags=re.IGNORECASE): + crs = int(crs.split(":")[-1]) + elif "GEOGCRS[" in crs: + # Very simple WKT2 CRS detection heuristic + logger.warning(f"Assuming this is a valid WK2 CRS string: {repr_truncate(crs)}") + else: + raise ValueError(f"Can not normalize CRS string {repr_truncate(crs)}") + else: + raise ValueError(f"Can not normalize CRS data {type(crs)}") + + return crs
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_sources/api-processbuilder.rst.txt b/_sources/api-processbuilder.rst.txt new file mode 100644 index 000000000..7ebdca75c --- /dev/null +++ b/_sources/api-processbuilder.rst.txt @@ -0,0 +1,87 @@ +.. FYI this file is intended to be inlined (with "include" RST directive) + in the ProcessBuilder class doc block, + which in turn is covered with autodoc/automodule from api-processes.rst. + + +The :py:class:`ProcessBuilder ` class +is a helper class that implements +(much like the :ref:`openEO process functions `) +each openEO process as a method. +On top of that it also adds syntactic sugar to support Python operators as well +(e.g. ``+`` is translated to the ``add`` process). + +.. attention:: + As normal user, you should never create a + :py:class:`ProcessBuilder ` instance + directly. + + You should only interact with this class inside a callback + function/lambda while building a child callback process graph + as discussed at :ref:`child_callback_callable`. + + +For example, let's start from this simple usage snippet +where we want to reduce the temporal dimension +by taking the temporal mean of each timeseries: + +.. code-block:: python + + def my_reducer(data): + return data.mean() + + cube.reduce_dimension(reducer=my_reducer, dimension="t") + +Note that this ``my_reducer`` function has a ``data`` argument, +which conceptually corresponds to an array of pixel values +(along the temporal dimension). +However, it's important to understand that the ``my_reducer`` function +is actually *not evaluated when you execute your process graph* +on an openEO back-end, e.g. as a batch jobs. +Instead, ``my_reducer`` is evaluated +*while building your process graph client-side* +(at the time you execute that ``cube.reduce_dimension()`` statement to be precise). +This means that that ``data`` argument is actually not a concrete array of EO data, +but some kind of *virtual placeholder*, +a :py:class:`ProcessBuilder ` instance, +that keeps track of the operations you intend to do on the EO data. + +To make that more concrete, it helps to add type hints +which will make it easier to discover what you can do with the argument +(depending on which editor or IDE you are using): + +.. code-block:: python + + from openeo.processes import ProcessBuilder + + def my_reducer(data: ProcessBuilder) -> ProcessBuilder: + return data.mean() + + cube.reduce_dimension(reducer=my_reducer, dimension="t") + + +Because :py:class:`ProcessBuilder ` methods +return new :py:class:`ProcessBuilder ` instances, +and because it support syntactic sugar to use Python operators on it, +and because :ref:`openeo.process functions ` +also accept and return :py:class:`ProcessBuilder ` instances, +we can mix methods, functions and operators in the callback function like this: + +.. code-block:: python + + from openeo.processes import ProcessBuilder, cos + + def my_reducer(data: ProcessBuilder) -> ProcessBuilder: + return cos(data.mean()) + 1.23 + + cube.reduce_dimension(reducer=my_reducer, dimension="t") + +or compactly, using an anonymous lambda expression: + +.. code-block:: python + + from openeo.processes import cos + + cube.reduce_dimension( + reducer=lambda data: cos(data.mean())) + 1.23, + dimension="t" + ) diff --git a/_sources/api-processes.rst.txt b/_sources/api-processes.rst.txt new file mode 100644 index 000000000..b52384e35 --- /dev/null +++ b/_sources/api-processes.rst.txt @@ -0,0 +1,68 @@ +========================= +API: ``openeo.processes`` +========================= + +The ``openeo.processes`` module contains building blocks and helpers +to construct so called "child callbacks" for openEO processes like +:py:meth:`openeo.rest.datacube.DataCube.apply` and +:py:meth:`openeo.rest.datacube.DataCube.reduce_dimension`, +as discussed at :ref:`child_callback_callable`. + +.. note:: + The contents of the ``openeo.processes`` module is automatically compiled + from the official openEO process specifications. + Developers that want to fix bugs in, or add implementations to this + module should not touch the file directly, but instead address it in the + upstream `openeo-processes `_ repository + or in the internal tooling to generate this file. + + +.. contents:: Sections: + :depth: 1 + :local: + :backlinks: top + + +.. _openeo_processes_functions: + +Functions in ``openeo.processes`` +--------------------------------- + +The ``openeo.processes`` module implements (at top-level) +a regular Python function for each openEO process +(not only the official stable ones, but also experimental ones in "proposal" state). + +These functions can be used directly as child callback, +for example as follows: + +.. code-block:: python + + from openeo.processes import absolute, max + + cube.apply(absolute) + cube.reduce_dimension(max, dimension="t") + + +Note how the signatures of the parent :py:class:`DataCube ` methods +and the callback functions match up: + +- :py:meth:`DataCube.apply() ` + expects a callback that receives a single numerical value, + which corresponds to the parameter signature of :py:func:`openeo.processes.absolute` +- :py:meth:`DataCube.reduce_dimension() ` + expects a callback that receives an array of numerical values, + which corresponds to the parameter signature :py:func:`openeo.processes.max` + + +.. automodule:: openeo.processes + :members: + :exclude-members: ProcessBuilder, process, _process + + +``ProcessBuilder`` helper class +-------------------------------- + +.. FYI the ProcessBuilder docs are provided through its doc block + with an RST "include" of "api-processbuilder.rst" + +.. autoclass:: openeo.processes.ProcessBuilder diff --git a/_sources/api.rst.txt b/_sources/api.rst.txt new file mode 100644 index 000000000..a20747cc4 --- /dev/null +++ b/_sources/api.rst.txt @@ -0,0 +1,177 @@ +============= +API (General) +============= + +High level Interface +-------------------- + +The high-level interface tries to provide an opinionated, Pythonic, API +to interact with openEO back-ends. It's aim is to hide some of the details +of using a web service, so the user can produce concise and readable code. + +Users that want to interact with openEO on a lower level, and have more control, can +use the lower level classes. + + +openeo +-------- + +.. autofunction:: openeo.connect + + +openeo.rest.datacube +----------------------- + +.. automodule:: openeo.rest.datacube + :members: DataCube + :inherited-members: + :special-members: __init__ + +.. automodule:: openeo.rest._datacube + :members: UDF + + +openeo.rest.vectorcube +------------------------ + +.. automodule:: openeo.rest.vectorcube + :members: VectorCube + :inherited-members: + + +openeo.rest.mlmodel +--------------------- + +.. automodule:: openeo.rest.mlmodel + :members: MlModel + :inherited-members: + + +openeo.rest.multiresult +----------------------- + +.. automodule:: openeo.rest.multiresult + :members: MultiResult + :inherited-members: + :special-members: __init__ + + +openeo.metadata +---------------- + +.. automodule:: openeo.metadata + :members: CollectionMetadata, BandDimension, SpatialDimension, TemporalDimension + + +openeo.api.process +-------------------- + +.. automodule:: openeo.api.process + :members: Parameter + + +openeo.api.logs +----------------- + +.. automodule:: openeo.api.logs + :members: LogEntry, normalize_log_level + + +openeo.rest.connection +---------------------- + +.. automodule:: openeo.rest.connection + :members: Connection + + +openeo.rest.job +------------------ + +.. automodule:: openeo.rest.job + :members: BatchJob, RESTJob, JobResults, ResultAsset + + +openeo.rest.conversions +------------------------- + +.. automodule:: openeo.rest.conversions + :members: + + +openeo.rest.udp +----------------- + +.. automodule:: openeo.rest.udp + :members: RESTUserDefinedProcess, build_process_dict + + +openeo.rest.userfile +---------------------- + +.. automodule:: openeo.rest.userfile + :members: + + +openeo.udf +------------- + +.. automodule:: openeo.udf.udf_data + :members: UdfData + +.. automodule:: openeo.udf.xarraydatacube + :members: XarrayDataCube + +.. automodule:: openeo.udf.structured_data + :members: StructuredData + +.. automodule:: openeo.udf.run_code + :members: execute_local_udf, extract_udf_dependencies + +.. automodule:: openeo.udf.debug + :members: inspect + + +openeo.util +------------- + +.. automodule:: openeo.util + :members: to_bbox_dict, BBoxDict, load_json_resource, normalize_crs + + +openeo.processes +---------------- + +.. Note that only openeo.processes.process is included here + the rest of openeo.processes is included from api-processes.rst + +.. autofunction:: openeo.processes.process + + +Graph building +---------------- + +Various utilities and helpers to simplify the construction of openEO process graphs. + +.. automodule:: openeo.rest.graph_building + :members: collection_property, CollectionProperty + +.. automodule:: openeo.internal.graph_building + :members: PGNode, FlatGraphableMixin + + +Testing +-------- + +Various utilities for testing use cases (unit tests, integration tests, benchmarking, ...) + +openeo.testing +`````````````` + +.. automodule:: openeo.testing + :members: + +openeo.testing.results +`````````````````````` + +.. automodule:: openeo.testing.results + :members: diff --git a/_sources/auth.rst.txt b/_sources/auth.rst.txt new file mode 100644 index 000000000..dfc8a47e9 --- /dev/null +++ b/_sources/auth.rst.txt @@ -0,0 +1,611 @@ +.. _authentication_chapter: + +************************************* +Authentication and Account Management +************************************* + + +While a couple of openEO operations can be done +anonymously, most of the interesting parts +of the API require you to identify as a registered +user. +The openEO API specifies two ways to authenticate +as a user: + +* OpenID Connect (recommended, but not always straightforward to use) +* Basic HTTP Authentication (not recommended, but practically easier in some situations) + +To illustrate how to authenticate with the openEO Python Client Library, +we start form a back-end connection:: + + import openeo + + connection = openeo.connect("https://openeo.example.com") + +Basic HTTP Auth +=============== + +Let's start with the easiest authentication method, +based on the Basic HTTP authentication scheme. +It is however *not recommended* for various reasons, +such as its limited *security* measures. +For example, if you are connecting to a back-end with a ``http://`` URL +instead of a ``https://`` one, you should certainly not use basic HTTP auth. + +With these security related caveats out of the way, you authenticate +using your username and password like this:: + + connection.authenticate_basic("john", "j0hn123") + +Subsequent usage of the connection object ``connection`` will +use authenticated calls. +For example, show information about the authenticated user:: + + >>> connection.describe_account() + {'user_id': 'john'} + + + +OpenID Connect Based Authentication +=================================== + +OpenID Connect (often abbreviated "OIDC") is an identity layer on top of the OAuth 2.0 protocol. +An in-depth discussion of the whole architecture would lead us too far here, +but some central OpenID Connect concepts are quite useful to understand +in the context of working with openEO: + +* There is **decoupling** between: + + * the *OpenID Connect identity provider* + which handles the authentication/authorization and stores user information + (e.g. an organization Google, Github, Microsoft, your academic/research institution, ...) + * the *openEO back-end* which manages earth observation collections + and executes your algorithms + + Instead of managing the authentication procedure itself, + an openEO back-end forwards a user to the relevant OpenID Connect provider to authenticate + and request access to basic profile information (e.g. email address). + On return, when the user allowed this access, + the openEO back-end receives the profile information and uses this to identify the user. + + Note that with this approach, the back-end does not have to + take care of all the security and privacy challenges + of properly handling user registration, passwords/authentication, etc. + Also, it allows the user to securely reuse an existing account + registered with an established organisation, instead of having + to register yet another account with some web service. + +* Your openEO script or application acts as + a so called **OpenID Connect client**, with an associated **client id**. + In most cases, a default client (id) defined by the openEO back-end will be used automatically. + For some applications a custom client might be necessary, + but this is out of scope of this documentation. + +* OpenID Connect authentication can be done with different kind of "**flows**" (also called "grants") + and picking the right flow depends on your specific use case. + The most common OIDC flows using the openEO Python Client Library are: + + * :ref:`authenticate_oidc_device` + * :ref:`authenticate_oidc_client_credentials` + * :ref:`authenticate_oidc_refresh_token` + + +OpenID Connect is clearly more complex than Basic HTTP Auth. +In the sections below we will discuss the practical details of each flow. + +General options +--------------- + +* A back-end might support **multiple OpenID Connect providers**. + The openEO Python Client Library will pick the first one by default, + but another another provider can specified explicity with the ``provider_id`` argument, e.g.: + + .. code-block:: python + + connection.authenticate_oidc_device( + provider_id="gl", + ... + ) + + + +.. _authenticate_oidc_device: + +OIDC Authentication: Device Code Flow +====================================== + +The device code flow (also called device authorization grant) +is an interactive flow that requires a web browser for the authentication +with the OpenID Connect provider. +The nice things is that the browser doesn't have to run on +the same system or network as where you run your application, +you could even use a browser on your mobile phone. + +Use :py:meth:`~openeo.rest.connection.Connection.authenticate_oidc_device` to initiate the flow: + +.. code-block:: python + + connection.authenticate_oidc_device() + +This will print a message like this: + +.. code-block:: text + + Visit https://oidc.example.net/device + and enter user code 'DTNY-KLNX' to authenticate. + +Some OpenID Connect Providers use a slightly longer URL that already includes +the user code, and then you don't need to enter the user code in one of the next steps: + +.. code-block:: text + + Visit https://oidc.example.net/device?user_code=DTNY-KLNX to authenticate. + +You should now visit this URL in your browser of choice. +Usually, it is intentionally a short URL to make it feasible to type it +instead of copy-pasting it (e.g. on another device). + +Authenticate with the OpenID Connect provider and, if requested, enter the user code +shown in the message. +When the URL already contains the user code, the page won't ask for this code. + +Meanwhile, the openEO Python Client Library is actively polling the OpenID Connect +provider and when you successfully complete the authentication, +it will receive the necessary tokens for authenticated communication +with the back-end and print: + +.. code-block:: text + + Authorized successfully. + +In case of authentication failure, the openEO Python Client Library +will stop polling at some point and raise an exception. + + + + +.. _authenticate_oidc_refresh_token: + +OIDC Authentication: Refresh Token Flow +======================================== + +When OpenID Connect authentication completes successfully, +the openID Python library receives an access token +to be used when doing authenticated calls to the back-end. +The access token usually has a short lifetime to reduce +the security risk when it would be stolen or intercepted. +The openID Python library also receives a *refresh token* +that can be used, through the Refresh Token flow, +to easily request a new access token, +without having to re-authenticate, +which makes it useful for **non-interactive uses cases**. + + +However, as it needs an existing refresh token, +the Refresh Token Flow requires +**first to authenticate with one of the other flows** +(but in practice this should not be done very often +because refresh tokens usually have a relatively long lifetime). +When doing the initial authentication, +you have to explicitly enable storage of the refresh token, +through the ``store_refresh_token`` argument, e.g.: + +.. code-block:: python + + connection.authenticate_oidc_device( + ... + store_refresh_token=True + + + +The refresh token will be stored in file in private file +in your home directory and will be used automatically +when authenticating with the Refresh Token Flow, +using :py:meth:`~openeo.rest.connection.Connection.authenticate_oidc_refresh_token`: + +.. code-block:: python + + connection.authenticate_oidc_refresh_token() + +You can also bootstrap the refresh token file +as described in :ref:`oidc_auth_get_refresh_token` + + + +.. _authenticate_oidc_client_credentials: + +OIDC Authentication: Client Credentials Flow +============================================= + +The OIDC Client Credentials flow does not involve interactive authentication (e.g. through a web browser), +which makes it a useful option for **non-interactive use cases**. + +.. important:: + This method requires a custom **OIDC client id** and **client secret**. + It is out of scope of this general documentation to explain + how to obtain these as it depends on the openEO back-end you are using + and the OIDC provider that is in play. + + Also, your openEO back-end might not allow it, because technically + you are authenticating a *client* instead of a *user*. + + Consult the support of the openEO back-end you want to use for more information. + +In its most simple form, given your client id and secret, +you can authenticate with +:py:meth:`~openeo.rest.connection.Connection.authenticate_oidc_client_credentials` +as follows: + +.. code-block:: python + + connection.authenticate_oidc_client_credentials( + client_id=client_id, + client_secret=client_secret, + ) + +You might also have to pass a custom provider id (argument ``provider_id``) +if your OIDC client is associated with an OIDC provider that is different from the default provider. + +.. caution:: + Make sure to *keep the client secret a secret* and avoid putting it directly in your source code + or, worse, committing it to a version control system. + Instead, fetch the secret from a protected source (e.g. a protected file, a database for sensitive data, ...) + or from environment variables. + +.. _authenticate_oidc_client_credentials_env_vars: + +OIDC Client Credentials Using Environment Variables +---------------------------------------------------- + +Since version 0.18.0, the openEO Python Client Library has built-in support to get the client id, +secret (and provider id) from environment variables +``OPENEO_AUTH_CLIENT_ID``, ``OPENEO_AUTH_CLIENT_SECRET`` and ``OPENEO_AUTH_PROVIDER_ID`` respectively. +Just call :py:meth:`~openeo.rest.connection.Connection.authenticate_oidc_client_credentials` +without arguments. + +Usage example assuming a Linux (Bash) shell context: + +.. code-block:: console + + $ export OPENEO_AUTH_CLIENT_ID="my-client-id" + $ export OPENEO_AUTH_CLIENT_SECRET="Cl13n7S3cr3t!?123" + $ export OPENEO_AUTH_PROVIDER_ID="oidcprovider" + $ python + >>> import openeo + >>> connection = openeo.connect("openeo.example.com") + >>> connection.authenticate_oidc_client_credentials() + + + + +.. _authenticate_oidc_automatic: + +OIDC Authentication: Dynamic Method Selection +============================================== + +The sections above discuss various authentication options, like +the :ref:`device code flow `, +:ref:`refresh tokens ` and +:ref:`client credentials flow `, +but often you want to *dynamically* switch between these depending on the situation: +e.g. use a refresh token if you have an active one, and fallback on the device code flow otherwise. +Or you want to be able to run the same code in an interactive environment and automated in an unattended manner, +without having to switch authentication methods explicitly in code. + +That is what :py:meth:`Connection.authenticate_oidc() ` is for: + +.. code-block:: python + + connection.authenticate_oidc() # is all you need + +In a basic situation (without any particular environment variables set as discussed further), +this method will first try to authenticate with refresh tokens (if any) +and fall back on the device code flow otherwise. +Ideally, when valid refresh tokens are available, this works without interaction, +but occasionally, when the refresh tokens expire, one has to do the interactive device code flow. + +Since version 0.18.0, the openEO Python Client Library also allows to trigger the +:ref:`client credentials flow ` +from :py:meth:`~openeo.rest.connection.Connection.authenticate_oidc` +by setting environment variable ``OPENEO_AUTH_METHOD`` +and the other :ref:`client credentials environment variables `. +For example: + +.. code-block:: shell + + $ export OPENEO_AUTH_METHOD="client_credentials" + $ export OPENEO_AUTH_CLIENT_ID="my-client-id" + $ export OPENEO_AUTH_CLIENT_SECRET="Cl13n7S3cr3t!?123" + $ export OPENEO_AUTH_PROVIDER_ID="oidcprovider" + $ python + >>> import openeo + >>> connection = openeo.connect("openeo.example.com") + >>> connection.authenticate_oidc() + + + + + + + + + +.. _auth_configuration_files: + +Auth config files and ``openeo-auth`` helper tool +==================================================== + +The openEO Python Client Library provides some features and tools +that ease the usability and security challenges +that come with authentication (especially in case of OpenID Connect). + +Note that the code examples above contain quite some **passwords and other secrets** +that should be kept safe from prying eyes. +It is bad practice to define these kind of secrets directly +in your scripts and source code because that makes it quite hard +to responsibly share or reuse your code. +Even worse is storing these secrets in your version control system, +where it might be near impossible to remove them again. +A better solution is to keep **secrets in separate configuration or cache files**, +outside of your normal source code tree +(to avoid committing them accidentally). + + +The openEO Python Client Library supports config files to store: +user names, passwords, client IDs, client secrets, etc, +so you don't have to specify them always in your scripts and applications. + +The openEO Python Client Library (when installed properly) +provides a command line tool ``openeo-auth`` to bootstrap and manage +these configs and secrets. +It is a command line tool that provides various "subcommands" +and has built-in help:: + + $ openeo-auth -h + usage: openeo-auth [-h] [--verbose] + {paths,config-dump,token-dump,add-basic,add-oidc,oidc-auth} + ... + + Tool to manage openEO related authentication and configuration. + + optional arguments: + -h, --help show this help message and exit + + Subcommands: + {paths,config-dump,token-dump,add-basic,add-oidc,oidc-auth} + paths Show paths to config/token files. + config-dump Dump config file. + ... + + + +For example, to see the expected paths of the config files:: + + $ openeo-auth paths + openEO auth config: /home/john/.config/openeo-python-client/auth-config.json (perms: 0o600, size: 1414B) + openEO OpenID Connect refresh token store: /home/john/.local/share/openeo-python-client/refresh-tokens.json (perms: 0o600, size: 846B) + + +With the ``config-dump`` and ``token-dump`` subcommands you can dump +the current configuration and stored refresh tokens, e.g.:: + + $ openeo-auth config-dump + ### /home/john/.config/openeo-python-client/auth-config.json ############### + { + "backends": { + "https://openeo.example.com": { + "basic": { + "username": "john", + "password": "", + "date": "2020-07-24T13:40:50Z" + ... + +The sensitive information (like passwords) are redacted by default. + + + +Basic HTTP Auth config +----------------------- + +With the ``add-basic`` subcommand you can add Basic HTTP Auth credentials +for a given back-end to the config. +It will interactively ask for username and password and +try if these credentials work:: + + $ openeo-auth add-basic https://openeo.example.com/ + Enter username and press enter: john + Enter password and press enter: + Trying to authenticate with 'https://openeo.example.com' + Successfully authenticated 'john' + Saved credentials to '/home/john/.config/openeo-python-client/auth-config.json' + +Now you can authenticate in your application without having to +specify username and password explicitly:: + + connection.authenticate_basic() + +OpenID Connect configs +----------------------- + +Likewise, with the ``add-oidc`` subcommand you can add OpenID Connect +credentials to the config:: + + $ openeo-auth add-oidc https://openeo.example.com/ + Using provider ID 'example' (issuer 'https://oidc.example.net/') + Enter client_id and press enter: client-d7393fba + Enter client_secret and press enter: + Saved client information to '/home/john/.config/openeo-python-client/auth-config.json' + +Now you can user OpenID Connect based authentication in your application +without having to specify the client ID and client secret explicitly, +like one of these calls:: + + connection.authenticate_oidc_authorization_code() + connection.authenticate_oidc_client_credentials() + connection.authenticate_oidc_resource_owner_password_credentials(username=username, password=password) + connection.authenticate_oidc_device() + connection.authenticate_oidc_refresh_token() + +Note that you still have to add additional options as required, like +``provider_id``, ``server_address``, ``store_refresh_token``, etc. + + +.. _oidc_auth_get_refresh_token: + +OpenID Connect refresh tokens +````````````````````````````` + +There is also a ``oidc-auth`` subcommand to execute an OpenID Connect +authentication flow and store the resulting refresh token. +This is intended to for bootstrapping the environment or system +on which you want to run openEO scripts or applications that use +the Refresh Token Flow for authentication. +For example:: + + $ openeo-auth oidc-auth https://openeo.example.com + Using config '/home/john/.config/openeo-python-client/auth-config.json'. + Starting OpenID Connect device flow. + To authenticate: visit https://oidc.example.net/device and enter the user code 'Q7ZNsy'. + Authorized successfully. + The OpenID Connect device flow was successful. + Stored refresh token in '/home/john/.local/share/openeo-python-client/refresh-tokens.json' + + + +.. _default_url_and_auto_auth: + +Default openEO back-end URL and auto-authentication +===================================================== + +.. versionadded:: 0.10.0 + + +If you often use the same openEO back-end URL and authentication scheme, +it can be handy to put these in a configuration file as discussed at :ref:`configuration_files`. + +.. note:: + Note that :ref:`these general configuration files ` are different + from the auth config files discussed earlier under :ref:`auth_configuration_files`. + The latter are for storing authentication related secrets + and are mostly managed automatically (e.g. by the ``oidc-auth`` helper tool). + The former are not for storing secrets and are usually edited manually. + +For example, to define a default back-end and automatically use OpenID Connect authentication +add these configuration options to the :ref:`desired configuration file `:: + + [Connection] + default_backend = openeo.cloud + default_backend.auto_authenticate = oidc + +Getting an authenticated connection is now as simple as:: + + >>> import openeo + >>> connection = openeo.connect() + Loaded openEO client config from openeo-client-config.ini + Using default back-end URL 'openeo.cloud' (from config) + Doing auto-authentication 'oidc' (from config) + Authenticated using refresh token. + + +Authentication for long-running applications and non-interactive contexts +=========================================================================== + +With OpenID Connect authentication, the *access token* +(which is used in the authentication headers) +is typically short-lived (e.g. couple of minutes or hours). +This practically means that an authenticated connection could expire and become unusable +before a **long-running script or application** finishes its whole workflow. +Luckily, OpenID Connect also includes usage of *refresh tokens*, +which have a much longer expiry and allow request a new access token +to re-authenticate the connection. +Since version 0.10.1, the openEO Python Client Library will automatically +attempt to re-authenticate a connection when access token expiry is detected +and valid refresh tokens are available. + +Likewise, refresh tokens can also be used for authentication in cases +where a script or application is **run automatically in the background on regular basis** (daily, weekly, ...). +If there is a non-expired refresh token available, the script can authenticate +without user interaction. + +Guidelines and tips +-------------------- + +Some guidelines to get long-term and non-interactive authentication working for your use case: + +- If you run a workflow periodically, but the interval between runs + is larger than the expiry time of the refresh token + (e.g. a monthly job, while the refresh token expires after, say, 10 days), + you could consider setting up a *custom OIDC client* with better suited + refresh token timeout. + The practical details of this heavily depend on the OIDC Identity Provider + in play and are out of scope of this discussion. +- Obtaining a refresh token requires manual/interactive authentication, + but once it is stored on the necessary machine(s) + in the refresh token store as discussed in :ref:`auth_configuration_files`, + no further manual interaction should be necessary + during the lifetime of the refresh token. + To do so, use one of the following methods: + + - Use the ``openeo-auth oidc-auth`` cli tool, for example to authenticate + for openeo back-end openeo.example.com:: + + $ openeo-auth oidc-auth openeo.example.com + ... + Stored refresh token in '/home/john/.local/share/openeo-python-client/refresh-tokens.json' + + + - Use a Python snippet to authenticate and store the refresh token:: + + import openeo + connection = openeo.connect("openeo.example.com") + connection.authenticate_oidc_device(store_refresh_token=True) + + + To verify that (and where) the refresh token is stored, use ``openeo-auth token-dump``:: + + $ openeo-auth token-dump + ### /home/john/.local/share/openeo-python-client/refresh-tokens.json ####### + { + "https://oidc.example.net": { + "default-client": { + "date": "2022-05-11T13:13:20Z", + "refresh_token": "" + }, + ... + + + +Best Practices and Troubleshooting Tips +======================================== + +.. warning:: + + Handle (OIDC) access and refresh tokens like secret, personal passwords. + **Never share your access or refresh tokens** with other people, + publicly, or for user support reasons. + + +Clear the refresh token file +---------------------------- + +When you have authentication or permission issues and you suspect +that your (locally cached) refresh tokens are the culprit: +remove your refresh token file in one of the following ways: + +- Locate the file with the ``openeo-auth`` command line tool:: + + $ openeo-auth paths + ... + openEO OpenID Connect refresh token store: /home/john/.local/share/openeo-python-client/refresh-tokens.json (perms: 0o600, size: 846B) + + and remove it. + Or, if you know what you are doing: remove the desired section from this JSON file. + +- Remove it directly with the ``token-clear`` subcommand of the ``openeo-auth`` command line tool:: + + $ openeo-auth token-clear + +- Remove it with this Python snippet:: + + from openeo.rest.auth.config import RefreshTokenStore + RefreshTokenStore().remove() diff --git a/_sources/basics.rst.txt b/_sources/basics.rst.txt new file mode 100644 index 000000000..64674553d --- /dev/null +++ b/_sources/basics.rst.txt @@ -0,0 +1,459 @@ +================ +Getting Started +================ + + +Connect to an openEO back-end +============================== + +First, establish a connection to an openEO back-end, using its connection URL. +For example the VITO/Terrascope backend: + +.. code-block:: python + + import openeo + + connection = openeo.connect("openeo.vito.be") + +The resulting :py:class:`~openeo.rest.connection.Connection` object is your central gateway to + +- list data collections, available processes, file formats and other capabilities of the back-end +- start building your openEO algorithm from the desired data on the back-end +- execute and monitor (batch) jobs on the back-end +- etc. + +.. seealso:: + + Use the `openEO Hub `_ to explore different back-end options + and their capabilities in a web-based way. + + +Collection discovery +===================== + +The Earth observation data (the input of your openEO jobs) is organised in +`so-called collections `_, +e.g. fundamental satellite collections like "Sentinel 1" or "Sentinel 2", +or preprocessed collections like "NDVI". + +You can programmatically list the collections that are available on a back-end +and their metadata using methods on the `connection` object we just created +(like :py:meth:`~openeo.rest.connection.Connection.list_collection_ids` +or :py:meth:`~openeo.rest.connection.Connection.describe_collection` + +.. code-block:: pycon + + >>> # Get all collection ids + >>> connection.list_collection_ids() + ['SENTINEL1_GRD', 'SENTINEL2_L2A', ... + + >>> # Get metadata of a single collection + >>> connection.describe_collection("SENTINEL2_L2A") + {'id': 'SENTINEL2_L2A', 'title': 'Sentinel-2 top of canopy ...', 'stac_version': '0.9.0', ... + +Congrats, you now just did your first real openEO queries to the openEO back-end +using the openEO Python client library. + +.. tip:: + The openEO Python client library comes with **Jupyter (notebook) integration** in a couple of places. + For example, put ``connection.describe_collection("SENTINEL2_L2A")`` (without ``print()``) + as last statement in a notebook cell + and you'll get a nice graphical rendering of the collection metadata. + +.. seealso:: + + Find out more about data discovery, loading and filtering at :ref:`data_access_chapter`. + + +Authentication +============== + +In the code snippets above we did not need to log in as a user +since we just queried publicly available back-end information. +However, to run non-trivial processing queries one has to authenticate +so that permissions, resource usage, etc. can be managed properly. + +To handle authentication, openEO leverages `OpenID Connect (OIDC) `_. +It offers some interesting features (e.g. a user can securely reuse an existing account), +but is a fairly complex topic, discussed in more depth at :ref:`authentication_chapter`. + +The openEO Python client library tries to make authentication as streamlined as possible. +In most cases for example, the following snippet is enough to obtain an authenticated connection: + +.. code-block:: python + + import openeo + + connection = openeo.connect("openeo.vito.be").authenticate_oidc() + +This statement will automatically reuse a previously authenticated session, when available. +Otherwise, e.g. the first time you do this, some user interaction is required +and it will print a web link and a short *user code*, for example: + +.. code-block:: + + To authenticate: visit https://aai.egi.eu/auth/realms/egi/device and enter the user code 'SLUO-BMUD'. + +Visit this web page in a browser, log in there with an existing account and enter the user code. +If everything goes well, the ``connection`` object in the script will be authenticated +and the back-end will be able to identify you in subsequent requests. + + + +.. _basic_example_evi_map_and_timeseries: + +Example use case: EVI map and timeseries +========================================= + +A common task in earth observation is to apply a formula to a number of spectral bands +in order to compute an 'index', such as NDVI, NDWI, EVI, ... +In this tutorial we'll go through a couple of steps to extract +EVI (enhanced vegetation index) values and timeseries, +and discuss some openEO concepts along the way. + + +Loading an initial data cube +============================= + +For calculating the EVI, we need the reflectance of the +red, blue and (near) infrared spectral components. +These spectral bands are part of the well-known Sentinel-2 data set +and is available on the current back-end under collection id ``SENTINEL2_L2A``. +We load an initial small spatio-temporal slice (a data cube) as follows: + +.. code-block:: python + + sentinel2_cube = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19}, + temporal_extent = ["2021-02-01", "2021-04-30"], + bands=["B02", "B04", "B08"] + ) + +Note how we specify a the region of interest, a time range and a set of bands to load. + +.. important:: + By filtering as early as possible (directly in :py:meth:`~openeo.rest.connection.Connection.load_collection` in this case), + we make sure the back-end only loads the data we are interested in + for better performance and keeping the processing costs low. + +.. seealso:: + See the chapter :ref:`data_access_chapter` for more details on data discovery, + general data loading (:ref:`data-loading-and-filtering`) and filtering + (e.g. :ref:`filtering-on-temporal-extent-section`). + + +The :py:meth:`~openeo.rest.connection.Connection.load_collection` method on the connection +object created a :py:class:`~openeo.rest.datacube.DataCube` object (variable ``sentinel2_cube``). +This :py:class:`~openeo.rest.datacube.DataCube` class of the openEO Python Client Library +provides loads of methods corresponding to various openEO processes, +e.g. for masking, filtering, aggregation, spectral index calculation, data fusion, etc. +In the next steps we will illustrate a couple of these. + + +.. important:: + It is important to highlight that we *did not load any real EO data* yet. + Instead we just created an abstract *client-side reference*, + encapsulating the collection id, the spatial extent, the temporal extent, etc. + The actual data loading will only happen at the back-end + once we explicitly trigger the execution of the data processing pipeline we are building. + + + +Band math +========= + +From this data cube, we can now select the individual bands +with the :py:meth:`DataCube.band() ` method +and rescale the digital number values to physical reflectances: + +.. code-block:: python + + blue = sentinel2_cube.band("B02") * 0.0001 + red = sentinel2_cube.band("B04") * 0.0001 + nir = sentinel2_cube.band("B08") * 0.0001 + +We now want to compute the enhanced vegetation index +and can do that directly with these band variables: + +.. code-block:: python + + evi_cube = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0) + +.. important:: + As noted before: while this looks like an actual calculation, + there is *no real data processing going on here*. + The ``evi_cube`` object at this point is just an abstract representation + of our algorithm under construction. + The mathematical operators we used here are *syntactic sugar* + for expressing this part of the algorithm in a very compact way. + + As an illustration of this, let's have peek at the *JSON representation* + of our algorithm so far, the so-called *openEO process graph*: + + .. code-block:: text + + >>> print(evi_cube.to_json(indent=None)) + {"process_graph": {"loadcollection1": {"process_id": "load_collection", ... + ... "id": "SENTINEL2_L2A", "spatial_extent": {"west": 5.15, "south": ... + ... "multiply1": { ... "y": 0.0001}}, ... + ... "multiply3": { ... {"x": 2.5, "y": {"from_node": "subtract1"}}} ... + ... + + Note how the ``load_collection`` arguments, rescaling and EVI calculation aspects + can be deciphered from this. + Rest assured, as user you normally you don't have to worry too much + about these process graph details, + the openEO Python Client library handles this behind the scenes for you. + + +Download (synchronously) +======================== + +Let's download this as a GeoTIFF file. +Because GeoTIFF does not support a temporal dimension, +we first eliminate it by taking the temporal maximum value for each pixel: + +.. code-block:: python + + evi_composite = evi_cube.max_time() + +.. note:: + + This :py:meth:`~openeo.rest.datacube.DataCube.max_time()` is not an official openEO process + but one of the many *convenience methods* in the openEO Python Client Library + to simplify common processing patterns. + It implements a ``reduce`` operation along the temporal dimension + with a ``max`` reducer/aggregator. + +Now we can download this to a local file: + +.. code-block:: python + + evi_composite.download("evi-composite.tiff") + +This download command **triggers the actual processing** on the back-end: +it sends the process graph to the back-end and waits for the result. +It is a *synchronous operation* (the :py:meth:`~openeo.rest.datacube.DataCube.download()` call +blocks until the result is fully downloaded) and because we work on a small spatio-temporal extent, +this should only take a couple of seconds. + +If we inspect the downloaded image, we see that the maximum EVI value is heavily impacted +by cloud related artefacts, which makes the result barely usable. +In the next steps we will address cloud masking. + +.. image:: _static/images/basics/evi-composite.png + + +Batch Jobs (asynchronous execution) +=================================== + +Synchronous downloads are handy for quick experimentation on small data cubes, +but if you start processing larger data cubes, you can easily +hit *computation time limits* or other constraints. +For these larger tasks, it is recommended to work with **batch jobs**, +which allow you to work asynchronously: +after you start your job, you can disconnect (stop your script or even close your computer) +and then minutes/hours later you can reconnect to check the batch job status and download results. +The openEO Python Client Library also provides helpers to keep track of a running batch job +and show a progress report. + +.. seealso:: + + See :ref:`batch-jobs-chapter` for more details. + + +Applying a cloud mask +========================= + +As mentioned above, we need to filter out cloud pixels to make the result more usable. +It is very common for earth observation data to have separate masking layers that for instance indicate +whether a pixel is covered by a (type of) cloud or not. +For Sentinel-2, one such layer is the "scene classification" layer generated by the Sen2Cor algorithm. +In this example, we will use this layer to mask out unwanted data. + +First, we load a new ``SENTINEL2_L2A`` based data cube with this specific ``SCL`` band as single band: + +.. code-block:: python + + s2_scl = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19}, + temporal_extent = ["2021-02-01", "2021-04-30"], + bands=["SCL"] + ) + +Now we can use the compact "band math" feature again to build a +binary mask with a simple comparison operation: + +.. code-block:: python + + # Select the "SCL" band from the data cube + scl_band = s2_scl.band("SCL") + # Build mask to mask out everything but class 4 (vegetation) + mask = (scl_band != 4) + +Before we can apply this mask to the EVI cube we have to resample it, +as the "SCL" layer has a "ground sample distance" of 20 meter, +while it is 10 meter for the "B02", "B04" and "B08" bands. +We can easily do the resampling by referring directly to the EVI cube. + +.. code-block:: python + + mask_resampled = mask.resample_cube_spatial(evi_cube) + + # Apply the mask to the `evi_cube` + evi_cube_masked = evi_cube.mask(mask_resampled) + + +We can now download this as a GeoTIFF, again after taking the temporal maximum: + +.. code-block:: python + + evi_cube_masked.max_time().download("evi-masked-composite.tiff") + +Now, the EVI map is a lot more valuable, as the non-vegetation locations +and observations are filtered out: + +.. image:: _static/images/basics/evi-masked-composite.png + + +Aggregated EVI timeseries +=========================== + +A common type of analysis is aggregating pixel values over one or more regions of interest +(also known as "zonal statistics) and tracking this aggregation over a period of time as a timeseries. +Let's extract the EVI timeseries for these two regions: + +.. code-block:: python + + features = {"type": "FeatureCollection", "features": [ + { + "type": "Feature", "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[ + [5.1417, 51.1785], [5.1414, 51.1772], [5.1444, 51.1768], [5.1443, 51.179], [5.1417, 51.1785] + ]]} + }, + { + "type": "Feature", "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[ + [5.156, 51.1892], [5.155, 51.1855], [5.163, 51.1855], [5.163, 51.1891], [5.156, 51.1892] + ]]} + } + ]} + + +.. note:: + + To have a self-containing example we define the geometries here as an inline GeoJSON-style dictionary. + In a real use case, your geometry will probably come from a local file or remote URL. + The openEO Python Client Library supports alternative ways of specifying the geometry + in methods like :py:meth:`~openeo.rest.datacube.DataCube.aggregate_spatial()`, e.g. + as Shapely geometry objects. + + +Building on the experience from previous sections, we first build a masked EVI cube +(covering a longer time window than before): + +.. code-block:: python + + # Load raw collection data + sentinel2_cube = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19}, + temporal_extent = ["2020-01-01", "2021-12-31"], + bands=["B02", "B04", "B08", "SCL"], + ) + + # Extract spectral bands and calculate EVI with the "band math" feature + blue = sentinel2_cube.band("B02") * 0.0001 + red = sentinel2_cube.band("B04") * 0.0001 + nir = sentinel2_cube.band("B08") * 0.0001 + evi = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0) + + # Use the scene classification layer to mask out non-vegetation pixels + scl = sentinel2_cube.band("SCL") + evi_masked = evi.mask(scl != 4) + +Now we use the :py:meth:`~openeo.rest.datacube.DataCube.aggregate_spatial()` method +to do spatial aggregation over the geometries we defined earlier. +Note how we can specify the aggregation function ``"mean"`` as a simple string for the ``reducer`` argument. + +.. code-block:: python + + evi_aggregation = evi_masked.aggregate_spatial( + geometries=features, + reducer="mean", + ) + +If we download this, we get the timeseries encoded as a JSON structure, other useful formats are CSV and netCDF. + +.. code-block:: python + + evi_aggregation.download("evi-aggregation.json") + +.. warning:: + + Technically, the output of the openEO process ``aggregate_spatial`` + is a so-called "vector cube". + At the time of this writing, the specification of this openEO concept + is not fully fleshed out yet in the openEO API. + openEO back-ends and clients to provide best-effort support for it, + but bear in mind that some details are subject to change. + +The openEO Python Client Library provides helper functions +to convert the downloaded JSON data to a pandas dataframe, +which we massage a bit more: + +.. code-block:: python + + import json + import pandas as pd + from openeo.rest.conversions import timeseries_json_to_pandas + + import json + with open("evi-aggregation.json") as f: + data = json.load(f) + + df = timeseries_json_to_pandas(data) + df.index = pd.to_datetime(df.index) + df = df.dropna() + df.columns = ("Field A", "Field B") + +This gives us finally our EVI timeseries dataframe: + +.. code-block:: pycon + + >>> df + Field A Field B + date + 2020-01-06 00:00:00+00:00 0.522499 0.300250 + 2020-01-16 00:00:00+00:00 0.529591 0.288079 + 2020-01-18 00:00:00+00:00 0.633011 0.327598 + ... ... ... + + +.. image:: _static/images/basics/evi-timeseries.png + + +Computing multiple statistics +============================= + +.. warning:: + This is an experimental feature of the GeoPySpark openEO back-end, + it may not be supported by other back-ends, + and is subject to change. + See `Open-EO/openeo-geopyspark-driver#726 `_ for further discussion, + +The same method also allows the computation of multiple statistics at once. This does rely +on 'callbacks' to construct a result with multiple statistics. +The use of such more complex processes is further explained in :ref:`callbackfunctions`. + +.. code-block:: python + + from openeo.processes import array_create, mean, sd, median, count + + evi_aggregation = evi_masked.aggregate_spatial( + geometries=features, + reducer=lambda x: array_create([mean(x), sd(x), median(x), count(x)]), + ) diff --git a/_sources/batch_jobs.rst.txt b/_sources/batch_jobs.rst.txt new file mode 100644 index 000000000..85b9953f2 --- /dev/null +++ b/_sources/batch_jobs.rst.txt @@ -0,0 +1,415 @@ + +.. index:: + single: batch job + see: job; batch job + +.. _batch-jobs-chapter: + +============ +Batch Jobs +============ + +Most of the simple, basic openEO usage examples show **synchronous** downloading of results: +you submit a process graph with a (HTTP POST) request and receive the result +as direct response of that same request. +This only works properly if the processing doesn't take too long (order of seconds, or a couple of minutes at most). + +For the heavier work (larger regions of interest, larger time series, more intensive processing, ...) +you have to use **batch jobs**, which are supported in the openEO API through separate HTTP requests, corresponding to these steps: + +- you create a job (providing a process graph and some other metadata like title, description, ...) +- you start the job +- you wait for the job to finish, periodically polling its status +- when the job finished successfully: get the listing of result assets +- you download the result assets (or use them in an other way) + +.. tip:: + + This documentation mainly discusses how to **programmatically** + create and interact with batch job using the openEO Python client library. + The openEO API however does not enforce usage of the same tool + for each step in the batch job life cycle. + + For example: if you prefer a graphical, web-based **interactive environment** + to manage and monitor your batch jobs, + feel free to *switch to an openEO web editor* + like `editor.openeo.org `_ + or `editor.openeo.cloud `_ + at any time. + After logging in with the same account you use in your Python scripts, + you should see your batch jobs listed under the "Data Processing" tab: + + .. image:: _static/images/batchjobs-webeditor-listing.png + + With the "action" buttons on the right, you can for example + inspect batch job details, start/stop/delete jobs, + download their results, get batch job logs, etc. + + + +.. index:: batch job; create + +Create a batch job +=================== + +In the openEO Python Client Library, if you have a (raster) data cube, you can easily +create a batch job with the :py:meth:`DataCube.create_job() ` method. +It's important to specify in what *format* the result should be stored, +which can be done with an explicit :py:meth:`DataCube.save_result() ` call before creating the job: + +.. code-block:: python + + cube = connection.load_collection(...) + ... + # Store raster data as GeoTIFF files + cube = cube.save_result(format="GTiff") + job = cube.create_job() + +or directly in :py:meth:`job.create_job() `: + +.. code-block:: python + + cube = connection.load_collection(...) + ... + job = cube.create_job(out_format="GTiff) + +While not necessary, it is also recommended to give your batch job a descriptive title +so it's easier to identify in your job listing, e.g.: + +.. code-block:: python + + job = cube.create_job(title="NDVI timeseries 2022") + + + +.. index:: batch job; object + +Batch job object +================= + +The ``job`` object returned by :py:meth:`~openeo.rest.datacube.DataCube.create_job()` +is a :py:class:`~openeo.rest.job.BatchJob` object. +It is basically a *client-side reference* to a batch job that *exists on the back-end* +and allows to interact with that batch job +(see the :py:class:`~openeo.rest.job.BatchJob` API docs for +available methods). + + +.. note:: + The :py:class:`~openeo.rest.job.BatchJob` class originally had + the more cryptic name :py:class:`~openeo.rest.job.RESTJob`, + which is still available as legacy alias, + but :py:class:`~openeo.rest.job.BatchJob` is (available and) recommended since version 0.11.0. + + +A batch job on a back-end is fully identified by its +:py:data:`~openeo.rest.job.BatchJob.job_id`: + +.. code-block:: pycon + + >>> job.job_id + 'd5b8b8f2-74ce-4c2e-b06d-bff6f9b14b8d' + + +Reconnecting to a batch job +---------------------------- + +Depending on your situation or use case: +make sure to properly take note of the batch job id. +It allows you to "reconnect" to your job on the back-end, +even if it was created at another time, +by another script/notebook or even with another openEO client. + +Given a back-end connection and the batch job id, +use :py:meth:`Connection.job() ` +to create a :py:class:`~openeo.rest.job.BatchJob` object for an existing batch job: + +.. code-block:: python + + job_id = "5d806224-fe79-4a54-be04-90757893795b" + job = connection.job(job_id) + + +Jupyter integration +-------------------- + +:py:class:`~openeo.rest.job.BatchJob` objects have basic Jupyter notebook integration. +Put your :py:class:`~openeo.rest.job.BatchJob` object as last statement +in a notebook cell and you get an overview of your batch jobs, +including job id, status, title and even process graph visualization: + +.. image:: _static/images/batchjobs-jupyter-created.png + + +.. index:: batch job; listing + +List your batch jobs +======================== + +You can list your batch jobs on the back-end with +:py:meth:`Connection.list_jobs() `, which returns a list of job metadata: + +.. code-block:: pycon + + >>> connection.list_jobs() + [{'title': 'NDVI timeseries 2022', 'status': 'created', 'id': 'd5b8b8f2-74ce-4c2e-b06d-bff6f9b14b8d', 'created': '2022-06-08T08:58:11Z'}, + {'title': 'NDVI timeseries 2021', 'status': 'finished', 'id': '4e720e70-88bd-40bc-92db-a366985ebd67', 'created': '2022-06-04T14:46:06Z'}, + ... + +The listing returned by :py:meth:`Connection.list_jobs() ` +has Jupyter notebook integration: + +.. image:: _static/images/batchjobs-jupyter-listing.png + + +.. index:: batch job; start + +Run a batch job +================= + +Starting a batch job is pretty straightforward with the +:py:meth:`~openeo.rest.job.BatchJob.start()` method: + +.. code-block:: python + + job.start() + +If this didn't raise any errors or exceptions your job +should now have started (status "running") +or be queued for processing (status "queued"). + + + +.. index:: batch job; status + +Wait for a batch job to finish +-------------------------------- + +A batch job typically takes some time to finish, +and you can check its status with the :py:meth:`~openeo.rest.job.BatchJob.status()` method: + +.. code-block:: pycon + + >>> job.status() + "running" + +The possible batch job status values, defined by the openEO API, are +"created", "queued", "running", "canceled", "finished" and "error". + +Usually, you can only reliably get results from your job, +as discussed in :ref:`batch_job_results`, +when it reaches status "finished". + + + +.. index:: batch job; polling loop + +Create, start and wait in one go +---------------------------------- + +You could, depending on your situation, manually check your job's status periodically +or set up a **polling loop** system to keep an eye on your job. +The openEO Python client library also provides helpers to do that for you. + +Working from an existing :py:class:`~openeo.rest.job.BatchJob` instance + + If you have a batch job that is already created as shown above, you can use + the :py:meth:`job.start_and_wait() ` method + to start it and periodically poll its status until it reaches status "finished" (or fails with status "error"). + Along the way it will print some progress messages. + + .. code-block:: pycon + + >>> job.start_and_wait() + 0:00:00 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': send 'start' + 0:00:36 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': queued (progress N/A) + 0:01:35 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': queued (progress N/A) + 0:02:19 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': running (progress N/A) + 0:02:50 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': running (progress N/A) + 0:03:28 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': finished (progress N/A) + + +Working from a :py:class:`~openeo.rest.datacube.DataCube` instance + + If you didn't create the batch job yet from a given :py:class:`~openeo.rest.datacube.DataCube` + you can do the job creation, starting and waiting in one go + with :py:meth:`cube.execute_batch() `: + + .. code-block:: pycon + + >>> job = cube.execute_batch() + 0:00:00 Job 'f9f4e3d3-bc13-441b-b76a-b7bfd3b59669': send 'start' + 0:00:23 Job 'f9f4e3d3-bc13-441b-b76a-b7bfd3b59669': queued (progress N/A) + ... + + Note that :py:meth:`cube.execute_batch() ` + returns a :py:class:`~openeo.rest.job.BatchJob` instance pointing to + the newly created batch job. + + +.. tip:: + + You can fine-tune the details of the polling loop (the poll frequency, + how the progress is printed, ...). + See :py:meth:`job.start_and_wait() ` + or :py:meth:`cube.execute_batch() ` + for more information. + + +.. index:: batch job; logs + + +.. _batch-job-logs: + +Batch job logs +=============== + +Batch jobs in openEO have **logs** to help with *monitoring and debugging* batch jobs. +The back-end typically uses this to dump information during data processing +that may be relevant for the user (e.g. warnings, resource stats, ...). +Moreover, openEO processes like ``inspect`` allow users to log their own information. + +Batch job logs can be fetched with :py:meth:`job.logs() ` + +.. code-block:: pycon + + >>> job.logs() + [{'id': 'log001', 'level': 'info', 'message': 'Job started with 4 workers'}, + {'id': 'log002', 'level': 'debug', 'message': 'Loading 5x3x6 tiles'}, + {'id': 'log003', 'level': 'error', 'message': "Failed to load data cube: corrupt data for tile 'J9A7K2'."}, + ... + +In a Jupyter notebook environment, this also comes with Jupyter integration: + +.. image:: _static/images/batchjobs-jupyter-logs.png + + + +Automatic batch job log printing +--------------------------------- + +When using +:py:meth:`job.start_and_wait() ` +or :py:meth:`cube.execute_batch() ` +to run a batch job and it fails, +the openEO Python client library will automatically +print the batch job logs and instructions to help with further investigation: + +.. code-block:: pycon + + >>> job.start_and_wait() + 0:00:00 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': send 'start' + 0:00:01 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': running (progress N/A) + 0:00:07 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': error (progress N/A) + + Your batch job '68caccff-54ee-470f-abaa-559ed2d4e53c' failed. + Logs can be inspected in an openEO (web) editor + or with `connection.job('68caccff-54ee-470f-abaa-559ed2d4e53c').logs()`. + + Printing logs: + [{'id': 'log001', 'level': 'info', 'message': 'Job started with 4 workers'}, + {'id': 'log002', 'level': 'debug', 'message': 'Loading 5x3x6 tiles'}, + {'id': 'log003', 'level': 'error', 'message': "Failed to load data cube: corrupt data for tile 'J9A7K2'."}] + + + +.. index:: batch job; results + +.. _batch_job_results: + +Download batch job results +========================== + +Once a batch job is finished you can get a handle to the results +(which can be a single file or multiple files) and metadata +with :py:meth:`~openeo.rest.job.BatchJob.get_results`: + +.. code-block:: pycon + + >>> results = job.get_results() + >>> results + + +The result metadata describes the spatio-temporal properties of the result +and is in fact a valid STAC item: + +.. code-block:: pycon + + >>> results.get_metadata() + { + 'bbox': [3.5, 51.0, 3.6, 51.1], + 'geometry': {'coordinates': [[[3.5, 51.0], [3.5, 51.1], [3.6, 51.1], [3.6, 51.0], [3.5, 51.0]]], 'type': 'Polygon'}, + 'assets': { + 'res001.tiff': { + 'href': 'https://openeo.example/download/432f3b3ef3a.tiff', + 'type': 'image/tiff; application=geotiff', + ... + 'res002.tiff': { + ... + + +Download all assets +-------------------- + +In the general case, when you have one or more result files (also called "assets"), +the easiest option to download them is +using :py:meth:`~openeo.rest.job.JobResults.download_files` (plural) +where you just specify a download folder +(otherwise the current working directory will be used by default): + +.. code-block:: python + + results.download_files("data/out") + +The resulting files will be named as they are advertised in the results metadata +(e.g. ``res001.tiff`` and ``res002.tiff`` in case of the metadata example above). + + +Download single asset +--------------------- + +If you know that there is just a single result file, you can also download it directly with +:py:meth:`~openeo.rest.job.JobResults.download_file` (singular) with the desired file name: + +.. code-block:: python + + results.download_file("data/out/result.tiff") + +This will fail however if there are multiple assets in the job result +(like in the metadata example above). +In that case you can still download a single by specifying which one you +want to download with the ``name`` argument: + +.. code-block:: python + + results.download_file("data/out/result.tiff", name="res002.tiff") + + +Fine-grained asset downloads +---------------------------- + +If you need a bit more control over which asset to download and how, +you can iterate over the result assets explicitly +and download these :py:class:`~openeo.rest.job.ResultAsset` instances +with :py:meth:`~openeo.rest.job.ResultAsset.download`, like this: + +.. code-block:: python + + for asset in results.get_assets(): + if asset.metadata["type"].startswith("image/tiff"): + asset.download("data/out/result-v2-" + asset.name) + + +Directly load batch job results +=============================== + +If you want to skip downloading an asset to disk, you can also load it directly. +For example, load a JSON asset with :py:meth:`~openeo.rest.job.ResultAsset.load_json`: + +.. code-block:: pycon + + >>> asset.metadata + {"type": "application/json", "href": "https://openeo.example/download/432f3b3ef3a.json"} + >>> data = asset.load_json() + >>> data + {"2021-02-24T10:59:23Z": [[3, 2, 5], [3, 4, 5]], ....} diff --git a/_sources/best_practices.rst.txt b/_sources/best_practices.rst.txt new file mode 100644 index 000000000..fd43b8f9c --- /dev/null +++ b/_sources/best_practices.rst.txt @@ -0,0 +1,93 @@ + +Best practices, coding style and general tips +=============================================== + +This is a collection of guidelines regarding best practices, +coding style and usage patterns for the openEO Python Client Library. + +It is in the first place an internal recommendation for openEO *developers* +to give documentation, code examples, demo's and tutorials +a *consistent* look and feel, +following common software engineering best practices. +Secondly, the wider audience of openEO *users* is also invited to pick up +a couple of tips and principles to improve their own code and scripts. + + +Background and inspiration +--------------------------- + +While some people consider coding style a personal choice or even irrelevant, +there are various reasons to settle on certain conventions. +Just the fact alone of following conventions +lowers the bar to get faster to the important details in someone else's code. +Apart from taste, there are also technical reasons to pick certain rules +to *streamline the programming workflow*, +not only for humans, +but also supporting tools (e.g. minimize risk on merge conflicts). + +While the Python language already has a strong focus on readability by design, +the Python community is strongly gravitating to even more strict conventions: + +- `pep8 `_: the mother of all Python code style guides +- `black `_: an opinionated code formatting tool + that gets more and more traction in popular, high profile projects. + +This openEO oriented style guide will highlight +and build on these recommendations. + + +General code style recommendations +------------------------------------ + +- Indentation with 4 spaces. +- Avoid star imports (``from module import *``). + While this seems like a quick way to import a bunch of functions/classes, + it makes it very hard for the reader to figure out where things come from. + It can also lead to strange bugs and behavior because it silently overwrites + references you previously imported. + + +Line (length) management +-------------------------- + +While desktop monitors offer plenty of (horizontal) space nowadays, +it is still a common recommendation to *avoid long source code lines*. +Not only are long lines hard to read and understand, +one should also consider that source code might still be viewed +on a small screen or tight viewport, +where scrolling horizontally is annoying or even impossible. +Unnecessarily long lines are also notorious +for not playing well with version control tools and workflows. + +Here are some guidelines on how to split long statements over multiple lines. + +Split long function/method calls directly after the opening parenthesis +and list arguments with a standard 4 space indentation +(not after the first argument with some ad-hoc indentation). +Put the closing parenthesis on its own line. + +.. code-block:: python + + # Avoid this: + s2_fapar = connection.load_collection("TERRASCOPE_S2_FAPAR_V2", + spatial_extent={'west': 16.138916, 'east': 16.524124, 'south': 48.1386, 'north': 48.320647}, + temporal_extent=["2020-05-01", "2020-05-20"]) + + # This is better: + s2_fapar = connection.load_collection( + "TERRASCOPE_S2_FAPAR_V2", + spatial_extent={"west": 16.138916, "east": 16.524124, "south": 48.1386, "north": 48.320647}, + temporal_extent=["2020-05-01", "2020-05-20"], + ) + +.. TODO how to handle chained method calls + + + +Jupyter(lab) tips and tricks +------------------------------- + +- Add a cell with ``openeo.client_version()`` (e.g. just after importing all your libraries) + to keep track of which version of the openeo Python client library you used in your notebook. + +.. TODO how to work with "helper" modules? diff --git a/_sources/changelog.md.txt b/_sources/changelog.md.txt new file mode 100644 index 000000000..66efc0fec --- /dev/null +++ b/_sources/changelog.md.txt @@ -0,0 +1,2 @@ +```{include} ../CHANGELOG.md +``` diff --git a/_sources/configuration.rst.txt b/_sources/configuration.rst.txt new file mode 100644 index 000000000..4cb30d9e0 --- /dev/null +++ b/_sources/configuration.rst.txt @@ -0,0 +1,96 @@ + +=============== +Configuration +=============== + +.. warning:: + Configuration files are an experimental feature + and some details are subject to change. + +.. versionadded:: 0.10.0 + + +.. _configuration_files: + +Configuration files +==================== + +Some functionality of the openEO Python client library can customized +through configuration files. + + +.. note:: + Note that these configuration files are different from the authentication secret/cache files + discussed at :ref:`auth_configuration_files`. + The latter are focussed on storing authentication secrets + and are mostly managed automatically. + The normal configuration files however should not contain secrets, + are usually edited manually, can be placed at various locations + and it is not uncommon to store them in version control where that makes sense. + + +Format +------- + +At the moment, only INI-style configs are supported. +This is a simple configuration format, easy to maintain +and it is supported out of the box in Python (without additional libraries). + +Example (note the use of sections and support for comments):: + + [General] + # Print loaded configuration file and default back-end URLs in interactive mode + verbose = auto + + [Connection] + default_backend = openeo.cloud + + +.. _configuration_file_locations: + +Location +--------- + +The following configuration locations are probed (in this order) for an existing configuration file. The first successful hit will be loaded: + +- the path in environment variable ``OPENEO_CLIENT_CONFIG`` if it is set (filename must end with extension ``.ini``) +- the file ``openeo-client-config.ini`` in the current working directory +- the file ``${OPENEO_CONFIG_HOME}/openeo-client-config.ini`` if the environment variable ``OPENEO_CONFIG_HOME`` is set +- the file ``${XDG_CONFIG_HOME}/openeo-python-client/openeo-client-config.ini`` if environment variable ``XDG_CONFIG_HOME`` is set +- the file ``.openeo-client-config.ini`` in the home folder of the user + + +Configuration options +---------------------- + +.. list-table:: + :widths: 10 10 40 + :header-rows: 1 + + * - Config Section + - Config + - Description and possible values + * - ``General`` + - ``verbose`` + - Verbosity mode when important config values are used: + + ``print``: always ``print()`` info + + ``auto`` (default): only ``print()`` when in an interactive context + + ``off``: don't print info + * - ``Connection`` + - ``default_backend`` + - Default back-end to connect to when :py:func:`openeo.connect()` + is used without explicit back-end URL. + Also see :ref:`default_url_and_auto_auth` + * - ``Connection`` + - ``default_backend.auto_authenticate`` + - Automatically authenticate in :py:func:`openeo.connect()` when using the ``default_backend`` config. Allowed values: + + ``basic`` for basic authentication + + ``oidc`` for OpenID Connect authentication + + ``off`` (default) for no authentication + + Also see :ref:`default_url_and_auto_auth` + * - ``Connection`` + - ``auto_authenticate`` + - Automatically authenticate in :py:func:`openeo.connect()`. + Allowed values: see ``default_backend.auto_authenticate``. + Also see :ref:`default_url_and_auto_auth` diff --git a/_sources/cookbook/ard.rst.txt b/_sources/cookbook/ard.rst.txt new file mode 100644 index 000000000..908e2bb83 --- /dev/null +++ b/_sources/cookbook/ard.rst.txt @@ -0,0 +1,113 @@ +.. _ard: + +============================== +Analysis Ready Data generation +============================== + +For certain use cases, the preprocessed data collections available in the openEO back-ends are not sufficient or simply not +available. For that case, openEO supports a few very common preprocessing scenario: + +- Atmospheric correction of optical data +- SAR backscatter computation + +These processes also offer a number of parameters to customize the processing. There's also variants with a default +parametrization that results in data that is compliant with CEOS CARD4L specifications https://ceos.org/ard/. + +We should note that these operations can be computationally expensive, so certainly affect overall processing time and +cost of your final algorithm. Hence, make sure to make an informed decision when you decide to use these methods. + +Atmospheric correction +---------------------- + +The `atmospheric correction `_ process can apply a chosen +method on raw 'L1C' data. The supported methods and input datasets depend on the back-end, because not every method is +validated or works on any dataset, and different back-ends try to offer a variety of options. This gives you as a user +more options to run and compare different methods, and select the most suitable one for your case. + + +To perform an `atmospheric correction `_, the user has to +load an uncorrected L1C optical dataset. On the resulting datacube, the :func:`~openeo.rest.datacube.DataCube.atmospheric_correction` +method can be invoked. Note that it may not be possible to apply certain processes to the raw input data: preprocessing +algorithms can be tightly coupled with the raw data, making it hard or impossible for the back-end to perform operations +in between loading and correcting the data. + +The CARD4L variant of this process is: :func:`~openeo.rest.datacube.DataCube.ard_surface_reflectance`. This process follows +CEOS specifications, and thus can additional processing steps, like a BRDF correction, that are not yet available as a +separate process. + +Reference implementations +######################### + +This section shows a few working examples for these processes. + +EODC back-end +************* + +EODC (https://openeo.eodc.eu/v1.0) supports ard_surface_reflectance, based on the FORCE toolbox. (https://github.com/davidfrantz/force) + +Geotrellis back-end +******************* + +The geotrellis back-end (https://openeo.vito.be) supports :func:`~openeo.rest.datacube.DataCube.atmospheric_correction` with iCor and SMAC as methods. +The version of iCor only offers basic atmoshperic correction features, without special options for water products: https://remotesensing.vito.be/case/icor +SMAC is implemented based on: https://github.com/olivierhagolle/SMAC +Both methods have been tested with Sentinel-2 as input. The viewing and sun angles need to be selected by the user to make them +available for the algorithm. + +This is an example of applying iCor:: + + l1c = connection.load_collection("SENTINEL2_L1C_SENTINELHUB", + spatial_extent={'west':3.758216409030558,'east':4.087806252,'south':51.291835566,'north':51.3927399}, + temporal_extent=["2017-03-07","2017-03-07"],bands=['B04','B03','B02','B09','B8A','B11','sunAzimuthAngles','sunZenithAngles','viewAzimuthMean','viewZenithMean'] ) + l1c.atmospheric_correction(method="iCor").download("rgb-icor.geotiff",format="GTiff") + + +SAR backscatter +--------------- + +Data from synthetic aperture radar sensors requires significant preprocessing to be calibrated and normalized for terrain. +This is referred to as backscatter computation, and supported by +`sar_backscatter `_ and the CARD4L compliant variant +`ard_normalized_radar_backscatter `_ + +The user should load a datacube containing raw SAR data, such as Sentinel-1 GRD. On the resulting datacube, the +:func:`~openeo.rest.datacube.DataCube.sar_backscatter` method can be invoked. The CEOS CARD4L variant is: +:func:`~openeo.rest.datacube.DataCube.ard_normalized_radar_backscatter`. These processes are tightly coupled to +metadata from specific sensors, so it is not possible to apply other processes to the datacube first, +with the exception of specifying filters in space and time. + + +Reference implementations +######################### + +This section shows a few working examples for these processes. + +EODC back-end +************* + +EODC (https://openeo.eodc.eu/v1.0) supports sar_backscatter, based on the Sentinel-1 toolbox. (https://sentinel.esa.int/web/sentinel/toolboxes/sentinel-1) + +Geotrellis back-end +******************* + +When working with the Sentinelhub SENTINEL1_GRD collection, both sar processes can be used. The underlying implementation is +provided by Sentinelhub, (https://docs.sentinel-hub.com/api/latest/data/sentinel-1-grd/#processing-options), and offers full +CARD4L compliant processing options. + +This is an example of :func:`~openeo.rest.datacube.DataCube.ard_normalized_radar_backscatter`:: + + s1grd = (connection.load_collection('SENTINEL1_GRD', bands=['VH', 'VV']) + .filter_bbox(west=2.59003, east=2.8949, north=51.2206, south=51.069) + .filter_temporal(extent=["2019-10-10","2019-10-10"])) + + job = s1grd.ard_normalized_radar_backscatter().execute_batch() + + for asset in job.get_results().get_assets(): + asset.download() + +When working with other GRD data, an implementation based on Orfeo Toolbox is used: + +- `Orfeo docs `_ +- `Implementation `_ + +The Orfeo implementation currently only supports sigma0 computation, and is not CARD4L compliant. diff --git a/_sources/cookbook/index.rst.txt b/_sources/cookbook/index.rst.txt new file mode 100644 index 000000000..719d2049b --- /dev/null +++ b/_sources/cookbook/index.rst.txt @@ -0,0 +1,14 @@ +openEO CookBook +=============== + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + ard + sampling + udp_sharing + spectral_indices + job_manager + localprocessing + tricks diff --git a/_sources/cookbook/job_manager.rst.txt b/_sources/cookbook/job_manager.rst.txt new file mode 100644 index 000000000..b5219dc72 --- /dev/null +++ b/_sources/cookbook/job_manager.rst.txt @@ -0,0 +1,122 @@ +==================================== +Multi Backend Job Manager +==================================== + +API +=== + +.. warning:: + This is a new experimental API, subject to change. + +.. autoclass:: openeo.extra.job_management.MultiBackendJobManager + :members: + +.. autoclass:: openeo.extra.job_management.JobDatabaseInterface + :members: + +.. autoclass:: openeo.extra.job_management.CsvJobDatabase + +.. autoclass:: openeo.extra.job_management.ParquetJobDatabase + + +.. autoclass:: openeo.extra.job_management.ProcessBasedJobCreator + :members: + :special-members: __call__ + + +.. _job-management-with-process-based-job-creator: + +Job creation based on parameterized processes +=============================================== + +The openEO API supports parameterized processes out of the box, +which allows to work with flexible, reusable openEO building blocks +in the form of :ref:`user-defined processes ` +or `remote openEO process definitions `_. +This can also be leveraged for job creation in the context of the +:py:class:`~openeo.extra.job_management.MultiBackendJobManager`: +define a "template" job as a parameterized process +and let the job manager fill in the parameters +from a given data frame. + +The :py:class:`~openeo.extra.job_management.ProcessBasedJobCreator` helper class +allows to do exactly that. +Given a reference to a parameterized process, +such as a user-defined process or remote process definition, +it can be used directly as ``start_job`` callable to +:py:meth:`~openeo.extra.job_management.MultiBackendJobManager.run_jobs` +which will fill in the process parameters from the dataframe. + +Basic :py:class:`~openeo.extra.job_management.ProcessBasedJobCreator` example +----------------------------------------------------------------------------- + +Basic usage example with a remote process definition: + +.. code-block:: python + :linenos: + :caption: Basic :py:class:`~openeo.extra.job_management.ProcessBasedJobCreator` example snippet + :emphasize-lines: 10-15, 28 + + from openeo.extra.job_management import ( + MultiBackendJobManager, + create_job_db, + ProcessBasedJobCreator, + ) + + # Job creator, based on a parameterized openEO process + # (specified by the remote process definition at given URL) + # which has parameters "start_date" and "bands" for example. + job_starter = ProcessBasedJobCreator( + namespace="https://example.com/my_process.json", + parameter_defaults={ + "bands": ["B02", "B03"], + }, + ) + + # Initialize job database from a dataframe, + # with desired parameter values to fill in. + df = pd.DataFrame({ + "start_date": ["2021-01-01", "2021-02-01", "2021-03-01"], + }) + job_db = create_job_db("jobs.csv").initialize_from_df(df) + + # Create and run job manager, + # which will start a job for each of the `start_date` values in the dataframe + # and use the default band list ["B02", "B03"] for the "bands" parameter. + job_manager = MultiBackendJobManager(...) + job_manager.run_jobs(job_db=job_db, start_job=job_starter) + +In this example, a :py:class:`ProcessBasedJobCreator` is instantiated +based on a remote process definition, +which has parameters ``start_date`` and ``bands``. +When passed to :py:meth:`~openeo.extra.job_management.MultiBackendJobManager.run_jobs`, +a job for each row in the dataframe will be created, +with parameter values based on matching columns in the dataframe: + +- the ``start_date`` parameter will be filled in + with the values from the "start_date" column of the dataframe, +- the ``bands`` parameter has no corresponding column in the dataframe, + and will get its value from the default specified in the ``parameter_defaults`` argument. + + +:py:class:`~openeo.extra.job_management.ProcessBasedJobCreator` with geometry handling +--------------------------------------------------------------------------------------------- + +Apart from the intuitive name-based parameter-column linking, +:py:class:`~openeo.extra.job_management.ProcessBasedJobCreator` +also automatically links: + +- a process parameters that accepts inline GeoJSON geometries/features + (which practically means it has a schema like ``{"type": "object", "subtype": "geojson"}``, + as produced by :py:meth:`Parameter.geojson `). +- with the geometry column in a `GeoPandas `_ dataframe. + +even if the name of the parameter does not exactly match +the name of the GeoPandas geometry column (``geometry`` by default). +This automatic liking is only done if there is only one +GeoJSON parameter and one geometry column in the dataframe. + + +.. admonition:: to do + + Add example with geometry handling. diff --git a/_sources/cookbook/localprocessing.rst.txt b/_sources/cookbook/localprocessing.rst.txt new file mode 100644 index 000000000..ece58ebd7 --- /dev/null +++ b/_sources/cookbook/localprocessing.rst.txt @@ -0,0 +1,184 @@ +=============================== +Client-side (local) processing +=============================== + +.. warning:: + This is a new experimental feature and API, subject to change. + +Background +---------- + +The client-side processing functionality allows to test and use openEO with its processes locally, i.e. without any connection to an openEO back-end. +It relies on the projects `openeo-pg-parser-networkx `_, which provides an openEO process graph parsing tool, and `openeo-processes-dask `_, which provides an Xarray and Dask implementation of most openEO processes. + +Installation +------------ + +.. note:: + This feature requires ``Python>=3.9``. + Tested with ``openeo-pg-parser-networkx==2023.5.1`` and + ``openeo-processes-dask==2023.7.1``. + +.. code:: bash + + pip install openeo[localprocessing] + +Usage +----- + +Every openEO process graph relies on data which is typically provided by a cloud infrastructure (the openEO back-end). +The client-side processing adds the possibility to read and use local netCDFs, geoTIFFs, ZARR files, and remote STAC Collections or Items for your experiments. + +STAC Collections and Items +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + The provided examples using STAC rely on third party STAC Catalogs, we can't guarantee that the urls will remain valid. + +With the ``load_stac`` process it's possible to load and use data provided by remote or local STAC Collections or Items. +The following code snippet loads Sentinel-2 L2A data from a public STAC Catalog, using specific spatial and temporal extent, band name and also properties for cloud coverage. + +.. code-block:: pycon + + >>> from openeo.local import LocalConnection + >>> local_conn = LocalConnection("./") + + >>> url = "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + >>> spatial_extent = {"west": 11, "east": 12, "south": 46, "north": 47} + >>> temporal_extent = ["2019-01-01", "2019-06-15"] + >>> bands = ["red"] + >>> properties = {"eo:cloud_cover": dict(lt=50)} + >>> s2_cube = local_conn.load_stac(url=url, + ... spatial_extent=spatial_extent, + ... temporal_extent=temporal_extent, + ... bands=bands, + ... properties=properties, + ... ) + >>> s2_cube.execute() + + dask.array + Coordinates: (12/53) + * time (time) datetime64[ns] 2019-01-02... + id (time) `_. + If the code can not handle you special netCDF, + you can still modify the function that reads the metadata from it (`openeo/local/collections.py#L19 `_) + and the function that reads the data (`openeo/local/processing.py#L26 `_). + +Local Processing +~~~~~~~~~~~~~~~~ + +Let's start with the provided sample netCDF of Sentinel-2 data: + +.. code-block:: pycon + + >>> local_collection = "openeo-localprocessing-data/sample_netcdf/S2_L2A_sample.nc" + >>> s2_datacube = local_conn.load_collection(local_collection) + >>> # Check if the data is loaded correctly + >>> s2_datacube.execute() + + dask.array + Coordinates: + * t (t) datetime64[ns] 2022-06-02 2022-06-05 ... 2022-06-27 2022-06-30 + * x (x) float64 6.75e+05 6.75e+05 6.75e+05 ... 6.843e+05 6.843e+05 + * y (y) float64 5.155e+06 5.155e+06 5.155e+06 ... 5.148e+06 5.148e+06 + crs |S1 ... + * bands (bands) object 'B04' 'B03' 'B02' 'B08' 'SCL' + Attributes: + Conventions: CF-1.9 + institution: openEO platform - Geotrellis backend: 0.9.5a1 + description: + title: + +As you can see in the previous example, we are using a call to execute() which will execute locally the generated openEO process graph. +In this case, the process graph consist only in a single load_collection, which performs lazy loading of the data. With this first step you can check if the data is being read correctly by openEO. + +Looking at the metadata of this netCDF sample, we can see that it contains the bands B04, B03, B02, B08 and SCL. +Additionally, we also see that it is composed by more than one element in time and that it covers the month of June 2022. + +We can now do a simple processing for demo purposes, let's compute the median NDVI in time and visualize the result: + +.. code:: python + + b04 = s2_datacube.band("B04") + b08 = s2_datacube.band("B08") + ndvi = (b08 - b04) / (b08 + b04) + ndvi_median = ndvi.reduce_dimension(dimension="t", reducer="median") + result_ndvi = ndvi_median.execute() + result_ndvi.plot.imshow(cmap="Greens") + +.. image:: ../_static/images/local/local_ndvi.jpg + +We can perform the same example using data provided by STAC Collection: + +.. code:: python + + from openeo.local import LocalConnection + local_conn = LocalConnection("./") + + url = "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a" + spatial_extent = {"east": 11.40, "north": 46.52, "south": 46.46, "west": 11.25} + temporal_extent = ["2022-06-01", "2022-06-30"] + bands = ["red", "nir"] + properties = {"eo:cloud_cover": dict(lt=80)} + s2_datacube = local_conn.load_stac( + url=url, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + ) + + b04 = s2_datacube.band("red") + b08 = s2_datacube.band("nir") + ndvi = (b08 - b04) / (b08 + b04) + ndvi_median = ndvi.reduce_dimension(dimension="time", reducer="median") + result_ndvi = ndvi_median.execute() diff --git a/_sources/cookbook/sampling.md.txt b/_sources/cookbook/sampling.md.txt new file mode 100644 index 000000000..ce06c1e6a --- /dev/null +++ b/_sources/cookbook/sampling.md.txt @@ -0,0 +1,61 @@ + +# Dataset sampling + + +A number of use cases do not require a full datacube to be computed, +but rather want to extract a result at specific locations. +Examples include extracting training data for model calibration, or computing the result for +areas where validation data is available. + +An important constraint is that most implementations assume that sampling is an operation +on relatively small areas, of for instance up to 512x512 pixels (but often much smaller). +When extracting larger areas, it is recommended to look into running a separate job per 'sample'. + +Sampling can be done for points or polygons: + +- point extractions basically result in a 'vector cube', so can be exported into tabular formats. +- polygon extractions can be stored to an individual netCDF per polygon so in this case the output is a sparse raster cube. + +To indicate to openEO that we only want to compute the datacube for certain polygon features, we use the +`openeo.rest.datacube.DataCube.filter_spatial` method. + +Next to that, we will also indicate that we want to write multiple output files. This is more convenient, as we will +want to have one or more raster outputs per sampling feature, for convenient further processing. To do this, we set +the 'sample_by_feature' output format property, which is available for the netCDF and GTiff output formats. + +Combining all of this, results in the following sample code: + +```python +s2_bands = auth_connection.load_collection( + "SENTINEL2_L2A", + bands=["B04"], + temporal_extent=["2020-05-01", "2020-06-01"], +) +s2_bands = s2_bands.filter_spatial( + "https://artifactory.vgt.vito.be/testdata-public/parcels/test_10.geojson", +) +job = s2_bands.create_job( + title="Sentinel2", + description="Sentinel-2 L2A bands", + out_format="netCDF", + sample_by_feature=True, +) +``` + + +Sampling only works for batch jobs, because it results in multiple output files, which can not be conveniently transferred +in a synchronous call. + +## Performance & scalability + +It's important to note that dataset sampling is not necessarily a cheap operation, since creation of a sparse datacube still +may require accessing a large number of raw EO assets. Backends of course can and should optimize to restrict processing +to a minimum, but the size of the required input datasets is often a determining factor for cost and performance rather +than the size of the output dataset. + +## Sampling at scale + +When doing large scale (e.g. continental) sampling, it is usually not possible or impractical to run it as a single openEO +batch job. The recommendation here is to apply a spatial grouping to your sampling locations, with a single group covering +an area of around 100x100km. The optimal size of a group may be backend dependant. Also remember that when working with +data in the UTM projection, you may want to avoid covering multiple UTM zones in a single group. diff --git a/_sources/cookbook/spectral_indices.rst.txt b/_sources/cookbook/spectral_indices.rst.txt new file mode 100644 index 000000000..21ebe849d --- /dev/null +++ b/_sources/cookbook/spectral_indices.rst.txt @@ -0,0 +1,88 @@ +==================================== +Spectral Indices +==================================== + +.. warning:: + This is a new experimental API, subject to change. + +``openeo.extra.spectral_indices`` is an auxiliary subpackage +to simplify the calculation of common spectral indices +used in various Earth observation applications (vegetation, water, urban etc.). +It leverages the spectral indices defined in the +`Awesome Spectral Indices `_ project +by `David Montero Loaiza `_. + +.. versionadded:: 0.9.1 + +Band mapping +============= + +The formulas provided by "Awesome Spectral Indices" are defined in terms of standardized variable names +like "B" for blue, "R" for red, "N" for near-infrared, "WV" for water vapour, etc. + +.. code-block:: json + + "NDVI": { + "formula": "(N - R)/(N + R)", + "long_name": "Normalized Difference Vegetation Index", + +Obviously, these formula variables have to be mapped properly to the band names of your cube. + +Automatic band mapping +----------------------- +In most simple cases, when there is enough collection metadata +to automatically detect the satellite platform (Sentinel2, Landsat8, ..) +and the original band names haven't been renamed, +this mapping will be handled automatically, e.g.: + +.. code-block:: python + :emphasize-lines: 2 + + cube = connection.load_collection("SENTINEL2_L2A", ...) + indices = compute_indices(cube, indices=["NDVI", "NDMI"]) + + + +.. _spectral_indices_manual_band_mapping: + +Manual band mapping +-------------------- + +In more complex cases, it might be necessary to specify some additional information to guide the band mapping. +If the band names follow the standard, but it's just the satellite platform can not be guessed +from the collection metadata, it is typically enough to specify the platform explicitly: + +.. code-block:: python + :emphasize-lines: 4 + + indices = compute_indices( + cube, + indices=["NDVI", "NDMI"], + platform="SENTINEL2", + ) + +Additionally, if the band names in your cube have been renamed, deviating from conventions, it is also +possible to explicitly specify the band name to spectral index variable name mapping: + +.. code-block:: python + :emphasize-lines: 4-8 + + indices = compute_indices( + cube, + indices=["NDVI", "NDMI"], + variable_map={ + "R": "S2-red", + "N": "S2-nir", + "S1": "S2-swir", + }, + ) + +.. versionadded:: 0.26.0 + Function arguments ``platform`` and ``variable_map`` to fine-tune the band mapping. + + +API +==== + +.. automodule:: openeo.extra.spectral_indices + :members: list_indices, compute_and_rescale_indices, append_and_rescale_indices, compute_indices, append_indices, compute_index, append_index diff --git a/_sources/cookbook/tricks.rst.txt b/_sources/cookbook/tricks.rst.txt new file mode 100644 index 000000000..4b9fb3fb2 --- /dev/null +++ b/_sources/cookbook/tricks.rst.txt @@ -0,0 +1,82 @@ +=============================== +Miscellaneous tips and tricks +=============================== + + +.. _process_graph_export: + +Export a process graph +----------------------- + +You can export the underlying process graph of +a :py:class:`~openeo.rest.datacube.DataCube`, :py:class:`~openeo.rest.vectorcube.VectorCube`, etc, +to a standardized JSON format, which allows interoperability with other openEO tools. + +For example, use :py:meth:`~openeo.rest.datacube.DataCube.print_json()` to directly print the JSON representation +in your interactive Jupyter or Python session: + +.. code-block:: pycon + + >>> dump = cube.print_json() + { + "process_graph": { + "loadcollection1": { + "process_id": "load_collection", + ... + +Or save it to a file, by getting the JSON representation first as a string +with :py:meth:`~openeo.rest.datacube.DataCube.to_json()`: + +.. code-block:: python + + # Export as JSON string + dump = cube.to_json() + + # Write to file in `pathlib` style + export_path = pathlib.Path("path/to/export.json") + export_path.write_text(dump, encoding="utf8") + + # Write to file in `open()` style + with open("path/to/export.json", encoding="utf8") as f: + f.write(dump) + + +.. warning:: + + Avoid using methods like :py:meth:`~openeo.rest.datacube.DataCube.flat_graph()`, + which are mainly intended for internal use. + Not only are these methods subject to change, they also lead to representations + with interoperability and reuse issues. + For example, naively printing or automatic (``repr``) rendering of + :py:meth:`~openeo.rest.datacube.DataCube.flat_graph()` output will roughly look like JSON, + but is in fact invalid: it uses single quotes (instead of double quotes) + and booleans values are title-case (instead of lower case). + + + + +Execute a process graph directly from raw JSON +----------------------------------------------- + +When you have a process graph in JSON format, as a string, a local file or a URL, +you can execute/download it without converting it do a DataCube first. +Just pass the string, path or URL directly to +:py:meth:`Connection.download() `, +:py:meth:`Connection.execute() ` or +:py:meth:`Connection.create_job() `. +For example: + +.. code-block:: python + + # `execute` with raw JSON string + connection.execute(""" + { + "add": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": true} + } + """) + + # `download` with local path to JSON file + connection.download("path/to/my-process-graph.json") + + # `create_job` with URL to JSON file + job = connection.create_job("https://jsonbin.example/my/process-graph.json") diff --git a/_sources/cookbook/udp_sharing.rst.txt b/_sources/cookbook/udp_sharing.rst.txt new file mode 100644 index 000000000..cbc18d1e4 --- /dev/null +++ b/_sources/cookbook/udp_sharing.rst.txt @@ -0,0 +1,133 @@ +==================================== +Sharing of user-defined processes +==================================== + + +.. warning:: + Beta feature - + At the time of this writing (July 2021), sharing of :ref:`user-defined processes ` + (publicly or among users) is not standardized in the openEO API. + There are however some experimental sharing features in the openEO Python Client Library + and some back-end providers that we are going to discuss here. + + Be warned that the details of this feature are subject to change. + For more status information, consult GitHub ticket + `Open-EO/openeo-api#310 `_. + + + + +Publicly publishing a user-defined process. +============================================ + +As discussed in :ref:`build_and_store_udp`, user-defined processes can be +stored with the :py:meth:`~openeo.rest.connection.Connection.save_user_defined_process` method +on a on a back-end :py:class:`~openeo.rest.connection.Connection`. +By default, these user-defined processes are private and only accessible by the user that saved it:: + + from openeo.processes import subtract, divide + from openeo.api.process import Parameter + + # Build user-defined process + f = Parameter.number("f", description="Degrees Fahrenheit.") + fahrenheit_to_celsius = divide(x=subtract(x=f, y=32), y=1.8) + + # Store user-defined process in openEO back-end. + udp = connection.save_user_defined_process( + "fahrenheit_to_celsius", + fahrenheit_to_celsius, + parameters=[f] + ) + + +Some back-ends, like the VITO/Terrascope back-end allow a user to flag a user-defined process as "public" +so that other users can access its description and metadata:: + + udp = connection.save_user_defined_process( + ... + public=True + ) + +The sharable, public URL of this user-defined process is available from the metadata given by +:py:meth:`RESTUserDefinedProcess.describe `. +It's listed as "canonical" link:: + + >>> udp.describe() + { + "id": "fahrenheit_to_celsius", + "links": [ + { + "rel": "canonical", + "href": "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius", + "title": "Public URL for user-defined process fahrenheit_to_celsius" + } + ], + ... + + +.. _udp_sharing_call_url_namespace: + +Using a public UDP through URL based "namespace" +================================================== + +Some back-ends, like the VITO/Terrascope back-end, allow to use a public UDP +through setting its public URL as the ``namespace`` property of the process graph node. + +For example, based on the ``fahrenheit_to_celsius`` UDP created above, +the "flat graph" representation of a process graph could look like this:: + + { + ... + "to_celsius": { + "process_id": "fahrenheit_to_celsius", + "namespace": "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius", + "arguments": {"f": 86} + } + + +As a very basic illustration with the openEO Python Client library, +we can create and evaluate a process graph, +containing a ``fahrenheit_to_celsius`` call as single process, +with :meth:`Connection.datacube_from_process ` as follows:: + + cube = connection.datacube_from_process( + process_id="fahrenheit_to_celsius", + namespace="https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius", + f=86 + ) + print(cube.execute()) + # Prints: 30.0 + + +Loading a published user-defined process as DataCube +====================================================== + + +From the public URL of the user-defined process, +it is also possible for another user to construct, fully client-side, +a new :py:class:`~openeo.rest.datacube.DataCube` +with :py:meth:`Connection.datacube_from_json() `. + +It is important to note that this approach is different from calling +a user-defined process as described in :ref:`evaluate_udp` and :ref:`udp_sharing_call_url_namespace`. +:py:meth:`Connection.datacube_from_json() ` +breaks open the encapsulation of the user-defined process and "unrolls" the process graph inside +into a new :py:class:`~openeo.rest.datacube.DataCube`. +This also implies that parameters defined in the user-defined process have to be provided when calling +:py:meth:`Connection.datacube_from_json() `: + + +.. code-block:: python + :emphasize-lines: 4 + + udp_url = "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius" + cube = connection.datacube_from_json( + udp_url, + parameters={"f": 86}, + ) + print(cube.execute()) + # Prints: 30.0 + +Note that :py:meth:`Connection.datacube_from_json() ` +not only supports loading UDPs from an URL but also from a raw JSON string or a local file path. +For more information, also see :ref:`datacube_from_json`. diff --git a/_sources/data_access.rst.txt b/_sources/data_access.rst.txt new file mode 100644 index 000000000..cdc0d0d81 --- /dev/null +++ b/_sources/data_access.rst.txt @@ -0,0 +1,345 @@ +.. _data_access_chapter: + +######################## +Finding and loading data +######################## + + +As illustrated in the basic concepts, most openEO scripts start with ``load_collection``, but this skips the step of +actually finding out which collection to load. This section dives a bit deeper into finding the right data, and some more +advanced data loading use cases. + +Data discovery +============== + +To explore data in a given back-end, it is recommended to use a more visual tool like the openEO Hub +(http://hub.openeo.org/). This shows available collections, and metadata in a user-friendly manner. + +Next to that, the client also offers various :py:class:`~openeo.rest.connection.Connection` methods +to explore collections and their metadata: + +- :py:meth:`~openeo.rest.connection.Connection.list_collection_ids` + to list all collection ids provided by the back-end +- :py:meth:`~openeo.rest.connection.Connection.list_collections` + to list the basic metadata of all collections +- :py:meth:`~openeo.rest.connection.Connection.describe_collection` + to get the complete metadata of a particular collection + +When using these methods inside a Jupyter notebook, you should notice that the output is rendered in a user friendly way. + +In a regular script, these methods can be used to programmatically find a collection that matches specific criteria. + +As a user, make sure to carefully read the documentation for a given collection, as there can be important differences. +You should also be aware of the data retention policy of a given collection: some data archives only retain the last 3 months +for instance, making them only suitable for specific types of analysis. Such differences can have an impact on the reproducibility +of your openEO scripts. + +Also note that the openEO metadata may use links to point to much more information for a particular collection. For instance +technical specification on how the data was preprocessed, or viewers that allow you to visually explore the data. This can +drastically improve your understanding of the dataset. + +Finally, licensing information is important to keep an eye on: not all data is free and open. + + +Initial exploration of an openEO collection +------------------------------------------- + +A common question from users is about very specific details of a collection, we'd like to list some examples and solutions here: + +- The collection data type, and range of values, can be determined by simply downloading a sample of data, as NetCDF or Geotiff. This can in fact be done at any point in the design of your script, to get a good idea of intermediate results. +- Data availability, and available timestamps can be retrieved by computing average values for your area of interest. Just construct a polygon, and retrieve those statistics. For optical data, this can also be used to get an idea on cloud statistics. +- Most collections have a native projection system, again a simple download will give you this information if its not clear from the metadata. + +.. _data-loading-and-filtering: + +Loading a data cube from a collection +===================================== + +Many examples already illustrate the basic openEO ``load_collection`` process through a :py:meth:`Connection.load_collection() ` call, +with filters on space, time and bands. +For example: + +.. code-block:: python + + cube = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 3.75, "east": 4.08, "south": 51.29, "north": 51.39}, + temporal_extent=["2021-05-07", "2021-05-14"], + bands=["B04", "B03", "B02"], + ) + + +The purpose of these filters in ``load_collection`` is to reduce the amount of raw data that is loaded (and processed) by the back-end. +This is essential to get a response to your processing request in reasonable time and keep processing costs low. +It's recommended to start initial exploration with a small spatio-temporal extent +and gradually increase the scope once initial tests work out. + +Next to specifying filters inside the ``load_collection`` process, +there are also possibilities to filter with separate filter processes, e.g. at a later stage in your process graph. +For most openEO back-ends, the following example snippet should be equivalent to the previous: + +.. code-block:: python + + cube = connection.load_collection("SENTINEL2_L2A") + cube = cube.filter_bbox(west=3.75, east=4.08, south=51.29, north=51.39) + cube = cube.filter_temporal("2021-05-07", "2021-05-14") + cube = cube.filter_bands(["B04", "B03", "B02"]) + + +Another nice feature is that processes that work with geometries or vector features +(e.g. aggregated statistics for a polygon, or masking by polygon) +can also be used by a back-end to automatically infer an appropriate spatial extent. +This way, you do not need to explicitly set these filters yourself. + +In the following sections, we want to dive a bit into details, and more advanced cases. + + +Filter on spatial extent +======================== + +A spatial extent is a bounding box that specifies the minimum and and maximum longitude and latitude of the region of interest you want to process. + +By default these latitude and longitude values are expressed in the standard Coordinate Reference System for the world, +which is EPSG:4326, also known as "WGS 84", or just "lat-long". + +.. code-block:: python + + connection.load_collection( + ..., + spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19}, + ) + +.. _filtering-on-temporal-extent-section: + +Filter on temporal extent +========================= + +Usually you don't need the complete time range provided by a collection +and you should specify an appropriate time window to load +as a ``temporal_extent`` pair containing a start and end date: + +.. code-block:: python + + connection.load_collection( + ..., + temporal_extent=["2021-05-07", "2021-05-14"], + ) + +In most use cases, day-level granularity is enough and you can just express the dates as strings in the format ``"yyyy-mm-dd"``. +You can also pass ``datetime.date`` objects (from Python standard library) if you already have your dates in that format. + +.. note:: + When you need finer, time-level granularity, you can pass ``datetime.datetime`` objects. + Or, when passed as a string, the openEO API requires date and time to be provided in RFC 3339 format. + For example for for 2020-03-17 at 12:34:56 in UTC:: + + "2020-03-17T12:34:56Z" + + + +.. _left-closed-temporal-extent: + +Left-closed intervals: start included, end excluded +--------------------------------------------------- + +Time ranges in openEO processes like ``load_collection`` and ``filter_temporal`` are handled as left-closed ("half-open") temporal intervals: +the start instant is included in the interval, but the end instant is excluded from the interval. + +For example, the interval defined by ``["2020-03-05", "2020-03-15"]`` covers observations +from 2020-03-05 up to (and including) 2020-03-14 (just before midnight), +but does not include observations from 2020-03-15. + +.. TODO: nicer diagram instead of this ASCII art +.. code-block:: text + + 2020-03-05 2020-03-14 2022-03-15 + ________|____________|_________________________|____________|____________|_____ + + [--------------------------------------------------(O + including excluding + 2020-03-05 00:00:00.000 2020-03-15 00:00:00.000 + + +While this might look unintuitive at first, +working with half-open intervals avoids common and hard to discover pitfalls when combining multiple intervals, +like unintended window overlaps or double counting observations at interval borders. + +.. _date-shorthand-handling: + +Year/month shorthand notation +------------------------------ + +.. note:: + + Year/month shorthand notation handling is available since version 0.23.0. + +Rounding down periods to dates +`````````````````````````````` + +The openEO Python Client Library supports some shorthand notations for the temporal extent, +which come in handy if you work with year/month based temporal intervals. +Date strings that only consist of a year or a month will be automatically +"rounded down" to the first day of that period. For example:: + + "2023" -> "2023-01-01" + "2023-08" -> "2023-08-01" + +This approach fits best with :ref:`left-closed interval handling `. + +For example, the following two ``load_collection`` calls are equivalent: + +.. code-block:: python + + # Filter for observations in 2021 (left-closed interval). + connection.load_collection(temporal_extent=["2021", "2022"], ...) + # The above is shorthand for: + connection.load_collection(temporal_extent=["2021-01-01", "2022-01-01"], ...) + +The same applies for :py:meth:`~openeo.rest.datacube.DataCube.filter_temporal()`, +which has a couple of additional call forms. +All these calls are equivalent: + +.. code-block:: python + + # Filter for March, April and May (left-closed interval) + cube = cube.filter_temporal("2021-03", "2021-06") + cube = cube.filter_temporal(["2021-03", "2021-06"]) + cube = cube.filter_temporal(start_date="2021-03", end_date="2021-06") + cube = cube.filter_temporal(extent=("2021-03", "2021-06")) + + # The above are shorthand for: + cube = cube.filter_temporal("2021-03-01", "2022-06-01") + +.. _single-string-temporal-extents: + +Single string temporal extents +`````````````````````````````` + +Apart from rounding down year or month string, the openEO Python Client Library provides an additional +``extent`` handling feature in methods like +:py:meth:`Connection.load_collection(temporal_extent=...) ` +and :py:meth:`DataCube.filter_temporal(extent=...) `. +Normally, the ``extent`` argument should be a list or tuple containing start and end date, +but if a single string is given, representing a year, month (or day) period, +it is automatically expanded to the appropriate interval, +again following the :ref:`left-closed interval principle `. +For example:: + + extent="2022" -> extent=("2022-01-01", "2023-01-01") + extent="2022-05" -> extent=("2022-05-01", "2022-06-01") + extent="2022-05-17" -> extent=("2022-05-17", "2022-05-18") + + +The following snippet shows some examples of equivalent calls: + +.. code-block:: python + + connection.load_collection(temporal_extent="2022", ...) + # The above is shorthand for: + connection.load_collection(temporal_extent=("2022-01-01", "2023-01-01"), ...) + + + cube = cube.filter_temporal(extent="2021-03") + # The above are shorthand for: + cube = cube.filter_temporal(extent=("2021-03-01", "2022-04-01")) + + +Filter on collection properties +=============================== + +Although openEO presents data in a data cube, a lot of collections are still backed by a product based catalog. This +allows filtering on properties of that catalog. + +A very common use case is to pre-filter Sentinel-2 products on cloud cover. +This avoids loading clouded data unnecessarily and increases performance. +:py:meth:`Connection.load_collection() ` provides +a dedicated ``max_cloud_cover`` argument (shortcut for the ``eo:cloud_cover`` property) for that: + +.. code-block:: python + :emphasize-lines: 4 + + connection.load_collection( + "SENTINEL2_L2A", + ..., + max_cloud_cover=80, + ) + +For more general cases, you can use the ``properties`` argument to filter on any collection property. +For example, to filter on the relative orbit number of SAR data: + +.. code-block:: python + :emphasize-lines: 4-6 + + connection.load_collection( + "SENTINEL1_GRD", + ..., + properties={ + "relativeOrbitNumber": lambda x: x==116 + }, + ) + +Version 0.26.0 of the openEO Python Client Library adds +:py:func:`~openeo.rest.graph_building.collection_property` +which makes defining such property filters more user-friendly by avoiding the ``lambda`` construct: + +.. code-block:: python + :emphasize-lines: 6-8 + + import openeo + + connection.load_collection( + "SENTINEL1_GRD", + ..., + properties=[ + openeo.collection_property("relativeOrbitNumber") == 116, + ], + ) + +Note that property names follow STAC metadata conventions, but some collections can have different names. + +Property filters in openEO are also specified by small process graphs, that allow the use of the same generic processes +defined by openEO. This is the 'lambda' process that you see in the property dictionary. Do note that not all processes +make sense for product filtering, and can not always be properly translated into the query language of the catalog. +Hence, some experimentation may be needed to find a filter that works. + +One important caveat in this example is that 'relativeOrbitNumber' is a catalog specific property name. Meaning that +different archives may choose a different name for a given property, and the properties that are available can depend +on the collection and the catalog that is used by it. This is not a problem caused by openEO, but by the limited +standardization between catalogs of EO data. + + +Handling large vector data sets +=============================== + +For simple use cases, it is common to directly embed geometries (vector data) in your openEO process graph. +Unfortunately, with large vector data sets this leads to very large process graphs +and you might hit certain limits, +resulting in HTTP errors like ``413 Request Entity Too Large`` or ``413 Payload Too Large``. + +This problem can be circumvented by first uploading your vector data to a file sharing service +(like Google Drive, DropBox, GitHub, ...) +and use its public URL in the process graph instead +through :py:meth:`Connection.vectorcube_from_paths `. +For example, as follows: + +.. code-block:: python + + # Load vector data from URL + url = "https://github.com/Open-EO/openeo-python-client/raw/master/tests/data/example_aoi.pq" + parcels = connection.vectorcube_from_paths([url], format="parquet") + + # Use the parcel vector data, for example to do aggregation. + cube = connection.load_collection( + "SENTINEL2_L2A", + bands=["B04", "B03", "B02"], + temporal_extent=["2021-05-12", "2021-06-01"], + ) + aggregations = cube.aggregate_spatial( + geometries=parcels, + reducer="mean", + ) + +Note that while openEO back-ends typically support multiple vector formats, like GeoJSON and GeoParquet, +it is usually recommended to use a compact format like GeoParquet, instead of GeoJSON. The list of supported formats +is also advertised by the backend, and can be queried with +:py:meth:`Connection.list_file_formats `. diff --git a/_sources/datacube_construction.rst.txt b/_sources/datacube_construction.rst.txt new file mode 100644 index 000000000..5422a7a87 --- /dev/null +++ b/_sources/datacube_construction.rst.txt @@ -0,0 +1,250 @@ + +======================= +DataCube construction +======================= + + +The ``load_collection`` process +================================= + +The most straightforward way to start building your openEO data cube is through the ``load_collection`` process. +As mentioned earlier, this is provided by the +:py:meth:`~openeo.rest.connection.Connection.load_collection` method +on a :py:class:`~openeo.rest.connection.Connection` object, +which produces a :py:class:`~openeo.rest.datacube.DataCube` instance. +For example:: + + cube = connection.load_collection("SENTINEL2_TOC") + +While this should cover the majority of use cases, +there some cases +where one wants to build a :py:class:`~openeo.rest.datacube.DataCube` object +from something else or something more than just a simple ``load_collection`` process. + + + +.. _datacube_from_process: + +Construct DataCube from process +================================= + +Through :ref:`user-defined processes ` one can encapsulate +one or more ``load_collection`` processes and additional processing steps in a single +reusable user-defined process. +For example, imagine a user-defined process "masked_s2" +that loads an openEO collection "SENTINEL2_TOC" and applies some kind of cloud masking. +The implementation details of the cloud masking are not important here, +but let's assume there is a parameter "dilation" to fine-tune the cloud mask. +Also note that the collection id "SENTINEL2_TOC" is hardcoded in the user-defined process. + +We can now construct a data cube from this user-defined process +with :py:meth:`~openeo.rest.connection.Connection.datacube_from_process` +as follows:: + + cube = connection.datacube_from_process("masked_s2", dilation=10) + + # Further processing of the cube: + cube = cube.filter_temporal("2020-09-01", "2020-09-10") + + +Note that :py:meth:`~openeo.rest.connection.Connection.datacube_from_process` can be +used with all kind of processes, not only user-defined processes. +For example, while this is not exactly a real EO data use case, +it will produce a valid openEO process graph that can be executed:: + + >>> cube = connection.datacube_from_process("mean", data=[2, 3, 5, 8]) + >>> cube.execute() + 4.5 + + + +.. _datacube_from_json: + +Construct a DataCube from JSON +=============================== + +openEO process graphs are typically stored and published in JSON format. +Most notably, user-defined processes are transferred between openEO client +and back-end in a JSON structure roughly like in this example:: + + { + "id": "evi", + "parameters": [ + {"name": "red", "schema": {"type": "number"}}, + {"name": "blue", "schema": {"type": "number"}}, + ... + ], + "process_graph": { + "sub": {"process_id": "subtract", "arguments": {"x": {"from_parameter": "nir"}, "y": {"from_parameter": "red"}}}, + "p1": {"process_id": "multiply", "arguments": {"x": 6, "y": {"from_parameter": "red"}}}, + "div": {"process_id": "divide", "arguments": {"x": {"from_node": "sub"}, "y": {"from_node": "sum"}}, + ... + + +It is possible to construct a :py:class:`~openeo.rest.datacube.DataCube` object that corresponds with this +process graph with the :py:meth:`Connection.datacube_from_json ` method. +It can be given one of: + + - a raw JSON string, + - a path to a local JSON file, + - an URL that points to a JSON resource + +The JSON structure should be one of: + + - a mapping (dictionary) like the example above with at least a ``"process_graph"`` item, + and optionally a ``"parameters"`` item. + - a mapping (dictionary) with ``{"process_id": ...}`` items + + +Some examples +--------------- + +Load a :py:class:`~openeo.rest.datacube.DataCube` from a raw JSON string, containing a +simple "flat graph" representation: + +.. code-block:: python + + raw_json = '''{ + "lc": {"process_id": "load_collection", "arguments": {"id": "SENTINEL2_TOC"}}, + "ak": {"process_id": "apply_kernel", "arguments": {"data": {"from_node": "lc"}, "kernel": [[1,2,1],[2,5,2],[1,2,1]]}, "result": true} + }''' + cube = connection.datacube_from_json(raw_json) + +Load from a raw JSON string, containing a mapping with "process_graph" and "parameters": + +.. code-block:: python + + raw_json = '''{ + "parameters": [ + {"name": "kernel", "schema": {"type": "array"}, "default": [[1,2,1], [2,5,2], [1,2,1]]} + ], + "process_graph": { + "lc": {"process_id": "load_collection", "arguments": {"id": "SENTINEL2_TOC"}}, + "ak": {"process_id": "apply_kernel", "arguments": {"data": {"from_node": "lc"}, "kernel": {"from_parameter": "kernel"}}, "result": true} + } + }''' + cube = connection.datacube_from_json(raw_json) + +Load directly from a local file or URL containing these kind of JSON representations: + +.. code-block:: python + + # Local file + cube = connection.datacube_from_json("path/to/my_udp.json") + + # URL + cube = connection.datacube_from_json("https://example.com/my_udp.json") + + +Parameterization +----------------- + +When the process graph uses parameters, you must specify the desired parameter values +at the time of calling :py:meth:`Connection.datacube_from_json `. + +For example, take this simple toy example of a process graph that takes the sum of 5 and a parameter "increment": + +.. code-block:: python + + raw_json = '''{"add": { + "process_id": "add", + "arguments": {"x": 5, "y": {"from_parameter": "increment"}}, + "result": true + }}''' + +Trying to build a :py:class:`~openeo.rest.datacube.DataCube` from it without specifying parameter values will fail +like this: + +.. code-block:: pycon + + >>> cube = connection.datacube_from_json(raw_json) + ProcessGraphVisitException: No substitution value for parameter 'increment'. + +Instead, specify the parameter value: + +.. code-block:: pycon + :emphasize-lines: 3 + + >>> cube = connection.datacube_from_json( + ... raw_json, + ... parameters={"increment": 4}, + ... ) + >>> cube.execute() + 9 + + +Parameters can also be defined with default values, which will be used when they are not specified +in the :py:meth:`Connection.datacube_from_json ` call: + +.. code-block:: python + + raw_json = '''{ + "parameters": [ + {"name": "increment", "schema": {"type": "number"}, "default": 100} + ], + "process_graph": { + "add": {"process_id": "add", "arguments": {"x": 5, "y": {"from_parameter": "increment"}}, "result": true} + } + }''' + + cube = connection.datacube_from_json(raw_json) + result = cube.execute()) + # result will be 105 + + +Re-parameterization +``````````````````` + +TODO + + + +.. _multi-result-process-graphs: +Building process graphs with multiple result nodes +=================================================== + +.. note:: + Multi-result support is added in version 0.35.0 + +Most openEO use cases are just about building a single result data cube, +which is readily covered in the openEO Python client library through classes like +:py:class:`~openeo.rest.datacube.DataCube` and :py:class:`~openeo.rest.vectorcube.VectorCube`. +It is straightforward to create a batch job from these, or execute/download them synchronously. + +The openEO API also allows multiple result nodes in a single process graph, +for example to persist intermediate results or produce results in different output formats. +To support this, the openEO Python client library provides the :py:class:`~openeo.rest.multiresult.MultiResult` class, +which allows to group multiple :py:class:`~openeo.rest.datacube.DataCube` and :py:class:`~openeo.rest.vectorcube.VectorCube` objects +in a single entity that can be used to create or run batch jobs. For example: + + +.. code-block:: python + + from openeo import MultiResult + + cube1 = ... + cube2 = ... + multi_result = MultiResult([cube1, cube2]) + job = multi_result.create_job() + + +Moreover, it is not necessary to explicitly create such a +:py:class:`~openeo.rest.multiresult.MultiResult` object, +as the :py:meth:`Connection.create_job() ` method +directly supports passing multiple data cube objects in a list, +which will be automatically grouped as a multi-result: + +.. code-block:: python + + cube1 = ... + cube2 = ... + job = connection.create_job([cube1, cube2]) + + +.. important:: + + Only a single :py:class:`~openeo.rest.connection.Connection` can be in play + when grouping multiple results like this. + As everything is to be merged in a single process graph + to be sent to a single backend, + it is not possible to mix cubes created from different connections. diff --git a/_sources/development.rst.txt b/_sources/development.rst.txt new file mode 100644 index 000000000..88297230d --- /dev/null +++ b/_sources/development.rst.txt @@ -0,0 +1,420 @@ +.. _development-and-maintenance: + +########################### +Development and maintenance +########################### + + +For development on the ``openeo`` package itself, +it is recommended to install a local git checkout of the project +in development mode (``-e``) +with additional development related dependencies (``[dev]``) +like this:: + + pip install -e .[dev] + +If you are on Windows and experience problems installing this way, you can find some solutions in section `Development Installation on Windows`_. + +Running the unit tests +====================== + +The test suite of the openEO Python Client leverages +the nice `pytest `_ framework. +It is installed automatically when installing the openEO Python Client +with the ``[dev]`` extra as shown above. +Running the whole tests is as simple as executing:: + + pytest + +There are a ton of command line options for fine-tuning +(e.g. select a subset of tests, how results should be reported, ...). +Run ``pytest -h`` for a quick overview +or check the `pytest `_ documentation for more information. + +For example:: + + # Skip tests that are marked as slow + pytest -m "not slow" + + +Building the documentation +========================== + +Building the documentation requires `Sphinx `_ +and some plugins +(which are installed automatically as part of the ``[dev]`` install). + +Quick and easy +--------------- + +The easiest way to build the documentation is working from the ``docs`` folder +and using the ``Makefile``: + +.. code-block:: shell + + # From `docs` folder + make html + +(assumes you have ``make`` available, if not: use ``python -msphinx -M html . _build``.) + +This will generate the docs in HTML format under ``docs/_build/html/``. +Open the HTML files manually, +or use Python's built-in web server to host them locally, e.g.: + +.. code-block:: shell + + # From `docs` folder + python -m http.server 8000 + +Then, visit http://127.0.0.1:8000/_build/html/ in your browser + + +Like a Pro +------------ + +When doing larger documentation work, it can be tedious to manually rebuild the docs +and refresh your browser to check the result. +Instead, use `sphinx-autobuild `_ +to automatically rebuild on documentation changes and live-reload it in your browser. +After installation (``pip install sphinx-autobuild`` in your development environment), +just run + +.. code-block:: shell + + # From project root + sphinx-autobuild docs/ --watch openeo/ docs/_build/html/ + +and then visit http://127.0.0.1:8000 . +When you change (and save) documentation source files, your browser should now +automatically refresh and show the newly built docs. Just like magic. + + +Contributing code +================== + +User contributions (such as bug fixes and new features, both in source code and documentation) +are greatly appreciated and welcome. + + +Pull requests +-------------- + +We use a traditional `GitHub Pull Request (PR) `_ workflow +for user contributions, which roughly follows these steps: + +- Create a personal fork of https://github.com/Open-EO/openeo-python-client + (unless you already have push permissions to an existing fork or the original repo) +- Preferably: work on your contribution in a new feature branch +- Push your feature branch to your fork and create a pull request +- The pull request is the place for review, discussion and fine-tuning of your work +- Once your pull request is in good shape it will be merged by a maintainer + + +.. _precommit: + +Pre-commit for basic code quality checks +------------------------------------------ + +We started using the `pre-commit `_ tool +for basic fine-tuning of code style and quality in new contributions. +It's currently not enforced, but **enabling pre-commit is recommended** and appreciated +when contributing code. + +.. note:: + + Note that the whole repository does not fully follow all code styles rules at the moment. + We're just gradually introducing it, piggybacking on new contributions and commits. + + +Pre-commit set up +"""""""""""""""""" + +- Install the general ``pre-commit`` command line tool: + + - The simplest option is to install it directly in the *virtual environment* + you are using for openEO Python client development (e.g. ``pip install pre-commit``). + - You can also install it *globally* on your system + (e.g. using `pipx `_, conda, homebrew, ...) + so you can use it across different projects. + +- Install the project specific git hook scripts by running this in the root of your local git clone: + + .. code-block:: console + + pre-commit install + + This will automatically install additional scripts and tools in a sandbox + to run the various checks defined in the project's ``.pre-commit-config.yaml`` configuration file. + +Pre-commit usage +""""""""""""""""" + +When you commit new changes, the freshly installed pre-commit hook +will now automatically run each of the configured linters/formatters/... +Some of these just flag issues (e.g. invalid JSON files) +while others even automatically fix problems (e.g. clean up excessive whitespace). + +If there is some kind of violation, the commit will be blocked. +Address these problems and try to commit again. + +.. attention:: + + Some pre-commit tools directly *edit* your files (e.g. formatting tweaks) + instead of just flagging issues. + This might feel intrusive at first, but once you get the hang of it, + it should allow to streamline your workflow. + + In particular, it is recommended to use the *staging* feature of git to prepare your commit. + Pre-commit's proposed changes are not staged automatically, + so you can more easily keep them separate and review. + +.. tip:: + + You can temporarily disable pre-commit for these rare cases + where you intentionally want to commit violating code style, + e.g. through ``git commit`` command line option ``-n``/``--no-verify``. + + + + +Creating a release +================== + +This section describes the procedure to create +properly versioned releases of the ``openeo`` package +that can be downloaded by end users (e.g. through ``pip`` from pypi.org) +and depended on by other projects. + +The releases will end up on: + +- PyPi: `https://pypi.org/project/openeo `_ +- VITO Artifactory: `https://artifactory.vgt.vito.be/api/pypi/python-openeo/simple/openeo/ `_ +- GitHub: `https://github.com/Open-EO/openeo-python-client/releases `_ + +Prerequisites +------------- + +- You have permissions to push branches and tags and maintain releases on + the `openeo-python-client project on GitHub `_. +- You have permissions to upload releases to the + `openeo project on pypi.org `_ +- The Python virtual environment you work in has the latest versions + of the ``twine`` package installed. + If you plan to build the wheel yourself (instead of letting GitHub or Jenkins do this), + you also need recent enough versions of the ``setuptools`` and ``wheel`` packages. + +Important files +--------------- + +``setup.py`` + describes the metadata of the package, + like package name ``openeo`` and version + (which is extracted from ``openeo/_version.py``). + +``openeo/_version.py`` + defines the version of the package. + During general **development**, this version string should contain + a `pre-release `_ + segment (e.g. ``a1`` for alpha releases, ``b1`` for beta releases, etc) + to avoid collision with final releases. For example:: + + __version__ = '0.8.0a1' + + As discussed below, this pre-release suffix should + only be removed during the release procedure + and restored when bumping the version after the release procedure. + +``CHANGELOG.md`` + keeps track of important changes associated with each release. + It follows the `Keep a Changelog `_ convention + and should be properly updated with each bug fix, feature addition/removal, ... + under the ``Unreleased`` section during development. + +Procedure +--------- + +These are the steps to create and publish a new release of the ``openeo`` package. +To avoid the confusion with ad-hoc injection of some abstract version placeholder +that has to be replaced properly, +we will use a concrete version ``0.8.0`` in the examples below. + +0. Make sure you are working on **latest master branch**, + without uncommitted changes and all tests are properly passing. + +#. Create release commit: + + A. **Drop the pre-release suffix** from the version string in ``openeo/_version.py`` + so that it just a "final" semantic versioning string, e.g. ``0.8.0`` + + B. **Update CHANGELOG.md**: rename the "Unreleased" section title + to contain version and date, e.g.:: + + ## [0.8.0] - 2020-12-15 + + remove empty subsections + and start a new "Unreleased" section above it, like:: + + ## [Unreleased] + + ### Added + + ### Changed + + ### Removed + + ### Fixed + + + C. **Commit** these changes in git with a commit message like ``Release 0.8.0`` + and **push** to GitHub:: + + git add openeo/_version.py CHANGELOG.md + git commit -m 'Release 0.8.0' + git push origin master + +#. Optional, but recommended: wait for **VITO Jenkins** to build this updated master + (trigger it manually if necessary), + so that a build of a final, non-alpha release ``0.8.0`` + is properly uploaded to **VITO artifactory**. + +#. Create release on `PyPI `_: + + A. **Obtain a wheel archive** of the package, with one of these approaches: + + - *Preferably, the path of least surprise*: build wheel through GitHub Actions. + Go to workflow `"Build wheel" `_, + manually trigger a build with "Run workflow" button, wait for it to finish successfully, + download generated ``artifact.zip``, and finally: unzip it to obtain ``openeo-0.8.0-py3-none-any.whl`` + + - *Or, if you know what you are doing* and you're sure you have a clean + local checkout, you can also build it locally:: + + python setup.py bdist_wheel + + This should create ``dist/openeo-0.8.0-py3-none-any.whl`` + + B. **Upload** this wheel to `openeo project on PyPI `_:: + + python -m twine upload openeo-0.8.0-py3-none-any.whl + + Check the `release history on PyPI `_ + to verify the twine upload. + Another way to verify that the freshly created release installs + is using docker to do a quick install-and-burn, + for example as follows (check the installed version in pip's output):: + + docker run --rm -it python python -m pip install --no-deps openeo + +#. Create a **git version tag** and push it to GitHub:: + + git tag v0.8.0 + git push origin v0.8.0 + +#. Create a **release in GitHub**: + Go to `https://github.com/Open-EO/openeo-python-client/releases/new `_, + Enter ``v0.8.0`` under "tag", + enter title: ``openEO Python Client v0.8.0``, + use the corresponding ``CHANGELOG.md`` section as description + and publish it + (no need to attach binaries). + +#. **Bump the version** in ``openeo/_version.py``, (usually the "minor" level) + and append a pre-release "a1" suffix again, for example:: + + __version__ = '0.9.0a1' + + Commit this (e.g. with message ``_version.py: bump to 0.9.0a1``) + and push to GitHub. + +#. Update `conda-forge package `_ too + (requires conda recipe maintainer role). + Normally, the "regro-cf-autotick-bot" will create a `pull request `_. + If it builds fine, merge it. + If not, fix the issue + (typically in `recipe/meta.yaml `_) + and merge. + +#. Optionally: make a post about the new release + on the `openEO Platform Forum `_ + or the `CDSE Forum `_. + +Verification +""""""""""""" + +The new release should now be available/listed at: + +- `https://pypi.org/project/openeo/#history `_ +- `https://github.com/Open-EO/openeo-python-client/releases `_ + +Here is a bash (subshell) oneliner to verify that the PyPI release works properly:: + + ( + cd /tmp &&\ + python -m venv venv-openeo &&\ + source venv-openeo/bin/activate &&\ + pip install -U openeo &&\ + python -c "import openeo;print(openeo);print(openeo.__version__)" + ) + +It tries to install the latest version of the ``openeo`` package in a temporary virtual env, +import it and print the package version. + + +Development Installation on Windows +=================================== + +Normally you can install the client the same way on Windows as on Linux, like so: + +.. code-block:: console + + pip install -e .[dev] + +Alternative development installation +------------------------------------- + +The standard pure-``pip`` based installation should work with the most recent code. +However, in the past we sometimes had issues with this procedure. +Should you experience problems, consider using an alternative conda-based installation procedure: + +1. Create and activate a new conda environment for developing the openeo-python-client. + For example: + + .. code-block:: console + + conda create -n openeopyclient + conda activate openeopyclient + +2. In that conda environment, install only the dependencies of ``openeo`` via conda, + but not the ``openeo`` package itself. + + .. code-block:: console + + # Install openeo dependencies (from the conda-forge channel) + conda install --only-deps -c conda-forge openeo + +3. Do a ``pip install`` from the project root in *editable mode* (``pip -e``): + + .. code-block:: console + + pip install -e .[dev] + + + +Update of generated files +========================== + +Some parts of the openEO Python Client Library source code are +generated/compiled from upstream sources (e.g. official openEO specifications). +Because updates are not often required, +it's just a semi-manual procedure (to run from the project root): + +.. code-block:: console + + # Update the sub-repositories (like git submodules, but optional) + python specs/update-subrepos.py + + # Update `openeo/processes.py` from specifications in openeo-processes repository + python openeo/internal/processes/generator.py specs/openeo-processes specs/openeo-processes/proposals --output openeo/processes.py + + # Update the openEO process mapping documentation page + python docs/process_mapping.py > docs/process_mapping.rst diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 000000000..b2c1ba643 --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,75 @@ + +openEO Python Client +===================== + +.. image:: https://img.shields.io/badge/Status-Stable-yellow.svg + +Welcome to the documentation of ``openeo``, +the official Python client library for interacting with **openEO** back-ends +to process remote sensing and Earth observation data. +It provides a **Pythonic** interface for the openEO API, +supporting data/process discovery, process graph building, +batch job management and much more. + + +Usage example +------------- + +A simple example, to give a feel of using this library: + +.. code-block:: python + + import openeo + + # Connect to openEO back-end. + connection = openeo.connect("openeo.vito.be").authenticate_oidc() + + # Load data cube from TERRASCOPE_S2_NDVI_V2 collection. + cube = connection.load_collection( + "TERRASCOPE_S2_NDVI_V2", + spatial_extent={"west": 5.05, "south": 51.21, "east": 5.1, "north": 51.23}, + temporal_extent=["2022-05-01", "2022-05-30"], + bands=["NDVI_10M"], + ) + # Rescale digital number to physical values and take temporal maximum. + cube = cube.apply(lambda x: 0.004 * x - 0.08).max_time() + + cube.download("ndvi-max.tiff") + + +.. image:: _static/images/welcome.png + + +Table of contents +----------------- + +.. toctree:: + :maxdepth: 2 + + self + installation + basics + data_access + processes + batch_jobs + udp + auth + udf + datacube_construction + machine_learning + configuration + cookbook/index + api + api-processes + process_mapping + development + best_practices + changelog + + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/_sources/installation.rst.txt b/_sources/installation.rst.txt new file mode 100644 index 000000000..68df3f4af --- /dev/null +++ b/_sources/installation.rst.txt @@ -0,0 +1,126 @@ +************* +Installation +************* + + +It is an explicit goal of the openEO Python client library to be as easy to install as possible, +unlocking the openEO ecosystem to a broad audience. +The package is a pure Python implementation and its dependencies are carefully considered (in number and complexity). + + +Basic install +============= + +It is recommended to work in a some kind of *virtual environment* (``venv``, ``conda``, ...) +to avoid polluting the base install of Python on your operating system +or introducing conflicts with other applications. +How you organize your virtual environments heavily depends on your use case and workflow, +and is out of scope of this documentation. + + +Installation with ``pip`` +------------------------- + +The openEO Python client library is available from `PyPI `_ +and can be easily installed with a tool like ``pip``, for example: + +.. code-block:: console + + $ pip install openeo + +To upgrade the package to the latest release: + +.. code-block:: console + + $ pip install --upgrade openeo + + +Installation with Conda +------------------------ + +The openEO Python client library is available on `conda-forge `_ +and can be easily installed in a conda environment, for example: + +.. code-block:: console + + $ conda install -c conda-forge openeo + + +Verifying and troubleshooting +----------------------------- + +You can check if the installation worked properly +by trying to import the ``openeo`` package in a Python script, interactive shell or notebook: + +.. code-block:: python + + import openeo + + print(openeo.client_version()) + +This should print the installed version of the ``openeo`` package. + +If the first line gives an error like ``ModuleNotFoundError: No module named 'openeo'``, +some troubleshooting tips: + +- Restart you Python shell or notebook (or start a fresh one). +- Double check that the installation went well, + e.g. try re-installing and keep an eye out for error/warning messages. +- Make sure that you are working in the same (virtual) environment you installed the package in. + +If you still have troubles installing and importing ``openeo``, +feel free to reach out in the `community forum `_ +or the `project's issue tracker `_. +Try to describe your setup in enough detail: your operating system, +which virtual environment system you use, +the installation tool (``pip``, ``conda`` or something else), ... + + + +.. _installation-optional-dependencies: + +Optional dependencies +====================== + +Depending on your use case, you might also want to install some additional libraries. +For example: + +- ``netCDF4`` or ``h5netcdf`` for loading and writing NetCDF files (e.g. integrated in ``xarray.load_dataset()``) +- ``matplotlib`` for visualisation (e.g. integrated plot functionality in ``xarray`` ) +- ``pyarrow`` for (read/write) support of Parquet files + (e.g. with :py:class:`~openeo.extra.job_management.MultiBackendJobManager`) +- ``rioxarray`` for GeoTIFF support in the assert helpers from ``openeo.testing.results`` +- ``geopandas`` for working with dataframes with geospatial support, + (e.g. with :py:class:`~openeo.extra.job_management.MultiBackendJobManager`) + + +Enabling additional features +---------------------------- + +To use the on-demand preview feature and other Jupyter-enabled features, you need to install the necessary dependencies. + +.. code-block:: console + + $ pip install openeo[jupyter] + + +Source or development install +============================== + +If you closely track the development of the ``openeo`` package at +`github.com/Open-EO/openeo-python-client `_ +and want to work with unreleased features or contribute to the development of the package, +you can install it as follows from the root of a git source checkout: + +.. code-block:: console + + $ pip install -e .[dev] + +The ``-e`` option enables "development mode", which makes sure that changes you make to the source code +happen directly on the installed package, so that you don't have to re-install the package each time +you make a change. + +The ``[dev]`` (a so-called "extra") installs additional development related dependencies, +for example to run the unit tests. + +You can also find more information about installation for development on the :ref:`development-and-maintenance` page. diff --git a/_sources/machine_learning.rst.txt b/_sources/machine_learning.rst.txt new file mode 100644 index 000000000..69f315e1b --- /dev/null +++ b/_sources/machine_learning.rst.txt @@ -0,0 +1,118 @@ +****************** +Machine Learning +****************** + +.. warning:: + This API and documentation is experimental, + under heavy development and subject to change. + + +.. versionadded:: 0.10.0 + + +Random Forest based Classification and Regression +=================================================== + +openEO defines a couple of processes for *random forest* based machine learning +for Earth Observation applications: + +- ``fit_class_random_forest`` for training a random forest based classification model +- ``fit_regr_random_forest`` for training a random forest based regression model +- ``predict_random_forest`` for inference/prediction + +The openEO Python Client library provides the necessary functionality to set up +and execute training and inference workflows. + +Training +--------- + +Let's focus on training a classification model, where we try to predict +a class like a land cover type or crop type based on predictors +we derive from EO data. +For example, assume we have a GeoJSON FeatureCollection +of sample points and a corresponding classification target value as follows:: + + feature_collection = {"type": "FeatureCollection", "features": [ + { + "type": "Feature", + "properties": {"id": "b3dw-wd23", "target": 3}, + "geometry": {"type": "Point", "coordinates": [3.4, 51.1]} + }, + { + "type": "Feature", + "properties": {"id": "r8dh-3jkd", "target": 5}, + "geometry": {"type": "Point", "coordinates": [3.6, 51.2]} + }, + ... + + +.. note:: + Confusingly, the concept "feature" has somewhat conflicting meanings + for different audiences. GIS/EO people use "feature" to refer to the "rows" + in this feature collection. + For the machine learning community however, the properties (the "columns") + are the features. + To avoid confusion in this discussion we will avoid the term "feature" + and instead use "sample point" for the former and "predictor" for the latter. + + +We first build a datacube of "predictor" bands. +For simplicity, we will just use the raw B02/B03/B04 band values here +and use the temporal mean to eliminate the time dimension:: + + cube = connection.load_collection( + "SENTINEL2", + temporal_extent=[start, end], + spatial_extent=bbox, + bands=["B02", "B03", "B04"] + ) + cube = cube.reduce_dimension(dimension="t", reducer="mean") + +We now use ``aggregate_spatial`` to sample this *raster data cube* at the sample points +and get a *vector cube* where we have the temporal mean of the B02/B03/B04 bands as predictor values:: + + predictors = cube.aggregate_spatial(feature_collection, reducer="mean") + +We can now train a *Random Forest* model by calling the +:py:meth:`~openeo.rest.vectorcube.VectorCube.fit_class_random_forest` method on the predictor vector cube +and passing the original target class data:: + + model = predictors.fit_class_random_forest( + target=feature_collection, + ) + # Save the model as a batch job result asset + # so that we can load it in another job. + model = model.save_ml_model() + +Finally execute this whole training flow as a batch job:: + + training_job = model.create_job() + training_job.start_and_wait() + + +Inference +---------- + +When the batch job finishes successfully, the trained model can then be used +with the ``predict_random_forest`` process on the raster data cube +(or another cube with the same band structure) to classify all the pixels. + +Technically, the openEO ``predict_random_forest`` process has to be used as a reducer function +inside a ``reduce_dimension`` call, but the openEO Python client library makes it +a bit easier by providing a :py:meth:`~openeo.rest.datacube.DataCube.predict_random_forest` method +directly on the :py:class:`~openeo.rest.datacube.DataCube` class, so that you can just do:: + + predicted = cube.predict_random_forest( + model=training_job.job_id, + dimension="bands" + ) + + predicted.download("predicted.GTiff") + + +We specified the model here by batch job id (string), +but it can also be specified in other ways: +as :py:class:`~openeo.rest.job.BatchJob` instance, +as URL to the corresponding STAC Item that implements the `ml-model` extension, +or as :py:class:`~openeo.rest.mlmodel.MlModel` instance (e.g. loaded through +:py:meth:`~openeo.rest.connection.Connection.load_ml_model`). diff --git a/_sources/process_mapping.rst.txt b/_sources/process_mapping.rst.txt new file mode 100644 index 000000000..60519285c --- /dev/null +++ b/_sources/process_mapping.rst.txt @@ -0,0 +1,332 @@ + +.. + !Warning! This is an auto-generated file. + Do not edit directly. + Generated from: ['docs/process_mapping.py'] + +.. _openeo_process_mapping: + +openEO Process Mapping +####################### + +The table below maps openEO processes to the corresponding +method or function in the openEO Python Client Library. + +.. list-table:: + :header-rows: 1 + + * - openEO process + - openEO Python Client Method + + * - `absolute `_ + - :py:meth:`ProcessBuilder.absolute() `, :py:meth:`absolute() ` + * - `add `_ + - :py:meth:`ProcessBuilder.__add__() `, :py:meth:`ProcessBuilder.__radd__() `, :py:meth:`ProcessBuilder.add() `, :py:meth:`add() `, :py:meth:`DataCube.add() `, :py:meth:`DataCube.__add__() `, :py:meth:`DataCube.__radd__() ` + * - `add_dimension `_ + - :py:meth:`ProcessBuilder.add_dimension() `, :py:meth:`add_dimension() `, :py:meth:`DataCube.add_dimension() ` + * - `aggregate_spatial `_ + - :py:meth:`ProcessBuilder.aggregate_spatial() `, :py:meth:`aggregate_spatial() `, :py:meth:`DataCube.aggregate_spatial() ` + * - `aggregate_spatial_window `_ + - :py:meth:`ProcessBuilder.aggregate_spatial_window() `, :py:meth:`aggregate_spatial_window() `, :py:meth:`DataCube.aggregate_spatial_window() ` + * - `aggregate_temporal `_ + - :py:meth:`ProcessBuilder.aggregate_temporal() `, :py:meth:`aggregate_temporal() `, :py:meth:`DataCube.aggregate_temporal() ` + * - `aggregate_temporal_period `_ + - :py:meth:`ProcessBuilder.aggregate_temporal_period() `, :py:meth:`aggregate_temporal_period() `, :py:meth:`DataCube.aggregate_temporal_period() ` + * - `all `_ + - :py:meth:`ProcessBuilder.all() `, :py:meth:`all() ` + * - `and `_ + - :py:meth:`DataCube.logical_and() `, :py:meth:`DataCube.__and__() ` + * - `and_ `_ + - :py:meth:`ProcessBuilder.and_() `, :py:meth:`and_() ` + * - `anomaly `_ + - :py:meth:`ProcessBuilder.anomaly() `, :py:meth:`anomaly() ` + * - `any `_ + - :py:meth:`ProcessBuilder.any() `, :py:meth:`any() ` + * - `apply `_ + - :py:meth:`ProcessBuilder.apply() `, :py:meth:`apply() `, :py:meth:`DataCube.apply() ` + * - `apply_dimension `_ + - :py:meth:`ProcessBuilder.apply_dimension() `, :py:meth:`apply_dimension() `, :py:meth:`DataCube.apply_dimension() ` + * - `apply_kernel `_ + - :py:meth:`ProcessBuilder.apply_kernel() `, :py:meth:`apply_kernel() `, :py:meth:`DataCube.apply_kernel() ` + * - `apply_neighborhood `_ + - :py:meth:`ProcessBuilder.apply_neighborhood() `, :py:meth:`apply_neighborhood() `, :py:meth:`DataCube.apply_neighborhood() ` + * - `arccos `_ + - :py:meth:`ProcessBuilder.arccos() `, :py:meth:`arccos() ` + * - `arcosh `_ + - :py:meth:`ProcessBuilder.arcosh() `, :py:meth:`arcosh() ` + * - `arcsin `_ + - :py:meth:`ProcessBuilder.arcsin() `, :py:meth:`arcsin() ` + * - `arctan `_ + - :py:meth:`ProcessBuilder.arctan() `, :py:meth:`arctan() ` + * - `arctan2 `_ + - :py:meth:`ProcessBuilder.arctan2() `, :py:meth:`arctan2() ` + * - `ard_normalized_radar_backscatter `_ + - :py:meth:`ProcessBuilder.ard_normalized_radar_backscatter() `, :py:meth:`ard_normalized_radar_backscatter() `, :py:meth:`DataCube.ard_normalized_radar_backscatter() ` + * - `ard_surface_reflectance `_ + - :py:meth:`ProcessBuilder.ard_surface_reflectance() `, :py:meth:`ard_surface_reflectance() `, :py:meth:`DataCube.ard_surface_reflectance() ` + * - `array_append `_ + - :py:meth:`ProcessBuilder.array_append() `, :py:meth:`array_append() ` + * - `array_apply `_ + - :py:meth:`ProcessBuilder.array_apply() `, :py:meth:`array_apply() ` + * - `array_concat `_ + - :py:meth:`ProcessBuilder.array_concat() `, :py:meth:`array_concat() ` + * - `array_contains `_ + - :py:meth:`ProcessBuilder.array_contains() `, :py:meth:`array_contains() ` + * - `array_create `_ + - :py:meth:`ProcessBuilder.array_create() `, :py:meth:`array_create() ` + * - `array_create_labeled `_ + - :py:meth:`ProcessBuilder.array_create_labeled() `, :py:meth:`array_create_labeled() ` + * - `array_element `_ + - :py:meth:`ProcessBuilder.__getitem__() `, :py:meth:`ProcessBuilder.array_element() `, :py:meth:`array_element() ` + * - `array_filter `_ + - :py:meth:`ProcessBuilder.array_filter() `, :py:meth:`array_filter() ` + * - `array_find `_ + - :py:meth:`ProcessBuilder.array_find() `, :py:meth:`array_find() ` + * - `array_find_label `_ + - :py:meth:`ProcessBuilder.array_find_label() `, :py:meth:`array_find_label() ` + * - `array_interpolate_linear `_ + - :py:meth:`ProcessBuilder.array_interpolate_linear() `, :py:meth:`array_interpolate_linear() ` + * - `array_labels `_ + - :py:meth:`ProcessBuilder.array_labels() `, :py:meth:`array_labels() ` + * - `array_modify `_ + - :py:meth:`ProcessBuilder.array_modify() `, :py:meth:`array_modify() ` + * - `arsinh `_ + - :py:meth:`ProcessBuilder.arsinh() `, :py:meth:`arsinh() ` + * - `artanh `_ + - :py:meth:`ProcessBuilder.artanh() `, :py:meth:`artanh() ` + * - `atmospheric_correction `_ + - :py:meth:`ProcessBuilder.atmospheric_correction() `, :py:meth:`atmospheric_correction() `, :py:meth:`DataCube.atmospheric_correction() ` + * - `between `_ + - :py:meth:`ProcessBuilder.between() `, :py:meth:`between() ` + * - `ceil `_ + - :py:meth:`ProcessBuilder.ceil() `, :py:meth:`ceil() ` + * - `climatological_normal `_ + - :py:meth:`ProcessBuilder.climatological_normal() `, :py:meth:`climatological_normal() ` + * - `clip `_ + - :py:meth:`ProcessBuilder.clip() `, :py:meth:`clip() ` + * - `cloud_detection `_ + - :py:meth:`ProcessBuilder.cloud_detection() `, :py:meth:`cloud_detection() ` + * - `constant `_ + - :py:meth:`ProcessBuilder.constant() `, :py:meth:`constant() ` + * - `cos `_ + - :py:meth:`ProcessBuilder.cos() `, :py:meth:`cos() ` + * - `cosh `_ + - :py:meth:`ProcessBuilder.cosh() `, :py:meth:`cosh() ` + * - `count `_ + - :py:meth:`ProcessBuilder.count() `, :py:meth:`count() `, :py:meth:`DataCube.count_time() ` + * - `create_raster_cube `_ + - :py:meth:`ProcessBuilder.create_raster_cube() `, :py:meth:`create_raster_cube() ` + * - `cummax `_ + - :py:meth:`ProcessBuilder.cummax() `, :py:meth:`cummax() ` + * - `cummin `_ + - :py:meth:`ProcessBuilder.cummin() `, :py:meth:`cummin() ` + * - `cumproduct `_ + - :py:meth:`ProcessBuilder.cumproduct() `, :py:meth:`cumproduct() ` + * - `cumsum `_ + - :py:meth:`ProcessBuilder.cumsum() `, :py:meth:`cumsum() ` + * - `date_shift `_ + - :py:meth:`ProcessBuilder.date_shift() `, :py:meth:`date_shift() ` + * - `dimension_labels `_ + - :py:meth:`ProcessBuilder.dimension_labels() `, :py:meth:`dimension_labels() `, :py:meth:`DataCube.dimension_labels() ` + * - `divide `_ + - :py:meth:`ProcessBuilder.__truediv__() `, :py:meth:`ProcessBuilder.__rtruediv__() `, :py:meth:`ProcessBuilder.divide() `, :py:meth:`divide() `, :py:meth:`DataCube.divide() `, :py:meth:`DataCube.__truediv__() `, :py:meth:`DataCube.__rtruediv__() ` + * - `drop_dimension `_ + - :py:meth:`ProcessBuilder.drop_dimension() `, :py:meth:`drop_dimension() `, :py:meth:`DataCube.drop_dimension() ` + * - `e `_ + - :py:meth:`ProcessBuilder.e() `, :py:meth:`e() ` + * - `eq `_ + - :py:meth:`ProcessBuilder.__eq__() `, :py:meth:`ProcessBuilder.eq() `, :py:meth:`eq() `, :py:meth:`DataCube.__eq__() ` + * - `exp `_ + - :py:meth:`ProcessBuilder.exp() `, :py:meth:`exp() ` + * - `extrema `_ + - :py:meth:`ProcessBuilder.extrema() `, :py:meth:`extrema() ` + * - `filter_bands `_ + - :py:meth:`ProcessBuilder.filter_bands() `, :py:meth:`filter_bands() `, :py:meth:`DataCube.filter_bands() ` + * - `filter_bbox `_ + - :py:meth:`ProcessBuilder.filter_bbox() `, :py:meth:`filter_bbox() `, :py:meth:`DataCube.filter_bbox() ` + * - `filter_labels `_ + - :py:meth:`ProcessBuilder.filter_labels() `, :py:meth:`filter_labels() ` + * - `filter_spatial `_ + - :py:meth:`ProcessBuilder.filter_spatial() `, :py:meth:`filter_spatial() `, :py:meth:`DataCube.filter_spatial() ` + * - `filter_temporal `_ + - :py:meth:`ProcessBuilder.filter_temporal() `, :py:meth:`filter_temporal() `, :py:meth:`DataCube.filter_temporal() ` + * - `first `_ + - :py:meth:`ProcessBuilder.first() `, :py:meth:`first() ` + * - `fit_class_random_forest `_ + - :py:meth:`ProcessBuilder.fit_class_random_forest() `, :py:meth:`fit_class_random_forest() `, :py:meth:`VectorCube.fit_class_random_forest() ` + * - `fit_curve `_ + - :py:meth:`ProcessBuilder.fit_curve() `, :py:meth:`fit_curve() `, :py:meth:`DataCube.fit_curve() ` + * - `fit_regr_random_forest `_ + - :py:meth:`ProcessBuilder.fit_regr_random_forest() `, :py:meth:`fit_regr_random_forest() `, :py:meth:`VectorCube.fit_regr_random_forest() ` + * - `flatten_dimensions `_ + - :py:meth:`ProcessBuilder.flatten_dimensions() `, :py:meth:`flatten_dimensions() `, :py:meth:`DataCube.flatten_dimensions() ` + * - `floor `_ + - :py:meth:`ProcessBuilder.floor() `, :py:meth:`floor() ` + * - `ge `_ + - :py:meth:`ProcessBuilder.__ge__() `, :py:meth:`DataCube.__ge__() ` + * - `gt `_ + - :py:meth:`ProcessBuilder.__gt__() `, :py:meth:`ProcessBuilder.gt() `, :py:meth:`gt() `, :py:meth:`DataCube.__gt__() ` + * - `gte `_ + - :py:meth:`ProcessBuilder.gte() `, :py:meth:`gte() ` + * - `if_ `_ + - :py:meth:`ProcessBuilder.if_() `, :py:meth:`if_() ` + * - `inspect `_ + - :py:meth:`ProcessBuilder.inspect() `, :py:meth:`inspect() ` + * - `int `_ + - :py:meth:`ProcessBuilder.int() `, :py:meth:`int() ` + * - `is_infinite `_ + - :py:meth:`ProcessBuilder.is_infinite() `, :py:meth:`is_infinite() ` + * - `is_nan `_ + - :py:meth:`ProcessBuilder.is_nan() `, :py:meth:`is_nan() ` + * - `is_nodata `_ + - :py:meth:`ProcessBuilder.is_nodata() `, :py:meth:`is_nodata() ` + * - `is_valid `_ + - :py:meth:`ProcessBuilder.is_valid() `, :py:meth:`is_valid() ` + * - `last `_ + - :py:meth:`ProcessBuilder.last() `, :py:meth:`last() ` + * - `le `_ + - :py:meth:`DataCube.__le__() ` + * - `linear_scale_range `_ + - :py:meth:`ProcessBuilder.linear_scale_range() `, :py:meth:`linear_scale_range() `, :py:meth:`DataCube.linear_scale_range() ` + * - `ln `_ + - :py:meth:`ProcessBuilder.ln() `, :py:meth:`ln() `, :py:meth:`DataCube.ln() ` + * - `load_collection `_ + - :py:meth:`ProcessBuilder.load_collection() `, :py:meth:`load_collection() `, :py:meth:`DataCube.load_collection() `, :py:meth:`Connection.load_collection() ` + * - `load_geojson `_ + - :py:meth:`VectorCube.load_geojson() `, :py:meth:`Connection.load_geojson() ` + * - `load_ml_model `_ + - :py:meth:`ProcessBuilder.load_ml_model() `, :py:meth:`load_ml_model() `, :py:meth:`MlModel.load_ml_model() ` + * - `load_result `_ + - :py:meth:`ProcessBuilder.load_result() `, :py:meth:`load_result() `, :py:meth:`Connection.load_result() ` + * - `load_stac `_ + - :py:meth:`Connection.load_stac() ` + * - `load_uploaded_files `_ + - :py:meth:`ProcessBuilder.load_uploaded_files() `, :py:meth:`load_uploaded_files() ` + * - `log `_ + - :py:meth:`ProcessBuilder.log() `, :py:meth:`log() `, :py:meth:`DataCube.logarithm() `, :py:meth:`DataCube.log2() `, :py:meth:`DataCube.log10() ` + * - `lt `_ + - :py:meth:`ProcessBuilder.__lt__() `, :py:meth:`ProcessBuilder.lt() `, :py:meth:`lt() `, :py:meth:`DataCube.__lt__() ` + * - `lte `_ + - :py:meth:`ProcessBuilder.__le__() `, :py:meth:`ProcessBuilder.lte() `, :py:meth:`lte() ` + * - `mask `_ + - :py:meth:`ProcessBuilder.mask() `, :py:meth:`mask() `, :py:meth:`DataCube.mask() ` + * - `mask_polygon `_ + - :py:meth:`ProcessBuilder.mask_polygon() `, :py:meth:`mask_polygon() `, :py:meth:`DataCube.mask_polygon() ` + * - `max `_ + - :py:meth:`ProcessBuilder.max() `, :py:meth:`max() `, :py:meth:`DataCube.max_time() ` + * - `mean `_ + - :py:meth:`ProcessBuilder.mean() `, :py:meth:`mean() `, :py:meth:`DataCube.mean_time() ` + * - `median `_ + - :py:meth:`ProcessBuilder.median() `, :py:meth:`median() `, :py:meth:`DataCube.median_time() ` + * - `merge_cubes `_ + - :py:meth:`ProcessBuilder.merge_cubes() `, :py:meth:`merge_cubes() `, :py:meth:`DataCube.merge_cubes() ` + * - `min `_ + - :py:meth:`ProcessBuilder.min() `, :py:meth:`min() `, :py:meth:`DataCube.min_time() ` + * - `mod `_ + - :py:meth:`ProcessBuilder.mod() `, :py:meth:`mod() ` + * - `multiply `_ + - :py:meth:`ProcessBuilder.__mul__() `, :py:meth:`ProcessBuilder.__rmul__() `, :py:meth:`ProcessBuilder.__neg__() `, :py:meth:`ProcessBuilder.multiply() `, :py:meth:`multiply() `, :py:meth:`DataCube.multiply() `, :py:meth:`DataCube.__neg__() `, :py:meth:`DataCube.__mul__() `, :py:meth:`DataCube.__rmul__() ` + * - `nan `_ + - :py:meth:`ProcessBuilder.nan() `, :py:meth:`nan() ` + * - `ndvi `_ + - :py:meth:`ProcessBuilder.ndvi() `, :py:meth:`ndvi() `, :py:meth:`DataCube.ndvi() ` + * - `neq `_ + - :py:meth:`ProcessBuilder.__ne__() `, :py:meth:`ProcessBuilder.neq() `, :py:meth:`neq() `, :py:meth:`DataCube.__ne__() ` + * - `normalized_difference `_ + - :py:meth:`ProcessBuilder.normalized_difference() `, :py:meth:`normalized_difference() `, :py:meth:`DataCube.normalized_difference() ` + * - `not `_ + - :py:meth:`DataCube.__invert__() ` + * - `not_ `_ + - :py:meth:`ProcessBuilder.not_() `, :py:meth:`not_() ` + * - `or `_ + - :py:meth:`DataCube.logical_or() `, :py:meth:`DataCube.__or__() ` + * - `or_ `_ + - :py:meth:`ProcessBuilder.or_() `, :py:meth:`or_() ` + * - `order `_ + - :py:meth:`ProcessBuilder.order() `, :py:meth:`order() ` + * - `pi `_ + - :py:meth:`ProcessBuilder.pi() `, :py:meth:`pi() ` + * - `power `_ + - :py:meth:`ProcessBuilder.__pow__() `, :py:meth:`ProcessBuilder.power() `, :py:meth:`power() `, :py:meth:`DataCube.__rpow__() `, :py:meth:`DataCube.__pow__() `, :py:meth:`DataCube.power() ` + * - `predict_curve `_ + - :py:meth:`ProcessBuilder.predict_curve() `, :py:meth:`predict_curve() `, :py:meth:`DataCube.predict_curve() ` + * - `predict_random_forest `_ + - :py:meth:`ProcessBuilder.predict_random_forest() `, :py:meth:`predict_random_forest() `, :py:meth:`DataCube.predict_random_forest() ` + * - `product `_ + - :py:meth:`ProcessBuilder.product() `, :py:meth:`product() ` + * - `quantiles `_ + - :py:meth:`ProcessBuilder.quantiles() `, :py:meth:`quantiles() ` + * - `rearrange `_ + - :py:meth:`ProcessBuilder.rearrange() `, :py:meth:`rearrange() ` + * - `reduce_dimension `_ + - :py:meth:`ProcessBuilder.reduce_dimension() `, :py:meth:`reduce_dimension() `, :py:meth:`DataCube.reduce_dimension() ` + * - `reduce_spatial `_ + - :py:meth:`ProcessBuilder.reduce_spatial() `, :py:meth:`reduce_spatial() ` + * - `rename_dimension `_ + - :py:meth:`ProcessBuilder.rename_dimension() `, :py:meth:`rename_dimension() `, :py:meth:`DataCube.rename_dimension() ` + * - `rename_labels `_ + - :py:meth:`ProcessBuilder.rename_labels() `, :py:meth:`rename_labels() `, :py:meth:`DataCube.rename_labels() ` + * - `resample_cube_spatial `_ + - :py:meth:`ProcessBuilder.resample_cube_spatial() `, :py:meth:`resample_cube_spatial() ` + * - `resample_cube_temporal `_ + - :py:meth:`ProcessBuilder.resample_cube_temporal() `, :py:meth:`resample_cube_temporal() `, :py:meth:`DataCube.resample_cube_temporal() ` + * - `resample_spatial `_ + - :py:meth:`ProcessBuilder.resample_spatial() `, :py:meth:`resample_spatial() `, :py:meth:`DataCube.resample_spatial() ` + * - `resolution_merge `_ + - :py:meth:`DataCube.resolution_merge() ` + * - `round `_ + - :py:meth:`ProcessBuilder.round() `, :py:meth:`round() ` + * - `run_udf `_ + - :py:meth:`ProcessBuilder.run_udf() `, :py:meth:`run_udf() `, :py:meth:`VectorCube.run_udf() ` + * - `run_udf_externally `_ + - :py:meth:`ProcessBuilder.run_udf_externally() `, :py:meth:`run_udf_externally() ` + * - `sar_backscatter `_ + - :py:meth:`ProcessBuilder.sar_backscatter() `, :py:meth:`sar_backscatter() `, :py:meth:`DataCube.sar_backscatter() ` + * - `save_ml_model `_ + - :py:meth:`ProcessBuilder.save_ml_model() `, :py:meth:`save_ml_model() ` + * - `save_result `_ + - :py:meth:`ProcessBuilder.save_result() `, :py:meth:`save_result() `, :py:meth:`VectorCube.save_result() `, :py:meth:`DataCube.save_result() ` + * - `sd `_ + - :py:meth:`ProcessBuilder.sd() `, :py:meth:`sd() ` + * - `sgn `_ + - :py:meth:`ProcessBuilder.sgn() `, :py:meth:`sgn() ` + * - `sin `_ + - :py:meth:`ProcessBuilder.sin() `, :py:meth:`sin() ` + * - `sinh `_ + - :py:meth:`ProcessBuilder.sinh() `, :py:meth:`sinh() ` + * - `sort `_ + - :py:meth:`ProcessBuilder.sort() `, :py:meth:`sort() ` + * - `sqrt `_ + - :py:meth:`ProcessBuilder.sqrt() `, :py:meth:`sqrt() ` + * - `subtract `_ + - :py:meth:`ProcessBuilder.__sub__() `, :py:meth:`ProcessBuilder.__rsub__() `, :py:meth:`ProcessBuilder.subtract() `, :py:meth:`subtract() `, :py:meth:`DataCube.subtract() `, :py:meth:`DataCube.__sub__() `, :py:meth:`DataCube.__rsub__() ` + * - `sum `_ + - :py:meth:`ProcessBuilder.sum() `, :py:meth:`sum() ` + * - `tan `_ + - :py:meth:`ProcessBuilder.tan() `, :py:meth:`tan() ` + * - `tanh `_ + - :py:meth:`ProcessBuilder.tanh() `, :py:meth:`tanh() ` + * - `text_begins `_ + - :py:meth:`ProcessBuilder.text_begins() `, :py:meth:`text_begins() ` + * - `text_concat `_ + - :py:meth:`ProcessBuilder.text_concat() `, :py:meth:`text_concat() ` + * - `text_contains `_ + - :py:meth:`ProcessBuilder.text_contains() `, :py:meth:`text_contains() ` + * - `text_ends `_ + - :py:meth:`ProcessBuilder.text_ends() `, :py:meth:`text_ends() ` + * - `trim_cube `_ + - :py:meth:`ProcessBuilder.trim_cube() `, :py:meth:`trim_cube() ` + * - `unflatten_dimension `_ + - :py:meth:`ProcessBuilder.unflatten_dimension() `, :py:meth:`unflatten_dimension() `, :py:meth:`DataCube.unflatten_dimension() ` + * - `variance `_ + - :py:meth:`ProcessBuilder.variance() `, :py:meth:`variance() ` + * - `vector_buffer `_ + - :py:meth:`ProcessBuilder.vector_buffer() `, :py:meth:`vector_buffer() ` + * - `vector_to_random_points `_ + - :py:meth:`ProcessBuilder.vector_to_random_points() `, :py:meth:`vector_to_random_points() ` + * - `vector_to_regular_points `_ + - :py:meth:`ProcessBuilder.vector_to_regular_points() `, :py:meth:`vector_to_regular_points() ` + * - `xor `_ + - :py:meth:`ProcessBuilder.xor() `, :py:meth:`xor() ` + +:subscript:`(Table autogenerated on 2023-08-07)` diff --git a/_sources/processes.rst.txt b/_sources/processes.rst.txt new file mode 100644 index 000000000..b81db1c53 --- /dev/null +++ b/_sources/processes.rst.txt @@ -0,0 +1,465 @@ +*********************** +Working with processes +*********************** + +In openEO, a **process** is an operation that performs a specific task on +a set of parameters and returns a result. +For example, with the ``add`` process you can add two numbers, in openEO's JSON notation:: + + { + "process_id": "add", + "arguments": {"x": 3, "y": 5} + } + + +A process is similar to a *function* in common programming languages, +and likewise, multiple processes can be combined or chained together +into new, more complex operations. + +A bit of terminology +==================== + +A **pre-defined process** is a process provided out of the box by a given *back-end*. +These are often the `centrally defined openEO processes `_, +such as common mathematical (``sum``, ``divide``, ``sqrt``, ...), +statistical (``mean``, ``max``, ...) and +image processing (``mask``, ``apply_kernel``, ...) +operations. +Back-ends are expected to support most of these standard ones, +but are free to pre-define additional ones too. + + +Processes can be combined into a larger pipeline, parameterized +and stored on the back-end as a so called **user-defined process**. +This allows you to build a library of reusable building blocks +that can be be inserted easily in multiple other places. +See :ref:`user-defined-processes` for more information. + + +How processes are combined into a larger unit +is internally represented by a so-called **process graph**. +It describes how the inputs and outputs of processes +should be linked together. +A user of the Python client should normally not worry about +the details of a process graph structure, as most of these aspects +are hidden behind regular Python functions, classes and methods. + + + +Using common pre-defined processes +=================================== + +The listing of pre-defined processes provided by a back-end +can be inspected with :func:`~openeo.rest.connection.Connection.list_processes`. +For example, to get a list of the process names (process ids):: + + >>> process_ids = [process["id"] for process in connection.list_processes()] + >>> print(process_ids[:16]) + ['arccos', 'arcosh', 'power', 'last', 'subtract', 'not', 'cosh', 'artanh', + 'is_valid', 'first', 'median', 'eq', 'absolute', 'arctan2', 'divide','is_nan'] + +More information about the processes, like a description +or expected parameters, can be queried like that, +but it is often easier to look them up on the +`official openEO process documentation `_ + +A single pre-defined process can be retrieved with +:func:`~openeo.rest.connection.Connection.describe_process`. + +Convenience methods +-------------------- + +Most of the important pre-defined processes are covered directly by methods +on classes like :class:`~openeo.rest.datacube.DataCube` or +:class:`~openeo.rest.vectorcube.VectorCube`. + +.. seealso:: + See :ref:`openeo_process_mapping` for a mapping of openEO processes + the corresponding methods in the openEO Python Client library. + +For example, to apply the ``filter_temporal`` process to a raster data cube:: + + cube = cube.filter_temporal("2020-02-20", "2020-06-06") + +Being regular Python methods, you get usual function call features +you're accustomed to: default values, keyword arguments, ``kwargs`` usage, ... +For example, to use a bounding box dictionary with ``kwargs``-expansion:: + + bbox = { + "west": 5.05, "south": 51.20, "east": 5.10, "north": 51.23 + } + cube = cube.filter_bbox(**bbox) + +Note that some methods try to be more flexible and convenient to use +than how the official process definition prescribes. +For example, the ``filter_temporal`` process expects an ``extent`` array +with 2 items (the start and end date), +but you can call the corresponding client method in multiple equivalent ways:: + + cube.filter_temporal("2019-07-01", "2019-08-01") + cube.filter_temporal(["2019-07-01", "2019-08-01"]) + cube.filter_temporal(extent=["2019-07-01", "2019-08-01"]) + cube.filter_temporal(start_date="2019-07-01", end_date="2019-08-01"]) + + +Advanced argument tweaking +--------------------------- + +.. versionadded:: 0.10.1 + +In some situations, you may want to finetune what the (convenience) methods generate. +For example, you want to play with non-standard, experimental arguments, +or there is a problem with a automatic argument handling/conversion feature. + +You can tweak the arguments of your current result node as follows. +Say, we want to add some non-standard ``feature_flags`` argument to the ``load_collection`` process node. +We first get the current result node with :py:meth:`~openeo.rest.datacube.DataCube.result_node` and use :py:meth:`~openeo.internal.graph_building.PGNode.update_arguments` to add an additional argument to it:: + + # `Connection.load_collection` does not support `feature_flags` argument + cube = connection.load_collection(...) + + # Add `feature_flag` argument `load_collection` process graph node + cube.result_node().update_arguments(feature_flags="rXPk") + + # The resulting process graph will now contain this non-standard argument: + # { + # "process_id": "load_collection", + # "arguments": { + # ... + # "feature_flags": "rXPk", + + +Generic API for adding processes +================================= + +An openEO back-end may offer processes that are not part of the core API, +or the client may not (yet) have a corresponding method +for a process that you wish to use. +In that case, you can fall back to a more generic API +that allows you to add processes directly. + +Basics +------ + +To add a simple process to the graph, use +the :func:`~openeo.rest.datacube.DataCube.process` method +on a :class:`~openeo.rest.datacube.DataCube`. +You have to specify the process id and arguments +(as a single dictionary or through keyword arguments ``**kwargs``). +It will return a new DataCube with the new process appended +to the internal process graph. + +.. # TODO this example makes no sense: it uses cube for what? + +A very simple example using the ``mean`` process and a +literal list in an arguments dictionary:: + + arguments= { + "data": [1, 3, -1] + } + res = cube.process("mean", arguments) + +or equivalently, leveraging keyword arguments:: + + res = cube.process("mean", data=[1, 3, -1]) + + +Passing data cube arguments +---------------------------- + +The example above is a bit convoluted however in the sense that +you start from a given data cube ``cube``, you add a ``mean`` process +that works on a given data array, while completely ignoring the original cube. +In reality you typically want to apply the process on the cube. +This is possible by passing a data cube object directly as argument, +for example with the ``ndvi`` process that at least expects +a data cube as ``data`` argument :: + + res = cube.process("ndvi", data=cube) + + +Note that you have to specify ``cube`` twice here: +a first time to call the method and a second time as argument. +Moreover, it requires you to define a Python variable for the data +cube, which is annoying if you want to use a chained expressions. +To solve these issues, you can use the :const:`~openeo.rest.datacube.THIS` +constant as symbolic reference to the "current" cube:: + + from openeo.rest.datacube import THIS + + res = ( + cube + .process("filter_bands", data=THIS) + .process("mask", data=THIS, mask=mask) + .process("ndvi", data=THIS) + ) + + +Passing results from other process calls as arguments +------------------------------------------------------ + +Another use case of generically applying (custom) processes is +passing a process result as argument to another process working on a cube. +For example, assume we have a custom process ``load_my_vector_cube`` +to load a vector cube from an online resource. +We can use this vector cube as geometry for +:py:meth:`DataCube.aggregate_spatial() ` +using :py:func:`openeo.processes.process()` as follows: + + +.. code-block:: python + + from openeo.processes import process + + res = cube.aggregate_spatial( + geometries=process("load_my_vector_cube", url="https://geo.example/features.db"), + reducer="mean" + ) + + +.. _callbackfunctions: + +Processes with child "callbacks" +================================ + +Some openEO processes expect some kind of sub-process +to be invoked on a subset or slice of the datacube. +For example: + +* process ``apply`` requires a transformation that will be applied + to each pixel in the cube (separately), e.g. in pseudocode + + .. code-block:: text + + cube.apply( + given a pixel value + => scale it with factor 0.01 + ) + +* process ``reduce_dimension`` requires an aggregation function to convert + an array of pixel values (along a given dimension) to a single value, + e.g. in pseudocode + + .. code-block:: text + + cube.reduce_dimension( + given a pixel timeseries (array) for a (x,y)-location + => temporal mean of that array + ) + +* process ``aggregate_spatial`` requires a function to aggregate the values + in one or more geometries + +These transformation functions are usually called "**callbacks**" +because instead of being called explicitly by the user, +they are called and managed by their "parent" process +(the ``apply``, ``reduce_dimension`` and ``aggregate_spatial`` in the examples) + + +The openEO Python Client Library currently provides a couple of DataCube methods +that expect such a callback, most commonly: + +- :py:meth:`openeo.rest.datacube.DataCube.aggregate_spatial` +- :py:meth:`openeo.rest.datacube.DataCube.aggregate_temporal` +- :py:meth:`openeo.rest.datacube.DataCube.apply` +- :py:meth:`openeo.rest.datacube.DataCube.apply_dimension` +- :py:meth:`openeo.rest.datacube.DataCube.apply_neighborhood` +- :py:meth:`openeo.rest.datacube.DataCube.reduce_dimension` + +The openEO Python Client Library supports several ways +to specify the desired callback for these functions: + + +.. contents:: + :depth: 1 + :local: + :backlinks: top + +Callback as string +------------------ + +The easiest way is passing a process name as a string, +for example: + +.. code-block:: python + + # Take the absolute value of each pixel + cube.apply("absolute") + + # Reduce a cube along the temporal dimension by taking the maximum value + cube.reduce_dimension(reducer="max", dimension="t") + +This approach is only possible if the desired transformation is available +as a single process. If not, use one of the methods below. + +It's also important to note that the "signature" of the provided callback process +should correspond properly with what the parent process expects. +For example: ``apply`` requires a callback process that receives a +number and returns one (like ``absolute`` or ``sqrt``), +while ``reduce_dimension`` requires a callback process that receives +an array of numbers and returns a single number (like ``max`` or ``mean``). + + +.. _child_callback_callable: + +Callback as a callable +----------------------- + +You can also specify the callback as a "callable": +which is a fancy word for a Python object that can be called, +but just think of it like a function you can call. + +You can use a regular Python function, like this: + +.. code-block:: python + + def transform(x): + return x * 2 + 3 + + cube.apply(transform) + +or, more compactly, a "lambda" +(a construct in Python to create anonymous inline functions): + +.. code-block:: python + + cube.apply(lambda x: x * 2 + 3) + + +The openEO Python Client Library implements most of the official openEO processes as +:ref:`functions in the "openeo.processes" module `, +which can be used directly as callback: + +.. code-block:: python + + from openeo.processes import absolute, max + + cube.apply(absolute) + cube.reduce_dimension(reducer=max, dimension="t") + + +The argument that will be passed to all these callback functions is +a :py:class:`ProcessBuilder ` instance. +This is a helper object with predefined methods for all standard openEO processes, +allowing to use an object oriented coding style to define the callback. +For example: + +.. code-block:: python + + from openeo.processes import ProcessBuilder + + def avg(data: ProcessBuilder): + return data.mean() + + cube.reduce_dimension(reducer=avg, dimension="t") + + +These methods also return :py:class:`ProcessBuilder ` objects, +which also allows writing callbacks in chained fashion: + +.. code-block:: python + + cube.apply( + lambda x: x.absolute().cos().add(y=1.23) + ) + + +All this gives a lot of flexibility to define callbacks compactly +in a desired coding style. +The following examples result in the same callback: + +.. code-block:: python + + from openeo.processes import ProcessBuilder, mean, cos, add + + # Chained methods + cube.reduce_dimension( + lambda data: data.mean().cos().add(y=1.23), + dimension="t" + ) + + # Functions + cube.reduce_dimension( + lambda data: add(x=cos(mean(data)), y=1.23), + dimension="t" + ) + + # Mixing methods, functions and operators + cube.reduce_dimension( + lambda data: cos(data.mean())) + 1.23, + dimension="t" + ) + + +Caveats +```````` + +Specifying callbacks through Python functions (or lambdas) +looks intuitive and straightforward, but it should be noted +that not everything is allowed in these functions. +You should just limit yourself to calling +:py:mod:`openeo.processes` functions, +:py:class:`ProcessBuilder ` methods +and basic math operators. +Don't call functions from other libraries like numpy or scipy. +Don't use Python control flow statements like ``if/else`` constructs +or ``for`` loops. + +The reason for this is that the openEO Python Client Library +does not translate the function source code itself +to an openEO process graph. +Instead, when building the openEO process graph, +it passes a special object to the function +and keeps track of which :py:mod:`openeo.processes` functions +were called to assemble the corresponding process graph. +If you use control flow statements or use numpy functions for example, +this procedure will incorrectly detect what you want to do in the callback. + +For example, if you mistakenly use the Python builtin :py:func:`sum` function +in a callback instead of :py:func:`openeo.processes.sum`, you will run into trouble. +Luckily the openEO Python client Library should raise an error if it detects that:: + + >>> # Wrongly using builtin `sum` function + >>> cube.reduce_dimension(dimension="t", reducer=sum) + RuntimeError: Exceeded ProcessBuilder iteration limit. + Are you mistakenly using a builtin like `sum()` or `all()` in a callback + instead of the appropriate helpers from `openeo.processes`? + + >>> # Explicit usage of `openeo.processes.sum` + >>> import openeo.processes + >>> cube.reduce_dimension(dimension="t", reducer=openeo.processes.sum) + + + + +Callback as ``PGNode`` +----------------------- + +You can also pass a :py:class:`~openeo.internal.graph_building.PGNode` object as callback. + +.. attention:: + This approach should generally not be used in normal use cases. + The other options discussed above should be preferred. + It's mainly intended for internal use and an occasional, advanced use case. + It requires in-depth knowledge of the openEO API + and openEO Python Client Library to construct correctly. + +Some examples: + +.. code-block:: python + + from openeo.internal.graph_building import PGNode + + cube.apply(PGNode( + "add", + x=PGNode( + "cos", + x=PGNode("absolute", x={"from_parameter": "x"}) + ), + y=1.23 + )) + + cube.reduce_dimension( + reducer=PGNode("max", data={"from_parameter": "data"}), + dimension="bands" + ) diff --git a/_sources/udf.rst.txt b/_sources/udf.rst.txt new file mode 100644 index 000000000..5f7983764 --- /dev/null +++ b/_sources/udf.rst.txt @@ -0,0 +1,701 @@ +.. index:: User-defined functions +.. index:: UDF + +.. _user-defined-functions: + +###################################### +User-Defined Functions (UDF) explained +###################################### + + +While openEO supports a wide range of pre-defined processes +and allows to build more complex user-defined processes from them, +you sometimes need operations or algorithms that are +not (yet) available or standardized as openEO process. +**User-Defined Functions (UDF)** is an openEO feature +(through the `run_udf `_ process) +that aims to fill that gap by allowing a user to express (a part of) +an **algorithm as a Python/R/... script to be run back-end side**. + +There are a lot of details to cover, +but here is a rudimentary example snippet +to give you a quick impression of how to work with UDFs +using the openEO Python Client library: + +.. code-block:: python + :caption: Basic UDF usage example snippet to rescale pixel values + + import openeo + + # Build a UDF object from an inline string with Python source code. + udf = openeo.UDF(""" + import xarray + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + cube.values = 0.0001 * cube.values + return cube + """) + + # Or load the UDF code from a separate file. + # udf = openeo.UDF.from_file("udf-code.py") + + # Apply the UDF to a cube. + rescaled_cube = cube.apply(process=udf) + + +Ideally, it allows you to embed existing Python/R/... implementations +in an openEO workflow (with some necessary "glue code"). +However, it is recommended to try to do as much pre- or postprocessing +with pre-defined processes +before blindly copy-pasting source code snippets as UDFs. +Pre-defined processes are typically well-optimized by the backend, +while UDFs can come with a performance penalty +and higher development/debug/maintenance costs. + + +.. warning:: + + Don not confuse **user-defined functions** (abbreviated as UDF) with + **user-defined processes** (sometimes abbreviated as UDP) in openEO, + which is a way to define and use your own process graphs + as reusable building blocks. + See :ref:`user-defined-processes` for more information. + + + +Applicability and Constraints +============================== + +.. index:: chunking + +openEO is designed to work transparently on large data sets +and your UDF has to follow a couple of guidelines to make that possible. +First of all, as data cubes play a central role in openEO, +your UDF should accept and return correct **data cube structures**, +with proper dimensions, dimension labels, etc. +Moreover, the back-end will typically divide your input data cube +in smaller chunks and process these chunks separately (e.g. on isolated workers). +Consequently, it's important that your **UDF algorithm operates correctly +in such a chunked processing context**. + +A very common mistake is to use index-based array indexing, rather than name based. The index based approach +assumes that datacube dimension order is fixed, which is not guaranteed. Next to that, it also reduces the readability +of your code. Label based indexing is a great feature of xarray, and should be used whenever possible. + +As a rule of thumb, the UDF should preserve the dimensions and shape of the input +data cube. The datacube chunk that is passed on by the backend does not have a fixed +specification, so the UDF needs to be able to accomodate different shapes and sizes of the data. + +There's important exceptions to this rule, that depend on the context in which the UDF is used. +For instance, a UDF used as a reducer should effectively remove the reduced dimension from the +output chunk. These details are documented in the next sections. + +UDFs as apply/reduce "callbacks" +--------------------------------- + +UDFs are typically used as "callback" processes for "meta" processes +like ``apply`` or ``reduce_dimension`` (also see :ref:`callbackfunctions`). +These meta-processes make abstraction of a datacube as a whole +and allow the callback to focus on a small slice of data or a single dimension. +Their nature instructs the backend how the data should be processed +and can be chunked: + +`apply `_ + Applies a process on *each pixel separately*. + The back-end has all freedom to choose chunking + (e.g. chunk spatially and temporally). + Dimensions and their labels are fully preserved. + This function has limited practical use in combination with UDF's. + +`apply_dimension `_ + Applies a process to all pixels *along a given dimension* + to produce a new series of values for that dimension. + The back-end will not split your data on that dimension. + For example, when working along the time dimension, + your UDF is guaranteed to receive a full timeseries, + but the data could be chunked spatially. + All dimensions and labels are preserved, + except for the dimension along which ``apply_dimension`` is applied: + the number of dimension labels is allowed to change. + +`reduce_dimension `_ + Applies a process to all pixels *along a given dimension* + to produce a single value, eliminating that dimension. + Like with ``apply_dimension``, the back-end will + not split your data on that dimension. + The dimension along which ``apply_dimension`` is applied must be removed + from the output. + For example, when applying ``reduce_dimension`` on a spatiotemporal cube + along the time dimension, + the UDF is guaranteed to receive full timeseries + (but the data could be chunked spatially) + and the output cube should only be a spatial cube, without a temporal dimension + +`apply_neighborhood `_ + Applies a process to a neighborhood of pixels + in a sliding-window fashion with (optional) overlap. + Data chunking in this case is explicitly controlled by the user. + Dimensions and number of labels are fully preserved. This is the most versatile + and widely used function to work with UDF's. + + + +UDF function names and signatures +================================== + +The UDF code you pass to the back-end is basically a Python script +that contains one or more functions. +Exactly one of these functions should have a proper UDF signature, +as defined in the :py:mod:`openeo.udf.udf_signatures` module, +so that the back-end knows what the *entrypoint* function is +of your UDF implementation. + + +Module ``openeo.udf.udf_signatures`` +------------------------------------- + + +.. automodule:: openeo.udf.udf_signatures + :members: + + + +.. _udf_example_apply: + +A first example: ``apply`` with an UDF to rescale pixel values +================================================================ + +In most of the examples here, we will start from an initial Sentinel2 data cube like this: + +.. code-block:: python + + s2_cube = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 4.00, "south": 51.04, "east": 4.10, "north": 51.1}, + temporal_extent=["2022-03-01", "2022-03-31"], + bands=["B02", "B03", "B04"] + ) + + +The raw values in this initial ``s2_cube`` data cube are **digital numbers** +(integer values ranging from 0 to several thousands) +and to get **physical reflectance** values (float values, typically in the range between 0 and 0.5), +we have to rescale them. +This is a simple local transformation, without any interaction between pixels, +which is the modus operandi of the ``apply`` processes. + +.. note:: + + In practice it will be a lot easier and more efficient to do this kind of rescaling + with pre-defined openEO math processes, for example: ``s2_cube.apply(lambda x: 0.0001 * x)``. + This is just a very simple illustration to get started with UDFs. In fact, it's very likely that + you will never want to use a UDF with apply. + +UDF script +---------- + +The UDF code is this short script (the part that does the actual value rescaling is highlighted): + +.. code-block:: python + :linenos: + :caption: ``udf-code.py`` + :emphasize-lines: 5 + + import xarray + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + cube.values = 0.0001 * cube.values + return cube + +Some details about this UDF script: + +- line 1: We import `xarray` as we use this as exchange format. +- line 3: We define a function named ``apply_datacube``, + which receives and returns a :py:class:`~xarray.DataArray` instance. + We follow here the :py:meth:`~openeo.udf.udf_signatures.apply_datacube()` UDF function signature. +- line 4: Because our scaling operation is so simple, we can transform the ``xarray.DataArray`` values in-place. +- line 5: Consequently, because the values were updated in-place, we can return the same Xarray object. + +Workflow script +---------------- + +In this first example, we'll cite a full, standalone openEO workflow script, +including creating the back-end connection, loading the initial data cube and downloading the result. +The UDF-specific part is highlighted. + +.. warning:: + This implementation depends on :py:class:`openeo.UDF ` improvements + that were introduced in version 0.13.0 of the openeo Python Client Library. + If you are currently stuck with working with an older version, + check :ref:`old_udf_api` for more information on the difference with the old API. + +.. code-block:: python + :linenos: + :caption: UDF usage example snippet + :emphasize-lines: 14-25 + + import openeo + + # Create connection to openEO back-end + connection = openeo.connect("...").authenticate_oidc() + + # Load initial data cube. + s2_cube = connection.load_collection( + "SENTINEL2_L2A", + spatial_extent={"west": 4.00, "south": 51.04, "east": 4.10, "north": 51.1}, + temporal_extent=["2022-03-01", "2022-03-31"], + bands=["B02", "B03", "B04"] + ) + + # Create a UDF object from inline source code. + udf = openeo.UDF(""" + import xarray + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + cube.values = 0.0001 * cube.values + return cube + """) + + # Pass UDF object as child process to `apply`. + rescaled = s2_cube.apply(process=udf) + + rescaled.download("apply-udf-scaling.nc") + +In line 15, we build an :py:class:`openeo.UDF ` object +from an inline string with the UDF source code. +This :py:class:`openeo.UDF ` object encapsulates various aspects +that are necessary to create a ``run_udf`` node in the process graph, +and we can pass it directly in line 25 as the ``process`` argument +to :py:meth:`DataCube.apply() `. + +.. tip:: + + Instead of putting your UDF code in an inline string like in the example, + it's often a good idea to **load the UDF code from a separate file**, + which is easier to maintain in your preferred editor or IDE. + You can do that directly with the + :py:meth:`openeo.UDF.from_file ` method: + + .. code-block:: python + + udf = openeo.UDF.from_file("udf-code.py") + +After downloading the result, we can inspect the band values locally. +Note see that they fall mainly in a range from 0 to 1 (in most cases even below 0.2), +instead of the original digital number range (thousands): + +.. image:: _static/images/udf/apply-rescaled-histogram.png + + +UDF's that transform cube metadata +================================== +This is a new/experimental feature so may still be subject to change. + +In some cases, a UDF can have impact on the metadata of a cube, but this can not always +be easily inferred by process graph evaluation logic without running the actual +(expensive) UDF code. This limits the possibilities to validate process graphs, +or for instance make an estimate of the size of a datacube after applying a UDF. + +To provide evaluation logic with this information, the user should implement the +:py:meth:`~openeo.udf.udf_signatures.apply_metadata()` function as part of the UDF. +Please refer to the documentation of that function for more information. + +.. literalinclude:: ../examples/udf/udf_modify_spatial.py + :language: python + :caption: Example of a UDF that adjusts spatial metadata ``udf_modify_spatial.py`` + :name: spatial_udf + +To invoke a UDF like this, the apply_neighborhood method is most suitable: + +.. code-block:: python + + udf_code = Path('udf_modify_spatial.py').read_text() + cube_updated = cube.apply_neighborhood( + lambda data: data.run_udf(udf=udf_code, runtime='Python-Jep', context=dict()), + size=[ + {'dimension': 'x', 'value': 128, 'unit': 'px'}, + {'dimension': 'y', 'value': 128, 'unit': 'px'} + ], overlap=[]) + + + +Example: ``apply_dimension`` with a UDF +======================================== + +This is useful when running custom code over all band values for a given pixel or all observations per pixel. +See section below 'Smoothing timeseries with a user defined function' for a concrete example. + +Example: ``reduce_dimension`` with a UDF +======================================== + +The key element for a UDF invoked in the context of `reduce_dimension` is that it should actually return +an Xarray DataArray _without_ the dimension that is specified to be reduced. + +So a reduce over time would receive a DataArray with `bands,t,y,x` dimensions, and return one with only `bands,y,x`. + + +Example: ``apply_neighborhood`` with a UDF +=========================================== + +The apply_neighborhood process is generally used when working with complex AI models that require a +spatiotemporal input stack with a fixed size. It supports the ability to specify overlap, to ensure that the model +has sufficient border information to generate a spatially coherent output across chunks of the raster data cube. + +In the example below, the UDF will receive chunks of 128x128 pixels: 112 is the chunk size, while 2 times 8 pixels of +overlap on each side of the chunk results in 128. + +The time and band dimensions are not specified, which means that all values along these dimensions are passed into +the datacube. + + +.. code-block:: python + + output_cube = inputs_cube.apply_neighborhood(my_udf, size=[ + {'dimension': 'x', 'value': 112, 'unit': 'px'}, + {'dimension': 'y', 'value': 112, 'unit': 'px'} + ], overlap=[ + {'dimension': 'x', 'value': 8, 'unit': 'px'}, + {'dimension': 'y', 'value': 8, 'unit': 'px'} + ]) + + + +.. warning:: + +The ``apply_neighborhood`` is the most versatile, but also most complex process. Make sure to keep an eye on the dimensions +and the shape of the DataArray returned by your UDF. For instance, a very common error is to somehow 'flip' the spatial dimensions. +Debugging the UDF locally can help, but then you will want to try and reproduce the input that you get also on the backend. +This can typically be achieved by using logging to inspect the DataArrays passed into your UDF backend side. + + + +Example: Smoothing timeseries with a user defined function (UDF) +================================================================== + +In this example, we start from the ``evi_cube`` that was created in the previous example, and want to +apply a temporal smoothing on it. More specifically, we want to use the "Savitzky Golay" smoother +that is available in the SciPy Python library. + + +To ensure that openEO understand your function, it needs to follow some rules, the UDF specification. +This is an example that follows those rules: + +.. literalinclude:: ../examples/udf/smooth_savitzky_golay.py + :language: python + :caption: Example UDF code ``smooth_savitzky_golay.py`` + :name: savgol_udf + +The method signature of the UDF is very important, because the back-end will use it to detect +the type of UDF. +This particular example accepts a :py:class:`~openeo.rest.datacube.DataCube` object as input and also returns a :py:class:`~openeo.rest.datacube.DataCube` object. +The type annotations and method name are actually used to detect how to invoke the UDF, so make sure they remain unchanged. + + +Once the UDF is defined in a separate file, we load it +and apply it along a dimension: + +.. code-block:: python + + smoothing_udf = openeo.UDF.from_file('smooth_savitzky_golay.py') + smoothed_evi = evi_cube_masked.apply_dimension(smoothing_udf, dimension="t") + + +Downloading a datacube and executing an UDF locally +============================================================= + +Sometimes it is advantageous to run a UDF on the client machine (for example when developing/testing that UDF). +This is possible by using the convenience function :py:func:`openeo.udf.run_code.execute_local_udf`. +The steps to run a UDF (like the code from ``smooth_savitzky_golay.py`` above) are as follows: + +* Run the processes (or process graph) preceding the UDF and download the result in 'NetCDF' or 'JSON' format. +* Run :py:func:`openeo.udf.run_code.execute_local_udf` on the data file. + +For example:: + + from pathlib import Path + from openeo.udf import execute_local_udf + + my_process = connection.load_collection(... + + my_process.download('test_input.nc', format='NetCDF') + + smoothing_udf = Path('smooth_savitzky_golay.py').read_text() + execute_local_udf(smoothing_udf, 'test_input.nc', fmt='netcdf') + +Note: this algorithm's primary purpose is to aid client side development of UDFs using small datasets. It is not designed for large jobs. + +UDF dependency management +========================= + +UDFs usually have some dependencies on existing libraries, e.g. to implement complex algorithms. +In case of Python UDFs, it can be assumed that common libraries like numpy and Xarray are readily available, +not in the least because they underpin the Python UDF function signatures. +More concretely, it is possible to inspect available libraries for the available UDF runtimes +through :py:meth:`Connection.list_udf_runtimes()`. +For example, to list the available libraries for runtime "Python" (version "3"): + +.. code-block:: pycon + + >>> connection.list_udf_runtimes()["Python"]["versions"]["3"]["libraries"] + {'geopandas': {'version': '0.13.2'}, + 'numpy': {'version': '1.22.4'}, + 'xarray': {'version': '0.16.2'}, + ... + +Managing and using additional dependencies or libraries that are not provided out-of-the-box by a backend +is a more challenging problem and the practical details can vary between backends. + + +.. _python-udf-dependency-declaration: + +Standard for declaring Python UDF dependencies +----------------------------------------------- + +.. warning:: + + This is based on a fairly recent standard and it might not be supported by your chosen backend yet. + + +`PEP 723 "Inline script metadata" `_ defines a standard +for *Python scripts* to declare dependencies inside a top-level comment block. +If the openEO backend of your choice supports this standard, it is the preferred approach +to declare the (``import``) dependencies of your Python UDF: + +- It avoids all the overhead for the UDF developer + to correctly and efficiently make desired dependencies available in the UDF. +- It allows the openEO backend to optimize dependencies handling. + +.. warning:: + + An openEO backend might only support this automatic UDF dependency handling feature + in batch jobs (because of their isolated nature), + but not for synchronous processing requests. + + +Declaration of UDF dependencies +``````````````````````````````` + +A basic example of how the UDF dependencies can be declared in top-level comment block of your Python UDF: + +.. code-block:: python + :emphasize-lines: 1-6 + + # /// script + # dependencies = [ + # "geojson", + # "fancy-eo-library", + # ] + # /// + # + # This openEO UDF script implements ... + # based on the fancy-eo-library ... using geosjon data ... + + import geojson + import fancyeo + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + ... + +Some considerations to make sure you have a valid metadata block: + +- Lines start with a single hash ``#`` and one space (the space can be omitted if the ``#`` is the only character on the line). +- The metadata block starts with a line ``# /// script`` and ends with ``# ///``. +- Between these delimiters you put the metadata fields in `TOML format `_, + each line prefixed with ``#`` and a space. +- Declare your UDF's dependencies in a ``dependencies`` field as a TOML array. + List each package on a separate line as shown above, or put them all on a single line. + It is also allowed to include comments, as long as the whole construct is valid TOML. +- Each ``dependencies`` entry must be a valid `PEP 508 `_ dependency specifier. + This practically means to use the package names (optionally with version constraints) + as expected by the ``pip install`` command. + +A more complex example to illustrate some more advanced aspects of the metadata block: + +.. code-block:: python + + # /// script + # dependencies = [ + # # A comment about using at least version 2.5.0 + # 'geojson>=2.5.0', # An inline comment + # # Note that TOML allows both single and double quotes for strings. + # + # # Install a package "fancyeo" from a (ZIP) source archive URL. + # "fancyeo @ https://github.com/fncy/fancyeo/archive/refs/tags/v3.2.0-alpha1.zip", + # # Or from a wheel URL, including a content hash to be verified before installing. + # "lousyeo @ https://example.com/lousyeo-6.6.6-py3-none-any.whl#sha1=4bbb3c72a9234ee998a6de940a148e346a", + # # Note that the last entry may have a trailing comma. + # ] + # /// + + +Verification +```````````` + +Use :py:func:`~openeo.udf.run_code.extract_udf_dependencies` to verify +that your metadata block can be parsed correctly: + +.. code-block:: pycon + + >>> from openeo.udf.run_code import extract_udf_dependencies + >>> extract_udf_dependencies(udf_code) + ['geojson>=2.5.0', + 'fancyeo @ https://github.com/fncy/fancyeo/archive/refs/tags/v3.2.0-alpha1.zip', + 'lousyeo @ https://example.com/lousyeo-6.6.6-py3-none-any.whl#sha1=4bbb3c72a9234ee998a6de940a148e346a'] + +If no valid metadata block is found, ``None`` will be returned. + +.. note:: + This function won't necessarily raise exceptions for syntax errors in the metadata block. + It might just fail to reliably detect anything and skip it as regular comment lines. + + +Ad-hoc dependency handling +--------------------------- + +If dependency handling through standardized UDF declarations is not supported by the backend, +there are still ways to manually handle additional dependencies in your UDF. +The exact details can vary between backends, but we can give some general pointers here: + +- Multiple Python dependencies can be packaged fairly easily by zipping a Python virtual environment. +- For some dependencies, it can be important that the Python major version of the virtual environment is the same as the one used by the backend. +- Python allows you to dynamically append (or prepend) libraries to the search path: ``sys.path.append("unzipped_virtualenv_location")`` + + + +Profile a process server-side +============================== + + +.. warning:: + Experimental feature - This feature only works on back-ends running the Geotrellis implementation, and has not yet been + adopted in the openEO API. + +Sometimes users want to 'profile' their UDF on the back-end. While it's recommended to first profile it offline, in the +same manner as you can debug UDF's, back-ends may support profiling directly. +Note that this will only generate statistics over the python part of the execution, therefore it is only suitable for profiling UDFs. + +Usage +------ + +Only batch jobs are supported! In order to turn on profiling, set 'profile' to 'true' in job options:: + + job_options={'profile':'true'} + ... # prepare the process + process.execute_batch('result.tif',job_options=job_options) + +When the process has finished, it will also download a file called 'profile_dumps.tar.gz': + +- ``rdd_-1.pstats`` is the profile data of the python driver, +- the rest are the profiling results of the individual rdd id-s (that can be correlated with the execution using the SPARK UI). + +Viewing profiling information +------------------------------ + +The simplest way is to visualize the results with a graphical visualization tool called kcachegrind. +In order to do that, install `kcachegrind `_ packages (most linux distributions have it installed by default) and it's python connector `pyprof2calltree `_. +From command line run:: + + pyprof2calltree rdd_.pstats. + +Another way is to use the builtin pstats functionality from within python:: + + import pstats + p = pstats.Stats('restats') + p.print_stats() + +Example +------- + + +An example code can be found `here `_ . + + + +.. _udf_logging_with_inspect: + +Logging from a UDF +===================== + +From time to time, when things are not working as expected, +you may want to log some additional debug information from your UDF, inspect the data that is being processed, +or log warnings. +This can be done using the :py:class:`~openeo.udf.debug.inspect()` function. + +For example: to discover the shape of the data cube chunk that you receive in your UDF function: + +.. code-block:: python + :caption: Sample UDF code with ``inspect()`` logging + :emphasize-lines: 1, 5 + + from openeo.udf import inspect + import xarray + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + inspect(data=[cube.shape], message="UDF logging shape of my cube") + cube.values = 0.0001 * cube.values + return cube + +After the batch job is finished (or failed), you can find this information in the logs of the batch job. +For example (as explained at :ref:`batch-job-logs`), +use :py:class:`BatchJob.logs() ` in a Jupyter notebook session +to retrieve and filter the logs interactively: + +.. image:: _static/images/udf/logging_arrayshape.png + +Which reveals in this example a chunking shape of ``[3, 256, 256]``. + +.. note:: + + Not all kinds of data (types) are accepted/supported by the ``data`` argument of :py:class:`~openeo.udf.debug.inspect`, + so you might have to experiment a bit to make sure the desired debug information is logged as desired. + + +.. _old_udf_api: + +``openeo.UDF`` API and usage changes in version 0.13.0 +======================================================== + +Prior to version 0.13.0 of the openEO Python Client Library, +loading and working with UDFs was a bit inconsistent and cumbersome. + +- The old ``openeo.UDF()`` required an explicit ``runtime`` argument, which was usually ``"Python"``. + In the new :py:class:`openeo.UDF `, the ``runtime`` argument is optional, + and it will be auto-detected (from the source code or file extension) when not given. +- The old ``openeo.UDF()`` required an explicit ``data`` argument, and figuring out the correct + value (e.g. something like ``{"from_parameter": "x"}``) required good knowledge of the openEO API and processes. + With the new :py:class:`openeo.UDF ` it is not necessary anymore to provide + the ``data`` argument. In fact, while the ``data`` argument is only still there for compatibility reasons, + it is unused and it will be removed in a future version. + A deprecation warning will be triggered when ``data`` is given a value. +- :py:meth:`DataCube.apply_dimension() ` has direct UDF support through + ``code`` and ``runtime`` arguments, preceding the more generic and standard ``process`` argument, while + comparable methods like :py:meth:`DataCube.apply() ` + or :py:meth:`DataCube.reduce_dimension() ` + only support a ``process`` argument with no dedicated arguments for UDFs. + + The goal is to improve uniformity across all these methods and use a generic ``process`` argument everywhere + (that also supports a :py:class:`openeo.UDF ` object for UDF use cases). + For now, the ``code``, ``runtime`` and ``version`` arguments are still present + in :py:meth:`DataCube.apply_dimension() ` + as before, but usage is deprecated. + + Simple example to sum it up: + + .. code-block:: python + + udf_code = """ + ... + def apply_datacube(cube, ... + """ + + # Legacy `apply_dimension` usage: still works for now, + # but it will trigger a deprecation warning. + cube.apply_dimension(code=udf_code, runtime="Python", dimension="t") + + # New, preferred approach with a standard `process` argument. + udf = openeo.UDF(udf_code) + cube.apply_dimension(process=udf, dimension="t") + + # Unchanged: usage of other apply/reduce/... methods + cube.apply(process=udf) + cube.reduce_dimension(reducer=udf, dimension="t") diff --git a/_sources/udp.rst.txt b/_sources/udp.rst.txt new file mode 100644 index 000000000..d6cf863f1 --- /dev/null +++ b/_sources/udp.rst.txt @@ -0,0 +1,527 @@ +.. _user-defined-processes: + +############################ +User-Defined Processes (UDP) +############################ + + +Code reuse with user-defined processes +======================================= + +As explained before, processes can be chained together in a process graph +to build a certain algorithm. +Often, you have certain (sub)chains that reoccur in the same process graph +of even in different process graphs or algorithms. + +The openEO API enables you to store such (sub)chains +on the back-end as a so called **user-defined process**. +This allows you to build your own *library of reusable building blocks*. + +.. warning:: + + Do not confuse **user-defined processes** (sometimes abbreviated as UDP) with + **user-defined functions** (UDF) in openEO, which is a mechanism to + inject Python or R scripts as process nodes in a process graph. + See :ref:`user-defined-functions` for more information. + +A user-defined process can not only be constructed from +pre-defined processes provided by the back-end, +but also other user-defined processes. + +Ultimately, the openEO API allows you to publicly expose your user-defined process, +so that other users can invoke it as a service. +This turns your openEO process into a web application +that can be executed using the regular openEO +support for synchronous and asynchronous jobs. + + +Process Parameters +==================== + +User-defined processes are usually **parameterized**, +meaning certain inputs are expected when calling the process. + +For example, if you often have to convert Fahrenheit to Celsius:: + + c = (f - 32) / 1.8 + +you could define a user-defined process ``fahrenheit_to_celsius``, +consisting of two simple mathematical operations +(pre-defined processes ``subtract`` and ``divide``). + +We can represent this in openEO's JSON based format as follows +(don't worry too much about the syntax details of this representation, +the openEO Python client will hide this usually):: + + + { + "subtract32": { + "process_id": "subtract", + "arguments": {"x": {"from_parameter": "fahrenheit"}, "y": 32} + }, + "divide18": { + "process_id": "divide", + "arguments": {"x": {"from_node": "subtract32"}, "y": 1.8}, + "result": true + } + } + + +The important point here is the parameter reference ``{"from_parameter": "fahrenheit"}`` in the subtraction. +When we call this user-defined process we will have to provide a Fahrenheit value. +For example with 70 degrees Fahrenheit (again in openEO JSON format here):: + + { + "process_id": "fahrenheit_to_celsius", + "arguments" {"fahrenheit": 70} + } + + +.. _udp-declaring-parameters: + +Declaring Parameters +--------------------- + +It's good style to declare what parameters your user-defined process expects and supports. +It allows you to document your parameters, define the data type(s) you expect +(the "schema" in openEO-speak) and define default values. + +The openEO Python client lets you define parameters as +:class:`~openeo.api.process.Parameter` instances. +In general you have to specify at least the parameter name, +a description and a schema (to declare the expected parameter type). +The "fahrenheit" parameter from the example above can be defined like this:: + + from openeo.api.process import Parameter + + fahrenheit_param = Parameter( + name="fahrenheit", + description="Degrees Fahrenheit", + schema={"type": "number"} + ) + +To simplify working with parameter schemas, the :class:`~openeo.api.process.Parameter` class +provides a couple of helpers to create common types of parameters. +In the example above, the "fahrenheit" parameter (a number) can also be created more compactly +with the :py:meth:`Parameter.number() ` helper:: + + fahrenheit_param = Parameter.number( + name="fahrenheit", description="Degrees Fahrenheit" + ) + +Some useful parameter helpers (class methods of the :py:class:`~openeo.api.process.Parameter` class): + +- :py:meth:`Parameter.string() ` + to create a string parameter, + e.g. to parameterize the collection id in a ``load_collection`` call in your UDP. +- :py:meth:`Parameter.integer() `, + :py:meth:`Parameter.number() `, + and :py:meth:`Parameter.boolean() ` + to create integer, floating point, or boolean parameters respectively. +- :py:meth:`Parameter.array() ` + to create an array parameter, + e.g. to parameterize the a band selection in a ``load_collection`` call in your UDP. +- :py:meth:`Parameter.datacube() ` + (or its legacy, deprecated cousin :py:meth:`Parameter.raster_cube() `) + to create a data cube parameter. +- :py:meth:`Parameter.bounding_box() ` to create + a parameter for specifying a spatial extent with "west", "south", "east", "north" bounds. +- :py:meth:`Parameter.date() ` and + :py:meth:`Parameter.date_time() ` + to create date or date+time parameters. +- :py:meth:`Parameter.temporal_interval() ` to create + a parameter for specifying a temporal interval with "start" and "end" dates. +- :py:meth:`Parameter.geojson() ` to create + a parameter for specifying a GeoJSON geometry. + + + +Consult the documentation of these helper class methods for additional features. +For example, declaring a default value for an integer parameter:: + + size_param = Parameter.integer( + name="size", description="Kernel size", default=4 + ) + + + +More advanced parameter schemas +-------------------------------- + +While the helper class methods of :py:class:`~openeo.api.process.Parameter` (discussed above) +cover the most common parameter usage, +you also might need to declare some parameters with a more special or specific schema. +You can do that through the ``schema`` argument +of the basic :py:class:`~openeo.api.process.Parameter()` constructor. +This "schema" argument follows the `JSON Schema draft-07 `_ specification, +which we will briefly illustrate here. + +Basic primitives can be declared through a (required) "type" field, for example: +``{"type": "string"}`` for strings, ``{"type": "integer"}`` for integers, etc. + +Likewise, arrays can be defined with a minimal ``{"type": "array"}``. +In addition, the expected type of the array items can also be specified, +e.g. an array of integers:: + + { + "type": "array", + "items": {"type": "integer"} + } + +Another, more complex type is ``{"type": "object"}`` for parameters +that are like Python dictionaries (or mappings). +For example, to define a bounding box parameter +that should contain certain fields with certain type:: + + { + "type": "object", + "properties": { + "west": {"type": "number"}, + "south": {"type": "number"}, + "east": {"type": "number"}, + "north": {"type": "number"}, + "crs": {"type": "string"} + } + } + +Check the documentation and examples of `JSON Schema draft-07 `_ +for even more features. + +On top of these generic types, the openEO API also defines a couple of custom (sub)types +in the `openeo-processes project `_ +(see the ``meta/subtype-schemas.json`` listing). +For example, the schema of an openEO data cube is:: + + { + "type": "object", + "subtype": "datacube" + } + + + +.. _build_and_store_udp: + +Building and storing user-defined process +============================================= + +There are a couple of ways to build and store user-defined processes: + +- using predefined :ref:`process functions ` +- :ref:`parameterized building of a data cube ` +- :ref:`directly from a well-formatted dictionary ` process graph representation + + + +.. _create_udp_through_process_functions: + +Through "process functions" +---------------------------- + +The openEO Python Client Library defines the +official processes in the :py:mod:`openeo.processes` module, +which can be used to build a process graph as follows:: + + from openeo.processes import subtract, divide + from openeo.api.process import Parameter + + # Define the input parameter. + f = Parameter.number("f", description="Degrees Fahrenheit.") + + # Do the calculations, using the parameter and other values + fahrenheit_to_celsius = divide(x=subtract(x=f, y=32), y=1.8) + + # Store user-defined process in openEO back-end. + connection.save_user_defined_process( + "fahrenheit_to_celsius", + fahrenheit_to_celsius, + parameters=[f] + ) + + +The ``fahrenheit_to_celsius`` object encapsulates the subtract and divide calculations in a symbolic way. +We can pass it directly to :py:meth:`~openeo.rest.connection.Connection.save_user_defined_process`. + + +If you want to inspect its openEO-style process graph representation, +use the :meth:`~openeo.rest.datacube.DataCube.to_json()` +or :meth:`~openeo.rest.datacube.DataCube.print_json()` method:: + + >>> fahrenheit_to_celsius.print_json() + { + "process_graph": { + "subtract1": { + "process_id": "subtract", + "arguments": { + "x": { + "from_parameter": "f" + }, + "y": 32 + } + }, + "divide1": { + "process_id": "divide", + "arguments": { + "x": { + "from_node": "subtract1" + }, + "y": 1.8 + }, + "result": true + } + } + } + + +.. _create_udp_parameterized_cube: + +From a parameterized data cube +------------------------------- + +It's also possible to work with a :class:`~openeo.rest.datacube.DataCube` directly +and parameterize it. +Let's create, as a simple but functional example, a custom ``load_collection`` +with hardcoded collection id and band name +and a parameterized spatial extent (with default):: + + spatial_extent = Parameter( + name="bbox", + schema="object", + default={"west": 3.7, "south": 51.03, "east": 3.75, "north": 51.05} + ) + + cube = connection.load_collection( + "SENTINEL2_L2A_SENTINELHUB", + spatial_extent=spatial_extent, + bands=["B04"] + ) + +Note how we just can pass :class:`~openeo.api.process.Parameter` objects as arguments +while building a :class:`~openeo.rest.datacube.DataCube`. + +.. note:: + + Not all :class:`~openeo.rest.datacube.DataCube` methods/processes properly support + :class:`~openeo.api.process.Parameter` arguments. + Please submit a bug report when you encounter missing or wrong parameterization support. + +We can now store this as a user-defined process called "fancy_load_collection" on the back-end:: + + connection.save_user_defined_process( + "fancy_load_collection", + cube, + parameters=[spatial_extent] + ) + +If you want to inspect its openEO-style process graph representation, +use the :meth:`~openeo.rest.datacube.DataCube.to_json()` +or :meth:`~openeo.rest.datacube.DataCube.print_json()` method:: + + >>> cube.print_json() + { + "loadcollection1": { + "process_id": "load_collection", + "arguments": { + "id": "SENTINEL2_L2A_SENTINELHUB", + "bands": [ + "B04" + ], + "spatial_extent": { + "from_parameter": "bbox" + }, + "temporal_extent": null + }, + "result": true + } + } + + + +.. _create_udp_from_dict: + +Using a predefined dictionary +------------------------------ + +In some (advanced) situation, you might already have +the process graph in dictionary format +(or JSON format, which is very close and easy to transform). +Another developer already prepared it for you, +or you prefer to fine-tune process graphs in a JSON editor. +It is very straightforward to submit this as a user-defined process. + +Say we start from the following Python dictionary, +representing the Fahrenheit to Celsius conversion we discussed before:: + + fahrenheit_to_celsius = { + "subtract1": { + "process_id": "subtract", + "arguments": {"x": {"from_parameter": "f"}, "y": 32} + }, + "divide1": { + "process_id": "divide", + "arguments": {"x": {"from_node": "subtract1"}, "y": 1.8}, + "result": True + }} + +We can store this directly, taking into account that we have to define +a parameter named ``f`` corresponding with the ``{"from_parameter": "f"}`` argument +from the dictionary above:: + + connection.save_user_defined_process( + user_defined_process_id="fahrenheit_to_celsius", + process_graph=fahrenheit_to_celsius, + parameters=[Parameter.number(name="f", description="Degrees Fahrenheit")] + ) + + +Store to a file +--------------- + +Some use cases might require storing the user-defined process in, +for example, a JSON file instead of storing it directly on a back-end. +Use :py:func:`~openeo.rest.udp.build_process_dict` to build a dictionary +compatible with the "process graph with metadata" format of the openEO API +and dump it in JSON format to a file:: + + import json + from openeo.rest.udp import build_process_dict + from openeo.processes import subtract, divide + from openeo.api.process import Parameter + + fahrenheit = Parameter.number("f", description="Degrees Fahrenheit.") + fahrenheit_to_celsius = divide(x=subtract(x=fahrenheit, y=32), y=1.8) + + spec = build_process_dict( + process_id="fahrenheit_to_celsius", + process_graph=fahrenheit_to_celsius, + parameters=[fahrenheit] + ) + + with open("fahrenheit_to_celsius.json", "w") as f: + json.dump(spec, f, indent=2) + +This results in a JSON file like this:: + + { + "id": "fahrenheit_to_celsius", + "process_graph": { + "subtract1": { + "process_id": "subtract", + ... + "parameters": [ + { + "name": "f", + ... + + +.. _evaluate_udp: + +Evaluate user-defined processes +================================ + +Let's evaluate the user-defined processes we defined. + +Because there is no pre-defined +wrapper function for our user-defined process, we use the +generic :func:`openeo.processes.process` function to build a simple +process graph that calls our ``fahrenheit_to_celsius`` process:: + + >>> pg = openeo.processes.process("fahrenheit_to_celsius", f=70) + >>> pg.print_json(indent=None) + {"process_graph": {"fahrenheittocelsius1": {"process_id": "fahrenheit_to_celsius", "arguments": {"f": 70}, "result": true}}} + + >>> res = connection.execute(pg) + >>> print(res) + 21.11111111111111 + + +To use our custom ``fancy_load_collection`` process, +we only have to specify a temporal extent, +and let the predefined and default values do their work. +We will use :func:`~openeo.rest.connection.Connection.datacube_from_process` +to construct a :class:`~openeo.rest.datacube.DataCube` object +which we can process further and download:: + + cube = connection.datacube_from_process("fancy_load_collection") + cube = cube.filter_temporal("2020-09-01", "2020-09-10") + cube.download("fancy.tiff", format="GTiff") + +See :ref:`datacube_from_process` for more information on :func:`~openeo.rest.connection.Connection.datacube_from_process`. + + +.. _udp_example_evi: + +UDP Example: EVI timeseries +========================================== + +In this UDP example, we'll build a reusable UDP ``evi_timeseries`` +to calculate the EVI timeseries for a given geometry. +It's a simplified version of the EVI workflow laid out in :ref:`basic_example_evi_map_and_timeseries`, +focussing on the UDP-specific aspects: defining and using parameters; +building, storing, and finally executing the UDP. + +.. code-block:: python + + import openeo + from openeo.api.process import Parameter + + # Create connection to openEO back-end + connection = openeo.connect("...").authenticate_oidc() + + # Declare the UDP parameters + temporal_extent = Parameter( + name="temporal_extent", + description="The date range to calculate the EVI for.", + schema={"type": "array", "subtype": "temporal-interval"}, + default =["2018-06-15", "2018-06-27"] + ) + geometry = Parameter( + name="geometry", + description="The geometry (a single (multi)polygon or a feature collection of (multi)polygons) of to calculate the EVI for.", + schema={"type": "object", "subtype": "geojson"} + ) + + # Load raw SENTINEL2_L2A data + sentinel2_cube = connection.load_collection( + "SENTINEL2_L2A", + temporal_extent=temporal_extent, + bands=["B02", "B04", "B08"], + ) + + # Extract spectral bands and calculate EVI with the "band math" feature + blue = sentinel2_cube.band("B02") * 0.0001 + red = sentinel2_cube.band("B04") * 0.0001 + nir = sentinel2_cube.band("B08") * 0.0001 + evi = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0) + + evi_aggregation = evi.aggregate_spatial( + geometries=geometry, + reducer="mean", + ) + + # Store the parameterized user-defined process at openEO back-end. + process_id = "evi_timeseries" + connection.save_user_defined_process( + user_defined_process_id=process_id, + process_graph=evi_aggregation, + parameters=[temporal_interval, geometry], + ) + +When this UDP ``evi_timeseries`` is successfully stored on the back-end, +we can use it through :func:`~openeo.rest.connection.Connection.datacube_from_process` +to get the EVI timeseries of a desired geometry and time window: + +.. code-block:: python + + time_window = ["2020-01-01", "2021-12-31"] + geometry = { + "type": "Polygon", + "coordinates": [[[5.1793, 51.2498], [5.1787, 51.2467], [5.1852, 51.2450], [5.1867, 51.2453], [5.1873, 51.2491], [5.1793, 51.2498]]], + } + + evi_timeseries = connection.datacube_from_process( + process_id="evi_timeseries", + temporal_extent=time_window, + geometry=geometry, + ) + + evi_timeseries.download("evi-aggregation.json") diff --git a/_static/alabaster.css b/_static/alabaster.css new file mode 100644 index 000000000..bf03222f7 --- /dev/null +++ b/_static/alabaster.css @@ -0,0 +1,663 @@ +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Cantarell, Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 1200px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 300px; +} + +div.sphinxsidebar { + width: 300px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 1200px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar { + max-height: 100%; + overflow-y: auto; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Cantarell, Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Cantarell, Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox { + margin: 1em 0; +} + +div.sphinxsidebar .search > div { + display: table-cell; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Cantarell, Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Cantarell, Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Liberation Mono', 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: unset; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + +@media screen and (max-width: 1200px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.sphinxsidebar { + display: block; + float: none; + width: unset; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + min-width: auto; /* fixes width on small screens, breaks .hll */ + padding: 0; + } + + .hll { + /* "fixes" the breakage */ + width: max-content; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Hide ugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} + +img.github { + position: absolute; + top: 0; + border: 0; + right: 0; +} \ No newline at end of file diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 000000000..d9846dacb --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,914 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: inherit; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 000000000..5e48835fc --- /dev/null +++ b/_static/custom.css @@ -0,0 +1,139 @@ +/* + * Customization of Alabaster theme + * per https://alabaster.readthedocs.io/en/latest/customization.html#custom-stylesheet + */ + +/* "Quick Search" should be capitalized. */ +div#searchbox h3 { + text-transform: capitalize; +} + +/* Much-improved spacing around code blocks. */ +div.highlight pre { + padding: 1ex; +} + +/* Reduce space between paragraphs for better visual structure */ +p { + margin: 1ex 0; +} + +/* Hide "view source code" links by default, only show on hover */ +dt .viewcode-link { + visibility: hidden; + font-size: 70%; +} + +dt:hover .viewcode-link { + visibility: visible; +} + +/* More breathing space between successive methods */ +dl { + margin-bottom: 1.5em; +} + +dl.field-list > dt { + /* Cleaner aligning of Parameters/Returns/Raises listing with method description paragraphs */ + padding-left: 0; + /* Make Parameters/Returns/Raises labels less dominant */ + text-transform: uppercase; + font-size: 70%; +} + +.sidebar-meta { + font-size: 80%; +} + +div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { + margin: 1.5em 0 0.5em 0; +} + +div.body h1 { + margin: 0 0 0.5em 0; +} + +.toctree-l1 { + padding: 0.1em 0.5em; + margin-left: -0.5em; +} + +div.sphinxsidebar .toctree-l1 a { + border: none; +} + +.toctree-l1.current { + background-color: #f3f5f7; + border-right: 0.5rem solid #a2cedb; +} + + +div.admonition, +div.versionadded, +.py div.versionchanged, +.py div.deprecated { + padding: 0.5em 1em; + border-style: solid; + border-width: 0 0 0 0.5rem; + border-color: #cccccc; + background-color: #f3f5f7; +} + +div.admonition :first-child, +div.versionadded :first-child, +.py div.versionchanged :first-child, +.py div.deprecated :first-child { + margin-top: 0; +} + + +div.admonition :last-child, +div.versionadded :last-child, +.py div.versionchanged :last-child, +.py div.deprecated :last-child { + margin-bottom: 0; +} + +div.admonition p.admonition-title { + font-size: 80%; + text-transform: uppercase; + font-weight: bold; +} + +div.admonition.note, +div.admonition.tip, +div.admonition.seealso, +div.admonition.hint, +div.versionadded, +.py div.versionchanged { + border-left-color: #42b983; +} + +div.admonition.warning, +div.admonition.attention, +div.admonition.caution, +div.admonition.danger, +div.admonition.error, +div.admonition.important, +.py div.deprecated { + border-left-color: #b9425e; +} + + +pre { + background-color: #e2f0f4; +} + +.highlight-default, .highlight-python, .highlight-pycon, .highlight-shell , .highlight-text { + border-right: 0.5rem solid #a2cedb; +} + +.highlight span.linenos { + color: #888; + font-size: 75%; + padding: 0 1ex; +} + +nav.contents.local { + border: none; +} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 000000000..0398ebb9f --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 000000000..257285cc5 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.35.0a1', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/_static/github-banner.svg b/_static/github-banner.svg new file mode 100644 index 000000000..c47d9dc0c --- /dev/null +++ b/_static/github-banner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/_static/images/basics/evi-composite.png b/_static/images/basics/evi-composite.png new file mode 100644 index 0000000000000000000000000000000000000000..5680bf03e55509af77051f8fd0fa483ca5337578 GIT binary patch literal 31940 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
!G}{+Y9*fOR|| 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≪02CyINip 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�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 ZIW$3#!(*ed#}NYlR1`E3MRHHy{vWgV6Bqyh literal 0 HcmV?d00001 diff --git a/_static/images/batchjobs-jupyter-created.png b/_static/images/batchjobs-jupyter-created.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd25f34c04ba92c79540389a4291640ae846cc0 GIT binary patch literal 56503 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 Wz52be*Am1(Sgo-AL;CVh5B~$w`zC?_ literal 0 HcmV?d00001 diff --git a/_static/images/batchjobs-jupyter-listing.png b/_static/images/batchjobs-jupyter-listing.png new file mode 100644 index 0000000000000000000000000000000000000000..6e94d16b17d2ab58919e7b8ff894dc5d0f4183b5 GIT binary patch literal 46962 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" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/api-processbuilder.html b/api-processbuilder.html new file mode 100644 index 000000000..bdb1b2247 --- /dev/null +++ b/api-processbuilder.html @@ -0,0 +1,196 @@ + + + + + + + + <no title> — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

The ProcessBuilder class +is a helper class that implements +(much like the openEO process functions) +each openEO process as a method. +On top of that it also adds syntactic sugar to support Python operators as well +(e.g. + is translated to the add process).

+
+

Attention

+

As normal user, you should never create a +ProcessBuilder instance +directly.

+

You should only interact with this class inside a callback +function/lambda while building a child callback process graph +as discussed at Callback as a callable.

+
+

For example, let’s start from this simple usage snippet +where we want to reduce the temporal dimension +by taking the temporal mean of each timeseries:

+
def my_reducer(data):
+    return data.mean()
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

Note that this my_reducer function has a data argument, +which conceptually corresponds to an array of pixel values +(along the temporal dimension). +However, it’s important to understand that the my_reducer function +is actually not evaluated when you execute your process graph +on an openEO back-end, e.g. as a batch jobs. +Instead, my_reducer is evaluated +while building your process graph client-side +(at the time you execute that cube.reduce_dimension() statement to be precise). +This means that that data argument is actually not a concrete array of EO data, +but some kind of virtual placeholder, +a ProcessBuilder instance, +that keeps track of the operations you intend to do on the EO data.

+

To make that more concrete, it helps to add type hints +which will make it easier to discover what you can do with the argument +(depending on which editor or IDE you are using):

+
from openeo.processes import ProcessBuilder
+
+def my_reducer(data: ProcessBuilder) -> ProcessBuilder:
+    return data.mean()
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

Because ProcessBuilder methods +return new ProcessBuilder instances, +and because it support syntactic sugar to use Python operators on it, +and because openeo.process functions +also accept and return ProcessBuilder instances, +we can mix methods, functions and operators in the callback function like this:

+
from openeo.processes import ProcessBuilder, cos
+
+def my_reducer(data: ProcessBuilder) -> ProcessBuilder:
+    return cos(data.mean()) + 1.23
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

or compactly, using an anonymous lambda expression:

+
from openeo.processes import cos
+
+cube.reduce_dimension(
+    reducer=lambda data: cos(data.mean())) + 1.23,
+    dimension="t"
+)
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/api-processes.html b/api-processes.html new file mode 100644 index 000000000..6d508c745 --- /dev/null +++ b/api-processes.html @@ -0,0 +1,4557 @@ + + + + + + + + API: openeo.processes — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

API: openeo.processes

+

The openeo.processes module contains building blocks and helpers +to construct so called “child callbacks” for openEO processes like +openeo.rest.datacube.DataCube.apply() and +openeo.rest.datacube.DataCube.reduce_dimension(), +as discussed at Callback as a callable.

+
+

Note

+

The contents of the openeo.processes module is automatically compiled +from the official openEO process specifications. +Developers that want to fix bugs in, or add implementations to this +module should not touch the file directly, but instead address it in the +upstream openeo-processes repository +or in the internal tooling to generate this file.

+
+ +
+

Functions in openeo.processes

+

The openeo.processes module implements (at top-level) +a regular Python function for each openEO process +(not only the official stable ones, but also experimental ones in “proposal” state).

+

These functions can be used directly as child callback, +for example as follows:

+
from openeo.processes import absolute, max
+
+cube.apply(absolute)
+cube.reduce_dimension(max, dimension="t")
+
+
+

Note how the signatures of the parent DataCube methods +and the callback functions match up:

+ +
+
+openeo.processes.absolute(x)[source]
+

Absolute value

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed absolute value.

+
+
+
+

See also

+

openeo.org documentation on process “absolute”.

+
+
+ +
+
+openeo.processes.add(x, y)[source]
+

Addition of two numbers

+
+
Parameters:
+
    +
  • x – The first summand.

  • +
  • y – The second summand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed sum of the two numbers.

+
+
+
+

See also

+

openeo.org documentation on process “add”.

+
+
+ +
+
+openeo.processes.add_dimension(data, name, label, type=<object object>)[source]
+

Add a new dimension

+
+
Parameters:
+
    +
  • data – A data cube to add the dimension to.

  • +
  • name – Name for the dimension.

  • +
  • label – A dimension label.

  • +
  • type – The type of dimension, defaults to other.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The data cube with a newly added dimension. The new dimension has exactly one dimension label. All +other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “add_dimension”.

+
+
+ +
+
+openeo.processes.aggregate_spatial(data, geometries, reducer, target_dimension=<object object>, context=<object object>)[source]
+

Zonal statistics for geometries

+
+
Parameters:
+
    +
  • data – A raster data cube with at least two spatial dimensions. The data cube implicitly gets +restricted to the bounds of the geometries as if filter_spatial() would have been used with the same +values for the corresponding parameters immediately before this process.

  • +
  • geometries – Geometries for which the aggregation will be computed. Feature properties are preserved +for vector data cubes and all GeoJSON Features. One value will be computed per label in the dimension of +type geometries, GeoJSON Feature or Geometry. For a FeatureCollection multiple values will be +computed, one value per contained Feature. No values will be computed for empty geometries. For example, +a single value will be computed for a MultiPolygon, but two values will be computed for a +FeatureCollection containing two polygons. - For polygons, the process considers all pixels for +which the point at the pixel center intersects with the corresponding polygon (as defined in the Simple +Features standard by the OGC). - For points, the process considers the closest pixel center. - For +lines (line strings), the process considers all the pixels whose centers are closest to at least one +point on the line. Thus, pixels may be part of multiple geometries and be part of multiple aggregations. +No operation is applied to geometries that are outside of the bounds of the data.

  • +
  • reducer – A reducer to be applied on all values of each geometry. A reducer is a single process such +as mean() or a set of processes, which computes a single value for a list of values, see the category +‘reducer’ for such processes.

  • +
  • target_dimension – By default (which is null), the process only computes the results and doesn’t +add a new dimension. If this parameter contains a new dimension name, the computation also stores +information about the total count of pixels (valid + invalid pixels) and the number of valid pixels (see +is_valid()) for each computed value. These values are added as a new dimension. The new dimension of +type other has the dimension labels value, total_count and valid_count. Fails with a +TargetDimensionExists exception if a dimension with the specified name exists.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A vector data cube with the computed results. Empty geometries still exist but without any +aggregated values (i.e. no-data). The spatial dimensions are replaced by a dimension of type ‘geometries’ +and if target_dimension is not null, a new dimension is added.

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_spatial”.

+
+
+ +
+
+openeo.processes.aggregate_spatial_window(data, reducer, size, boundary=<object object>, align=<object object>, context=<object object>)[source]
+

Zonal statistics for rectangular windows

+
+
Parameters:
+
    +
  • data – A raster data cube with exactly two horizontal spatial dimensions and an arbitrary number of +additional dimensions. The process is applied to all additional dimensions individually.

  • +
  • reducer – A reducer to be applied on the list of values, which contain all pixels covered by the +window. A reducer is a single process such as mean() or a set of processes, which computes a single +value for a list of values, see the category ‘reducer’ for such processes.

  • +
  • size – Window size in pixels along the horizontal spatial dimensions. The first value corresponds to +the x axis, the second value corresponds to the y axis.

  • +
  • boundary – Behavior to apply if the number of values for the axes x and y is not a multiple of +the corresponding value in the size parameter. Options are: - pad (default): pad the data cube with +the no-data value null to fit the required window size. - trim: trim the data cube to fit the required +window size. Set the parameter align to specifies to which corner the data is aligned to.

  • +
  • align – If the data requires padding or trimming (see parameter boundary), specifies to which +corner of the spatial extent the data is aligned to. For example, if the data is aligned to the upper left, +the process pads/trims at the lower-right.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube with the newly computed values and the same dimensions. The resolution will +change depending on the chosen values for the size and boundary parameter. It usually decreases for the +dimensions which have the corresponding parameter size set to values greater than 1. The dimension +labels will be set to the coordinate at the center of the window. The other dimension properties (name, +type and reference system) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_spatial_window”.

+
+
+ +
+
+openeo.processes.aggregate_temporal(data, intervals, reducer, labels=<object object>, dimension=<object object>, context=<object object>)[source]
+

Temporal aggregations

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • intervals – Left-closed temporal intervals, which are allowed to overlap. Each temporal interval in +the array has exactly two elements: 1. The first element is the start of the temporal interval. The +specified time instant is included in the interval. 2. The second element is the end of the temporal +interval. The specified time instant is excluded from the interval. The second element must always be +greater/later than the first element, except when using time without date. Otherwise, a +TemporalExtentEmpty exception is thrown.

  • +
  • reducer – A reducer to be applied for the values contained in each interval. A reducer is a single +process such as mean() or a set of processes, which computes a single value for a list of values, see +the category ‘reducer’ for such processes. Intervals may not contain any values, which for most reducers +leads to no-data (null) values by default.

  • +
  • labels – Distinct labels for the intervals, which can contain dates and/or times. Is only required to +be specified if the values for the start of the temporal intervals are not distinct and thus the default +labels would not be unique. The number of labels and the number of groups need to be equal.

  • +
  • dimension – The name of the temporal dimension for aggregation. All data along the dimension is +passed through the specified reducer. If the dimension is not set or set to null, the data cube is +expected to only have one temporal dimension. Fails with a TooManyDimensions exception if it has more +dimensions. Fails with a DimensionNotAvailable exception if the specified dimension does not exist.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A new data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged, except for the resolution and dimension labels of the given +temporal dimension.

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_temporal”.

+
+
+ +
+
+openeo.processes.aggregate_temporal_period(data, period, reducer, dimension=<object object>, context=<object object>)[source]
+

Temporal aggregations based on calendar hierarchies

+
+
Parameters:
+
    +
  • data – The source data cube.

  • +
  • period – The time intervals to aggregate. The following pre-defined values are available: * hour: +Hour of the day * day: Day of the year * week: Week of the year * dekad: Ten day periods, counted per +year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month +can range from 8 to 11 days. For example, the third dekad of a year spans from January 21 till January 31 +(11 days), the fourth dekad spans from February 1 till February 10 (10 days) and the sixth dekad spans from +February 21 till February 28 or February 29 in a leap year (8 or 9 days respectively). * month: Month of +the year * season: Three month periods of the calendar seasons (December - February, March - May, June - +August, September - November). * tropical-season: Six month periods of the tropical seasons (November - +April, May - October). * year: Proleptic years * decade: Ten year periods ([0-to-9 +decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the next year +ending in a 9. * decade-ad: Ten year periods ([1-to-0 +decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) +calendar era, from a year ending in a 1 to the next year ending in a 0.

  • +
  • reducer – A reducer to be applied for the values contained in each period. A reducer is a single +process such as mean() or a set of processes, which computes a single value for a list of values, see +the category ‘reducer’ for such processes. Periods may not contain any values, which for most reducers +leads to no-data (null) values by default.

  • +
  • dimension – The name of the temporal dimension for aggregation. All data along the dimension is +passed through the specified reducer. If the dimension is not set or set to null, the source data cube is +expected to only have one temporal dimension. Fails with a TooManyDimensions exception if it has more +dimensions. Fails with a DimensionNotAvailable exception if the specified dimension does not exist.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A new data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged, except for the resolution and dimension labels of the given +temporal dimension. The specified temporal dimension has the following dimension labels (YYYY = four- +digit year, MM = two-digit month, DD two-digit day of month): * hour: YYYY-MM-DD-00 - YYYY-MM- +DD-23 * day: YYYY-001 - YYYY-365 * week: YYYY-01 - YYYY-52 * dekad: YYYY-00 - YYYY-36 * +month: YYYY-01 - YYYY-12 * season: YYYY-djf (December - February), YYYY-mam (March - May), +YYYY-jja (June - August), YYYY-son (September - November). * tropical-season: YYYY-ndjfma (November +- April), YYYY-mjjaso (May - October). * year: YYYY * decade: YYY0 * decade-ad: YYY1 The +dimension labels in the new data cube are complete for the whole extent of the source data cube. For +example, if period is set to day and the source data cube has two dimension labels at the beginning of +the year (2020-01-01) and the end of a year (2020-12-31), the process returns a data cube with 365 +dimension labels (2020-001, 2020-002, …, 2020-365). In contrast, if period is set to day and +the source data cube has just one dimension label 2020-01-05, the process returns a data cube with just a +single dimension label (2020-005).

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_temporal_period”.

+
+
+ +
+
+openeo.processes.all(data, ignore_nodata=<object object>)[source]
+

Are all of the values true?

+
+
Parameters:
+
    +
  • data – A set of boolean values.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Boolean result of the logical operation.

+
+
+
+

See also

+

openeo.org documentation on process “all”.

+
+
+ +
+
+openeo.processes.and_(x, y)[source]
+

Logical AND

+
+
Parameters:
+
    +
  • x – A boolean value.

  • +
  • y – A boolean value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Boolean result of the logical AND.

+
+
+
+

See also

+

openeo.org documentation on process “and_”.

+
+
+ +
+
+openeo.processes.anomaly(data, normals, period)[source]
+

Compute anomalies

+
+
Parameters:
+
    +
  • data – A data cube with exactly one temporal dimension and the following dimension labels for the +given period (YYYY = four-digit year, MM = two-digit month, DD two-digit day of month): * hour: +YYYY-MM-DD-00 - YYYY-MM-DD-23 * day: YYYY-001 - YYYY-365 * week: YYYY-01 - YYYY-52 * +dekad: YYYY-00 - YYYY-36 * month: YYYY-01 - YYYY-12 * season: YYYY-djf (December - +February), YYYY-mam (March - May), YYYY-jja (June - August), YYYY-son (September - November). * +tropical-season: YYYY-ndjfma (November - April), YYYY-mjjaso (May - October). * year: YYYY * +decade: YYY0 * decade-ad: YYY1 * single-period / climatology-period: Any +aggregate_temporal_period() can compute such a data cube.

  • +
  • normals – A data cube with normals, e.g. daily, monthly or yearly values computed from a process such +as climatological_normal(). Must contain exactly one temporal dimension with the following dimension +labels for the given period: * hour: 00 - 23 * day: 001 - 365 * week: 01 - 52 * dekad: +00 - 36 * month: 01 - 12 * season: djf (December - February), mam (March - May), jja +(June - August), son (September - November) * tropical-season: ndjfma (November - April), mjjaso +(May - October) * year: Four-digit year numbers * decade: Four-digit year numbers, the last digit being +a 0 * decade-ad: Four-digit year numbers, the last digit being a 1 * single-period / climatology- +period: A single dimension label with any name is expected.

  • +
  • period – Specifies the time intervals available in the normals data cube. The following options are +available: * hour: Hour of the day * day: Day of the year * week: Week of the year * dekad: Ten +day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The +third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 +each year. * month: Month of the year * season: Three month periods of the calendar seasons (December - +February, March - May, June - August, September - November). * tropical-season: Six month periods of the +tropical seasons (November - April, May - October). * year: Proleptic years * decade: Ten year periods +([0-to-9 decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the +next year ending in a 9. * decade-ad: Ten year periods ([1-to-0 +decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) +calendar era, from a year ending in a 1 to the next year ending in a 0. * single-period / climatology- +period: A single period of arbitrary length

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “anomaly”.

+
+
+ +
+
+openeo.processes.any(data, ignore_nodata=<object object>)[source]
+

Is at least one value true?

+
+
Parameters:
+
    +
  • data – A set of boolean values.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Boolean result of the logical operation.

+
+
+
+

See also

+

openeo.org documentation on process “any”.

+
+
+ +
+
+openeo.processes.apply(data, process, context=<object object>)[source]
+

Apply a process to each value

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • process – A process that accepts and returns a single value and is applied on each individual value +in the data cube. The process may consist of multiple sub-processes and could, for example, consist of +processes such as absolute() or linear_scale_range().

  • +
  • context – Additional data to be passed to the process.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values and the same dimensions. The dimension properties +(name, type, labels, reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “apply”.

+
+
+ +
+
+openeo.processes.apply_dimension(data, process, dimension, target_dimension=<object object>, context=<object object>)[source]
+

Apply a process to all values along a dimension

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • process – Process to be applied on all values along the given dimension. The specified process needs +to accept an array and must return an array with at least one element. A process may consist of multiple +sub-processes.

  • +
  • dimension – The name of the source dimension to apply the process on. Fails with a +DimensionNotAvailable exception if the specified dimension does not exist.

  • +
  • target_dimension – The name of the target dimension or null (the default) to use the source +dimension specified in the parameter dimension. By specifying a target dimension, the source dimension +is removed. The target dimension with the specified name and the type other (see add_dimension()) is +created, if it doesn’t exist yet.

  • +
  • context – Additional data to be passed to the process.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values. All dimensions stay the same, except for the +dimensions specified in corresponding parameters. There are three cases how the dimensions can change: 1. +The source dimension is the target dimension: - The (number of) dimensions remain unchanged as the +source dimension is the target dimension. - The source dimension properties name and type remain +unchanged. - The dimension labels, the reference system and the resolution are preserved only if the +number of values in the source dimension is equal to the number of values computed by the process. +Otherwise, all other dimension properties change as defined in the list below. 2. The source dimension is +not the target dimension. The target dimension exists with a single label only: - The number of +dimensions decreases by one as the source dimension is ‘dropped’ and the target dimension is filled with +the processed data that originates from the source dimension. - The target dimension properties name and +type remain unchanged. All other dimension properties change as defined in the list below. 3. The source +dimension is not the target dimension and the latter does not exist: - The number of dimensions remain +unchanged, but the source dimension is replaced with the target dimension. - The target dimension has +the specified name and the type other. All other dimension properties are set as defined in the list below. +Unless otherwise stated above, for the given (target) dimension the following applies: - the number of +dimension labels is equal to the number of values computed by the process, - the dimension labels are +incrementing integers starting from zero, - the resolution changes, and - the reference system is +undefined.

+
+
+
+

See also

+

openeo.org documentation on process “apply_dimension”.

+
+
+ +
+
+openeo.processes.apply_kernel(data, kernel, factor=<object object>, border=<object object>, replace_invalid=<object object>)[source]
+

Apply a spatial convolution with a kernel

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • kernel – Kernel as a two-dimensional array of weights. The inner level of the nested array aligns +with the x axis and the outer level aligns with the y axis. Each level of the kernel must have an +uneven number of elements, otherwise the process throws a KernelDimensionsUneven exception.

  • +
  • factor – A factor that is multiplied to each value after the kernel has been applied. This is +basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required +for some kernel-based algorithms such as the Gaussian blur.

  • +
  • border – Determines how the data is extended when the kernel overlaps with the borders. Defaults to +fill the border with zeroes. The following options are available: * numeric value - fill with a user- +defined constant number n: nnnnnn|abcdefgh|nnnnnn (default, with n = 0) * replicate - repeat the +value from the pixel at the border: aaaaaa|abcdefgh|hhhhhh * reflect - mirror/reflect from the border: +fedcba|abcdefgh|hgfedc * reflect_pixel - mirror/reflect from the center of the pixel at the border: +gfedcb|abcdefgh|gfedcb * wrap - repeat/wrap the image: cdefgh|abcdefgh|abcdef

  • +
  • replace_invalid – This parameter specifies the value to replace non-numerical or infinite numerical +values with. By default, those values are replaced with zeroes.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values and the same dimensions. The dimension properties +(name, type, labels, reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “apply_kernel”.

+
+
+ +
+
+openeo.processes.apply_neighborhood(data, process, size, overlap=<object object>, context=<object object>)[source]
+

Apply a process to pixels in a n-dimensional neighborhood

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • process – Process to be applied on all neighborhoods.

  • +
  • size – Neighborhood sizes along each dimension. This object maps dimension names to either a +physical measure (e.g. 100 m, 10 days) or pixels (e.g. 32 pixels). For dimensions not specified, the +default is to provide all values. Be aware that including all values from overly large dimensions may not +be processed at once.

  • +
  • overlap – Overlap of neighborhoods along each dimension to avoid border effects. By default no +overlap is provided. For instance a temporal dimension can add 1 month before and after a neighborhood. In +the spatial dimensions, this is often a number of pixels. The overlap specified is added before and after, +so an overlap of 8 pixels will add 8 pixels on both sides of the window, so 16 in total. Be aware that +large overlaps increase the need for computational resources and modifying overlapping data in subsequent +operations have no effect.

  • +
  • context – Additional data to be passed to the process.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube with the newly computed values and the same dimensions. The dimension +properties (name, type, labels, reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “apply_neighborhood”.

+
+
+ +
+
+openeo.processes.apply_polygon(data, polygons, process, mask_value=<object object>, context=<object object>)[source]
+

Apply a process to segments of the data cube

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • polygons – A vector data cube containing at least one polygon. The provided vector data can be one of +the following: * A Polygon or MultiPolygon geometry, * a Feature with a Polygon or MultiPolygon +geometry, or * a FeatureCollection containing at least one Feature with Polygon or MultiPolygon +geometries. * Empty geometries are ignored.

  • +
  • process – A process that accepts and returns a single data cube and is applied on each individual sub +data cube. The process may consist of multiple sub-processes.

  • +
  • mask_value – All pixels for which the point at the pixel center does not intersect with the +polygon are replaced with the given value, which defaults to null (no data). It can provide a +distinction between no data values within the polygon and masked pixels outside of it.

  • +
  • context – Additional data to be passed to the process.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values and the same dimensions. The dimension properties +(name, type, labels, reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “apply_polygon”.

+
+
+ +
+
+openeo.processes.arccos(x)[source]
+

Inverse cosine

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arccos”.

+
+
+ +
+
+openeo.processes.arcosh(x)[source]
+

Inverse hyperbolic cosine

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arcosh”.

+
+
+ +
+
+openeo.processes.arcsin(x)[source]
+

Inverse sine

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arcsin”.

+
+
+ +
+
+openeo.processes.arctan(x)[source]
+

Inverse tangent

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arctan”.

+
+
+ +
+
+openeo.processes.arctan2(y, x)[source]
+

Inverse tangent of two numbers

+
+
Parameters:
+
    +
  • y – A number to be used as the dividend.

  • +
  • x – A number to be used as the divisor.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arctan2”.

+
+
+ +
+
+openeo.processes.ard_normalized_radar_backscatter(data, elevation_model=<object object>, contributing_area=<object object>, ellipsoid_incidence_angle=<object object>, noise_removal=<object object>, options=<object object>)[source]
+

CARD4L compliant SAR NRB generation

+
+
Parameters:
+
    +
  • data – The source data cube containing SAR input.

  • +
  • elevation_model – The digital elevation model to use. Set to null (the default) to allow the back- +end to choose, which will improve portability, but reduce reproducibility.

  • +
  • contributing_area – If set to true, a DEM-based local contributing area band named +contributing_area is added. The values are given in square meters.

  • +
  • ellipsoid_incidence_angle – If set to true, an ellipsoidal incidence angle band named +ellipsoid_incidence_angle is added. The values are given in degrees.

  • +
  • noise_removal – If set to false, no noise removal is applied. Defaults to true, which removes +noise.

  • +
  • options – Proprietary options for the backscatter computations. Specifying proprietary options will +reduce portability.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Backscatter values expressed as gamma0 in linear scale. In addition to the bands +contributing_area and ellipsoid_incidence_angle that can optionally be added with corresponding +parameters, the following bands are always added to the data cube: - mask: A data mask that indicates +which values are valid (1), invalid (0) or contain no-data (null). - local_incidence_angle: A band with +DEM-based local incidence angles in degrees. The data returned is CARD4L compliant with corresponding +metadata.

+
+
+
+

See also

+

openeo.org documentation on process “ard_normalized_radar_backscatter”.

+
+
+ +
+
+openeo.processes.ard_surface_reflectance(data, atmospheric_correction_method, cloud_detection_method, elevation_model=<object object>, atmospheric_correction_options=<object object>, cloud_detection_options=<object object>)[source]
+

CARD4L compliant Surface Reflectance generation

+
+
Parameters:
+
    +
  • data – The source data cube containing multi-spectral optical top of the atmosphere (TOA) +reflectances. There must be a single dimension of type bands available.

  • +
  • atmospheric_correction_method – The atmospheric correction method to use.

  • +
  • cloud_detection_method – The cloud detection method to use. Each method supports detecting different +atmospheric disturbances such as clouds, cloud shadows, aerosols, haze, ozone and/or water vapour in +optical imagery.

  • +
  • elevation_model – The digital elevation model to use. Set to null (the default) to allow the back- +end to choose, which will improve portability, but reduce reproducibility.

  • +
  • atmospheric_correction_options – Proprietary options for the atmospheric correction method. +Specifying proprietary options will reduce portability.

  • +
  • cloud_detection_options – Proprietary options for the cloud detection method. Specifying proprietary +options will reduce portability.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Data cube containing bottom of atmosphere reflectances for each spectral band in the source data +cube, with atmospheric disturbances like clouds and cloud shadows removed. No-data values (null) are +directly set in the bands. Depending on the methods used, several additional bands will be added to the +data cube: Data cube containing bottom of atmosphere reflectances for each spectral band in the source +data cube, with atmospheric disturbances like clouds and cloud shadows removed. Depending on the methods +used, several additional bands will be added to the data cube: - date (optional): Specifies per-pixel +acquisition timestamps. - incomplete-testing (required): Identifies pixels with a value of 1 for which +the per-pixel tests (at least saturation, cloud and cloud shadows, see CARD4L specification for details) +have not all been successfully completed. Otherwise, the value is 0. - saturation (required) / +saturation_{band} (optional): Indicates where pixels in the input spectral bands are saturated (1) or not +(0). If the saturation is given per band, the band names are saturation_{band} with {band} being the +band name from the source data cube. - cloud, shadow (both required),`aerosol`, haze, ozone, +water_vapor (all optional): Indicates the probability of pixels being an atmospheric disturbance such as +clouds. All bands have values between 0 (clear) and 1, which describes the probability that it is an +atmospheric disturbance. - snow-ice (optional): Points to a file that indicates whether a pixel is +assessed as being snow/ice (1) or not (0). All values describe the probability and must be between 0 and 1. +- land-water (optional): Indicates whether a pixel is assessed as being land (1) or water (0). All values +describe the probability and must be between 0 and 1. - incidence-angle (optional): Specifies per-pixel +incidence angles in degrees. - azimuth (optional): Specifies per-pixel azimuth angles in degrees. - sun- +azimuth: (optional): Specifies per-pixel sun azimuth angles in degrees. - sun-elevation (optional): +Specifies per-pixel sun elevation angles in degrees. - terrain-shadow (optional): Indicates with a value +of 1 whether a pixel is not directly illuminated due to terrain shadowing. Otherwise, the value is 0. - +terrain-occlusion (optional): Indicates with a value of 1 whether a pixel is not visible to the sensor +due to terrain occlusion during off-nadir viewing. Otherwise, the value is 0. - terrain-illumination +(optional): Contains coefficients used for terrain illumination correction are provided for each pixel. +The data returned is CARD4L compliant with corresponding metadata.

+
+
+
+

See also

+

openeo.org documentation on process “ard_surface_reflectance”.

+
+
+ +
+
+openeo.processes.array_append(data, value, label=<object object>)[source]
+

Append a value to an array

+
+
Parameters:
+
    +
  • data – An array.

  • +
  • value – Value to append to the array.

  • +
  • label – If the given array is a labeled array, a new label for the new value should be given. If not +given or null, the array index as string is used as the label. If in any case the label exists, a +LabelExists exception is thrown.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The new array with the value being appended.

+
+
+
+

See also

+

openeo.org documentation on process “array_append”.

+
+
+ +
+
+openeo.processes.array_apply(data, process, context=<object object>)[source]
+

Apply a process to each array element

+
+
Parameters:
+
    +
  • data – An array.

  • +
  • process – A process that accepts and returns a single value and is applied on each individual value +in the array. The process may consist of multiple sub-processes and could, for example, consist of +processes such as absolute() or linear_scale_range().

  • +
  • context – Additional data to be passed to the process.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the newly computed values. The number of elements are the same as for the original +array.

+
+
+
+

See also

+

openeo.org documentation on process “array_apply”.

+
+
+ +
+
+openeo.processes.array_concat(array1, array2)[source]
+

Merge two arrays

+
+
Parameters:
+
    +
  • array1 – The first array.

  • +
  • array2 – The second array.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The merged array.

+
+
+
+

See also

+

openeo.org documentation on process “array_concat”.

+
+
+ +
+
+openeo.processes.array_contains(data, value)[source]
+

Check whether the array contains a given value

+
+
Parameters:
+
    +
  • data – List to find the value in.

  • +
  • value – Value to find in data. If the value is null, this process returns always false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if the list contains the value, false` otherwise.

+
+
+
+

See also

+

openeo.org documentation on process “array_contains”.

+
+
+ +
+
+openeo.processes.array_create(data=<object object>, repeat=<object object>)[source]
+

Create an array

+
+
Parameters:
+
    +
  • data – A (native) array to fill the newly created array with. Defaults to an empty array.

  • +
  • repeat – The number of times the (native) array specified in data is repeatedly added after each +other to the new array being created. Defaults to 1.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The newly created array.

+
+
+
+

See also

+

openeo.org documentation on process “array_create”.

+
+
+ +
+
+openeo.processes.array_create_labeled(data, labels)[source]
+

Create a labeled array

+
+
Parameters:
+
    +
  • data – An array of values to be used.

  • +
  • labels – An array of labels to be used.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The newly created labeled array.

+
+
+
+

See also

+

openeo.org documentation on process “array_create_labeled”.

+
+
+ +
+
+openeo.processes.array_element(data, index=<object object>, label=<object object>, return_nodata=<object object>)[source]
+

Get an element from an array

+
+
Parameters:
+
    +
  • data – An array.

  • +
  • index – The zero-based index of the element to retrieve.

  • +
  • label – The label of the element to retrieve. Throws an ArrayNotLabeled exception, if the given +array is not a labeled array and this parameter is set.

  • +
  • return_nodata – By default this process throws an ArrayElementNotAvailable exception if the index +or label is invalid. If you want to return null instead, set this flag to true.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The value of the requested element.

+
+
+
+

See also

+

openeo.org documentation on process “array_element”.

+
+
+ +
+
+openeo.processes.array_filter(data, condition, context=<object object>)[source]
+

Filter an array based on a condition

+
+
Parameters:
+
    +
  • data – An array.

  • +
  • condition – A condition that is evaluated against each value, index and/or label in the array. Only +the array elements for which the condition returns true are preserved.

  • +
  • context – Additional data to be passed to the condition.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array filtered by the specified condition. The number of elements are less than or equal +compared to the original array.

+
+
+
+

See also

+

openeo.org documentation on process “array_filter”.

+
+
+ +
+
+openeo.processes.array_find(data, value, reverse=<object object>)[source]
+

Get the index for a value in an array

+
+
Parameters:
+
    +
  • data – List to find the value in.

  • +
  • value – Value to find in data. If the value is null, this process returns always null.

  • +
  • reverse – By default, this process finds the index of the first match. To return the index of the +last match instead, set this flag to true.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The index of the first element with the specified value. If no element was found, null is +returned.

+
+
+
+

See also

+

openeo.org documentation on process “array_find”.

+
+
+ +
+
+openeo.processes.array_find_label(data, label)[source]
+

Get the index for a label in a labeled array

+
+
Parameters:
+
    +
  • data – List to find the label in.

  • +
  • label – Label to find in data.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The index of the element with the specified label assigned. If no such label was found, null is +returned.

+
+
+
+

See also

+

openeo.org documentation on process “array_find_label”.

+
+
+ +
+
+openeo.processes.array_interpolate_linear(data)[source]
+

One-dimensional linear interpolation for arrays

+
+
Parameters:
+

data – An array of numbers and no-data values. If the given array is a labeled array, the labels +must have a natural/inherent label order and the process expects the labels to be sorted accordingly. This +is the default behavior in openEO for spatial and temporal dimensions.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with no-data values being replaced with interpolated values. If not at least 2 numerical +values are available in the array, the array stays the same.

+
+
+
+

See also

+

openeo.org documentation on process “array_interpolate_linear”.

+
+
+ +
+
+openeo.processes.array_labels(data)[source]
+

Get the labels for an array

+
+
Parameters:
+

data – An array.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The labels or indices as array.

+
+
+
+

See also

+

openeo.org documentation on process “array_labels”.

+
+
+ +
+
+openeo.processes.array_modify(data, values, index, length=<object object>)[source]
+

Change the content of an array (remove, insert, update)

+
+
Parameters:
+
    +
  • data – The array to modify.

  • +
  • values – The values to insert into the data array.

  • +
  • index – The index in the data array of the element to insert the value(s) before. If the index is +greater than the number of elements in the data array, the process throws an ArrayElementNotAvailable +exception. To insert after the last element, there are two options: 1. Use the simpler processes +array_append() to append a single value or array_concat() to append multiple values. 2. Specify the +number of elements in the array. You can retrieve the number of elements with the process count(), +having the parameter condition set to true.

  • +
  • length – The number of elements in the data array to remove (or replace) starting from the given +index. If the array contains fewer elements, the process simply removes all elements up to the end.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with values added, updated or removed.

+
+
+
+

See also

+

openeo.org documentation on process “array_modify”.

+
+
+ +
+
+openeo.processes.arsinh(x)[source]
+

Inverse hyperbolic sine

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “arsinh”.

+
+
+ +
+
+openeo.processes.artanh(x)[source]
+

Inverse hyperbolic tangent

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed angle in radians.

+
+
+
+

See also

+

openeo.org documentation on process “artanh”.

+
+
+ +
+
+openeo.processes.atmospheric_correction(data, method, elevation_model=<object object>, options=<object object>)[source]
+

Apply atmospheric correction

+
+
Parameters:
+
    +
  • data – Data cube containing multi-spectral optical top of atmosphere reflectances to be corrected.

  • +
  • method – The atmospheric correction method to use. To get reproducible results, you have to set a +specific method. Set to null to allow the back-end to choose, which will improve portability, but reduce +reproducibility as you may get different results if you run the processes multiple times.

  • +
  • elevation_model – The digital elevation model to use. Set to null (the default) to allow the back- +end to choose, which will improve portability, but reduce reproducibility.

  • +
  • options – Proprietary options for the atmospheric correction method. Specifying proprietary options +will reduce portability.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Data cube containing bottom of atmosphere reflectances.

+
+
+
+

See also

+

openeo.org documentation on process “atmospheric_correction”.

+
+
+ +
+
+openeo.processes.between(x, min, max, exclude_max=<object object>)[source]
+

Between comparison

+
+
Parameters:
+
    +
  • x – The value to check.

  • +
  • min – Lower boundary (inclusive) to check against.

  • +
  • max – Upper boundary (inclusive) to check against.

  • +
  • exclude_max – Exclude the upper boundary max if set to true. Defaults to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is between the specified bounds, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “between”.

+
+
+ +
+
+openeo.processes.ceil(x)[source]
+

Round fractions up

+
+
Parameters:
+

x – A number to round up.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The number rounded up.

+
+
+
+

See also

+

openeo.org documentation on process “ceil”.

+
+
+ +
+
+openeo.processes.climatological_normal(data, period, climatology_period=<object object>)[source]
+

Compute climatology normals

+
+
Parameters:
+
    +
  • data – A data cube with exactly one temporal dimension. The data cube must span at least the temporal +interval specified in the parameter climatology-period. Seasonal periods may span two consecutive years, +e.g. temporal winter that includes months December, January and February. If the required months before the +actual climate period are available, the season is taken into account. If not available, the first season +is not taken into account and the seasonal mean is based on one year less than the other seasonal normals. +The incomplete season at the end of the last year is never taken into account.

  • +
  • period – The time intervals to aggregate the average value for. The following pre-defined frequencies +are supported: * day: Day of the year * month: Month of the year * climatology-period: The period +specified in the climatology-period. * season: Three month periods of the calendar seasons (December - +February, March - May, June - August, September - November). * tropical-season: Six month periods of the +tropical seasons (November - April, May - October).

  • +
  • climatology_period – The climatology period as a closed temporal interval. The first element of the +array is the first year to be fully included in the temporal interval. The second element is the last year +to be fully included in the temporal interval. The default climatology period is from 1981 until 2010 +(both inclusive) right now, but this might be updated over time to what is commonly used in climatology. If +you don’t want to keep your research to be reproducible, please explicitly specify a period.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged, except for the resolution and dimension labels of the temporal +dimension. The temporal dimension has the following dimension labels: * day: 001 - 365 * month: +01 - 12 * climatology-period: climatology-period * season: djf (December - February), mam +(March - May), jja (June - August), son (September - November) * tropical-season: ndjfma (November +- April), mjjaso (May - October)

+
+
+
+

See also

+

openeo.org documentation on process “climatological_normal”.

+
+
+ +
+
+openeo.processes.clip(x, min, max)[source]
+

Clip a value between a minimum and a maximum

+
+
Parameters:
+
    +
  • x – A number.

  • +
  • min – Minimum value. If the value is lower than this value, the process will return the value of this +parameter.

  • +
  • max – Maximum value. If the value is greater than this value, the process will return the value of +this parameter.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The value clipped to the specified range.

+
+
+
+

See also

+

openeo.org documentation on process “clip”.

+
+
+ +
+
+openeo.processes.cloud_detection(data, method, options=<object object>)[source]
+

Create cloud masks

+
+
Parameters:
+
    +
  • data – The source data cube containing multi-spectral optical top of the atmosphere (TOA) +reflectances on which to perform cloud detection.

  • +
  • method – The cloud detection method to use. To get reproducible results, you have to set a specific +method. Set to null to allow the back-end to choose, which will improve portability, but reduce +reproducibility as you may get different results if you run the processes multiple times.

  • +
  • options – Proprietary options for the cloud detection method. Specifying proprietary options will +reduce portability.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with bands for the atmospheric disturbances. Each of the masks contains values between +0 and 1. The data cube has the same spatial and temporal dimensions as the source data cube and a dimension +that contains a dimension label for each of the supported/considered atmospheric disturbance.

+
+
+
+

See also

+

openeo.org documentation on process “cloud_detection”.

+
+
+ +
+
+openeo.processes.constant(x)[source]
+

Define a constant value

+
+
Parameters:
+

x – The value of the constant.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The value of the constant.

+
+
+
+

See also

+

openeo.org documentation on process “constant”.

+
+
+ +
+
+openeo.processes.cos(x)[source]
+

Cosine

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed cosine of x.

+
+
+
+

See also

+

openeo.org documentation on process “cos”.

+
+
+ +
+
+openeo.processes.cosh(x)[source]
+

Hyperbolic cosine

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed hyperbolic cosine of x.

+
+
+
+

See also

+

openeo.org documentation on process “cosh”.

+
+
+ +
+
+openeo.processes.count(data, condition=<object object>, context=<object object>)[source]
+

Count the number of elements

+
+
Parameters:
+
    +
  • data – An array with elements of any data type.

  • +
  • condition – A condition consists of one or more processes, which in the end return a boolean value. +It is evaluated against each element in the array. An element is counted only if the condition returns +true. Defaults to count valid elements in a list (see is_valid()). Setting this parameter to boolean +true counts all elements in the list. false is not a valid value for this parameter.

  • +
  • context – Additional data to be passed to the condition.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The counted number of elements.

+
+
+
+

See also

+

openeo.org documentation on process “count”.

+
+
+ +
+
+openeo.processes.create_data_cube()[source]
+

Create an empty data cube

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An empty data cube with no dimensions.

+
+
+
+

See also

+

openeo.org documentation on process “create_data_cube”.

+
+
+ +
+
+openeo.processes.cummax(data, ignore_nodata=<object object>)[source]
+

Cumulative maxima

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default. +Setting this flag to false considers no-data values so that null is set for all the following elements.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the computed cumulative maxima.

+
+
+
+

See also

+

openeo.org documentation on process “cummax”.

+
+
+ +
+
+openeo.processes.cummin(data, ignore_nodata=<object object>)[source]
+

Cumulative minima

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default. +Setting this flag to false considers no-data values so that null is set for all the following elements.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the computed cumulative minima.

+
+
+
+

See also

+

openeo.org documentation on process “cummin”.

+
+
+ +
+
+openeo.processes.cumproduct(data, ignore_nodata=<object object>)[source]
+

Cumulative products

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default. +Setting this flag to false considers no-data values so that null is set for all the following elements.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the computed cumulative products.

+
+
+
+

See also

+

openeo.org documentation on process “cumproduct”.

+
+
+ +
+
+openeo.processes.cumsum(data, ignore_nodata=<object object>)[source]
+

Cumulative sums

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not and ignores them by default. +Setting this flag to false considers no-data values so that null is set for all the following elements.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the computed cumulative sums.

+
+
+
+

See also

+

openeo.org documentation on process “cumsum”.

+
+
+ +
+
+openeo.processes.date_between(x, min, max, exclude_max=<object object>)[source]
+

Between comparison for dates and times

+
+
Parameters:
+
    +
  • x – The value to check.

  • +
  • min – Lower boundary (inclusive) to check against.

  • +
  • max – Upper boundary (inclusive) to check against.

  • +
  • exclude_max – Exclude the upper boundary max if set to true. Defaults to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is between the specified bounds, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “date_between”.

+
+
+ +
+
+openeo.processes.date_difference(date1, date2, unit=<object object>)[source]
+

Computes the difference between two time instants

+
+
Parameters:
+
    +
  • date1 – The base date, optionally with a time component.

  • +
  • date2 – The other date, optionally with a time component.

  • +
  • unit – The unit for the returned value. The following units are available: - millisecond - second - +leap seconds are ignored in computations. - minute - hour - day - month - year

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns the difference between date1 and date2 in the given unit (seconds by default), including a +fractional part if required. For comparison purposes this means: - If date1 < date2, the returned +value is positive. - If date1 = date2, the returned value is 0. - If date1 > date2, the returned +value is negative.

+
+
+
+

See also

+

openeo.org documentation on process “date_difference”.

+
+
+ +
+
+openeo.processes.date_shift(date, value, unit)[source]
+

Manipulates dates and times by addition or subtraction

+
+
Parameters:
+
    +
  • date – The date (and optionally time) to manipulate. If the given date doesn’t include the time, the +process assumes that the time component is 00:00:00Z (i.e. midnight, in UTC). The millisecond part of the +time is optional and defaults to 0 if not given.

  • +
  • value – The period of time in the unit given that is added (positive numbers) or subtracted (negative +numbers). The value 0 doesn’t have any effect.

  • +
  • unit – The unit for the value given. The following pre-defined units are available: - millisecond: +Milliseconds - second: Seconds - leap seconds are ignored in computations. - minute: Minutes - hour: Hours +- day: Days - changes only the the day part of a date - week: Weeks (equivalent to 7 days) - month: Months +- year: Years Manipulations with the unit year, month, week or day do never change the time. If +any of the manipulations result in an invalid date or time, the corresponding part is rounded down to the +next valid date or time respectively. For example, adding a month to 2020-01-31 would result in +2020-02-29.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The manipulated date. If a time component was given in the parameter date, the time component is +returned with the date.

+
+
+
+

See also

+

openeo.org documentation on process “date_shift”.

+
+
+ +
+
+openeo.processes.dimension_labels(data, dimension)[source]
+

Get the dimension labels

+
+
Parameters:
+
    +
  • data – The data cube.

  • +
  • dimension – The name of the dimension to get the labels for.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The labels as an array.

+
+
+
+

See also

+

openeo.org documentation on process “dimension_labels”.

+
+
+ +
+
+openeo.processes.divide(x, y)[source]
+

Division of two numbers

+
+
Parameters:
+
    +
  • x – The dividend.

  • +
  • y – The divisor.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed result.

+
+
+
+

See also

+

openeo.org documentation on process “divide”.

+
+
+ +
+
+openeo.processes.drop_dimension(data, name)[source]
+

Remove a dimension

+
+
Parameters:
+
    +
  • data – The data cube to drop a dimension from.

  • +
  • name – Name of the dimension to drop.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube without the specified dimension. The number of dimensions decreases by one, but the +dimension properties (name, type, labels, reference system and resolution) for all other dimensions remain +unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “drop_dimension”.

+
+
+ +
+
+openeo.processes.e()[source]
+

Euler’s number (e)

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The numerical value of Euler’s number.

+
+
+
+

See also

+

openeo.org documentation on process “e”.

+
+
+ +
+
+openeo.processes.eq(x, y, delta=<object object>, case_sensitive=<object object>)[source]
+

Equal to comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
  • delta – Only applicable for comparing two numbers. If this optional parameter is set to a positive +non-zero number the equality of two numbers is checked against a delta value. This is especially useful to +circumvent problems with floating-point inaccuracy in machine-based computation. This option is basically +an alias for the following computation: lte(abs(minus([x, y]), delta)

  • +
  • case_sensitive – Only applicable for comparing two strings. Case sensitive comparison can be disabled +by setting this parameter to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is equal to y, null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “eq”.

+
+
+ +
+
+openeo.processes.exp(p)[source]
+

Exponentiation to the base e

+
+
Parameters:
+

p – The numerical exponent.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed value for e raised to the power of p.

+
+
+
+

See also

+

openeo.org documentation on process “exp”.

+
+
+ +
+
+openeo.processes.extrema(data, ignore_nodata=<object object>)[source]
+

Minimum and maximum values

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that an array with two null values is returned if any +value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array containing the minimum and maximum values for the specified numbers. The first element is +the minimum, the second element is the maximum. If the input array is empty both elements are set to +null.

+
+
+
+

See also

+

openeo.org documentation on process “extrema”.

+
+
+ +
+
+openeo.processes.filter_bands(data, bands=<object object>, wavelengths=<object object>)[source]
+

Filter the bands by names

+
+
Parameters:
+
    +
  • data – A data cube with bands.

  • +
  • bands – A list of band names. Either the unique band name (metadata field name in bands) or one of +the common band names (metadata field common_name in bands). If the unique band name and the common name +conflict, the unique band name has a higher priority. The order of the specified array defines the order +of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the +original order.

  • +
  • wavelengths – A list of sub-lists with each sub-list consisting of two elements. The first element is +the minimum wavelength and the second element is the maximum wavelength. Wavelengths are specified in +micrometers (μm). The order of the specified array defines the order of the bands in the data cube. If +multiple bands match the wavelengths, all matched bands are included in the original order.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube limited to a subset of its original bands. The dimensions and dimension properties +(name, type, labels, reference system and resolution) remain unchanged, except that the dimension of type +bands has less (or the same) dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_bands”.

+
+
+ +
+
+openeo.processes.filter_bbox(data, extent)[source]
+

Spatial filter using a bounding box

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • extent – A bounding box, which may include a vertical axis (see base and height).

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube restricted to the bounding box. The dimensions and dimension properties (name, type, +labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less (or +the same) dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_bbox”.

+
+
+ +
+
+openeo.processes.filter_labels(data, condition, dimension, context=<object object>)[source]
+

Filter dimension labels based on a condition

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • condition – A condition that is evaluated against each dimension label in the specified dimension. A +dimension label and the corresponding data is preserved for the given dimension, if the condition returns +true.

  • +
  • dimension – The name of the dimension to filter on. Fails with a DimensionNotAvailable exception if +the specified dimension does not exist.

  • +
  • context – Additional data to be passed to the condition.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged, except that the given dimension has less (or the same) dimension +labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_labels”.

+
+
+ +
+
+openeo.processes.filter_spatial(data, geometries)[source]
+

Spatial filter raster data cubes using geometries

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • geometries – One or more geometries used for filtering, given as GeoJSON or vector data cube. If +multiple geometries are provided, the union of them is used. Empty geometries are ignored. Limits the data +cube to the bounding box of the given geometries. No implicit masking gets applied. To mask the pixels of +the data cube use mask_polygon().

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube restricted to the specified geometries. The dimensions and dimension properties +(name, type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions +have less (or the same) dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_spatial”.

+
+
+ +
+
+openeo.processes.filter_temporal(data, extent, dimension=<object object>)[source]
+

Temporal filter based on temporal intervals

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • extent – Left-closed temporal interval, i.e. an array with exactly two elements: 1. The first +element is the start of the temporal interval. The specified time instant is included in the interval. +2. The second element is the end of the temporal interval. The specified time instant is excluded from +the interval. The second element must always be greater/later than the first element. Otherwise, a +TemporalExtentEmpty exception is thrown. Also supports unbounded intervals by setting one of the +boundaries to null, but never both.

  • +
  • dimension – The name of the temporal dimension to filter on. If no specific dimension is specified, +the filter applies to all temporal dimensions. Fails with a DimensionNotAvailable exception if the +specified dimension does not exist.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube restricted to the specified temporal extent. The dimensions and dimension properties +(name, type, labels, reference system and resolution) remain unchanged, except that the temporal dimensions +(determined by dimensions parameter) may have less dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_temporal”.

+
+
+ +
+
+openeo.processes.filter_vector(data, geometries, relation=<object object>)[source]
+

Spatial vector filter using geometries

+
+
Parameters:
+
    +
  • data – A vector data cube with the candidate geometries.

  • +
  • geometries – One or more base geometries used for filtering, given as vector data cube. If multiple +base geometries are provided, the union of them is used.

  • +
  • relation – The spatial filter predicate for comparing the geometries provided through (a) +geometries (base geometries) and (b) data (candidate geometries).

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A vector data cube restricted to the specified geometries. The dimensions and dimension properties +(name, type, labels, reference system and resolution) remain unchanged, except that the geometries +dimension has less (or the same) dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_vector”.

+
+
+ +
+
+openeo.processes.first(data, ignore_nodata=<object object>)[source]
+

First element

+
+
Parameters:
+
    +
  • data – An array with elements of any data type.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if the first value is such a +value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The first element of the input array.

+
+
+
+

See also

+

openeo.org documentation on process “first”.

+
+
+ +
+
+openeo.processes.fit_curve(data, parameters, function, ignore_nodata=<object object>)[source]
+

Curve fitting

+
+
Parameters:
+
    +
  • data – A labeled array, the labels correspond to the variable y and the values correspond to the +variable x.

  • +
  • parameters – Defined the number of parameters for the model function and provides an initial guess +for them. At least one parameter is required.

  • +
  • function – The model function. It must take the parameters to fit as array through the first argument +and the independent variable x as the second argument. It is recommended to store the model function as +a user-defined process on the back-end to be able to re-use the model function with the computed optimal +values for the parameters afterwards.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is passed to the model function.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the optimal values for the parameters.

+
+
+
+

See also

+

openeo.org documentation on process “fit_curve”.

+
+
+ +
+
+openeo.processes.flatten_dimensions(data, dimensions, target_dimension, label_separator=<object object>)[source]
+

Combine multiple dimensions into a single dimension

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • dimensions – The names of the dimension to combine. The order of the array defines the order in which +the dimension labels and values are combined (see the example in the process description). Fails with a +DimensionNotAvailable exception if at least one of the specified dimensions does not exist.

  • +
  • target_dimension – The name of the new target dimension. A new dimensions will be created with the +given names and type other (see add_dimension()). Fails with a TargetDimensionExists exception if a +dimension with the specified name exists.

  • +
  • label_separator – The string that will be used as a separator for the concatenated dimension labels. +To unambiguously revert the dimension labels with the process unflatten_dimension(), the given string +must not be contained in any of the dimension labels.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the new shape. The dimension properties (name, type, labels, reference system and +resolution) for all other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “flatten_dimensions”.

+
+
+ +
+
+openeo.processes.floor(x)[source]
+

Round fractions down

+
+
Parameters:
+

x – A number to round down.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The number rounded down.

+
+
+
+

See also

+

openeo.org documentation on process “floor”.

+
+
+ +
+
+openeo.processes.gt(x, y)[source]
+

Greater than comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is strictly greater than y or null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “gt”.

+
+
+ +
+
+openeo.processes.gte(x, y)[source]
+

Greater than or equal to comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is greater than or equal to y, null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “gte”.

+
+
+ +
+
+openeo.processes.if_(value, accept, reject=<object object>)[source]
+

If-Then-Else conditional

+
+
Parameters:
+
    +
  • value – A boolean value.

  • +
  • accept – A value that is returned if the boolean value is true.

  • +
  • reject – A value that is returned if the boolean value is not true. Defaults to null.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Either the accept or reject argument depending on the given boolean value.

+
+
+
+

See also

+

openeo.org documentation on process “if_”.

+
+
+ +
+
+openeo.processes.inspect(data, message=<object object>, code=<object object>, level=<object object>)[source]
+

Add information to the logs

+
+
Parameters:
+
    +
  • data – Data to log.

  • +
  • message – A message to send in addition to the data.

  • +
  • code – A label to help identify one or more log entries originating from this process in the list of +all log entries. It can help to group or filter log entries and is usually not unique.

  • +
  • level – The severity level of this message, defaults to info.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The data as passed to the data parameter without any modification.

+
+
+
+

See also

+

openeo.org documentation on process “inspect”.

+
+
+ +
+
+openeo.processes.int(x)[source]
+

Integer part of a number

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Integer part of the number.

+
+
+
+

See also

+

openeo.org documentation on process “int”.

+
+
+ +
+
+openeo.processes.is_infinite(x)[source]
+

Value is an infinite number

+
+
Parameters:
+

x – The data to check.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if the data is an infinite number, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “is_infinite”.

+
+
+ +
+
+openeo.processes.is_nan(x)[source]
+

Value is not a number

+
+
Parameters:
+

x – The data to check.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns true for NaN and all non-numeric data types, otherwise returns false.

+
+
+
+

See also

+

openeo.org documentation on process “is_nan”.

+
+
+ +
+
+openeo.processes.is_nodata(x)[source]
+

Value is a no-data value

+
+
Parameters:
+

x – The data to check.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if the data is a no-data value, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “is_nodata”.

+
+
+ +
+
+openeo.processes.is_valid(x)[source]
+

Value is valid data

+
+
Parameters:
+

x – The data to check.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if the data is valid, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “is_valid”.

+
+
+ +
+
+openeo.processes.last(data, ignore_nodata=<object object>)[source]
+

Last element

+
+
Parameters:
+
    +
  • data – An array with elements of any data type.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if the last value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The last element of the input array.

+
+
+
+

See also

+

openeo.org documentation on process “last”.

+
+
+ +
+
+openeo.processes.linear_scale_range(x, inputMin, inputMax, outputMin=<object object>, outputMax=<object object>)[source]
+

Linear transformation between two ranges

+
+
Parameters:
+
    +
  • x – A number to transform. The number gets clipped to the bounds specified in inputMin and +inputMax.

  • +
  • inputMin – Minimum value the input can obtain.

  • +
  • inputMax – Maximum value the input can obtain.

  • +
  • outputMin – Minimum value of the desired output range.

  • +
  • outputMax – Maximum value of the desired output range.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The transformed number.

+
+
+
+

See also

+

openeo.org documentation on process “linear_scale_range”.

+
+
+ +
+
+openeo.processes.ln(x)[source]
+

Natural logarithm

+
+
Parameters:
+

x – A number to compute the natural logarithm for.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed natural logarithm.

+
+
+
+

See also

+

openeo.org documentation on process “ln”.

+
+
+ +
+
+openeo.processes.load_collection(id, spatial_extent, temporal_extent, bands=<object object>, properties=<object object>)[source]
+

Load a collection

+
+
Parameters:
+
    +
  • id – The collection id.

  • +
  • spatial_extent – Limits the data to load from the collection to the specified bounding box or +polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel +center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard +by the OGC). * For vector data, the process loads the geometry into the data cube if the geometry is fully +within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be +one of the following feature types: * A Polygon or MultiPolygon geometry, * a Feature with a +Polygon or MultiPolygon geometry, or * a FeatureCollection containing at least one Feature with +Polygon or MultiPolygon geometries. * Empty geometries are ignored. Set this parameter to null to +set no limit for the spatial extent. Be careful with this when loading large datasets! It is recommended to +use this parameter instead of using filter_bbox() or filter_spatial() directly after loading +unbounded data.

  • +
  • temporal_extent – Limits the data to load from the collection to the specified left-closed temporal +interval. Applies to all temporal dimensions. The interval has to be specified as an array with exactly two +elements: 1. The first element is the start of the temporal interval. The specified time instant is +included in the interval. 2. The second element is the end of the temporal interval. The specified time +instant is excluded from the interval. The second element must always be greater/later than the first +element. Otherwise, a TemporalExtentEmpty exception is thrown. Also supports unbounded intervals by +setting one of the boundaries to null, but never both. Set this parameter to null to set no limit for +the temporal extent. Be careful with this when loading large datasets! It is recommended to use this +parameter instead of using filter_temporal() directly after loading unbounded data.

  • +
  • bands – Only adds the specified bands into the data cube so that bands that don’t match the list of +band names are not available. Applies to all dimensions of type bands. Either the unique band name +(metadata field name in bands) or one of the common band names (metadata field common_name in bands) +can be specified. If the unique band name and the common name conflict, the unique band name has a higher +priority. The order of the specified array defines the order of the bands in the data cube. If multiple +bands match a common name, all matched bands are included in the original order. It is recommended to use +this parameter instead of using filter_bands() directly after loading unbounded data.

  • +
  • properties – Limits the data by metadata properties to include only data in the data cube which all +given conditions return true for (AND operation). Specify key-value-pairs with the key being the name of +the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value +must be a condition (user-defined process) to be evaluated against the collection metadata, see the +example.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube for further processing. The dimensions and dimension properties (name, type, labels, +reference system and resolution) correspond to the collection’s metadata, but the dimension labels are +restricted as specified in the parameters.

+
+
+
+

See also

+

openeo.org documentation on process “load_collection”.

+
+
+ +
+
+openeo.processes.load_geojson(data, properties=<object object>)[source]
+

Converts GeoJSON into a vector data cube

+
+
Parameters:
+
    +
  • data – A GeoJSON object to convert into a vector data cube. The GeoJSON type GeometryCollection is +not supported. Each geometry in the GeoJSON data results in a dimension label in the geometries +dimension.

  • +
  • properties – A list of properties from the GeoJSON file to construct an additional dimension from. A +new dimension with the name properties and type other is created if at least one property is provided. +Only applies for GeoJSON Features and FeatureCollections. Missing values are generally set to no-data +(null). Depending on the number of properties provided, the process creates the dimension differently: +- Single property with scalar values: A single dimension label with the name of the property and a single +value per geometry. - Single property of type array: The dimension labels correspond to the array indices. +There are as many values and labels per geometry as there are for the largest array. - Multiple properties +with scalar values: The dimension labels correspond to the property names. There are as many values and +labels per geometry as there are properties provided here.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A vector data cube containing the geometries, either one or two dimensional.

+
+
+
+

See also

+

openeo.org documentation on process “load_geojson”.

+
+
+ +
+
+openeo.processes.load_ml_model(id)[source]
+

Load a ML model

+
+
Parameters:
+

id – The STAC Item to load the machine learning model from. The STAC Item must implement the ml- +model extension.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A machine learning model to be used with machine learning processes such as +predict_random_forest().

+
+
+
+

See also

+

openeo.org documentation on process “load_ml_model”.

+
+
+ +
+
+openeo.processes.load_result(id, spatial_extent=<object object>, temporal_extent=<object object>, bands=<object object>)[source]
+

Load batch job results

+
+
Parameters:
+
    +
  • id – The id of a batch job with results.

  • +
  • spatial_extent – Limits the data to load from the batch job result to the specified bounding box or +polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel +center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard +by the OGC). * For vector data, the process loads the geometry into the data cube of the geometry is fully +within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be +one of the following feature types: * A Polygon or MultiPolygon geometry, * a Feature with a +Polygon or MultiPolygon geometry, or * a FeatureCollection containing at least one Feature with +Polygon or MultiPolygon geometries. Set this parameter to null to set no limit for the spatial +extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead +of using filter_bbox() or filter_spatial() directly after loading unbounded data.

  • +
  • temporal_extent – Limits the data to load from the batch job result to the specified left-closed +temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array with +exactly two elements: 1. The first element is the start of the temporal interval. The specified instance +in time is included in the interval. 2. The second element is the end of the temporal interval. The +specified instance in time is excluded from the interval. The specified temporal strings follow [RFC +3339](https://www.rfc-editor.org/rfc/rfc3339.html). Also supports open intervals by setting one of the +boundaries to null, but never both. Set this parameter to null to set no limit for the temporal +extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead +of using filter_temporal() directly after loading unbounded data.

  • +
  • bands – Only adds the specified bands into the data cube so that bands that don’t match the list of +band names are not available. Applies to all dimensions of type bands. Either the unique band name +(metadata field name in bands) or one of the common band names (metadata field common_name in bands) +can be specified. If the unique band name and the common name conflict, the unique band name has a higher +priority. The order of the specified array defines the order of the bands in the data cube. If multiple +bands match a common name, all matched bands are included in the original order. It is recommended to use +this parameter instead of using filter_bands() directly after loading unbounded data.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube for further processing.

+
+
+
+

See also

+

openeo.org documentation on process “load_result”.

+
+
+ +
+
+openeo.processes.load_stac(url, spatial_extent=<object object>, temporal_extent=<object object>, bands=<object object>, properties=<object object>)[source]
+

Loads data from STAC

+
+
Parameters:
+
    +
  • url – The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a specific +STAC API Collection that allows to filter items and to download assets. This includes batch job results, +which itself are compliant to STAC. For external URLs, authentication details such as API keys or tokens +may need to be included in the URL. Batch job results can be specified in two ways: - For Batch job +results at the same back-end, a URL pointing to the corresponding batch job results endpoint should be +provided. The URL usually ends with /jobs/{id}/results and {id} is the corresponding batch job ID. - +For external results, a signed URL must be provided. Not all back-ends support signed URLs, which are +provided as a link with the link relation canonical in the batch job result metadata.

  • +
  • spatial_extent – Limits the data to load to the specified bounding box or polygons. * For raster +data, the process loads the pixel into the data cube if the point at the pixel center intersects with the +bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). * For vector +data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or +any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be +in the data cube if no spatial extent has been provided. The GeoJSON can be one of the following feature +types: * A Polygon or MultiPolygon geometry, * a Feature with a Polygon or MultiPolygon +geometry, or * a FeatureCollection containing at least one Feature with Polygon or MultiPolygon +geometries. Set this parameter to null to set no limit for the spatial extent. Be careful with this when +loading large datasets! It is recommended to use this parameter instead of using filter_bbox() or +filter_spatial() directly after loading unbounded data.

  • +
  • temporal_extent – Limits the data to load to the specified left-closed temporal interval. Applies to +all temporal dimensions. The interval has to be specified as an array with exactly two elements: 1. The +first element is the start of the temporal interval. The specified instance in time is included in the +interval. 2. The second element is the end of the temporal interval. The specified instance in time is +excluded from the interval. The second element must always be greater/later than the first element. +Otherwise, a TemporalExtentEmpty exception is thrown. Also supports open intervals by setting one of the +boundaries to null, but never both. Set this parameter to null to set no limit for the temporal +extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead +of using filter_temporal() directly after loading unbounded data.

  • +
  • bands – Only adds the specified bands into the data cube so that bands that don’t match the list of +band names are not available. Applies to all dimensions of type bands. Either the unique band name +(metadata field name in bands) or one of the common band names (metadata field common_name in bands) +can be specified. If the unique band name and the common name conflict, the unique band name has a higher +priority. The order of the specified array defines the order of the bands in the data cube. If multiple +bands match a common name, all matched bands are included in the original order. It is recommended to use +this parameter instead of using filter_bands() directly after loading unbounded data.

  • +
  • properties – Limits the data by metadata properties to include only data in the data cube which all +given conditions return true for (AND operation). Specify key-value-pairs with the key being the name of +the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value +must be a condition (user-defined process) to be evaluated against a STAC API. This parameter is not +supported for static STAC.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube for further processing.

+
+
+
+

See also

+

openeo.org documentation on process “load_stac”.

+
+
+ +
+
+openeo.processes.load_uploaded_files(paths, format, options=<object object>)[source]
+

Load files from the user workspace

+
+
Parameters:
+
    +
  • paths – The files to read. Folders can’t be specified, specify all files instead. An exception is +thrown if a file can’t be read.

  • +
  • format – The file format to read from. It must be one of the values that the server reports as +supported input file formats, which usually correspond to the short GDAL/OGR codes. If the format is not +suitable for loading the data, a FormatUnsuitable exception will be thrown. This parameter is case +insensitive.

  • +
  • options – The file format parameters to be used to read the files. Must correspond to the parameters +that the server reports as supported parameters for the chosen format. The parameter names and valid +values usually correspond to the GDAL/OGR format options.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube for further processing.

+
+
+
+

See also

+

openeo.org documentation on process “load_uploaded_files”.

+
+
+ +
+
+openeo.processes.load_url(url, format, options=<object object>)[source]
+

Load data from a URL

+
+
Parameters:
+
    +
  • url – The URL to read from. Authentication details such as API keys or tokens may need to be included +in the URL.

  • +
  • format – The file format to use when loading the data. It must be one of the values that the server +reports as supported input file formats, which usually correspond to the short GDAL/OGR codes. If the +format is not suitable for loading the data, a FormatUnsuitable exception will be thrown. This parameter +is case insensitive.

  • +
  • options – The file format parameters to use when reading the data. Must correspond to the parameters +that the server reports as supported parameters for the chosen format. The parameter names and valid +values usually correspond to the GDAL/OGR format options.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube for further processing.

+
+
+
+

See also

+

openeo.org documentation on process “load_url”.

+
+
+ +
+
+openeo.processes.log(x, base)[source]
+

Logarithm to a base

+
+
Parameters:
+
    +
  • x – A number to compute the logarithm for.

  • +
  • base – The numerical base.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed logarithm.

+
+
+
+

See also

+

openeo.org documentation on process “log”.

+
+
+ +
+
+openeo.processes.lt(x, y)[source]
+

Less than comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is strictly less than y, null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “lt”.

+
+
+ +
+
+openeo.processes.lte(x, y)[source]
+

Less than or equal to comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is less than or equal to y, null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “lte”.

+
+
+ +
+
+openeo.processes.mask(data, mask, replacement=<object object>)[source]
+

Apply a raster mask

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • mask – A mask as a raster data cube. Every pixel in data must have a corresponding element in +mask.

  • +
  • replacement – The value used to replace masked values with.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, +reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “mask”.

+
+
+ +
+
+openeo.processes.mask_polygon(data, mask, replacement=<object object>, inside=<object object>)[source]
+

Apply a polygon mask

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • mask – A GeoJSON object or a vector data cube containing at least one polygon. The provided vector +data can be one of the following: * A Polygon or MultiPolygon geometry, * a Feature with a Polygon +or MultiPolygon geometry, or * a FeatureCollection containing at least one Feature with Polygon or +MultiPolygon geometries. * Empty geometries are ignored.

  • +
  • replacement – The value used to replace masked values with.

  • +
  • inside – If set to true all pixels for which the point at the pixel center does intersect with +any polygon are replaced.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, +reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “mask_polygon”.

+
+
+ +
+
+openeo.processes.max(data, ignore_nodata=<object object>)[source]
+

Maximum value

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The maximum value.

+
+
+
+

See also

+

openeo.org documentation on process “max”.

+
+
+ +
+
+openeo.processes.mean(data, ignore_nodata=<object object>)[source]
+

Arithmetic mean (average)

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed arithmetic mean.

+
+
+
+

See also

+

openeo.org documentation on process “mean”.

+
+
+ +
+
+openeo.processes.median(data, ignore_nodata=<object object>)[source]
+

Statistical median

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed statistical median.

+
+
+
+

See also

+

openeo.org documentation on process “median”.

+
+
+ +
+
+openeo.processes.merge_cubes(cube1, cube2, overlap_resolver=<object object>, context=<object object>)[source]
+

Merge two data cubes

+
+
Parameters:
+
    +
  • cube1 – The base data cube.

  • +
  • cube2 – The other data cube to be merged with the base data cube.

  • +
  • overlap_resolver – A reduction operator that resolves the conflict if the data overlaps. The reducer +must return a value of the same data type as the input values are. The reduction operator may be a single +process such as multiply() or consist of multiple sub-processes. null (the default) can be specified +if no overlap resolver is required.

  • +
  • context – Additional data to be passed to the overlap resolver.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The merged data cube. See the process description for details regarding the dimensions and +dimension properties (name, type, labels, reference system and resolution).

+
+
+
+

See also

+

openeo.org documentation on process “merge_cubes”.

+
+
+ +
+
+openeo.processes.min(data, ignore_nodata=<object object>)[source]
+

Minimum value

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The minimum value.

+
+
+
+

See also

+

openeo.org documentation on process “min”.

+
+
+ +
+
+openeo.processes.mod(x, y)[source]
+

Modulo

+
+
Parameters:
+
    +
  • x – A number to be used as the dividend.

  • +
  • y – A number to be used as the divisor.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The remainder after division.

+
+
+
+

See also

+

openeo.org documentation on process “mod”.

+
+
+ +
+
+openeo.processes.multiply(x, y)[source]
+

Multiplication of two numbers

+
+
Parameters:
+
    +
  • x – The multiplier.

  • +
  • y – The multiplicand.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed product of the two numbers.

+
+
+
+

See also

+

openeo.org documentation on process “multiply”.

+
+
+ +
+
+openeo.processes.nan()[source]
+

Not a Number (NaN)

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns NaN.

+
+
+
+

See also

+

openeo.org documentation on process “nan”.

+
+
+ +
+
+openeo.processes.ndvi(data, nir=<object object>, red=<object object>, target_band=<object object>)[source]
+

Normalized Difference Vegetation Index

+
+
Parameters:
+
    +
  • data – A raster data cube with two bands that have the common names red and nir assigned.

  • +
  • nir – The name of the NIR band. Defaults to the band that has the common name nir assigned. Either +the unique band name (metadata field name in bands) or one of the common band names (metadata field +common_name in bands) can be specified. If the unique band name and the common name conflict, the unique +band name has a higher priority.

  • +
  • red – The name of the red band. Defaults to the band that has the common name red assigned. Either +the unique band name (metadata field name in bands) or one of the common band names (metadata field +common_name in bands) can be specified. If the unique band name and the common name conflict, the unique +band name has a higher priority.

  • +
  • target_band – By default, the dimension of type bands is dropped. To keep the dimension specify a +new band name in this parameter so that a new dimension label with the specified name will be added for the +computed values.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube containing the computed NDVI values. The structure of the data cube differs +depending on the value passed to target_band: * target_band is null: The data cube does not contain +the dimension of type bands, the number of dimensions decreases by one. The dimension properties (name, +type, labels, reference system and resolution) for all other dimensions remain unchanged. * target_band +is a string: The data cube keeps the same dimensions. The dimension properties remain unchanged, but the +number of dimension labels for the dimension of type bands increases by one. The additional label is +named as specified in target_band.

+
+
+
+

See also

+

openeo.org documentation on process “ndvi”.

+
+
+ +
+
+openeo.processes.neq(x, y, delta=<object object>, case_sensitive=<object object>)[source]
+

Not equal to comparison

+
+
Parameters:
+
    +
  • x – First operand.

  • +
  • y – Second operand.

  • +
  • delta – Only applicable for comparing two numbers. If this optional parameter is set to a positive +non-zero number the non-equality of two numbers is checked against a delta value. This is especially useful +to circumvent problems with floating-point inaccuracy in machine-based computation. This option is +basically an alias for the following computation: gt(abs(minus([x, y]), delta)

  • +
  • case_sensitive – Only applicable for comparing two strings. Case sensitive comparison can be disabled +by setting this parameter to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if x is not equal to y, null if any operand is null, otherwise false.

+
+
+
+

See also

+

openeo.org documentation on process “neq”.

+
+
+ +
+
+openeo.processes.normalized_difference(x, y)[source]
+

Normalized difference

+
+
Parameters:
+
    +
  • x – The value for the first band.

  • +
  • y – The value for the second band.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed normalized difference.

+
+
+
+

See also

+

openeo.org documentation on process “normalized_difference”.

+
+
+ +
+
+openeo.processes.not_(x)[source]
+

Inverting a boolean

+
+
Parameters:
+

x – Boolean value to invert.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Inverted boolean value.

+
+
+
+

See also

+

openeo.org documentation on process “not_”.

+
+
+ +
+
+openeo.processes.or_(x, y)[source]
+

Logical OR

+
+
Parameters:
+
    +
  • x – A boolean value.

  • +
  • y – A boolean value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Boolean result of the logical OR.

+
+
+
+

See also

+

openeo.org documentation on process “or_”.

+
+
+ +
+
+openeo.processes.order(data, asc=<object object>, nodata=<object object>)[source]
+

Get the order of array elements

+
+
Parameters:
+
    +
  • data – An array to compute the order for.

  • +
  • asc – The default sort order is ascending, with smallest values first. To sort in reverse +(descending) order, set this parameter to false.

  • +
  • nodata – Controls the handling of no-data values (null). By default, they are removed. If set to +true, missing values in the data are put last; if set to false, they are put first.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed permutation.

+
+
+
+

See also

+

openeo.org documentation on process “order”.

+
+
+ +
+
+openeo.processes.pi()[source]
+

Pi (π)

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The numerical value of Pi.

+
+
+
+

See also

+

openeo.org documentation on process “pi”.

+
+
+ +
+
+openeo.processes.power(base, p)[source]
+

Exponentiation

+
+
Parameters:
+
    +
  • base – The numerical base.

  • +
  • p – The numerical exponent.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed value for base raised to the power of p.

+
+
+
+

See also

+

openeo.org documentation on process “power”.

+
+
+ +
+
+openeo.processes.predict_curve(parameters, function, dimension, labels=<object object>)[source]
+

Predict values

+
+
Parameters:
+
    +
  • parameters – A data cube with optimal values, e.g. computed by the process fit_curve().

  • +
  • function – The model function. It must take the parameters to fit as array through the first argument +and the independent variable x as the second argument. It is recommended to store the model function as +a user-defined process on the back-end.

  • +
  • dimension – The name of the dimension for predictions.

  • +
  • labels – The labels to predict values for. If no labels are given, predicts values only for no-data +(null) values in the data cube.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the predicted values with the provided dimension dimension having as many +labels as provided through labels.

+
+
+
+

See also

+

openeo.org documentation on process “predict_curve”.

+
+
+ +
+
+openeo.processes.predict_random_forest(data, model)[source]
+

Predict values based on a Random Forest model

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • model – A model object that can be trained with the processes fit_regr_random_forest() +(regression) and fit_class_random_forest() (classification).

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The predicted value. Returns null if any of the given values in the array is a no-data value.

+
+
+
+

See also

+

openeo.org documentation on process “predict_random_forest”.

+
+
+ +
+
+openeo.processes.product(data, ignore_nodata=<object object>)[source]
+

Compute the product by multiplying numbers

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed product of the sequence of numbers.

+
+
+
+

See also

+

openeo.org documentation on process “product”.

+
+
+ +
+
+openeo.processes.quantiles(data, probabilities=<object object>, q=<object object>, ignore_nodata=<object object>)[source]
+

Quantiles

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • probabilities – Quantiles to calculate. Either a list of probabilities or the number of intervals: * +Provide an array with a sorted list of probabilities in ascending order to calculate quantiles for. The +probabilities must be between 0 and 1 (inclusive). If not sorted in ascending order, an +AscendingProbabilitiesRequired exception is thrown. * Provide an integer to specify the number of +intervals to calculate quantiles for. Calculates q-quantiles with equal-sized intervals.

  • +
  • q – Number of intervals to calculate quantiles for. Calculates q-quantiles with equal-sized +intervals. This parameter has been deprecated. Please use the parameter probabilities instead.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that an array with null values is returned if any +element is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

An array with the computed quantiles. The list has either * as many elements as the given list of +probabilities had or * `q`-1 elements. If the input array is empty the resulting array is filled with +as many null values as required according to the list above. See the ‘Empty array’ example for an +example.

+
+
+
+

See also

+

openeo.org documentation on process “quantiles”.

+
+
+ +
+
+openeo.processes.rearrange(data, order)[source]
+

Sort an array based on a permutation

+
+
Parameters:
+
    +
  • data – The array to rearrange.

  • +
  • order – The permutation used for rearranging.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The rearranged array.

+
+
+
+

See also

+

openeo.org documentation on process “rearrange”.

+
+
+ +
+
+openeo.processes.reduce_dimension(data, reducer, dimension, context=<object object>)[source]
+

Reduce dimensions

+
+
Parameters:
+
    +
  • data – A data cube.

  • +
  • reducer – A reducer to apply on the specified dimension. A reducer is a single process such as +mean() or a set of processes, which computes a single value for a list of values, see the category +‘reducer’ for such processes.

  • +
  • dimension – The name of the dimension over which to reduce. Fails with a DimensionNotAvailable +exception if the specified dimension does not exist.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values. It is missing the given dimension, the number of +dimensions decreases by one. The dimension properties (name, type, labels, reference system and resolution) +for all other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “reduce_dimension”.

+
+
+ +
+
+openeo.processes.reduce_spatial(data, reducer, context=<object object>)[source]
+

Reduce spatial dimensions ‘x’ and ‘y’

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • reducer – A reducer to apply on the horizontal spatial dimensions. A reducer is a single process such +as mean() or a set of processes, which computes a single value for a list of values, see the category +‘reducer’ for such processes.

  • +
  • context – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the newly computed values. It is missing the horizontal spatial dimensions, the +number of dimensions decreases by two. The dimension properties (name, type, labels, reference system and +resolution) for all other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “reduce_spatial”.

+
+
+ +
+
+openeo.processes.rename_dimension(data, source, target)[source]
+

Rename a dimension

+
+
Parameters:
+
    +
  • data – The data cube.

  • +
  • source – The current name of the dimension. Fails with a DimensionNotAvailable exception if the +specified dimension does not exist.

  • +
  • target – A new Name for the dimension. Fails with a DimensionExists exception if a dimension with +the specified name exists.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the same dimensions, but the name of one of the dimensions changes. The old name +can not be referred to any longer. The dimension properties (name, type, labels, reference system and +resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “rename_dimension”.

+
+
+ +
+
+openeo.processes.rename_labels(data, dimension, target, source=<object object>)[source]
+

Rename dimension labels

+
+
Parameters:
+
    +
  • data – The data cube.

  • +
  • dimension – The name of the dimension to rename the labels for.

  • +
  • target – The new names for the labels. If a target dimension label already exists in the data cube, +a LabelExists exception is thrown.

  • +
  • source – The original names of the labels to be renamed to corresponding array elements in the +parameter target. It is allowed to only specify a subset of labels to rename, as long as the target and +source parameter have the same length. The order of the labels doesn’t need to match the order of the +dimension labels in the data cube. By default, the array is empty so that the dimension labels in the data +cube are expected to be enumerated. If the dimension labels are not enumerated and the given array is +empty, the LabelsNotEnumerated exception is thrown. If one of the source dimension labels doesn’t exist, +the LabelNotAvailable exception is thrown.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The data cube with the same dimensions. The dimension properties (name, type, labels, reference +system and resolution) remain unchanged, except that for the given dimension the labels change. The old +labels can not be referred to any longer. The number of labels remains the same.

+
+
+
+

See also

+

openeo.org documentation on process “rename_labels”.

+
+
+ +
+
+openeo.processes.resample_cube_spatial(data, target, method=<object object>)[source]
+

Resample the spatial dimensions to match a target data cube

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • target – A raster data cube that describes the spatial target resolution.

  • +
  • method – Resampling method to use. The following options are available and are meant to align with +[gdalwarp](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * average: average (mean) +resampling, computes the weighted average of all valid pixels * bilinear: bilinear resampling * cubic: +cubic resampling * cubicspline: cubic spline resampling * lanczos: Lanczos windowed sinc resampling * +max: maximum resampling, selects the maximum value from all valid pixels * med: median resampling, +selects the median value of all valid pixels * min: minimum resampling, selects the minimum value from +all valid pixels * mode: mode resampling, selects the value which appears most often of all the sampled +points * near: nearest neighbour resampling (default) * q1: first quartile resampling, selects the +first quartile value of all valid pixels * q3: third quartile resampling, selects the third quartile +value of all valid pixels * rms root mean square (quadratic mean) of all valid pixels * sum: compute +the weighted sum of all valid pixels Valid pixels are determined based on the function is_valid().

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube with the same dimensions. The dimension properties (name, type, labels, +reference system and resolution) remain unchanged, except for the resolution and dimension labels of the +spatial dimensions.

+
+
+
+

See also

+

openeo.org documentation on process “resample_cube_spatial”.

+
+
+ +
+
+openeo.processes.resample_cube_temporal(data, target, dimension=<object object>, valid_within=<object object>)[source]
+

Resample temporal dimensions to match a target data cube

+
+
Parameters:
+
    +
  • data – A data cube with one or more temporal dimensions.

  • +
  • target – A data cube that describes the temporal target resolution.

  • +
  • dimension – The name of the temporal dimension to resample, which must exist with this name in both +data cubes. If the dimension is not set or is set to null, the process resamples all temporal dimensions +that exist with the same names in both data cubes. The following exceptions may occur: * A dimension is +given, but it does not exist in any of the data cubes: DimensionNotAvailable * A dimension is given, but +one of them is not temporal: DimensionMismatch * No specific dimension name is given and there are no +temporal dimensions with the same name in the data: DimensionMismatch

  • +
  • valid_within – Setting this parameter to a numerical value enables that the process searches for +valid values within the given period of days before and after the target timestamps. Valid values are +determined based on the function is_valid(). For example, the limit of 7 for the target timestamps +2020-01-15 12:00:00 looks for a nearest neighbor after 2020-01-08 12:00:00 and before 2020-01-22 +12:00:00. If no valid value is found within the given period, the value will be set to no-data (null).

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the same dimensions and the same dimension properties (name, type, labels, +reference system and resolution) for all non-temporal dimensions. For the temporal dimension, the name and +type remain unchanged, but the dimension labels, resolution and reference system may change.

+
+
+
+

See also

+

openeo.org documentation on process “resample_cube_temporal”.

+
+
+ +
+
+openeo.processes.resample_spatial(data, resolution=<object object>, projection=<object object>, method=<object object>, align=<object object>)[source]
+

Resample and warp the spatial dimensions

+
+
Parameters:
+
    +
  • data – A raster data cube.

  • +
  • resolution – Resamples the data cube to the target resolution, which can be specified either as +separate values for x and y or as a single value for both axes. Specified in the units of the target +projection. Doesn’t change the resolution by default (0).

  • +
  • projection – Warps the data cube to the target projection, specified as as [EPSG +code](http://www.epsg-registry.org/) or [WKT2 CRS +string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). By default (null), the projection is +not changed.

  • +
  • method – Resampling method to use. The following options are available and are meant to align with +[gdalwarp](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * average: average (mean) +resampling, computes the weighted average of all valid pixels * bilinear: bilinear resampling * cubic: +cubic resampling * cubicspline: cubic spline resampling * lanczos: Lanczos windowed sinc resampling * +max: maximum resampling, selects the maximum value from all valid pixels * med: median resampling, +selects the median value of all valid pixels * min: minimum resampling, selects the minimum value from +all valid pixels * mode: mode resampling, selects the value which appears most often of all the sampled +points * near: nearest neighbour resampling (default) * q1: first quartile resampling, selects the +first quartile value of all valid pixels * q3: third quartile resampling, selects the third quartile +value of all valid pixels * rms root mean square (quadratic mean) of all valid pixels * sum: compute +the weighted sum of all valid pixels Valid pixels are determined based on the function is_valid().

  • +
  • align – Specifies to which corner of the spatial extent the new resampled data is aligned to.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A raster data cube with values warped onto the new projection. It has the same dimensions and the +same dimension properties (name, type, labels, reference system and resolution) for all non-spatial or +vertical spatial dimensions. For the horizontal spatial dimensions the name and type remain unchanged, but +reference system, labels and resolution may change depending on the given parameters.

+
+
+
+

See also

+

openeo.org documentation on process “resample_spatial”.

+
+
+ +
+
+openeo.processes.round(x, p=<object object>)[source]
+

Round to a specified precision

+
+
Parameters:
+
    +
  • x – A number to round.

  • +
  • p – A positive number specifies the number of digits after the decimal point to round to. A negative +number means rounding to a power of ten, so for example -2 rounds to the nearest hundred. Defaults to +0.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The rounded number.

+
+
+
+

See also

+

openeo.org documentation on process “round”.

+
+
+ +
+
+openeo.processes.run_udf(data, udf, runtime, version=<object object>, context=<object object>)[source]
+

Run a UDF

+
+
Parameters:
+
    +
  • data – The data to be passed to the UDF.

  • +
  • udf – Either source code, an absolute URL or a path to a UDF script.

  • +
  • runtime – A UDF runtime identifier available at the back-end.

  • +
  • version – An UDF runtime version. If set to null, the default runtime version specified for each +runtime is used.

  • +
  • context – Additional data such as configuration options to be passed to the UDF.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The data processed by the UDF. The returned value can be of any data type and is exactly what the +UDF code returns.

+
+
+
+

See also

+

openeo.org documentation on process “run_udf”.

+
+
+ +
+
+openeo.processes.run_udf_externally(data, url, context=<object object>)[source]
+

Run an externally hosted UDF container

+
+
Parameters:
+
    +
  • data – The data to be passed to the UDF.

  • +
  • url – Absolute URL to a remote UDF service.

  • +
  • context – Additional data such as configuration options to be passed to the UDF.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The data processed by the UDF. The returned value can in principle be of any data type, but it +depends on what is returned by the UDF code. Please see the implemented UDF interface for details.

+
+
+
+

See also

+

openeo.org documentation on process “run_udf_externally”.

+
+
+ +
+
+openeo.processes.sar_backscatter(data, coefficient=<object object>, elevation_model=<object object>, mask=<object object>, contributing_area=<object object>, local_incidence_angle=<object object>, ellipsoid_incidence_angle=<object object>, noise_removal=<object object>, options=<object object>)[source]
+

Computes backscatter from SAR input

+
+
Parameters:
+
    +
  • data – The source data cube containing SAR input.

  • +
  • coefficient – Select the radiometric correction coefficient. The following options are available: * +beta0: radar brightness * sigma0-ellipsoid: ground area computed with ellipsoid earth model * +sigma0-terrain: ground area computed with terrain earth model * gamma0-ellipsoid: ground area computed +with ellipsoid earth model in sensor line of sight * gamma0-terrain: ground area computed with terrain +earth model in sensor line of sight (default) * null: non-normalized backscatter

  • +
  • elevation_model – The digital elevation model to use. Set to null (the default) to allow the back- +end to choose, which will improve portability, but reduce reproducibility.

  • +
  • mask – If set to true, a data mask is added to the bands with the name mask. It indicates which +values are valid (1), invalid (0) or contain no-data (null).

  • +
  • contributing_area – If set to true, a DEM-based local contributing area band named +contributing_area is added. The values are given in square meters.

  • +
  • local_incidence_angle – If set to true, a DEM-based local incidence angle band named +local_incidence_angle is added. The values are given in degrees.

  • +
  • ellipsoid_incidence_angle – If set to true, an ellipsoidal incidence angle band named +ellipsoid_incidence_angle is added. The values are given in degrees.

  • +
  • noise_removal – If set to false, no noise removal is applied. Defaults to true, which removes +noise.

  • +
  • options – Proprietary options for the backscatter computations. Specifying proprietary options will +reduce portability.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Backscatter values corresponding to the chosen parametrization. The values are given in linear +scale.

+
+
+
+

See also

+

openeo.org documentation on process “sar_backscatter”.

+
+
+ +
+
+openeo.processes.save_result(data, format, options=<object object>)[source]
+

Save processed data

+
+
Parameters:
+
    +
  • data – The data to deliver in the given file format.

  • +
  • format – The file format to use. It must be one of the values that the server reports as supported +output file formats, which usually correspond to the short GDAL/OGR codes. This parameter is case +insensitive. * If the data cube is empty and the file format can’t store empty data cubes, a +DataCubeEmpty exception is thrown. * If the file format is otherwise not suitable for storing the +underlying data structure, a FormatUnsuitable exception is thrown.

  • +
  • options – The file format parameters to be used to create the file(s). Must correspond to the +parameters that the server reports as supported parameters for the chosen format. The parameter names and +valid values usually correspond to the GDAL/OGR format options.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Always returns true as in case of an error an exception is thrown which aborts the execution of +the process.

+
+
+
+

See also

+

openeo.org documentation on process “save_result”.

+
+
+ +
+
+openeo.processes.sd(data, ignore_nodata=<object object>)[source]
+

Standard deviation

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed sample standard deviation.

+
+
+
+

See also

+

openeo.org documentation on process “sd”.

+
+
+ +
+
+openeo.processes.sgn(x)[source]
+

Signum

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed signum value of x.

+
+
+
+

See also

+

openeo.org documentation on process “sgn”.

+
+
+ +
+
+openeo.processes.sin(x)[source]
+

Sine

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed sine of x.

+
+
+
+

See also

+

openeo.org documentation on process “sin”.

+
+
+ +
+
+openeo.processes.sinh(x)[source]
+

Hyperbolic sine

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed hyperbolic sine of x.

+
+
+
+

See also

+

openeo.org documentation on process “sinh”.

+
+
+ +
+
+openeo.processes.sort(data, asc=<object object>, nodata=<object object>)[source]
+

Sort data

+
+
Parameters:
+
    +
  • data – An array with data to sort.

  • +
  • asc – The default sort order is ascending, with smallest values first. To sort in reverse +(descending) order, set this parameter to false.

  • +
  • nodata – Controls the handling of no-data values (null). By default, they are removed. If set to +true, missing values in the data are put last; if set to false, they are put first.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The sorted array.

+
+
+
+

See also

+

openeo.org documentation on process “sort”.

+
+
+ +
+
+openeo.processes.sqrt(x)[source]
+

Square root

+
+
Parameters:
+

x – A number.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed square root.

+
+
+
+

See also

+

openeo.org documentation on process “sqrt”.

+
+
+ +
+
+openeo.processes.subtract(x, y)[source]
+

Subtraction of two numbers

+
+
Parameters:
+
    +
  • x – The minuend.

  • +
  • y – The subtrahend.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed result.

+
+
+
+

See also

+

openeo.org documentation on process “subtract”.

+
+
+ +
+
+openeo.processes.sum(data, ignore_nodata=<object object>)[source]
+

Compute the sum by adding up numbers

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed sum of the sequence of numbers.

+
+
+
+

See also

+

openeo.org documentation on process “sum”.

+
+
+ +
+
+openeo.processes.tan(x)[source]
+

Tangent

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed tangent of x.

+
+
+
+

See also

+

openeo.org documentation on process “tan”.

+
+
+ +
+
+openeo.processes.tanh(x)[source]
+

Hyperbolic tangent

+
+
Parameters:
+

x – An angle in radians.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed hyperbolic tangent of x.

+
+
+
+

See also

+

openeo.org documentation on process “tanh”.

+
+
+ +
+
+openeo.processes.text_begins(data, pattern, case_sensitive=<object object>)[source]
+

Text begins with another text

+
+
Parameters:
+
    +
  • data – Text in which to find something at the beginning.

  • +
  • pattern – Text to find at the beginning of data. Regular expressions are not supported.

  • +
  • case_sensitive – Case sensitive comparison can be disabled by setting this parameter to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if data begins with pattern, false` otherwise.

+
+
+
+

See also

+

openeo.org documentation on process “text_begins”.

+
+
+ +
+
+openeo.processes.text_concat(data, separator=<object object>)[source]
+

Concatenate elements to a single text

+
+
Parameters:
+
    +
  • data – A set of elements. Numbers, boolean values and null values get converted to their (lower case) +string representation. For example: 1 (integer), -1.5 (number), true / false (boolean values)

  • +
  • separator – A separator to put between each of the individual texts. Defaults to an empty string.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A string containing a string representation of all the array elements in the same order, with the +separator between each element.

+
+
+
+

See also

+

openeo.org documentation on process “text_concat”.

+
+
+ +
+
+openeo.processes.text_contains(data, pattern, case_sensitive=<object object>)[source]
+

Text contains another text

+
+
Parameters:
+
    +
  • data – Text in which to find something in.

  • +
  • pattern – Text to find in data. Regular expressions are not supported.

  • +
  • case_sensitive – Case sensitive comparison can be disabled by setting this parameter to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if data contains the pattern, false` otherwise.

+
+
+
+

See also

+

openeo.org documentation on process “text_contains”.

+
+
+ +
+
+openeo.processes.text_ends(data, pattern, case_sensitive=<object object>)[source]
+

Text ends with another text

+
+
Parameters:
+
    +
  • data – Text in which to find something at the end.

  • +
  • pattern – Text to find at the end of data. Regular expressions are not supported.

  • +
  • case_sensitive – Case sensitive comparison can be disabled by setting this parameter to false.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

true if data ends with pattern, false` otherwise.

+
+
+
+

See also

+

openeo.org documentation on process “text_ends”.

+
+
+ +
+
+openeo.processes.trim_cube(data)[source]
+

Remove dimension labels with no-data values

+
+
Parameters:
+

data – A data cube to trim.

+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A trimmed data cube with the same dimensions. The dimension properties name, type, reference +system and resolution remain unchanged. The number of dimension labels may decrease.

+
+
+
+

See also

+

openeo.org documentation on process “trim_cube”.

+
+
+ +
+
+openeo.processes.unflatten_dimension(data, dimension, target_dimensions, label_separator=<object object>)[source]
+

Split a single dimensions into multiple dimensions

+
+
Parameters:
+
    +
  • data – A data cube that is consistently structured so that operation can execute flawlessly (e.g. the +dimension labels need to contain the label_separator exactly 1 time for two target dimensions, 2 times +for three target dimensions etc.).

  • +
  • dimension – The name of the dimension to split.

  • +
  • target_dimensions – The names of the new target dimensions. New dimensions will be created with the +given names and type other (see add_dimension()). Fails with a TargetDimensionExists exception if +any of the dimensions exists. The order of the array defines the order in which the dimensions and +dimension labels are added to the data cube (see the example in the process description).

  • +
  • label_separator – The string that will be used as a separator to split the dimension labels.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A data cube with the new shape. The dimension properties (name, type, labels, reference system and +resolution) for all other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “unflatten_dimension”.

+
+
+ +
+
+openeo.processes.variance(data, ignore_nodata=<object object>)[source]
+

Variance

+
+
Parameters:
+
    +
  • data – An array of numbers.

  • +
  • ignore_nodata – Indicates whether no-data values are ignored or not. Ignores them by default. Setting +this flag to false considers no-data values so that null is returned if any value is such a value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

The computed sample variance.

+
+
+
+

See also

+

openeo.org documentation on process “variance”.

+
+
+ +
+
+openeo.processes.vector_buffer(geometries, distance)[source]
+

Buffer geometries by distance

+
+
Parameters:
+
    +
  • geometries – Geometries to apply the buffer on. Feature properties are preserved.

  • +
  • distance – The distance of the buffer in meters. A positive distance expands the geometries, +resulting in outward buffering (dilation), while a negative distance shrinks the geometries, resulting in +inward buffering (erosion). If the unit of the spatial reference system is not meters, a UnitMismatch +error is thrown. Use vector_reproject() to convert the geometries to a suitable spatial reference +system.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns a vector data cube with the computed new geometries of which some may be empty.

+
+
+
+

See also

+

openeo.org documentation on process “vector_buffer”.

+
+
+ +
+
+openeo.processes.vector_reproject(data, projection, dimension=<object object>)[source]
+

Reprojects the geometry dimension

+
+
Parameters:
+
    +
  • data – A vector data cube.

  • +
  • projection – Coordinate reference system to reproject to. Specified as an [EPSG +code](http://www.epsg-registry.org/) or [WKT2 CRS +string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html).

  • +
  • dimension – The name of the geometry dimension to reproject. If no specific dimension is specified, +the filter applies to all geometry dimensions. Fails with a DimensionNotAvailable exception if the +specified dimension does not exist.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

A vector data cube with geometries projected to the new coordinate reference system. The reference +system of the geometry dimension changes, all other dimensions and properties remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “vector_reproject”.

+
+
+ +
+
+openeo.processes.vector_to_random_points(data, geometry_count=<object object>, total_count=<object object>, group=<object object>, seed=<object object>)[source]
+

Sample random points from geometries

+
+
Parameters:
+
    +
  • data – Input geometries for sample extraction.

  • +
  • geometry_count – The maximum number of points to compute per geometry. Points in the input +geometries can be selected only once by the sampling.

  • +
  • total_count – The maximum number of points to compute overall. Throws a CountMismatch exception if +the specified value is less than the provided number of geometries.

  • +
  • group – Specifies whether the sampled points should be grouped by input geometry (default) or be +generated as independent points. * If the sampled points are grouped, the process generates a MultiPoint +per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is +generated as a distinct Point geometry without identifier.

  • +
  • seed – A randomization seed to use for random sampling. If not given or null, no seed is used and +results may differ on subsequent use.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns a vector data cube with the sampled points.

+
+
+
+

See also

+

openeo.org documentation on process “vector_to_random_points”.

+
+
+ +
+
+openeo.processes.vector_to_regular_points(data, distance, group=<object object>)[source]
+

Sample regular points from geometries

+
+
Parameters:
+
    +
  • data – Input geometries for sample extraction.

  • +
  • distance – Defines the minimum distance in meters that is required between two samples generated +inside a single geometry. If the unit of the spatial reference system is not meters, a UnitMismatch +error is thrown. Use vector_reproject() to convert the geometries to a suitable spatial reference +system. - For polygons, the distance defines the cell sizes of a regular grid that starts at the +upper-left bound of each polygon. The centroid of each cell is then a sample point. If the centroid is not +enclosed in the polygon, no point is sampled. If no point can be sampled for the geometry at all, the first +coordinate of the geometry is returned as point. - For lines (line strings), the sampling starts with a +point at the first coordinate of the line and then walks along the line and samples a new point each time +the distance to the previous point has been reached again. - For points, the point is returned as +given.

  • +
  • group – Specifies whether the sampled points should be grouped by input geometry (default) or be +generated as independent points. * If the sampled points are grouped, the process generates a MultiPoint +per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is +generated as a distinct Point geometry without identifier.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Returns a vector data cube with the sampled points.

+
+
+
+

See also

+

openeo.org documentation on process “vector_to_regular_points”.

+
+
+ +
+
+openeo.processes.xor(x, y)[source]
+

Logical XOR (exclusive or)

+
+
Parameters:
+
    +
  • x – A boolean value.

  • +
  • y – A boolean value.

  • +
+
+
Return type:
+

ProcessBuilder

+
+
Returns:
+

Boolean result of the logical XOR.

+
+
+
+

See also

+

openeo.org documentation on process “xor”.

+
+
+ +
+
+

ProcessBuilder helper class

+
+
+class openeo.processes.ProcessBuilder(pgnode)[source]
+

The ProcessBuilder class +is a helper class that implements +(much like the openEO process functions) +each openEO process as a method. +On top of that it also adds syntactic sugar to support Python operators as well +(e.g. + is translated to the add process).

+
+

Attention

+

As normal user, you should never create a +ProcessBuilder instance +directly.

+

You should only interact with this class inside a callback +function/lambda while building a child callback process graph +as discussed at Callback as a callable.

+
+

For example, let’s start from this simple usage snippet +where we want to reduce the temporal dimension +by taking the temporal mean of each timeseries:

+
def my_reducer(data):
+    return data.mean()
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

Note that this my_reducer function has a data argument, +which conceptually corresponds to an array of pixel values +(along the temporal dimension). +However, it’s important to understand that the my_reducer function +is actually not evaluated when you execute your process graph +on an openEO back-end, e.g. as a batch jobs. +Instead, my_reducer is evaluated +while building your process graph client-side +(at the time you execute that cube.reduce_dimension() statement to be precise). +This means that that data argument is actually not a concrete array of EO data, +but some kind of virtual placeholder, +a ProcessBuilder instance, +that keeps track of the operations you intend to do on the EO data.

+

To make that more concrete, it helps to add type hints +which will make it easier to discover what you can do with the argument +(depending on which editor or IDE you are using):

+
from openeo.processes import ProcessBuilder
+
+def my_reducer(data: ProcessBuilder) -> ProcessBuilder:
+    return data.mean()
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

Because ProcessBuilder methods +return new ProcessBuilder instances, +and because it support syntactic sugar to use Python operators on it, +and because openeo.process functions +also accept and return ProcessBuilder instances, +we can mix methods, functions and operators in the callback function like this:

+
from openeo.processes import ProcessBuilder, cos
+
+def my_reducer(data: ProcessBuilder) -> ProcessBuilder:
+    return cos(data.mean()) + 1.23
+
+cube.reduce_dimension(reducer=my_reducer, dimension="t")
+
+
+

or compactly, using an anonymous lambda expression:

+
from openeo.processes import cos
+
+cube.reduce_dimension(
+    reducer=lambda data: cos(data.mean())) + 1.23,
+    dimension="t"
+)
+
+
+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/api.html b/api.html new file mode 100644 index 000000000..0e2e074bf --- /dev/null +++ b/api.html @@ -0,0 +1,6809 @@ + + + + + + + + API (General) — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

API (General)

+
+

High level Interface

+

The high-level interface tries to provide an opinionated, Pythonic, API +to interact with openEO back-ends. It’s aim is to hide some of the details +of using a web service, so the user can produce concise and readable code.

+

Users that want to interact with openEO on a lower level, and have more control, can +use the lower level classes.

+
+
+

openeo

+
+
+openeo.connect(url=None, *, auth_type=None, auth_options=None, session=None, default_timeout=None, auto_validate=True)[source]
+

This method is the entry point to OpenEO. +You typically create one connection object in your script or application +and re-use it for all calls to that backend.

+

If the backend requires authentication, you can pass authentication data directly to this function, +but it could be easier to authenticate as follows:

+
>>> # For basic authentication
+>>> conn = connect(url).authenticate_basic(username="john", password="foo")
+>>> # For OpenID Connect authentication
+>>> conn = connect(url).authenticate_oidc(client_id="myclient")
+
+
+
+
Parameters:
+
    +
  • url (Optional[str]) – The http url of the OpenEO back-end.

  • +
  • auth_type (Optional[str]) – Which authentication to use: None, “basic” or “oidc” (for OpenID Connect)

  • +
  • auth_options (Optional[dict]) – Options/arguments specific to the authentication type

  • +
  • default_timeout (Optional[int]) – default timeout (in seconds) for requests

  • +
  • auto_validate (bool) – toggle to automatically validate process graphs before execution

  • +
+
+
Return type:
+

Connection

+
+
+
+

Added in version 0.24.0: added auto_validate argument

+
+
+ +
+
+

openeo.rest.datacube

+

The main module for creating earth observation processes. It aims to easily build complex process chains, that can +be evaluated by an openEO backend.

+
+
+openeo.rest.datacube.THIS
+

Symbolic reference to the current data cube, to be used as argument in DataCube.process() calls

+
+ +
+
+class openeo.rest.datacube.DataCube(graph, connection=None, metadata=None)[source]
+

Class representing a openEO (raster) data cube.

+

The data cube is represented by its corresponding openeo “process graph” +and this process graph can be “grown” to a desired workflow by calling the appropriate methods.

+
+
+__init__(graph, connection=None, metadata=None)[source]
+
+ +
+
+add(other, reverse=False)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “add”.

+
+
+ +
+
+add_dimension(name, label, type=None)[source]
+

Adds a new named dimension to the data cube. +Afterwards, the dimension can be referenced with the specified name. If a dimension with the specified name exists, +the process fails with a DimensionExists error. The dimension label of the dimension is set to the specified label.

+

This call does not modify the datacube in place, but returns a new datacube with the additional dimension.

+
+
Parameters:
+
    +
  • name (str) – The name of the dimension to add

  • +
  • label (str) – The dimension label.

  • +
  • type (Optional[str]) – Dimension type, allowed values: ‘spatial’, ‘temporal’, ‘bands’, ‘other’, default value is ‘other’

  • +
+
+
Returns:
+

The data cube with a newly added dimension. The new dimension has exactly one dimension label. All other dimensions remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “add_dimension”.

+
+
+ +
+
+aggregate_spatial(geometries, reducer, target_dimension=None, crs=None, context=None)[source]
+

Aggregates statistics for one or more geometries (e.g. zonal statistics for polygons) +over the spatial dimensions.

+
+
Parameters:
+
    +
  • geometries (Union[BaseGeometry, dict, str, Path, Parameter, VectorCube]) – a shapely geometry, a GeoJSON-style dictionary, +a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file.

  • +
  • reducer (Union[str, Callable, PGNode]) –

    the “child callback”: +the name of a single openEO process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns a single numerical value. +For example:

    + +

  • +
  • target_dimension (Optional[str]) – The new dimension name to be used for storing the results.

  • +
  • crs (Union[int, str, None]) – The spatial reference system of the provided polygon. +By default, longitude-latitude (EPSG:4326) is assumed. +See openeo.util.normalize_crs() for more details about additional normalization that is applied to this argument.

  • +
  • context (Optional[dict]) –

    Additional data to be passed to the reducer process.

    +
    +

    Note

    +

    this crs argument is a non-standard/experimental feature, only supported by specific back-ends. +See https://github.com/Open-EO/openeo-processes/issues/235 for details.

    +
    +

  • +
+
+
Return type:
+

VectorCube

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_spatial”.

+
+
+ +
+
+aggregate_spatial_window(reducer, size, boundary='pad', align='upper-left', context=None)[source]
+

Aggregates statistics over the horizontal spatial dimensions (axes x and y) of the data cube.

+

The pixel grid for the axes x and y is divided into non-overlapping windows with the size +specified in the parameter size. If the number of values for the axes x and y is not a multiple +of the corresponding window size, the behavior specified in the parameters boundary and align +is applied. For each of these windows, the reducer process computes the result.

+
+
Parameters:
+
    +
  • reducer (Union[str, Callable, PGNode]) – the “child callback”: +the name of a single openEO process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

  • +
  • size (List[int]) – Window size in pixels along the horizontal spatial dimensions. +The first value corresponds to the x axis, the second value corresponds to the y axis.

  • +
  • boundary (str) –

    Behavior to apply if the number of values for the axes x and y is not a +multiple of the corresponding value in the size parameter. +Options are:

    +
    +
      +
    • pad (default): pad the data cube with the no-data value null to fit the required window size.

    • +
    • trim: trim the data cube to fit the required window size.

    • +
    +
    +

    Use the parameter align to align the data to the desired corner.

    +

  • +
  • align (str) – If the data requires padding or trimming (see parameter boundary), specifies +to which corner of the spatial extent the data is aligned to. For example, if the data is +aligned to the upper left, the process pads/trims at the lower-right.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A data cube with the newly computed values and the same dimensions.

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_spatial_window”.

+
+
+ +
+
+aggregate_temporal(intervals, reducer, labels=None, dimension=None, context=None)[source]
+

Computes a temporal aggregation based on an array of date and/or time intervals.

+

Calendar hierarchies such as year, month, week etc. must be transformed into specific intervals by the clients. For each interval, all data along the dimension will be passed through the reducer. The computed values will be projected to the labels, so the number of labels and the number of intervals need to be equal.

+

If the dimension is not set, the data cube is expected to only have one temporal dimension.

+
+
Parameters:
+
    +
  • intervals (List[list]) – Temporal left-closed intervals so that the start time is contained, but not the end time.

  • +
  • reducer (Union[str, Callable, PGNode]) –

    the “child callback”: +the name of a single openEO process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns a single numerical value. +For example:

    + +

  • +
  • labels (Optional[List[str]]) – Labels for the intervals. The number of labels and the number of groups need to be equal.

  • +
  • dimension (Optional[str]) – The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension.

  • +
  • context (Optional[dict]) – Additional data to be passed to the reducer. Not set by default.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A DataCube containing a result for each time window

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_temporal”.

+
+
+ +
+
+aggregate_temporal_period(period, reducer, dimension=None, context=None)[source]
+

Computes a temporal aggregation based on calendar hierarchies such as years, months or seasons. For other calendar hierarchies aggregate_temporal can be used.

+

For each interval, all data along the dimension will be passed through the reducer.

+

If the dimension is not set or is set to null, the data cube is expected to only have one temporal dimension.

+

The period argument specifies the time intervals to aggregate. The following pre-defined values are available:

+
    +
  • hour: Hour of the day

  • +
  • day: Day of the year

  • +
  • week: Week of the year

  • +
  • dekad: Ten day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 each year.

  • +
  • month: Month of the year

  • +
  • season: Three month periods of the calendar seasons (December - February, March - May, June - August, September - November).

  • +
  • tropical-season: Six month periods of the tropical seasons (November - April, May - October).

  • +
  • year: Proleptic years

  • +
  • decade: Ten year periods (0-to-9 decade), from a year ending in a 0 to the next year ending in a 9.

  • +
  • decade-ad: Ten year periods (1-to-0 decade) better aligned with the Anno Domini (AD) calendar era, from a year ending in a 1 to the next year ending in a 0.

  • +
+
+
Parameters:
+
    +
  • period (str) – The period of the time intervals to aggregate.

  • +
  • reducer (Union[str, PGNode, Callable]) – A reducer to be applied on all values along the specified dimension. The reducer must be a callable process (or a set processes) that accepts an array and computes a single return value of the same type as the input values, for example median.

  • +
  • dimension (Optional[str]) – The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension.

  • +
  • context (Optional[Dict]) – Additional data to be passed to the reducer.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A data cube with the same dimensions. The dimension properties (name, type, labels, reference system and resolution) remain unchanged.

+
+
+
+

See also

+

openeo.org documentation on process “aggregate_temporal_period”.

+
+
+ +
+
+apply(process, context=None)[source]
+

Applies a unary process (a local operation) to each value of the specified or all dimensions in the data cube.

+
+
Parameters:
+
    +
  • process (Union[str, Callable, UDF, PGNode]) –

    the “child callback”: +the name of a single process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives a single numerical value +and returns a single numerical value. +For example:

    + +

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube.

+
+
+
+

See also

+

openeo.org documentation on process “apply”.

+
+
+ +
+
+apply_dimension(code=None, runtime=None, process=None, version=None, dimension='t', target_dimension=None, context=None)[source]
+

Applies a process to all pixel values along a dimension of a raster data cube. For example, +if the temporal dimension is specified the process will work on a time series of pixel values.

+

The process to apply is specified by either code and runtime in case of a UDF, or by providing a callback function +in the process argument.

+

The process reduce_dimension also applies a process to pixel values along a dimension, but drops +the dimension afterwards. The process apply applies a process to each pixel value in the data cube.

+

The target dimension is the source dimension if not specified otherwise in the target_dimension parameter. +The pixel values in the target dimension get replaced by the computed pixel values. The name, type and +reference system are preserved.

+

The dimension labels are preserved when the target dimension is the source dimension and the number of +pixel values in the source dimension is equal to the number of values computed by the process. Otherwise, +the dimension labels will be incrementing integers starting from zero, which can be changed using +rename_labels afterwards. The number of labels will equal to the number of values computed by the process.

+
+
Parameters:
+
    +
  • code (Optional[str]) – [deprecated] UDF code or process identifier (optional)

  • +
  • runtime – [deprecated] UDF runtime to use (optional)

  • +
  • process (Union[str, Callable, UDF, PGNode]) –

    the “child callback”: +the name of a single process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns an array of numerical values. +For example:

    + +

  • +
  • version (Optional[str]) – [deprecated] Version of the UDF runtime to use

  • +
  • dimension (str) – The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist.

  • +
  • target_dimension (Optional[str]) – The name of the target dimension or null (the default) to use the source dimension +specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. +The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn’t exist yet.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A datacube with the UDF applied to the given dimension.

+
+
Raises:
+

DimensionNotAvailable

+
+
+
+

Changed in version 0.13.0: arguments code, runtime and version are deprecated if favor of the standard approach +of using an UDF object in the process argument. +See openeo.UDF API and usage changes in version 0.13.0 for more background about the changes.

+
+
+

See also

+

openeo.org documentation on process “apply_dimension”.

+
+
+ +
+
+apply_kernel(kernel, factor=1.0, border=0, replace_invalid=0)[source]
+

Applies a focal operation based on a weighted kernel to each value of the specified dimensions in the data cube.

+

The border parameter determines how the data is extended when the kernel overlaps with the borders. +The following options are available:

+
    +
  • numeric value - fill with a user-defined constant number n: nnnnnn|abcdefgh|nnnnnn (default, with n = 0)

  • +
  • replicate - repeat the value from the pixel at the border: aaaaaa|abcdefgh|hhhhhh

  • +
  • reflect - mirror/reflect from the border: fedcba|abcdefgh|hgfedc

  • +
  • reflect_pixel - mirror/reflect from the center of the pixel at the border: gfedcb|abcdefgh|gfedcb

  • +
  • wrap - repeat/wrap the image: cdefgh|abcdefgh|abcdef

  • +
+
+
Parameters:
+
    +
  • kernel (Union[ndarray, List[List[float]]]) – The kernel to be applied on the data cube. The kernel has to be as many dimensions as the data cube has dimensions.

  • +
  • factor – A factor that is multiplied to each value computed by the focal operation. This is basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required for some kernel-based algorithms such as the Gaussian blur.

  • +
  • border – Determines how the data is extended when the kernel overlaps with the borders. Defaults to fill the border with zeroes.

  • +
  • replace_invalid – This parameter specifies the value to replace non-numerical or infinite numerical values with. By default, those values are replaced with zeroes.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube.

+
+
+
+

See also

+

openeo.org documentation on process “apply_kernel”.

+
+
+ +
+
+apply_neighborhood(process, size, overlap=None, context=None)[source]
+

Applies a focal process to a data cube.

+

A focal process is a process that works on a ‘neighbourhood’ of pixels. The neighbourhood can extend into multiple dimensions, this extent is specified by the size argument. It is not only (part of) the size of the input window, but also the size of the output for a given position of the sliding window. The sliding window moves with multiples of size.

+

An overlap can be specified so that neighbourhoods can have overlapping boundaries. This allows for continuity of the output. The values included in the data cube as overlap can’t be modified by the given process.

+

The neighbourhood size should be kept small enough, to avoid running beyond computational resources, but a too small size will result in a larger number of process invocations, which may slow down processing. Window sizes for spatial dimensions typically are in the range of 64 to 512 pixels, while overlaps of 8 to 32 pixels are common.

+

The process must not add new dimensions, or remove entire dimensions, but the result can have different dimension labels.

+

For the special case of 2D convolution, it is recommended to use apply_kernel().

+
+
Parameters:
+
    +
  • size (List[Dict])

  • +
  • overlap (List[dict])

  • +
  • process (Union[str, PGNode, Callable, UDF]) – a callback function that creates a process graph, see Processes with child “callbacks”

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

+
+
+
+

See also

+

openeo.org documentation on process “apply_neighborhood”.

+
+
+ +
+
+apply_polygon(geometries=None, process=None, mask_value=None, context=None, **kwargs)[source]
+

Apply a process to segments of the data cube that are defined by the given polygons. +For each polygon provided, all pixels for which the point at the pixel center intersects +with the polygon (as defined in the Simple Features standard by the OGC) are collected into sub data cubes. +If a pixel is part of multiple of the provided polygons (e.g., when the polygons overlap), +the GeometriesOverlap exception is thrown. +Each sub data cube is passed individually to the given process.

+
+
Parameters:
+
    +
  • geometries (Union[BaseGeometry, dict, str, Path, Parameter, VectorCube]) – Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, +a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file.

  • +
  • process (Union[str, PGNode, Callable, UDF]) – “child callback” function, see Processes with child “callbacks”

  • +
  • mask_value (Optional[float]) – The value used for pixels outside the polygon.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+

Changed in version 0.32.0: Argument polygons was renamed to geometries. +While deprecated, the old name polygons is still supported +as keyword argument for backwards compatibility.

+
+
+

See also

+

openeo.org documentation on process “apply_polygon”.

+
+
+ +
+
+ard_normalized_radar_backscatter(elevation_model=None, contributing_area=False, ellipsoid_incidence_angle=False, noise_removal=True)[source]
+

Computes CARD4L compliant backscatter (gamma0) from SAR input. +This method is a variant of sar_backscatter(), +with restricted parameters to generate backscatter according to CARD4L specifications.

+

Note that backscatter computation may require instrument specific metadata that is tightly coupled to the original SAR products. +As a result, this process may only work in combination with loading data from specific collections, not with general data cubes.

+
+
Parameters:
+
    +
  • elevation_model (str) – The digital elevation model to use. Set to None (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility.

  • +
  • contributing_area – If set to true, a DEM-based local contributing area band named contributing_area +is added. The values are given in square meters.

  • +
  • ellipsoid_incidence_angle (bool) – If set to True, an ellipsoidal incidence angle band named ellipsoid_incidence_angle is added. The values are given in degrees.

  • +
  • noise_removal (bool) – If set to false, no noise removal is applied. Defaults to True, which removes noise.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

Backscatter values expressed as gamma0. The data returned is CARD4L compliant and contains metadata. By default, the backscatter values are given in linear scale.

+
+
+
+

See also

+

openeo.org documentation on process “ard_normalized_radar_backscatter”.

+
+
+ +
+
+ard_surface_reflectance(atmospheric_correction_method, cloud_detection_method, elevation_model=None, atmospheric_correction_options=None, cloud_detection_options=None)[source]
+

Computes CARD4L compliant surface reflectance values from optical input.

+
+
Parameters:
+
    +
  • atmospheric_correction_method (str) – The atmospheric correction method to use.

  • +
  • cloud_detection_method (str) – The cloud detection method to use.

  • +
  • elevation_model (str) – The digital elevation model to use, leave empty to allow the back-end to make a suitable choice.

  • +
  • atmospheric_correction_options (dict) – Proprietary options for the atmospheric correction method.

  • +
  • cloud_detection_options (dict) – Proprietary options for the cloud detection method.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

Data cube containing bottom of atmosphere reflectances with atmospheric disturbances like clouds and cloud shadows removed. The data returned is CARD4L compliant and contains metadata.

+
+
+
+

See also

+

openeo.org documentation on process “ard_surface_reflectance”.

+
+
+ +
+
+atmospheric_correction(method=None, elevation_model=None, options=None)[source]
+

Applies an atmospheric correction that converts top of atmosphere reflectance values into bottom of atmosphere/top of canopy reflectance values.

+

Note that multiple atmospheric methods exist, but may not be supported by all backends. The method parameter gives +you the option of requiring a specific method, but this may result in an error if the backend does not support it.

+
+
Parameters:
+
    +
  • method (str) – The atmospheric correction method to use. To get reproducible results, you have to set a specific method. Set to null to allow the back-end to choose, which will improve portability, but reduce reproducibility as you may get different results if you run the processes multiple times.

  • +
  • elevation_model (str) – The digital elevation model to use, leave empty to allow the back-end to make a suitable choice.

  • +
  • options (dict) – Proprietary options for the atmospheric correction method.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

datacube with bottom of atmosphere reflectances

+
+
+
+

See also

+

openeo.org documentation on process “atmospheric_correction”.

+
+
+ +
+
+band(band)[source]
+

Filter out a single band

+
+
Parameters:
+

band (Union[str, int]) – band name, band common name or band index.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+ +
+
+band_filter(bands)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.1.0: Usage of this legacy method is deprecated. Use +filter_bands() instead.

+
+
+ +
+
+chunk_polygon(chunks, process, mask_value=None, context=None)[source]
+

Apply a process to spatial chunks of a data cube.

+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+
Parameters:
+
    +
  • chunks (Union[BaseGeometry, dict, str, Path, Parameter, VectorCube]) – Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, +a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file.

  • +
  • process (Union[str, PGNode, Callable, UDF]) – “child callback” function, see Processes with child “callbacks”

  • +
  • mask_value (float) – The value used for cells outside the polygon. +This provides a distinction between NoData cells within the polygon (due to e.g. clouds) +and masked cells outside it. If no value is provided, NoData cells are used outside the polygon.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.26.0: Use apply_polygon().

+
+
+ +
+
+count_time()[source]
+

Counts the number of images with a valid mask in a time series for all bands of the input dataset.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “count”.

+
+
+ +
+
+classmethod create_collection(cls, collection_id, connection=None, spatial_extent=None, temporal_extent=None, bands=None, fetch_metadata=True, properties=None, max_cloud_cover=None)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.4.6: Usage of this legacy class method is deprecated. Use +load_collection() instead.

+
+
+ +
+
+create_job(out_format=None, *, title=None, description=None, plan=None, budget=None, job_options=None, validate=None, auto_add_save_result=True, **format_options)[source]
+

Sends the datacube’s process graph as a batch job to the back-end +and return a BatchJob instance.

+

Note that the batch job will just be created at the back-end, +it still needs to be started and tracked explicitly. +Use execute_batch() instead to have the openEO Python client take care of that job management.

+
+
Parameters:
+
    +
  • out_format (Optional[str]) – output file format.

  • +
  • title (Optional[str]) – job title

  • +
  • description (Optional[str]) – job description

  • +
  • plan (Optional[str]) – The billing plan to process and charge the job with

  • +
  • budget (Optional[float]) – Maximum budget to be spent on executing the job. +Note that some backends do not honor this limit.

  • +
  • job_options (Optional[dict]) – custom job options.

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job.

+
+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+dimension_labels(dimension)[source]
+

Gives all labels for a dimension in the data cube. The labels have the same order as in the data cube.

+
+
Parameters:
+

dimension (str) – The name of the dimension to get the labels for.

+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “dimension_labels”.

+
+
+ +
+
+divide(other, reverse=False)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “divide”.

+
+
+ +
+
+download(outputfile=None, format=None, options=None, *, validate=None, auto_add_save_result=True)[source]
+

Execute synchronously and download the raster data cube, e.g. as GeoTIFF.

+

If outputfile is provided, the result is stored on disk locally, otherwise, a bytes object is returned. +The bytes object can be passed on to a suitable decoder for decoding.

+
+
Parameters:
+
    +
  • outputfile (Union[str, Path, None]) – Optional, an output file if the result needs to be stored on disk.

  • +
  • format (Optional[str]) – Optional, an output format supported by the backend.

  • +
  • options (Optional[dict]) – Optional, file format options

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

Optional[bytes]

+
+
Returns:
+

None if the result is stored to disk, or a bytes object returned by the backend.

+
+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+drop_dimension(name)[source]
+

Drops a dimension from the data cube. +Dropping a dimension only works on dimensions with a single dimension label left, otherwise the process fails +with a DimensionLabelCountMismatch exception. Dimension values can be reduced to a single value with a filter +such as filter_bands or the reduce_dimension process. If a dimension with the specified name does not exist, +the process fails with a DimensionNotAvailable exception.

+
+
Parameters:
+

name (str) – The name of the dimension to drop

+
+
Returns:
+

The data cube with the given dimension dropped.

+
+
+
+

See also

+

openeo.org documentation on process “drop_dimension”.

+
+
+ +
+
+execute(*, validate=None, auto_decode=True)[source]
+

Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed.

+
+
Parameters:
+
    +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_decode (bool) – Boolean flag to enable/disable automatic JSON decoding of the response. Defaults to True.

  • +
+
+
Return type:
+

Union[dict, Response]

+
+
Returns:
+

parsed JSON response as a dict if auto_decode is True, otherwise response object

+
+
+
+ +
+
+execute_batch(outputfile=None, out_format=None, *, title=None, description=None, plan=None, budget=None, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, job_options=None, validate=None, auto_add_save_result=True, **format_options)[source]
+

Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. +This method is mostly recommended if the batch job is expected to run in a reasonable amount of time.

+

For very long-running jobs, you probably do not want to keep the client running.

+
+
Parameters:
+
    +
  • outputfile (Union[str, Path, None]) – The path of a file to which a result can be written

  • +
  • out_format (Optional[str]) – (optional) File format to use for the job result.

  • +
  • job_options (Optional[dict])

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

BatchJob

+
+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+static execute_local_udf(udf, datacube=None, fmt='netcdf')[source]
+
+

Deprecated since version 0.7.0: Use openeo.udf.run_code.execute_local_udf() instead

+
+
+ +
+
+filter_bands(bands)[source]
+

Filter the data cube by the given bands

+
+
Parameters:
+

bands (Union[List[Union[str, int]], str]) – list of band names, common names or band indices. Single band name can also be given as string.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “filter_bands”.

+
+
+ +
+
+filter_bbox(*args, west=None, south=None, east=None, north=None, crs=None, base=None, height=None, bbox=None)[source]
+

Limits the data cube to the specified bounding box.

+

The bounding box can be specified in multiple ways.

+
+
    +
  • With keyword arguments:

    +
    >>> cube.filter_bbox(west=3, south=51, east=4, north=52, crs=4326)
    +
    +
    +
  • +
  • With a (west, south, east, north) list or tuple +(note that EPSG:4326 is the default CRS, so it’s not necessary to specify it explicitly):

    +
    >>> cube.filter_bbox([3, 51, 4, 52])
    +>>> cube.filter_bbox(bbox=[3, 51, 4, 52])
    +
    +
    +
  • +
  • With a bbox dictionary:

    +
    >>> bbox = {"west": 3, "south": 51, "east": 4, "north": 52, "crs": 4326}
    +>>> cube.filter_bbox(bbox)
    +>>> cube.filter_bbox(bbox=bbox)
    +>>> cube.filter_bbox(**bbox)
    +
    +
    +
  • +
  • With a shapely geometry (of which the bounding box will be used):

    +
    >>> cube.filter_bbox(geometry)
    +>>> cube.filter_bbox(bbox=geometry)
    +
    +
    +
  • +
  • Passing a parameter:

    +
    >>> bbox_param = Parameter(name="my_bbox", schema="object")
    +>>> cube.filter_bbox(bbox_param)
    +>>> cube.filter_bbox(bbox=bbox_param)
    +
    +
    +
  • +
  • With a CRS other than EPSG 4326:

    +
    >>> cube.filter_bbox(
    +... west=652000, east=672000, north=5161000, south=5181000,
    +... crs=32632
    +... )
    +
    +
    +
  • +
  • Deprecated: positional arguments are also supported, +but follow a non-standard order for legacy reasons:

    +
    >>> west, east, north, south = 3, 4, 52, 51
    +>>> cube.filter_bbox(west, east, north, south)
    +
    +
    +
  • +
+
+
+
Parameters:
+

crs (Union[int, str, None]) – value describing the coordinate reference system. +Typically just an int (interpreted as EPSG code, e.g. 4326) +or a string (handled as authority string, e.g. "EPSG:4326"). +See openeo.util.normalize_crs() for more details about additional normalization that is applied to this argument.

+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “filter_bbox”.

+
+
+ +
+
+filter_labels(condition, dimension, context=None)[source]
+

Filters the dimension labels in the data cube for the given dimension. +Only the dimension labels that match the specified condition are preserved, +all other labels with their corresponding data get removed.

+
+
Parameters:
+
    +
  • condition (Union[PGNode, Callable]) – the “child callback” which will be given a single label value (number or string) +and returns a boolean expressing if the label should be preserved. +Also see Processes with child “callbacks”.

  • +
  • dimension (str) – The name of the dimension to filter on.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Added in version 0.27.0.

+
+
+

See also

+

openeo.org documentation on process “filter_labels”.

+
+
+ +
+
+filter_spatial(geometries)[source]
+

Limits the data cube over the spatial dimensions to the specified geometries.

+
+
    +
  • For polygons, the filter retains a pixel in the data cube if the point at the pixel center intersects with +at least one of the polygons (as defined in the Simple Features standard by the OGC).

  • +
  • For points, the process considers the closest pixel center.

  • +
  • For lines (line strings), the process considers all the pixels whose centers are closest to at least one +point on the line.

  • +
+
+

More specifically, pixels outside of the bounding box of the given geometry will not be available after filtering. +All pixels inside the bounding box that are not retained will be set to null (no data).

+
+
Parameters:
+

geometries – One or more geometries used for filtering, specified as GeoJSON in EPSG:4326.

+
+
Return type:
+

DataCube

+
+
Returns:
+

A data cube restricted to the specified geometries. The dimensions and dimension properties (name, +type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less +(or the same) dimension labels.

+
+
+
+

See also

+

openeo.org documentation on process “filter_spatial”.

+
+
+ +
+
+filter_temporal(*args, start_date=None, end_date=None, extent=None)[source]
+

Limit the DataCube to a certain date range, which can be specified in several ways:

+
>>> cube.filter_temporal("2019-07-01", "2019-08-01")
+>>> cube.filter_temporal(["2019-07-01", "2019-08-01"])
+>>> cube.filter_temporal(extent=["2019-07-01", "2019-08-01"])
+>>> cube.filter_temporal(start_date="2019-07-01", end_date="2019-08-01"])
+
+
+

See Filter on temporal extent for more details on temporal extent handling and shorthand notation.

+
+
Parameters:
+
    +
  • start_date (Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]) – start date of the filter (inclusive), as a string or date object

  • +
  • end_date (Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]) – end date of the filter (exclusive), as a string or date object

  • +
  • extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) – temporal extent. +Typically, specified as a two-item list or tuple containing start and end date.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Changed in version 0.23.0: Arguments start_date, end_date and extent: +add support for year/month shorthand notation as discussed at Year/month shorthand notation.

+
+
+

See also

+

openeo.org documentation on process “filter_temporal”.

+
+
+ +
+
+fit_curve(parameters, function, dimension)[source]
+

Use non-linear least squares to fit a model function y = f(x, parameters) to data.

+

The process throws an InvalidValues exception if invalid values are encountered. +Invalid values are finite numbers (see also is_valid()).

+
+

Warning

+

experimental process: not generally supported, API subject to change. +https://github.com/Open-EO/openeo-processes/pull/240

+
+
+
Parameters:
+
+
+
+
+

See also

+

openeo.org documentation on process “fit_curve”.

+
+
+ +
+
+flat_graph()
+

Get the process graph in internal flat dict representation. +:rtype: Dict[str, dict]

+
+

Warning

+

This method is mainly intended for internal use. +It is not recommended for general use and is subject to change.

+

Instead, it is recommended to use +to_json() or print_json() +to obtain a standardized, interoperable JSON representation of the process graph. +See Export a process graph for more information.

+
+
+ +
+
+flatten_dimensions(dimensions, target_dimension, label_separator=None)[source]
+

Combines multiple given dimensions into a single dimension by flattening the values +and merging the dimension labels with the given label_separator. Non-string dimension labels will +be converted to strings. This process is the opposite of the process unflatten_dimension() +but executing both processes subsequently doesn’t necessarily create a data cube that +is equal to the original data cube.

+
+
Parameters:
+
    +
  • dimensions (List[str]) – The names of the dimension to combine.

  • +
  • target_dimension (str) – The name of a target dimension with a single dimension label to replace.

  • +
  • label_separator (Optional[str]) – The string that will be used as a separator for the concatenated dimension labels.

  • +
+
+
Returns:
+

A data cube with the new shape.

+
+
+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+

Added in version 0.10.0.

+
+
+

See also

+

openeo.org documentation on process “flatten_dimensions”.

+
+
+ +
+
+graph_add_node(process_id, arguments=None, metadata=None, namespace=None, **kwargs)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.1.1: Usage of this legacy method is deprecated. Use +process() instead.

+
+
+ +
+
+linear_scale_range(input_min, input_max, output_min, output_max)[source]
+

Performs a linear transformation between the input and output range.

+

The given number in x is clipped to the bounds specified in inputMin and inputMax so that the underlying formula

+
+

((x - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin) + outputMin

+

never returns any value lower than outputMin or greater than outputMax.

+
+

Potential use case include scaling values to the 8-bit range (0 - 255) often used for numeric representation of +values in one of the channels of the RGB colour model or calculating percentages (0 - 100).

+

The no-data value null is passed through and therefore gets propagated.

+
+
Parameters:
+
    +
  • input_min – Minimum input value

  • +
  • input_max – Maximum input value

  • +
  • output_min – Minimum value of the desired output range.

  • +
  • output_max – Maximum value of the desired output range.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “linear_scale_range”.

+
+
+ +
+
+ln()[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “ln”.

+
+
+ +
+
+classmethod load_collection(collection_id, connection=None, spatial_extent=None, temporal_extent=None, bands=None, fetch_metadata=True, properties=None, max_cloud_cover=None)[source]
+

Create a new Raster Data cube.

+
+
Parameters:
+
    +
  • collection_id (Union[str, Parameter]) – image collection identifier

  • +
  • connection (Optional[Connection]) – The backend connection to use. +Can be None to work without connection and collection metadata.

  • +
  • spatial_extent (Union[Dict[str, float], Parameter, None]) – limit data to specified bounding box or polygons

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) – limit data to specified temporal interval. +Typically, just a two-item list or tuple containing start and end date. +See Filter on temporal extent for more details on temporal extent handling and shorthand notation.

  • +
  • bands (Union[None, List[str], Parameter]) – only add the specified bands.

  • +
  • properties (Union[None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty]) – limit data by metadata property predicates. +See collection_property() for easy construction of such predicates.

  • +
  • max_cloud_cover (Optional[float]) – shortcut to set maximum cloud cover (“eo:cloud_cover” collection property)

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

new DataCube containing the collection

+
+
+
+

Changed in version 0.13.0: added the max_cloud_cover argument.

+
+
+

Changed in version 0.23.0: Argument temporal_extent: add support for year/month shorthand notation +as discussed at Year/month shorthand notation.

+
+
+

Changed in version 0.26.0: Add collection_property() support to properties argument.

+
+
+

See also

+

openeo.org documentation on process “load_collection”.

+
+
+ +
+
+classmethod load_disk_collection(connection, file_format, glob_pattern, **options)[source]
+

Loads image data from disk as a DataCube. +This is backed by a non-standard process (‘load_disk_data’). This will eventually be replaced by standard options such as +openeo.rest.connection.Connection.load_stac() or https://processes.openeo.org/#load_uploaded_files

+
+
Parameters:
+
    +
  • connection (Connection) – The connection to use to connect with the backend.

  • +
  • file_format (str) – the file format, e.g. ‘GTiff’

  • +
  • glob_pattern (str) – a glob pattern that matches the files to load from disk

  • +
  • options – options specific to the file format

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

the data as a DataCube

+
+
+
+

Deprecated since version 0.25.0: Depends on non-standard process, replace with +openeo.rest.connection.Connection.load_stac() where +possible.

+
+
+ +
+
+classmethod load_stac(url, spatial_extent=None, temporal_extent=None, bands=None, properties=None, connection=None)[source]
+

Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable DataCube. +A batch job result can be loaded by providing a reference to it.

+

If supported by the underlying metadata and file format, the data that is added to the data cube can be +restricted with the parameters spatial_extent, temporal_extent and bands. +If no data is available for the given extents, a NoDataAvailable error is thrown.

+

Remarks:

+
    +
  • The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as +specified in the metadata if the bands parameter is set to null.

  • +
  • If no additional parameter is specified this would imply that the whole data set is expected to be loaded. +Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only +load the data that is actually required after evaluating subsequent processes such as filters. +This means that the values should be processed only after the data has been limited to the required extent +and as a consequence also to a manageable size.

  • +
+
+
Parameters:
+
    +
  • url (str) –

    The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) +or a specific STAC API Collection that allows to filter items and to download assets. +This includes batch job results, which itself are compliant to STAC. +For external URLs, authentication details such as API keys or tokens may need to be included in the URL.

    +

    Batch job results can be specified in two ways:

    +
      +
    • For Batch job results at the same back-end, a URL pointing to the corresponding batch job results +endpoint should be provided. The URL usually ends with /jobs/{id}/results and {id} +is the corresponding batch job ID.

    • +
    • For external results, a signed URL must be provided. Not all back-ends support signed URLs, +which are provided as a link with the link relation canonical in the batch job result metadata.

    • +
    +

  • +
  • spatial_extent (Union[Dict[str, float], Parameter, None]) –

    Limits the data to load to the specified bounding box or polygons.

    +

    For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects +with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).

    +

    For vector data, the process loads the geometry into the data cube if the geometry is fully within the +bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +Empty geometries may only be in the data cube if no spatial extent has been provided.

    +

    The GeoJSON can be one of the following feature types:

    +
      +
    • A Polygon or MultiPolygon geometry,

    • +
    • a Feature with a Polygon or MultiPolygon geometry, or

    • +
    • a FeatureCollection containing at least one Feature with Polygon or MultiPolygon geometries.

    • +
    +

    Set this parameter to None to set no limit for the spatial extent. +Be careful with this when loading large datasets. It is recommended to use this parameter instead of +using filter_bbox() or filter_spatial() directly after loading unbounded data.

    +

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) –

    Limits the data to load to the specified left-closed temporal interval. +Applies to all temporal dimensions. +The interval has to be specified as an array with exactly two elements:

    +
      +
    1. The first element is the start of the temporal interval. +The specified instance in time is included in the interval.

    2. +
    3. The second element is the end of the temporal interval. +The specified instance in time is excluded from the interval.

    4. +
    +

    The second element must always be greater/later than the first element. +Otherwise, a TemporalExtentEmpty exception is thrown.

    +

    Also supports open intervals by setting one of the boundaries to None, but never both.

    +

    Set this parameter to None to set no limit for the temporal extent. +Be careful with this when loading large datasets. It is recommended to use this parameter instead of +using filter_temporal() directly after loading unbounded data.

    +

  • +
  • bands (Optional[List[str]]) –

    Only adds the specified bands into the data cube so that bands that don’t match the list +of band names are not available. Applies to all dimensions of type bands.

    +

    Either the unique band name (metadata field name in bands) or one of the common band names +(metadata field common_name in bands) can be specified. +If the unique band name and the common name conflict, the unique band name has a higher priority.

    +

    The order of the specified array defines the order of the bands in the data cube. +If multiple bands match a common name, all matched bands are included in the original order.

    +

    It is recommended to use this parameter instead of using filter_bands() directly after loading unbounded data.

    +

  • +
  • properties (Optional[Dict[str, Union[str, PGNode, Callable]]]) –

    Limits the data by metadata properties to include only data in the data cube which +all given conditions return True for (AND operation).

    +

    Specify key-value-pairs with the key being the name of the metadata property, +which can be retrieved with the openEO Data Discovery for Collections. +The value must be a condition (user-defined process) to be evaluated against a STAC API. +This parameter is not supported for static STAC.

    +

  • +
  • connection (Optional[Connection]) – The connection to use to connect with the backend.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Added in version 0.33.0.

+
+
+ +
+
+log10()[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “log”.

+
+
+ +
+
+log2()[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “log”.

+
+
+ +
+
+logarithm(base)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “log”.

+
+
+ +
+
+logical_and(other)[source]
+

Apply element-wise logical and operation

+
+
Parameters:
+

other (DataCube)

+
+
Return type:
+

DataCube

+
+
Returns:
+

logical_and(this, other)

+
+
+
+

See also

+

openeo.org documentation on process “and”.

+
+
+ +
+
+logical_or(other)[source]
+

Apply element-wise logical or operation

+
+
Parameters:
+

other (DataCube)

+
+
Return type:
+

DataCube

+
+
Returns:
+

logical_or(this, other)

+
+
+
+

See also

+

openeo.org documentation on process “or”.

+
+
+ +
+
+mask(mask=None, replacement=None)[source]
+

Applies a mask to a raster data cube. To apply a vector mask use mask_polygon.

+

A mask is a raster data cube for which corresponding pixels among data and mask +are compared and those pixels in data are replaced whose pixels in mask are non-zero +(for numbers) or true (for boolean values). +The pixel values are replaced with the value specified for replacement, +which defaults to null (no data).

+
+
Parameters:
+
    +
  • mask (DataCube) – the raster mask

  • +
  • replacement – the value to replace the masked pixels with

  • +
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “mask”.

+
+
+ +
+
+mask_polygon(mask, srs=None, replacement=None, inside=None)[source]
+

Applies a polygon mask to a raster data cube. To apply a raster mask use mask.

+

All pixels for which the point at the pixel center does not intersect with any +polygon (as defined in the Simple Features standard by the OGC) are replaced. +This behaviour can be inverted by setting the parameter inside to true.

+

The pixel values are replaced with the value specified for replacement, +which defaults to no data.

+
+
Parameters:
+
    +
  • mask (Union[BaseGeometry, dict, str, Path, Parameter, VectorCube]) – The geometry to mask with: a shapely geometry, a GeoJSON-style dictionary, +a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file.

  • +
  • srs (str) –

    The spatial reference system of the provided polygon. +By default longitude-latitude (EPSG:4326) is assumed.

    +
    +

    Note

    +

    this srs argument is a non-standard/experimental feature, only supported by specific back-ends. +See https://github.com/Open-EO/openeo-processes/issues/235 for details.

    +
    +

  • +
  • replacement – the value to replace the masked pixels with

  • +
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “mask_polygon”.

+
+
+ +
+
+max_time()[source]
+

Finds the maximum value of a time series for all bands of the input dataset.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “max”.

+
+
+ +
+
+mean_time()[source]
+

Finds the mean value of a time series for all bands of the input dataset.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “mean”.

+
+
+ +
+
+median_time()[source]
+

Finds the median value of a time series for all bands of the input dataset.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “median”.

+
+
+ +
+
+merge(other, overlap_resolver=None, context=None)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.4.6: Usage of this legacy method is deprecated. Use +merge_cubes() instead.

+
+
+ +
+
+merge_cubes(other, overlap_resolver=None, context=None)[source]
+

Merging two data cubes

+

The data cubes have to be compatible. A merge operation without overlap should be reversible with (a set of) filter operations for each of the two cubes. The process performs the join on overlapping dimensions, with the same name and type. +An overlapping dimension has the same name, type, reference system and resolution in both dimensions, but can have different labels. One of the dimensions can have different labels, for all other dimensions the labels must be equal. If data overlaps, the parameter overlap_resolver must be specified to resolve the overlap.

+

Examples for merging two data cubes:

+
    +
  1. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first cube and B3 and B4. An overlap resolver is not needed. The merged data cube has the dimensions x, y, t and bands and the dimension bands has four dimension labels: B1, B2, B3, B4.

  2. +
  3. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first data cube and B2 and B3 for the second. An overlap resolver is required to resolve overlap in band B2. The merged data cube has the dimensions x, y, t and bands and the dimension bands has three dimension labels: B1, B2, B3.

  4. +
  5. +
    Data cubes with the dimensions x, y and t have the same dimension labels in x,y and t. There are two options:
      +
    • Keep the overlapping values separately in the merged data cube: An overlap resolver is not needed, but for each data cube you need to add a new dimension using add_dimension. The new dimensions must be equal, except that the labels for the new dimensions must differ by name. The merged data cube has the same dimensions and labels as the original data cubes, plus the dimension added with add_dimension, which has the two dimension labels after the merge.

    • +
    • Combine the overlapping values into a single value: An overlap resolver is required to resolve the overlap for all pixels. The merged data cube has the same dimensions and labels as the original data cubes, but all pixel values have been processed by the overlap resolver.

    • +
    +
    +
    +
  6. +
  7. Merging a data cube with dimensions x, y, t with another cube with dimensions x, y will join on the x, y dimension, so the lower dimension cube is merged with each time step in the higher dimensional cube. This can for instance be used to apply a digital elevation model to a spatiotemporal data cube.

  8. +
+
+
Parameters:
+
    +
  • other (DataCube) – The data cube to merge with.

  • +
  • overlap_resolver (Union[str, PGNode, Callable]) – A reduction operator that resolves the conflict if the data overlaps. The reducer must return a value of the same data type as the input values are. The reduction operator may be a single process such as multiply or consist of multiple sub-processes. null (the default) can be specified if no overlap resolver is required.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

The merged data cube.

+
+
+
+

See also

+

openeo.org documentation on process “merge_cubes”.

+
+
+ +
+
+min_time()[source]
+

Finds the minimum value of a time series for all bands of the input dataset.

+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “min”.

+
+
+ +
+
+multiply(other, reverse=False)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “multiply”.

+
+
+ +
+
+ndvi(nir=None, red=None, target_band=None)[source]
+

Normalized Difference Vegetation Index (NDVI)

+
+
Parameters:
+
    +
  • nir (str) – (optional) name of NIR band

  • +
  • red (str) – (optional) name of red band

  • +
  • target_band (str) – (optional) name of the newly created band

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “ndvi”.

+
+
+ +
+
+normalized_difference(other)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “normalized_difference”.

+
+
+ +
+
+polygonal_histogram_timeseries(polygon)[source]
+

Extract a histogram time series for the given (multi)polygon. Its points are +expected to be in the EPSG:4326 coordinate +reference system.

+
+
Parameters:
+

polygon (Union[Polygon, MultiPolygon, str]) – The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file

+
+
Return type:
+

VectorCube

+
+
+
+

Deprecated since version 0.10.0: Use aggregate_spatial() with reducer 'histogram'.

+
+
+ +
+
+polygonal_mean_timeseries(polygon)[source]
+

Extract a mean time series for the given (multi)polygon. Its points are +expected to be in the EPSG:4326 coordinate +reference system.

+
+
Parameters:
+

polygon (Union[Polygon, MultiPolygon, str]) – The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file

+
+
Return type:
+

VectorCube

+
+
+
+

Deprecated since version 0.10.0: Use aggregate_spatial() with reducer 'mean'.

+
+
+ +
+
+polygonal_median_timeseries(polygon)[source]
+

Extract a median time series for the given (multi)polygon. Its points are +expected to be in the EPSG:4326 coordinate +reference system.

+
+
Parameters:
+

polygon (Union[Polygon, MultiPolygon, str]) – The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file

+
+
Return type:
+

VectorCube

+
+
+
+

Deprecated since version 0.10.0: Use aggregate_spatial() with reducer 'median'.

+
+
+ +
+
+polygonal_standarddeviation_timeseries(polygon)[source]
+

Extract a time series of standard deviations for the given (multi)polygon. Its points are +expected to be in the EPSG:4326 coordinate +reference system.

+
+
Parameters:
+

polygon (Union[Polygon, MultiPolygon, str]) – The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file

+
+
Return type:
+

VectorCube

+
+
+
+

Deprecated since version 0.10.0: Use aggregate_spatial() with reducer 'sd'.

+
+
+ +
+
+power(p)[source]
+
+

See also

+

openeo.org documentation on process “power”.

+
+
+ +
+
+predict_curve(parameters, function, dimension, labels=None)[source]
+

Predict values using a model function and pre-computed parameters.

+
+

Warning

+

experimental process: not generally supported, API subject to change. +https://github.com/Open-EO/openeo-processes/pull/240

+
+
+
Parameters:
+
+
+
+
+

See also

+

openeo.org documentation on process “predict_curve”.

+
+
+ +
+
+predict_random_forest(model, dimension='bands')[source]
+

Apply reduce_dimension process with a predict_random_forest reducer.

+
+
Parameters:
+
    +
  • model (Union[str, BatchJob, MlModel]) –

    a reference to a trained model, one of

    +
      +
    • a MlModel instance (e.g. loaded from Connection.load_ml_model())

    • +
    • a BatchJob instance of a batch job that saved a single random forest model

    • +
    • a job id (str) of a batch job that saved a single random forest model

    • +
    • a STAC item URL (str) to load the random forest from. +(The STAC Item must implement the ml-model extension.)

    • +
    +

  • +
  • dimension (str) – dimension along which to apply the reduce_dimension process.

  • +
+
+
+
+

Added in version 0.10.0.

+
+
+

See also

+

openeo.org documentation on process “predict_random_forest”.

+
+
+ +
+
+preview(center=None, zoom=None)[source]
+

Creates a service with the process graph and displays a map widget. Only supports XYZ.

+
+
Parameters:
+
    +
  • center (Optional[Iterable]) – (optional) Map center. Default is (0,0).

  • +
  • zoom (Optional[int]) – (optional) Zoom level of the map. Default is 1.

  • +
+
+
Returns:
+

ipyleaflet Map object and the displayed Service

+
+
+
+

Warning

+

experimental feature, subject to change.

+
+
+

Added in version 0.19.0.

+
+
+ +
+
+print_json(*, file=None, indent=2, separators=None, end='\\n')
+

Print interoperable JSON representation of the process graph.

+

See DataCube.to_json() to get the JSON representation as a string +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • file – file-like object (stream) to print to (current sys.stdout by default). +Or a path (string or pathlib.Path) to a file to write to.

  • +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
  • end (str) – additional string to be printed at the end (newline by default).

  • +
+
+
+
+

Added in version 0.12.0.

+
+
+

Added in version 0.23.0: added the end argument.

+
+
+ +
+
+process(process_id, arguments=None, metadata=None, namespace=None, **kwargs)[source]
+

Generic helper to create a new DataCube by applying a process.

+
+
Parameters:
+
    +
  • process_id (str) – process id of the process.

  • +
  • arguments (Optional[dict]) – argument dictionary for the process.

  • +
  • metadata (Optional[CollectionMetadata]) – optional: metadata to override original cube metadata (e.g. when reducing dimensions)

  • +
  • namespace (Optional[str]) – optional: process namespace

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

new DataCube instance

+
+
+
+ +
+
+process_with_node(pg, metadata=None)[source]
+

Generic helper to create a new DataCube by applying a process (given as process graph node)

+
+
Parameters:
+
    +
  • pg (PGNode) – process graph node (containing process id and arguments)

  • +
  • metadata (Optional[CollectionMetadata]) – optional: metadata to override original cube metadata (e.g. when reducing dimensions)

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

new DataCube instance

+
+
+
+ +
+
+raster_to_vector()[source]
+

Converts this raster data cube into a VectorCube. +The bounding polygon of homogenous areas of pixels is constructed.

+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+
Return type:
+

VectorCube

+
+
Returns:
+

a VectorCube

+
+
+
+ +
+
+reduce_bands(reducer)[source]
+

Shortcut for reduce_dimension() along the band dimension

+
+
Parameters:
+

reducer (Union[str, PGNode, Callable, UDF]) – “child callback” function, see Processes with child “callbacks”

+
+
Return type:
+

DataCube

+
+
+
+ +
+
+reduce_bands_udf(code, runtime=None, version=None)[source]
+

Use reduce_dimension process with given UDF along band/spectral dimension. +:rtype: DataCube

+
+

Deprecated since version 0.13.0: Use reduce_bands() with UDF as reducer.

+
+
+ +
+
+reduce_dimension(dimension, reducer, context=None, process_id='reduce_dimension', band_math_mode=False)[source]
+

Add a reduce process with given reducer callback along given dimension

+
+
Parameters:
+
    +
  • dimension (str) – the label of the dimension to reduce

  • +
  • reducer (Union[str, Callable, UDF, PGNode]) –

    the “child callback”: +the name of a single openEO process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns a single numerical value. +For example:

    + +

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “reduce_dimension”.

+
+
+ +
+
+reduce_spatial(reducer, context=None)[source]
+

Add a reduce process with given reducer callback along the spatial dimensions

+
+
Parameters:
+
    +
  • reducer (Union[str, Callable, UDF, PGNode]) –

    the “child callback”: +the name of a single openEO process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns a single numerical value. +For example:

    + +

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “reduce_spatial”.

+
+
+ +
+
+reduce_temporal(reducer)[source]
+

Shortcut for reduce_dimension() along the temporal dimension

+
+
Parameters:
+

reducer (Union[str, PGNode, Callable, UDF]) – “child callback” function, see Processes with child “callbacks”

+
+
Return type:
+

DataCube

+
+
+
+ +
+
+reduce_temporal_simple(reducer)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.13.0: Usage of this legacy method is deprecated. Use +reduce_temporal() instead.

+
+
+ +
+
+reduce_temporal_udf(code, runtime='Python', version='latest')[source]
+

Apply reduce (reduce_dimension) process with given UDF along temporal dimension.

+
+
Parameters:
+
    +
  • code (str) – The UDF code, compatible with the given runtime and version

  • +
  • runtime – The UDF runtime

  • +
  • version – The UDF runtime version

  • +
+
+
+
+

Deprecated since version 0.13.0: Use reduce_temporal() with UDF as reducer

+
+
+ +
+
+reduce_tiles_over_time(code, runtime='Python', version='latest')
+
+

Deprecated since version 0.1.1: Usage of this legacy method is deprecated. Use +reduce_temporal_udf() instead.

+
+
+ +
+
+rename_dimension(source, target)[source]
+

Renames a dimension in the data cube while preserving all other properties.

+
+
Parameters:
+
    +
  • source (str) – The current name of the dimension. Fails with a DimensionNotAvailable error if the specified dimension does not exist.

  • +
  • target (str) – A new Name for the dimension. Fails with a DimensionExists error if a dimension with the specified name exists.

  • +
+
+
Returns:
+

A new datacube with the dimension renamed.

+
+
+
+

See also

+

openeo.org documentation on process “rename_dimension”.

+
+
+ +
+
+rename_labels(dimension, target, source=None)[source]
+

Renames the labels of the specified dimension in the data cube from source to target.

+
+
Parameters:
+
    +
  • dimension (str) – Dimension name

  • +
  • target (list) – The new names for the labels.

  • +
  • source (list) – The names of the labels as they are currently in the data cube.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

An DataCube instance

+
+
+
+

See also

+

openeo.org documentation on process “rename_labels”.

+
+
+ +
+
+resample_cube_spatial(target, method='near')[source]
+

Resamples the spatial dimensions (x,y) from a source data cube to align with the corresponding +dimensions of the given target data cube. +Returns a new data cube with the resampled dimensions.

+

To resample a data cube to a specific resolution or projection regardless of an existing target +data cube, refer to resample_spatial().

+
+
Parameters:
+
    +
  • target (DataCube) – A data cube that describes the spatial target resolution.

  • +
  • method (str) – Resampling method to use.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

+
+
+
+ +
+
+resample_cube_temporal(target, dimension=None, valid_within=None)[source]
+

Resamples one or more given temporal dimensions from a source data cube to align with the corresponding +dimensions of the given target data cube using the nearest neighbor method. +Returns a new data cube with the resampled dimensions.

+

By default, this process simply takes the nearest neighbor independent of the value (including values such as +no-data / null). Depending on the data cubes this may lead to values being assigned to two target timestamps. +To only consider valid values in a specific range around the target timestamps, use the parameter valid_within.

+

The rare case of ties is resolved by choosing the earlier timestamps.

+
+
Parameters:
+
    +
  • target (DataCube) – A data cube that describes the temporal target resolution.

  • +
  • dimension (Optional[str]) – The name of the temporal dimension to resample.

  • +
  • valid_within (Optional[int])

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

+
+
+
+

Added in version 0.10.0.

+
+
+

See also

+

openeo.org documentation on process “resample_cube_temporal”.

+
+
+ +
+
+resample_spatial(resolution, projection=None, method='near', align='upper-left')[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “resample_spatial”.

+
+
+ +
+
+resolution_merge(high_resolution_bands, low_resolution_bands, method=None)[source]
+

Resolution merging algorithms try to improve the spatial resolution of lower resolution bands +(e.g. Sentinel-2 20M) based on higher resolution bands. (e.g. Sentinel-2 10M).

+

External references:

+

Pansharpening explained

+

Example publication: ‘Improving the Spatial Resolution of Land Surface Phenology by Fusing Medium- and +Coarse-Resolution Inputs’

+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+
Parameters:
+
    +
  • high_resolution_bands (List[str]) – A list of band names to use as ‘high-resolution’ band. Either the unique band name (metadata field name in bands) or one of the common band names (metadata field common_name in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will remain unmodified.

  • +
  • low_resolution_bands (List[str]) – A list of band names for which the spatial resolution should be increased. Either the unique band name (metadata field name in bands) or one of the common band names (metadata field common_name in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will be modified by the process.

  • +
  • method (str) – The method to use. The supported algorithms can vary between back-ends. Set to null (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility..

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A datacube with the same bands and metadata as the input, but algorithmically increased spatial resolution for the selected bands.

+
+
+
+

See also

+

openeo.org documentation on process “resolution_merge”.

+
+
+ +
+
+result_node()
+

Get the current result node (PGNode) of the process graph. +:rtype: PGNode

+
+

Added in version 0.10.1.

+
+
+ +
+
+sar_backscatter(coefficient='gamma0-terrain', elevation_model=None, mask=False, contributing_area=False, local_incidence_angle=False, ellipsoid_incidence_angle=False, noise_removal=True, options=None)[source]
+

Computes backscatter from SAR input.

+

Note that backscatter computation may require instrument specific metadata that is tightly coupled to the +original SAR products. As a result, this process may only work in combination with loading data from +specific collections, not with general data cubes.

+
+
Parameters:
+
    +
  • coefficient (Optional[str]) –

    Select the radiometric correction coefficient. +The following options are available:

    +
      +
    • ”beta0”: radar brightness

    • +
    • ”sigma0-ellipsoid”: ground area computed with ellipsoid earth model

    • +
    • ”sigma0-terrain”: ground area computed with terrain earth model

    • +
    • ”gamma0-ellipsoid”: ground area computed with ellipsoid earth model in sensor line of sight

    • +
    • ”gamma0-terrain”: ground area computed with terrain earth model in sensor line of sight (default)

    • +
    • None: non-normalized backscatter

    • +
    +

  • +
  • elevation_model (Optional[str]) – The digital elevation model to use. Set to None (the default) to allow +the back-end to choose, which will improve portability, but reduce reproducibility.

  • +
  • mask (bool) – If set to true, a data mask is added to the bands with the name mask. +It indicates which values are valid (1), invalid (0) or contain no-data (null).

  • +
  • contributing_area (bool) – If set to true, a DEM-based local contributing area band named contributing_area +is added. The values are given in square meters.

  • +
  • local_incidence_angle (bool) – If set to true, a DEM-based local incidence angle band named +local_incidence_angle is added. The values are given in degrees.

  • +
  • ellipsoid_incidence_angle (bool) – If set to true, an ellipsoidal incidence angle band named +ellipsoid_incidence_angle is added. The values are given in degrees.

  • +
  • noise_removal (bool) – If set to false, no noise removal is applied. Defaults to true, which removes noise.

  • +
  • options (Optional[dict]) – dictionary with additional (backend-specific) options.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

+
+
+
+

Added in version 0.4.9.

+
+
+

Changed in version 0.4.10: replace orthorectify and rtc arguments with coefficient.

+
+
+

See also

+

openeo.org documentation on process “sar_backscatter”.

+
+
+ +
+
+save_result(format='GTiff', options=None)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “save_result”.

+
+
+ +
+
+save_user_defined_process(user_defined_process_id, public=False, summary=None, description=None, returns=None, categories=None, examples=None, links=None)[source]
+

Saves this process graph in the backend as a user-defined process for the authenticated user.

+
+
Parameters:
+
    +
  • user_defined_process_id (str) – unique identifier for the process

  • +
  • public (bool) – visible to other users?

  • +
  • summary (Optional[str]) – A short summary of what the process does.

  • +
  • description (Optional[str]) – Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation.

  • +
  • returns (Optional[dict]) – Description and schema of the return value.

  • +
  • categories (Optional[List[str]]) – A list of categories.

  • +
  • examples (Optional[List[dict]]) – A list of examples.

  • +
  • links (Optional[List[dict]]) – A list of links.

  • +
+
+
Return type:
+

RESTUserDefinedProcess

+
+
Returns:
+

a RESTUserDefinedProcess instance

+
+
+
+ +
+
+send_job(out_format=None, *, title=None, description=None, plan=None, budget=None, job_options=None, validate=None, auto_add_save_result=True, **format_options)
+
+
Return type:
+

BatchJob

+
+
+
+

Deprecated since version 0.10.0: Usage of this legacy method is deprecated. Use +create_job() instead.

+
+
+ +
+
+subtract(other, reverse=False)[source]
+
+
Return type:
+

DataCube

+
+
+
+

See also

+

openeo.org documentation on process “subtract”.

+
+
+ +
+
+to_json(*, indent=2, separators=None)
+

Get interoperable JSON representation of the process graph.

+

See DataCube.print_json() to directly print the JSON representation +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

JSON string

+
+
+
+ +
+
+unflatten_dimension(dimension, target_dimensions, label_separator=None)[source]
+

Splits a single dimension into multiple dimensions by systematically extracting values and splitting +the dimension labels by the given label_separator. +This process is the opposite of the process flatten_dimensions() but executing both processes +subsequently doesn’t necessarily create a data cube that is equal to the original data cube.

+
+
Parameters:
+
    +
  • dimension (str) – The name of the dimension to split.

  • +
  • target_dimensions (List[str]) – The names of the target dimensions.

  • +
  • label_separator (Optional[str]) – The string that will be used as a separator to split the dimension labels.

  • +
+
+
Returns:
+

A data cube with the new shape.

+
+
+
+

Warning

+

experimental process: not generally supported, API subject to change.

+
+
+

Added in version 0.10.0.

+
+
+

See also

+

openeo.org documentation on process “unflatten_dimension”.

+
+
+ +
+
+validate()[source]
+

Validate a process graph without executing it.

+
+
Return type:
+

List[dict]

+
+
Returns:
+

list of errors (dictionaries with “code” and “message” fields)

+
+
+
+ +
+ +
+
+class openeo.rest._datacube.UDF(code, runtime=None, data=None, version=None, context=None, _source=None)[source]
+

Helper class to load UDF code (e.g. from file) and embed them as “callback” or child process in a process graph.

+

Usage example:

+
udf = UDF.from_file("my-udf-code.py")
+cube = cube.apply(process=udf)
+
+
+
+

Changed in version 0.13.0: Added auto-detection of runtime. +Specifying the data argument is not necessary anymore, and actually deprecated. +Added from_file() to simplify loading UDF code from a file. +See openeo.UDF API and usage changes in version 0.13.0 for more background about the changes.

+
+
+
+classmethod from_file(path, runtime=None, version=None, context=None)[source]
+

Load a UDF from a local file.

+
+

See also

+

from_url() for loading from a URL.

+
+
+
Parameters:
+
    +
  • path (Union[str, Path]) – path to the local file with UDF source code

  • +
  • runtime (Optional[str]) – optional UDF runtime identifier, will be auto-detected from source code if omitted.

  • +
  • version (Optional[str]) – optional UDF runtime version string

  • +
  • context (Optional[dict]) – optional additional UDF context data

  • +
+
+
Return type:
+

UDF

+
+
+
+ +
+
+classmethod from_url(url, runtime=None, version=None, context=None)[source]
+

Load a UDF from a URL.

+
+

See also

+

from_file() for loading from a local file.

+
+
+
Parameters:
+
    +
  • url (str) – URL path to load the UDF source code from

  • +
  • runtime (Optional[str]) – optional UDF runtime identifier, will be auto-detected from source code if omitted.

  • +
  • version (Optional[str]) – optional UDF runtime version string

  • +
  • context (Optional[dict]) – optional additional UDF context data

  • +
+
+
Return type:
+

UDF

+
+
+
+ +
+
+get_run_udf_callback(connection=None, data_parameter='data')[source]
+

For internal use: construct run_udf node to be used as callback in apply, reduce_dimension, …

+
+
Return type:
+

PGNode

+
+
+
+ +
+ +
+
+

openeo.rest.vectorcube

+
+
+class openeo.rest.vectorcube.VectorCube(graph, connection, metadata=None)[source]
+

A Vector Cube, or ‘Vector Collection’ is a data structure containing ‘Features’: +https://www.w3.org/TR/sdw-bp/#dfn-feature

+

The features in this cube are restricted to have a geometry. Geometries can be points, lines, polygons etcetera. +A geometry is specified in a ‘coordinate reference system’. https://www.w3.org/TR/sdw-bp/#dfn-coordinate-reference-system-(crs)

+
+
+apply_dimension(process, dimension, target_dimension=None, context=None)[source]
+

Applies a process to all values along a dimension of a data cube. +For example, if the temporal dimension is specified the process will work on the values of a time series.

+

The process to apply is specified by providing a callback function in the process argument.

+
+
Parameters:
+
    +
  • process (Union[str, Callable, UDF, PGNode]) –

    the “child callback”: +the name of a single process, +or a callback function as discussed in Processes with child “callbacks”, +or a UDF instance.

    +

    The callback should correspond to a process that +receives an array of numerical values +and returns an array of numerical values. +For example:

    + +

  • +
  • dimension (str) – The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist.

  • +
  • target_dimension (Optional[str]) – The name of the target dimension or null (the default) to use the source dimension +specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. +The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn’t exist yet.

  • +
  • context (Optional[dict]) – Additional data to be passed to the process.

  • +
+
+
Return type:
+

VectorCube

+
+
Returns:
+

A datacube with the UDF applied to the given dimension.

+
+
Raises:
+

DimensionNotAvailable

+
+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “apply_dimension”.

+
+
+ +
+
+create_job(out_format=None, *, title=None, description=None, plan=None, budget=None, job_options=None, validate=None, auto_add_save_result=True, **format_options)[source]
+

Sends a job to the backend and returns a ClientJob instance.

+
+
Parameters:
+
    +
  • out_format (Optional[str]) – String Format of the job result.

  • +
  • title (Optional[str]) – job title

  • +
  • description (Optional[str]) – job description

  • +
  • plan (Optional[str]) – The billing plan to process and charge the job with

  • +
  • budget (Optional[float]) – Maximum budget to be spent on executing the job. +Note that some backends do not honor this limit.

  • +
  • job_options (Optional[dict]) – A dictionary containing (custom) job options

  • +
  • format_options – String Parameters for the job result format

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job.

+
+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+download(outputfile=None, format=None, options=None, *, validate=None, auto_add_save_result=True)[source]
+

Execute synchronously and download the vector cube.

+

The result will be stored to the output path, when specified. +If no output path (or None) is given, the raw download content will be returned as bytes object.

+
+
Parameters:
+
    +
  • outputfile (Union[str, Path, None]) – (optional) output file to store the result to

  • +
  • format (Optional[str]) – (optional) output format to use.

  • +
  • options (Optional[dict]) – (optional) additional output format options.

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

Optional[bytes]

+
+
+
+

Changed in version 0.21.0: When not specified explicitly, output format is guessed from output file extension.

+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+execute(*, validate=None)[source]
+

Executes the process graph.

+
+
Return type:
+

dict

+
+
+
+ +
+
+execute_batch(outputfile=None, out_format=None, *, title=None, description=None, plan=None, budget=None, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, job_options=None, validate=None, auto_add_save_result=True, **format_options)[source]
+

Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. +This method is mostly recommended if the batch job is expected to run in a reasonable amount of time.

+

For very long running jobs, you probably do not want to keep the client running.

+
+
Parameters:
+
    +
  • job_options (Optional[dict])

  • +
  • outputfile (Union[str, Path, None]) – The path of a file to which a result can be written

  • +
  • out_format (Optional[str]) – (optional) output format to use.

  • +
  • format_options – (optional) additional output format options

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_add_save_result (bool) – Automatically add a save_result node to the process graph if there is none yet.

  • +
+
+
Return type:
+

BatchJob

+
+
+
+

Changed in version 0.21.0: When not specified explicitly, output format is guessed from output file extension.

+
+
+

Changed in version 0.32.0: Added auto_add_save_result option

+
+
+ +
+
+filter_bands(bands)[source]
+
+
Return type:
+

VectorCube

+
+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “filter_bands”.

+
+
+ +
+
+filter_bbox(*, west=None, south=None, east=None, north=None, extent=None, crs=None)[source]
+
+
Return type:
+

VectorCube

+
+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “filter_bbox”.

+
+
+ +
+
+filter_labels(condition, dimension, context=None)[source]
+

Filters the dimension labels in the data cube for the given dimension. +Only the dimension labels that match the specified condition are preserved, +all other labels with their corresponding data get removed.

+
+
Parameters:
+
    +
  • condition (Union[PGNode, Callable]) – the “child callback” which will be given a single label value (number or string) +and returns a boolean expressing if the label should be preserved. +Also see Processes with child “callbacks”.

  • +
  • dimension (str) – The name of the dimension to filter on.

  • +
+
+
Return type:
+

VectorCube

+
+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “filter_labels”.

+
+
+ +
+
+filter_vector(geometries, relation='intersects')[source]
+
+
Return type:
+

VectorCube

+
+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “filter_vector”.

+
+
+ +
+
+fit_class_random_forest(target, max_variables=None, num_trees=100, seed=None)[source]
+

Executes the fit of a random forest classification based on the user input of target and predictors. +The Random Forest classification model is based on the approach by Breiman (2001).

+
+

Warning

+

EXPERIMENTAL: not generally supported, API subject to change.

+
+
+
Parameters:
+
    +
  • target (dict) – The training sites for the classification model as a vector data cube. This is associated with the target +variable for the Random Forest model. The geometry has to be associated with a value to predict (e.g. fractional +forest canopy cover).

  • +
  • max_variables (Optional[int]) – Specifies how many split variables will be used at a node. Default value is null, which corresponds to the +number of predictors divided by 3.

  • +
  • num_trees (int) – The number of trees build within the Random Forest classification.

  • +
  • seed (Optional[int]) – A randomization seed to use for the random sampling in training.

  • +
+
+
Return type:
+

MlModel

+
+
+
+

Added in version 0.16.0: Originally added in version 0.10.0 as DataCube method, +but moved to VectorCube in version 0.16.0.

+
+
+

See also

+

openeo.org documentation on process “fit_class_random_forest”.

+
+
+ +
+
+fit_regr_random_forest(target, max_variables=None, num_trees=100, seed=None)[source]
+

Executes the fit of a random forest regression based on training data. +The Random Forest regression model is based on the approach by Breiman (2001).

+
+

Warning

+

EXPERIMENTAL: not generally supported, API subject to change.

+
+
+
Parameters:
+
    +
  • target (dict) – The training sites for the regression model as a vector data cube. +This is associated with the target variable for the Random Forest model. +The geometry has to associated with a value to predict (e.g. fractional forest canopy cover).

  • +
  • max_variables (Optional[int]) – Specifies how many split variables will be used at a node. Default value is null, which corresponds to the +number of predictors divided by 3.

  • +
  • num_trees (int) – The number of trees build within the Random Forest classification.

  • +
  • seed (Optional[int]) – A randomization seed to use for the random sampling in training.

  • +
+
+
Return type:
+

MlModel

+
+
+
+

Added in version 0.16.0: Originally added in version 0.10.0 as DataCube method, +but moved to VectorCube in version 0.16.0.

+
+
+

See also

+

openeo.org documentation on process “fit_regr_random_forest”.

+
+
+ +
+
+flat_graph()
+

Get the process graph in internal flat dict representation. +:rtype: Dict[str, dict]

+
+

Warning

+

This method is mainly intended for internal use. +It is not recommended for general use and is subject to change.

+

Instead, it is recommended to use +to_json() or print_json() +to obtain a standardized, interoperable JSON representation of the process graph. +See Export a process graph for more information.

+
+
+ +
+
+classmethod load_geojson(connection, data, properties=None)[source]
+

Converts GeoJSON data as defined by RFC 7946 into a vector data cube.

+
+
Parameters:
+
    +
  • connection (Connection) – the connection to use to connect with the openEO back-end.

  • +
  • data (Union[dict, str, Path, BaseGeometry, Parameter]) –

    the geometry to load. One of:

    +
      +
    • GeoJSON-style data structure: e.g. a dictionary with "type": "Polygon" and "coordinates" fields

    • +
    • a path to a local GeoJSON file

    • +
    • a GeoJSON string

    • +
    • a shapely geometry object

    • +
    +

  • +
  • properties (Optional[List[str]]) – A list of properties from the GeoJSON file to construct an additional dimension from.

  • +
+
+
Return type:
+

VectorCube

+
+
Returns:
+

new VectorCube instance

+
+
+
+

Warning

+

EXPERIMENTAL: this process is experimental with the potential for major things to change.

+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “load_geojson”.

+
+
+ +
+
+classmethod load_url(connection, url, format, options=None)[source]
+

Loads a file from a URL

+
+
Parameters:
+
    +
  • connection (Connection) – the connection to use to connect with the openEO back-end.

  • +
  • url (str) – The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL.

  • +
  • format (str) – The file format to use when loading the data.

  • +
  • options (Optional[dict]) – The file format parameters to use when reading the data. +Must correspond to the parameters that the server reports as supported parameters for the chosen format

  • +
+
+
Return type:
+

VectorCube

+
+
Returns:
+

new VectorCube instance

+
+
+
+

Warning

+

EXPERIMENTAL: this process is experimental with the potential for major things to change.

+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “load_url”.

+
+
+ +
+
+print_json(*, file=None, indent=2, separators=None, end='\\n')
+

Print interoperable JSON representation of the process graph.

+

See DataCube.to_json() to get the JSON representation as a string +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • file – file-like object (stream) to print to (current sys.stdout by default). +Or a path (string or pathlib.Path) to a file to write to.

  • +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
  • end (str) – additional string to be printed at the end (newline by default).

  • +
+
+
+
+

Added in version 0.12.0.

+
+
+

Added in version 0.23.0: added the end argument.

+
+
+ +
+
+process(process_id, arguments=None, metadata=None, namespace=None, **kwargs)[source]
+

Generic helper to create a new VectorCube by applying a process.

+
+
Parameters:
+
    +
  • process_id (str) – process id of the process.

  • +
  • args – argument dictionary for the process.

  • +
+
+
Return type:
+

VectorCube

+
+
Returns:
+

new VectorCube instance

+
+
+
+ +
+
+result_node()
+

Get the current result node (PGNode) of the process graph. +:rtype: PGNode

+
+

Added in version 0.10.1.

+
+
+ +
+
+run_udf(udf, runtime=None, version=None, context=None)[source]
+

Run a UDF on the vector cube.

+

It is recommended to provide the UDF just as UDF instance. +(the other arguments could be used to override UDF parameters if necessary).

+
+
Parameters:
+
    +
  • udf (Union[str, UDF]) – UDF code as a string or UDF instance

  • +
  • runtime (Optional[str]) – UDF runtime

  • +
  • version (Optional[str]) – UDF version

  • +
  • context (Optional[dict]) – UDF context

  • +
+
+
Return type:
+

VectorCube

+
+
+
+

Warning

+

EXPERIMENTAL: not generally supported, API subject to change.

+
+
+

Added in version 0.10.0.

+
+
+

Changed in version 0.16.0: Added support to pass self-contained UDF instance.

+
+
+

See also

+

openeo.org documentation on process “run_udf”.

+
+
+ +
+
+save_result(format='GeoJSON', options=None)[source]
+
+

See also

+

openeo.org documentation on process “save_result”.

+
+
+ +
+
+send_job(out_format=None, *, title=None, description=None, plan=None, budget=None, job_options=None, validate=None, auto_add_save_result=True, **format_options)
+
+
Return type:
+

BatchJob

+
+
+
+

Deprecated since version 0.10.0: Usage of this legacy method is deprecated. Use +create_job() instead.

+
+
+ +
+
+to_json(*, indent=2, separators=None)
+

Get interoperable JSON representation of the process graph.

+

See DataCube.print_json() to directly print the JSON representation +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

JSON string

+
+
+
+ +
+
+vector_to_raster(target)[source]
+

Converts this vector cube (VectorCube) into a raster data cube (DataCube). +The bounding polygon of homogenous areas of pixels is constructed.

+
+
Parameters:
+

target (DataCube) – a reference raster data cube to adopt the CRS/projection/resolution from.

+
+
Return type:
+

DataCube

+
+
+
+

Warning

+

vector_to_raster is an experimental, non-standard process. It is not widely supported, and its API is subject to change.

+
+
+

Added in version 0.28.0.

+
+
+ +
+ +
+
+

openeo.rest.mlmodel

+
+
+class openeo.rest.mlmodel.MlModel(graph, connection)[source]
+

A machine learning model.

+

It is the result of a training procedure, e.g. output of a fit_... process, +and can be used for prediction (classification or regression) with the corresponding predict_... process.

+
+

Added in version 0.10.0.

+
+
+
+create_job(*, title=None, description=None, plan=None, budget=None, job_options=None)[source]
+

Sends a job to the backend and returns a ClientJob instance.

+
+
Parameters:
+
    +
  • title (Optional[str]) – job title

  • +
  • description (Optional[str]) – job description

  • +
  • plan (Optional[str]) – The billing plan to process and charge the job with

  • +
  • budget (Optional[float]) – Maximum budget to be spent on executing the job. +Note that some backends do not honor this limit.

  • +
  • job_options (Optional[dict]) – A dictionary containing (custom) job options

  • +
  • format_options – String Parameters for the job result format

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job.

+
+
+
+ +
+
+execute_batch(outputfile, *, title=None, description=None, plan=None, budget=None, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, job_options=None)[source]
+

Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. +This method is mostly recommended if the batch job is expected to run in a reasonable amount of time.

+

For very long running jobs, you probably do not want to keep the client running.

+
+
Parameters:
+
    +
  • job_options

  • +
  • outputfile (Union[str, Path]) – The path of a file to which a result can be written

  • +
  • out_format – (optional) Format of the job result.

  • +
  • format_options – String Parameters for the job result format

  • +
+
+
Return type:
+

BatchJob

+
+
+
+ +
+
+flat_graph()
+

Get the process graph in internal flat dict representation. +:rtype: Dict[str, dict]

+
+

Warning

+

This method is mainly intended for internal use. +It is not recommended for general use and is subject to change.

+

Instead, it is recommended to use +to_json() or print_json() +to obtain a standardized, interoperable JSON representation of the process graph. +See Export a process graph for more information.

+
+
+ +
+
+static load_ml_model(connection, id)[source]
+

Loads a machine learning model from a STAC Item.

+
+
Parameters:
+
    +
  • connection (Connection) – connection object

  • +
  • id (Union[str, BatchJob]) – STAC item reference, as URL, batch job (id) or user-uploaded file

  • +
+
+
Return type:
+

MlModel

+
+
Returns:
+

+
+
+
+

Added in version 0.10.0.

+
+
+

See also

+

openeo.org documentation on process “load_ml_model”.

+
+
+ +
+
+print_json(*, file=None, indent=2, separators=None, end='\\n')
+

Print interoperable JSON representation of the process graph.

+

See DataCube.to_json() to get the JSON representation as a string +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • file – file-like object (stream) to print to (current sys.stdout by default). +Or a path (string or pathlib.Path) to a file to write to.

  • +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
  • end (str) – additional string to be printed at the end (newline by default).

  • +
+
+
+
+

Added in version 0.12.0.

+
+
+

Added in version 0.23.0: added the end argument.

+
+
+ +
+
+result_node()
+

Get the current result node (PGNode) of the process graph. +:rtype: PGNode

+
+

Added in version 0.10.1.

+
+
+ +
+
+save_ml_model(options=None)[source]
+

Saves a machine learning model as part of a batch job.

+
+
Parameters:
+

options (Optional[dict]) – Additional parameters to create the file(s).

+
+
+
+ +
+
+to_json(*, indent=2, separators=None)
+

Get interoperable JSON representation of the process graph.

+

See DataCube.print_json() to directly print the JSON representation +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

JSON string

+
+
+
+ +
+ +
+
+

openeo.rest.multiresult

+
+
+class openeo.rest.multiresult.MultiResult(leaves, connection=None)[source]
+

Helper to create and run batch jobs with process graphs +that contain multiple result nodes +or, more generally speaking, multiple process graph “leaf” nodes.

+

Provide multiple +DataCube/VectorCube +instances to the constructor, +and start a batch job from that, +for example as follows:

+
from openeo import MultiResult
+
+cube1 = ...
+cube2 = ...
+multi_result = MultiResult([cube1, cube2])
+job = multi_result.create_job()
+
+
+ +
+

Added in version 0.35.0.

+
+
+
+__init__(leaves, connection=None)[source]
+

Build a MultiResult instance from multiple leaf nodes

+
+
Parameters:
+
    +
  • leaves (List[FlatGraphableMixin]) – list of objects that can be +converted to an openEO-style (flat) process graph representation, +typically DataCube +or VectorCube instances.

  • +
  • connection (Optional[Connection]) – Optional connection to use for creating/starting batch jobs, +for special use cases where the provided leaf instances +are not already associated with a connection.

  • +
+
+
+
+ +
+
+print_json(*, file=None, indent=2, separators=None, end='\\n')
+

Print interoperable JSON representation of the process graph.

+

See DataCube.to_json() to get the JSON representation as a string +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • file – file-like object (stream) to print to (current sys.stdout by default). +Or a path (string or pathlib.Path) to a file to write to.

  • +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
  • end (str) – additional string to be printed at the end (newline by default).

  • +
+
+
+
+

Added in version 0.12.0.

+
+
+

Added in version 0.23.0: added the end argument.

+
+
+ +
+
+to_json(*, indent=2, separators=None)
+

Get interoperable JSON representation of the process graph.

+

See DataCube.print_json() to directly print the JSON representation +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

JSON string

+
+
+
+ +
+ +
+
+

openeo.metadata

+
+
+class openeo.metadata.BandDimension(name, bands)[source]
+
+
+append_band(band)[source]
+

Create new BandDimension with appended band.

+
+
Return type:
+

BandDimension

+
+
+
+ +
+
+band_index(band)[source]
+

Resolve a given band (common) name/index to band index

+
+
Parameters:
+

band (Union[int, str]) – band name, common name or index

+
+
Return int:
+

band index

+
+
Return type:
+

int

+
+
+
+ +
+
+band_name(band, allow_common=True)[source]
+

Resolve (common) name or index to a valid (common) name

+
+
Return type:
+

str

+
+
+
+ +
+
+filter_bands(bands)[source]
+

Construct new BandDimension with subset of bands, +based on given band indices or (common) names

+
+
Return type:
+

BandDimension

+
+
+
+ +
+
+rename(name)[source]
+

Create new dimension with new name.

+
+
Return type:
+

Dimension

+
+
+
+ +
+
+rename_labels(target, source)[source]
+

Rename labels, if the type of dimension allows it.

+
+
Parameters:
+
    +
  • target – List of target labels

  • +
  • source – Source labels, or empty list

  • +
+
+
Return type:
+

Dimension

+
+
Returns:
+

A new dimension with modified labels, or the same if no change is applied.

+
+
+
+ +
+ +
+
+class openeo.metadata.CollectionMetadata(metadata, dimensions=None)[source]
+

Wrapper for EO Data Collection metadata.

+

Simplifies getting values from deeply nested mappings, +allows additional parsing and normalizing compatibility issues.

+

Metadata is expected to follow format defined by +https://openeo.org/documentation/1.0/developers/api/reference.html#operation/describe-collection +(with partial support for older versions)

+
+ +
+
+class openeo.metadata.SpatialDimension(name, extent, crs=4326, step=None)[source]
+
+
+rename(name)[source]
+

Create new dimension with new name.

+
+
Return type:
+

Dimension

+
+
+
+ +
+ +
+
+class openeo.metadata.TemporalDimension(name, extent)[source]
+
+
+rename(name)[source]
+

Create new dimension with new name.

+
+
Return type:
+

Dimension

+
+
+
+ +
+
+rename_labels(target, source)[source]
+

Rename labels, if the type of dimension allows it.

+
+
Parameters:
+
    +
  • target – List of target labels

  • +
  • source – Source labels, or empty list

  • +
+
+
Return type:
+

Dimension

+
+
Returns:
+

A new dimension with modified labels, or the same if no change is applied.

+
+
+
+ +
+ +
+
+

openeo.api.process

+
+
+class openeo.api.process.Parameter(name, description=None, schema=None, default=<object object>, optional=None)[source]
+

A (process) parameter to build parameterized +user-defined processes.

+

Parameter objects can be defined +with at least a name and expected schema +(e.g. is the parameter a placeholder for a string, a bounding box, a date, …) +and can then be used +with various functions and classes, +like DataCube, +to build parameterized user-defined processes.

+

Apart from the generic Parameter constructor, +this class also provides various helpers (class methods) +to easily create parameters for common parameter types.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
  • schema (Union[list, dict, str, None]) – JSON schema describing the expected data type and structure of the parameter.

  • +
  • default – default value for the parameter when it’s optional.

  • +
  • optional (Optional[bool]) – toggle to indicate whether the parameter is optional or required.

  • +
+
+
+
+
+classmethod array(name, description=None, *, item_schema=None, **kwargs)[source]
+

Helper to easily create parameter with an ‘array’ schema.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
  • item_schema (Union[str, dict, None]) – Schema of the array items given in JSON Schema style, e.g. {"type": "string"}. +Simple schemas can also be specified as single string: +e.g. "string" will be expanded to {"type": "string"}.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Changed in version 0.23.0: Added item_schema argument.

+
+
+ +
+
+classmethod boolean(name, description=None, **kwargs)[source]
+

Helper to easily create a ‘boolean’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+ +
+
+classmethod bounding_box(name, description="Spatial extent specified as a bounding box with 'west', 'south', 'east' and 'north' fields.", **kwargs)[source]
+

Helper to easily create a ‘bounding box’ parameter, which allows to specify a spatial extent +with “west”, “south”, “east” and “north” bounds (and optionally a CRS identifier).

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.30.0.

+
+
+ +
+
+classmethod datacube(name='data', description='A data cube.', **kwargs)[source]
+

Helper to easily create a ‘datacube’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.22.0.

+
+
+ +
+
+classmethod date(name, description='A date.', **kwargs)[source]
+

Helper to easily create a ‘date’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.30.0.

+
+
+ +
+
+classmethod date_time(name, description='A date with time.', **kwargs)[source]
+

Helper to easily create a ‘date-time’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.30.0.

+
+
+ +
+
+classmethod geojson(name, description='Geometries specified as GeoJSON object.', **kwargs)[source]
+

Helper to easily create a ‘geojson’ parameter, which allows to specify geometries as an inline GeoJSON object.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.30.0.

+
+
+ +
+
+classmethod integer(name, description=None, **kwargs)[source]
+

Helper to create an ‘integer’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+ +
+
+classmethod number(name, description=None, **kwargs)[source]
+

Helper to easily create a ‘number’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+ +
+
+classmethod object(name, description=None, *, subtype=None, **kwargs)[source]
+

Helper to create an ‘object’ type parameter

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
  • subtype (Optional[str]) – subtype of the ‘object’ schema

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.26.0.

+
+
+ +
+
+classmethod raster_cube(name='data', description='A data cube.', **kwargs)[source]
+

Helper to easily create a ‘raster-cube’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+ +
+
+classmethod spatial_extent(name='spatial_extent', description='Limits the data to process to the specified bounding box or polygons.\\n\\nFor raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).\\nFor vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided.\\n\\nEmpty geometries are ignored.\\nSet this parameter to null to set no limit for the spatial extent. ', **kwargs)[source]
+

Helper to easily create a ‘spatial_extent’ parameter, which is compatible with the ‘load_collection’ argument of +the same name. This allows to conveniently create user-defined processes that can be applied to a bounding box and vector data +for spatial filtering. It is also possible for users to set to null, and define spatial filtering using other processes.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.32.0.

+
+
+ +
+
+classmethod string(name, description=None, *, values=None, subtype=None, format=None, **kwargs)[source]
+

Helper to easily create a ‘string’ parameter.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (Optional[str]) – human-readable description of the parameter.

  • +
  • values (Optional[List[str]]) – Optional list of allowed string values to make this an “enum”.

  • +
  • subtype (Optional[str]) – Optional subtype of the ‘string’ schema.

  • +
  • format (Optional[str]) – Optional format of the ‘string’ schema.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+ +
+
+classmethod temporal_interval(name, description='Temporal extent specified as two-element array with start and end date/date-time.', **kwargs)[source]
+

Helper to easily create a ‘temporal-interval’ parameter, which allows to specify a temporal extent +as a two-element array with start and end date/date-time.

+
+
Parameters:
+
    +
  • name (str) – parameter name, which will be used to assign concrete values to. +It is recommended to stick to the convention of snake case naming (using lowercase with underscores).

  • +
  • description (str) – human-readable description of the parameter.

  • +
+
+
Return type:
+

Parameter

+
+
+

See the generic Parameter constructor for information on additional arguments (except schema).

+
+

Added in version 0.30.0.

+
+
+ +
+
+to_dict()[source]
+

Convert to dictionary for JSON-serialization.

+
+
Return type:
+

dict

+
+
+
+ +
+ +
+
+

openeo.api.logs

+
+
+class openeo.api.logs.LogEntry(*args, **kwargs)[source]
+

Log message and info for jobs and services

+
+
Fields:
    +
  • id: Unique ID for the log, string, REQUIRED

  • +
  • code: Error code, string, optional

  • +
  • level: Severity level, string (error, warning, info or debug), REQUIRED

  • +
  • message: Error message, string, REQUIRED

  • +
  • time: Date and time of the error event as RFC3339 date-time, string, available since API 1.1.0

  • +
  • path: A “stack trace” for the process, array of dicts

  • +
  • links: Related links, array of dicts

  • +
  • usage: Usage metrics available as property ‘usage’, dict, available since API 1.1.0 +May contain the following metrics: cpu, memory, duration, network, disk, storage and other custom ones +Each of the metrics is also a dict with the following parts: value (numeric) and unit (string)

  • +
  • data: Arbitrary data the user wants to “log” for debugging purposes. +Please note that this property may not exist as there’s a difference +between None and non-existing. None for example refers to no-data in +many cases while the absence of the property means that the user did +not provide any data for debugging.

  • +
+
+
+
+ +
+
+openeo.api.logs.normalize_log_level(log_level, default=10)[source]
+

Helper function to convert a openEO API log level (e.g. string “error”) +to the integer constants defined in Python’s standard library logging module (e.g. logging.ERROR).

+
+
Parameters:
+
    +
  • log_level (Union[int, str, None]) – log level to normalize: a log level string in the style of +the openEO API (“error”, “warning”, “info”, or “debug”), +an integer value (e.g. a logging constant), or None.

  • +
  • default (int) – fallback log level to return on unknown log level strings or None input.

  • +
+
+
Raises:
+

TypeError – when log_level is any other type than str, an int or None.

+
+
Return type:
+

int

+
+
Returns:
+

One of the following log level constants from the standard module logging: +logging.ERROR, logging.WARNING, logging.INFO, or logging.DEBUG .

+
+
+
+ +
+
+

openeo.rest.connection

+

This module provides a Connection object to manage and persist settings when interacting with the OpenEO API.

+
+
+class openeo.rest.connection.Connection(url, *, session=None, default_timeout=None, auto_validate=True, slow_response_threshold=None, auth_config=None, refresh_token_store=None, oidc_auth_renewer=None, auth=None)[source]
+

Connection to an openEO backend.

+
+
Parameters:
+
    +
  • url (str) – Backend root url

  • +
  • session (Optional[Session]) – Optional requests.Session object to use for requests.

  • +
  • default_timeout (Optional[int]) – Default timeout for requests in seconds.

  • +
  • auto_validate (bool) – toggle to automatically validate process graphs before execution

  • +
  • slow_response_threshold (Optional[float]) – Optional threshold in seconds +to consider a response as slow and log a warning.

  • +
  • auth_config (Optional[AuthConfig]) – Optional AuthConfig object +to fetch authentication related configuration from.

  • +
  • refresh_token_store (Optional[RefreshTokenStore]) – For advanced usage: +custom RefreshTokenStore object +to use for storing/loading refresh tokens.

  • +
  • oidc_auth_renewer (Optional[OidcAuthenticator]) – For advanced usage: +optional OidcAuthenticator object to use for renewing OIDC tokens.

  • +
  • auth (Optional[AuthBase]) – Optional requests.auth.AuthBase object to use for requests. +Usage of this parameter is deprecated, use the specific authentication methods instead.

  • +
+
+
+
+
+as_curl(data, path='/result', method='POST', obfuscate_auth=False)[source]
+

Build curl command to evaluate given process graph or data cube +(including authorization and content-type headers).

+
>>> print(connection.as_curl(cube))
+curl -i -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer ...' \
+    --data '{"process":{"process_graph":{...}}' \
+    https://openeo.example/openeo/1.1/result
+
+
+
+
Parameters:
+
    +
  • data (Union[dict, DataCube, FlatGraphableMixin]) – something that is convertable to an openEO process graph: a dictionary, +a DataCube object, +a ProcessBuilder, …

  • +
  • path – endpoint to send request to: typically "/result" (default) for synchronous requests +or "/jobs" for batch jobs

  • +
  • method – HTTP method to use (typically "POST")

  • +
  • obfuscate_auth (bool) – don’t show actual bearer token

  • +
+
+
Return type:
+

str

+
+
Returns:
+

curl command as a string

+
+
+
+ +
+
+assert_user_defined_process_support()[source]
+

Capabilities document based verification that back-end supports user-defined processes.

+
+

Added in version 0.23.0.

+
+
+ +
+
+authenticate_basic(username=None, password=None)[source]
+

Authenticate a user to the backend using basic username and password.

+
+
Parameters:
+
    +
  • username (Optional[str]) – User name

  • +
  • password (Optional[str]) – User passphrase

  • +
+
+
Return type:
+

Connection

+
+
+
+ +
+
+authenticate_oidc(provider_id=None, client_id=None, client_secret=None, *, store_refresh_token=True, use_pkce=None, display=<built-in function print>, max_poll_time=300)[source]
+

Generic method to do OpenID Connect authentication.

+

In the context of interactive usage, this method first tries to use refresh tokens +and falls back on device code flow.

+

For non-interactive, machine-to-machine contexts, it is also possible to trigger +the usage of the “client_credentials” flow through environment variables. +Assuming you have set up a OIDC client (with a secret): +set OPENEO_AUTH_METHOD to client_credentials, +set OPENEO_AUTH_CLIENT_ID to the client id, +and set OPENEO_AUTH_CLIENT_SECRET to the client secret.

+

See OIDC Authentication: Dynamic Method Selection for more details.

+
+
Parameters:
+
    +
  • provider_id (Optional[str]) – provider id to use

  • +
  • client_id (Optional[str]) – client id to use

  • +
  • client_secret (Optional[str]) – client secret to use

  • +
  • max_poll_time (float) – maximum time in seconds to keep polling for successful authentication.

  • +
+
+
+
+

Added in version 0.6.0.

+
+
+

Changed in version 0.17.0: Add max_poll_time argument

+
+
+

Changed in version 0.18.0: Add support for client credentials flow.

+
+
+ +
+
+authenticate_oidc_access_token(access_token, provider_id=None)[source]
+

Set up authorization headers directly with an OIDC access token.

+

Connection provides multiple methods to handle various OIDC authentication flows end-to-end. +If you already obtained a valid OIDC access token in another “out-of-band” way, you can use this method to +set up the authorization headers appropriately.

+
+
Parameters:
+
    +
  • access_token (str) – OIDC access token

  • +
  • provider_id (Optional[str]) – id of the OIDC provider as listed by the openEO backend (/credentials/oidc). +If not specified, the first (default) OIDC provider will be used.

  • +
  • skip_verification – Skip clients-side verification of the provider_id +against the backend’s list of providers to avoid and related OIDC configuration

  • +
+
+
Return type:
+

Connection

+
+
+
+

Added in version 0.31.0.

+
+
+

Changed in version 0.33.0: Return connection object to support chaining.

+
+
+ +
+
+authenticate_oidc_authorization_code(client_id=None, client_secret=None, provider_id=None, timeout=None, server_address=None, webbrowser_open=None, store_refresh_token=False)[source]
+

OpenID Connect Authorization Code Flow (with PKCE). +:rtype: Connection

+
+

Deprecated since version 0.19.0: Usage of the Authorization Code flow is deprecated (because of its complexity) and will be removed. +It is recommended to use the Device Code flow with authenticate_oidc_device() +or Client Credentials flow with authenticate_oidc_client_credentials().

+
+
+ +
+
+authenticate_oidc_client_credentials(client_id=None, client_secret=None, provider_id=None)[source]
+

Authenticate with OIDC Client Credentials flow

+

Client id, secret and provider id can be specified directly through the available arguments. +It is also possible to leave these arguments empty and specify them through +environment variables OPENEO_AUTH_CLIENT_ID, +OPENEO_AUTH_CLIENT_SECRET and OPENEO_AUTH_PROVIDER_ID respectively +as discussed in OIDC Client Credentials Using Environment Variables.

+
+
Parameters:
+
    +
  • client_id (Optional[str]) – client id to use

  • +
  • client_secret (Optional[str]) – client secret to use

  • +
  • provider_id (Optional[str]) – provider id to use +Fallback value can be set through environment variable OPENEO_AUTH_PROVIDER_ID.

  • +
+
+
Return type:
+

Connection

+
+
+
+

Changed in version 0.18.0: Allow specifying client id, secret and provider id through environment variables.

+
+
+ +
+
+authenticate_oidc_device(client_id=None, client_secret=None, provider_id=None, *, store_refresh_token=False, use_pkce=None, max_poll_time=300, **kwargs)[source]
+

Authenticate with the OIDC Device Code flow

+
+
Parameters:
+
    +
  • client_id (Optional[str]) – client id to use instead of the default one

  • +
  • client_secret (Optional[str]) – client secret to use instead of the default one

  • +
  • provider_id (Optional[str]) – provider id to use. +Fallback value can be set through environment variable OPENEO_AUTH_PROVIDER_ID.

  • +
  • store_refresh_token (bool) – whether to store the received refresh token automatically

  • +
  • use_pkce (Optional[bool]) – Use PKCE instead of client secret. +If not set explicitly to True (use PKCE) or False (use client secret), +it will be attempted to detect the best mode automatically. +Note that PKCE for device code is not widely supported among OIDC providers.

  • +
  • max_poll_time (float) – maximum time in seconds to keep polling for successful authentication.

  • +
+
+
Return type:
+

Connection

+
+
+
+

Changed in version 0.5.1: Add use_pkce argument

+
+
+

Changed in version 0.17.0: Add max_poll_time argument

+
+
+

Changed in version 0.19.0: Support fallback provider id through environment variable OPENEO_AUTH_PROVIDER_ID.

+
+
+ +
+
+authenticate_oidc_refresh_token(client_id=None, refresh_token=None, client_secret=None, provider_id=None, *, store_refresh_token=False)[source]
+

Authenticate with OIDC Refresh Token flow

+
+
Parameters:
+
    +
  • client_id (Optional[str]) – client id to use

  • +
  • refresh_token (Optional[str]) – refresh token to use

  • +
  • client_secret (Optional[str]) – client secret to use

  • +
  • provider_id (Optional[str]) – provider id to use. +Fallback value can be set through environment variable OPENEO_AUTH_PROVIDER_ID.

  • +
  • store_refresh_token (bool) – whether to store the received refresh token automatically

  • +
+
+
Return type:
+

Connection

+
+
+
+

Changed in version 0.19.0: Support fallback provider id through environment variable OPENEO_AUTH_PROVIDER_ID.

+
+
+ +
+
+authenticate_oidc_resource_owner_password_credentials(username, password, client_id=None, client_secret=None, provider_id=None, store_refresh_token=False)[source]
+

OpenId Connect Resource Owner Password Credentials

+
+
Return type:
+

Connection

+
+
+
+ +
+
+capabilities()[source]
+

Loads all available capabilities.

+
+
Return type:
+

RESTCapabilities

+
+
+
+ +
+
+collection_items(name, spatial_extent=None, temporal_extent=None, limit=None)[source]
+

Loads items for a specific image collection. +May not be available for all collections.

+

This is an experimental API and is subject to change.

+
+
Parameters:
+
    +
  • name – String Id of the collection

  • +
  • spatial_extent (Optional[List[float]]) – Limits the items to the given bounding box in WGS84: +1. Lower left corner, coordinate axis 1 +2. Lower left corner, coordinate axis 2 +3. Upper right corner, coordinate axis 1 +4. Upper right corner, coordinate axis 2

  • +
  • temporal_extent (Optional[List[Union[str, datetime]]]) – Limits the items to the specified temporal interval.

  • +
  • limit (Optional[int]) – The amount of items per request/page. If None, the back-end decides. +The interval has to be specified as an array with exactly two elements (start, end). +Also supports open intervals by setting one of the boundaries to None, but never both.

  • +
+
+
Return type:
+

Iterator[dict]

+
+
Returns:
+

data_list: List A list of items

+
+
+
+ +
+
+create_job(process_graph, *, title=None, description=None, plan=None, budget=None, additional=None, validate=None)[source]
+

Create a new job from given process graph on the back-end.

+
+
Parameters:
+
    +
  • process_graph (Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]]) – openEO-style (flat) process graph representation, +or an object that can be converted to such a representation: +a dictionary, a DataCube object, +a string with a JSON representation, +a local file path or URL to a JSON representation, +a MultiResult object, …

  • +
  • title (Optional[str]) – job title

  • +
  • description (Optional[str]) – job description

  • +
  • plan (Optional[str]) – The billing plan to process and charge the job with

  • +
  • budget (Optional[float]) – Maximum budget to be spent on executing the job. +Note that some backends do not honor this limit.

  • +
  • additional (Optional[dict]) – additional job options to pass to the backend

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job

+
+
+
+

Changed in version 0.35.0: Add multi-result support.

+
+
+ +
+
+datacube_from_flat_graph(flat_graph, parameters=None)[source]
+

Construct a DataCube from a flat dictionary representation of a process graph.

+ +
+
Parameters:
+
    +
  • flat_graph (dict) – flat dictionary representation of a process graph +or a process dictionary with such a flat process graph under a “process_graph” field +(and optionally parameter metadata under a “parameters” field).

  • +
  • parameters (Optional[dict]) – Optional dictionary mapping parameter names to parameter values +to use for parameters occurring in the process graph (e.g. as used in user-defined processes)

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A DataCube corresponding with the operations encoded in the process graph

+
+
+
+ +
+
+datacube_from_json(src, parameters=None)[source]
+

Construct a DataCube from JSON resource containing (flat) process graph representation.

+ +
+
Parameters:
+
    +
  • src (Union[str, Path]) – raw JSON string, URL to JSON resource or path to local JSON file

  • +
  • parameters (Optional[dict]) – Optional dictionary mapping parameter names to parameter values +to use for parameters occurring in the process graph (e.g. as used in user-defined processes)

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A DataCube corresponding with the operations encoded in the process graph

+
+
+
+ +
+
+datacube_from_process(process_id, namespace=None, **kwargs)[source]
+

Load a data cube from a (custom) process.

+
+
Parameters:
+
    +
  • process_id (str) – The process id.

  • +
  • namespace (Optional[str]) – optional: process namespace

  • +
  • kwargs – The arguments of the custom process

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

A DataCube, without valid metadata, as the client is not aware of this custom process.

+
+
+
+ +
+
+describe_account()[source]
+

Describes the currently authenticated user account.

+
+
Return type:
+

dict

+
+
+
+ +
+
+describe_collection(collection_id)[source]
+

Get full collection metadata for given collection id.

+
+

See also

+

list_collection_ids() +to list all collection ids provided by the back-end.

+
+
+
Parameters:
+

collection_id (str) – collection id

+
+
Return type:
+

dict

+
+
Returns:
+

collection metadata.

+
+
+
+ +
+
+describe_process(id, namespace=None)[source]
+

Returns a single process from the back end.

+
+
Parameters:
+
    +
  • id (str) – The id of the process.

  • +
  • namespace (Optional[str]) – The namespace of the process.

  • +
+
+
Return type:
+

dict

+
+
Returns:
+

The process definition.

+
+
+
+ +
+
+download(graph, outputfile=None, *, timeout=None, validate=None, chunk_size=10000000)[source]
+

Downloads the result of a process graph synchronously, +and save the result to the given file or return bytes object if no outputfile is specified. +This method is useful to export binary content such as images. For json content, the execute method is recommended.

+
+
Parameters:
+
    +
  • graph (Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]]) – (flat) dict representing a process graph, or process graph as raw JSON string, +or as local file path or URL

  • +
  • outputfile (Union[Path, str, None]) – output file

  • +
  • timeout (Optional[int]) – timeout to wait for response

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • chunk_size (int) – chunk size for streaming response.

  • +
+
+
Return type:
+

Optional[bytes]

+
+
+
+ +
+
+execute(process_graph, *, timeout=None, validate=None, auto_decode=True)[source]
+

Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed.

+
+
Parameters:
+
    +
  • process_graph (Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]]) – (flat) dict representing a process graph, or process graph as raw JSON string, +or as local file path or URL

  • +
  • validate (Optional[bool]) – Optional toggle to enable/prevent validation of the process graphs before execution +(overruling the connection’s auto_validate setting).

  • +
  • auto_decode (bool) – Boolean flag to enable/disable automatic JSON decoding of the response. Defaults to True.

  • +
+
+
Return type:
+

Union[dict, Response]

+
+
Returns:
+

parsed JSON response as a dict if auto_decode is True, otherwise response object

+
+
+
+ +
+
+get_file(path, metadata=None)[source]
+

Gets a handle to a user-uploaded file in the user workspace on the back-end.

+
+
Parameters:
+

path (Union[str, PurePosixPath]) – The path on the user workspace.

+
+
Return type:
+

UserFile

+
+
+
+ +
+
+imagecollection(collection_id, spatial_extent=None, temporal_extent=None, bands=None, properties=None, max_cloud_cover=None, fetch_metadata=True)
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.4.10: Usage of this legacy method is deprecated. Use +load_collection() instead.

+
+
+ +
+
+job(job_id)[source]
+

Get the job based on the id. The job with the given id should already exist.

+

Use openeo.rest.connection.Connection.create_job() to create new jobs

+
+
Parameters:
+

job_id (str) – the job id of an existing job

+
+
Return type:
+

BatchJob

+
+
Returns:
+

A job object.

+
+
+
+ +
+
+job_logs(job_id, offset)[source]
+

Get batch job logs. +:rtype: list

+
+

Deprecated since version 0.4.10: Use openeo.rest.job.BatchJob.logs() instead.

+
+
+ +
+
+job_results(job_id)[source]
+

Get batch job results metadata. +:rtype: dict

+
+

Deprecated since version 0.4.10: Use openeo.rest.job.BatchJob.get_results() instead.

+
+
+ +
+
+list_collection_ids()[source]
+

List all collection ids provided by the back-end.

+
+

See also

+

describe_collection() +to get the metadata of a particular collection.

+
+
+
Return type:
+

List[str]

+
+
Returns:
+

list of collection ids

+
+
+
+ +
+
+list_collections()[source]
+

List basic metadata of all collections provided by the back-end.

+
+

Caution

+

Only the basic collection metadata will be returned. +To obtain full metadata of a particular collection, +it is recommended to use describe_collection() instead.

+
+
+
Return type:
+

List[dict]

+
+
Returns:
+

list of dictionaries with basic collection metadata.

+
+
+
+ +
+
+list_file_formats()[source]
+

Get available input and output formats

+
+
Return type:
+

dict

+
+
+
+ +
+
+list_file_types()
+
+
Return type:
+

dict

+
+
+
+

Deprecated since version 0.4.6: Usage of this legacy method is deprecated. Use +list_output_formats() instead.

+
+
+ +
+
+list_files()[source]
+

Lists all user-uploaded files in the user workspace on the back-end.

+
+
Return type:
+

List[UserFile]

+
+
Returns:
+

List of the user-uploaded files.

+
+
+
+ +
+
+list_jobs()[source]
+

Lists all jobs of the authenticated user.

+
+
Return type:
+

List[dict]

+
+
Returns:
+

job_list: Dict of all jobs of the user.

+
+
+
+ +
+
+list_processes(namespace=None)[source]
+

Loads all available processes of the back end.

+
+
Parameters:
+

namespace (Optional[str]) – The namespace for which to list processes.

+
+
Return type:
+

List[dict]

+
+
Returns:
+

processes_dict: Dict All available processes of the back end.

+
+
+
+ +
+
+list_service_types()[source]
+

Loads all available service types.

+
+
Return type:
+

dict

+
+
Returns:
+

data_dict: Dict All available service types

+
+
+
+ +
+
+list_services()[source]
+

Loads all available services of the authenticated user.

+
+
Return type:
+

dict

+
+
Returns:
+

data_dict: Dict All available services

+
+
+
+ +
+
+list_udf_runtimes()[source]
+

List information about the available UDF runtimes.

+
+
Return type:
+

dict

+
+
Returns:
+

A dictionary with metadata about each available UDF runtime.

+
+
+
+ +
+
+list_user_defined_processes()[source]
+

Lists all user-defined processes of the authenticated user.

+
+
Return type:
+

List[dict]

+
+
+
+ +
+
+load_collection(collection_id, spatial_extent=None, temporal_extent=None, bands=None, properties=None, max_cloud_cover=None, fetch_metadata=True)[source]
+

Load a DataCube by collection id.

+
+
Parameters:
+
    +
  • collection_id (Union[str, Parameter]) – image collection identifier

  • +
  • spatial_extent (Union[Dict[str, float], Parameter, None]) – limit data to specified bounding box or polygons

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) – limit data to specified temporal interval. +Typically, just a two-item list or tuple containing start and end date. +See Filter on temporal extent for more details on temporal extent handling and shorthand notation.

  • +
  • bands (Union[None, List[str], Parameter]) – only add the specified bands.

  • +
  • properties (Union[None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty]) – limit data by collection metadata property predicates. +See collection_property() for easy construction of such predicates.

  • +
  • max_cloud_cover (Optional[float]) – shortcut to set maximum cloud cover (“eo:cloud_cover” collection property)

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

a datacube containing the requested data

+
+
+
+

Added in version 0.13.0: added the max_cloud_cover argument.

+
+
+

Changed in version 0.23.0: Argument temporal_extent: add support for year/month shorthand notation +as discussed at Year/month shorthand notation.

+
+
+

Changed in version 0.26.0: Add collection_property() support to properties argument.

+
+
+

See also

+

openeo.org documentation on process “load_collection”.

+
+
+ +
+
+load_disk_collection(format, glob_pattern, options=None)[source]
+

Loads image data from disk as a DataCube.

+

This is backed by a non-standard process (‘load_disk_data’). This will eventually be replaced by standard options such as +openeo.rest.connection.Connection.load_stac() or https://processes.openeo.org/#load_uploaded_files

+
+
Parameters:
+
    +
  • format (str) – the file format, e.g. ‘GTiff’

  • +
  • glob_pattern (str) – a glob pattern that matches the files to load from disk

  • +
  • options (Optional[dict]) – options specific to the file format

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Deprecated since version 0.25.0: Depends on non-standard process, replace with +openeo.rest.connection.Connection.load_stac() where +possible.

+
+
+ +
+
+load_geojson(data, properties=None)[source]
+

Converts GeoJSON data as defined by RFC 7946 into a vector data cube.

+
+
Parameters:
+
    +
  • data (Union[dict, str, Path, BaseGeometry, Parameter]) –

    the geometry to load. One of:

    +
      +
    • GeoJSON-style data structure: e.g. a dictionary with "type": "Polygon" and "coordinates" fields

    • +
    • a path to a local GeoJSON file

    • +
    • a GeoJSON string

    • +
    • a shapely geometry object

    • +
    +

  • +
  • properties (Optional[List[str]]) – A list of properties from the GeoJSON file to construct an additional dimension from.

  • +
+
+
Returns:
+

new VectorCube instance

+
+
+
+

Warning

+

EXPERIMENTAL: this process is experimental with the potential for major things to change.

+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “load_geojson”.

+
+
+ +
+
+load_ml_model(id)[source]
+

Loads a machine learning model from a STAC Item.

+
+
Parameters:
+

id (Union[str, BatchJob]) – STAC item reference, as URL, batch job (id) or user-uploaded file

+
+
Return type:
+

MlModel

+
+
Returns:
+

+
+
+
+

Added in version 0.10.0.

+
+
+ +
+
+load_result(id, spatial_extent=None, temporal_extent=None, bands=None)[source]
+

Loads batch job results by job id from the server-side user workspace. +The job must have been stored by the authenticated user on the back-end currently connected to.

+
+
Parameters:
+
    +
  • id (str) – The id of a batch job with results.

  • +
  • spatial_extent (Optional[Dict[str, float]]) – limit data to specified bounding box or polygons

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) – limit data to specified temporal interval. +Typically, just a two-item list or tuple containing start and end date. +See Filter on temporal extent for more details on temporal extent handling and shorthand notation.

  • +
  • bands (Optional[List[str]]) – only add the specified bands

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

a DataCube

+
+
+
+

Changed in version 0.23.0: Argument temporal_extent: add support for year/month shorthand notation +as discussed at Year/month shorthand notation.

+
+
+

See also

+

openeo.org documentation on process “load_result”.

+
+
+ +
+
+load_stac(url, spatial_extent=None, temporal_extent=None, bands=None, properties=None)[source]
+

Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable DataCube. +A batch job result can be loaded by providing a reference to it.

+

If supported by the underlying metadata and file format, the data that is added to the data cube can be +restricted with the parameters spatial_extent, temporal_extent and bands. +If no data is available for the given extents, a NoDataAvailable error is thrown.

+

Remarks:

+
    +
  • The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as +specified in the metadata if the bands parameter is set to null.

  • +
  • If no additional parameter is specified this would imply that the whole data set is expected to be loaded. +Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only +load the data that is actually required after evaluating subsequent processes such as filters. +This means that the values should be processed only after the data has been limited to the required extent +and as a consequence also to a manageable size.

  • +
+
+
Parameters:
+
    +
  • url (str) –

    The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) +or a specific STAC API Collection that allows to filter items and to download assets. +This includes batch job results, which itself are compliant to STAC. +For external URLs, authentication details such as API keys or tokens may need to be included in the URL.

    +

    Batch job results can be specified in two ways:

    +
      +
    • For Batch job results at the same back-end, a URL pointing to the corresponding batch job results +endpoint should be provided. The URL usually ends with /jobs/{id}/results and {id} +is the corresponding batch job ID.

    • +
    • For external results, a signed URL must be provided. Not all back-ends support signed URLs, +which are provided as a link with the link relation canonical in the batch job result metadata.

    • +
    +

  • +
  • spatial_extent (Union[Dict[str, float], Parameter, None]) –

    Limits the data to load to the specified bounding box or polygons.

    +

    For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects +with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).

    +

    For vector data, the process loads the geometry into the data cube if the geometry is fully within the +bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +Empty geometries may only be in the data cube if no spatial extent has been provided.

    +

    The GeoJSON can be one of the following feature types:

    +
      +
    • A Polygon or MultiPolygon geometry,

    • +
    • a Feature with a Polygon or MultiPolygon geometry, or

    • +
    • a FeatureCollection containing at least one Feature with Polygon or MultiPolygon geometries.

    • +
    +

    Set this parameter to None to set no limit for the spatial extent. +Be careful with this when loading large datasets. It is recommended to use this parameter instead of +using filter_bbox() or filter_spatial() directly after loading unbounded data.

    +

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) –

    Limits the data to load to the specified left-closed temporal interval. +Applies to all temporal dimensions. +The interval has to be specified as an array with exactly two elements:

    +
      +
    1. The first element is the start of the temporal interval. +The specified instance in time is included in the interval.

    2. +
    3. The second element is the end of the temporal interval. +The specified instance in time is excluded from the interval.

    4. +
    +

    The second element must always be greater/later than the first element. +Otherwise, a TemporalExtentEmpty exception is thrown.

    +

    Also supports open intervals by setting one of the boundaries to None, but never both.

    +

    Set this parameter to None to set no limit for the temporal extent. +Be careful with this when loading large datasets. It is recommended to use this parameter instead of +using filter_temporal() directly after loading unbounded data.

    +

  • +
  • bands (Optional[List[str]]) –

    Only adds the specified bands into the data cube so that bands that don’t match the list +of band names are not available. Applies to all dimensions of type bands.

    +

    Either the unique band name (metadata field name in bands) or one of the common band names +(metadata field common_name in bands) can be specified. +If the unique band name and the common name conflict, the unique band name has a higher priority.

    +

    The order of the specified array defines the order of the bands in the data cube. +If multiple bands match a common name, all matched bands are included in the original order.

    +

    It is recommended to use this parameter instead of using filter_bands() directly after loading unbounded data.

    +

  • +
  • properties (Optional[Dict[str, Union[str, PGNode, Callable]]]) –

    Limits the data by metadata properties to include only data in the data cube which +all given conditions return True for (AND operation).

    +

    Specify key-value-pairs with the key being the name of the metadata property, +which can be retrieved with the openEO Data Discovery for Collections. +The value must be a condition (user-defined process) to be evaluated against a STAC API. +This parameter is not supported for static STAC.

    +

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Added in version 0.17.0.

+
+
+

Changed in version 0.23.0: Argument temporal_extent: add support for year/month shorthand notation +as discussed at Year/month shorthand notation.

+
+
+

See also

+

openeo.org documentation on process “load_stac”.

+
+
+ +
+
+load_stac_from_job(job, spatial_extent=None, temporal_extent=None, bands=None, properties=None)[source]
+

Convenience function to directly load the results of a finished openEO job +(as a STAC collection) with load_stac() in a new openEO process graph.

+

When available, the “canonical” link (signed URL) of the job results will be used.

+
+
Parameters:
+
    +
  • job (Union[BatchJob, str]) – a BatchJob or job id pointing to a finished job. +Note that the BatchJob approach allows to point +to a batch job on a different back-end.

  • +
  • spatial_extent (Union[Dict[str, float], Parameter, None]) – limit data to specified bounding box or polygons

  • +
  • temporal_extent (Union[Sequence[Union[str, date, Parameter, PGNode, ProcessBuilderBase, None]], Parameter, str, None]) – limit data to specified temporal interval.

  • +
  • bands (Optional[List[str]]) – limit data to the specified bands

  • +
+
+
Return type:
+

DataCube

+
+
+
+

Added in version 0.30.0.

+
+
+ +
+
+load_url(url, format, options=None)[source]
+

Loads a file from a URL

+
+
Parameters:
+
    +
  • url (str) – The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL.

  • +
  • format (str) – The file format to use when loading the data.

  • +
  • options (Optional[dict]) – The file format parameters to use when reading the data. +Must correspond to the parameters that the server reports as supported parameters for the chosen format

  • +
+
+
Returns:
+

new VectorCube instance

+
+
+
+

Warning

+

EXPERIMENTAL: this process is experimental with the potential for major things to change.

+
+
+

Added in version 0.22.0.

+
+
+

See also

+

openeo.org documentation on process “load_url”.

+
+
+ +
+
+remove_service(service_id)[source]
+

Stop and remove a secondary web service.

+
+
Parameters:
+

service_id (str) – service identifier

+
+
Returns:
+

+
+
+
+

Deprecated since version 0.8.0: Use openeo.rest.service.Service.delete_service() +instead.

+
+
+ +
+
+request(method, path, headers=None, auth=None, check_error=True, expected_status=None, **kwargs)[source]
+

Generic request send

+
+ +
+
+save_user_defined_process(user_defined_process_id, process_graph, parameters=None, public=False, summary=None, description=None, returns=None, categories=None, examples=None, links=None)[source]
+

Store a process graph and its metadata on the backend as a user-defined process for the authenticated user.

+
+
Parameters:
+
    +
  • user_defined_process_id (str) – unique identifier for the user-defined process

  • +
  • process_graph (Union[dict, ProcessBuilderBase]) – a process graph

  • +
  • parameters (List[Union[dict, Parameter]]) – a list of parameters

  • +
  • public (bool) – visible to other users?

  • +
  • summary (Optional[str]) – A short summary of what the process does.

  • +
  • description (Optional[str]) – Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation.

  • +
  • returns (Optional[dict]) – Description and schema of the return value.

  • +
  • categories (Optional[List[str]]) – A list of categories.

  • +
  • examples (Optional[List[dict]]) – A list of examples.

  • +
  • links (Optional[List[dict]]) – A list of links.

  • +
+
+
Return type:
+

RESTUserDefinedProcess

+
+
Returns:
+

a RESTUserDefinedProcess instance

+
+
+
+ +
+
+service(service_id)[source]
+

Get the secondary web service based on the id. The service with the given id should already exist.

+

Use openeo.rest.connection.Connection.create_service() to create new services

+
+
Parameters:
+

job_id – the service id of an existing secondary web service

+
+
Return type:
+

Service

+
+
Returns:
+

A service object.

+
+
+
+ +
+
+upload_file(source, target=None)[source]
+

Uploads a file to the given target location in the user workspace on the back-end.

+

If a file at the target path exists in the user workspace it will be replaced.

+
+
Parameters:
+
    +
  • source (Union[Path, str]) – A path to a file on the local file system to upload.

  • +
  • target (Union[str, PurePosixPath, None]) – The desired path (which can contain a folder structure if desired) on the user workspace. +If not set: defaults to the original filename (without any folder structure) of the local file .

  • +
+
+
Return type:
+

UserFile

+
+
+
+ +
+
+user_defined_process(user_defined_process_id)[source]
+

Get the user-defined process based on its id. The process with the given id should already exist.

+
+
Parameters:
+

user_defined_process_id (str) – the id of the user-defined process

+
+
Return type:
+

RESTUserDefinedProcess

+
+
Returns:
+

a RESTUserDefinedProcess instance

+
+
+
+ +
+
+user_jobs()[source]
+
+
Return type:
+

List[dict]

+
+
+
+

Deprecated since version 0.4.10: use list_jobs() instead

+
+
+ +
+
+validate_process_graph(process_graph)[source]
+

Validate a process graph without executing it.

+
+
Parameters:
+

process_graph (Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]]) – openEO-style (flat) process graph representation, +or an object that can be converted to such a representation: +a dictionary, a DataCube object, +a string with a JSON representation, +a local file path or URL to a JSON representation, +a MultiResult object, …

+
+
Return type:
+

List[dict]

+
+
Returns:
+

list of errors (dictionaries with “code” and “message” fields)

+
+
+
+ +
+
+vectorcube_from_paths(paths, format, options={})[source]
+

Loads one or more files referenced by url or path that is accessible by the backend.

+
+
Parameters:
+
    +
  • paths (List[str]) – The files to read.

  • +
  • format (str) – The file format to read from. It must be one of the values that the server reports as supported input file formats.

  • +
  • options (dict) – The file format parameters to be used to read the files. Must correspond to the parameters that the server reports as supported parameters for the chosen format.

  • +
+
+
Return type:
+

VectorCube

+
+
Returns:
+

A VectorCube.

+
+
+
+

Added in version 0.14.0.

+
+
+ +
+
+classmethod version_discovery(url, session=None, timeout=None)[source]
+

Do automatic openEO API version discovery from given url, using a “well-known URI” strategy.

+
+
Parameters:
+

url (str) – initial backend url (not including “/.well-known/openeo”)

+
+
Return type:
+

str

+
+
Returns:
+

root url of highest supported backend version

+
+
+
+ +
+
+version_info()[source]
+

List version of the openEO client, API, back-end, etc.

+
+ +
+ +
+
+

openeo.rest.job

+
+
+class openeo.rest.job.BatchJob(job_id, connection)[source]
+

Handle for an openEO batch job, allowing it to describe, start, cancel, inspect results, etc.

+
+

Added in version 0.11.0: This class originally had the more cryptic name RESTJob, +which is still available as legacy alias, +but BatchJob is recommended since version 0.11.0.

+
+
+
+delete()[source]
+

Delete this batch job.

+
+

Added in version 0.20.0: This method was previously called delete_job().

+
+

This method uses openEO endpoint DELETE /jobs/{job_id}

+
+ +
+
+delete_job()
+

Delete this batch job.

+
+

Deprecated since version 0.20.0: Usage of this legacy method is deprecated. Use delete() instead.

+
+
+ +
+
+describe()[source]
+

Get detailed metadata about a submitted batch job +(title, process graph, status, progress, …). +:rtype: dict

+
+

Added in version 0.20.0: This method was previously called describe_job().

+
+

This method uses openEO endpoint GET /jobs/{job_id}

+
+ +
+
+describe_job()
+

Get detailed metadata about a submitted batch job +(title, process graph, status, progress, …). +:rtype: dict

+
+

Deprecated since version 0.20.0: Usage of this legacy method is deprecated. Use describe() instead.

+
+
+ +
+
+download_result(target=None)[source]
+

Download single job result to the target file path or into folder (current working dir by default).

+

Fails if there are multiple result files.

+
+
Parameters:
+

target (Union[str, Path]) – String or path where the file should be downloaded to.

+
+
Return type:
+

Path

+
+
+
+ +
+
+download_results(target=None)[source]
+

Download all job result files into given folder (current working dir by default).

+

The names of the files are taken directly from the backend.

+
+
Parameters:
+

target (Union[str, Path]) – String/path, folder where to put the result files.

+
+
Return type:
+

Dict[Path, dict]

+
+
Returns:
+

file_list: Dict containing the downloaded file path as value and asset metadata

+
+
+
+

Deprecated since version 0.4.10: Instead use BatchJob.get_results() and the more +flexible download functionality of JobResults

+
+
+ +
+
+estimate()[source]
+

Calculate time/cost estimate for a job.

+

This method uses openEO endpoint GET /jobs/{job_id}/estimate

+
+ +
+
+estimate_job()
+

Calculate time/cost estimate for a job.

+
+

Deprecated since version 0.20.0: Usage of this legacy method is deprecated. Use estimate() instead.

+
+
+ +
+
+get_result()[source]
+
+

Deprecated since version 0.4.10: Use BatchJob.get_results() instead.

+
+
+ +
+
+get_results()[source]
+

Get handle to batch job results for result metadata inspection or downloading resulting assets. +:rtype: JobResults

+
+

Added in version 0.4.10.

+
+
+ +
+
+get_results_metadata_url(*, full=False)[source]
+

Get results metadata URL

+
+
Return type:
+

str

+
+
+
+ +
+
+job_id
+

Unique identifier of the batch job (string).

+
+ +
+
+list_results()[source]
+

Get batch job results metadata. +:rtype: dict

+
+

Deprecated since version 0.4.10: Use get_results() instead.

+
+
+ +
+
+logs(offset=None, level=None)[source]
+

Retrieve job logs.

+
+
Parameters:
+
    +
  • offset (Optional[str]) –

    The last identifier (property id of a LogEntry) the client has received.

    +

    If provided, the back-ends only sends the entries that occurred after the specified identifier. +If not provided or empty, start with the first entry.

    +

    Defaults to None.

    +

  • +
  • level (Union[int, str, None]) –

    Minimum log level to retrieve.

    +

    You can use either constants from Python’s standard module logging +or their names (case-insensitive).

    +
    +
    For example:

    logging.INFO, "info" or "INFO" can all be used to show the messages +for level logging.INFO and above, i.e. also logging.WARNING and +logging.ERROR will be included.

    +
    +
    +

    Default is to show all log levels, in other words logging.DEBUG. +This is also the result when you explicitly pass log_level=None or log_level=””.

    +

  • +
+
+
Return type:
+

List[LogEntry]

+
+
Returns:
+

A list containing the log entries for the batch job.

+
+
+
+ +
+
+run_synchronous(outputfile=None, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30)[source]
+

Start the job, wait for it to finish and download result

+
+
Return type:
+

BatchJob

+
+
+
+ +
+
+start()[source]
+

Start this batch job.

+
+
Return type:
+

BatchJob

+
+
Returns:
+

Started batch job

+
+
+
+

Added in version 0.20.0: This method was previously called start_job().

+
+

This method uses openEO endpoint POST /jobs/{job_id}/results

+
+ +
+
+start_and_wait(print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, soft_error_max=10)[source]
+

Start the batch job, poll its status and wait till it finishes (or fails)

+
+
Parameters:
+
    +
  • print – print/logging function to show progress/status

  • +
  • max_poll_interval (int) – maximum number of seconds to sleep between status polls

  • +
  • connection_retry_interval (int) – how long to wait when status poll failed due to connection issue

  • +
  • soft_error_max – maximum number of soft errors (e.g. temporary connection glitches) to allow

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

+
+
+
+ +
+
+start_job()
+

Start this batch job. +:rtype: BatchJob

+
+

Deprecated since version 0.20.0: Usage of this legacy method is deprecated. Use start() instead.

+
+
+ +
+
+status()[source]
+

Get the status of the batch job

+
+
Return type:
+

str

+
+
Returns:
+

batch job status, one of “created”, “queued”, “running”, “canceled”, “finished” or “error”.

+
+
+
+ +
+
+stop()[source]
+

Stop this batch job.

+
+

Added in version 0.20.0: This method was previously called stop_job().

+
+

This method uses openEO endpoint DELETE /jobs/{job_id}/results

+
+ +
+
+stop_job()
+

Stop this batch job.

+
+

Deprecated since version 0.20.0: Usage of this legacy method is deprecated. Use stop() instead.

+
+
+ +
+ +
+
+class openeo.rest.job.JobResults(job)[source]
+

Results of a batch job: listing of one or more output files (assets) +and some metadata.

+
+

Added in version 0.4.10.

+
+
+
+download_file(target=None, name=None)[source]
+

Download single asset. Can be used when there is only one asset in the +JobResults, or when the desired asset name is given explicitly.

+
+
Parameters:
+
    +
  • target (Union[Path, str]) – path to download to. Can be an existing directory +(in which case the filename advertised by backend will be used) +or full file name. By default, the working directory will be used.

  • +
  • name (str) – asset name to download (not required when there is only one asset)

  • +
+
+
Return type:
+

Path

+
+
Returns:
+

path of downloaded asset

+
+
+
+ +
+
+download_files(target=None, include_stac_metadata=True)[source]
+

Download all assets to given folder.

+
+
Parameters:
+
    +
  • target (Union[Path, str]) – path to folder to download to (must be a folder if it already exists)

  • +
  • include_stac_metadata (bool) – whether to download the job result metadata as a STAC (JSON) file.

  • +
+
+
Return type:
+

List[Path]

+
+
Returns:
+

list of paths to the downloaded assets.

+
+
+
+ +
+
+get_asset(name=None)[source]
+

Get single asset by name or without name if there is only one.

+
+
Return type:
+

ResultAsset

+
+
+
+ +
+
+get_assets()[source]
+

Get all assets from the job results.

+
+
Return type:
+

List[ResultAsset]

+
+
+
+ +
+
+get_metadata(force=False)[source]
+

Get batch job results metadata (parsed JSON)

+
+
Return type:
+

dict

+
+
+
+ +
+ +
+
+class openeo.rest.job.RESTJob(job_id, connection)[source]
+

Legacy alias for BatchJob.

+
+

Deprecated since version 0.11.0: Use BatchJob instead

+
+
+ +
+
+class openeo.rest.job.ResultAsset(job, name, href, metadata)[source]
+

Result asset of a batch job (e.g. a GeoTIFF or JSON file)

+
+

Added in version 0.4.10.

+
+
+
+download(target=None, *, chunk_size=10000000)[source]
+

Download asset to given location

+
+
Parameters:
+
    +
  • target (Union[str, Path, None]) – download target path. Can be an existing folder +(in which case the filename advertised by backend will be used) +or full file name. By default, the working directory will be used.

  • +
  • chunk_size (int) – chunk size for streaming response.

  • +
+
+
Return type:
+

Path

+
+
+
+ +
+
+href
+

Download URL of the asset.

+
+ +
+
+load_bytes()[source]
+

Load asset in memory as raw bytes.

+
+
Return type:
+

bytes

+
+
+
+ +
+
+load_json()[source]
+

Load asset in memory and parse as JSON.

+
+
Return type:
+

dict

+
+
+
+ +
+
+metadata
+

Asset metadata provided by the backend, possibly containing keys “type” (for media type), “roles”, “title”, “description”.

+
+ +
+
+name
+

Asset name as advertised by the backend.

+
+ +
+ +
+
+

openeo.rest.conversions

+

Helpers for data conversions between Python ecosystem data types and openEO data structures.

+
+
+exception openeo.rest.conversions.InvalidTimeSeriesException[source]
+
+ +
+
+openeo.rest.conversions.datacube_from_file(filename, fmt='netcdf')[source]
+
+
Return type:
+

XarrayDataCube

+
+
+
+

Deprecated since version 0.7.0: Use XarrayDataCube.from_file() instead.

+
+
+ +
+
+openeo.rest.conversions.datacube_plot(datacube, *args, **kwargs)[source]
+
+

Deprecated since version 0.7.0: Use XarrayDataCube.plot() instead.

+
+
+ +
+
+openeo.rest.conversions.datacube_to_file(datacube, filename, fmt='netcdf')[source]
+
+

Deprecated since version 0.7.0: Use XarrayDataCube.save_to_file() instead.

+
+
+ +
+
+openeo.rest.conversions.timeseries_json_to_pandas(timeseries, index='date', auto_collapse=True)[source]
+

Convert a timeseries JSON object as returned by the aggregate_spatial process to a pandas DataFrame object

+

This timeseries data has three dimensions in general: date, polygon index and band index. +One of these will be used as index of the resulting dataframe (as specified by the index argument), +and the other two will be used as multilevel columns. +When there is just a single polygon or band in play, the dataframe will be simplified +by removing the corresponding dimension if auto_collapse is enabled (on by default).

+
+
Parameters:
+
    +
  • timeseries (dict) – dictionary as returned by aggregate_spatial

  • +
  • index (str) – which dimension should be used for the DataFrame index: ‘date’ or ‘polygon’

  • +
  • auto_collapse – whether single band or single polygon cases should be simplified automatically

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pandas DataFrame or Series

+
+
+
+ +
+
+

openeo.rest.udp

+
+
+class openeo.rest.udp.RESTUserDefinedProcess(user_defined_process_id, connection)[source]
+

Wrapper for a user-defined process stored (or to be stored) on an openEO back-end

+
+
+delete()[source]
+

Remove user-defined process from back-end

+
+
Return type:
+

None

+
+
+
+ +
+
+describe()[source]
+

Get metadata of this user-defined process.

+
+
Return type:
+

dict

+
+
+
+ +
+
+store(process_graph, parameters=None, public=False, summary=None, description=None, returns=None, categories=None, examples=None, links=None)[source]
+

Store a process graph and its metadata on the backend as a user-defined process

+
+ +
+
+update(process_graph, parameters=None, public=False, summary=None, description=None)[source]
+
+

Deprecated since version 0.4.11: Use store instead. Method update is misleading: OpenEO API +does not provide (partial) updates of user-defined processes, +only fully overwriting ‘store’ operations.

+
+
+ +
+ +
+
+openeo.rest.udp.build_process_dict(process_graph, process_id=None, summary=None, description=None, parameters=None, returns=None, categories=None, examples=None, links=None)[source]
+

Build a dictionary describing a process with metadaa (process_graph, parameters, description, …)

+
+
Parameters:
+
    +
  • process_graph (Union[dict, FlatGraphableMixin, Path, List[FlatGraphableMixin]]) – dict or builder representing a process graph

  • +
  • process_id (Optional[str]) – identifier of the process

  • +
  • summary (Optional[str]) – short summary of what the process does

  • +
  • description (Optional[str]) – detailed description

  • +
  • parameters (Optional[List[Union[dict, Parameter]]]) – list of process parameters (which have name, schema, default value, …)

  • +
  • returns (Optional[dict]) – description and schema of what the process returns

  • +
  • categories (Optional[List[str]]) – list of categories

  • +
  • examples (Optional[List[dict]]) – list of examples, may be used for unit tests

  • +
  • links (Optional[List[dict]]) – list of links related to the process

  • +
+
+
Return type:
+

dict

+
+
Returns:
+

dictionary in openEO “process graph with metadata” format

+
+
+
+ +
+
+

openeo.rest.userfile

+
+
+class openeo.rest.userfile.UserFile(path, *, connection, metadata=None)[source]
+

Handle to a (user-uploaded) file in the user workspace on a openEO back-end.

+
+
+delete()[source]
+

Delete the user-uploaded file from the user workspace on the back-end.

+
+ +
+
+download(target=None)[source]
+

Downloads a user-uploaded file from the user workspace on the back-end +locally to the given location.

+
+
Parameters:
+

target (Union[Path, str]) – local download target path. Can be an existing folder +(in which case the file name advertised by backend will be used) +or full file name. By default, the working directory will be used.

+
+
Return type:
+

Path

+
+
+
+ +
+
+classmethod from_metadata(metadata, connection)[source]
+

Build UserFile from a workspace file metadata dictionary.

+
+
Return type:
+

UserFile

+
+
+
+ +
+
+to_dict()[source]
+

Returns the provided metadata as dict.

+
+
Return type:
+

Dict[str, Any]

+
+
+
+ +
+
+upload(source)[source]
+

Uploads a local file to the path corresponding to this UserFile in the user workspace +and returns new UserFile of newly uploaded file.

+
+
+

Tip

+

Usually you’ll just need +Connection.upload_file() +instead of this UserFile method.

+
+
+

If the file exists in the user workspace it will be replaced.

+
+
Parameters:
+

source (Union[Path, str]) – A path to a file on the local file system to upload.

+
+
Return type:
+

UserFile

+
+
Returns:
+

new UserFile instance of the newly uploaded file

+
+
+
+ +
+ +
+
+

openeo.udf

+
+
+class openeo.udf.udf_data.UdfData(proj=None, datacube_list=None, feature_collection_list=None, structured_data_list=None, user_context=None)[source]
+

Container for data passed to a user defined function (UDF)

+
+
+property datacube_list: List[XarrayDataCube] | None
+

Get the data cube list

+
+ +
+
+property feature_collection_list: List[FeatureCollection] | None
+

get all feature collections as list

+
+ +
+
+classmethod from_dict(udf_dict)[source]
+

Create a udf data object from a python dictionary that was created from +the JSON definition of the UdfData class

+
+
Parameters:
+

udf_dict (dict) – The dictionary that contains the udf data definition

+
+
Return type:
+

UdfData

+
+
+
+ +
+
+get_datacube_list()[source]
+

Get the data cube list

+
+
Return type:
+

Optional[List[XarrayDataCube]]

+
+
+
+ +
+
+get_feature_collection_list()[source]
+

get all feature collections as list

+
+
Return type:
+

Optional[List[FeatureCollection]]

+
+
+
+ +
+
+get_structured_data_list()[source]
+

Get all structured data entries

+
+
Return type:
+

Optional[List[StructuredData]]

+
+
Returns:
+

A list of StructuredData objects

+
+
+
+ +
+
+set_datacube_list(datacube_list)[source]
+

Set the data cube list

+
+
Parameters:
+

datacube_list (Optional[List[XarrayDataCube]]) – A list of data cubes

+
+
+
+ +
+
+set_structured_data_list(structured_data_list)[source]
+

Set the list of structured data

+
+
Parameters:
+

structured_data_list (Optional[List[StructuredData]]) – A list of StructuredData objects

+
+
+
+ +
+
+property structured_data_list: List[StructuredData] | None
+

Get all structured data entries

+
+
Returns:
+

A list of StructuredData objects

+
+
+
+ +
+
+to_dict()[source]
+

Convert this UdfData object into a dictionary that can be converted into +a valid JSON representation

+
+
Return type:
+

dict

+
+
+
+ +
+
+property user_context: dict
+

Return the user context that was passed to the run_udf function

+
+ +
+ +
+
+class openeo.udf.xarraydatacube.XarrayDataCube(array)[source]
+

This is a thin wrapper around xarray.DataArray +providing a basic “DataCube” interface for openEO UDF usage around multi-dimensional data.

+
+
+property array: DataArray
+

Get the xarray.DataArray that contains the data and dimension definition

+
+ +
+
+classmethod from_dict(xdc_dict)[source]
+

Create a XarrayDataCube from a Python dictionary that was created from +the JSON definition of the data cube

+
+
Parameters:
+

data – The dictionary that contains the data cube definition

+
+
Return type:
+

XarrayDataCube

+
+
+
+ +
+
+classmethod from_file(path, fmt=None, **kwargs)[source]
+

Load data file as XarrayDataCube in memory

+
+
Parameters:
+
    +
  • path (Union[str, Path]) – the file on disk

  • +
  • fmt – format to load from, e.g. “netcdf” or “json” +(will be auto-detected when not specified)

  • +
+
+
Return type:
+

XarrayDataCube

+
+
Returns:
+

loaded data cube

+
+
+
+ +
+
+get_array()[source]
+

Get the xarray.DataArray that contains the data and dimension definition

+
+
Return type:
+

DataArray

+
+
+
+ +
+
+plot(title=None, limits=None, show_bandnames=True, show_dates=True, show_axeslabels=False, fontsize=10.0, oversample=1, cmap='RdYlBu_r', cbartext=None, to_file=None, to_show=True)[source]
+

Visualize a XarrayDataCube with matplotlib

+
+
Parameters:
+
    +
  • datacube – data to plot

  • +
  • title (str) – title text drawn in the top left corner (default: nothing)

  • +
  • limits – range of the contour plot as a tuple(min,max) (default: None, in which case the min/max is computed from the data)

  • +
  • show_bandnames (bool) – whether to plot the column names (default: True)

  • +
  • show_dates (bool) – whether to show the dates for each row (default: True)

  • +
  • show_axeslabels (bool) – whether to show the labels on the axes (default: False)

  • +
  • fontsize (float) – font size in pixels (default: 10)

  • +
  • oversample (float) – one value is plotted into oversample x oversample number of pixels (default: 1 which means each value is plotted as a single pixel)

  • +
  • cmap (Union[str, ‘matplotlib.colors.Colormap’]) – built-in matplotlib color map name or ColorMap object (default: RdYlBu_r which is a blue-yellow-red rainbow)

  • +
  • cbartext (str) – text on top of the legend (default: nothing)

  • +
  • to_file (str) – filename to save the image to (default: None, which means no file is generated)

  • +
  • to_show (bool) – whether to show the image in a matplotlib window (default: True)

  • +
+
+
Returns:
+

None

+
+
+
+ +
+
+save_to_file(path, fmt=None, **kwargs)[source]
+

Store XarrayDataCube to file

+
+
Parameters:
+
    +
  • path (Union[str, Path]) – destination file on disk

  • +
  • fmt – format to save as, e.g. “netcdf” or “json” +(will be auto-detected when not specified)

  • +
+
+
+
+ +
+
+to_dict()[source]
+

Convert this hypercube into a dictionary that can be converted into +a valid JSON representation

+
+
Return type:
+

dict

+
+
+
>>> example = {
+...     "id": "test_data",
+...     "data": [
+...         [[0.0, 0.1], [0.2, 0.3]],
+...         [[0.0, 0.1], [0.2, 0.3]],
+...     ],
+...     "dimension": [
+...         {"name": "time", "coordinates": ["2001-01-01", "2001-01-02"]},
+...         {"name": "X", "coordinates": [50.0, 60.0]},
+...         {"name": "Y"},
+...     ],
+... }
+
+
+
+ +
+ +
+
+class openeo.udf.structured_data.StructuredData(data, description=None, type=None)[source]
+

This class represents structured data that is produced by an UDF and can not be represented +as a raster or vector data cube. For example: the result of a statistical +computation.

+

Usage example:

+
>>> StructuredData([3, 5, 8, 13])
+>>> StructuredData({"mean": 5, "median": 8})
+>>> StructuredData([('col_1', 'col_2'), (1, 2), (2, 3)], type="table")
+
+
+
+ +

Note: this module was initially developed under the openeo-udf project (https://github.com/Open-EO/openeo-udf)

+
+
+openeo.udf.run_code.execute_local_udf(udf, datacube, fmt='netcdf')[source]
+

Locally executes an user defined function on a previously downloaded datacube.

+
+
Parameters:
+
    +
  • udf (Union[str, UDF]) – the code of the user defined function

  • +
  • datacube (Union[str, DataArray, XarrayDataCube]) – the path to the downloaded data in disk or a DataCube

  • +
  • fmt – format of the file if datacube is string

  • +
+
+
Returns:
+

the resulting DataCube

+
+
+
+ +
+
+openeo.udf.run_code.extract_udf_dependencies(udf)[source]
+

Extract dependencies from UDF code declared in a top-level comment block +following the inline script metadata specification (PEP 508).

+

Basic example UDF snippet declaring expected dependencies as embedded metadata +in a comment block:

+
# /// script
+# dependencies = [
+#     "geojson",
+# ]
+# ///
+
+import geojson
+
+def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+    ...
+
+
+
+

See also

+

Standard for declaring Python UDF dependencies for more in-depth information.

+
+
+
Parameters:
+

udf (Union[str, UDF]) – UDF code as a string or UDF object

+
+
Return type:
+

Optional[List[str]]

+
+
Returns:
+

List of extracted dependencies or None when no valid metadata block with dependencies was found.

+
+
+
+

Added in version 0.30.0.

+
+
+ +

Debug utilities for UDFs

+
+
+openeo.udf.debug.inspect(data=None, message='', code='User', level='info')[source]
+

Implementation of the openEO inspect process for UDF contexts.

+

Note that it is up to the back-end implementation to properly capture this logging +and include it in the batch job logs.

+
+
Parameters:
+
    +
  • data – data to log

  • +
  • message (str) – message to send in addition to the data

  • +
  • code (str) – A label to help identify one or more log entries

  • +
  • level (str) – The severity level of this message. Allowed values: “error”, “warning”, “info”, “debug”

  • +
+
+
+
+

Added in version 0.10.1.

+
+
+

See also

+

Logging from a UDF

+
+
+ +
+
+

openeo.util

+

Various utilities and helpers.

+
+
+class openeo.util.BBoxDict(*, west, south, east, north, crs=None)[source]
+

Dictionary based helper to easily create/work with bounding box dictionaries +(having keys “west”, “south”, “east”, “north”, and optionally “crs”).

+
+
Parameters:
+

crs (Union[int, str, None]) – value describing the coordinate reference system. +Typically just an int (interpreted as EPSG code, e.g. 4326) +or a string (handled as authority string, e.g. "EPSG:4326"). +See openeo.util.normalize_crs() for more details about additional normalization that is applied to this argument.

+
+
+
+

Added in version 0.10.1.

+
+
+
+classmethod from_dict(data)[source]
+

Build from dictionary with at least keys “west”, “south”, “east”, and “north”.

+
+
Return type:
+

BBoxDict

+
+
+
+ +
+
+classmethod from_sequence(seq, crs=None)[source]
+

Build from sequence of 4 bounds (west, south, east and north).

+
+
Return type:
+

BBoxDict

+
+
+
+ +
+ +
+
+openeo.util.load_json_resource(src)[source]
+

Helper to load some kind of JSON resource

+
+
Parameters:
+

src (Union[str, Path]) – a JSON resource: a raw JSON string, +a path to (local) JSON file, or a URL to a remote JSON resource

+
+
Return type:
+

dict

+
+
Returns:
+

data structured parsed from JSON

+
+
+
+ +
+
+openeo.util.normalize_crs(crs, *, use_pyproj=True)[source]
+

Normalize the given value (describing a CRS or Coordinate Reference System) +to an openEO compatible EPSG code (int) or WKT2 CRS string.

+

At minimum, the following input values are handled:

+
    +
  • an integer value (e.g. 4326) is interpreted as an EPSG code

  • +
  • a string that just contains an integer (e.g. "4326") +or with and additional "EPSG:" prefix (e.g. "EPSG:4326") +will also be interpreted as an EPSG value

  • +
+

Additional support and behavior depends on the availability of the pyproj library:

+
    +
  • When available, it will be used for parsing and validation: +everything supported by pyproj.CRS.from_user_input is allowed. +See the pyproj docs for more details.

  • +
  • Otherwise, some best effort validation is done: +EPSG looking integer or string values will be parsed as such as discussed above. +Other strings will be assumed to be WKT2 already. +Other data structures will not be accepted.

  • +
+
+
Parameters:
+
    +
  • crs (Any) – value that encodes a coordinate reference system, typically just an int (EPSG code) or string (authority string). +If the pyproj library is available, everything supported by it is allowed.

  • +
  • use_pyproj (bool) – whether pyproj should be leveraged at all +(mainly useful for testing the “no pyproj available” code path)

  • +
+
+
Return type:
+

Union[None, int, str]

+
+
Returns:
+

EPSG code as int, or WKT2 string. Or None if input was empty.

+
+
Raises:
+

ValueError – When the given CRS data can not be parsed/converted/normalized.

+
+
+
+ +
+
+openeo.util.to_bbox_dict(x, *, crs=None)[source]
+

Convert given data or object to a bounding box dictionary +(having keys “west”, “south”, “east”, “north”, and optionally “crs”).

+

Supports various input types/formats:

+
    +
  • list/tuple (assumed to be in west-south-east-north order)

    +
    >>> to_bbox_dict([3, 50, 4, 51])
    +{'west': 3, 'south': 50, 'east': 4, 'north': 51}
    +
    +
    +
  • +
  • dictionary (unnecessary items will be stripped)

    +
    >>> to_bbox_dict({
    +...     "color": "red", "shape": "triangle",
    +...     "west": 1, "south": 2, "east": 3, "north": 4, "crs": "EPSG:4326",
    +... })
    +{'west': 1, 'south': 2, 'east': 3, 'north': 4, 'crs': 'EPSG:4326'}
    +
    +
    +
  • +
  • a shapely geometry

  • +
+
+

Added in version 0.10.1.

+
+
+
Parameters:
+
    +
  • x (Any) – input data that describes west-south-east-north bounds in some way, e.g. as a dictionary, +a list, a tuple, ashapely geometry, …

  • +
  • crs (Union[int, str, None]) – (optional) CRS field

  • +
+
+
Return type:
+

BBoxDict

+
+
Returns:
+

dictionary (subclass) with keys “west”, “south”, “east”, “north”, and optionally “crs”.

+
+
+
+ +
+
+

openeo.processes

+
+
+openeo.processes.process(process_id, arguments=None, namespace=None, **kwargs)
+

Apply process, using given arguments

+
+
Parameters:
+
    +
  • process_id (str) – process id of the process.

  • +
  • arguments (dict) – argument dictionary for the process.

  • +
  • namespace (Optional[str]) – process namespace (only necessary to specify for non-predefined or non-user-defined processes)

  • +
+
+
Returns:
+

new ProcessBuilder instance

+
+
+
+ +
+
+

Graph building

+

Various utilities and helpers to simplify the construction of openEO process graphs.

+
+

Public openEO process graph building utilities

+
+
+
+class openeo.rest.graph_building.CollectionProperty(name, _builder=None)[source]
+

Helper object to easily create simple collection metadata property filters +to be used with Connection.load_collection().

+
+

Note

+

This class should not be used directly by end user code. +Use the collection_property() factory instead.

+
+
+

Warning

+

this is an experimental feature, naming might change.

+
+
+ +
+
+openeo.rest.graph_building.collection_property(name)[source]
+

Helper to easily create simple collection metadata property filters +to be used with Connection.load_collection().

+

Usage example:

+
from openeo import collection_property
+...
+
+connection.load_collection(
+    ...
+    properties=[
+        collection_property("eo:cloud_cover") <= 75,
+        collection_property("platform") == "Sentinel-2B",
+    ]
+)
+
+
+
+

Warning

+

this is an experimental feature, naming might change.

+
+
+

Added in version 0.26.0.

+
+
+
Parameters:
+

name (str) – name of the collection property to filter on

+
+
Return type:
+

CollectionProperty

+
+
Returns:
+

an object that supports operators like <=, == to easily build simple property filters.

+
+
+
+ +
+

Internal openEO process graph building utilities

+

Internal functionality for abstracting, building, manipulating and processing openEO process graphs.

+
+
+
+class openeo.internal.graph_building.FlatGraphableMixin[source]
+

Mixin for classes that can be exported/converted to +a “flat graph” representation of an openEO process graph.

+
+
+print_json(*, file=None, indent=2, separators=None, end='\\n')[source]
+

Print interoperable JSON representation of the process graph.

+

See DataCube.to_json() to get the JSON representation as a string +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • file – file-like object (stream) to print to (current sys.stdout by default). +Or a path (string or pathlib.Path) to a file to write to.

  • +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
  • end (str) – additional string to be printed at the end (newline by default).

  • +
+
+
+
+

Added in version 0.12.0.

+
+
+

Added in version 0.23.0: added the end argument.

+
+
+ +
+
+to_json(*, indent=2, separators=None)[source]
+

Get interoperable JSON representation of the process graph.

+

See DataCube.print_json() to directly print the JSON representation +and Export a process graph for more usage information.

+

Also see json.dumps docs for more information on the JSON formatting options.

+
+
Parameters:
+
    +
  • indent (Optional[int]) – JSON indentation level.

  • +
  • separators (Optional[Tuple[str, str]]) – (optional) tuple of item/key separators.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

JSON string

+
+
+
+ +
+ +
+
+class openeo.internal.graph_building.PGNode(process_id, arguments=None, namespace=None, **kwargs)[source]
+

A process node in a process graph: has at least a process_id and arguments.

+

Note that a full openEO “process graph” is essentially a directed acyclic graph of nodes +pointing to each other. A full process graph is practically equivalent with its “result” node, +as it points (directly or indirectly) to all the other nodes it depends on.

+
+

Warning

+

This class is an implementation detail meant for internal use. +It is not recommended for general use in normal user code. +Instead, use process graph abstraction builders like +Connection.load_collection(), +Connection.datacube_from_process(), +Connection.datacube_from_flat_graph(), +Connection.datacube_from_json(), +Connection.load_ml_model(), +openeo.processes.process(),

+
+
+
+flat_graph()[source]
+

Get the process graph in internal flat dict representation.

+
+
Return type:
+

Dict[str, dict]

+
+
+
+ +
+
+static from_flat_graph(flat_graph, parameters=None)[source]
+

Unflatten a given flat dict representation of a process graph and return result node.

+
+
Return type:
+

PGNode

+
+
+
+ +
+
+to_dict()[source]
+

Convert process graph to a nested dictionary structure. +Uses deep copy style: nodes that are reused in graph will be deduplicated

+
+
Return type:
+

dict

+
+
+
+ +
+
+static to_process_graph_argument(value)[source]
+

Normalize given argument properly to a “process_graph” argument +to be used as reducer/subprocess for processes like +reduce_dimension, aggregate_spatial, apply, merge_cubes, resample_cube_temporal

+
+
Return type:
+

dict

+
+
+
+ +
+
+update_arguments(**kwargs)[source]
+

Add/Update arguments of the process node.

+
+

Added in version 0.10.1.

+
+
+ +
+
+walk_nodes()[source]
+

Walk this node and all it’s parents

+
+
Return type:
+

Iterator[PGNode]

+
+
+
+ +
+ +
+
+

Testing

+

Various utilities for testing use cases (unit tests, integration tests, benchmarking, …)

+
+

openeo.testing

+

Utilities for testing of openEO client workflows.

+
+
+class openeo.testing.TestDataLoader(root)[source]
+

Helper to resolve paths to test data files, load them as JSON, optionally preprocess them, etc.

+

It’s intended to be used as a pytest fixture, e.g. from conftest.py:

+
@pytest.fixture
+def test_data() -> TestDataLoader:
+    return TestDataLoader(root=Path(__file__).parent / "data")
+
+
+
+

Added in version 0.30.0.

+
+
+
+get_path(filename)[source]
+

Get absolute path to a test data file

+
+
Return type:
+

Path

+
+
+
+ +
+
+load_json(filename, preprocess=None)[source]
+

Parse data from a test JSON file

+
+
Return type:
+

dict

+
+
+
+ +
+ +
+
+

openeo.testing.results

+

Assert functions for comparing actual (batch job) results against expected reference data.

+
+
+openeo.testing.results.assert_job_results_allclose(actual, expected, *, rtol=1e-06, atol=1e-06, tmp_path=None)[source]
+

Assert that two job results sets are equal (with tolerance).

+
+
Parameters:
+
    +
  • actual (Union[BatchJob, JobResults, str, Path]) – actual job results, provided as BatchJob object, +JobResults() object or path to directory with downloaded assets.

  • +
  • expected (Union[BatchJob, JobResults, str, Path]) – expected job results, provided as BatchJob object, +JobResults() object or path to directory with downloaded assets.

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
  • tmp_path (Optional[Path]) – root temp path to download results if needed. +It’s recommended to pass pytest’s tmp_path fixture here

  • +
+
+
Raises:
+

AssertionError – if not equal within the given tolerance

+
+
+
+

Added in version 0.31.0.

+
+
+

Warning

+

This function is experimental and subject to change.

+
+
+ +
+
+openeo.testing.results.assert_xarray_allclose(actual, expected, *, rtol=1e-06, atol=1e-06)[source]
+

Assert that two Xarray DataSet or DataArray instances are equal (with tolerance).

+
+
Parameters:
+
    +
  • actual (Union[Dataset, DataArray, str, Path]) – actual data, provided as Xarray object or path to NetCDF/GeoTIFF file.

  • +
  • expected (Union[Dataset, DataArray, str, Path]) – expected or reference data, provided as Xarray object or path to NetCDF/GeoTIFF file.

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
+
+
Raises:
+

AssertionError – if not equal within the given tolerance

+
+
+
+

Added in version 0.31.0.

+
+
+

Warning

+

This function is experimental and subject to change.

+
+
+ +
+
+openeo.testing.results.assert_xarray_dataarray_allclose(actual, expected, *, rtol=1e-06, atol=1e-06)[source]
+

Assert that two Xarray DataArray instances are equal (with tolerance).

+
+
Parameters:
+
    +
  • actual (Union[DataArray, str, Path]) – actual data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file.

  • +
  • expected (Union[DataArray, str, Path]) – expected or reference data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file.

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
+
+
Raises:
+

AssertionError – if not equal within the given tolerance

+
+
+
+

Added in version 0.31.0.

+
+
+

Warning

+

This function is experimental and subject to change.

+
+
+ +
+
+openeo.testing.results.assert_xarray_dataset_allclose(actual, expected, *, rtol=1e-06, atol=1e-06)[source]
+

Assert that two Xarray DataSet instances are equal (with tolerance).

+
+
Parameters:
+
    +
  • actual (Union[Dataset, str, Path]) – actual data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file

  • +
  • expected (Union[Dataset, str, Path]) – expected or reference data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file.

  • +
  • rtol (float) – relative tolerance

  • +
  • atol (float) – absolute tolerance

  • +
+
+
Raises:
+

AssertionError – if not equal within the given tolerance

+
+
+
+

Added in version 0.31.0.

+
+
+

Warning

+

This function is experimental and subject to change.

+
+
+ +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/auth.html b/auth.html new file mode 100644 index 000000000..774544757 --- /dev/null +++ b/auth.html @@ -0,0 +1,665 @@ + + + + + + + + Authentication and Account Management — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Authentication and Account Management

+

While a couple of openEO operations can be done +anonymously, most of the interesting parts +of the API require you to identify as a registered +user. +The openEO API specifies two ways to authenticate +as a user:

+
    +
  • OpenID Connect (recommended, but not always straightforward to use)

  • +
  • Basic HTTP Authentication (not recommended, but practically easier in some situations)

  • +
+

To illustrate how to authenticate with the openEO Python Client Library, +we start form a back-end connection:

+
import openeo
+
+connection = openeo.connect("https://openeo.example.com")
+
+
+
+

Basic HTTP Auth

+

Let’s start with the easiest authentication method, +based on the Basic HTTP authentication scheme. +It is however not recommended for various reasons, +such as its limited security measures. +For example, if you are connecting to a back-end with a http:// URL +instead of a https:// one, you should certainly not use basic HTTP auth.

+

With these security related caveats out of the way, you authenticate +using your username and password like this:

+
connection.authenticate_basic("john", "j0hn123")
+
+
+

Subsequent usage of the connection object connection will +use authenticated calls. +For example, show information about the authenticated user:

+
>>> connection.describe_account()
+{'user_id': 'john'}
+
+
+
+
+

OpenID Connect Based Authentication

+

OpenID Connect (often abbreviated “OIDC”) is an identity layer on top of the OAuth 2.0 protocol. +An in-depth discussion of the whole architecture would lead us too far here, +but some central OpenID Connect concepts are quite useful to understand +in the context of working with openEO:

+
    +
  • There is decoupling between:

    +
      +
    • the OpenID Connect identity provider +which handles the authentication/authorization and stores user information +(e.g. an organization Google, Github, Microsoft, your academic/research institution, …)

    • +
    • the openEO back-end which manages earth observation collections +and executes your algorithms

    • +
    +

    Instead of managing the authentication procedure itself, +an openEO back-end forwards a user to the relevant OpenID Connect provider to authenticate +and request access to basic profile information (e.g. email address). +On return, when the user allowed this access, +the openEO back-end receives the profile information and uses this to identify the user.

    +

    Note that with this approach, the back-end does not have to +take care of all the security and privacy challenges +of properly handling user registration, passwords/authentication, etc. +Also, it allows the user to securely reuse an existing account +registered with an established organisation, instead of having +to register yet another account with some web service.

    +
  • +
  • Your openEO script or application acts as +a so called OpenID Connect client, with an associated client id. +In most cases, a default client (id) defined by the openEO back-end will be used automatically. +For some applications a custom client might be necessary, +but this is out of scope of this documentation.

  • +
  • OpenID Connect authentication can be done with different kind of “flows” (also called “grants”) +and picking the right flow depends on your specific use case. +The most common OIDC flows using the openEO Python Client Library are:

    + +
  • +
+

OpenID Connect is clearly more complex than Basic HTTP Auth. +In the sections below we will discuss the practical details of each flow.

+
+

General options

+
    +
  • A back-end might support multiple OpenID Connect providers. +The openEO Python Client Library will pick the first one by default, +but another another provider can specified explicity with the provider_id argument, e.g.:

    +
    connection.authenticate_oidc_device(
    +    provider_id="gl",
    +    ...
    +)
    +
    +
    +
  • +
+
+
+
+

OIDC Authentication: Device Code Flow

+

The device code flow (also called device authorization grant) +is an interactive flow that requires a web browser for the authentication +with the OpenID Connect provider. +The nice things is that the browser doesn’t have to run on +the same system or network as where you run your application, +you could even use a browser on your mobile phone.

+

Use authenticate_oidc_device() to initiate the flow:

+
connection.authenticate_oidc_device()
+
+
+

This will print a message like this:

+
Visit https://oidc.example.net/device
+and enter user code 'DTNY-KLNX' to authenticate.
+
+
+

Some OpenID Connect Providers use a slightly longer URL that already includes +the user code, and then you don’t need to enter the user code in one of the next steps:

+
Visit https://oidc.example.net/device?user_code=DTNY-KLNX to authenticate.
+
+
+

You should now visit this URL in your browser of choice. +Usually, it is intentionally a short URL to make it feasible to type it +instead of copy-pasting it (e.g. on another device).

+

Authenticate with the OpenID Connect provider and, if requested, enter the user code +shown in the message. +When the URL already contains the user code, the page won’t ask for this code.

+

Meanwhile, the openEO Python Client Library is actively polling the OpenID Connect +provider and when you successfully complete the authentication, +it will receive the necessary tokens for authenticated communication +with the back-end and print:

+
Authorized successfully.
+
+
+

In case of authentication failure, the openEO Python Client Library +will stop polling at some point and raise an exception.

+
+
+

OIDC Authentication: Refresh Token Flow

+

When OpenID Connect authentication completes successfully, +the openID Python library receives an access token +to be used when doing authenticated calls to the back-end. +The access token usually has a short lifetime to reduce +the security risk when it would be stolen or intercepted. +The openID Python library also receives a refresh token +that can be used, through the Refresh Token flow, +to easily request a new access token, +without having to re-authenticate, +which makes it useful for non-interactive uses cases.

+

However, as it needs an existing refresh token, +the Refresh Token Flow requires +first to authenticate with one of the other flows +(but in practice this should not be done very often +because refresh tokens usually have a relatively long lifetime). +When doing the initial authentication, +you have to explicitly enable storage of the refresh token, +through the store_refresh_token argument, e.g.:

+
connection.authenticate_oidc_device(
+    ...
+    store_refresh_token=True
+
+
+

The refresh token will be stored in file in private file +in your home directory and will be used automatically +when authenticating with the Refresh Token Flow, +using authenticate_oidc_refresh_token():

+
connection.authenticate_oidc_refresh_token()
+
+
+

You can also bootstrap the refresh token file +as described in OpenID Connect refresh tokens

+
+
+

OIDC Authentication: Client Credentials Flow

+

The OIDC Client Credentials flow does not involve interactive authentication (e.g. through a web browser), +which makes it a useful option for non-interactive use cases.

+
+

Important

+

This method requires a custom OIDC client id and client secret. +It is out of scope of this general documentation to explain +how to obtain these as it depends on the openEO back-end you are using +and the OIDC provider that is in play.

+

Also, your openEO back-end might not allow it, because technically +you are authenticating a client instead of a user.

+

Consult the support of the openEO back-end you want to use for more information.

+
+

In its most simple form, given your client id and secret, +you can authenticate with +authenticate_oidc_client_credentials() +as follows:

+
connection.authenticate_oidc_client_credentials(
+    client_id=client_id,
+    client_secret=client_secret,
+)
+
+
+

You might also have to pass a custom provider id (argument provider_id) +if your OIDC client is associated with an OIDC provider that is different from the default provider.

+
+

Caution

+

Make sure to keep the client secret a secret and avoid putting it directly in your source code +or, worse, committing it to a version control system. +Instead, fetch the secret from a protected source (e.g. a protected file, a database for sensitive data, …) +or from environment variables.

+
+
+

OIDC Client Credentials Using Environment Variables

+

Since version 0.18.0, the openEO Python Client Library has built-in support to get the client id, +secret (and provider id) from environment variables +OPENEO_AUTH_CLIENT_ID, OPENEO_AUTH_CLIENT_SECRET and OPENEO_AUTH_PROVIDER_ID respectively. +Just call authenticate_oidc_client_credentials() +without arguments.

+

Usage example assuming a Linux (Bash) shell context:

+
$ export OPENEO_AUTH_CLIENT_ID="my-client-id"
+$ export OPENEO_AUTH_CLIENT_SECRET="Cl13n7S3cr3t!?123"
+$ export OPENEO_AUTH_PROVIDER_ID="oidcprovider"
+$ python
+>>> import openeo
+>>> connection = openeo.connect("openeo.example.com")
+>>> connection.authenticate_oidc_client_credentials()
+<Connection to 'https://openeo.example.com/openeo/1.1/' with OidcBearerAuth>
+
+
+
+
+
+

OIDC Authentication: Dynamic Method Selection

+

The sections above discuss various authentication options, like +the device code flow, +refresh tokens and +client credentials flow, +but often you want to dynamically switch between these depending on the situation: +e.g. use a refresh token if you have an active one, and fallback on the device code flow otherwise. +Or you want to be able to run the same code in an interactive environment and automated in an unattended manner, +without having to switch authentication methods explicitly in code.

+

That is what Connection.authenticate_oidc() is for:

+
connection.authenticate_oidc() # is all you need
+
+
+

In a basic situation (without any particular environment variables set as discussed further), +this method will first try to authenticate with refresh tokens (if any) +and fall back on the device code flow otherwise. +Ideally, when valid refresh tokens are available, this works without interaction, +but occasionally, when the refresh tokens expire, one has to do the interactive device code flow.

+

Since version 0.18.0, the openEO Python Client Library also allows to trigger the +client credentials flow +from authenticate_oidc() +by setting environment variable OPENEO_AUTH_METHOD +and the other client credentials environment variables. +For example:

+
$ export OPENEO_AUTH_METHOD="client_credentials"
+$ export OPENEO_AUTH_CLIENT_ID="my-client-id"
+$ export OPENEO_AUTH_CLIENT_SECRET="Cl13n7S3cr3t!?123"
+$ export OPENEO_AUTH_PROVIDER_ID="oidcprovider"
+$ python
+>>> import openeo
+>>> connection = openeo.connect("openeo.example.com")
+>>> connection.authenticate_oidc()
+<Connection to 'https://openeo.example.com/openeo/1.1/' with OidcBearerAuth>
+
+
+
+
+

Auth config files and openeo-auth helper tool

+

The openEO Python Client Library provides some features and tools +that ease the usability and security challenges +that come with authentication (especially in case of OpenID Connect).

+

Note that the code examples above contain quite some passwords and other secrets +that should be kept safe from prying eyes. +It is bad practice to define these kind of secrets directly +in your scripts and source code because that makes it quite hard +to responsibly share or reuse your code. +Even worse is storing these secrets in your version control system, +where it might be near impossible to remove them again. +A better solution is to keep secrets in separate configuration or cache files, +outside of your normal source code tree +(to avoid committing them accidentally).

+

The openEO Python Client Library supports config files to store: +user names, passwords, client IDs, client secrets, etc, +so you don’t have to specify them always in your scripts and applications.

+

The openEO Python Client Library (when installed properly) +provides a command line tool openeo-auth to bootstrap and manage +these configs and secrets. +It is a command line tool that provides various “subcommands” +and has built-in help:

+
$ openeo-auth -h
+usage: openeo-auth [-h] [--verbose]
+                   {paths,config-dump,token-dump,add-basic,add-oidc,oidc-auth}
+                   ...
+
+Tool to manage openEO related authentication and configuration.
+
+optional arguments:
+  -h, --help            show this help message and exit
+
+Subcommands:
+  {paths,config-dump,token-dump,add-basic,add-oidc,oidc-auth}
+    paths               Show paths to config/token files.
+    config-dump         Dump config file.
+...
+
+
+

For example, to see the expected paths of the config files:

+
$ openeo-auth paths
+openEO auth config: /home/john/.config/openeo-python-client/auth-config.json (perms: 0o600, size: 1414B)
+openEO OpenID Connect refresh token store: /home/john/.local/share/openeo-python-client/refresh-tokens.json (perms: 0o600, size: 846B)
+
+
+

With the config-dump and token-dump subcommands you can dump +the current configuration and stored refresh tokens, e.g.:

+
$ openeo-auth config-dump
+### /home/john/.config/openeo-python-client/auth-config.json ###############
+{
+  "backends": {
+    "https://openeo.example.com": {
+      "basic": {
+        "username": "john",
+        "password": "<redacted>",
+        "date": "2020-07-24T13:40:50Z"
+...
+
+
+

The sensitive information (like passwords) are redacted by default.

+
+

Basic HTTP Auth config

+

With the add-basic subcommand you can add Basic HTTP Auth credentials +for a given back-end to the config. +It will interactively ask for username and password and +try if these credentials work:

+
$ openeo-auth add-basic https://openeo.example.com/
+Enter username and press enter: john
+Enter password and press enter:
+Trying to authenticate with 'https://openeo.example.com'
+Successfully authenticated 'john'
+Saved credentials to '/home/john/.config/openeo-python-client/auth-config.json'
+
+
+

Now you can authenticate in your application without having to +specify username and password explicitly:

+
connection.authenticate_basic()
+
+
+
+
+

OpenID Connect configs

+

Likewise, with the add-oidc subcommand you can add OpenID Connect +credentials to the config:

+
$ openeo-auth add-oidc https://openeo.example.com/
+Using provider ID 'example' (issuer 'https://oidc.example.net/')
+Enter client_id and press enter: client-d7393fba
+Enter client_secret and press enter:
+Saved client information to '/home/john/.config/openeo-python-client/auth-config.json'
+
+
+

Now you can user OpenID Connect based authentication in your application +without having to specify the client ID and client secret explicitly, +like one of these calls:

+
connection.authenticate_oidc_authorization_code()
+connection.authenticate_oidc_client_credentials()
+connection.authenticate_oidc_resource_owner_password_credentials(username=username, password=password)
+connection.authenticate_oidc_device()
+connection.authenticate_oidc_refresh_token()
+
+
+

Note that you still have to add additional options as required, like +provider_id, server_address, store_refresh_token, etc.

+
+

OpenID Connect refresh tokens

+

There is also a oidc-auth subcommand to execute an OpenID Connect +authentication flow and store the resulting refresh token. +This is intended to for bootstrapping the environment or system +on which you want to run openEO scripts or applications that use +the Refresh Token Flow for authentication. +For example:

+
$ openeo-auth oidc-auth https://openeo.example.com
+Using config '/home/john/.config/openeo-python-client/auth-config.json'.
+Starting OpenID Connect device flow.
+To authenticate: visit https://oidc.example.net/device and enter the user code 'Q7ZNsy'.
+Authorized successfully.
+The OpenID Connect device flow was successful.
+Stored refresh token in '/home/john/.local/share/openeo-python-client/refresh-tokens.json'
+
+
+
+
+
+
+

Default openEO back-end URL and auto-authentication

+
+

Added in version 0.10.0.

+
+

If you often use the same openEO back-end URL and authentication scheme, +it can be handy to put these in a configuration file as discussed at Configuration files.

+
+

Note

+

Note that these general configuration files are different +from the auth config files discussed earlier under Auth config files and openeo-auth helper tool. +The latter are for storing authentication related secrets +and are mostly managed automatically (e.g. by the oidc-auth helper tool). +The former are not for storing secrets and are usually edited manually.

+
+

For example, to define a default back-end and automatically use OpenID Connect authentication +add these configuration options to the desired configuration file:

+
[Connection]
+default_backend = openeo.cloud
+default_backend.auto_authenticate = oidc
+
+
+

Getting an authenticated connection is now as simple as:

+
>>> import openeo
+>>> connection = openeo.connect()
+Loaded openEO client config from openeo-client-config.ini
+Using default back-end URL 'openeo.cloud' (from config)
+Doing auto-authentication 'oidc' (from config)
+Authenticated using refresh token.
+
+
+
+
+

Authentication for long-running applications and non-interactive contexts

+

With OpenID Connect authentication, the access token +(which is used in the authentication headers) +is typically short-lived (e.g. couple of minutes or hours). +This practically means that an authenticated connection could expire and become unusable +before a long-running script or application finishes its whole workflow. +Luckily, OpenID Connect also includes usage of refresh tokens, +which have a much longer expiry and allow request a new access token +to re-authenticate the connection. +Since version 0.10.1, the openEO Python Client Library will automatically +attempt to re-authenticate a connection when access token expiry is detected +and valid refresh tokens are available.

+

Likewise, refresh tokens can also be used for authentication in cases +where a script or application is run automatically in the background on regular basis (daily, weekly, …). +If there is a non-expired refresh token available, the script can authenticate +without user interaction.

+
+

Guidelines and tips

+

Some guidelines to get long-term and non-interactive authentication working for your use case:

+
    +
  • If you run a workflow periodically, but the interval between runs +is larger than the expiry time of the refresh token +(e.g. a monthly job, while the refresh token expires after, say, 10 days), +you could consider setting up a custom OIDC client with better suited +refresh token timeout. +The practical details of this heavily depend on the OIDC Identity Provider +in play and are out of scope of this discussion.

  • +
  • Obtaining a refresh token requires manual/interactive authentication, +but once it is stored on the necessary machine(s) +in the refresh token store as discussed in Auth config files and openeo-auth helper tool, +no further manual interaction should be necessary +during the lifetime of the refresh token. +To do so, use one of the following methods:

    +
      +
    • Use the openeo-auth oidc-auth cli tool, for example to authenticate +for openeo back-end openeo.example.com:

      +
      $ openeo-auth oidc-auth openeo.example.com
      +...
      +Stored refresh token in '/home/john/.local/share/openeo-python-client/refresh-tokens.json'
      +
      +
      +
    • +
    • Use a Python snippet to authenticate and store the refresh token:

      +
      import openeo
      +connection = openeo.connect("openeo.example.com")
      +connection.authenticate_oidc_device(store_refresh_token=True)
      +
      +
      +
    • +
    +

    To verify that (and where) the refresh token is stored, use openeo-auth token-dump:

    +
    $ openeo-auth token-dump
    +### /home/john/.local/share/openeo-python-client/refresh-tokens.json #######
    +{
    +  "https://oidc.example.net": {
    +    "default-client": {
    +      "date": "2022-05-11T13:13:20Z",
    +      "refresh_token": "<redacted>"
    +    },
    +...
    +
    +
    +
  • +
+
+
+
+

Best Practices and Troubleshooting Tips

+
+

Warning

+

Handle (OIDC) access and refresh tokens like secret, personal passwords. +Never share your access or refresh tokens with other people, +publicly, or for user support reasons.

+
+
+

Clear the refresh token file

+

When you have authentication or permission issues and you suspect +that your (locally cached) refresh tokens are the culprit: +remove your refresh token file in one of the following ways:

+
    +
  • Locate the file with the openeo-auth command line tool:

    +
    $ openeo-auth paths
    +...
    +openEO OpenID Connect refresh token store: /home/john/.local/share/openeo-python-client/refresh-tokens.json (perms: 0o600, size: 846B)
    +
    +
    +

    and remove it. +Or, if you know what you are doing: remove the desired section from this JSON file.

    +
  • +
  • Remove it directly with the token-clear subcommand of the openeo-auth command line tool:

    +
    $ openeo-auth token-clear
    +
    +
    +
  • +
  • Remove it with this Python snippet:

    +
    from openeo.rest.auth.config import RefreshTokenStore
    +RefreshTokenStore().remove()
    +
    +
    +
  • +
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/basics.html b/basics.html new file mode 100644 index 000000000..760816e2f --- /dev/null +++ b/basics.html @@ -0,0 +1,527 @@ + + + + + + + + Getting Started — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Getting Started

+
+

Connect to an openEO back-end

+

First, establish a connection to an openEO back-end, using its connection URL. +For example the VITO/Terrascope backend:

+
import openeo
+
+connection = openeo.connect("openeo.vito.be")
+
+
+

The resulting Connection object is your central gateway to

+
    +
  • list data collections, available processes, file formats and other capabilities of the back-end

  • +
  • start building your openEO algorithm from the desired data on the back-end

  • +
  • execute and monitor (batch) jobs on the back-end

  • +
  • etc.

  • +
+
+

See also

+

Use the openEO Hub to explore different back-end options +and their capabilities in a web-based way.

+
+
+
+

Collection discovery

+

The Earth observation data (the input of your openEO jobs) is organised in +so-called collections, +e.g. fundamental satellite collections like “Sentinel 1” or “Sentinel 2”, +or preprocessed collections like “NDVI”.

+

You can programmatically list the collections that are available on a back-end +and their metadata using methods on the connection object we just created +(like list_collection_ids() +or describe_collection()

+
>>> # Get all collection ids
+>>> connection.list_collection_ids()
+['SENTINEL1_GRD', 'SENTINEL2_L2A', ...
+
+>>> # Get metadata of a single collection
+>>> connection.describe_collection("SENTINEL2_L2A")
+{'id': 'SENTINEL2_L2A', 'title': 'Sentinel-2 top of canopy ...', 'stac_version': '0.9.0', ...
+
+
+

Congrats, you now just did your first real openEO queries to the openEO back-end +using the openEO Python client library.

+
+

Tip

+

The openEO Python client library comes with Jupyter (notebook) integration in a couple of places. +For example, put connection.describe_collection("SENTINEL2_L2A") (without print()) +as last statement in a notebook cell +and you’ll get a nice graphical rendering of the collection metadata.

+
+
+

See also

+

Find out more about data discovery, loading and filtering at Finding and loading data.

+
+
+
+

Authentication

+

In the code snippets above we did not need to log in as a user +since we just queried publicly available back-end information. +However, to run non-trivial processing queries one has to authenticate +so that permissions, resource usage, etc. can be managed properly.

+

To handle authentication, openEO leverages OpenID Connect (OIDC). +It offers some interesting features (e.g. a user can securely reuse an existing account), +but is a fairly complex topic, discussed in more depth at Authentication and Account Management.

+

The openEO Python client library tries to make authentication as streamlined as possible. +In most cases for example, the following snippet is enough to obtain an authenticated connection:

+
import openeo
+
+connection = openeo.connect("openeo.vito.be").authenticate_oidc()
+
+
+

This statement will automatically reuse a previously authenticated session, when available. +Otherwise, e.g. the first time you do this, some user interaction is required +and it will print a web link and a short user code, for example:

+
To authenticate: visit https://aai.egi.eu/auth/realms/egi/device and enter the user code 'SLUO-BMUD'.
+
+
+

Visit this web page in a browser, log in there with an existing account and enter the user code. +If everything goes well, the connection object in the script will be authenticated +and the back-end will be able to identify you in subsequent requests.

+
+
+

Example use case: EVI map and timeseries

+

A common task in earth observation is to apply a formula to a number of spectral bands +in order to compute an ‘index’, such as NDVI, NDWI, EVI, … +In this tutorial we’ll go through a couple of steps to extract +EVI (enhanced vegetation index) values and timeseries, +and discuss some openEO concepts along the way.

+
+
+

Loading an initial data cube

+

For calculating the EVI, we need the reflectance of the +red, blue and (near) infrared spectral components. +These spectral bands are part of the well-known Sentinel-2 data set +and is available on the current back-end under collection id SENTINEL2_L2A. +We load an initial small spatio-temporal slice (a data cube) as follows:

+
sentinel2_cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19},
+    temporal_extent = ["2021-02-01", "2021-04-30"],
+    bands=["B02", "B04", "B08"]
+)
+
+
+

Note how we specify a the region of interest, a time range and a set of bands to load.

+
+

Important

+

By filtering as early as possible (directly in load_collection() in this case), +we make sure the back-end only loads the data we are interested in +for better performance and keeping the processing costs low.

+
+
+

See also

+

See the chapter Finding and loading data for more details on data discovery, +general data loading (Loading a data cube from a collection) and filtering +(e.g. Filter on temporal extent).

+
+

The load_collection() method on the connection +object created a DataCube object (variable sentinel2_cube). +This DataCube class of the openEO Python Client Library +provides loads of methods corresponding to various openEO processes, +e.g. for masking, filtering, aggregation, spectral index calculation, data fusion, etc. +In the next steps we will illustrate a couple of these.

+
+

Important

+

It is important to highlight that we did not load any real EO data yet. +Instead we just created an abstract client-side reference, +encapsulating the collection id, the spatial extent, the temporal extent, etc. +The actual data loading will only happen at the back-end +once we explicitly trigger the execution of the data processing pipeline we are building.

+
+
+
+

Band math

+

From this data cube, we can now select the individual bands +with the DataCube.band() method +and rescale the digital number values to physical reflectances:

+
blue = sentinel2_cube.band("B02") * 0.0001
+red = sentinel2_cube.band("B04") * 0.0001
+nir = sentinel2_cube.band("B08") * 0.0001
+
+
+

We now want to compute the enhanced vegetation index +and can do that directly with these band variables:

+
evi_cube = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0)
+
+
+
+

Important

+

As noted before: while this looks like an actual calculation, +there is no real data processing going on here. +The evi_cube object at this point is just an abstract representation +of our algorithm under construction. +The mathematical operators we used here are syntactic sugar +for expressing this part of the algorithm in a very compact way.

+

As an illustration of this, let’s have peek at the JSON representation +of our algorithm so far, the so-called openEO process graph:

+
>>> print(evi_cube.to_json(indent=None))
+{"process_graph": {"loadcollection1": {"process_id": "load_collection", ...
+... "id": "SENTINEL2_L2A", "spatial_extent": {"west": 5.15, "south": ...
+... "multiply1": { ... "y": 0.0001}}, ...
+... "multiply3": { ... {"x": 2.5, "y": {"from_node": "subtract1"}}} ...
+...
+
+
+

Note how the load_collection arguments, rescaling and EVI calculation aspects +can be deciphered from this. +Rest assured, as user you normally you don’t have to worry too much +about these process graph details, +the openEO Python Client library handles this behind the scenes for you.

+
+
+
+

Download (synchronously)

+

Let’s download this as a GeoTIFF file. +Because GeoTIFF does not support a temporal dimension, +we first eliminate it by taking the temporal maximum value for each pixel:

+
evi_composite = evi_cube.max_time()
+
+
+
+

Note

+

This max_time() is not an official openEO process +but one of the many convenience methods in the openEO Python Client Library +to simplify common processing patterns. +It implements a reduce operation along the temporal dimension +with a max reducer/aggregator.

+
+

Now we can download this to a local file:

+
evi_composite.download("evi-composite.tiff")
+
+
+

This download command triggers the actual processing on the back-end: +it sends the process graph to the back-end and waits for the result. +It is a synchronous operation (the download() call +blocks until the result is fully downloaded) and because we work on a small spatio-temporal extent, +this should only take a couple of seconds.

+

If we inspect the downloaded image, we see that the maximum EVI value is heavily impacted +by cloud related artefacts, which makes the result barely usable. +In the next steps we will address cloud masking.

+_images/evi-composite.png +
+
+

Batch Jobs (asynchronous execution)

+

Synchronous downloads are handy for quick experimentation on small data cubes, +but if you start processing larger data cubes, you can easily +hit computation time limits or other constraints. +For these larger tasks, it is recommended to work with batch jobs, +which allow you to work asynchronously: +after you start your job, you can disconnect (stop your script or even close your computer) +and then minutes/hours later you can reconnect to check the batch job status and download results. +The openEO Python Client Library also provides helpers to keep track of a running batch job +and show a progress report.

+
+

See also

+

See Batch Jobs for more details.

+
+
+
+

Applying a cloud mask

+

As mentioned above, we need to filter out cloud pixels to make the result more usable. +It is very common for earth observation data to have separate masking layers that for instance indicate +whether a pixel is covered by a (type of) cloud or not. +For Sentinel-2, one such layer is the “scene classification” layer generated by the Sen2Cor algorithm. +In this example, we will use this layer to mask out unwanted data.

+

First, we load a new SENTINEL2_L2A based data cube with this specific SCL band as single band:

+
s2_scl = connection.load_collection(
+    "SENTINEL2_L2A",
+    spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19},
+    temporal_extent = ["2021-02-01", "2021-04-30"],
+    bands=["SCL"]
+)
+
+
+

Now we can use the compact “band math” feature again to build a +binary mask with a simple comparison operation:

+
# Select the "SCL" band from the data cube
+scl_band = s2_scl.band("SCL")
+# Build mask to mask out everything but class 4 (vegetation)
+mask = (scl_band != 4)
+
+
+

Before we can apply this mask to the EVI cube we have to resample it, +as the “SCL” layer has a “ground sample distance” of 20 meter, +while it is 10 meter for the “B02”, “B04” and “B08” bands. +We can easily do the resampling by referring directly to the EVI cube.

+
mask_resampled = mask.resample_cube_spatial(evi_cube)
+
+# Apply the mask to the `evi_cube`
+evi_cube_masked = evi_cube.mask(mask_resampled)
+
+
+

We can now download this as a GeoTIFF, again after taking the temporal maximum:

+
evi_cube_masked.max_time().download("evi-masked-composite.tiff")
+
+
+

Now, the EVI map is a lot more valuable, as the non-vegetation locations +and observations are filtered out:

+_images/evi-masked-composite.png +
+
+

Aggregated EVI timeseries

+

A common type of analysis is aggregating pixel values over one or more regions of interest +(also known as “zonal statistics) and tracking this aggregation over a period of time as a timeseries. +Let’s extract the EVI timeseries for these two regions:

+
features = {"type": "FeatureCollection", "features": [
+    {
+        "type": "Feature", "properties": {},
+        "geometry": {"type": "Polygon", "coordinates": [[
+            [5.1417, 51.1785], [5.1414, 51.1772], [5.1444, 51.1768], [5.1443, 51.179], [5.1417, 51.1785]
+        ]]}
+    },
+    {
+        "type": "Feature", "properties": {},
+        "geometry": {"type": "Polygon", "coordinates": [[
+            [5.156, 51.1892], [5.155, 51.1855], [5.163, 51.1855], [5.163, 51.1891], [5.156, 51.1892]
+        ]]}
+    }
+]}
+
+
+
+

Note

+

To have a self-containing example we define the geometries here as an inline GeoJSON-style dictionary. +In a real use case, your geometry will probably come from a local file or remote URL. +The openEO Python Client Library supports alternative ways of specifying the geometry +in methods like aggregate_spatial(), e.g. +as Shapely geometry objects.

+
+

Building on the experience from previous sections, we first build a masked EVI cube +(covering a longer time window than before):

+
# Load raw collection data
+sentinel2_cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19},
+    temporal_extent = ["2020-01-01", "2021-12-31"],
+    bands=["B02", "B04", "B08", "SCL"],
+)
+
+# Extract spectral bands and calculate EVI with the "band math" feature
+blue = sentinel2_cube.band("B02") * 0.0001
+red = sentinel2_cube.band("B04") * 0.0001
+nir = sentinel2_cube.band("B08") * 0.0001
+evi = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0)
+
+# Use the scene classification layer to mask out non-vegetation pixels
+scl = sentinel2_cube.band("SCL")
+evi_masked = evi.mask(scl != 4)
+
+
+

Now we use the aggregate_spatial() method +to do spatial aggregation over the geometries we defined earlier. +Note how we can specify the aggregation function "mean" as a simple string for the reducer argument.

+
evi_aggregation = evi_masked.aggregate_spatial(
+    geometries=features,
+    reducer="mean",
+)
+
+
+

If we download this, we get the timeseries encoded as a JSON structure, other useful formats are CSV and netCDF.

+
evi_aggregation.download("evi-aggregation.json")
+
+
+
+

Warning

+

Technically, the output of the openEO process aggregate_spatial +is a so-called “vector cube”. +At the time of this writing, the specification of this openEO concept +is not fully fleshed out yet in the openEO API. +openEO back-ends and clients to provide best-effort support for it, +but bear in mind that some details are subject to change.

+
+

The openEO Python Client Library provides helper functions +to convert the downloaded JSON data to a pandas dataframe, +which we massage a bit more:

+
import json
+import pandas as pd
+from openeo.rest.conversions import timeseries_json_to_pandas
+
+import json
+with open("evi-aggregation.json") as f:
+    data = json.load(f)
+
+df = timeseries_json_to_pandas(data)
+df.index = pd.to_datetime(df.index)
+df = df.dropna()
+df.columns = ("Field A", "Field B")
+
+
+

This gives us finally our EVI timeseries dataframe:

+
>>> df
+                           Field A   Field B
+date
+2020-01-06 00:00:00+00:00  0.522499  0.300250
+2020-01-16 00:00:00+00:00  0.529591  0.288079
+2020-01-18 00:00:00+00:00  0.633011  0.327598
+...                             ...       ...
+
+
+_images/evi-timeseries.png +
+
+

Computing multiple statistics

+
+

Warning

+

This is an experimental feature of the GeoPySpark openEO back-end, +it may not be supported by other back-ends, +and is subject to change. +See Open-EO/openeo-geopyspark-driver#726 for further discussion,

+
+

The same method also allows the computation of multiple statistics at once. This does rely +on ‘callbacks’ to construct a result with multiple statistics. +The use of such more complex processes is further explained in Processes with child “callbacks”.

+
from openeo.processes import array_create, mean, sd, median, count
+
+evi_aggregation = evi_masked.aggregate_spatial(
+    geometries=features,
+    reducer=lambda x: array_create([mean(x), sd(x), median(x), count(x)]),
+)
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/batch_jobs.html b/batch_jobs.html new file mode 100644 index 000000000..22330384f --- /dev/null +++ b/batch_jobs.html @@ -0,0 +1,446 @@ + + + + + + + + Batch Jobs — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Batch Jobs

+

Most of the simple, basic openEO usage examples show synchronous downloading of results: +you submit a process graph with a (HTTP POST) request and receive the result +as direct response of that same request. +This only works properly if the processing doesn’t take too long (order of seconds, or a couple of minutes at most).

+

For the heavier work (larger regions of interest, larger time series, more intensive processing, …) +you have to use batch jobs, which are supported in the openEO API through separate HTTP requests, corresponding to these steps:

+
    +
  • you create a job (providing a process graph and some other metadata like title, description, …)

  • +
  • you start the job

  • +
  • you wait for the job to finish, periodically polling its status

  • +
  • when the job finished successfully: get the listing of result assets

  • +
  • you download the result assets (or use them in an other way)

  • +
+
+

Tip

+

This documentation mainly discusses how to programmatically +create and interact with batch job using the openEO Python client library. +The openEO API however does not enforce usage of the same tool +for each step in the batch job life cycle.

+

For example: if you prefer a graphical, web-based interactive environment +to manage and monitor your batch jobs, +feel free to switch to an openEO web editor +like editor.openeo.org +or editor.openeo.cloud +at any time. +After logging in with the same account you use in your Python scripts, +you should see your batch jobs listed under the “Data Processing” tab:

+_images/batchjobs-webeditor-listing.png +

With the “action” buttons on the right, you can for example +inspect batch job details, start/stop/delete jobs, +download their results, get batch job logs, etc.

+
+
+

Create a batch job

+

In the openEO Python Client Library, if you have a (raster) data cube, you can easily +create a batch job with the DataCube.create_job() method. +It’s important to specify in what format the result should be stored, +which can be done with an explicit DataCube.save_result() call before creating the job:

+
cube = connection.load_collection(...)
+...
+# Store raster data as GeoTIFF files
+cube = cube.save_result(format="GTiff")
+job = cube.create_job()
+
+
+

or directly in job.create_job():

+
cube = connection.load_collection(...)
+...
+job = cube.create_job(out_format="GTiff)
+
+
+

While not necessary, it is also recommended to give your batch job a descriptive title +so it’s easier to identify in your job listing, e.g.:

+
job = cube.create_job(title="NDVI timeseries 2022")
+
+
+
+
+

Batch job object

+

The job object returned by create_job() +is a BatchJob object. +It is basically a client-side reference to a batch job that exists on the back-end +and allows to interact with that batch job +(see the BatchJob API docs for +available methods).

+
+

Note

+

The BatchJob class originally had +the more cryptic name RESTJob, +which is still available as legacy alias, +but BatchJob is (available and) recommended since version 0.11.0.

+
+

A batch job on a back-end is fully identified by its +job_id:

+
>>> job.job_id
+'d5b8b8f2-74ce-4c2e-b06d-bff6f9b14b8d'
+
+
+
+

Reconnecting to a batch job

+

Depending on your situation or use case: +make sure to properly take note of the batch job id. +It allows you to “reconnect” to your job on the back-end, +even if it was created at another time, +by another script/notebook or even with another openEO client.

+

Given a back-end connection and the batch job id, +use Connection.job() +to create a BatchJob object for an existing batch job:

+
job_id = "5d806224-fe79-4a54-be04-90757893795b"
+job = connection.job(job_id)
+
+
+
+
+

Jupyter integration

+

BatchJob objects have basic Jupyter notebook integration. +Put your BatchJob object as last statement +in a notebook cell and you get an overview of your batch jobs, +including job id, status, title and even process graph visualization:

+_images/batchjobs-jupyter-created.png +
+
+
+

List your batch jobs

+

You can list your batch jobs on the back-end with +Connection.list_jobs(), which returns a list of job metadata:

+
>>> connection.list_jobs()
+[{'title': 'NDVI timeseries 2022', 'status': 'created', 'id': 'd5b8b8f2-74ce-4c2e-b06d-bff6f9b14b8d', 'created': '2022-06-08T08:58:11Z'},
+ {'title': 'NDVI timeseries 2021', 'status': 'finished', 'id': '4e720e70-88bd-40bc-92db-a366985ebd67', 'created': '2022-06-04T14:46:06Z'},
+ ...
+
+
+

The listing returned by Connection.list_jobs() +has Jupyter notebook integration:

+_images/batchjobs-jupyter-listing.png +
+
+

Run a batch job

+

Starting a batch job is pretty straightforward with the +start() method:

+
job.start()
+
+
+

If this didn’t raise any errors or exceptions your job +should now have started (status “running”) +or be queued for processing (status “queued”).

+
+

Wait for a batch job to finish

+

A batch job typically takes some time to finish, +and you can check its status with the status() method:

+
>>> job.status()
+"running"
+
+
+

The possible batch job status values, defined by the openEO API, are +“created”, “queued”, “running”, “canceled”, “finished” and “error”.

+

Usually, you can only reliably get results from your job, +as discussed in Download batch job results, +when it reaches status “finished”.

+
+
+

Create, start and wait in one go

+

You could, depending on your situation, manually check your job’s status periodically +or set up a polling loop system to keep an eye on your job. +The openEO Python client library also provides helpers to do that for you.

+

Working from an existing BatchJob instance

+
+

If you have a batch job that is already created as shown above, you can use +the job.start_and_wait() method +to start it and periodically poll its status until it reaches status “finished” (or fails with status “error”). +Along the way it will print some progress messages.

+
>>> job.start_and_wait()
+0:00:00 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': send 'start'
+0:00:36 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': queued (progress N/A)
+0:01:35 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': queued (progress N/A)
+0:02:19 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': running (progress N/A)
+0:02:50 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': running (progress N/A)
+0:03:28 Job 'b0e8adcf-087f-41de-afe6-b3c0ea88ff38': finished (progress N/A)
+
+
+
+

Working from a DataCube instance

+
+

If you didn’t create the batch job yet from a given DataCube +you can do the job creation, starting and waiting in one go +with cube.execute_batch():

+
>>> job = cube.execute_batch()
+0:00:00 Job 'f9f4e3d3-bc13-441b-b76a-b7bfd3b59669': send 'start'
+0:00:23 Job 'f9f4e3d3-bc13-441b-b76a-b7bfd3b59669': queued (progress N/A)
+...
+
+
+

Note that cube.execute_batch() +returns a BatchJob instance pointing to +the newly created batch job.

+
+
+

Tip

+

You can fine-tune the details of the polling loop (the poll frequency, +how the progress is printed, …). +See job.start_and_wait() +or cube.execute_batch() +for more information.

+
+
+
+
+

Batch job logs

+

Batch jobs in openEO have logs to help with monitoring and debugging batch jobs. +The back-end typically uses this to dump information during data processing +that may be relevant for the user (e.g. warnings, resource stats, …). +Moreover, openEO processes like inspect allow users to log their own information.

+

Batch job logs can be fetched with job.logs()

+
>>> job.logs()
+[{'id': 'log001', 'level': 'info', 'message': 'Job started with 4 workers'},
+ {'id': 'log002', 'level': 'debug', 'message': 'Loading 5x3x6 tiles'},
+ {'id': 'log003', 'level': 'error', 'message': "Failed to load data cube: corrupt data for tile 'J9A7K2'."},
+...
+
+
+

In a Jupyter notebook environment, this also comes with Jupyter integration:

+_images/batchjobs-jupyter-logs.png +
+

Automatic batch job log printing

+

When using +job.start_and_wait() +or cube.execute_batch() +to run a batch job and it fails, +the openEO Python client library will automatically +print the batch job logs and instructions to help with further investigation:

+
>>> job.start_and_wait()
+0:00:00 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': send 'start'
+0:00:01 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': running (progress N/A)
+0:00:07 Job '68caccff-54ee-470f-abaa-559ed2d4e53c': error (progress N/A)
+
+Your batch job '68caccff-54ee-470f-abaa-559ed2d4e53c' failed.
+Logs can be inspected in an openEO (web) editor
+or with `connection.job('68caccff-54ee-470f-abaa-559ed2d4e53c').logs()`.
+
+Printing logs:
+[{'id': 'log001', 'level': 'info', 'message': 'Job started with 4 workers'},
+{'id': 'log002', 'level': 'debug', 'message': 'Loading 5x3x6 tiles'},
+{'id': 'log003', 'level': 'error', 'message': "Failed to load data cube: corrupt data for tile 'J9A7K2'."}]
+
+
+
+
+
+

Download batch job results

+

Once a batch job is finished you can get a handle to the results +(which can be a single file or multiple files) and metadata +with get_results():

+
>>> results = job.get_results()
+>>> results
+<JobResults for job '57da31da-7fd4-463a-9d7d-c9c51646b6a4'>
+
+
+

The result metadata describes the spatio-temporal properties of the result +and is in fact a valid STAC item:

+
>>> results.get_metadata()
+{
+    'bbox': [3.5, 51.0, 3.6, 51.1],
+    'geometry': {'coordinates': [[[3.5, 51.0], [3.5, 51.1], [3.6, 51.1], [3.6, 51.0], [3.5, 51.0]]], 'type': 'Polygon'},
+    'assets': {
+        'res001.tiff': {
+            'href': 'https://openeo.example/download/432f3b3ef3a.tiff',
+            'type': 'image/tiff; application=geotiff',
+            ...
+        'res002.tiff': {
+            ...
+
+
+
+

Download all assets

+

In the general case, when you have one or more result files (also called “assets”), +the easiest option to download them is +using download_files() (plural) +where you just specify a download folder +(otherwise the current working directory will be used by default):

+
results.download_files("data/out")
+
+
+

The resulting files will be named as they are advertised in the results metadata +(e.g. res001.tiff and res002.tiff in case of the metadata example above).

+
+
+

Download single asset

+

If you know that there is just a single result file, you can also download it directly with +download_file() (singular) with the desired file name:

+
results.download_file("data/out/result.tiff")
+
+
+

This will fail however if there are multiple assets in the job result +(like in the metadata example above). +In that case you can still download a single by specifying which one you +want to download with the name argument:

+
results.download_file("data/out/result.tiff", name="res002.tiff")
+
+
+
+
+

Fine-grained asset downloads

+

If you need a bit more control over which asset to download and how, +you can iterate over the result assets explicitly +and download these ResultAsset instances +with download(), like this:

+
for asset in results.get_assets():
+    if asset.metadata["type"].startswith("image/tiff"):
+        asset.download("data/out/result-v2-" + asset.name)
+
+
+
+
+
+

Directly load batch job results

+

If you want to skip downloading an asset to disk, you can also load it directly. +For example, load a JSON asset with load_json():

+
>>> asset.metadata
+{"type": "application/json", "href": "https://openeo.example/download/432f3b3ef3a.json"}
+>>> data = asset.load_json()
+>>> data
+{"2021-02-24T10:59:23Z": [[3, 2, 5], [3, 4, 5]], ....}
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/best_practices.html b/best_practices.html new file mode 100644 index 000000000..b6a3b9a8e --- /dev/null +++ b/best_practices.html @@ -0,0 +1,213 @@ + + + + + + + + Best practices, coding style and general tips — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Best practices, coding style and general tips

+

This is a collection of guidelines regarding best practices, +coding style and usage patterns for the openEO Python Client Library.

+

It is in the first place an internal recommendation for openEO developers +to give documentation, code examples, demo’s and tutorials +a consistent look and feel, +following common software engineering best practices. +Secondly, the wider audience of openEO users is also invited to pick up +a couple of tips and principles to improve their own code and scripts.

+
+

Background and inspiration

+

While some people consider coding style a personal choice or even irrelevant, +there are various reasons to settle on certain conventions. +Just the fact alone of following conventions +lowers the bar to get faster to the important details in someone else’s code. +Apart from taste, there are also technical reasons to pick certain rules +to streamline the programming workflow, +not only for humans, +but also supporting tools (e.g. minimize risk on merge conflicts).

+

While the Python language already has a strong focus on readability by design, +the Python community is strongly gravitating to even more strict conventions:

+
    +
  • pep8: the mother of all Python code style guides

  • +
  • black: an opinionated code formatting tool +that gets more and more traction in popular, high profile projects.

  • +
+

This openEO oriented style guide will highlight +and build on these recommendations.

+
+
+

General code style recommendations

+
    +
  • Indentation with 4 spaces.

  • +
  • Avoid star imports (from module import *). +While this seems like a quick way to import a bunch of functions/classes, +it makes it very hard for the reader to figure out where things come from. +It can also lead to strange bugs and behavior because it silently overwrites +references you previously imported.

  • +
+
+
+

Line (length) management

+

While desktop monitors offer plenty of (horizontal) space nowadays, +it is still a common recommendation to avoid long source code lines. +Not only are long lines hard to read and understand, +one should also consider that source code might still be viewed +on a small screen or tight viewport, +where scrolling horizontally is annoying or even impossible. +Unnecessarily long lines are also notorious +for not playing well with version control tools and workflows.

+

Here are some guidelines on how to split long statements over multiple lines.

+

Split long function/method calls directly after the opening parenthesis +and list arguments with a standard 4 space indentation +(not after the first argument with some ad-hoc indentation). +Put the closing parenthesis on its own line.

+
# Avoid this:
+s2_fapar = connection.load_collection("TERRASCOPE_S2_FAPAR_V2",
+                                      spatial_extent={'west': 16.138916, 'east': 16.524124, 'south': 48.1386, 'north': 48.320647},
+                                      temporal_extent=["2020-05-01", "2020-05-20"])
+
+# This is better:
+s2_fapar = connection.load_collection(
+    "TERRASCOPE_S2_FAPAR_V2",
+    spatial_extent={"west": 16.138916, "east": 16.524124, "south": 48.1386, "north": 48.320647},
+    temporal_extent=["2020-05-01", "2020-05-20"],
+)
+
+
+
+
+

Jupyter(lab) tips and tricks

+
    +
  • Add a cell with openeo.client_version() (e.g. just after importing all your libraries) +to keep track of which version of the openeo Python client library you used in your notebook.

  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/changelog.html b/changelog.html new file mode 100644 index 000000000..527f9473f --- /dev/null +++ b/changelog.html @@ -0,0 +1,1336 @@ + + + + + + + + Changelog — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Changelog

+

All notable changes to this project will be documented in this file.

+

The format is based on Keep a Changelog, +and this project adheres to Semantic Versioning.

+
+

[Unreleased]

+
+

Added

+
    +
  • Added MultiResult helper class to build process graphs with multiple result nodes (#391)

  • +
+
+
+

Changed

+
+
+

Removed

+
+
+

Fixed

+
    +
  • MultiBackendJobManager: Fix issue with duplicate job starting across multiple backends (#654)

  • +
  • MultiBackendJobManager: Fix encoding issue of job metadata in on_job_done (#657)

  • +
  • MultiBackendJobManager: Avoid SettingWithCopyWarning (#641)

  • +
+
+
+
+

[0.34.0] - 2024-10-31

+
+

Removed

+
    +
  • Drop support for Python 3.7 (#578)

  • +
+
+
+

Fixed

+
    +
  • Fixed broken support for title and description job properties in execute_batch() (#652)

  • +
+
+
+
+

[0.33.0] - 2024-10-18

+
+

Added

+
    +
  • Added DataCube.load_stac() to also support creating a load_stac based cube without a connection (#638)

  • +
  • MultiBackendJobManager: Added initialize_from_df(df) (to CsvJobDatabase and ParquetJobDatabase) to initialize (and persist) the job database from a given DataFrame. +Also added create_job_db() factory to easily create a job database from a given dataframe and its type guessed from filename extension. +(#635)

  • +
  • MultiBackendJobManager.run_jobs() now returns a dictionary with counters/stats about various events during the full run of the job manager (#645)

  • +
  • Added (experimental) ProcessBasedJobCreator to be used as start_job callable with MultiBackendJobManager to create multiple jobs from a single parameterized process (e.g. a UDP or remote process definition) (#604)

  • +
+
+
+

Fixed

+
    +
  • When using DataCube.load_collection() without a connection, it is not necessary anymore to also explicitly set fetch_metadata=False (#638)

  • +
+
+
+
+

[0.32.0] - 2024-09-27

+
+

Added

+
    +
  • load_stac/metadata_from_stac: add support for extracting actual temporal dimension metadata (#567)

  • +
  • MultiBackendJobManager: add cancel_running_job_after option to automatically cancel jobs that are running for too long (#590)

  • +
  • Added openeo.api.process.Parameter helper to easily create a “spatial_extent” UDP parameter

  • +
  • Wrap OIDC token request failure in more descriptive OidcException (related to #624)

  • +
  • Added auto_add_save_result option (on by default) to disable automatic addition of save_result node on download/create_job/execute_batch (#513)

  • +
  • Add support for apply_vectorcube UDF signature in run_udf_code (Open-EO/openeo-geopyspark-driver#881)

  • +
  • MultiBackendJobManager: add API to the update loop in a separate thread, allowing controlled interruption.

  • +
+
+
+

Changed

+
    +
  • MultiBackendJobManager: changed job metadata storage API, to enable working with large databases

  • +
  • DataCube.apply_polygon(): rename polygons argument to geometries, but keep support for legacy polygons for now (#592, #511)

  • +
  • Disallow ambiguous single string argument in DataCube.filter_temporal() (#628)

  • +
  • Automatic adding of save_result from download() or create_job(): +inspect whole process graph for pre-existing save_result nodes +(related to #623, #401, #583)

  • +
  • Disallow ambiguity of combining explicit save_result nodes +and implicit save_result addition from download()/create_job() calls with format +(related to #623, #401, #583)

  • +
+
+
+

Fixed

+
    +
  • apply_dimension with a target_dimension argument was not correctly adjusting datacube metadata on the client side, causing a mismatch.

  • +
  • Preserve non-spatial dimension metadata in aggregate_spatial (#612)

  • +
+
+
+
+

[0.31.0] - 2024-07-26

+
+

Added

+
    +
  • Add experimental openeo.testing.results subpackage with reusable test utilities for comparing batch job results with reference data

  • +
  • MultiBackendJobManager: add initial support for storing job metadata in Parquet file (instead of CSV) (#571)

  • +
  • Add Connection.authenticate_oidc_access_token() to set up authorization headers with an access token that is obtained “out-of-band” (#598)

  • +
  • Add JobDatabaseInterface to allow custom job metadata storage with MultiBackendJobManager (#571)

  • +
+
+
+
+

[0.30.0] - 2024-06-18

+
+

Added

+
    +
  • Add openeo.udf.run_code.extract_udf_dependencies() to extract UDF dependency declarations from UDF code +(related to Open-EO/openeo-geopyspark-driver#237)

  • +
  • Document PEP 723 based Python UDF dependency declarations (Open-EO/openeo-geopyspark-driver#237)

  • +
  • Added more openeo.api.process.Parameter helpers to easily create “bounding_box”, “date”, “datetime”, “geojson” and “temporal_interval” parameters for UDP construction.

  • +
  • Added convenience method Connection.load_stac_from_job(job) to easily load the results of a batch job with the load_stac process (#566)

  • +
  • load_stac/metadata_from_stac: add support for extracting band info from “item_assets” in collection metadata (#573)

  • +
  • Added initial openeo.testing submodule for reusable test utilities

  • +
+
+
+

Fixed

+
    +
  • Initial fix for broken DataCube.reduce_temporal() after load_stac (#568)

  • +
+
+
+
+

[0.29.0] - 2024-05-03

+
+

Added

+
    +
  • Start depending on pystac, initially for better load_stac support (#133, #527)

  • +
+
+
+

Changed

+
    +
  • OIDC device code flow: hide progress bar on completed (or timed out) authentication

  • +
+
+
+
+

[0.28.0] - 2024-03-18

+
+

Added

+
    +
  • Introduced superclass CubeMetadata for CollectionMetadata for essential metadata handling (just dimensions for now) without collection-specific STAC metadata parsing. (#464)

  • +
  • Added VectorCube.vector_to_raster() (#550)

  • +
+
+
+

Changed

+
    +
  • Changed default chunk_size of various download functions from None to 10MB. This improves the handling of large downloads and reduces memory usage. (#528)

  • +
  • 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)

  • +
+
+
+

Fixed

+
    +
  • Preserve geo-referenced x and y coordinates in execute_local_udf (#549)

  • +
+
+
+
+

[0.27.0] - 2024-01-12

+
+

Added

+
    +
  • Add DataCube.filter_labels()

  • +
+
+
+

Changed

+
    +
  • Update autogenerated functions/methods in openeo.processes to definitions from openeo-processes project version 2.0.0-rc1. +This removes create_raster_cube, fit_class_random_forest, fit_regr_random_forest and save_ml_model. +Although removed from openeo-processes 2.0.0-rc1, support for load_result, predict_random_forest and load_ml_model +is preserved but deprecated. (#424)

  • +
  • Show more informative error message on 403 Forbidden errors from CDSE firewall (#512)

  • +
  • Handle API error responses more strict and avoid hiding possibly important information in JSON-formatted but non-compliant error responses.

  • +
+
+
+

Fixed

+
    +
  • Fix band name support in DataCube.band() when no metadata is available (#515)

  • +
  • Support optional child callbacks in generated openeo.processes, e.g. merge_cubes (#522)

  • +
  • Fix broken pre-flight validation in Connection.save_user_defined_process (#526)

  • +
+
+
+
+

[0.26.0] - 2023-11-27 - “SRR6” release

+
+

Added

+
    +
  • Support new UDF signature: def apply_datacube(cube: DataArray, context: dict) -> DataArray +(#310)

  • +
  • Add collection_property() helper to easily build collection metadata property filters for Connection.load_collection() +(#331)

  • +
  • Add DataCube.apply_polygon() (standardized version of experimental chunk_polygon) (#424)

  • +
  • Various improvements to band mapping with the Awesome Spectral Indices feature. +Allow explicitly specifying the satellite platform for band name mapping (e.g. “Sentinel2” or “LANDSAT8”) if cube metadata lacks info. +Follow the official band mapping from Awesome Spectral Indices better. +Allow manually specifying the desired band mapping. +(#485, #501)

  • +
  • Also attempt to automatically refresh OIDC access token on a 401 TokenInvalid response (in addition to 403 TokenInvalid) (#508)

  • +
  • Add Parameter.object() factory for object type parameters

  • +
+
+
+

Removed

+
    +
  • Remove custom spectral indices “NDGI”, “NDMI” and “S2WI” from “extra-indices-dict.json” +that were shadowing the official definitions from Awesome Spectral Indices (#501)

  • +
+
+
+

Fixed

+
    +
  • Initial support for “spectral indices” that use constants defined by Awesome Spectral Indices (#501)

  • +
+
+
+
+

[0.25.0] - 2023-11-02

+
+

Changed

+
    +
  • Introduce OpenEoApiPlainError for API error responses that are not well-formed +for better distinction with properly formed API error responses (OpenEoApiError). +(#491).

  • +
+
+
+

Fixed

+
    +
  • Fix missing validate support in LocalConnection.execute (#493)

  • +
+
+
+
+

[0.24.0] - 2023-10-27

+
+

Added

+
    +
  • Add DataCube.reduce_spatial()

  • +
  • Added option (enabled by default) to automatically validate a process graph before execution. +Validation issues just trigger warnings for now. (#404)

  • +
  • Added “Sentinel1” band mapping support to “Awesome Spectral Indices” wrapper (#484)

  • +
  • Run tests in GitHub Actions against Python 3.12 as well

  • +
+
+
+

Changed

+
    +
  • Enforce XarrayDataCube dimension order in execute_local_udf() to (t, bands, y, x) +to improve UDF interoperability with existing back-end implementations.

  • +
+
+
+
+

[0.23.0] - 2023-10-02

+
+

Added

+
    +
  • Support year/month shorthand date notations in temporal extent arguments of Connection.load_collection, DataCube.filter_temporal and related (#421)

  • +
  • Support parameterized bands in load_collection (#471)

  • +
  • Allow specifying item schema in Parameter.array()

  • +
  • Support “subtype” and “format” schema options in Parameter.string()

  • +
+
+
+

Changed

+
    +
  • Before doing user-defined process (UDP) listing/creation: verify that back-end supports that (through openEO capabilities document) to improve error message.

  • +
  • Skip metadata-based normalization/validation and stop showing unhelpful warnings/errors +like “No cube:dimensions metadata” or “Invalid dimension” +when no metadata is available client-side anyway (e.g. when using datacube_from_process, parameterized cube building, …). +(#442)

  • +
+
+
+

Removed

+
    +
  • Bumped minimal supported Python version to 3.7 (#460)

  • +
+
+
+

Fixed

+
    +
  • Support handling of “callback” parameters in openeo.processes callables (#470)

  • +
+
+
+
+

[0.22.0] - 2023-08-09

+
+

Added

+
    +
  • Processes that take a CRS as argument now try harder to normalize your input to +a CRS representation that aligns with the openEO API (using pyproj library when available) +(#259)

  • +
  • Initial load_geojson support with Connection.load_geojson() (#424)

  • +
  • Initial load_url (for vector cubes) support with Connection.load_url() (#424)

  • +
  • Add VectorCube.apply_dimension() (Open-EO/openeo-python-driver#197)

  • +
  • Support lambda based property filtering in Connection.load_stac() (#425)

  • +
  • VectorCube: initial support for filter_bands, filter_bbox, filter_labels and filter_vector (#459)

  • +
+
+
+

Changed

+
    +
  • Connection based requests: always use finite timeouts by default (20 minutes in general, 30 minutes for synchronous execute requests) +(#454)

  • +
+
+
+

Fixed

+
    +
  • Fix: MultibackendJobManager should stop when finished, also when job finishes with error (#452)

  • +
+
+
+
+

[0.21.1] - 2023-07-19

+
+

Fixed

+
    +
  • Fix spatial_extent/temporal_extent handling in “localprocessing” load_stac (#451)

  • +
+
+
+
+

[0.21.0] - 2023-07-19

+
+

Added

+
    +
  • Add support in VectoCube.download() and VectorCube.execute_batch() to guess output format from extension of a given filename +(#401, #449)

  • +
  • Added load_stac for Client Side Processing, based on the openeo-processes-dask implementation

  • +
+
+
+

Changed

+
    +
  • Updated docs for Client Side Processing with load_stac examples, available at https://open-eo.github.io/openeo-python-client/cookbook/localprocessing.html

  • +
+
+
+

Fixed

+
    +
  • Avoid double save_result nodes when combining VectorCube.save_result() and .download(). +(#401, #448)

  • +
+
+
+
+

[0.20.0] - 2023-06-30

+
+

Added

+
    +
  • Added automatically renewal of access tokens with OIDC client credentials grant (Connection.authenticate_oidc_client_credentials) +(#436)

  • +
+
+
+

Changed

+
    +
  • Simplified BatchJob methods start(), stop(), describe(), … +Legacy aliases start_job(), describe_job(), … are still available and don’t trigger a deprecation warning for now. +(#280)

  • +
  • Update openeo.extra.spectral_indices to Awesome Spectral Indices v0.4.0

  • +
+
+
+
+

[0.19.0] - 2023-06-16

+
+

Added

+
    +
  • Generalized support for setting (default) OIDC provider id through env var OPENEO_AUTH_PROVIDER_ID +#419

  • +
  • Added OidcDeviceCodePollTimeout: specific exception for OIDC device code flow poll timeouts

  • +
  • On-demand preview: Added DataCube.preview() to generate a XYZ service with the process graph and display a map widget

  • +
+
+
+

Fixed

+
    +
  • Fix format option conflict between save_result and create_job +#433

  • +
  • Ensure that OIDC device code link opens in a new tab/window #443

  • +
+
+
+
+

[0.18.0] - 2023-05-31

+
+

Added

+
    +
  • Support OIDC client credentials grant from a generic connection.authenticate_oidc() call +through environment variables +#419

  • +
+
+
+

Fixed

+
    +
  • Fixed UDP parameter conversion issue in build_process_dict when using parameter in context of run_udf +#431

  • +
+
+
+
+

[0.17.0] and [0.17.1] - 2023-05-16

+
+

Added

+
    +
  • Connection.authenticate_oidc(): add argument max_poll_time to set maximum device code flow poll time

  • +
  • Show progress bar while waiting for OIDC authentication with device code flow, +including special mode for in Jupyter notebooks. +(#237)

  • +
  • Basic support for load_stac process with Connection.load_stac() +(#425)

  • +
  • Add DataCube.aggregate_spatial_window()

  • +
+
+
+

Fixed

+
    +
  • Include “scope” parameter in OIDC token request with client credentials grant.

  • +
  • Support fractional seconds in Rfc3339.parse_datetime +(#418)

  • +
+
+
+
+

[0.16.0] - 2023-04-17 - “SRR5” release

+
+

Added

+
    +
  • Full support for user-uploaded files (/files endpoints) +(#377)

  • +
  • Initial, experimental “local processing” feature to use +openEO Python Client Library functionality on local +GeoTIFF/NetCDF files and also do the processing locally +using the openeo_processes_dask package +(#338)

  • +
  • Add BatchJob.get_results_metadata_url().

  • +
+
+
+

Changed

+
    +
  • Connection.list_files() returns a list of UserFile objects instead of a list of metadata dictionaries. +Use UserFile.metadata to get the original dictionary. +(#377)

  • +
  • DataCube.aggregate_spatial() returns a VectorCube now, instead of a DataCube +(#386). +The (experimental) fit_class_random_forest() and fit_regr_random_forest() methods +moved accordingly to the VectorCube class.

  • +
  • Improved documentation on openeo.processes and ProcessBuilder +(#390).

  • +
  • DataCube.create_job() and Connection.create_job() now require +keyword arguments for all but the first argument for clarity. +(#412).

  • +
  • Pass minimum log level to backend when retrieving batch job and secondary service logs. +(Open-EO/openeo-api#485, +Open-EO/openeo-python-driver#170)

  • +
+
+
+

Removed

+
    +
  • Dropped support for pre-1.0.0 versions of the openEO API +(#134):

    +
      +
    • Remove ImageCollectionClient and related helpers +(now unused leftovers from version 0.4.0 and earlier). +(Also #100)

    • +
    • Drop support for pre-1.0.0 job result metadata

    • +
    • Require at least version 1.0.0 of the openEO API for a back-end in Connection +and all its methods.

    • +
    +
  • +
+
+
+

Fixed

+
    +
  • Reinstated old behavior of authentication related user files (e.g. refresh token store) on Windows: when PrivateJsonFile may be readable by others, just log a message instead of raising PermissionError (387)

  • +
  • VectorCube.create_job() and MlModel.create_job() are properly aligned with DataCube.create_job() +regarding setting job title, description, etc. +(#412).

  • +
  • More robust handling of billing currency/plans in capabilities +(#414)

  • +
  • Avoid blindly adding a save_result node from DataCube.execute_batch() when there is already one +(#401)

  • +
+
+
+
+

[0.15.0] - 2023-03-03

+
+

Added

+
    +
  • The openeo Python client library can now also be installed with conda (conda-forge channel) +(#176)

  • +
  • Allow using a custom requests.Session in openeo.rest.auth.oidc logic

  • +
+
+
+

Changed

+
    +
  • Less verbose log printing on failed batch job #332

  • +
  • Improve (UTC) timezone handling in openeo.util.Rfc3339 and add rfc3339.today()/rfc3339.utcnow().

  • +
+
+
+
+

[0.14.1] - 2023-02-06

+
+

Fixed

+
    +
  • Fine-tuned XarrayDataCube tests for conda building and packaging (#176)

  • +
+
+
+
+

[0.14.0] - 2023-02-01

+
+

Added

+
    +
  • Jupyter integration: show process graph visualization of DataCube objects instead of generic repr. (#336)

  • +
  • Add Connection.vectorcube_from_paths() to load a vector cube +from files (on back-end) or URLs with load_uploaded_files process.

  • +
  • Python 3.10 and 3.11 are now officially supported +(test run now also for 3.10 and 3.11 in GitHub Actions, #346)

  • +
  • Support for simplified OIDC device code flow, (#335)

  • +
  • Added MultiBackendJobManager, based on implementation from openeo-classification project +(#361)

  • +
  • Added resilience to MultiBackendJobManager for backend failures (#365)

  • +
+
+
+

Changed

+
    +
  • execute_batch also skips temporal 502 Bad Gateway errors. #352

  • +
+
+
+

Fixed

+
    +
  • Fixed/improved math operator/process support for DataCubes in “apply” mode (non-“band math”), +allowing expressions like 10 * cube.log10() and ~(cube == 0) +(#123)

  • +
  • Support PrivateJsonFile permissions properly on Windows, using oschmod library. +(#198)

  • +
  • Fixed some broken unit tests on Windows related to path (separator) handling. +(#350)

  • +
+
+
+
+

[0.13.0] - 2022-10-10 - “UDF UX” release

+
+

Added

+
    +
  • Add max_cloud_cover argument to load_collection() to simplify setting maximum cloud cover (property eo:cloud_cover) (#328)

  • +
+
+
+

Changed

+
    +
  • Improve default dimension metadata of a datacube created with openeo.rest.datacube.DataCube.load_disk_collection

  • +
  • DataCube.download(): only automatically add save_result node when there is none yet.

  • +
  • Deprecation warnings: make sure they are shown by default and can be hidden when necessary.

  • +
  • Rework and improve openeo.UDF helper class for UDF usage +(#312).

    +
      +
    • allow loading directly from local file or URL

    • +
    • autodetect runtime from file/URL suffix or source code

    • +
    • hide implementation details around data argument (e.g.data={"from_parameter": "x"})

    • +
    • old usage patterns of openeo.UDF and DataCube.apply_dimension() still work but trigger deprecation warnings

    • +
    +
  • +
  • Show warning when using load_collection property filters that are not defined in the collection metadata (summaries).

  • +
+
+
+
+

[0.12.1] - 2022-09-15

+
+

Changed

+
    +
  • Eliminate dependency on distutils.version.LooseVersion which started to trigger deprecation warnings (#316).

  • +
+
+
+

Removed

+
    +
  • Remove old Connection.oidc_auth_user_id_token_as_bearer workaround flag (#300)

  • +
+
+
+

Fixed

+
    +
  • Fix refresh token handling in case of OIDC token request with refresh token grant (#326)

  • +
+
+
+
+

[0.12.0] - 2022-09-09

+
+

Added

+
    +
  • Allow passing raw JSON string, JSON file path or URL to Connection.download(), +Connection.execute() and Connection.create_job()

  • +
  • Add support for reverse math operators on DataCube in apply mode (#323)

  • +
  • Add DataCube.print_json() to simplify exporting process graphs in Jupyter or other interactive environments (#324)

  • +
  • Raise DimensionAlreadyExistsException when trying to add_dimension() a dimension with existing name (Open-EO/openeo-geopyspark-driver#205)

  • +
+
+
+

Changed

+
    +
  • DataCube.execute_batch() now also guesses the output format from the filename, +and allows using format argument next to the current out_format +to align with the DataCube.download() method. (#240)

  • +
  • Better client-side handling of merged band name metadata in DataCube.merge_cubes()

  • +
+
+
+

Removed

+
    +
  • Remove legacy DataCube.graph and DataCube.flatten() to prevent usage patterns that cause interoperability issues +(#155, #209, #324)

  • +
+
+
+
+

[0.11.0] - 2022-07-02

+
+

Added

+
    +
  • Add support for passing a PGNode/VectorCube as geometry to aggregate_spatial, mask_polygon, …

  • +
  • Add support for second order callbacks e.g. is_valid in count in reduce_dimension (#317)

  • +
+
+
+

Changed

+
    +
  • Rename RESTJob class name to less cryptic and more user-friendly BatchJob. +Original RESTJob is still available as deprecated alias. +(#280)

  • +
  • Dropped default reducer (“max”) from DataCube.reduce_temporal_simple()

  • +
  • Various documentation improvements:

    +
      +
    • general styling, landing page and structure tweaks (#285)

    • +
    • batch job docs (#286)

    • +
    • getting started docs (#308)

    • +
    • part of UDF docs (#309)

    • +
    • added process-to-method mapping docs

    • +
    +
  • +
  • Drop hardcoded h5netcdf engine from XarrayIO.from_netcdf_file() +and XarrayIO.to_netcdf_file() (#314)

  • +
  • Changed argument name of Connection.describe_collection() from name to collection_id +to be more in line with other methods/functions.

  • +
+
+
+

Fixed

+
    +
  • Fix context/condition confusion bug with count callback in DataCube.reduce_dimension() (#317)

  • +
+
+
+
+

[0.10.1] - 2022-05-18 - “LPS22” release

+
+

Added

+
    +
  • Add context parameter to DataCube.aggregate_spatial(), DataCube.apply_dimension(), +DataCube.apply_neighborhood(), DataCube.apply(), DataCube.merge_cubes(). +(#291)

  • +
  • Add DataCube.fit_regr_random_forest() (#293)

  • +
  • Add PGNode.update_arguments(), which combined with DataCube.result_node() allows to do advanced process graph argument tweaking/updating without using ._pg hacks.

  • +
  • JobResults.download_files(): also download (by default) the job result metadata as STAC JSON file (#184)

  • +
  • OIDC handling in Connection: try to automatically refresh access token when expired (#298)

  • +
  • Connection.create_job raises exception if response does not contain a valid job_id

  • +
  • Add openeo.udf.debug.inspect for using the openEO inspect process in a UDF (#302)

  • +
  • Add openeo.util.to_bbox_dict() to simplify building a openEO style bbox dictionary, e.g. from a list or shapely geometry (#304)

  • +
+
+
+

Removed

+
    +
  • Removed deprecated (and non-functional) zonal_statistics method from old ImageCollectionClient API. (#144)

  • +
+
+
+
+

[0.10.0] - 2022-04-08 - “SRR3” release

+
+

Added

+
    +
  • Add support for comparison operators (<, >, <= and >=) in callback process building

  • +
  • Added Connection.describe_process() to retrieve and show a single process

  • +
  • Added DataCube.flatten_dimensions() and DataCube.unflatten_dimension +(Open-EO/openeo-processes#308, Open-EO/openeo-processes#316)

  • +
  • Added VectorCube.run_udf (to avoid non-standard process_with_node(UDF(...)) usage)

  • +
  • Added DataCube.fit_class_random_forest() and Connection.load_ml_model() to train and load Machine Learning models +(#279)

  • +
  • Added DataCube.predict_random_forest() to easily use reduce_dimension with a predict_random_forest reducer +using a MlModel (trained with fit_class_random_forest) +(#279)

  • +
  • Added DataCube.resample_cube_temporal (#284)

  • +
  • Add target_dimension argument to DataCube.aggregate_spatial (#288)

  • +
  • Add basic configuration file system to define a default back-end URL and enable auto-authentication (#264, #187)

  • +
  • Add context argument to DataCube.chunk_polygon()

  • +
  • Add Connection.version_info() to list version information about the client, the API and the back-end

  • +
+
+
+

Changed

+
    +
  • Include openEO API error id automatically in exception message to simplify user support and post-mortem analysis.

  • +
  • Use Connection.default_timeout (when set) also on version discovery request

  • +
  • Drop ImageCollection from DataCube’s class hierarchy. +This practically removes very old (pre-0.4.0) methods like date_range_filter and bbox_filter from DataCube. +(#100, #278)

  • +
  • Deprecate DataCube.send_job in favor of DataCube.create_job for better consistency (internally and with other libraries) (#276)

  • +
  • Update (autogenerated) openeo.processes module to 1.2.0 release (2021-12-13) of openeo-processes

  • +
  • Update (autogenerated) openeo.processes module to draft version of 2022-03-16 (e4df8648) of openeo-processes

  • +
  • Update openeo.extra.spectral_indices to a post-0.0.6 version of Awesome Spectral Indices

  • +
+
+
+

Removed

+
    +
  • Removed deprecated zonal_statistics method from DataCube. (#144)

  • +
  • Deprecate old-style DataCube.polygonal_mean_timeseries(), DataCube.polygonal_histogram_timeseries(), +DataCube.polygonal_median_timeseries() and DataCube.polygonal_standarddeviation_timeseries()

  • +
+
+
+

Fixed

+
    +
  • Support rename_labels on temporal dimension (#274)

  • +
  • Basic support for mixing DataCube and ProcessBuilder objects/processing (#275)

  • +
+
+
+
+

[0.9.2] - 2022-01-14

+
+

Added

+
    +
  • Add experimental support for chunk_polygon process (Open-EO/openeo-processes#287)

  • +
  • Add support for spatial_extent, temporal_extent and bands to Connection.load_result()

  • +
  • Setting the environment variable OPENEO_BASEMAP_URL allows to set a new templated URL to a XYZ basemap for the Vue Components library, OPENEO_BASEMAP_ATTRIBUTION allows to set the attribution for the basemap (#260)

  • +
  • Initial support for experimental “federation:missing” flag on partial openEO Platform user job listings (Open-EO/openeo-api#419)

  • +
  • Best effort detection of mistakenly using Python builtin sum or all functions in callbacks (Forum #113)

  • +
  • Automatically print batch job logs when job doesn’t finish successfully (using execute_batch/run_synchronous/start_and_wait).

  • +
+
+
+
+

[0.9.1] - 2021-11-16

+
+

Added

+
    +
  • Add options argument to DataCube.atmospheric_correction (Open-EO/openeo-python-driver#91)

  • +
  • Add atmospheric_correction_options and cloud_detection_options arguments to DataCube.ard_surface_reflectance (Open-EO/openeo-python-driver#91)

  • +
  • UDP storing: add support for “returns”, “categories”, “examples” and “links” properties (#242)

  • +
  • Add openeo.extra.spectral_indices: experimental API to easily compute spectral indices (vegetation, water, urban, …) +on a DataCube, using the index definitions from Awesome Spectral Indices

  • +
+
+
+

Changed

+
    +
  • Batch job status poll loop: ignore (temporary) “service unavailable” errors (Open-EO/openeo-python-driver#96)

  • +
  • Batch job status poll loop: fail when there are too many soft errors (temporary connection/availability issues)

  • +
+
+
+

Fixed

+
    +
  • Fix DataCube.ard_surface_reflectance() to use process ard_surface_reflectance instead of atmospheric_correction

  • +
+
+
+
+

[0.9.0] - 2021-10-11

+
+

Added

+
    +
  • Add command line tool openeo-auth token-clear to remove OIDC refresh token cache

  • +
  • Add support for OIDC device authorization grant without PKCE nor client secret, +(#225, openeo-api#410)

  • +
  • Add DataCube.dimension_labels() (EP-4008)

  • +
  • Add Connection.load_result() (EP-4008)

  • +
  • Add proper support for child callbacks in fit_curve and predict_curve (#229)

  • +
  • ProcessBuilder: Add support for array_element(data, n) through data[n] syntax (#228)

  • +
  • ProcessBuilder: Add support for eq and neq through == and != operators (EP-4011)

  • +
  • Add DataCube.validate() for process graph validation (EP-4012 related)

  • +
  • Add Connection.as_curl() for generating curl command to evaluate a process graph or DataCube from the command line

  • +
  • Add support in DataCube.download() to guess output format from extension of a given filename

  • +
+
+
+

Changed

+
    +
  • Improve default handling of crs (and base/height) in filter_bbox: avoid explicitly sending null unnecessarily +(#233).

  • +
  • Update documentation/examples/tests: EPSG CRS in filter_bbox should be integer code, not string +(#233).

  • +
  • Raise ProcessGraphVisitException from ProcessGraphVisitor.resolve_from_node() (instead of generic ValueError)

  • +
  • DataCube.linear_scale_range is now a shortcut for DataCube.apply(lambda  x:x.x.linear_scale_range( input_min, input_max, output_min, output_max)). +Instead of creating an invalid process graph that tries to invoke linear_scale_range on a datacube directly.

  • +
  • Nicer error message when back-end does not support basic auth (#247)

  • +
+
+
+

Removed

+
    +
  • Remove unused and outdated (0.4-style) File/RESTFile classes (#115)

  • +
  • Deprecate usage of DataCube.graph property (#209)

  • +
+
+
+
+

[0.8.2] - 2021-08-24

+

Minor release to address version packaging issue.

+
+
+

[0.8.1] - 2021-08-24

+
+

Added

+
    +
  • Support nested callbacks inside array arguments, for instance in array_modify, array_create

  • +
  • Support array_concat

  • +
  • add ProcessGraphUnflattener and PGNodeGraphUnflattener to unflatten a flat dict representation of a process +graph to a PGNode graph (EP-3609)

  • +
  • Add Connection.datacube_from_flat_graph and Connection.datacube_from_json to construct a DataCube +from flat process graph representation (e.g. JSON file or JSON URL) (EP-3609)

  • +
  • Add documentation about UDP unflattening and sharing (EP-3609)

  • +
  • Add fit_curve and predict_curve, two methods used in change detection

  • +
+
+
+

Changed

+
    +
  • Update processes.py based on 1.1.0 release op openeo-processes project

  • +
  • processes.py: include all processes from “proposals” folder of openeo-processes project

  • +
  • Jupyter integration: Visual rendering for process graphs shown instead of a plain JSON representation.

  • +
  • Migrate from Travis CI to GitHub Actions for documentation building and unit tests (#178, EP-3645)

  • +
+
+
+

Removed

+
    +
  • Removed unit test runs for Python 3.5 (#210)

  • +
+
+
+
+

[0.8.0] - 2021-06-25

+
+

Added

+
    +
  • Allow, but raise warning when specifying a CRS for the geometry passed to aggregate_spatial and mask_polygon, +which is non-standard/experimental feature, only supported by specific back-ends +(#204)

  • +
  • Add optional argument to Parameter and fix re-encoding parameters with default value. (EP-3846)

  • +
  • Add support to test strict equality with ComparableVersion

  • +
  • Jupyter integration: add rich HTML rendering for more backend metadata (Job, Job Estimate, Logs, Services, User-Defined Processes)

  • +
  • Add support for filter_spatial

  • +
  • Add support for aggregate_temporal_period

  • +
  • Added class Service for secondary web-services

  • +
  • Added a method service to Connection

  • +
  • Add Rfc3339.parse_date and Rfc3339.parse_date_or_datetime

  • +
+
+
+

Changed

+
    +
  • Disallow redirects on POST/DELETE/… requests and require status code 200 on POST /result requests. +This improves error information where POST /result would involve a redirect. (EP-3889)

  • +
  • Class JobLogEntry got replaced with a more complete and re-usable LogEntry dict

  • +
  • The following methods return a Service class instead of a dict: tiled_viewing_service in ImageCollection, ImageCollectionClient and DataCube, create_service in Connection

  • +
+
+
+

Deprecated

+
    +
  • The method remove_service in Connection has been deprecated in favor of delete_service in the Service class

  • +
+
+
+
+

[0.7.0] - 2021-04-21

+
+

Added

+ +
+
+

Changed

+
    +
  • Eliminate development/optional dependency on openeo_udf project +(#159, #190, EP-3578). +Now the openEO client library itself contains the necessary classes and implementation to run UDF code locally.

  • +
+
+
+

Fixed

+
    +
  • Connection: don’t send default auth headers to non-backend domains (#201)

  • +
+
+
+
+

[0.6.1] - 2021-03-29

+
+

Changed

+
    +
  • Improve OpenID Connect usability on Windows: don’t raise exception on file permissions +that can not be changed (by os.chmod on Windows) (#198)

  • +
+
+
+
+

[0.6.0] - 2021-03-26

+
+

Added

+
    +
  • Add initial/experimental support for OIDC device code flow with PKCE (alternative for client secret) (#191 / EP-3700)

  • +
  • When creating a connection: use “https://” by default when no protocol is specified

  • +
  • DataCube.mask_polygon: support Parameter argument for mask

  • +
  • Add initial/experimental support for default OIDC client (#192, Open-EO/openeo-api#366)

  • +
  • Add Connection.authenticate_oidc for user-friendlier OIDC authentication: first try refresh token and fall back on device code flow

  • +
  • Add experimental support for array_modify process (Open-EO/openeo-processes#202)

  • +
+
+
+

Removed

+
    +
  • Remove old/deprecated Connection.authenticate_OIDC()

  • +
+
+
+
+

[0.5.0] - 2021-03-17

+
+

Added

+
    +
  • Add namespace support to DataCube.process, PGNode, ProcessGraphVisitor (minor API breaking change) and related. +Allows building process graphs with processes from non-“backend” namespaces +(#182)

  • +
  • collection_items to request collection items through a STAC API

  • +
  • paginate as a basic method to support link-based pagination

  • +
  • Add namespace support to Connection.datacube_from_process

  • +
  • Add basic support for band name aliases in metadata.Band for band index lookup (EP-3670)

  • +
+
+
+

Changed

+
    +
  • OpenEoApiError moved from openeo.rest.connection to openeo.rest

  • +
  • Added HTML representation for list_jobs, list_services, list_files and for job results

  • +
  • Improve refresh token handling in OIDC logic: avoid requesting refresh token +(which can fail if OIDC client is not set up for that) when not necessary (EP-3700)

  • +
  • RESTJob.start_and_wait: add status line when sending “start” request, and drop microsecond resolution from status lines

  • +
+
+
+

Fixed

+
    +
  • Updated Vue Components library (solves issue with loading from slower back-ends where no result was shown)

  • +
+
+
+
+

[0.4.10] - 2021-02-26

+
+

Added

+
    +
  • Add “reflected” operator support to ProcessBuilder

  • +
  • Add RESTJob.get_results(), JobResults and ResultAsset for more fine-grained batch job result handling. (EP-3739)

  • +
  • Add documentation on batch job result (asset) handling and downloading

  • +
+
+
+

Changed

+
    +
  • Mark Connection.imagecollection more clearly as deprecated/legacy alias of Connection.load_collection

  • +
  • Deprecated job_results() and job_logs() on Connection object, it’s better to work through RESTJob object.

  • +
  • Update DataCube.sar_backscatter to the latest process spec: add coefficient argument +and remove orthorectify, rtc. (openeo-processes#210)

  • +
+
+
+

Removed

+
    +
  • Remove outdated batch job result download logic left-overs

  • +
  • Remove (outdated) abstract base class openeo.job.Job: did not add value, only caused maintenance overhead. (#115)

  • +
+
+
+
+

[0.4.9] - 2021-01-29

+
+

Added

+
    +
  • Make DataCube.filter_bbox() easier to use: allow passing a bbox tuple, list, dict or even shapely geometry directly as first positional argument or as bbox keyword argument. +Handling of the legacy non-standard west-east-north-south positional argument order is preserved for now (#136)

  • +
  • Add “band math” methods DataCube.ln(), DataCube.logarithm(base), DataCube.log10() and DataCube.log2()

  • +
  • Improved support for creating and handling parameters when defining user-defined processes (EP-3698)

  • +
  • Initial Jupyter integration: add rich HTML rendering of backend metadata (collections, file formats, UDF runtimes, …) +(#170)

  • +
  • add resolution_merge process (experimental) (EP-3687, openeo-processes#221)

  • +
  • add sar_backscatter process (experimental) (EP-3612, openeo-processes#210)

  • +
+
+
+

Fixed

+
    +
  • Fixed ‘Content-Encoding’ handling in Connection.download: client did not automatically decompress /result +responses when necessary (#175)

  • +
+
+
+
+

[0.4.8] - 2020-11-17

+
+

Added

+
    +
  • Add DataCube.aggregate_spatial()

  • +
+
+
+

Changed

+
    +
  • Get/create default RefreshTokenStore lazily in Connection

  • +
  • Various documentation tweaks

  • +
+
+
+
+

[0.4.7] - 2020-10-22

+
+

Added

+
    +
  • Add support for title/description/plan/budget in DataCube.send_job (#157 / #158)

  • +
  • Add DataCube.to_json() to easily get JSON representation of a DataCube

  • +
  • Allow to subclass CollectionMetadata and preserve original type when “cloning”

  • +
+
+
+

Changed

+
    +
  • Changed execute_batch to support downloading multiple files (within EP-3359, support profiling)

  • +
  • Don’t send None-valued title/description/plan/budget fields from DataCube.send_job (#157 / #158)

  • +
+
+
+

Removed

+
    +
  • Remove duplicate and broken Connection.list_processgraphs

  • +
+
+
+

Fixed

+
    +
  • Various documentation fixes and tweaks

  • +
  • Avoid merge_cubes warning when using non-band-math DataCube operators

  • +
+
+
+
+

[0.4.6] - 2020-10-15

+
+

Added

+
    +
  • Add DataCube.aggregate_temporal

  • +
  • Add initial support to download profiling information

  • +
+
+
+

Changed

+
    +
  • Deprecated legacy functions/methods are better documented as such and link to a recommended alternative (EP-3617).

  • +
  • Get/create default AuthConfig in Connection lazily (allows client to run in environments without existing (default) config folder)

  • +
+
+
+

Deprecated

+
    +
  • Deprecate zonal_statistics in favor of aggregate_spatial

  • +
+
+
+

Removed

+
    +
  • Remove support for old, non-standard stretch_colors process (Use linear_scale_range instead).

  • +
+
+
+
+

[0.4.5] - 2020-10-01

+
+

Added

+
    +
  • Also handle dict arguments in dereference_from_node_arguments (EP-3509)

  • +
  • Add support for less/greater than and equal operators

  • +
  • Raise warning when user defines a UDP with same id as a pre-defined one (EP-3544, #147)

  • +
  • Add rename_labels support in metadata (EP-3585)

  • +
  • Improve “callback” handling (sub-process graphs): add predefined callbacks for all official processes and functionality to assemble these (EP-3555, #153)

  • +
  • Moved datacube write/save/plot utilities from udf to client (EP-3456)

  • +
  • Add documentation on OpenID Connect authentication (EP-3485)

  • +
+
+
+

Fixed

+
    +
  • Fix kwargs handling in TimingLogger decorator

  • +
+
+
+
+

[0.4.4] - 2020-08-20

+
+

Added

+
    +
  • Add openeo-auth command line tool to manage OpenID Connect (and basic auth) related configs (EP-3377/EP-3493)

  • +
  • Support for using config files for OpenID Connect and basic auth based authentication, instead of hardcoding credentials (EP-3377/EP-3493)

  • +
+
+
+

Fixed

+
    +
  • Fix target_band handling in DataCube.ndvi (EP-3496)

  • +
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/configuration.html b/configuration.html new file mode 100644 index 000000000..0238f620d --- /dev/null +++ b/configuration.html @@ -0,0 +1,239 @@ + + + + + + + + Configuration — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Configuration

+
+

Warning

+

Configuration files are an experimental feature +and some details are subject to change.

+
+
+

Added in version 0.10.0.

+
+
+

Configuration files

+

Some functionality of the openEO Python client library can customized +through configuration files.

+
+

Note

+

Note that these configuration files are different from the authentication secret/cache files +discussed at Auth config files and openeo-auth helper tool. +The latter are focussed on storing authentication secrets +and are mostly managed automatically. +The normal configuration files however should not contain secrets, +are usually edited manually, can be placed at various locations +and it is not uncommon to store them in version control where that makes sense.

+
+
+

Format

+

At the moment, only INI-style configs are supported. +This is a simple configuration format, easy to maintain +and it is supported out of the box in Python (without additional libraries).

+

Example (note the use of sections and support for comments):

+
[General]
+# Print loaded configuration file and default back-end URLs in interactive mode
+verbose = auto
+
+[Connection]
+default_backend = openeo.cloud
+
+
+
+
+

Location

+

The following configuration locations are probed (in this order) for an existing configuration file. The first successful hit will be loaded:

+
    +
  • the path in environment variable OPENEO_CLIENT_CONFIG if it is set (filename must end with extension .ini)

  • +
  • the file openeo-client-config.ini in the current working directory

  • +
  • the file ${OPENEO_CONFIG_HOME}/openeo-client-config.ini if the environment variable OPENEO_CONFIG_HOME is set

  • +
  • the file ${XDG_CONFIG_HOME}/openeo-python-client/openeo-client-config.ini if environment variable XDG_CONFIG_HOME is set

  • +
  • the file .openeo-client-config.ini in the home folder of the user

  • +
+
+
+

Configuration options

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +

Config Section

Config

Description and possible values

General

verbose

+
Verbosity mode when important config values are used:
    +
  • print: always print() info

  • +
  • auto (default): only print() when in an interactive context

  • +
  • off: don’t print info

  • +
+
+
+

Connection

default_backend

Default back-end to connect to when openeo.connect() +is used without explicit back-end URL. +Also see Default openEO back-end URL and auto-authentication

Connection

default_backend.auto_authenticate

+
Automatically authenticate in openeo.connect() when using the default_backend config. Allowed values:
    +
  • basic for basic authentication

  • +
  • oidc for OpenID Connect authentication

  • +
  • off (default) for no authentication

  • +
+
+
+

Also see Default openEO back-end URL and auto-authentication

+

Connection

auto_authenticate

Automatically authenticate in openeo.connect(). +Allowed values: see default_backend.auto_authenticate. +Also see Default openEO back-end URL and auto-authentication

+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/ard.html b/cookbook/ard.html new file mode 100644 index 000000000..fe3f6e363 --- /dev/null +++ b/cookbook/ard.html @@ -0,0 +1,234 @@ + + + + + + + + Analysis Ready Data generation — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Analysis Ready Data generation

+

For certain use cases, the preprocessed data collections available in the openEO back-ends are not sufficient or simply not +available. For that case, openEO supports a few very common preprocessing scenario:

+
    +
  • Atmospheric correction of optical data

  • +
  • SAR backscatter computation

  • +
+

These processes also offer a number of parameters to customize the processing. There’s also variants with a default +parametrization that results in data that is compliant with CEOS CARD4L specifications https://ceos.org/ard/.

+

We should note that these operations can be computationally expensive, so certainly affect overall processing time and +cost of your final algorithm. Hence, make sure to make an informed decision when you decide to use these methods.

+
+

Atmospheric correction

+

The atmospheric correction process can apply a chosen +method on raw ‘L1C’ data. The supported methods and input datasets depend on the back-end, because not every method is +validated or works on any dataset, and different back-ends try to offer a variety of options. This gives you as a user +more options to run and compare different methods, and select the most suitable one for your case.

+

To perform an atmospheric correction, the user has to +load an uncorrected L1C optical dataset. On the resulting datacube, the atmospheric_correction() +method can be invoked. Note that it may not be possible to apply certain processes to the raw input data: preprocessing +algorithms can be tightly coupled with the raw data, making it hard or impossible for the back-end to perform operations +in between loading and correcting the data.

+

The CARD4L variant of this process is: ard_surface_reflectance(). This process follows +CEOS specifications, and thus can additional processing steps, like a BRDF correction, that are not yet available as a +separate process.

+
+

Reference implementations

+

This section shows a few working examples for these processes.

+
+

EODC back-end

+

EODC (https://openeo.eodc.eu/v1.0) supports ard_surface_reflectance, based on the FORCE toolbox. (https://github.com/davidfrantz/force)

+
+
+

Geotrellis back-end

+

The geotrellis back-end (https://openeo.vito.be) supports atmospheric_correction() with iCor and SMAC as methods. +The version of iCor only offers basic atmoshperic correction features, without special options for water products: https://remotesensing.vito.be/case/icor +SMAC is implemented based on: https://github.com/olivierhagolle/SMAC +Both methods have been tested with Sentinel-2 as input. The viewing and sun angles need to be selected by the user to make them +available for the algorithm.

+

This is an example of applying iCor:

+
l1c = connection.load_collection("SENTINEL2_L1C_SENTINELHUB",
+        spatial_extent={'west':3.758216409030558,'east':4.087806252,'south':51.291835566,'north':51.3927399},
+        temporal_extent=["2017-03-07","2017-03-07"],bands=['B04','B03','B02','B09','B8A','B11','sunAzimuthAngles','sunZenithAngles','viewAzimuthMean','viewZenithMean'] )
+l1c.atmospheric_correction(method="iCor").download("rgb-icor.geotiff",format="GTiff")
+
+
+
+
+
+
+

SAR backscatter

+

Data from synthetic aperture radar sensors requires significant preprocessing to be calibrated and normalized for terrain. +This is referred to as backscatter computation, and supported by +sar_backscatter and the CARD4L compliant variant +ard_normalized_radar_backscatter

+

The user should load a datacube containing raw SAR data, such as Sentinel-1 GRD. On the resulting datacube, the +sar_backscatter() method can be invoked. The CEOS CARD4L variant is: +ard_normalized_radar_backscatter(). These processes are tightly coupled to +metadata from specific sensors, so it is not possible to apply other processes to the datacube first, +with the exception of specifying filters in space and time.

+
+

Reference implementations

+

This section shows a few working examples for these processes.

+
+

EODC back-end

+

EODC (https://openeo.eodc.eu/v1.0) supports sar_backscatter, based on the Sentinel-1 toolbox. (https://sentinel.esa.int/web/sentinel/toolboxes/sentinel-1)

+
+
+

Geotrellis back-end

+

When working with the Sentinelhub SENTINEL1_GRD collection, both sar processes can be used. The underlying implementation is +provided by Sentinelhub, (https://docs.sentinel-hub.com/api/latest/data/sentinel-1-grd/#processing-options), and offers full +CARD4L compliant processing options.

+

This is an example of ard_normalized_radar_backscatter():

+
s1grd = (connection.load_collection('SENTINEL1_GRD', bands=['VH', 'VV'])
+ .filter_bbox(west=2.59003, east=2.8949, north=51.2206, south=51.069)
+ .filter_temporal(extent=["2019-10-10","2019-10-10"]))
+
+job = s1grd.ard_normalized_radar_backscatter().execute_batch()
+
+for asset in job.get_results().get_assets():
+    asset.download()
+
+
+

When working with other GRD data, an implementation based on Orfeo Toolbox is used:

+ +

The Orfeo implementation currently only supports sigma0 computation, and is not CARD4L compliant.

+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/index.html b/cookbook/index.html new file mode 100644 index 000000000..d4caa0784 --- /dev/null +++ b/cookbook/index.html @@ -0,0 +1,218 @@ + + + + + + + + openEO CookBook — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/job_manager.html b/cookbook/job_manager.html new file mode 100644 index 000000000..06a7e0a93 --- /dev/null +++ b/cookbook/job_manager.html @@ -0,0 +1,766 @@ + + + + + + + + Multi Backend Job Manager — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Multi Backend Job Manager

+
+

API

+
+

Warning

+

This is a new experimental API, subject to change.

+
+
+
+class openeo.extra.job_management.MultiBackendJobManager(poll_sleep=60, root_dir='.', *, cancel_running_job_after=None)[source]
+

Tracker for multiple jobs on multiple backends.

+

Usage example:

+
import logging
+import pandas as pd
+import openeo
+from openeo.extra.job_management import MultiBackendJobManager
+
+logging.basicConfig(
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+    level=logging.INFO
+)
+
+manager = MultiBackendJobManager()
+manager.add_backend("foo", connection=openeo.connect("http://foo.test"))
+manager.add_backend("bar", connection=openeo.connect("http://bar.test"))
+
+jobs_df = pd.DataFrame(...)
+output_file = "jobs.csv"
+
+def start_job(
+    row: pd.Series,
+    connection: openeo.Connection,
+    **kwargs
+) -> openeo.BatchJob:
+    year = row["year"]
+    cube = connection.load_collection(
+        ...,
+        temporal_extent=[f"{year}-01-01", f"{year+1}-01-01"],
+    )
+    ...
+    return cube.create_job(...)
+
+manager.run_jobs(df=jobs_df, start_job=start_job, output_file=output_file)
+
+
+

See run_jobs() for more information on the start_job callable.

+
+
Parameters:
+
    +
  • poll_sleep (int) – How many seconds to sleep between polls.

  • +
  • root_dir (Union[str, Path, None]) –

    Root directory to save files for the jobs, e.g. metadata and error logs. +This defaults to “.” the current directory.

    +

    Each job gets its own subfolder in this root directory. +You can use the following methods to find the relevant paths, +based on the job ID:

    +
    +
      +
    • get_job_dir

    • +
    • get_error_log_path

    • +
    • get_job_metadata_path

    • +
    +
    +

  • +
  • cancel_running_job_after (Optional[int]) – Optional temporal limit (in seconds) after which running jobs should be canceled +by the job manager.

  • +
+
+
+
+

Added in version 0.14.0.

+
+
+

Changed in version 0.32.0: Added cancel_running_job_after parameter.

+
+
+
+add_backend(name, connection, parallel_jobs=2)[source]
+

Register a backend with a name and a Connection getter.

+
+
Parameters:
+
    +
  • name (str) – Name of the backend.

  • +
  • connection (Union[Connection, Callable[[], Connection]]) – Either a Connection to the backend, or a callable to create a backend connection.

  • +
  • parallel_jobs (int) – Maximum number of jobs to allow in parallel on a backend.

  • +
+
+
+
+ +
+
+ensure_job_dir_exists(job_id)[source]
+

Create the job folder if it does not exist yet.

+
+
Return type:
+

Path

+
+
+
+ +
+
+get_error_log_path(job_id)[source]
+

Path where error log file for the job is saved.

+
+
Return type:
+

Path

+
+
+
+ +
+
+get_job_dir(job_id)[source]
+

Path to directory where job metadata, results and error logs are be saved.

+
+
Return type:
+

Path

+
+
+
+ +
+
+get_job_metadata_path(job_id)[source]
+

Path where job metadata file is saved.

+
+
Return type:
+

Path

+
+
+
+ +
+
+on_job_cancel(job, row)[source]
+

Handle a job that was cancelled. Can be overridden to provide custom behaviour.

+

Default implementation does not do anything.

+
+
Parameters:
+
    +
  • job (BatchJob) – The job that was canceled.

  • +
  • row – DataFrame row containing the job’s metadata.

  • +
+
+
+
+ +
+
+on_job_done(job, row)[source]
+

Handles jobs that have finished. Can be overridden to provide custom behaviour.

+

Default implementation downloads the results into a folder containing the title.

+
+
Parameters:
+
    +
  • job (BatchJob) – The job that has finished.

  • +
  • row – DataFrame row containing the job’s metadata.

  • +
+
+
+
+ +
+
+on_job_error(job, row)[source]
+

Handles jobs that stopped with errors. Can be overridden to provide custom behaviour.

+

Default implementation writes the error logs to a JSON file.

+
+
Parameters:
+
    +
  • job (BatchJob) – The job that has finished.

  • +
  • row – DataFrame row containing the job’s metadata.

  • +
+
+
+
+ +
+
+run_jobs(df=None, start_job=<function _start_job_default>, job_db=None, **kwargs)[source]
+

Runs jobs, specified in a dataframe, and tracks parameters.

+
+
Parameters:
+
    +
  • df (Optional[DataFrame]) – DataFrame that specifies the jobs, and tracks the jobs’ statuses. If None, the job_db has to be specified and will be used.

  • +
  • start_job (Callable[[], BatchJob]) –

    A callback which will be invoked with, amongst others, +the row of the dataframe for which a job should be created and/or started. +This callable should return a openeo.rest.job.BatchJob object.

    +

    The following parameters will be passed to start_job:

    +
    +
    +
    row (pandas.Series):

    The row in the pandas dataframe that stores the jobs state and other tracked data.

    +
    +
    connection_provider:

    A getter to get a connection by backend name. +Typically, you would need either the parameter connection_provider, +or the parameter connection, but likely you will not need both.

    +
    +
    connection (Connection):

    The Connection itself, that has already been created. +Typically, you would need either the parameter connection_provider, +or the parameter connection, but likely you will not need both.

    +
    +
    provider (str):

    The name of the backend that will run the job.

    +
    +
    +
    +

    You do not have to define all the parameters described below, but if you leave +any of them out, then remember to include the *args and **kwargs parameters. +Otherwise you will have an exception because run_jobs() passes unknown parameters to start_job.

    +

  • +
  • job_db (Union[str, Path, JobDatabaseInterface, None]) –

    Job database to load/store existing job status data and other metadata from/to. +Can be specified as a path to CSV or Parquet file, +or as a custom database object following the JobDatabaseInterface interface.

    +
    +

    Note

    +

    Support for Parquet files depends on the pyarrow package +as optional dependency.

    +
    +

  • +
+
+
Return type:
+

dict

+
+
Returns:
+

dictionary with stats collected during the job running loop. +Note that the set of fields in this dictionary is experimental +and subject to change

+
+
+
+

Changed in version 0.31.0: Added support for persisting the job metadata in Parquet format.

+
+
+

Changed in version 0.31.0: Replace output_file argument with job_db argument, +which can be a path to a CSV or Parquet file, +or a user-defined JobDatabaseInterface object. +The deprecated output_file argument is still supported for now.

+
+
+

Changed in version 0.33.0: return a stats dictionary

+
+
+ +
+
+start_job_thread(start_job, job_db)[source]
+

Start running the jobs in a separate thread, returns afterwards.

+
+
Parameters:
+
    +
  • start_job (Callable[[], BatchJob]) –

    A callback which will be invoked with, amongst others, +the row of the dataframe for which a job should be created and/or started. +This callable should return a openeo.rest.job.BatchJob object.

    +

    The following parameters will be passed to start_job:

    +
    +
    +
    row (pandas.Series):

    The row in the pandas dataframe that stores the jobs state and other tracked data.

    +
    +
    connection_provider:

    A getter to get a connection by backend name. +Typically, you would need either the parameter connection_provider, +or the parameter connection, but likely you will not need both.

    +
    +
    connection (Connection):

    The Connection itself, that has already been created. +Typically, you would need either the parameter connection_provider, +or the parameter connection, but likely you will not need both.

    +
    +
    provider (str):

    The name of the backend that will run the job.

    +
    +
    +
    +

    You do not have to define all the parameters described below, but if you leave +any of them out, then remember to include the *args and **kwargs parameters. +Otherwise you will have an exception because run_jobs() passes unknown parameters to start_job.

    +

  • +
  • job_db (JobDatabaseInterface) –

    Job database to load/store existing job status data and other metadata from/to. +Can be specified as a path to CSV or Parquet file, +or as a custom database object following the JobDatabaseInterface interface.

    +
    +

    Note

    +

    Support for Parquet files depends on the pyarrow package +as optional dependency.

    +
    +

  • +
+
+
+
+

Added in version 0.32.0.

+
+
+ +
+
+stop_job_thread(timeout_seconds=<object object>)[source]
+

Stop the job polling thread.

+
+
Parameters:
+

timeout_seconds (Optional[float]) – The time to wait for the thread to stop. +By default, it will wait for 2 times the poll_sleep time. +Set to None to wait indefinitely.

+
+
+
+

Added in version 0.32.0.

+
+
+ +
+ +
+
+class openeo.extra.job_management.JobDatabaseInterface[source]
+

Interface for a database of job metadata to use with the MultiBackendJobManager, +allowing to regularly persist the job metadata while polling the job statuses +and resume/restart the job tracking after it was interrupted.

+
+

Added in version 0.31.0.

+
+
+
+abstract count_by_status(statuses)[source]
+

Retrieve the number of jobs per status.

+
+
Return type:
+

dict

+
+
Returns:
+

dictionary with status as key and the count as value.

+
+
+
+ +
+
+abstract exists()[source]
+

Does the job database already exist, to read job data from?

+
+
Return type:
+

bool

+
+
+
+ +
+
+abstract get_by_status(statuses, max=None)[source]
+

Returns a dataframe with jobs, filtered by status.

+
+
Parameters:
+
    +
  • statuses (List[str]) – List of statuses to include.

  • +
  • max – Maximum number of jobs to return.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

DataFrame with jobs filtered by status.

+
+
+
+ +
+
+abstract persist(df)[source]
+

Store job data to the database. +The provided dataframe may contain partial information, which is merged into the larger database.

+
+
Parameters:
+

df (DataFrame) – job data to store.

+
+
+
+ +
+
+abstract read()[source]
+

Read job data from the database as pandas DataFrame.

+
+
Return type:
+

DataFrame

+
+
Returns:
+

loaded job data.

+
+
+
+ +
+ +
+
+class openeo.extra.job_management.CsvJobDatabase(path)[source]
+

Persist/load job metadata with a CSV file.

+
+
Implements:
+

JobDatabaseInterface

+
+
Parameters:
+

path (Union[str, Path]) – Path to local CSV file.

+
+
+
+

Note

+

Support for GeoPandas dataframes depends on the geopandas package +as optional dependency.

+
+
+

Added in version 0.31.0.

+
+
+ +
+
+class openeo.extra.job_management.ParquetJobDatabase(path)[source]
+

Persist/load job metadata with a Parquet file.

+
+
Implements:
+

JobDatabaseInterface

+
+
Parameters:
+

path (Union[str, Path]) – Path to the Parquet file.

+
+
+
+

Note

+

Support for Parquet files depends on the pyarrow package +as optional dependency.

+

Support for GeoPandas dataframes depends on the geopandas package +as optional dependency.

+
+
+

Added in version 0.31.0.

+
+
+ +
+
+class openeo.extra.job_management.ProcessBasedJobCreator(*, process_id=None, namespace=None, parameter_defaults=None, parameter_column_map=None)[source]
+

Batch job creator +(to be used together with MultiBackendJobManager) +that takes a parameterized openEO process definition +(e.g a user-defined process (UDP) or a remote openEO process definition), +and creates a batch job +for each row of the dataframe managed by the MultiBackendJobManager +by filling in the process parameters with corresponding row values.

+
+

See also

+

See Job creation based on parameterized processes +for more information and examples.

+
+

Process parameters are linked to dataframe columns by name. +While this intuitive name-based matching should cover most use cases, +there are additional options for overrides or fallbacks:

+
    +
  • When provided, parameter_column_map will be consulted +for resolving a process parameter name (key in the dictionary) +to a desired dataframe column name (corresponding value).

  • +
  • One common case is handled automatically as convenience functionality.

    +

    When:

    +
      +
    • parameter_column_map is not provided (or set to None),

    • +
    • and there is a single parameter that accepts inline GeoJSON geometries,

    • +
    • and the dataframe is a GeoPandas dataframe with a single geometry column,

    • +
    +

    then this parameter and this geometries column will be linked automatically.

    +
  • +
  • If a parameter can not be matched with a column by name as described above, +a default value will be picked, +first by looking in parameter_defaults (if provided), +and then by looking up the default value from the parameter schema in the process definition.

  • +
  • Finally if no (default) value can be determined and the parameter +is not flagged as optional, an error will be raised.

  • +
+
+
Parameters:
+
    +
  • process_id (Optional[str]) – (optional) openEO process identifier. +Can be omitted when working with a remote process definition +that is fully defined with a URL in the namespace parameter.

  • +
  • namespace (Optional[str]) – (optional) openEO process namespace. +Typically used to provide a URL to a remote process definition.

  • +
  • parameter_defaults (Optional[dict]) – (optional) default values for process parameters, +to be used when not available in the dataframe managed by +MultiBackendJobManager.

  • +
  • parameter_column_map (Optional[dict]) – Optional overrides +for linking process parameters to dataframe columns: +mapping of process parameter names as key +to dataframe column names as value.

  • +
+
+
+
+

Added in version 0.33.0.

+
+
+

Warning

+

This is an experimental API subject to change, +and we greatly welcome +feedback and suggestions for improvement.

+
+
+
+__call__(*arg, **kwargs)[source]
+

Syntactic sugar for calling start_job().

+
+
Return type:
+

BatchJob

+
+
+
+ +
+
+start_job(row, connection, **_)[source]
+

Implementation of the start_job callable interface +of MultiBackendJobManager.run_jobs() +to create a job based on given dataframe row

+
+
Parameters:
+
    +
  • row (Series) – The row in the pandas dataframe that stores the jobs state and other tracked data.

  • +
  • connection (Connection) – The connection to the backend.

  • +
+
+
Return type:
+

BatchJob

+
+
+
+ +
+ +
+
+

Job creation based on parameterized processes

+

The openEO API supports parameterized processes out of the box, +which allows to work with flexible, reusable openEO building blocks +in the form of user-defined processes +or remote openEO process definitions. +This can also be leveraged for job creation in the context of the +MultiBackendJobManager: +define a “template” job as a parameterized process +and let the job manager fill in the parameters +from a given data frame.

+

The ProcessBasedJobCreator helper class +allows to do exactly that. +Given a reference to a parameterized process, +such as a user-defined process or remote process definition, +it can be used directly as start_job callable to +run_jobs() +which will fill in the process parameters from the dataframe.

+
+

Basic ProcessBasedJobCreator example

+

Basic usage example with a remote process definition:

+
+
Basic ProcessBasedJobCreator example snippet
+
 1from openeo.extra.job_management import (
+ 2    MultiBackendJobManager,
+ 3    create_job_db,
+ 4    ProcessBasedJobCreator,
+ 5)
+ 6
+ 7# Job creator, based on a parameterized openEO process
+ 8# (specified by the remote process definition at given URL)
+ 9# which has parameters "start_date" and "bands" for example.
+10job_starter = ProcessBasedJobCreator(
+11    namespace="https://example.com/my_process.json",
+12    parameter_defaults={
+13        "bands": ["B02", "B03"],
+14    },
+15)
+16
+17# Initialize job database from a dataframe,
+18# with desired parameter values to fill in.
+19df = pd.DataFrame({
+20    "start_date": ["2021-01-01", "2021-02-01", "2021-03-01"],
+21})
+22job_db = create_job_db("jobs.csv").initialize_from_df(df)
+23
+24# Create and run job manager,
+25# which will start a job for each of the `start_date` values in the dataframe
+26# and use the default band list ["B02", "B03"] for the "bands" parameter.
+27job_manager = MultiBackendJobManager(...)
+28job_manager.run_jobs(job_db=job_db, start_job=job_starter)
+
+
+
+

In this example, a ProcessBasedJobCreator is instantiated +based on a remote process definition, +which has parameters start_date and bands. +When passed to run_jobs(), +a job for each row in the dataframe will be created, +with parameter values based on matching columns in the dataframe:

+
    +
  • the start_date parameter will be filled in +with the values from the “start_date” column of the dataframe,

  • +
  • the bands parameter has no corresponding column in the dataframe, +and will get its value from the default specified in the parameter_defaults argument.

  • +
+
+
+

ProcessBasedJobCreator with geometry handling

+

Apart from the intuitive name-based parameter-column linking, +ProcessBasedJobCreator +also automatically links:

+
    +
  • a process parameters that accepts inline GeoJSON geometries/features +(which practically means it has a schema like {"type": "object", "subtype": "geojson"}, +as produced by Parameter.geojson).

  • +
  • with the geometry column in a GeoPandas dataframe.

  • +
+

even if the name of the parameter does not exactly match +the name of the GeoPandas geometry column (geometry by default). +This automatic liking is only done if there is only one +GeoJSON parameter and one geometry column in the dataframe.

+
+

to do

+

Add example with geometry handling.

+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/localprocessing.html b/cookbook/localprocessing.html new file mode 100644 index 000000000..3ed24f2bd --- /dev/null +++ b/cookbook/localprocessing.html @@ -0,0 +1,307 @@ + + + + + + + + Client-side (local) processing — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Client-side (local) processing

+
+

Warning

+

This is a new experimental feature and API, subject to change.

+
+
+

Background

+

The client-side processing functionality allows to test and use openEO with its processes locally, i.e. without any connection to an openEO back-end. +It relies on the projects openeo-pg-parser-networkx, which provides an openEO process graph parsing tool, and openeo-processes-dask, which provides an Xarray and Dask implementation of most openEO processes.

+
+
+

Installation

+
+

Note

+

This feature requires Python>=3.9. +Tested with openeo-pg-parser-networkx==2023.5.1 and +openeo-processes-dask==2023.7.1.

+
+
pip install openeo[localprocessing]
+
+
+
+
+

Usage

+

Every openEO process graph relies on data which is typically provided by a cloud infrastructure (the openEO back-end). +The client-side processing adds the possibility to read and use local netCDFs, geoTIFFs, ZARR files, and remote STAC Collections or Items for your experiments.

+
+

STAC Collections and Items

+
+

Warning

+

The provided examples using STAC rely on third party STAC Catalogs, we can’t guarantee that the urls will remain valid.

+
+

With the load_stac process it’s possible to load and use data provided by remote or local STAC Collections or Items. +The following code snippet loads Sentinel-2 L2A data from a public STAC Catalog, using specific spatial and temporal extent, band name and also properties for cloud coverage.

+
>>> from openeo.local import LocalConnection
+>>> local_conn = LocalConnection("./")
+
+>>> url = "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a"
+>>> spatial_extent = {"west": 11, "east": 12, "south": 46, "north": 47}
+>>> temporal_extent = ["2019-01-01", "2019-06-15"]
+>>> bands = ["red"]
+>>> properties = {"eo:cloud_cover": dict(lt=50)}
+>>> s2_cube = local_conn.load_stac(url=url,
+...    spatial_extent=spatial_extent,
+...    temporal_extent=temporal_extent,
+...    bands=bands,
+...    properties=properties,
+... )
+>>> s2_cube.execute()
+<xarray.DataArray 'stackstac-08730b1b5458a4ed34edeee60ac79254' (time: 177,
+                                                                band: 1,
+                                                                y: 11354,
+                                                                x: 8025)>
+dask.array<getitem, shape=(177, 1, 11354, 8025), dtype=float64, chunksize=(1, 1, 1024, 1024), chunktype=numpy.ndarray>
+Coordinates: (12/53)
+  * time                                     (time) datetime64[ns] 2019-01-02...
+    id                                       (time) <U24 'S2B_32TPR_20190102_...
+  * band                                     (band) <U3 'red'
+  * x                                        (x) float64 6.52e+05 ... 7.323e+05
+  * y                                        (y) float64 5.21e+06 ... 5.096e+06
+    s2:product_uri                           (time) <U65 'S2B_MSIL2A_20190102...
+    ...                                       ...
+    raster:bands                             object {'nodata': 0, 'data_type'...
+    gsd                                      int32 10
+    common_name                              <U3 'red'
+    center_wavelength                        float64 0.665
+    full_width_half_max                      float64 0.038
+    epsg                                     int32 32632
+Attributes:
+    spec:        RasterSpec(epsg=32632, bounds=(600000.0, 4990200.0, 809760.0...
+    crs:         epsg:32632
+    transform:   | 10.00, 0.00, 600000.00|\n| 0.00,-10.00, 5300040.00|\n| 0.0...
+    resolution:  10.0
+
+
+
+
+

Local Collections

+

If you want to use our sample data, please clone this repository:

+
git clone https://github.com/Open-EO/openeo-localprocessing-data.git
+
+
+

With some sample data we can now check the STAC metadata for the local files by doing:

+
from openeo.local import LocalConnection
+local_data_folders = [
+    "./openeo-localprocessing-data/sample_netcdf",
+    "./openeo-localprocessing-data/sample_geotiff",
+]
+local_conn = LocalConnection(local_data_folders)
+local_conn.list_collections()
+
+
+

This code will parse the metadata content of each netCDF, geoTIFF or ZARR file in the provided folders and return a JSON object containing the STAC representation of the metadata. +If this code is run in a Jupyter Notebook, the metadata will be rendered nicely.

+
+

Tip

+

The code expects local files to have a similar structure to the sample files +provided at github.com/Open-EO/openeo-localprocessing-data. +If the code can not handle you special netCDF, +you can still modify the function that reads the metadata from it (openeo/local/collections.py#L19) +and the function that reads the data (openeo/local/processing.py#L26).

+
+
+
+

Local Processing

+

Let’s start with the provided sample netCDF of Sentinel-2 data:

+
>>> local_collection = "openeo-localprocessing-data/sample_netcdf/S2_L2A_sample.nc"
+>>> s2_datacube = local_conn.load_collection(local_collection)
+>>> # Check if the data is loaded correctly
+>>> s2_datacube.execute()
+<xarray.DataArray (bands: 5, t: 12, y: 705, x: 935)>
+dask.array<stack, shape=(5, 12, 705, 935), dtype=float32, chunksize=(1, 12, 705, 935), chunktype=numpy.ndarray>
+Coordinates:
+  * t        (t) datetime64[ns] 2022-06-02 2022-06-05 ... 2022-06-27 2022-06-30
+  * x        (x) float64 6.75e+05 6.75e+05 6.75e+05 ... 6.843e+05 6.843e+05
+  * y        (y) float64 5.155e+06 5.155e+06 5.155e+06 ... 5.148e+06 5.148e+06
+    crs      |S1 ...
+  * bands    (bands) object 'B04' 'B03' 'B02' 'B08' 'SCL'
+Attributes:
+    Conventions:  CF-1.9
+    institution:  openEO platform - Geotrellis backend: 0.9.5a1
+    description:
+    title:
+
+
+

As you can see in the previous example, we are using a call to execute() which will execute locally the generated openEO process graph. +In this case, the process graph consist only in a single load_collection, which performs lazy loading of the data. With this first step you can check if the data is being read correctly by openEO.

+

Looking at the metadata of this netCDF sample, we can see that it contains the bands B04, B03, B02, B08 and SCL. +Additionally, we also see that it is composed by more than one element in time and that it covers the month of June 2022.

+

We can now do a simple processing for demo purposes, let’s compute the median NDVI in time and visualize the result:

+
b04 = s2_datacube.band("B04")
+b08 = s2_datacube.band("B08")
+ndvi = (b08 - b04) / (b08 + b04)
+ndvi_median = ndvi.reduce_dimension(dimension="t", reducer="median")
+result_ndvi = ndvi_median.execute()
+result_ndvi.plot.imshow(cmap="Greens")
+
+
+../_images/local_ndvi.jpg +

We can perform the same example using data provided by STAC Collection:

+
from openeo.local import LocalConnection
+local_conn = LocalConnection("./")
+
+url = "https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a"
+spatial_extent =  {"east": 11.40, "north": 46.52, "south": 46.46, "west": 11.25}
+temporal_extent = ["2022-06-01", "2022-06-30"]
+bands = ["red", "nir"]
+properties = {"eo:cloud_cover": dict(lt=80)}
+s2_datacube = local_conn.load_stac(
+    url=url,
+    spatial_extent=spatial_extent,
+    temporal_extent=temporal_extent,
+    bands=bands,
+    properties=properties,
+)
+
+b04 = s2_datacube.band("red")
+b08 = s2_datacube.band("nir")
+ndvi = (b08 - b04) / (b08 + b04)
+ndvi_median = ndvi.reduce_dimension(dimension="time", reducer="median")
+result_ndvi = ndvi_median.execute()
+
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/sampling.html b/cookbook/sampling.html new file mode 100644 index 000000000..e726e92db --- /dev/null +++ b/cookbook/sampling.html @@ -0,0 +1,193 @@ + + + + + + + + Dataset sampling — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Dataset sampling

+

A number of use cases do not require a full datacube to be computed, +but rather want to extract a result at specific locations. +Examples include extracting training data for model calibration, or computing the result for +areas where validation data is available.

+

An important constraint is that most implementations assume that sampling is an operation +on relatively small areas, of for instance up to 512x512 pixels (but often much smaller). +When extracting larger areas, it is recommended to look into running a separate job per ‘sample’.

+

Sampling can be done for points or polygons:

+
    +
  • point extractions basically result in a ‘vector cube’, so can be exported into tabular formats.

  • +
  • polygon extractions can be stored to an individual netCDF per polygon so in this case the output is a sparse raster cube.

  • +
+

To indicate to openEO that we only want to compute the datacube for certain polygon features, we use the +openeo.rest.datacube.DataCube.filter_spatial method.

+

Next to that, we will also indicate that we want to write multiple output files. This is more convenient, as we will +want to have one or more raster outputs per sampling feature, for convenient further processing. To do this, we set +the ‘sample_by_feature’ output format property, which is available for the netCDF and GTiff output formats.

+

Combining all of this, results in the following sample code:

+
s2_bands = auth_connection.load_collection(
+    "SENTINEL2_L2A",
+    bands=["B04"],
+    temporal_extent=["2020-05-01", "2020-06-01"],
+)
+s2_bands = s2_bands.filter_spatial(
+    "https://artifactory.vgt.vito.be/testdata-public/parcels/test_10.geojson",
+)
+job = s2_bands.create_job(
+    title="Sentinel2",
+    description="Sentinel-2 L2A bands",
+    out_format="netCDF",
+    sample_by_feature=True,
+)
+
+
+

Sampling only works for batch jobs, because it results in multiple output files, which can not be conveniently transferred +in a synchronous call.

+
+

Performance & scalability

+

It’s important to note that dataset sampling is not necessarily a cheap operation, since creation of a sparse datacube still +may require accessing a large number of raw EO assets. Backends of course can and should optimize to restrict processing +to a minimum, but the size of the required input datasets is often a determining factor for cost and performance rather +than the size of the output dataset.

+
+
+

Sampling at scale

+

When doing large scale (e.g. continental) sampling, it is usually not possible or impractical to run it as a single openEO +batch job. The recommendation here is to apply a spatial grouping to your sampling locations, with a single group covering +an area of around 100x100km. The optimal size of a group may be backend dependant. Also remember that when working with +data in the UTM projection, you may want to avoid covering multiple UTM zones in a single group.

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/spectral_indices.html b/cookbook/spectral_indices.html new file mode 100644 index 000000000..335846299 --- /dev/null +++ b/cookbook/spectral_indices.html @@ -0,0 +1,450 @@ + + + + + + + + Spectral Indices — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Spectral Indices

+
+

Warning

+

This is a new experimental API, subject to change.

+
+

openeo.extra.spectral_indices is an auxiliary subpackage +to simplify the calculation of common spectral indices +used in various Earth observation applications (vegetation, water, urban etc.). +It leverages the spectral indices defined in the +Awesome Spectral Indices project +by David Montero Loaiza.

+
+

Added in version 0.9.1.

+
+
+

Band mapping

+

The formulas provided by “Awesome Spectral Indices” are defined in terms of standardized variable names +like “B” for blue, “R” for red, “N” for near-infrared, “WV” for water vapour, etc.

+
"NDVI": {
+     "formula": "(N - R)/(N + R)",
+     "long_name": "Normalized Difference Vegetation Index",
+
+
+

Obviously, these formula variables have to be mapped properly to the band names of your cube.

+
+

Automatic band mapping

+

In most simple cases, when there is enough collection metadata +to automatically detect the satellite platform (Sentinel2, Landsat8, ..) +and the original band names haven’t been renamed, +this mapping will be handled automatically, e.g.:

+
cube = connection.load_collection("SENTINEL2_L2A", ...)
+indices = compute_indices(cube, indices=["NDVI", "NDMI"])
+
+
+
+
+

Manual band mapping

+

In more complex cases, it might be necessary to specify some additional information to guide the band mapping. +If the band names follow the standard, but it’s just the satellite platform can not be guessed +from the collection metadata, it is typically enough to specify the platform explicitly:

+
indices = compute_indices(
+    cube,
+    indices=["NDVI", "NDMI"],
+    platform="SENTINEL2",
+)
+
+
+

Additionally, if the band names in your cube have been renamed, deviating from conventions, it is also +possible to explicitly specify the band name to spectral index variable name mapping:

+
indices = compute_indices(
+    cube,
+    indices=["NDVI", "NDMI"],
+    variable_map={
+        "R": "S2-red",
+        "N": "S2-nir",
+        "S1": "S2-swir",
+    },
+)
+
+
+
+

Added in version 0.26.0: Function arguments platform and variable_map to fine-tune the band mapping.

+
+
+
+
+

API

+
+
+openeo.extra.spectral_indices.append_and_rescale_indices(datacube, index_dict, *, variable_map=None, platform=None)[source]
+

Computes a list of indices from a datacube and appends them to the existing datacube

+
+
Parameters:
+
    +
  • datacube (DataCube) – input data cube

  • +
  • index_dict (dict) –

    a dictionary that contains the input- and output range of the collection on which you calculate the indices +as well as the indices that you want to calculate with their responding input- and output ranges +It follows the following format:

    +
    {
    +    "collection": {
    +        "input_range": [0,8000],
    +        "output_range": [0,250]
    +    },
    +    "indices": {
    +        "NDVI": {
    +            "input_range": [-1,1],
    +            "output_range": [0,250]
    +        },
    +    }
    +}
    +
    +
    +

    See list_indices() for supported indices.

    +

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

data cube with appended indices

+
+
+
+

Warning

+

this “rescaled” index helper uses an experimental API (e.g. index_dict argument) that is subject to change.

+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.append_index(datacube, index, *, variable_map=None, platform=None)[source]
+

Compute a single spectral index and append it to the given data cube.

+
+
Parameters:
+
    +
  • cube – input data cube

  • +
  • index (str) – name of the index to compute and append. See list_indices() for supported indices.

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

data cube with appended index

+
+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.append_indices(datacube, indices, *, variable_map=None, platform=None)[source]
+

Compute multiple spectral indices and append them to the given data cube.

+
+
Parameters:
+
    +
  • datacube (DataCube) – input data cube

  • +
  • indices (List[str]) – list of names of the indices to compute and append. See list_indices() for supported indices.

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

data cube with appended indices

+
+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.compute_and_rescale_indices(datacube, index_dict, *, append=False, variable_map=None, platform=None)[source]
+

Computes a list of indices from a data cube

+
+
Parameters:
+
    +
  • datacube (DataCube) – input data cube

  • +
  • index_dict (dict) –

    a dictionary that contains the input- and output range of the collection on which you calculate the indices +as well as the indices that you want to calculate with their responding input- and output ranges +It follows the following format:

    +
    {
    +    "collection": {
    +        "input_range": [0,8000],
    +        "output_range": [0,250]
    +    },
    +    "indices": {
    +        "NDVI": {
    +            "input_range": [-1,1],
    +            "output_range": [0,250]
    +        },
    +    }
    +}
    +
    +
    +

    If you don’t want to rescale your data, you can fill the input-, index- and output-range with None.

    +

    See list_indices() for supported indices.

    +

  • +
  • append (bool) – append the indices as bands to the given data cube +instead of creating a new cube with only the calculated indices

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

the datacube with the indices attached as bands

+
+
+
+

Warning

+

this “rescaled” index helper uses an experimental API (e.g. index_dict argument) that is subject to change.

+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.compute_index(datacube, index, *, variable_map=None, platform=None)[source]
+

Compute a single spectral index from a data cube.

+
+
Parameters:
+
    +
  • datacube (DataCube) – input data cube

  • +
  • index (str) – name of the index to compute. See list_indices() for supported indices.

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

data cube containing the index as band

+
+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.compute_indices(datacube, indices, *, append=False, variable_map=None, platform=None)[source]
+

Compute multiple spectral indices from the given data cube.

+
+
Parameters:
+
    +
  • datacube (DataCube) – input data cube

  • +
  • indices (List[str]) – list of names of the indices to compute and append. See list_indices() for supported indices.

  • +
  • append (bool) – append the indices as bands to the given data cube +instead of creating a new cube with only the calculated indices

  • +
  • variable_map (Optional[Dict[str, str]]) – (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. +To be specified if the given data cube has non-standard band names, +or the satellite platform can not be recognized from the data cube metadata. +See Manual band mapping for more information.

  • +
  • platform (Optional[str]) – optionally specify the satellite platform (to determine band name mapping) +if the given data cube has no or an unhandled collection id in its metadata. +See Manual band mapping for more information.

  • +
+
+
Return type:
+

DataCube

+
+
Returns:
+

data cube containing the indices as bands

+
+
+
+

Added in version 0.26.0: Added variable_map and platform arguments.

+
+
+ +
+
+openeo.extra.spectral_indices.list_indices()[source]
+

List names of supported spectral indices

+
+
Return type:
+

List[str]

+
+
+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/tricks.html b/cookbook/tricks.html new file mode 100644 index 000000000..2fd0769a8 --- /dev/null +++ b/cookbook/tricks.html @@ -0,0 +1,208 @@ + + + + + + + + Miscellaneous tips and tricks — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Miscellaneous tips and tricks

+
+

Export a process graph

+

You can export the underlying process graph of +a DataCube, VectorCube, etc, +to a standardized JSON format, which allows interoperability with other openEO tools.

+

For example, use print_json() to directly print the JSON representation +in your interactive Jupyter or Python session:

+
>>> dump = cube.print_json()
+{
+  "process_graph": {
+    "loadcollection1": {
+      "process_id": "load_collection",
+...
+
+
+

Or save it to a file, by getting the JSON representation first as a string +with to_json():

+
# Export as JSON string
+dump = cube.to_json()
+
+# Write to file in `pathlib` style
+export_path = pathlib.Path("path/to/export.json")
+export_path.write_text(dump, encoding="utf8")
+
+# Write to file in `open()` style
+with open("path/to/export.json", encoding="utf8") as f:
+    f.write(dump)
+
+
+
+

Warning

+

Avoid using methods like flat_graph(), +which are mainly intended for internal use. +Not only are these methods subject to change, they also lead to representations +with interoperability and reuse issues. +For example, naively printing or automatic (repr) rendering of +flat_graph() output will roughly look like JSON, +but is in fact invalid: it uses single quotes (instead of double quotes) +and booleans values are title-case (instead of lower case).

+
+
+
+

Execute a process graph directly from raw JSON

+

When you have a process graph in JSON format, as a string, a local file or a URL, +you can execute/download it without converting it do a DataCube first. +Just pass the string, path or URL directly to +Connection.download(), +Connection.execute() or +Connection.create_job(). +For example:

+
# `execute` with raw JSON string
+connection.execute("""
+    {
+        "add": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": true}
+    }
+""")
+
+# `download` with local path to JSON file
+connection.download("path/to/my-process-graph.json")
+
+# `create_job` with URL to JSON file
+job = connection.create_job("https://jsonbin.example/my/process-graph.json")
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/udp_sharing.html b/cookbook/udp_sharing.html new file mode 100644 index 000000000..9cb97eaf9 --- /dev/null +++ b/cookbook/udp_sharing.html @@ -0,0 +1,255 @@ + + + + + + + + Sharing of user-defined processes — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Sharing of user-defined processes

+
+

Warning

+

Beta feature - +At the time of this writing (July 2021), sharing of user-defined processes +(publicly or among users) is not standardized in the openEO API. +There are however some experimental sharing features in the openEO Python Client Library +and some back-end providers that we are going to discuss here.

+

Be warned that the details of this feature are subject to change. +For more status information, consult GitHub ticket +Open-EO/openeo-api#310.

+
+
+

Publicly publishing a user-defined process.

+

As discussed in Building and storing user-defined process, user-defined processes can be +stored with the save_user_defined_process() method +on a on a back-end Connection. +By default, these user-defined processes are private and only accessible by the user that saved it:

+
from openeo.processes import subtract, divide
+from openeo.api.process import Parameter
+
+# Build user-defined process
+f = Parameter.number("f", description="Degrees Fahrenheit.")
+fahrenheit_to_celsius = divide(x=subtract(x=f, y=32), y=1.8)
+
+# Store user-defined process in openEO back-end.
+udp = connection.save_user_defined_process(
+    "fahrenheit_to_celsius",
+    fahrenheit_to_celsius,
+    parameters=[f]
+)
+
+
+

Some back-ends, like the VITO/Terrascope back-end allow a user to flag a user-defined process as “public” +so that other users can access its description and metadata:

+
udp = connection.save_user_defined_process(
+    ...
+    public=True
+)
+
+
+

The sharable, public URL of this user-defined process is available from the metadata given by +RESTUserDefinedProcess.describe. +It’s listed as “canonical” link:

+
>>> udp.describe()
+{
+    "id": "fahrenheit_to_celsius",
+    "links": [
+        {
+            "rel": "canonical",
+            "href": "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius",
+            "title": "Public URL for user-defined process fahrenheit_to_celsius"
+        }
+    ],
+    ...
+
+
+
+
+

Using a public UDP through URL based “namespace”

+

Some back-ends, like the VITO/Terrascope back-end, allow to use a public UDP +through setting its public URL as the namespace property of the process graph node.

+

For example, based on the fahrenheit_to_celsius UDP created above, +the “flat graph” representation of a process graph could look like this:

+
{
+    ...
+    "to_celsius": {
+        "process_id": "fahrenheit_to_celsius",
+        "namespace": "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius",
+        "arguments": {"f": 86}
+    }
+
+
+

As a very basic illustration with the openEO Python Client library, +we can create and evaluate a process graph, +containing a fahrenheit_to_celsius call as single process, +with Connection.datacube_from_process as follows:

+
cube = connection.datacube_from_process(
+    process_id="fahrenheit_to_celsius",
+    namespace="https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius",
+    f=86
+)
+print(cube.execute())
+# Prints: 30.0
+
+
+
+
+

Loading a published user-defined process as DataCube

+

From the public URL of the user-defined process, +it is also possible for another user to construct, fully client-side, +a new DataCube +with Connection.datacube_from_json().

+

It is important to note that this approach is different from calling +a user-defined process as described in Evaluate user-defined processes and Using a public UDP through URL based “namespace”. +Connection.datacube_from_json() +breaks open the encapsulation of the user-defined process and “unrolls” the process graph inside +into a new DataCube. +This also implies that parameters defined in the user-defined process have to be provided when calling +Connection.datacube_from_json():

+
udp_url = "https://openeo.vito.be/openeo/1.0/processes/u:johndoe/fahrenheit_to_celsius"
+cube = connection.datacube_from_json(
+    udp_url,
+    parameters={"f": 86},
+)
+print(cube.execute())
+# Prints: 30.0
+
+
+

Note that Connection.datacube_from_json() +not only supports loading UDPs from an URL but also from a raw JSON string or a local file path. +For more information, also see Construct a DataCube from JSON.

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/data_access.html b/data_access.html new file mode 100644 index 000000000..31f89e46c --- /dev/null +++ b/data_access.html @@ -0,0 +1,412 @@ + + + + + + + + Finding and loading data — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Finding and loading data

+

As illustrated in the basic concepts, most openEO scripts start with load_collection, but this skips the step of +actually finding out which collection to load. This section dives a bit deeper into finding the right data, and some more +advanced data loading use cases.

+
+

Data discovery

+

To explore data in a given back-end, it is recommended to use a more visual tool like the openEO Hub +(http://hub.openeo.org/). This shows available collections, and metadata in a user-friendly manner.

+

Next to that, the client also offers various Connection methods +to explore collections and their metadata:

+ +

When using these methods inside a Jupyter notebook, you should notice that the output is rendered in a user friendly way.

+

In a regular script, these methods can be used to programmatically find a collection that matches specific criteria.

+

As a user, make sure to carefully read the documentation for a given collection, as there can be important differences. +You should also be aware of the data retention policy of a given collection: some data archives only retain the last 3 months +for instance, making them only suitable for specific types of analysis. Such differences can have an impact on the reproducibility +of your openEO scripts.

+

Also note that the openEO metadata may use links to point to much more information for a particular collection. For instance +technical specification on how the data was preprocessed, or viewers that allow you to visually explore the data. This can +drastically improve your understanding of the dataset.

+

Finally, licensing information is important to keep an eye on: not all data is free and open.

+
+

Initial exploration of an openEO collection

+

A common question from users is about very specific details of a collection, we’d like to list some examples and solutions here:

+
    +
  • The collection data type, and range of values, can be determined by simply downloading a sample of data, as NetCDF or Geotiff. This can in fact be done at any point in the design of your script, to get a good idea of intermediate results.

  • +
  • Data availability, and available timestamps can be retrieved by computing average values for your area of interest. Just construct a polygon, and retrieve those statistics. For optical data, this can also be used to get an idea on cloud statistics.

  • +
  • Most collections have a native projection system, again a simple download will give you this information if its not clear from the metadata.

  • +
+
+
+
+

Loading a data cube from a collection

+

Many examples already illustrate the basic openEO load_collection process through a Connection.load_collection() call, +with filters on space, time and bands. +For example:

+
cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    spatial_extent={"west": 3.75, "east": 4.08, "south": 51.29, "north": 51.39},
+    temporal_extent=["2021-05-07", "2021-05-14"],
+    bands=["B04", "B03", "B02"],
+)
+
+
+

The purpose of these filters in load_collection is to reduce the amount of raw data that is loaded (and processed) by the back-end. +This is essential to get a response to your processing request in reasonable time and keep processing costs low. +It’s recommended to start initial exploration with a small spatio-temporal extent +and gradually increase the scope once initial tests work out.

+

Next to specifying filters inside the load_collection process, +there are also possibilities to filter with separate filter processes, e.g. at a later stage in your process graph. +For most openEO back-ends, the following example snippet should be equivalent to the previous:

+
cube = connection.load_collection("SENTINEL2_L2A")
+cube = cube.filter_bbox(west=3.75, east=4.08, south=51.29, north=51.39)
+cube = cube.filter_temporal("2021-05-07", "2021-05-14")
+cube = cube.filter_bands(["B04", "B03", "B02"])
+
+
+

Another nice feature is that processes that work with geometries or vector features +(e.g. aggregated statistics for a polygon, or masking by polygon) +can also be used by a back-end to automatically infer an appropriate spatial extent. +This way, you do not need to explicitly set these filters yourself.

+

In the following sections, we want to dive a bit into details, and more advanced cases.

+
+
+

Filter on spatial extent

+

A spatial extent is a bounding box that specifies the minimum and and maximum longitude and latitude of the region of interest you want to process.

+

By default these latitude and longitude values are expressed in the standard Coordinate Reference System for the world, +which is EPSG:4326, also known as “WGS 84”, or just “lat-long”.

+
connection.load_collection(
+    ...,
+    spatial_extent={"west": 5.14, "south": 51.17, "east": 5.17, "north": 51.19},
+)
+
+
+
+
+

Filter on temporal extent

+

Usually you don’t need the complete time range provided by a collection +and you should specify an appropriate time window to load +as a temporal_extent pair containing a start and end date:

+
connection.load_collection(
+    ...,
+    temporal_extent=["2021-05-07", "2021-05-14"],
+)
+
+
+

In most use cases, day-level granularity is enough and you can just express the dates as strings in the format "yyyy-mm-dd". +You can also pass datetime.date objects (from Python standard library) if you already have your dates in that format.

+
+

Note

+

When you need finer, time-level granularity, you can pass datetime.datetime objects. +Or, when passed as a string, the openEO API requires date and time to be provided in RFC 3339 format. +For example for for 2020-03-17 at 12:34:56 in UTC:

+
"2020-03-17T12:34:56Z"
+
+
+
+
+

Left-closed intervals: start included, end excluded

+

Time ranges in openEO processes like load_collection and filter_temporal are handled as left-closed (“half-open”) temporal intervals: +the start instant is included in the interval, but the end instant is excluded from the interval.

+

For example, the interval defined by ["2020-03-05", "2020-03-15"] covers observations +from 2020-03-05 up to (and including) 2020-03-14 (just before midnight), +but does not include observations from 2020-03-15.

+
          2020-03-05                             2020-03-14   2022-03-15
+________|____________|_________________________|____________|____________|_____
+
+        [--------------------------------------------------(O
+    including                                           excluding
+2020-03-05 00:00:00.000                             2020-03-15 00:00:00.000
+
+
+

While this might look unintuitive at first, +working with half-open intervals avoids common and hard to discover pitfalls when combining multiple intervals, +like unintended window overlaps or double counting observations at interval borders.

+
+
+

Year/month shorthand notation

+
+

Note

+

Year/month shorthand notation handling is available since version 0.23.0.

+
+
+

Rounding down periods to dates

+

The openEO Python Client Library supports some shorthand notations for the temporal extent, +which come in handy if you work with year/month based temporal intervals. +Date strings that only consist of a year or a month will be automatically +“rounded down” to the first day of that period. For example:

+
"2023"    -> "2023-01-01"
+"2023-08" -> "2023-08-01"
+
+
+

This approach fits best with left-closed interval handling.

+

For example, the following two load_collection calls are equivalent:

+
# Filter for observations in 2021 (left-closed interval).
+connection.load_collection(temporal_extent=["2021", "2022"], ...)
+# The above is shorthand for:
+connection.load_collection(temporal_extent=["2021-01-01", "2022-01-01"], ...)
+
+
+

The same applies for filter_temporal(), +which has a couple of additional call forms. +All these calls are equivalent:

+
# Filter for March, April and May (left-closed interval)
+cube = cube.filter_temporal("2021-03", "2021-06")
+cube = cube.filter_temporal(["2021-03", "2021-06"])
+cube = cube.filter_temporal(start_date="2021-03", end_date="2021-06")
+cube = cube.filter_temporal(extent=("2021-03", "2021-06"))
+
+# The above are shorthand for:
+cube = cube.filter_temporal("2021-03-01", "2022-06-01")
+
+
+
+
+

Single string temporal extents

+

Apart from rounding down year or month string, the openEO Python Client Library provides an additional +extent handling feature in methods like +Connection.load_collection(temporal_extent=...) +and DataCube.filter_temporal(extent=...). +Normally, the extent argument should be a list or tuple containing start and end date, +but if a single string is given, representing a year, month (or day) period, +it is automatically expanded to the appropriate interval, +again following the left-closed interval principle. +For example:

+
extent="2022"        ->  extent=("2022-01-01", "2023-01-01")
+extent="2022-05"     ->  extent=("2022-05-01", "2022-06-01")
+extent="2022-05-17"  ->  extent=("2022-05-17", "2022-05-18")
+
+
+

The following snippet shows some examples of equivalent calls:

+
connection.load_collection(temporal_extent="2022", ...)
+# The above is shorthand for:
+connection.load_collection(temporal_extent=("2022-01-01", "2023-01-01"), ...)
+
+
+cube = cube.filter_temporal(extent="2021-03")
+# The above are shorthand for:
+cube = cube.filter_temporal(extent=("2021-03-01", "2022-04-01"))
+
+
+
+
+
+
+

Filter on collection properties

+

Although openEO presents data in a data cube, a lot of collections are still backed by a product based catalog. This +allows filtering on properties of that catalog.

+

A very common use case is to pre-filter Sentinel-2 products on cloud cover. +This avoids loading clouded data unnecessarily and increases performance. +Connection.load_collection() provides +a dedicated max_cloud_cover argument (shortcut for the eo:cloud_cover property) for that:

+
connection.load_collection(
+    "SENTINEL2_L2A",
+    ...,
+    max_cloud_cover=80,
+)
+
+
+

For more general cases, you can use the properties argument to filter on any collection property. +For example, to filter on the relative orbit number of SAR data:

+
connection.load_collection(
+    "SENTINEL1_GRD",
+    ...,
+    properties={
+        "relativeOrbitNumber": lambda x: x==116
+    },
+)
+
+
+

Version 0.26.0 of the openEO Python Client Library adds +collection_property() +which makes defining such property filters more user-friendly by avoiding the lambda construct:

+
import openeo
+
+connection.load_collection(
+    "SENTINEL1_GRD",
+    ...,
+    properties=[
+        openeo.collection_property("relativeOrbitNumber") == 116,
+    ],
+)
+
+
+

Note that property names follow STAC metadata conventions, but some collections can have different names.

+

Property filters in openEO are also specified by small process graphs, that allow the use of the same generic processes +defined by openEO. This is the ‘lambda’ process that you see in the property dictionary. Do note that not all processes +make sense for product filtering, and can not always be properly translated into the query language of the catalog. +Hence, some experimentation may be needed to find a filter that works.

+

One important caveat in this example is that ‘relativeOrbitNumber’ is a catalog specific property name. Meaning that +different archives may choose a different name for a given property, and the properties that are available can depend +on the collection and the catalog that is used by it. This is not a problem caused by openEO, but by the limited +standardization between catalogs of EO data.

+
+
+

Handling large vector data sets

+

For simple use cases, it is common to directly embed geometries (vector data) in your openEO process graph. +Unfortunately, with large vector data sets this leads to very large process graphs +and you might hit certain limits, +resulting in HTTP errors like 413 Request Entity Too Large or 413 Payload Too Large.

+

This problem can be circumvented by first uploading your vector data to a file sharing service +(like Google Drive, DropBox, GitHub, …) +and use its public URL in the process graph instead +through Connection.vectorcube_from_paths. +For example, as follows:

+
# Load vector data from URL
+url = "https://github.com/Open-EO/openeo-python-client/raw/master/tests/data/example_aoi.pq"
+parcels = connection.vectorcube_from_paths([url], format="parquet")
+
+# Use the parcel vector data, for example to do aggregation.
+cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    bands=["B04", "B03", "B02"],
+    temporal_extent=["2021-05-12", "2021-06-01"],
+)
+aggregations = cube.aggregate_spatial(
+    geometries=parcels,
+    reducer="mean",
+)
+
+
+

Note that while openEO back-ends typically support multiple vector formats, like GeoJSON and GeoParquet, +it is usually recommended to use a compact format like GeoParquet, instead of GeoJSON. The list of supported formats +is also advertised by the backend, and can be queried with +Connection.list_file_formats.

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/datacube_construction.html b/datacube_construction.html new file mode 100644 index 000000000..6ebb03c60 --- /dev/null +++ b/datacube_construction.html @@ -0,0 +1,344 @@ + + + + + + + + DataCube construction — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

DataCube construction

+
+

The load_collection process

+

The most straightforward way to start building your openEO data cube is through the load_collection process. +As mentioned earlier, this is provided by the +load_collection() method +on a Connection object, +which produces a DataCube instance. +For example:

+
cube = connection.load_collection("SENTINEL2_TOC")
+
+
+

While this should cover the majority of use cases, +there some cases +where one wants to build a DataCube object +from something else or something more than just a simple load_collection process.

+
+
+

Construct DataCube from process

+

Through user-defined processes one can encapsulate +one or more load_collection processes and additional processing steps in a single +reusable user-defined process. +For example, imagine a user-defined process “masked_s2” +that loads an openEO collection “SENTINEL2_TOC” and applies some kind of cloud masking. +The implementation details of the cloud masking are not important here, +but let’s assume there is a parameter “dilation” to fine-tune the cloud mask. +Also note that the collection id “SENTINEL2_TOC” is hardcoded in the user-defined process.

+

We can now construct a data cube from this user-defined process +with datacube_from_process() +as follows:

+
cube = connection.datacube_from_process("masked_s2", dilation=10)
+
+# Further processing of the cube:
+cube = cube.filter_temporal("2020-09-01", "2020-09-10")
+
+
+

Note that datacube_from_process() can be +used with all kind of processes, not only user-defined processes. +For example, while this is not exactly a real EO data use case, +it will produce a valid openEO process graph that can be executed:

+
>>> cube = connection.datacube_from_process("mean", data=[2, 3, 5, 8])
+>>> cube.execute()
+4.5
+
+
+
+
+

Construct a DataCube from JSON

+

openEO process graphs are typically stored and published in JSON format. +Most notably, user-defined processes are transferred between openEO client +and back-end in a JSON structure roughly like in this example:

+
{
+  "id": "evi",
+  "parameters": [
+    {"name": "red", "schema": {"type": "number"}},
+    {"name": "blue", "schema": {"type": "number"}},
+    ...
+  ],
+  "process_graph": {
+    "sub": {"process_id": "subtract", "arguments": {"x": {"from_parameter": "nir"}, "y": {"from_parameter": "red"}}},
+    "p1": {"process_id": "multiply", "arguments": {"x": 6, "y": {"from_parameter": "red"}}},
+    "div": {"process_id": "divide", "arguments": {"x": {"from_node": "sub"}, "y": {"from_node": "sum"}},
+    ...
+
+
+

It is possible to construct a DataCube object that corresponds with this +process graph with the Connection.datacube_from_json method. +It can be given one of:

+
+
    +
  • a raw JSON string,

  • +
  • a path to a local JSON file,

  • +
  • an URL that points to a JSON resource

  • +
+
+

The JSON structure should be one of:

+
+
    +
  • a mapping (dictionary) like the example above with at least a "process_graph" item, +and optionally a "parameters" item.

  • +
  • a mapping (dictionary) with {"process_id": ...} items

  • +
+
+
+

Some examples

+

Load a DataCube from a raw JSON string, containing a +simple “flat graph” representation:

+
raw_json = '''{
+    "lc": {"process_id": "load_collection", "arguments": {"id": "SENTINEL2_TOC"}},
+    "ak": {"process_id": "apply_kernel", "arguments": {"data": {"from_node": "lc"}, "kernel": [[1,2,1],[2,5,2],[1,2,1]]}, "result": true}
+}'''
+cube = connection.datacube_from_json(raw_json)
+
+
+

Load from a raw JSON string, containing a mapping with “process_graph” and “parameters”:

+
raw_json = '''{
+    "parameters": [
+        {"name": "kernel", "schema": {"type": "array"}, "default": [[1,2,1], [2,5,2], [1,2,1]]}
+    ],
+    "process_graph": {
+        "lc": {"process_id": "load_collection", "arguments": {"id": "SENTINEL2_TOC"}},
+        "ak": {"process_id": "apply_kernel", "arguments": {"data": {"from_node": "lc"}, "kernel": {"from_parameter": "kernel"}}, "result": true}
+    }
+}'''
+cube = connection.datacube_from_json(raw_json)
+
+
+

Load directly from a local file or URL containing these kind of JSON representations:

+
# Local file
+cube = connection.datacube_from_json("path/to/my_udp.json")
+
+# URL
+cube = connection.datacube_from_json("https://example.com/my_udp.json")
+
+
+
+
+

Parameterization

+

When the process graph uses parameters, you must specify the desired parameter values +at the time of calling Connection.datacube_from_json.

+

For example, take this simple toy example of a process graph that takes the sum of 5 and a parameter “increment”:

+
raw_json = '''{"add": {
+    "process_id": "add",
+    "arguments": {"x": 5, "y": {"from_parameter": "increment"}},
+    "result": true
+}}'''
+
+
+

Trying to build a DataCube from it without specifying parameter values will fail +like this:

+
>>> cube = connection.datacube_from_json(raw_json)
+ProcessGraphVisitException: No substitution value for parameter 'increment'.
+
+
+

Instead, specify the parameter value:

+
>>> cube = connection.datacube_from_json(
+...    raw_json,
+...    parameters={"increment": 4},
+... )
+>>> cube.execute()
+9
+
+
+

Parameters can also be defined with default values, which will be used when they are not specified +in the Connection.datacube_from_json call:

+
raw_json = '''{
+    "parameters": [
+        {"name": "increment", "schema": {"type": "number"}, "default": 100}
+    ],
+    "process_graph": {
+        "add": {"process_id": "add", "arguments": {"x": 5, "y": {"from_parameter": "increment"}}, "result": true}
+    }
+}'''
+
+cube = connection.datacube_from_json(raw_json)
+result = cube.execute())
+# result will be 105
+
+
+
+

Re-parameterization

+

TODO

+
+
+
+
+

Building process graphs with multiple result nodes

+
+

Note

+

Multi-result support is added in version 0.35.0

+
+

Most openEO use cases are just about building a single result data cube, +which is readily covered in the openEO Python client library through classes like +DataCube and VectorCube. +It is straightforward to create a batch job from these, or execute/download them synchronously.

+

The openEO API also allows multiple result nodes in a single process graph, +for example to persist intermediate results or produce results in different output formats. +To support this, the openEO Python client library provides the MultiResult class, +which allows to group multiple DataCube and VectorCube objects +in a single entity that can be used to create or run batch jobs. For example:

+
from openeo import MultiResult
+
+cube1 = ...
+cube2 = ...
+multi_result = MultiResult([cube1, cube2])
+job = multi_result.create_job()
+
+
+

Moreover, it is not necessary to explicitly create such a +MultiResult object, +as the Connection.create_job() method +directly supports passing multiple data cube objects in a list, +which will be automatically grouped as a multi-result:

+
cube1 = ...
+cube2 = ...
+job = connection.create_job([cube1, cube2])
+
+
+
+

Important

+

Only a single Connection can be in play +when grouping multiple results like this. +As everything is to be merged in a single process graph +to be sent to a single backend, +it is not possible to mix cubes created from different connections.

+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/development.html b/development.html new file mode 100644 index 000000000..6a5d86a3e --- /dev/null +++ b/development.html @@ -0,0 +1,519 @@ + + + + + + + + Development and maintenance — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Development and maintenance

+

For development on the openeo package itself, +it is recommended to install a local git checkout of the project +in development mode (-e) +with additional development related dependencies ([dev]) +like this:

+
pip install -e .[dev]
+
+
+

If you are on Windows and experience problems installing this way, you can find some solutions in section Development Installation on Windows.

+
+

Running the unit tests

+

The test suite of the openEO Python Client leverages +the nice pytest framework. +It is installed automatically when installing the openEO Python Client +with the [dev] extra as shown above. +Running the whole tests is as simple as executing:

+
pytest
+
+
+

There are a ton of command line options for fine-tuning +(e.g. select a subset of tests, how results should be reported, …). +Run pytest -h for a quick overview +or check the pytest documentation for more information.

+

For example:

+
# Skip tests that are marked as slow
+pytest -m "not slow"
+
+
+
+
+

Building the documentation

+

Building the documentation requires Sphinx +and some plugins +(which are installed automatically as part of the [dev] install).

+
+

Quick and easy

+

The easiest way to build the documentation is working from the docs folder +and using the Makefile:

+
# From `docs` folder
+make html
+
+
+

(assumes you have make available, if not: use python -msphinx -M html .  _build.)

+

This will generate the docs in HTML format under docs/_build/html/. +Open the HTML files manually, +or use Python’s built-in web server to host them locally, e.g.:

+
# From `docs` folder
+python -m http.server 8000
+
+
+

Then, visit http://127.0.0.1:8000/_build/html/ in your browser

+
+
+

Like a Pro

+

When doing larger documentation work, it can be tedious to manually rebuild the docs +and refresh your browser to check the result. +Instead, use sphinx-autobuild +to automatically rebuild on documentation changes and live-reload it in your browser. +After installation (pip install sphinx-autobuild in your development environment), +just run

+
# From project root
+sphinx-autobuild docs/ --watch openeo/ docs/_build/html/
+
+
+

and then visit http://127.0.0.1:8000 . +When you change (and save) documentation source files, your browser should now +automatically refresh and show the newly built docs. Just like magic.

+
+
+
+

Contributing code

+

User contributions (such as bug fixes and new features, both in source code and documentation) +are greatly appreciated and welcome.

+
+

Pull requests

+

We use a traditional GitHub Pull Request (PR) workflow +for user contributions, which roughly follows these steps:

+
    +
  • Create a personal fork of https://github.com/Open-EO/openeo-python-client +(unless you already have push permissions to an existing fork or the original repo)

  • +
  • Preferably: work on your contribution in a new feature branch

  • +
  • Push your feature branch to your fork and create a pull request

  • +
  • The pull request is the place for review, discussion and fine-tuning of your work

  • +
  • Once your pull request is in good shape it will be merged by a maintainer

  • +
+
+
+

Pre-commit for basic code quality checks

+

We started using the pre-commit tool +for basic fine-tuning of code style and quality in new contributions. +It’s currently not enforced, but enabling pre-commit is recommended and appreciated +when contributing code.

+
+

Note

+

Note that the whole repository does not fully follow all code styles rules at the moment. +We’re just gradually introducing it, piggybacking on new contributions and commits.

+
+
+

Pre-commit set up

+
    +
  • Install the general pre-commit command line tool:

    +
      +
    • The simplest option is to install it directly in the virtual environment +you are using for openEO Python client development (e.g. pip install pre-commit).

    • +
    • You can also install it globally on your system +(e.g. using pipx, conda, homebrew, …) +so you can use it across different projects.

    • +
    +
  • +
  • Install the project specific git hook scripts by running this in the root of your local git clone:

    +
    pre-commit install
    +
    +
    +

    This will automatically install additional scripts and tools in a sandbox +to run the various checks defined in the project’s .pre-commit-config.yaml configuration file.

    +
  • +
+
+
+

Pre-commit usage

+

When you commit new changes, the freshly installed pre-commit hook +will now automatically run each of the configured linters/formatters/… +Some of these just flag issues (e.g. invalid JSON files) +while others even automatically fix problems (e.g. clean up excessive whitespace).

+

If there is some kind of violation, the commit will be blocked. +Address these problems and try to commit again.

+
+

Attention

+

Some pre-commit tools directly edit your files (e.g. formatting tweaks) +instead of just flagging issues. +This might feel intrusive at first, but once you get the hang of it, +it should allow to streamline your workflow.

+

In particular, it is recommended to use the staging feature of git to prepare your commit. +Pre-commit’s proposed changes are not staged automatically, +so you can more easily keep them separate and review.

+
+
+

Tip

+

You can temporarily disable pre-commit for these rare cases +where you intentionally want to commit violating code style, +e.g. through git commit command line option -n/--no-verify.

+
+
+
+
+
+

Creating a release

+

This section describes the procedure to create +properly versioned releases of the openeo package +that can be downloaded by end users (e.g. through pip from pypi.org) +and depended on by other projects.

+

The releases will end up on:

+ +
+

Prerequisites

+
    +
  • You have permissions to push branches and tags and maintain releases on +the openeo-python-client project on GitHub.

  • +
  • You have permissions to upload releases to the +openeo project on pypi.org

  • +
  • The Python virtual environment you work in has the latest versions +of the twine package installed. +If you plan to build the wheel yourself (instead of letting GitHub or Jenkins do this), +you also need recent enough versions of the setuptools and wheel packages.

  • +
+
+
+

Important files

+
+
setup.py

describes the metadata of the package, +like package name openeo and version +(which is extracted from openeo/_version.py).

+
+
openeo/_version.py

defines the version of the package. +During general development, this version string should contain +a pre-release +segment (e.g. a1 for alpha releases, b1 for beta releases, etc) +to avoid collision with final releases. For example:

+
__version__ = '0.8.0a1'
+
+
+

As discussed below, this pre-release suffix should +only be removed during the release procedure +and restored when bumping the version after the release procedure.

+
+
CHANGELOG.md

keeps track of important changes associated with each release. +It follows the Keep a Changelog convention +and should be properly updated with each bug fix, feature addition/removal, … +under the Unreleased section during development.

+
+
+
+
+

Procedure

+

These are the steps to create and publish a new release of the openeo package. +To avoid the confusion with ad-hoc injection of some abstract version placeholder +that has to be replaced properly, +we will use a concrete version 0.8.0 in the examples below.

+
    +
  1. Make sure you are working on latest master branch, +without uncommitted changes and all tests are properly passing.

  2. +
  3. Create release commit:

    +
      +
    1. Drop the pre-release suffix from the version string in openeo/_version.py +so that it just a “final” semantic versioning string, e.g. 0.8.0

    2. +
    3. Update CHANGELOG.md: rename the “Unreleased” section title +to contain version and date, e.g.:

      +
      ## [0.8.0] - 2020-12-15
      +
      +
      +

      remove empty subsections +and start a new “Unreleased” section above it, like:

      +
      ## [Unreleased]
      +
      +### Added
      +
      +### Changed
      +
      +### Removed
      +
      +### Fixed
      +
      +
      +
    4. +
    5. Commit these changes in git with a commit message like Release 0.8.0 +and push to GitHub:

      +
      git add openeo/_version.py CHANGELOG.md
      +git commit -m 'Release 0.8.0'
      +git push origin master
      +
      +
      +
    6. +
    +
  4. +
  5. Optional, but recommended: wait for VITO Jenkins to build this updated master +(trigger it manually if necessary), +so that a build of a final, non-alpha release 0.8.0 +is properly uploaded to VITO artifactory.

  6. +
  7. Create release on PyPI:

    +
      +
    1. Obtain a wheel archive of the package, with one of these approaches:

      +
        +
      • Preferably, the path of least surprise: build wheel through GitHub Actions. +Go to workflow “Build wheel”, +manually trigger a build with “Run workflow” button, wait for it to finish successfully, +download generated artifact.zip, and finally: unzip it to obtain openeo-0.8.0-py3-none-any.whl

      • +
      • Or, if you know what you are doing and you’re sure you have a clean +local checkout, you can also build it locally:

        +
        python setup.py bdist_wheel
        +
        +
        +

        This should create dist/openeo-0.8.0-py3-none-any.whl

        +
      • +
      +
    2. +
    3. Upload this wheel to openeo project on PyPI:

      +
      python -m twine upload openeo-0.8.0-py3-none-any.whl
      +
      +
      +

      Check the release history on PyPI +to verify the twine upload. +Another way to verify that the freshly created release installs +is using docker to do a quick install-and-burn, +for example as follows (check the installed version in pip’s output):

      +
      docker run --rm -it python python -m pip install --no-deps openeo
      +
      +
      +
    4. +
    +
  8. +
  9. Create a git version tag and push it to GitHub:

    +
    git tag v0.8.0
    +git push origin v0.8.0
    +
    +
    +
  10. +
  11. Create a release in GitHub: +Go to https://github.com/Open-EO/openeo-python-client/releases/new, +Enter v0.8.0 under “tag”, +enter title: openEO Python Client v0.8.0, +use the corresponding CHANGELOG.md section as description +and publish it +(no need to attach binaries).

  12. +
  13. Bump the version in openeo/_version.py, (usually the “minor” level) +and append a pre-release “a1” suffix again, for example:

    +
    __version__ = '0.9.0a1'
    +
    +
    +

    Commit this (e.g. with message _version.py: bump to 0.9.0a1) +and push to GitHub.

    +
  14. +
  15. Update conda-forge package too +(requires conda recipe maintainer role). +Normally, the “regro-cf-autotick-bot” will create a pull request. +If it builds fine, merge it. +If not, fix the issue +(typically in recipe/meta.yaml) +and merge.

  16. +
  17. Optionally: make a post about the new release +on the openEO Platform Forum +or the CDSE Forum.

  18. +
+
+

Verification

+

The new release should now be available/listed at:

+ +

Here is a bash (subshell) oneliner to verify that the PyPI release works properly:

+
(
+    cd /tmp &&\
+    python -m venv venv-openeo &&\
+    source venv-openeo/bin/activate &&\
+    pip install -U openeo &&\
+    python -c "import openeo;print(openeo);print(openeo.__version__)"
+)
+
+
+

It tries to install the latest version of the openeo package in a temporary virtual env, +import it and print the package version.

+
+
+
+
+

Development Installation on Windows

+

Normally you can install the client the same way on Windows as on Linux, like so:

+
pip install -e .[dev]
+
+
+
+

Alternative development installation

+

The standard pure-pip based installation should work with the most recent code. +However, in the past we sometimes had issues with this procedure. +Should you experience problems, consider using an alternative conda-based installation procedure:

+
    +
  1. Create and activate a new conda environment for developing the openeo-python-client. +For example:

    +
    conda create -n openeopyclient
    +conda activate openeopyclient
    +
    +
    +
  2. +
  3. In that conda environment, install only the dependencies of openeo via conda, +but not the openeo package itself.

    +
    # Install openeo dependencies (from the conda-forge channel)
    +conda install --only-deps -c conda-forge openeo
    +
    +
    +
  4. +
  5. Do a pip install from the project root in editable mode (pip -e):

    +
    pip install -e .[dev]
    +
    +
    +
  6. +
+
+
+
+

Update of generated files

+

Some parts of the openEO Python Client Library source code are +generated/compiled from upstream sources (e.g. official openEO specifications). +Because updates are not often required, +it’s just a semi-manual procedure (to run from the project root):

+
# Update the sub-repositories (like git submodules, but optional)
+python specs/update-subrepos.py
+
+# Update `openeo/processes.py` from specifications in openeo-processes repository
+python openeo/internal/processes/generator.py  specs/openeo-processes specs/openeo-processes/proposals --output openeo/processes.py
+
+# Update the openEO process mapping documentation page
+python docs/process_mapping.py > docs/process_mapping.rst
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 000000000..01190d392 --- /dev/null +++ b/genindex.html @@ -0,0 +1,1796 @@ + + + + + + + Index — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | F + | G + | H + | I + | J + | L + | M + | N + | O + | P + | Q + | R + | S + | T + | U + | V + | W + | X + +
+

_

+ + + +
+ +

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + +
+ +

I

+ + + +
+ +

J

+ + + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + +
+ +

X

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..bd10d563f --- /dev/null +++ b/index.html @@ -0,0 +1,378 @@ + + + + + + + + openEO Python Client — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

openEO Python Client

+https://img.shields.io/badge/Status-Stable-yellow.svg +

Welcome to the documentation of openeo, +the official Python client library for interacting with openEO back-ends +to process remote sensing and Earth observation data. +It provides a Pythonic interface for the openEO API, +supporting data/process discovery, process graph building, +batch job management and much more.

+
+

Usage example

+

A simple example, to give a feel of using this library:

+
import openeo
+
+# Connect to openEO back-end.
+connection = openeo.connect("openeo.vito.be").authenticate_oidc()
+
+# Load data cube from TERRASCOPE_S2_NDVI_V2 collection.
+cube = connection.load_collection(
+    "TERRASCOPE_S2_NDVI_V2",
+    spatial_extent={"west": 5.05, "south": 51.21, "east": 5.1, "north": 51.23},
+    temporal_extent=["2022-05-01", "2022-05-30"],
+    bands=["NDVI_10M"],
+)
+# Rescale digital number to physical values and take temporal maximum.
+cube = cube.apply(lambda x: 0.004 * x - 0.08).max_time()
+
+cube.download("ndvi-max.tiff")
+
+
+_images/welcome.png +
+
+

Table of contents

+
+ +
+
+
+

Indices and tables

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/installation.html b/installation.html new file mode 100644 index 000000000..8362ec574 --- /dev/null +++ b/installation.html @@ -0,0 +1,230 @@ + + + + + + + + Installation — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Installation

+

It is an explicit goal of the openEO Python client library to be as easy to install as possible, +unlocking the openEO ecosystem to a broad audience. +The package is a pure Python implementation and its dependencies are carefully considered (in number and complexity).

+
+

Basic install

+

It is recommended to work in a some kind of virtual environment (venv, conda, …) +to avoid polluting the base install of Python on your operating system +or introducing conflicts with other applications. +How you organize your virtual environments heavily depends on your use case and workflow, +and is out of scope of this documentation.

+
+

Installation with pip

+

The openEO Python client library is available from PyPI +and can be easily installed with a tool like pip, for example:

+
$ pip install openeo
+
+
+

To upgrade the package to the latest release:

+
$ pip install --upgrade openeo
+
+
+
+
+

Installation with Conda

+

The openEO Python client library is available on conda-forge +and can be easily installed in a conda environment, for example:

+
$ conda install -c conda-forge openeo
+
+
+
+
+

Verifying and troubleshooting

+

You can check if the installation worked properly +by trying to import the openeo package in a Python script, interactive shell or notebook:

+
import openeo
+
+print(openeo.client_version())
+
+
+

This should print the installed version of the openeo package.

+

If the first line gives an error like ModuleNotFoundError: No module named 'openeo', +some troubleshooting tips:

+
    +
  • Restart you Python shell or notebook (or start a fresh one).

  • +
  • Double check that the installation went well, +e.g. try re-installing and keep an eye out for error/warning messages.

  • +
  • Make sure that you are working in the same (virtual) environment you installed the package in.

  • +
+

If you still have troubles installing and importing openeo, +feel free to reach out in the community forum +or the project’s issue tracker. +Try to describe your setup in enough detail: your operating system, +which virtual environment system you use, +the installation tool (pip, conda or something else), …

+
+
+
+

Optional dependencies

+

Depending on your use case, you might also want to install some additional libraries. +For example:

+
    +
  • netCDF4 or h5netcdf for loading and writing NetCDF files (e.g. integrated in xarray.load_dataset())

  • +
  • matplotlib for visualisation (e.g. integrated plot functionality in xarray )

  • +
  • pyarrow for (read/write) support of Parquet files +(e.g. with MultiBackendJobManager)

  • +
  • rioxarray for GeoTIFF support in the assert helpers from openeo.testing.results

  • +
  • geopandas for working with dataframes with geospatial support, +(e.g. with MultiBackendJobManager)

  • +
+
+

Enabling additional features

+

To use the on-demand preview feature and other Jupyter-enabled features, you need to install the necessary dependencies.

+
$ pip install openeo[jupyter]
+
+
+
+
+
+

Source or development install

+

If you closely track the development of the openeo package at +github.com/Open-EO/openeo-python-client +and want to work with unreleased features or contribute to the development of the package, +you can install it as follows from the root of a git source checkout:

+
$ pip install -e .[dev]
+
+
+

The -e option enables “development mode”, which makes sure that changes you make to the source code +happen directly on the installed package, so that you don’t have to re-install the package each time +you make a change.

+

The [dev] (a so-called “extra”) installs additional development related dependencies, +for example to run the unit tests.

+

You can also find more information about installation for development on the Development and maintenance page.

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/lib/openeo/__init__.py b/lib/openeo/__init__.py new file mode 100644 index 000000000..782843f8f --- /dev/null +++ b/lib/openeo/__init__.py @@ -0,0 +1,29 @@ +""" + + +""" + +__title__ = 'openeo' +__author__ = 'Jeroen Dries' + + +class BaseOpenEoException(Exception): + pass + + +import importlib.metadata + +from openeo._version import __version__ +from openeo.rest.connection import Connection, connect, session +from openeo.rest.datacube import UDF, DataCube +from openeo.rest.graph_building import collection_property +from openeo.rest.job import BatchJob, RESTJob +from openeo.rest.multiresult import MultiResult +from openeo.rest.vectorcube import VectorCube + + +def client_version() -> str: + try: + return importlib.metadata.version("openeo") + except importlib.metadata.PackageNotFoundError: + return __version__ diff --git a/lib/openeo/_version.py b/lib/openeo/_version.py new file mode 100644 index 000000000..36e72e148 --- /dev/null +++ b/lib/openeo/_version.py @@ -0,0 +1 @@ +__version__ = "0.35.0a1" diff --git a/lib/openeo/api/__init__.py b/lib/openeo/api/__init__.py new file mode 100644 index 000000000..88cc8b8b5 --- /dev/null +++ b/lib/openeo/api/__init__.py @@ -0,0 +1,3 @@ +""" +Wrappers for openEO API concepts. +""" diff --git a/lib/openeo/api/logs.py b/lib/openeo/api/logs.py new file mode 100644 index 000000000..5a7ae02d5 --- /dev/null +++ b/lib/openeo/api/logs.py @@ -0,0 +1,99 @@ +import logging +from typing import Optional, Union + + +class LogEntry(dict): + """ + Log message and info for jobs and services + + Fields: + - ``id``: Unique ID for the log, string, REQUIRED + - ``code``: Error code, string, optional + - ``level``: Severity level, string (error, warning, info or debug), REQUIRED + - ``message``: Error message, string, REQUIRED + - ``time``: Date and time of the error event as RFC3339 date-time, string, available since API 1.1.0 + - ``path``: A "stack trace" for the process, array of dicts + - ``links``: Related links, array of dicts + - ``usage``: Usage metrics available as property 'usage', dict, available since API 1.1.0 + May contain the following metrics: cpu, memory, duration, network, disk, storage and other custom ones + Each of the metrics is also a dict with the following parts: value (numeric) and unit (string) + - ``data``: Arbitrary data the user wants to "log" for debugging purposes. + Please note that this property may not exist as there's a difference + between None and non-existing. None for example refers to no-data in + many cases while the absence of the property means that the user did + not provide any data for debugging. + """ + + _required = {"id", "level", "message"} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Check required fields + missing = self._required.difference(self.keys()) + if missing: + raise ValueError("Missing required fields: {m}".format(m=sorted(missing))) + + @property + def id(self): + return self["id"] + + # Legacy alias + log_id = id + + @property + def message(self): + return self["message"] + + @property + def level(self): + return self["level"] + + # TODO: add properties for "code", "time", "path", "links" and "data" with sensible defaults? + + +def normalize_log_level( + log_level: Union[int, str, None], default: int = logging.DEBUG +) -> int: + """ + Helper function to convert a openEO API log level (e.g. string "error") + to the integer constants defined in Python's standard library ``logging`` module (e.g. ``logging.ERROR``). + + :param log_level: log level to normalize: a log level string in the style of + the openEO API ("error", "warning", "info", or "debug"), + an integer value (e.g. a ``logging`` constant), or ``None``. + + :param default: fallback log level to return on unknown log level strings or ``None`` input. + + :raises TypeError: when log_level is any other type than str, an int or None. + :return: One of the following log level constants from the standard module ``logging``: + ``logging.ERROR``, ``logging.WARNING``, ``logging.INFO``, or ``logging.DEBUG`` . + """ + if isinstance(log_level, str): + log_level = log_level.upper() + if log_level in ["CRITICAL", "ERROR", "FATAL"]: + return logging.ERROR + elif log_level in ["WARNING", "WARN"]: + return logging.WARNING + elif log_level == "INFO": + return logging.INFO + elif log_level == "DEBUG": + return logging.DEBUG + else: + return default + elif isinstance(log_level, int): + return log_level + elif log_level is None: + return default + else: + raise TypeError( + f"Value for log_level is not an int or str: type={type(log_level)}, value={log_level!r}" + ) + + +def log_level_name(log_level: Union[int, str, None]) -> str: + """ + Get the name of a normalized log level. + This value conforms to log level names used in the openEO API. + """ + return logging.getLevelName(normalize_log_level(log_level)).lower() diff --git a/lib/openeo/api/process.py b/lib/openeo/api/process.py new file mode 100644 index 000000000..a00f2f9c2 --- /dev/null +++ b/lib/openeo/api/process.py @@ -0,0 +1,443 @@ +from __future__ import annotations + +import warnings +from typing import List, Optional, Union + + +class Parameter: + """ + A (process) parameter to build parameterized + :ref:`user-defined processes`. + + Parameter objects can be :ref:`defined ` + with at least a name and expected schema + (e.g. is the parameter a placeholder for a string, a bounding box, a date, ...) + and can then be :ref:`used ` + with various functions and classes, + like :py:class:`~openeo.rest.datacube.DataCube`, + to build parameterized user-defined processes. + + Apart from the generic :py:class:`Parameter` constructor, + this class also provides various helpers (class methods) + to easily create parameters for common parameter types. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param schema: JSON schema describing the expected data type and structure of the parameter. + :param default: default value for the parameter when it's optional. + :param optional: toggle to indicate whether the parameter is optional or required. + """ + # TODO unify with openeo.internal.processes.parse.Parameter? + __slots__ = ("name", "description", "schema", "default", "optional") + + _DEFAULT_UNDEFINED = object() + + def __init__( + self, + name: str, + description: Optional[str] = None, + schema: Union[list, dict, str, None] = None, + default=_DEFAULT_UNDEFINED, + optional: Optional[bool] = None, + ): + self.name = name + if description is None: + # Description is required in openEO API, we are a bit more permissive here. + warnings.warn("Parameter without description: using name as description.") + description = name + self.description = description + self.schema = {"type": schema} if isinstance(schema, str) else (schema or {}) + # TODO: automatically set `optional` when `default` is set? + self.default = default + self.optional = optional + + def to_dict(self) -> dict: + """ + Convert to dictionary for JSON-serialization. + """ + d = {"name": self.name, "description": self.description, "schema": self.schema} + if self.optional is not None: + d["optional"] = self.optional + if self.default is not self._DEFAULT_UNDEFINED: + d["default"] = self.default + d["optional"] = True + return d + + @classmethod + def raster_cube(cls, name: str = "data", description: str = "A data cube.", **kwargs) -> Parameter: + """ + Helper to easily create a 'raster-cube' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + schema = {"type": "object", "subtype": "raster-cube"} + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def datacube(cls, name: str = "data", description: str = "A data cube.", **kwargs) -> Parameter: + """ + Helper to easily create a 'datacube' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.22.0 + """ + schema = {"type": "object", "subtype": "datacube"} + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def string( + cls, + name: str, + description: Optional[str] = None, + *, + values: Optional[List[str]] = None, + subtype: Optional[str] = None, + format: Optional[str] = None, + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'string' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param values: Optional list of allowed string values to make this an "enum". + :param subtype: Optional subtype of the 'string' schema. + :param format: Optional format of the 'string' schema. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + schema = {"type": "string"} + if values is not None: + schema["enum"] = values + if subtype: + schema["subtype"] = subtype + if format: + schema["format"] = format + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def integer(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to create an 'integer' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "integer"}, **kwargs) + + @classmethod + def number(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to easily create a 'number' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "number"}, **kwargs) + + @classmethod + def boolean(cls, name: str, description: Optional[str] = None, **kwargs) -> Parameter: + """ + Helper to easily create a 'boolean' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + """ + return cls(name=name, description=description, schema={"type": "boolean"}, **kwargs) + + @classmethod + def array( + cls, + name: str, + description: Optional[str] = None, + *, + item_schema: Optional[Union[str, dict]] = None, + **kwargs, + ) -> Parameter: + """ + Helper to easily create parameter with an 'array' schema. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param item_schema: Schema of the array items given in JSON Schema style, e.g. ``{"type": "string"}``. + Simple schemas can also be specified as single string: + e.g. ``"string"`` will be expanded to ``{"type": "string"}``. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionchanged:: 0.23.0 + Added ``item_schema`` argument. + """ + schema = {"type": "array"} + if item_schema: + if isinstance(item_schema, str): + item_schema = {"type": item_schema} + schema["items"] = item_schema + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def object( + cls, name: str, description: Optional[str] = None, *, subtype: Optional[str] = None, **kwargs + ) -> Parameter: + """ + Helper to create an 'object' type parameter + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + :param subtype: subtype of the 'object' schema + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.26.0 + """ + schema = {"type": "object"} + if subtype: + schema["subtype"] = subtype + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def bounding_box( + cls, + name: str, + description: str = "Spatial extent specified as a bounding box with 'west', 'south', 'east' and 'north' fields.", + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'bounding box' parameter, which allows to specify a spatial extent + with "west", "south", "east" and "north" bounds (and optionally a CRS identifier). + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = { + "type": "object", + "subtype": "bounding-box", + "required": ["west", "south", "east", "north"], + "properties": { + "west": { + "type": "number", + "description": "West (lower left corner, coordinate axis 1).", + }, + "south": { + "type": "number", + "description": "South (lower left corner, coordinate axis 2).", + }, + "east": { + "type": "number", + "description": "East (upper right corner, coordinate axis 1).", + }, + "north": { + "type": "number", + "description": "North (upper right corner, coordinate axis 2).", + }, + "crs": { + "description": "Coordinate reference system of the extent, specified as as [EPSG code](http://www.epsg-registry.org/) or [WKT2 CRS string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). Defaults to `4326` (EPSG code 4326) unless the client explicitly requests a different coordinate reference system.", + "anyOf": [ + { + "type": "integer", + "subtype": "epsg-code", + "title": "EPSG Code", + "minimum": 1000, + }, + { + "type": "string", + "subtype": "wkt2-definition", + "title": "WKT2 definition", + }, + ], + "default": 4326, + }, + # TODO: support base and height? + }, + } + return cls(name=name, description=description, schema=schema, **kwargs) + + _spatial_extent_description = """Limits the data to process to the specified bounding box or polygons. + +For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). +For vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided. + +Empty geometries are ignored. +Set this parameter to null to set no limit for the spatial extent. """ + + @classmethod + def spatial_extent( + cls, + name: str = "spatial_extent", + description: str = _spatial_extent_description, + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'spatial_extent' parameter, which is compatible with the 'load_collection' argument of + the same name. This allows to conveniently create user-defined processes that can be applied to a bounding box and vector data + for spatial filtering. It is also possible for users to set to null, and define spatial filtering using other processes. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.32.0 + """ + schema = [ + { + "title": "Bounding Box", + "type": "object", + "subtype": "bounding-box", + "required": ["west", "south", "east", "north"], + "properties": { + "west": {"description": "West (lower left corner, coordinate axis 1).", "type": "number"}, + "south": {"description": "South (lower left corner, coordinate axis 2).", "type": "number"}, + "east": {"description": "East (upper right corner, coordinate axis 1).", "type": "number"}, + "north": {"description": "North (upper right corner, coordinate axis 2).", "type": "number"}, + "base": { + "description": "Base (optional, lower left corner, coordinate axis 3).", + "type": ["number", "null"], + "default": None, + }, + "height": { + "description": "Height (optional, upper right corner, coordinate axis 3).", + "type": ["number", "null"], + "default": None, + }, + "crs": { + "description": "Coordinate reference system of the extent, specified as as [EPSG code](http://www.epsg-registry.org/) or [WKT2 CRS string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). Defaults to `4326` (EPSG code 4326) unless the client explicitly requests a different coordinate reference system.", + "anyOf": [ + { + "title": "EPSG Code", + "type": "integer", + "subtype": "epsg-code", + "minimum": 1000, + "examples": [3857], + }, + {"title": "WKT2", "type": "string", "subtype": "wkt2-definition"}, + ], + "default": 4326, + }, + }, + }, + { + "title": "Vector data cube", + "description": "Limits the data cube to the bounding box of the given geometries in the vector data cube. For raster data, all pixels inside the bounding box that do not intersect with any of the polygons will be set to no data (`null`). Empty geometries are ignored.", + "type": "object", + "subtype": "datacube", + "dimensions": [{"type": "geometry"}], + }, + { + "title": "No filter", + "description": "Don't filter spatially. All data is included in the data cube.", + "type": "null", + }, + ] + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def date(cls, name: str, description: str = "A date.", **kwargs) -> Parameter: + """ + Helper to easily create a 'date' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "string", "subtype": "date", "format": "date"} + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def date_time(cls, name: str, description: str = "A date with time.", **kwargs) -> Parameter: + """ + Helper to easily create a 'date-time' parameter. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "string", "subtype": "date-time", "format": "date-time"} + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def geojson(cls, name: str, description: str = "Geometries specified as GeoJSON object.", **kwargs) -> Parameter: + """ + Helper to easily create a 'geojson' parameter, which allows to specify geometries as an inline GeoJSON object. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = {"type": "object", "subtype": "geojson"} + return cls(name=name, description=description, schema=schema, **kwargs) + + @classmethod + def temporal_interval( + cls, + name: str, + description: str = "Temporal extent specified as two-element array with start and end date/date-time.", + **kwargs, + ) -> Parameter: + """ + Helper to easily create a 'temporal-interval' parameter, which allows to specify a temporal extent + as a two-element array with start and end date/date-time. + + :param name: parameter name, which will be used to assign concrete values to. + It is recommended to stick to the convention of snake case naming (using lowercase with underscores). + :param description: human-readable description of the parameter. + + See the generic :py:class:`Parameter` constructor for information on additional arguments (except ``schema``). + + .. versionadded:: 0.30.0 + """ + schema = { + "type": "array", + "subtype": "temporal-interval", + "uniqueItems": True, + "minItems": 2, + "maxItems": 2, + "items": { + "anyOf": [ + {"type": "string", "subtype": "date-time", "format": "date-time"}, + {"type": "string", "subtype": "date", "format": "date"}, + {"type": "null"}, + ] + }, + } + return cls(name=name, description=description, schema=schema, **kwargs) diff --git a/lib/openeo/capabilities.py b/lib/openeo/capabilities.py new file mode 100644 index 000000000..5d80bf3ec --- /dev/null +++ b/lib/openeo/capabilities.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import contextlib +import re +from abc import ABC +from typing import Tuple, Union + +# TODO Is this base class (still) useful? + + +class Capabilities(ABC): + """Represents capabilities of a connection / back end.""" + + def __init__(self, data): + pass + + def version(self): + """ Get openEO version. DEPRECATED: use api_version instead""" + # Field: version + # TODO: raise deprecation warning here? + return self.api_version() + + def api_version(self) -> str: + """Get OpenEO API version.""" + raise NotImplementedError + + @property + def api_version_check(self) -> ComparableVersion: + """Helper to easily check if the API version is at least or below some threshold version.""" + api_version = self.api_version() + if not api_version: + raise ApiVersionException("No API version found") + return ComparableVersion(api_version) + + def list_features(self): + """ List all supported features / endpoints.""" + # Field: endpoints + pass + + def has_features(self, method_name): + """ Check whether a feature / endpoint is supported.""" + # Field: endpoints > ... + pass + + def currency(self): + """ Get default billing currency.""" + # Field: billing > currency + pass + + def list_plans(self): + """ List all billing plans.""" + # Field: billing > plans + pass + + +# Type annotation aliases +_VersionTuple = Tuple[Union[int, str], ...] + + +class ComparableVersion: + """ + Helper to compare a version (e.g. API version) against another (threshold) version + + >>> v = ComparableVersion('1.2.3') + >>> v.at_least('1.2.1') + True + >>> v.at_least('1.10.2') + False + >>> v > "2.0" + False + + To express a threshold condition you sometimes want the reference or threshold value on + the left hand side or right hand side of the logical expression. + There are two groups of methods to handle each case: + + - right hand side referencing methods. These read more intuitively. For example: + + `a.at_least(b)`: a is equal or higher than b + `a.below(b)`: a is lower than b + + - left hand side referencing methods. These allow "currying" a threshold value + in a reusable condition callable. For example: + + `a.or_higher(b)`: b is equal or higher than a + `a.accept_lower(b)`: b is lower than a + + Implementation is loosely based on (now deprecated) `distutils.version.LooseVersion`, + which pragmatically parses version strings as a sequence of numbers (compared numerically) + or alphabetic strings (compared lexically), e.g.: 1.5.1, 1.5.2b2, 161, 8.02, 2g6, 2.2beta29. + """ + + _component_re = re.compile(r'(\d+ | [a-zA-Z]+ | \.)', re.VERBOSE) + + def __init__(self, version: Union[str, 'ComparableVersion', tuple]): + if isinstance(version, ComparableVersion): + self._version = version._version + elif isinstance(version, tuple): + self._version = version + elif isinstance(version, str): + self._version = self._parse(version) + else: + raise ValueError(version) + + @classmethod + def _parse(cls, version_string: str) -> _VersionTuple: + components = [ + x for x in cls._component_re.split(version_string) + if x and x != '.' + ] + for i, obj in enumerate(components): + with contextlib.suppress(ValueError): + components[i] = int(obj) + return tuple(components) + + @property + def parts(self) -> _VersionTuple: + """Version components as a tuple""" + return self._version + + def __repr__(self): + return '{c}({v!r})'.format(c=type(self).__name__, v=self._version) + + def __str__(self): + return ".".join(map(str, self._version)) + + def __hash__(self): + return hash(self._version) + + def to_string(self): + return str(self) + + @staticmethod + def _pad(a: Union[str, ComparableVersion], b: Union[str, ComparableVersion]) -> Tuple[_VersionTuple, _VersionTuple]: + """Pad version tuples with zero/empty to get same length for intuitive comparison""" + a = ComparableVersion(a)._version + b = ComparableVersion(b)._version + if len(a) > len(b): + b = b + tuple(0 if isinstance(x, int) else "" for x in a[len(b) :]) + elif len(b) > len(a): + a = a + tuple(0 if isinstance(x, int) else "" for x in b[len(a) :]) + return a, b + + def __eq__(self, other: Union[str, ComparableVersion]) -> bool: + a, b = self._pad(self, other) + return a == b + + def __ge__(self, other: Union[str, ComparableVersion]) -> bool: + a, b = self._pad(self, other) + return a >= b + + def __gt__(self, other: Union[str, ComparableVersion]) -> bool: + a, b = self._pad(self, other) + return a > b + + def __le__(self, other: Union[str, ComparableVersion]) -> bool: + a, b = self._pad(self, other) + return a <= b + + def __lt__(self, other: Union[str, ComparableVersion]) -> bool: + a, b = self._pad(self, other) + return a < b + + def equals(self, other: Union[str, 'ComparableVersion']): + return self == other + + # Right hand side referencing expressions. + def at_least(self, other: Union[str, 'ComparableVersion']): + """Self is at equal or higher than other.""" + return self >= other + + def above(self, other: Union[str, 'ComparableVersion']): + """Self is higher than other.""" + return self > other + + def at_most(self, other: Union[str, 'ComparableVersion']): + """Self is equal or lower than other.""" + return self <= other + + def below(self, other: Union[str, 'ComparableVersion']): + """Self is lower than other.""" + return self < other + + # Left hand side referencing expressions. + def or_higher(self, other: Union[str, 'ComparableVersion']): + """Other is equal or higher than self.""" + return ComparableVersion(other) >= self + + def or_lower(self, other: Union[str, 'ComparableVersion']): + """Other is equal or lower than self""" + return ComparableVersion(other) <= self + + def accept_lower(self, other: Union[str, 'ComparableVersion']): + """Other is lower than self.""" + return ComparableVersion(other) < self + + def accept_higher(self, other: Union[str, 'ComparableVersion']): + """Other is higher than self.""" + return ComparableVersion(other) > self + + def require_at_least(self, other: Union[str, "ComparableVersion"]): + """Raise exception if self is not at least other.""" + if not self.at_least(other): + raise ApiVersionException( + f"openEO API version should be at least {other!s}, but got {self!s}." + ) + + +class ApiVersionException(RuntimeError): + pass diff --git a/lib/openeo/config.py b/lib/openeo/config.py new file mode 100644 index 000000000..8c46a1924 --- /dev/null +++ b/lib/openeo/config.py @@ -0,0 +1,209 @@ +""" + +openEO client configuration (e.g. through config files) + +""" + +from __future__ import annotations + +import logging +import os +import platform +from configparser import ConfigParser +from copy import deepcopy +from pathlib import Path +from typing import Any, Iterator, List, Optional, Sequence, Union + +from openeo.util import in_interactive_mode + +_log = logging.getLogger(__name__) + +DEFAULT_APP_NAME = "openeo-python-client" + + +def _get_user_dir( + app_name=DEFAULT_APP_NAME, + xdg_env_var="XDG_CONFIG_HOME", + win_env_var="APPDATA", + fallback="~/.config", + win_fallback="~\\AppData\\Roaming", + macos_fallback="~/Library/Preferences", + auto_create=True, +) -> Path: + """ + Get platform specific config/data/cache folder + """ + # Platform specific root locations (from highest priority to lowest) + env = os.environ + if platform.system() == "Windows": + roots = [env.get(win_env_var), win_fallback, fallback] + elif platform.system() == "Darwin": + roots = [env.get(xdg_env_var), macos_fallback, fallback] + else: + # Assume unix + roots = [env.get(xdg_env_var), fallback] + + # Filter out None's, expand user prefix and append app name + dirs = [Path(r).expanduser() / app_name for r in roots if r] + # Prepend with OPENEO_CONFIG_HOME if set. + if env.get("OPENEO_CONFIG_HOME"): + dirs.insert(0, Path(env.get("OPENEO_CONFIG_HOME"))) + + # Use highest prio dir that already exists. + for p in dirs: + if p.exists() and p.is_dir(): + return p + + # No existing dir: create highest prio one (if possible) + if auto_create: + for p in dirs: + try: + p.mkdir(parents=True) + _log.info("Created user dir for {a!r}: {p}".format(a=app_name, p=p)) + return p + except OSError: + pass + + raise Exception("Failed to find user dir for {a!r}. Tried: {p!r}".format(a=app_name, p=dirs)) + + +def get_user_config_dir(app_name=DEFAULT_APP_NAME, auto_create=True) -> Path: + """ + Get platform specific config folder + """ + return _get_user_dir( + app_name=app_name, + xdg_env_var="XDG_CONFIG_HOME", + win_env_var="APPDATA", + fallback="~/.config", + win_fallback="~\\AppData\\Roaming", + macos_fallback="~/Library/Preferences", + auto_create=auto_create, + ) + + +def get_user_data_dir(app_name=DEFAULT_APP_NAME, auto_create=True) -> Path: + """ + Get platform specific data folder + """ + return _get_user_dir( + app_name=app_name, + xdg_env_var="XDG_DATA_HOME", + win_env_var="APPDATA", + fallback="~/.local/share", + win_fallback="~\\AppData\\Roaming", + macos_fallback="~/Library", + auto_create=auto_create, + ) + + +class ClientConfig: + """ + openEO client configuration. Essentially a flat mapping of config key-value pairs. + """ + + # TODO: support for loading JSON based config files? + + def __init__(self): + self._config = {} + self._sources = [] + + @classmethod + def _key(cls, key: Union[str, Sequence[str]]): + """Normalize a key: make lower case and flatten sequences""" + if not isinstance(key, str): + key = ".".join(str(k) for k in key) + return key.lower() + + def _set(self, key: Union[str, Sequence[str]], value: Any): + """Set config value at key""" + self._config[self._key(key)] = value + + def get(self, key: Union[str, Sequence[str]], default=None) -> Any: + """Get setting at given key""" + # TODO: option to cast/convert to certain type? + return self._config.get(self._key(key), default) + + def load_ini_file(self, path: Union[str, Path]) -> ClientConfig: + cp = ConfigParser() + read_ok = cp.read(path) + self._sources.extend(read_ok) + return self.load_config_parser(cp) + + def load_config_parser(self, parser: ConfigParser) -> ClientConfig: + for section in parser.sections(): + for option, value in parser.items(section=section): + self._set(key=(section, option), value=value) + return self + + def dump(self) -> dict: + return deepcopy(self._config) + + @property + def sources(self) -> List[str]: + return [str(s) for s in self._sources] + + def __repr__(self): + return f"<{type(self).__name__} from {self.sources}>" + + +class ConfigLoader: + @classmethod + def config_locations(cls) -> Iterator[Path]: + """Config location candidates""" + # From highest to lowest priority + if "OPENEO_CLIENT_CONFIG" in os.environ: + yield Path(os.environ["OPENEO_CLIENT_CONFIG"]) + yield Path.cwd() / "openeo-client-config.ini" + if "OPENEO_CONFIG_HOME" in os.environ: + yield Path(os.environ["OPENEO_CONFIG_HOME"]) / "openeo-client-config.ini" + if "XDG_CONFIG_HOME" in os.environ: + yield Path(os.environ["XDG_CONFIG_HOME"]) / DEFAULT_APP_NAME / "openeo-client-config.ini" + yield Path.home() / ".openeo-client-config.ini" + + @classmethod + def load(cls) -> ClientConfig: + # TODO: (option to) merge layered configs instead of returning on first hit? + config = ClientConfig() + for path in cls.config_locations(): + _log.debug(f"Config file candidate: {path}") + if path.exists(): + if path.suffix.lower() == ".ini": + _log.debug(f"Loading config from {path}") + try: + config.load_ini_file(path) + break + except Exception: + _log.warning(f"Failed to load config from {path}", exc_info=True) + return config + + +# Global config (lazily loaded by :py:func:`get_config`) +_global_config = None + + +def get_config() -> ClientConfig: + """Get global openEO client config (:py:class:`ClientConfig`) (lazy loaded).""" + global _global_config + if _global_config is None: + _global_config = ConfigLoader.load() + # Note: explicit `', '.join()` instead of implicit `repr` on full `sources` list + # as the latter causes ugly escaping of Windows path separator. + message = f"Loaded openEO client config from sources: [{', '.join(_global_config.sources)}]" + _log.info(message) + if _global_config.sources: + config_log(message) + + return _global_config + + +def get_config_option(key: Optional[str] = None, default=None) -> str: + """Get config value for given key from global config (lazy loaded).""" + return get_config().get(key=key, default=default) + + +def config_log(message: str): + """Print a config related message if verbosity is configured for that.""" + verbose = get_config_option("general.verbose", default="auto") + if verbose == "print" or (verbose == "auto" and in_interactive_mode()): + print(message) diff --git a/lib/openeo/dates.py b/lib/openeo/dates.py new file mode 100644 index 000000000..834c23f90 --- /dev/null +++ b/lib/openeo/dates.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +import datetime as dt +import re +from enum import Enum +from typing import Any, Tuple, Union + +from openeo.util import rfc3339 + + +def get_temporal_extent( + *args, + start_date: Union[str, dt.date, None, Any] = None, + end_date: Union[str, dt.date, None, Any] = None, + extent: Union[list, tuple, str, None] = None, + convertor=rfc3339.normalize, +) -> Tuple[Union[str, None], Union[str, None]]: + """ + Helper to derive a date extent from various call forms: + + >>> get_temporal_extent("2019-01-01") + ("2019-01-01", None) + >>> get_temporal_extent("2019-01-01", "2019-05-15") + ("2019-01-01", "2019-05-15") + >>> get_temporal_extent(["2019-01-01", "2019-05-15"]) + ("2019-01-01", "2019-05-15") + >>> get_temporal_extent(start_date="2019-01-01", end_date="2019-05-15"]) + ("2019-01-01", "2019-05-15") + >>> get_temporal_extent(extent=["2019-01-01", "2019-05-15"]) + ("2019-01-01", "2019-05-15") + + It also supports resolving year/month shorthand notation (rounding down to first day of year or month): + + >>> get_temporal_extent("2019") + ("2019-01-01", None) + >>> get_temporal_extent(start_date="2019-02", end_date="2019-03"]) + ("2019-02-01", "2019-03-01") + + And even interpretes extents given as a single string: + + >>> get_temporal_extent(extent="2021") + ("2021-01-01", "2022-01-01") + + """ + if (bool(len(args) > 0) + bool(start_date or end_date) + bool(extent)) > 1: + raise ValueError("At most one of `*args`, `start_date/end_date`, or `extent` should be provided") + if args: + # Convert positional `*args` to `start_date`/`end_date` argument + if len(args) == 2: + start_date, end_date = args + elif len(args) == 1: + arg = args[0] + if isinstance(arg, (list, tuple)): + if len(args) > 2: + raise ValueError(f"Unable to handle {args} as a temporal extent") + start_date, end_date = tuple(arg) + (None,) * (2 - len(arg)) + else: + start_date, end_date = arg, None + else: + raise ValueError(f"Unable to handle {args} as a temporal extent") + elif extent: + if isinstance(extent, (list, tuple)) and len(extent) == 2: + start_date, end_date = extent + elif isinstance(extent, str): + # Special case: extent is given as a single string (e.g. "2021" for full year extent + # or "2021-04" for full month extent): convert that to the appropriate extent tuple. + start_date, end_date = _convert_abbreviated_date(extent), _get_end_of_time_slot(extent) + else: + raise ValueError(f"Unable to handle {extent} as a temporal extent") + start_date = _convert_abbreviated_date(start_date) + end_date = _convert_abbreviated_date(end_date) + return convertor(start_date) if start_date else None, convertor(end_date) if end_date else None + + +class _TypeOfDateString(Enum): + """Enum that denotes which kind of date a string represents. + + This is an internal helper class, not intended to be public. + """ + + INVALID = 0 # It was neither of the options below + YEAR = 1 + MONTH = 2 + DAY = 3 + DATETIME = 4 + + +_REGEX_DAY = re.compile(r"^(\d{4})[:/_-](\d{2})[:/_-](\d{2})$") +_REGEX_MONTH = re.compile(r"^(\d{4})[:/_-](\d{2})$") +_REGEX_YEAR = re.compile(r"^\d{4}$") + + +def _get_end_of_time_slot(date: str) -> Union[dt.date, str]: + """Calculate the end of a left-closed period: the first day after a year or month.""" + if not isinstance(date, str): + return date + + date_converted = _convert_abbreviated_date(date) + granularity = _type_of_date_string(date) + if granularity == _TypeOfDateString.YEAR: + return dt.date(date_converted.year + 1, 1, 1) + elif granularity == _TypeOfDateString.MONTH: + if date_converted.month == 12: + return dt.date(date_converted.year + 1, 1, 1) + else: + return dt.date(date_converted.year, date_converted.month + 1, 1) + elif granularity == _TypeOfDateString.DAY: + # TODO: also support day granularity in _convert_abbreviated_date so that we don't need ad-hoc parsing here + return dt.date(*(int(x) for x in _REGEX_DAY.match(date).group(1, 2, 3))) + dt.timedelta(days=1) + else: + # Don't convert: it is a day or datetime. + return date + + +def _convert_abbreviated_date( + date: Union[str, dt.date, dt.datetime, Any], +) -> Union[str, dt.date, dt.datetime, Any]: + """ + Helper function to convert a year- or month-abreviated strings (e.g. "2021" or "2021-03") into a date + (first day of the corresponding period). Other values are returned as original. + + :param date: some kind of date representation: + + - A string, formatted "yyyy", "yyyy-mm", "yyyy-mm-dd" or with even more granularity + - Any other type (e.g. ``datetime.date``, ``datetime.datetime``, a parameter, ...) + + :return: + If input was a string representing a year or a month: + a ``datetime.date`` that represents the first day of that year or month. + Otherwise, the original version is returned as-is. + + :raises ValueError: + when ``date`` was a string but not recognized as a date representation + + Examples + -------- + + >>> # For year and month: "round down" to fist day: + >>> _convert_abbreviated_date("2021") + datetime.date(2021, 1, 1) + >>> _convert_abbreviated_date("2022-08") + datetime.date(2022, 8, 1) + + >>> # Preserve other values + >>> _convert_abbreviated_date("2022-08-15") + '2022-08-15' + """ + if not isinstance(date, str): + return date + + # TODO: avoid double regex matching? Once in _type_of_date_string and once here. + type_of_date = _type_of_date_string(date) + if type_of_date == _TypeOfDateString.INVALID: + raise ValueError( + f"The value of date='{date}' does not represent any of: " + + "a year ('yyyy'), a year + month ('yyyy-dd'), a date, or a datetime." + ) + + if type_of_date in [_TypeOfDateString.DATETIME, _TypeOfDateString.DAY]: + # TODO: also convert these to `date` or `datetime` for more internal consistency. + return date + + if type_of_date == _TypeOfDateString.MONTH: + match_month = _REGEX_MONTH.match(date) + year = int(match_month.group(1)) + month = int(match_month.group(2)) + else: + year = int(date) + month = 1 + + return dt.date(year, month, 1) + + +def _type_of_date_string(date: str) -> _TypeOfDateString: + """Returns which type of date the string represents: year, month, day or datetime.""" + + if not isinstance(date, str): + raise TypeError("date must be a string") + + try: + rfc3339.parse_datetime(date) + return _TypeOfDateString.DATETIME + except ValueError: + pass + + # Using a separate and stricter regular expressions to detect day, month, + # or year. Having a regex that only matches one type of period makes it + # easier to check it is effectively only a year, or only a month, + # but not a day. Datetime strings are more complex so we use rfc3339 to + # check whether or not it represents a datetime. + match_day = _REGEX_DAY.match(date) + match_month = _REGEX_MONTH.match(date) + match_year = _REGEX_YEAR.match(date) + + if match_day: + return _TypeOfDateString.DAY + if match_month: + return _TypeOfDateString.MONTH + if match_year: + return _TypeOfDateString.YEAR + + return _TypeOfDateString.INVALID diff --git a/lib/openeo/extra/__init__.py b/lib/openeo/extra/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/openeo/extra/job_management.py b/lib/openeo/extra/job_management.py new file mode 100644 index 000000000..fa241ff6b --- /dev/null +++ b/lib/openeo/extra/job_management.py @@ -0,0 +1,1116 @@ +import abc +import collections +import contextlib +import datetime +import json +import logging +import re +import time +import warnings +from pathlib import Path +from threading import Thread +from typing import Callable, Dict, List, NamedTuple, Optional, Union + +import numpy +import pandas as pd +import requests +import shapely.errors +import shapely.geometry.base +import shapely.wkt +from requests.adapters import HTTPAdapter, Retry + +from openeo import BatchJob, Connection +from openeo.internal.processes.parse import ( + Parameter, + Process, + parse_remote_process_definition, +) +from openeo.rest import OpenEoApiError +from openeo.util import LazyLoadCache, deep_get, repr_truncate, rfc3339 + +_log = logging.getLogger(__name__) + +class _Backend(NamedTuple): + """Container for backend info/settings""" + + # callable to create a backend connection + get_connection: Callable[[], Connection] + # Maximum number of jobs to allow in parallel on a backend + parallel_jobs: int + + +MAX_RETRIES = 5 + +# Sentinel value to indicate that a parameter was not set +_UNSET = object() + + +class JobDatabaseInterface(metaclass=abc.ABCMeta): + """ + Interface for a database of job metadata to use with the :py:class:`MultiBackendJobManager`, + allowing to regularly persist the job metadata while polling the job statuses + and resume/restart the job tracking after it was interrupted. + + .. versionadded:: 0.31.0 + """ + + @abc.abstractmethod + def exists(self) -> bool: + """Does the job database already exist, to read job data from?""" + ... + + @abc.abstractmethod + def read(self) -> pd.DataFrame: + """ + Read job data from the database as pandas DataFrame. + + :return: loaded job data. + """ + ... + + @abc.abstractmethod + def persist(self, df: pd.DataFrame): + """ + Store job data to the database. + The provided dataframe may contain partial information, which is merged into the larger database. + + :param df: job data to store. + """ + ... + + @abc.abstractmethod + def count_by_status(self, statuses: List[str]) -> dict: + """ + Retrieve the number of jobs per status. + + :return: dictionary with status as key and the count as value. + """ + ... + + @abc.abstractmethod + def get_by_status(self, statuses: List[str], max=None) -> pd.DataFrame: + """ + Returns a dataframe with jobs, filtered by status. + + :param statuses: List of statuses to include. + :param max: Maximum number of jobs to return. + + :return: DataFrame with jobs filtered by status. + """ + ... + + +def _start_job_default(row: pd.Series, connection: Connection, *args, **kwargs): + raise NotImplementedError("No 'start_job' callable provided") + + +class MultiBackendJobManager: + """ + Tracker for multiple jobs on multiple backends. + + Usage example: + + .. code-block:: python + + import logging + import pandas as pd + import openeo + from openeo.extra.job_management import MultiBackendJobManager + + logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO + ) + + manager = MultiBackendJobManager() + manager.add_backend("foo", connection=openeo.connect("http://foo.test")) + manager.add_backend("bar", connection=openeo.connect("http://bar.test")) + + jobs_df = pd.DataFrame(...) + output_file = "jobs.csv" + + def start_job( + row: pd.Series, + connection: openeo.Connection, + **kwargs + ) -> openeo.BatchJob: + year = row["year"] + cube = connection.load_collection( + ..., + temporal_extent=[f"{year}-01-01", f"{year+1}-01-01"], + ) + ... + return cube.create_job(...) + + manager.run_jobs(df=jobs_df, start_job=start_job, output_file=output_file) + + See :py:meth:`.run_jobs` for more information on the ``start_job`` callable. + + :param poll_sleep: + How many seconds to sleep between polls. + + :param root_dir: + Root directory to save files for the jobs, e.g. metadata and error logs. + This defaults to "." the current directory. + + Each job gets its own subfolder in this root directory. + You can use the following methods to find the relevant paths, + based on the job ID: + + - get_job_dir + - get_error_log_path + - get_job_metadata_path + + :param cancel_running_job_after: + Optional temporal limit (in seconds) after which running jobs should be canceled + by the job manager. + + .. versionadded:: 0.14.0 + + .. versionchanged:: 0.32.0 + Added ``cancel_running_job_after`` parameter. + """ + + def __init__( + self, + poll_sleep: int = 60, + root_dir: Optional[Union[str, Path]] = ".", + *, + cancel_running_job_after: Optional[int] = None, + ): + """Create a MultiBackendJobManager.""" + self._stop_thread = None + self.backends: Dict[str, _Backend] = {} + self.poll_sleep = poll_sleep + self._connections: Dict[str, _Backend] = {} + + # An explicit None or "" should also default to "." + self._root_dir = Path(root_dir or ".") + + self._cancel_running_job_after = ( + datetime.timedelta(seconds=cancel_running_job_after) if cancel_running_job_after is not None else None + ) + self._thread = None + + def add_backend( + self, + name: str, + connection: Union[Connection, Callable[[], Connection]], + parallel_jobs: int = 2, + ): + """ + Register a backend with a name and a Connection getter. + + :param name: + Name of the backend. + :param connection: + Either a Connection to the backend, or a callable to create a backend connection. + :param parallel_jobs: + Maximum number of jobs to allow in parallel on a backend. + """ + + # TODO: Code might become simpler if we turn _Backend into class move this logic there. + # We would need to keep add_backend here as part of the public API though. + # But the amount of unrelated "stuff to manage" would be less (better cohesion) + if isinstance(connection, Connection): + c = connection + connection = lambda: c + assert callable(connection) + self.backends[name] = _Backend(get_connection=connection, parallel_jobs=parallel_jobs) + + def _get_connection(self, backend_name: str, resilient: bool = True) -> Connection: + """Get a connection for the backend and optionally make it resilient (adds retry behavior) + + The default is to get a resilient connection, but if necessary you can turn it off with + resilient=False + """ + + # TODO: Code could be simplified if _Backend is a class and this method is moved there. + # TODO: Is it better to make this a public method? + + # Reuse the connection if we can, in order to avoid modifying the same connection several times. + # This is to avoid adding the retry HTTPAdapter multiple times. + # Remember that the get_connection attribute on _Backend can be a Connection object instead + # of a callable, so we don't want to assume it is a fresh connection that doesn't have the + # retry adapter yet. + if backend_name in self._connections: + return self._connections[backend_name] + + connection = self.backends[backend_name].get_connection() + # If we really need it we can skip making it resilient, but by default it should be resilient. + if resilient: + self._make_resilient(connection) + + self._connections[backend_name] = connection + return connection + + @staticmethod + def _make_resilient(connection): + """Add an HTTPAdapter that retries the request if it fails. + + Retry for the following HTTP 50x statuses: + 502 Bad Gateway + 503 Service Unavailable + 504 Gateway Timeout + """ + # TODO: refactor this helper out of this class and unify with `openeo_driver.util.http.requests_with_retry` + status_forcelist = [500, 502, 503, 504] + retries = Retry( + total=MAX_RETRIES, + read=MAX_RETRIES, + other=MAX_RETRIES, + status=MAX_RETRIES, + backoff_factor=0.1, + status_forcelist=status_forcelist, + allowed_methods=["HEAD", "GET", "OPTIONS", "POST"], + ) + connection.session.mount("https://", HTTPAdapter(max_retries=retries)) + connection.session.mount("http://", HTTPAdapter(max_retries=retries)) + + @staticmethod + def _normalize_df(df: pd.DataFrame) -> pd.DataFrame: + """ + Normalize given pandas dataframe (creating a new one): + ensure we have the required columns. + + :param df: The dataframe to normalize. + :return: a new dataframe that is normalized. + """ + # check for some required columns. + required_with_default = [ + ("status", "not_started"), + ("id", None), + ("start_time", None), + ("running_start_time", None), + # TODO: columns "cpu", "memory", "duration" are not referenced directly + # within MultiBackendJobManager making it confusing to claim they are required. + # However, they are through assumptions about job "usage" metadata in `_track_statuses`. + # => proposed solution: allow to configure usage columns when adding a backend + ("cpu", None), + ("memory", None), + ("duration", None), + ("backend_name", None), + ] + new_columns = {col: val for (col, val) in required_with_default if col not in df.columns} + df = df.assign(**new_columns) + + return df + + def start_job_thread(self, start_job: Callable[[], BatchJob], job_db: JobDatabaseInterface): + """ + Start running the jobs in a separate thread, returns afterwards. + + :param start_job: + A callback which will be invoked with, amongst others, + the row of the dataframe for which a job should be created and/or started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + The following parameters will be passed to ``start_job``: + + ``row`` (:py:class:`pandas.Series`): + The row in the pandas dataframe that stores the jobs state and other tracked data. + + ``connection_provider``: + A getter to get a connection by backend name. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``connection`` (:py:class:`Connection`): + The :py:class:`Connection` itself, that has already been created. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``provider`` (``str``): + The name of the backend that will run the job. + + You do not have to define all the parameters described below, but if you leave + any of them out, then remember to include the ``*args`` and ``**kwargs`` parameters. + Otherwise you will have an exception because :py:meth:`run_jobs` passes unknown parameters to ``start_job``. + :param job_db: + Job database to load/store existing job status data and other metadata from/to. + Can be specified as a path to CSV or Parquet file, + or as a custom database object following the :py:class:`JobDatabaseInterface` interface. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency `. + + .. versionadded:: 0.32.0 + """ + + # Resume from existing db + _log.info(f"Resuming `run_jobs` from existing {job_db}") + df = job_db.read() + + self._stop_thread = False + def run_loop(): + while ( + sum(job_db.count_by_status(statuses=["not_started", "created", "queued", "running"]).values()) > 0 + and not self._stop_thread + ): + self._job_update_loop(job_db=job_db, start_job=start_job) + + # Do sequence of micro-sleeps to allow for quick thread exit + for _ in range(int(max(1, self.poll_sleep))): + time.sleep(1) + if self._stop_thread: + break + + self._thread = Thread(target=run_loop) + self._thread.start() + + def stop_job_thread(self, timeout_seconds: Optional[float] = _UNSET): + """ + Stop the job polling thread. + + :param timeout_seconds: The time to wait for the thread to stop. + By default, it will wait for 2 times the poll_sleep time. + Set to None to wait indefinitely. + + .. versionadded:: 0.32.0 + """ + if self._thread is not None: + self._stop_thread = True + if timeout_seconds is _UNSET: + timeout_seconds = 2 * self.poll_sleep + self._thread.join(timeout_seconds) + if self._thread.is_alive(): + _log.warning("Job thread did not stop after timeout") + else: + _log.error("No job thread to stop") + + def run_jobs( + self, + df: Optional[pd.DataFrame] = None, + start_job: Callable[[], BatchJob] = _start_job_default, + job_db: Union[str, Path, JobDatabaseInterface, None] = None, + **kwargs, + ) -> dict: + """Runs jobs, specified in a dataframe, and tracks parameters. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. If None, the job_db has to be specified and will be used. + + :param start_job: + A callback which will be invoked with, amongst others, + the row of the dataframe for which a job should be created and/or started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + The following parameters will be passed to ``start_job``: + + ``row`` (:py:class:`pandas.Series`): + The row in the pandas dataframe that stores the jobs state and other tracked data. + + ``connection_provider``: + A getter to get a connection by backend name. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``connection`` (:py:class:`Connection`): + The :py:class:`Connection` itself, that has already been created. + Typically, you would need either the parameter ``connection_provider``, + or the parameter ``connection``, but likely you will not need both. + + ``provider`` (``str``): + The name of the backend that will run the job. + + You do not have to define all the parameters described below, but if you leave + any of them out, then remember to include the ``*args`` and ``**kwargs`` parameters. + Otherwise you will have an exception because :py:meth:`run_jobs` passes unknown parameters to ``start_job``. + + :param job_db: + Job database to load/store existing job status data and other metadata from/to. + Can be specified as a path to CSV or Parquet file, + or as a custom database object following the :py:class:`JobDatabaseInterface` interface. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency `. + + :return: dictionary with stats collected during the job running loop. + Note that the set of fields in this dictionary is experimental + and subject to change + + .. versionchanged:: 0.31.0 + Added support for persisting the job metadata in Parquet format. + + .. versionchanged:: 0.31.0 + Replace ``output_file`` argument with ``job_db`` argument, + which can be a path to a CSV or Parquet file, + or a user-defined :py:class:`JobDatabaseInterface` object. + The deprecated ``output_file`` argument is still supported for now. + + .. versionchanged:: 0.33.0 + return a stats dictionary + """ + # TODO Defining start_jobs as a Protocol might make its usage more clear, and avoid complicated docstrings, + + # Backwards compatibility for deprecated `output_file` argument + if "output_file" in kwargs: + if job_db is not None: + raise ValueError("Only one of `output_file` and `job_db` should be provided") + warnings.warn( + "The `output_file` argument is deprecated. Use `job_db` instead.", DeprecationWarning, stacklevel=2 + ) + job_db = kwargs.pop("output_file") + assert not kwargs, f"Unexpected keyword arguments: {kwargs!r}" + + if isinstance(job_db, (str, Path)): + job_db = get_job_db(path=job_db) + + if not isinstance(job_db, JobDatabaseInterface): + raise ValueError(f"Unsupported job_db {job_db!r}") + + if job_db.exists(): + # Resume from existing db + _log.info(f"Resuming `run_jobs` from existing {job_db}") + elif df is not None: + # TODO: start showing deprecation warnings for this usage pattern? + job_db.initialize_from_df(df) + + stats = collections.defaultdict(int) + while sum(job_db.count_by_status(statuses=["not_started", "created", "queued", "running"]).values()) > 0: + self._job_update_loop(job_db=job_db, start_job=start_job, stats=stats) + stats["run_jobs loop"] += 1 + + time.sleep(self.poll_sleep) + stats["sleep"] += 1 + + return stats + + def _job_update_loop( + self, job_db: JobDatabaseInterface, start_job: Callable[[], BatchJob], stats: Optional[dict] = None + ): + """ + Inner loop logic of job management: + go through the necessary jobs to check for status updates, + trigger status events, start new jobs when there is room for them, etc. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + with ignore_connection_errors(context="get statuses"): + self._track_statuses(job_db, stats=stats) + stats["track_statuses"] += 1 + + not_started = job_db.get_by_status(statuses=["not_started"], max=200).copy() + if len(not_started) > 0: + # Check number of jobs running at each backend + running = job_db.get_by_status(statuses=["created", "queued", "running"]) + stats["job_db get_by_status"] += 1 + per_backend = running.groupby("backend_name").size().to_dict() + _log.info(f"Running per backend: {per_backend}") + total_added = 0 + for backend_name in self.backends: + backend_load = per_backend.get(backend_name, 0) + if backend_load < self.backends[backend_name].parallel_jobs: + to_add = self.backends[backend_name].parallel_jobs - backend_load + for i in not_started.index[total_added : total_added + to_add]: + self._launch_job(start_job, df=not_started, i=i, backend_name=backend_name, stats=stats) + stats["job launch"] += 1 + + job_db.persist(not_started.loc[i : i + 1]) + stats["job_db persist"] += 1 + total_added += 1 + + def _launch_job(self, start_job, df, i, backend_name, stats: Optional[dict] = None): + """Helper method for launching jobs + + :param start_job: + A callback which will be invoked with the row of the dataframe for which a job should be started. + This callable should return a :py:class:`openeo.rest.job.BatchJob` object. + + See also: + `MultiBackendJobManager.run_jobs` for the parameters and return type of this callable + + Even though it is called here in `_launch_job` and that is where the constraints + really come from, the public method `run_jobs` needs to document `start_job` anyway, + so let's avoid duplication in the docstrings. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. + + :param i: + index of the job's row in dataframe df + + :param backend_name: + name of the backend that will execute the job. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + df.loc[i, "backend_name"] = backend_name + row = df.loc[i] + try: + _log.info(f"Starting job on backend {backend_name} for {row.to_dict()}") + connection = self._get_connection(backend_name, resilient=True) + + stats["start_job call"] += 1 + job = start_job( + row=row, + connection_provider=self._get_connection, + connection=connection, + provider=backend_name, + ) + except requests.exceptions.ConnectionError as e: + _log.warning(f"Failed to start job for {row.to_dict()}", exc_info=True) + df.loc[i, "status"] = "start_failed" + stats["start_job error"] += 1 + else: + df.loc[i, "start_time"] = rfc3339.utcnow() + if job: + df.loc[i, "id"] = job.job_id + with ignore_connection_errors(context="get status"): + status = job.status() + stats["job get status"] += 1 + df.loc[i, "status"] = status + if status == "created": + # start job if not yet done by callback + try: + job.start() + stats["job start"] += 1 + df.loc[i, "status"] = job.status() + stats["job get status"] += 1 + except OpenEoApiError as e: + _log.error(e) + df.loc[i, "status"] = "start_failed" + stats["job start error"] += 1 + else: + # TODO: what is this "skipping" about actually? + df.loc[i, "status"] = "skipped" + stats["start_job skipped"] += 1 + + def on_job_done(self, job: BatchJob, row): + """ + Handles jobs that have finished. Can be overridden to provide custom behaviour. + + Default implementation downloads the results into a folder containing the title. + + :param job: The job that has finished. + :param row: DataFrame row containing the job's metadata. + """ + # TODO: param `row` is never accessed in this method. Remove it? Is this intended for future use? + + job_metadata = job.describe() + job_dir = self.get_job_dir(job.job_id) + metadata_path = self.get_job_metadata_path(job.job_id) + + self.ensure_job_dir_exists(job.job_id) + job.get_results().download_files(target=job_dir) + + with metadata_path.open("w", encoding="utf-8") as f: + json.dump(job_metadata, f, ensure_ascii=False) + + def on_job_error(self, job: BatchJob, row): + """ + Handles jobs that stopped with errors. Can be overridden to provide custom behaviour. + + Default implementation writes the error logs to a JSON file. + + :param job: The job that has finished. + :param row: DataFrame row containing the job's metadata. + """ + # TODO: param `row` is never accessed in this method. Remove it? Is this intended for future use? + + error_logs = job.logs(level="error") + error_log_path = self.get_error_log_path(job.job_id) + + if len(error_logs) > 0: + self.ensure_job_dir_exists(job.job_id) + error_log_path.write_text(json.dumps(error_logs, indent=2)) + + def on_job_cancel(self, job: BatchJob, row): + """ + Handle a job that was cancelled. Can be overridden to provide custom behaviour. + + Default implementation does not do anything. + + :param job: The job that was canceled. + :param row: DataFrame row containing the job's metadata. + """ + pass + + def _cancel_prolonged_job(self, job: BatchJob, row): + """Cancel the job if it has been running for too long.""" + job_running_start_time = rfc3339.parse_datetime(row["running_start_time"], with_timezone=True) + elapsed = datetime.datetime.now(tz=datetime.timezone.utc) - job_running_start_time + if elapsed > self._cancel_running_job_after: + try: + _log.info( + f"Cancelling long-running job {job.job_id} (after {elapsed}, running since {job_running_start_time})" + ) + job.stop() + except OpenEoApiError as e: + _log.error(f"Failed to cancel long-running job {job.job_id}: {e}") + + def get_job_dir(self, job_id: str) -> Path: + """Path to directory where job metadata, results and error logs are be saved.""" + return self._root_dir / f"job_{job_id}" + + def get_error_log_path(self, job_id: str) -> Path: + """Path where error log file for the job is saved.""" + return self.get_job_dir(job_id) / f"job_{job_id}_errors.json" + + def get_job_metadata_path(self, job_id: str) -> Path: + """Path where job metadata file is saved.""" + return self.get_job_dir(job_id) / f"job_{job_id}.json" + + def ensure_job_dir_exists(self, job_id: str) -> Path: + """Create the job folder if it does not exist yet.""" + job_dir = self.get_job_dir(job_id) + if not job_dir.exists(): + job_dir.mkdir(parents=True) + + def _track_statuses(self, job_db: JobDatabaseInterface, stats: Optional[dict] = None): + """ + Tracks status (and stats) of running jobs (in place). + Optionally cancels jobs when running too long. + """ + stats = stats if stats is not None else collections.defaultdict(int) + + active = job_db.get_by_status(statuses=["created", "queued", "running"]).copy() + for i in active.index: + job_id = active.loc[i, "id"] + backend_name = active.loc[i, "backend_name"] + previous_status = active.loc[i, "status"] + + try: + con = self._get_connection(backend_name) + the_job = con.job(job_id) + job_metadata = the_job.describe() + stats["job describe"] += 1 + new_status = job_metadata["status"] + + _log.info( + f"Status of job {job_id!r} (on backend {backend_name}) is {new_status!r} (previously {previous_status!r})" + ) + + if new_status == "finished": + stats["job finished"] += 1 + self.on_job_done(the_job, active.loc[i]) + + if previous_status != "error" and new_status == "error": + stats["job failed"] += 1 + self.on_job_error(the_job, active.loc[i]) + + if previous_status in {"created", "queued"} and new_status == "running": + stats["job started running"] += 1 + active.loc[i, "running_start_time"] = rfc3339.utcnow() + + if new_status == "canceled": + stats["job canceled"] += 1 + self.on_job_cancel(the_job, active.loc[i]) + + if self._cancel_running_job_after and new_status == "running": + self._cancel_prolonged_job(the_job, active.loc[i]) + + active.loc[i, "status"] = new_status + + # TODO: there is well hidden coupling here with "cpu", "memory" and "duration" from `_normalize_df` + for key in job_metadata.get("usage", {}).keys(): + if key in active.columns: + active.loc[i, key] = _format_usage_stat(job_metadata, key) + + except OpenEoApiError as e: + # TODO: inspect status code and e.g. differentiate between 4xx/5xx + stats["job tracking error"] += 1 + _log.warning(f"Error while tracking status of job {job_id!r} on backend {backend_name}: {e!r}") + + stats["job_db persist"] += 1 + job_db.persist(active) + + +def _format_usage_stat(job_metadata: dict, field: str) -> str: + value = deep_get(job_metadata, "usage", field, "value", default=0) + unit = deep_get(job_metadata, "usage", field, "unit", default="") + return f"{value} {unit}".strip() + + +@contextlib.contextmanager +def ignore_connection_errors(context: Optional[str] = None, sleep: int = 5): + """Context manager to ignore connection errors.""" + # TODO: move this out of this module and make it a more public utility? + try: + yield + except requests.exceptions.ConnectionError as e: + _log.warning(f"Ignoring connection error (context {context or 'n/a'}): {e}") + # Back off a bit + time.sleep(sleep) + + +class FullDataFrameJobDatabase(JobDatabaseInterface): + + def __init__(self): + super().__init__() + self._df = None + + def initialize_from_df(self, df: pd.DataFrame, *, on_exists: str = "error"): + """ + Initialize the job database from a given dataframe, + which will be first normalized to be compatible + with :py:class:`MultiBackendJobManager` usage. + + :param df: dataframe with some columns your ``start_job`` callable expects + :param on_exists: what to do when the job database already exists (persisted on disk): + - "error": (default) raise an exception + - "skip": work with existing database, ignore given dataframe and skip any initialization + + :return: initialized job database. + + .. versionadded:: 0.33.0 + """ + # TODO: option to provide custom MultiBackendJobManager subclass with custom normalize? + if self.exists(): + if on_exists == "skip": + return self + elif on_exists == "error": + raise FileExistsError(f"Job database {self!r} already exists.") + else: + # TODO handle other on_exists modes: e.g. overwrite, merge, ... + raise ValueError(f"Invalid on_exists={on_exists!r}") + df = MultiBackendJobManager._normalize_df(df) + self.persist(df) + # Return self to allow chaining with constructor. + return self + + @property + def df(self) -> pd.DataFrame: + if self._df is None: + self._df = self.read() + return self._df + + def count_by_status(self, statuses: List[str]) -> dict: + status_histogram = self.df.groupby("status").size().to_dict() + return {k:v for k,v in status_histogram.items() if k in statuses} + + def get_by_status(self, statuses, max=None) -> pd.DataFrame: + """ + Returns a dataframe with jobs, filtered by status. + + :param statuses: List of statuses to include. + :param max: Maximum number of jobs to return. + + :return: DataFrame with jobs filtered by status. + """ + df = self.df + filtered = df[df.status.isin(statuses)] + return filtered.head(max) if max is not None else filtered + + def _merge_into_df(self, df: pd.DataFrame): + if self._df is not None: + self._df.update(df, overwrite=True) + else: + self._df = df + + +class CsvJobDatabase(FullDataFrameJobDatabase): + """ + Persist/load job metadata with a CSV file. + + :implements: :py:class:`JobDatabaseInterface` + :param path: Path to local CSV file. + + .. note:: + Support for GeoPandas dataframes depends on the ``geopandas`` package + as :ref:`optional dependency `. + + .. versionadded:: 0.31.0 + """ + def __init__(self, path: Union[str, Path]): + super().__init__() + self.path = Path(path) + + def __repr__(self): + return f"{self.__class__.__name__}({str(self.path)!r})" + + def exists(self) -> bool: + return self.path.exists() + + def _is_valid_wkt(self, wkt: str) -> bool: + try: + shapely.wkt.loads(wkt) + return True + except shapely.errors.WKTReadingError: + return False + + def read(self) -> pd.DataFrame: + df = pd.read_csv(self.path) + if ( + "geometry" in df.columns + and df["geometry"].dtype.name != "geometry" + and self._is_valid_wkt(df["geometry"].iloc[0]) + ): + import geopandas + + # `df.to_csv()` in `persist()` has encoded geometries as WKT, so we decode that here. + df = geopandas.GeoDataFrame(df, geometry=geopandas.GeoSeries.from_wkt(df["geometry"])) + return df + + def persist(self, df: pd.DataFrame): + self._merge_into_df(df) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.df.to_csv(self.path, index=False) + + +class ParquetJobDatabase(FullDataFrameJobDatabase): + """ + Persist/load job metadata with a Parquet file. + + :implements: :py:class:`JobDatabaseInterface` + :param path: Path to the Parquet file. + + .. note:: + Support for Parquet files depends on the ``pyarrow`` package + as :ref:`optional dependency `. + + Support for GeoPandas dataframes depends on the ``geopandas`` package + as :ref:`optional dependency `. + + .. versionadded:: 0.31.0 + """ + def __init__(self, path: Union[str, Path]): + super().__init__() + self.path = Path(path) + + def __repr__(self): + return f"{self.__class__.__name__}({str(self.path)!r})" + + def exists(self) -> bool: + return self.path.exists() + + def read(self) -> pd.DataFrame: + # Unfortunately, a naive `pandas.read_parquet()` does not easily allow + # reconstructing geometries from a GeoPandas Parquet file. + # And vice-versa, `geopandas.read_parquet()` does not support reading + # Parquet file without geometries. + # So we have to guess which case we have. + # TODO is there a cleaner way to do this? + import pyarrow.parquet + + metadata = pyarrow.parquet.read_metadata(self.path) + if b"geo" in metadata.metadata: + import geopandas + return geopandas.read_parquet(self.path) + else: + return pd.read_parquet(self.path) + + def persist(self, df: pd.DataFrame): + self._merge_into_df(df) + self.path.parent.mkdir(parents=True, exist_ok=True) + self.df.to_parquet(self.path, index=False) + + +def get_job_db(path: Union[str, Path]) -> JobDatabaseInterface: + """ + Factory to get a job database at a given path, + guessing the database type from filename extension. + + :param path: path to job database file. + + .. versionadded:: 0.33.0 + """ + path = Path(path) + if path.suffix.lower() in {".csv"}: + job_db = CsvJobDatabase(path=path) + elif path.suffix.lower() in {".parquet", ".geoparquet"}: + job_db = ParquetJobDatabase(path=path) + else: + raise ValueError(f"Could not guess job database type from {path!r}") + return job_db + + +def create_job_db(path: Union[str, Path], df: pd.DataFrame, *, on_exists: str = "error"): + """ + Factory to create a job database at given path, + initialized from a given dataframe, + and its database type guessed from filename extension. + + :param path: Path to the job database file. + :param df: DataFrame to store in the job database. + :param on_exists: What to do when the job database already exists: + - "error": (default) raise an exception + - "skip": work with existing database, ignore given dataframe and skip any initialization + + .. versionadded:: 0.33.0 + """ + job_db = get_job_db(path) + if isinstance(job_db, FullDataFrameJobDatabase): + job_db.initialize_from_df(df=df, on_exists=on_exists) + else: + raise NotImplementedError(f"Initialization of {type(job_db)} is not supported.") + return job_db + + +class ProcessBasedJobCreator: + """ + Batch job creator + (to be used together with :py:class:`MultiBackendJobManager`) + that takes a parameterized openEO process definition + (e.g a user-defined process (UDP) or a remote openEO process definition), + and creates a batch job + for each row of the dataframe managed by the :py:class:`MultiBackendJobManager` + by filling in the process parameters with corresponding row values. + + .. seealso:: + See :ref:`job-management-with-process-based-job-creator` + for more information and examples. + + Process parameters are linked to dataframe columns by name. + While this intuitive name-based matching should cover most use cases, + there are additional options for overrides or fallbacks: + + - When provided, ``parameter_column_map`` will be consulted + for resolving a process parameter name (key in the dictionary) + to a desired dataframe column name (corresponding value). + - One common case is handled automatically as convenience functionality. + + When: + + - ``parameter_column_map`` is not provided (or set to ``None``), + - and there is a *single parameter* that accepts inline GeoJSON geometries, + - and the dataframe is a GeoPandas dataframe with a *single geometry* column, + + then this parameter and this geometries column will be linked automatically. + + - If a parameter can not be matched with a column by name as described above, + a default value will be picked, + first by looking in ``parameter_defaults`` (if provided), + and then by looking up the default value from the parameter schema in the process definition. + - Finally if no (default) value can be determined and the parameter + is not flagged as optional, an error will be raised. + + + :param process_id: (optional) openEO process identifier. + Can be omitted when working with a remote process definition + that is fully defined with a URL in the ``namespace`` parameter. + :param namespace: (optional) openEO process namespace. + Typically used to provide a URL to a remote process definition. + :param parameter_defaults: (optional) default values for process parameters, + to be used when not available in the dataframe managed by + :py:class:`MultiBackendJobManager`. + :param parameter_column_map: Optional overrides + for linking process parameters to dataframe columns: + mapping of process parameter names as key + to dataframe column names as value. + + .. versionadded:: 0.33.0 + + .. warning:: + This is an experimental API subject to change, + and we greatly welcome + `feedback and suggestions for improvement `_. + + """ + def __init__( + self, + *, + process_id: Optional[str] = None, + namespace: Union[str, None] = None, + parameter_defaults: Optional[dict] = None, + parameter_column_map: Optional[dict] = None, + ): + if process_id is None and namespace is None: + raise ValueError("At least one of `process_id` and `namespace` should be provided.") + self._process_id = process_id + self._namespace = namespace + self._parameter_defaults = parameter_defaults or {} + self._parameter_column_map = parameter_column_map + self._cache = LazyLoadCache() + + def _get_process_definition(self, connection: Connection) -> Process: + if isinstance(self._namespace, str) and re.match("https?://", self._namespace): + # Remote process definition handling + return self._cache.get( + key=("remote_process_definition", self._namespace, self._process_id), + load=lambda: parse_remote_process_definition(namespace=self._namespace, process_id=self._process_id), + ) + elif self._namespace is None: + # Handling of a user-specific UDP + udp_raw = connection.user_defined_process(self._process_id).describe() + return Process.from_dict(udp_raw) + else: + raise NotImplementedError( + f"Unsupported process definition source udp_id={self._process_id!r} namespace={self._namespace!r}" + ) + + + def start_job(self, row: pd.Series, connection: Connection, **_) -> BatchJob: + """ + Implementation of the ``start_job`` callable interface + of :py:meth:`MultiBackendJobManager.run_jobs` + to create a job based on given dataframe row + + :param row: The row in the pandas dataframe that stores the jobs state and other tracked data. + :param connection: The connection to the backend. + """ + # TODO: refactor out some methods, for better reuse and decoupling: + # `get_arguments()` (to build the arguments dictionary), `get_cube()` (to create the cube), + + process_definition = self._get_process_definition(connection=connection) + process_id = process_definition.id + parameters = process_definition.parameters or [] + + if self._parameter_column_map is None: + self._parameter_column_map = self._guess_parameter_column_map(parameters=parameters, row=row) + + arguments = {} + for parameter in parameters: + param_name = parameter.name + column_name = self._parameter_column_map.get(param_name, param_name) + if column_name in row.index: + # Get value from dataframe row + value = row.loc[column_name] + elif param_name in self._parameter_defaults: + # Fallback on default values from constructor + value = self._parameter_defaults[param_name] + elif parameter.has_default(): + # Explicitly use default value from parameter schema + value = parameter.default + elif parameter.optional: + # Skip optional parameters without any fallback default value + continue + else: + raise ValueError(f"Missing required parameter {param_name !r} for process {process_id!r}") + + # Prepare some values/dtypes for JSON encoding + if isinstance(value, numpy.integer): + value = int(value) + elif isinstance(value, numpy.number): + value = float(value) + elif isinstance(value, shapely.geometry.base.BaseGeometry): + value = shapely.geometry.mapping(value) + + arguments[param_name] = value + + cube = connection.datacube_from_process(process_id=process_id, namespace=self._namespace, **arguments) + + title = row.get("title", f"Process {process_id!r} with {repr_truncate(arguments)}") + description = row.get("description", f"Process {process_id!r} (namespace {self._namespace}) with {arguments}") + job = connection.create_job(cube, title=title, description=description) + + return job + + def __call__(self, *arg, **kwargs) -> BatchJob: + """Syntactic sugar for calling :py:meth:`start_job`.""" + return self.start_job(*arg, **kwargs) + + @staticmethod + def _guess_parameter_column_map(parameters: List[Parameter], row: pd.Series) -> dict: + """ + Guess parameter-column mapping from given parameter list and dataframe row + """ + parameter_column_map = {} + # Geometry based mapping: try to automatically map geometry columns to geojson parameters + geojson_parameters = [p.name for p in parameters if p.schema.accepts_geojson()] + geometry_columns = [i for (i, v) in row.items() if isinstance(v, shapely.geometry.base.BaseGeometry)] + if geojson_parameters and geometry_columns: + if len(geojson_parameters) == 1 and len(geometry_columns) == 1: + # Most common case: one geometry parameter and one geometry column: can be mapped naively + parameter_column_map[geojson_parameters[0]] = geometry_columns[0] + elif all(p in geometry_columns for p in geojson_parameters): + # Each geometry param has geometry column with same name: easy to map + parameter_column_map.update((p, p) for p in geojson_parameters) + else: + raise RuntimeError( + f"Problem with mapping geometry columns ({geometry_columns}) to process parameters ({geojson_parameters})" + ) + _log.debug(f"Guessed parameter-column map: {parameter_column_map}") + return parameter_column_map diff --git a/lib/openeo/extra/spectral_indices/__init__.py b/lib/openeo/extra/spectral_indices/__init__.py new file mode 100644 index 000000000..d83c37813 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/__init__.py @@ -0,0 +1,2 @@ + +from openeo.extra.spectral_indices.spectral_indices import * diff --git a/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/LICENSE b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/LICENSE new file mode 100644 index 000000000..7bd30da58 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 David Montero Loaiza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/bands.json b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/bands.json new file mode 100644 index 000000000..052f82015 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/bands.json @@ -0,0 +1,785 @@ +{ + "A": { + "common_name": "coastal", + "long_name": "Aersols", + "max_wavelength": 455, + "min_wavelength": 400, + "platforms": { + "landsat8": { + "band": "B1", + "bandwidth": 20.0, + "name": "Coastal Aerosol", + "platform": "Landsat 8", + "wavelength": 440.0 + }, + "landsat9": { + "band": "B1", + "bandwidth": 20.0, + "name": "Coastal Aerosol", + "platform": "Landsat 8", + "wavelength": 440.0 + }, + "planetscope": { + "band": "B1", + "bandwidth": 21.0, + "name": "Coastal Blue", + "platform": "PlanetScope", + "wavelength": 441.5 + }, + "sentinel2a": { + "band": "B1", + "bandwidth": 21, + "name": "Aerosols", + "platform": "Sentinel-2A", + "wavelength": 442.7 + }, + "sentinel2b": { + "band": "B1", + "bandwidth": 21, + "name": "Aerosols", + "platform": "Sentinel-2B", + "wavelength": 442.3 + }, + "wv2": { + "band": "B1", + "bandwidth": 50.0, + "name": "Coastal Blue", + "platform": "WorldView-2", + "wavelength": 425.0 + }, + "wv3": { + "band": "B1", + "bandwidth": 50.0, + "name": "Coastal Blue", + "platform": "WorldView-3", + "wavelength": 425.0 + } + }, + "short_name": "A" + }, + "B": { + "common_name": "blue", + "long_name": "Blue", + "max_wavelength": 530, + "min_wavelength": 450, + "platforms": { + "landsat4": { + "band": "B1", + "bandwidth": 70.0, + "name": "Blue", + "platform": "Landsat 4", + "wavelength": 485.0 + }, + "landsat5": { + "band": "B1", + "bandwidth": 70.0, + "name": "Blue", + "platform": "Landsat 5", + "wavelength": 485.0 + }, + "landsat7": { + "band": "B1", + "bandwidth": 70.0, + "name": "Blue", + "platform": "Landsat 7", + "wavelength": 485.0 + }, + "landsat8": { + "band": "B2", + "bandwidth": 60.0, + "name": "Blue", + "platform": "Landsat 8", + "wavelength": 480.0 + }, + "landsat9": { + "band": "B2", + "bandwidth": 60.0, + "name": "Blue", + "platform": "Landsat 9", + "wavelength": 480.0 + }, + "modis": { + "band": "B3", + "bandwidth": 20.0, + "name": "Blue", + "platform": "Terra/Aqua: MODIS", + "wavelength": 469.0 + }, + "planetscope": { + "band": "B2", + "bandwidth": 50.0, + "name": "Blue", + "platform": "PlanetScope", + "wavelength": 490.0 + }, + "sentinel2a": { + "band": "B2", + "bandwidth": 66.0, + "name": "Blue", + "platform": "Sentinel-2A", + "wavelength": 492.4 + }, + "sentinel2b": { + "band": "B2", + "bandwidth": 66.0, + "name": "Blue", + "platform": "Sentinel-2B", + "wavelength": 492.1 + }, + "wv2": { + "band": "B2", + "bandwidth": 60.0, + "name": "Blue", + "platform": "WorldView-2", + "wavelength": 480.0 + }, + "wv3": { + "band": "B2", + "bandwidth": 60.0, + "name": "Blue", + "platform": "WorldView-3", + "wavelength": 480.0 + } + }, + "short_name": "B" + }, + "G": { + "common_name": "green", + "long_name": "Green", + "max_wavelength": 600, + "min_wavelength": 510, + "platforms": { + "landsat4": { + "band": "B2", + "bandwidth": 80.0, + "name": "Green", + "platform": "Landsat 4", + "wavelength": 560.0 + }, + "landsat5": { + "band": "B2", + "bandwidth": 80.0, + "name": "Green", + "platform": "Landsat 5", + "wavelength": 560.0 + }, + "landsat7": { + "band": "B2", + "bandwidth": 80.0, + "name": "Green", + "platform": "Landsat 7", + "wavelength": 560.0 + }, + "landsat8": { + "band": "B3", + "bandwidth": 60.0, + "name": "Green", + "platform": "Landsat 8", + "wavelength": 560.0 + }, + "landsat9": { + "band": "B3", + "bandwidth": 60.0, + "name": "Green", + "platform": "Landsat 9", + "wavelength": 560.0 + }, + "modis": { + "band": "B4", + "bandwidth": 20.0, + "name": "Green", + "platform": "Terra/Aqua: MODIS", + "wavelength": 555.0 + }, + "planetscope": { + "band": "B4", + "bandwidth": 36.0, + "name": "Green", + "platform": "PlanetScope", + "wavelength": 565.0 + }, + "sentinel2a": { + "band": "B3", + "bandwidth": 36.0, + "name": "Green", + "platform": "Sentinel-2A", + "wavelength": 559.8 + }, + "sentinel2b": { + "band": "B3", + "bandwidth": 36.0, + "name": "Green", + "platform": "Sentinel-2B", + "wavelength": 559.0 + }, + "wv2": { + "band": "B3", + "bandwidth": 70.0, + "name": "Green", + "platform": "WorldView-2", + "wavelength": 545.0 + }, + "wv3": { + "band": "B3", + "bandwidth": 70.0, + "name": "Green", + "platform": "WorldView-3", + "wavelength": 545.0 + } + }, + "short_name": "G" + }, + "G1": { + "common_name": "green", + "long_name": "Green 1", + "max_wavelength": 550, + "min_wavelength": 510, + "platforms": { + "modis": { + "band": "B11", + "bandwidth": 10.0, + "name": "Green", + "platform": "Terra/Aqua: MODIS", + "wavelength": 531.0 + }, + "planetscope": { + "band": "B3", + "bandwidth": 36.0, + "name": "Green", + "platform": "PlanetScope", + "wavelength": 531.0 + } + }, + "short_name": "G1" + }, + "N": { + "common_name": "nir", + "long_name": "Near-Infrared (NIR)", + "max_wavelength": 900, + "min_wavelength": 760, + "platforms": { + "landsat4": { + "band": "B4", + "bandwidth": 140.0, + "name": "Near-Infrared (NIR)", + "platform": "Landsat 4", + "wavelength": 830.0 + }, + "landsat5": { + "band": "B4", + "bandwidth": 140.0, + "name": "Near-Infrared (NIR)", + "platform": "Landsat 5", + "wavelength": 830.0 + }, + "landsat7": { + "band": "B4", + "bandwidth": 130.0, + "name": "Near-Infrared (NIR)", + "platform": "Landsat 7", + "wavelength": 835.0 + }, + "landsat8": { + "band": "B5", + "bandwidth": 30.0, + "name": "Near-Infrared (NIR)", + "platform": "Landsat 8", + "wavelength": 865.0 + }, + "landsat9": { + "band": "B5", + "bandwidth": 30.0, + "name": "Near-Infrared (NIR)", + "platform": "Landsat 9", + "wavelength": 865.0 + }, + "modis": { + "band": "B2", + "bandwidth": 35.0, + "name": "Near-Infrared (NIR)", + "platform": "Terra/Aqua: MODIS", + "wavelength": 858.5 + }, + "planetscope": { + "band": "B8", + "bandwidth": 40.0, + "name": "Near-Infrared (NIR)", + "platform": "PlanetScope", + "wavelength": 865.0 + }, + "sentinel2a": { + "band": "B8", + "bandwidth": 106.0, + "name": "Near-Infrared (NIR)", + "platform": "Sentinel-2A", + "wavelength": 832.8 + }, + "sentinel2b": { + "band": "B8", + "bandwidth": 106.0, + "name": "Near-Infrared (NIR)", + "platform": "Sentinel-2B", + "wavelength": 833.0 + }, + "wv2": { + "band": "B7", + "bandwidth": 125.0, + "name": "Near-IR1", + "platform": "WorldView-2", + "wavelength": 832.5 + }, + "wv3": { + "band": "B7", + "bandwidth": 125.0, + "name": "Near-IR1", + "platform": "WorldView-3", + "wavelength": 832.5 + } + }, + "short_name": "N" + }, + "N2": { + "common_name": "nir08", + "long_name": "Near-Infrared (NIR) 2", + "max_wavelength": 880, + "min_wavelength": 850, + "platforms": { + "sentinel2a": { + "band": "B8A", + "bandwidth": 21.0, + "name": "Near-Infrared (NIR) 2 (Red Edge 4 in Google Earth Engine)", + "platform": "Sentinel-2A", + "wavelength": 864.7 + }, + "sentinel2b": { + "band": "B8A", + "bandwidth": 21.0, + "name": "Near-Infrared (NIR) 2 (Red Edge 4 in Google Earth Engine)", + "platform": "Sentinel-2B", + "wavelength": 864.0 + } + }, + "short_name": "N2" + }, + "R": { + "common_name": "red", + "long_name": "Red", + "max_wavelength": 690, + "min_wavelength": 620, + "platforms": { + "landsat4": { + "band": "B3", + "bandwidth": 60.0, + "name": "Red", + "platform": "Landsat 4", + "wavelength": 660.0 + }, + "landsat5": { + "band": "B3", + "bandwidth": 60.0, + "name": "Red", + "platform": "Landsat 5", + "wavelength": 660.0 + }, + "landsat7": { + "band": "B3", + "bandwidth": 60.0, + "name": "Red", + "platform": "Landsat 7", + "wavelength": 660.0 + }, + "landsat8": { + "band": "B4", + "bandwidth": 30.0, + "name": "Red", + "platform": "Landsat 8", + "wavelength": 655.0 + }, + "landsat9": { + "band": "B4", + "bandwidth": 30.0, + "name": "Red", + "platform": "Landsat 9", + "wavelength": 655.0 + }, + "modis": { + "band": "B1", + "bandwidth": 50.0, + "name": "Red", + "platform": "Terra/Aqua: MODIS", + "wavelength": 645.0 + }, + "planetscope": { + "band": "B6", + "bandwidth": 30.0, + "name": "Red", + "platform": "PlanetScope", + "wavelength": 665.0 + }, + "sentinel2a": { + "band": "B4", + "bandwidth": 31.0, + "name": "Red", + "platform": "Sentinel-2A", + "wavelength": 664.6 + }, + "sentinel2b": { + "band": "B4", + "bandwidth": 31.0, + "name": "Red", + "platform": "Sentinel-2B", + "wavelength": 665.0 + }, + "wv2": { + "band": "B5", + "bandwidth": 60.0, + "name": "Red", + "platform": "WorldView-2", + "wavelength": 660.0 + }, + "wv3": { + "band": "B5", + "bandwidth": 60.0, + "name": "Red", + "platform": "WorldView-3", + "wavelength": 660.0 + } + }, + "short_name": "R" + }, + "RE1": { + "common_name": "rededge", + "long_name": "Red Edge 1", + "max_wavelength": 715, + "min_wavelength": 695, + "platforms": { + "planetscope": { + "band": "B7", + "bandwidth": 16.0, + "name": "Red Edge", + "platform": "PlanetScope", + "wavelength": 705.0 + }, + "sentinel2a": { + "band": "B5", + "bandwidth": 15.0, + "name": "Red Edge 1", + "platform": "Sentinel-2A", + "wavelength": 704.1 + }, + "sentinel2b": { + "band": "B5", + "bandwidth": 15.0, + "name": "Red Edge 1", + "platform": "Sentinel-2B", + "wavelength": 703.8 + } + }, + "short_name": "RE1" + }, + "RE2": { + "common_name": "rededge", + "long_name": "Red Edge 2", + "max_wavelength": 750, + "min_wavelength": 730, + "platforms": { + "sentinel2a": { + "band": "B6", + "bandwidth": 15.0, + "name": "Red Edge 2", + "platform": "Sentinel-2A", + "wavelength": 740.5 + }, + "sentinel2b": { + "band": "B6", + "bandwidth": 15.0, + "name": "Red Edge 2", + "platform": "Sentinel-2B", + "wavelength": 739.1 + } + }, + "short_name": "RE2" + }, + "RE3": { + "common_name": "rededge", + "long_name": "Red Edge 3", + "max_wavelength": 795, + "min_wavelength": 765, + "platforms": { + "sentinel2a": { + "band": "B7", + "bandwidth": 20.0, + "name": "Red Edge 3", + "platform": "Sentinel-2A", + "wavelength": 782.8 + }, + "sentinel2b": { + "band": "B7", + "bandwidth": 20.0, + "name": "Red Edge 3", + "platform": "Sentinel-2B", + "wavelength": 779.7 + } + }, + "short_name": "RE3" + }, + "S1": { + "common_name": "swir16", + "long_name": "Short-wave Infrared (SWIR) 1", + "max_wavelength": 1750, + "min_wavelength": 1550, + "platforms": { + "landsat4": { + "band": "B5", + "bandwidth": 200.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Landsat 4", + "wavelength": 1650.0 + }, + "landsat5": { + "band": "B5", + "bandwidth": 200.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Landsat 5", + "wavelength": 1650.0 + }, + "landsat7": { + "band": "B5", + "bandwidth": 200.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Landsat 7", + "wavelength": 1650.0 + }, + "landsat8": { + "band": "B6", + "bandwidth": 80.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Landsat 8", + "wavelength": 1610.0 + }, + "landsat9": { + "band": "B6", + "bandwidth": 80.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Landsat 9", + "wavelength": 1610.0 + }, + "modis": { + "band": "B6", + "bandwidth": 24.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Terra/Aqua: MODIS", + "wavelength": 1640.0 + }, + "sentinel2a": { + "band": "B11", + "bandwidth": 91.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Sentinel-2A", + "wavelength": 1613.7 + }, + "sentinel2b": { + "band": "B11", + "bandwidth": 94.0, + "name": "Short-wave Infrared (SWIR) 1", + "platform": "Sentinel-2B", + "wavelength": 1610.4 + } + }, + "short_name": "S1" + }, + "S2": { + "common_name": "swir22", + "long_name": "Short-wave Infrared (SWIR) 2", + "max_wavelength": 2350, + "min_wavelength": 2080, + "platforms": { + "landsat4": { + "band": "B7", + "bandwidth": 270.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Landsat 4", + "wavelength": 2215.0 + }, + "landsat5": { + "band": "B7", + "bandwidth": 270.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Landsat 5", + "wavelength": 2215.0 + }, + "landsat7": { + "band": "B7", + "bandwidth": 260.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Landsat 7", + "wavelength": 2220.0 + }, + "landsat8": { + "band": "B7", + "bandwidth": 180.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Landsat 8", + "wavelength": 2200.0 + }, + "landsat9": { + "band": "B7", + "bandwidth": 180.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Landsat 9", + "wavelength": 2200.0 + }, + "modis": { + "band": "B7", + "bandwidth": 50.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Terra/Aqua: MODIS", + "wavelength": 2130.0 + }, + "sentinel2a": { + "band": "B12", + "bandwidth": 175.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Sentinel-2A", + "wavelength": 2202.4 + }, + "sentinel2b": { + "band": "B12", + "bandwidth": 185.0, + "name": "Short-wave Infrared (SWIR) 2", + "platform": "Sentinel-2B", + "wavelength": 2185.7 + } + }, + "short_name": "S2" + }, + "T": { + "common_name": "lwir", + "long_name": "Thermal Infrared", + "max_wavelength": 12500, + "min_wavelength": 10400, + "platforms": { + "landsat4": { + "band": "B6", + "bandwidth": 2100.0, + "name": "Thermal Infrared", + "platform": "Landsat 4", + "wavelength": 11450.0 + }, + "landsat5": { + "band": "B6", + "bandwidth": 2100.0, + "name": "Thermal Infrared", + "platform": "Landsat 5", + "wavelength": 11450.0 + }, + "landsat7": { + "band": "B6", + "bandwidth": 2100.0, + "name": "Thermal Infrared", + "platform": "Landsat 7", + "wavelength": 11450.0 + } + }, + "short_name": "T" + }, + "T1": { + "common_name": "lwir11", + "long_name": "Thermal Infrared 1", + "max_wavelength": 11190, + "min_wavelength": 10600, + "platforms": { + "landsat8": { + "band": "B10", + "bandwidth": 590.0, + "name": "Thermal Infrared 1", + "platform": "Landsat 8", + "wavelength": 10895.0 + }, + "landsat9": { + "band": "B10", + "bandwidth": 590.0, + "name": "Thermal Infrared 1", + "platform": "Landsat 9", + "wavelength": 10895.0 + } + }, + "short_name": "T1" + }, + "T2": { + "common_name": "lwir12", + "long_name": "Thermal Infrared 2", + "max_wavelength": 12510, + "min_wavelength": 11500, + "platforms": { + "landsat8": { + "band": "B11", + "bandwidth": 1010.0, + "name": "Thermal Infrared 2", + "platform": "Landsat 8", + "wavelength": 12005.0 + }, + "landsat9": { + "band": "B11", + "bandwidth": 1010.0, + "name": "Thermal Infrared 2", + "platform": "Landsat 9", + "wavelength": 12005.0 + } + }, + "short_name": "T2" + }, + "WV": { + "common_name": "nir09", + "long_name": "Water Vapour", + "max_wavelength": 960, + "min_wavelength": 930, + "platforms": { + "sentinel2a": { + "band": "B9", + "bandwidth": 20.0, + "name": "Water Vapour", + "platform": "Sentinel-2A", + "wavelength": 945.1 + }, + "sentinel2b": { + "band": "B9", + "bandwidth": 21.0, + "name": "Water Vapour", + "platform": "Sentinel-2B", + "wavelength": 943.2 + } + }, + "short_name": "WV" + }, + "Y": { + "common_name": "yellow", + "long_name": "Yellow", + "max_wavelength": 625, + "min_wavelength": 585, + "platforms": { + "planetscope": { + "band": "B5", + "bandwidth": 20.0, + "name": "Yellow", + "platform": "PlanetScope", + "wavelength": 610.0 + }, + "wv2": { + "band": "B4", + "bandwidth": 40.0, + "name": "Yellow", + "platform": "WorldView-2", + "wavelength": 605.0 + }, + "wv3": { + "band": "B4", + "bandwidth": 40.0, + "name": "Yellow", + "platform": "WorldView-3", + "wavelength": 605.0 + } + }, + "short_name": "Y" + } +} diff --git a/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/constants.json b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/constants.json new file mode 100644 index 000000000..3aa7cd7b5 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/constants.json @@ -0,0 +1,107 @@ +{ + "C1": { + "default": 6.0, + "description": "Coefficient 1 for the aerosol resistance term", + "short_name": "C1" + }, + "C2": { + "default": 7.5, + "description": "Coefficient 2 for the aerosol resistance term", + "short_name": "C2" + }, + "L": { + "default": 1.0, + "description": "Canopy background adjustment", + "short_name": "L" + }, + "PAR": { + "default": null, + "description": "Photosynthetically Active Radiation", + "short_name": "PAR" + }, + "alpha": { + "default": 0.1, + "description": "Weighting coefficient used for WDRVI", + "short_name": "alpha" + }, + "beta": { + "default": 0.05, + "description": "Calibration parameter used for NDSInw", + "short_name": "beta" + }, + "c": { + "default": 1.0, + "description": "Trade-off parameter in the polynomial kernel", + "short_name": "c" + }, + "cexp": { + "default": 1.16, + "description": "Exponent used for OCVI", + "short_name": "cexp" + }, + "fdelta": { + "default": 0.581, + "description": "Adjustment factor used for SEVI", + "short_name": "fdelta" + }, + "g": { + "default": 2.5, + "description": "Gain factor", + "short_name": "g" + }, + "gamma": { + "default": 1.0, + "description": "Weighting coefficient used for ARVI", + "short_name": "gamma" + }, + "k": { + "default": 0.0, + "description": "Slope parameter by soil used for NIRvH2", + "short_name": "k" + }, + "lambdaG": { + "default": null, + "description": "Green wavelength (nm) used for NDGI", + "short_name": "lambdaG" + }, + "lambdaN": { + "default": null, + "description": "NIR wavelength (nm) used for NIRvH2 and NDGI", + "short_name": "lambdaN" + }, + "lambdaR": { + "default": null, + "description": "Red wavelength (nm) used for NIRvH2 and NDGI", + "short_name": "lambdaR" + }, + "nexp": { + "default": 2.0, + "description": "Exponent used for GDVI", + "short_name": "nexp" + }, + "omega": { + "default": 2.0, + "description": "Weighting coefficient used for MBWI", + "short_name": "omega" + }, + "p": { + "default": 2.0, + "description": "Kernel degree in the polynomial kernel", + "short_name": "p" + }, + "sigma": { + "default": 0.5, + "description": "Length-scale parameter in the RBF kernel", + "short_name": "sigma" + }, + "sla": { + "default": 1.0, + "description": "Soil line slope", + "short_name": "sla" + }, + "slb": { + "default": 0.0, + "description": "Soil line intercept", + "short_name": "slb" + } +} diff --git a/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/spectral-indices-dict.json b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/spectral-indices-dict.json new file mode 100644 index 000000000..04fbce636 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/resources/awesome-spectral-indices/spectral-indices-dict.json @@ -0,0 +1,4616 @@ +{ + "SpectralIndices": { + "AFRI1600": { + "application_domain": "vegetation", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-17", + "formula": "(N - 0.66 * S1) / (N + 0.66 * S1)", + "long_name": "Aerosol Free Vegetation Index (1600 nm)", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00190-0", + "short_name": "AFRI1600" + }, + "AFRI2100": { + "application_domain": "vegetation", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-17", + "formula": "(N - 0.5 * S2) / (N + 0.5 * S2)", + "long_name": "Aerosol Free Vegetation Index (2100 nm)", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00190-0", + "short_name": "AFRI2100" + }, + "ANDWI": { + "application_domain": "water", + "bands": [ + "B", + "G", + "R", + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "(B + G + R - N - S1 - S2)/(B + G + R + N + S1 + S2)", + "long_name": "Augmented Normalized Difference Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.envsoft.2021.105030", + "short_name": "ANDWI" + }, + "ARI": { + "application_domain": "vegetation", + "bands": [ + "G", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(1 / G) - (1 / RE1)", + "long_name": "Anthocyanin Reflectance Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1562/0031-8655(2001)074%3C0038:OPANEO%3E2.0.CO;2", + "short_name": "ARI" + }, + "ARI2": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "N * ((1 / G) - (1 / RE1))", + "long_name": "Anthocyanin Reflectance Index 2", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1562/0031-8655(2001)074%3C0038:OPANEO%3E2.0.CO;2", + "short_name": "ARI2" + }, + "ARVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "gamma", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "(N - (R - gamma * (R - B))) / (N + (R - gamma * (R - B)))", + "long_name": "Atmospherically Resistant Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1109/36.134076", + "short_name": "ARVI" + }, + "ATSAVI": { + "application_domain": "vegetation", + "bands": [ + "sla", + "N", + "R", + "slb" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "sla * (N - sla * R - slb) / (sla * N + R - sla * slb + 0.08 * (1 + sla ** 2.0))", + "long_name": "Adjusted Transformed Soil-Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(91)90009-U", + "short_name": "ATSAVI" + }, + "AVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(N * (1.0 - R) * (N - R)) ** (1/3)", + "long_name": "Advanced Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.465.8749&rep=rep1&type=pdf", + "short_name": "AVI" + }, + "AWEInsh": { + "application_domain": "water", + "bands": [ + "G", + "S1", + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "4.0 * (G - S1) - 0.25 * N + 2.75 * S2", + "long_name": "Automated Water Extraction Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2013.08.029", + "short_name": "AWEInsh" + }, + "AWEIsh": { + "application_domain": "water", + "bands": [ + "B", + "G", + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "B + 2.5 * G - 1.5 * (N + S1) - 0.25 * S2", + "long_name": "Automated Water Extraction Index with Shadows Elimination", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2013.08.029", + "short_name": "AWEIsh" + }, + "BAI": { + "application_domain": "burn", + "bands": [ + "R", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "1.0 / ((0.1 - R) ** 2.0 + (0.06 - N) ** 2.0)", + "long_name": "Burned Area Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://digital.csic.es/bitstream/10261/6426/1/Martin_Isabel_Serie_Geografica.pdf", + "short_name": "BAI" + }, + "BAIM": { + "application_domain": "burn", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-20", + "formula": "1.0/((0.05 - N) ** 2.0) + ((0.2 - S2) ** 2.0)", + "long_name": "Burned Area Index adapted to MODIS", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.foreco.2006.08.248", + "short_name": "BAIM" + }, + "BAIS2": { + "application_domain": "burn", + "bands": [ + "RE2", + "RE3", + "N2", + "R", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(1.0 - ((RE2 * RE3 * N2) / R) ** 0.5) * (((S2 - N2)/(S2 + N2) ** 0.5) + 1.0)", + "long_name": "Burned Area Index for Sentinel 2", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/ecrs-2-05177", + "short_name": "BAIS2" + }, + "BCC": { + "application_domain": "vegetation", + "bands": [ + "B", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "B / (R + G + B)", + "long_name": "Blue Chromatic Coordinate", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(87)90088-5", + "short_name": "BCC" + }, + "BI": { + "application_domain": "soil", + "bands": [ + "S1", + "R", + "N", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "((S1 + R) - (N + B))/((S1 + R) + (N + B))", + "long_name": "Bare Soil Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.465.8749&rep=rep1&type=pdf", + "short_name": "BI" + }, + "BITM": { + "application_domain": "soil", + "bands": [ + "B", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-11-20", + "formula": "(((B**2.0)+(G**2.0)+(R**2.0))/3.0)**0.5", + "long_name": "Landsat TM-based Brightness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(98)00030-3", + "short_name": "BITM" + }, + "BIXS": { + "application_domain": "soil", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2022-11-20", + "formula": "(((G**2.0)+(R**2.0))/2.0)**0.5", + "long_name": "SPOT HRV XS-based Brightness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(98)00030-3", + "short_name": "BIXS" + }, + "BLFEI": { + "application_domain": "urban", + "bands": [ + "G", + "R", + "S2", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-02-09", + "formula": "(((G+R+S2)/3.0)-S1)/(((G+R+S2)/3.0)+S1)", + "long_name": "Built-Up Land Features Extraction Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/10106049.2018.1497094", + "short_name": "BLFEI" + }, + "BNDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "B" + ], + "contributor": "https://github.com/MATRIX4284", + "date_of_addition": "2021-04-07", + "formula": "(N - B)/(N + B)", + "long_name": "Blue Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S1672-6308(07)60027-4", + "short_name": "BNDVI" + }, + "BRBA": { + "application_domain": "urban", + "bands": [ + "R", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "R/S1", + "long_name": "Band Ratio for Built-up Area", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.omicsonline.org/scientific-reports/JGRS-SR136.pdf", + "short_name": "BRBA" + }, + "BWDRVI": { + "application_domain": "vegetation", + "bands": [ + "alpha", + "N", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(alpha * N - B) / (alpha * N + B)", + "long_name": "Blue Wide Dynamic Range Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2135/cropsci2007.01.0031", + "short_name": "BWDRVI" + }, + "BaI": { + "application_domain": "soil", + "bands": [ + "R", + "S1", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "R + S1 - N", + "long_name": "Bareness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1109/IGARSS.2005.1525743", + "short_name": "BaI" + }, + "CCI": { + "application_domain": "vegetation", + "bands": [ + "G1", + "R" + ], + "contributor": "https://github.com/joanvlasschaert", + "date_of_addition": "2023-03-12", + "formula": "(G1 - R)/(G1 + R)", + "long_name": "Chlorophyll Carotenoid Index", + "platforms": [ + "MODIS" + ], + "reference": "https://doi.org/10.1073/pnas.1606162113", + "short_name": "CCI" + }, + "CIG": { + "application_domain": "vegetation", + "bands": [ + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N / G) - 1.0", + "long_name": "Chlorophyll Index Green", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1078/0176-1617-00887", + "short_name": "CIG" + }, + "CIRE": { + "application_domain": "vegetation", + "bands": [ + "N", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "(N / RE1) - 1", + "long_name": "Chlorophyll Index Red Edge", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1078/0176-1617-00887", + "short_name": "CIRE" + }, + "CSI": { + "application_domain": "burn", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "N/S2", + "long_name": "Char Soil Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2005.04.014", + "short_name": "CSI" + }, + "CSIT": { + "application_domain": "burn", + "bands": [ + "N", + "S2", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "N / (S2 * T / 10000.0)", + "long_name": "Char Soil Index Thermal", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160600954704", + "short_name": "CSIT" + }, + "CVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N * R) / (G ** 2.0)", + "long_name": "Chlorophyll Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1007/s11119-010-9204-3", + "short_name": "CVI" + }, + "DBI": { + "application_domain": "urban", + "bands": [ + "B", + "T1", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "((B - T1)/(B + T1)) - ((N - R)/(N + R))", + "long_name": "Dry Built-Up Index", + "platforms": [ + "Landsat-OLI" + ], + "reference": "https://doi.org/10.3390/land7030081", + "short_name": "DBI" + }, + "DBSI": { + "application_domain": "soil", + "bands": [ + "S1", + "G", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "((S1 - G)/(S1 + G)) - ((N - R)/(N + R))", + "long_name": "Dry Bareness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/land7030081", + "short_name": "DBSI" + }, + "DPDD": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "(VV + VH)/2.0 ** 0.5", + "long_name": "Dual-Pol Diagonal Distance", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1016/j.rse.2018.09.003", + "short_name": "DPDD" + }, + "DSI": { + "application_domain": "vegetation", + "bands": [ + "S1", + "N" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2022-10-26", + "formula": "S1/N", + "long_name": "Drought Stress Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.asprs.org/wp-content/uploads/pers/1999journal/apr/1999_apr_495-501.pdf", + "short_name": "DSI" + }, + "DSWI1": { + "application_domain": "vegetation", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-29", + "formula": "N/S1", + "long_name": "Disease-Water Stress Index 1", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160310001618031", + "short_name": "DSWI1" + }, + "DSWI2": { + "application_domain": "vegetation", + "bands": [ + "S1", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-29", + "formula": "S1/G", + "long_name": "Disease-Water Stress Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160310001618031", + "short_name": "DSWI2" + }, + "DSWI3": { + "application_domain": "vegetation", + "bands": [ + "S1", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-29", + "formula": "S1/R", + "long_name": "Disease-Water Stress Index 3", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160310001618031", + "short_name": "DSWI3" + }, + "DSWI4": { + "application_domain": "vegetation", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-29", + "formula": "G/R", + "long_name": "Disease-Water Stress Index 4", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431160310001618031", + "short_name": "DSWI4" + }, + "DSWI5": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "S1", + "R" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2022-10-26", + "formula": "(N + G)/(S1 + R)", + "long_name": "Disease-Water Stress Index 5", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160310001618031", + "short_name": "DSWI5" + }, + "DVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "N - R", + "long_name": "Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(94)00114-3", + "short_name": "DVI" + }, + "DVIplus": { + "application_domain": "vegetation", + "bands": [ + "lambdaN", + "lambdaR", + "lambdaG", + "G", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-20", + "formula": "((lambdaN - lambdaR)/(lambdaN - lambdaG)) * G + (1.0 - ((lambdaN - lambdaR)/(lambdaN - lambdaG))) * N - R", + "long_name": "Difference Vegetation Index Plus", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2019.03.028", + "short_name": "DVIplus" + }, + "DpRVIHH": { + "application_domain": "radar", + "bands": [ + "HV", + "HH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-12-25", + "formula": "(4.0 * HV)/(HH + HV)", + "long_name": "Dual-Polarized Radar Vegetation Index HH", + "platforms": [ + "Sentinel-1 (Dual Polarisation HH-HV)" + ], + "reference": "https://www.tandfonline.com/doi/abs/10.5589/m12-043", + "short_name": "DpRVIHH" + }, + "DpRVIVV": { + "application_domain": "radar", + "bands": [ + "VH", + "VV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-12-25", + "formula": "(4.0 * VH)/(VV + VH)", + "long_name": "Dual-Polarized Radar Vegetation Index VV", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.3390/app9040655", + "short_name": "DpRVIVV" + }, + "EBBI": { + "application_domain": "urban", + "bands": [ + "S1", + "N", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "(S1 - N) / (10.0 * ((S1 + T) ** 0.5))", + "long_name": "Enhanced Built-Up and Bareness Index", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.3390/rs4102957", + "short_name": "EBBI" + }, + "EMBI": { + "application_domain": "soil", + "bands": [ + "S1", + "S2", + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "((((S1 - S2 - N)/(S1 + S2 + N)) + 0.5) - ((G - S1)/(G + S1)) - 0.5)/((((S1 - S2 - N)/(S1 + S2 + N)) + 0.5) + ((G - S1)/(G + S1)) + 1.5)", + "long_name": "Enhanced Modified Bare Soil Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.jag.2022.102703", + "short_name": "EMBI" + }, + "EVI": { + "application_domain": "vegetation", + "bands": [ + "g", + "N", + "R", + "C1", + "C2", + "B", + "L" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "g * (N - R) / (N + C1 * R - C2 * B + L)", + "long_name": "Enhanced Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(96)00112-5", + "short_name": "EVI" + }, + "EVI2": { + "application_domain": "vegetation", + "bands": [ + "g", + "N", + "R", + "L" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "g * (N - R) / (N + 2.4 * R + L)", + "long_name": "Two-Band Enhanced Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2008.06.006", + "short_name": "EVI2" + }, + "ExG": { + "application_domain": "vegetation", + "bands": [ + "G", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "2 * G - R - B", + "long_name": "Excess Green Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.13031/2013.27838", + "short_name": "ExG" + }, + "ExGR": { + "application_domain": "vegetation", + "bands": [ + "G", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(2.0 * G - R - B) - (1.3 * R - G)", + "long_name": "ExG - ExR Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.compag.2008.03.009", + "short_name": "ExGR" + }, + "ExR": { + "application_domain": "vegetation", + "bands": [ + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "1.3 * R - G", + "long_name": "Excess Red Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1117/12.336896", + "short_name": "ExR" + }, + "FCVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-20", + "formula": "N - ((R + G + B)/3.0)", + "long_name": "Fluorescence Correction Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2020.111676", + "short_name": "FCVI" + }, + "GARI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "B", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - (G - (B - R))) / (N - (G + (B - R)))", + "long_name": "Green Atmospherically Resistant Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(96)00072-7", + "short_name": "GARI" + }, + "GBNDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - (G + B))/(N + (G + B))", + "long_name": "Green-Blue Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S1672-6308(07)60027-4", + "short_name": "GBNDVI" + }, + "GCC": { + "application_domain": "vegetation", + "bands": [ + "G", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "G / (R + G + B)", + "long_name": "Green Chromatic Coordinate", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(87)90088-5", + "short_name": "GCC" + }, + "GDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "nexp", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "((N ** nexp) - (R ** nexp)) / ((N ** nexp) + (R ** nexp))", + "long_name": "Generalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.3390/rs6021211", + "short_name": "GDVI" + }, + "GEMI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "((2.0*((N ** 2.0)-(R ** 2.0)) + 1.5*N + 0.5*R)/(N + R + 0.5))*(1.0 - 0.25*((2.0 * ((N ** 2.0) - (R ** 2)) + 1.5 * N + 0.5 * R)/(N + R + 0.5)))-((R - 0.125)/(1 - R))", + "long_name": "Global Environment Monitoring Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://dx.doi.org/10.1007/bf00031911", + "short_name": "GEMI" + }, + "GLI": { + "application_domain": "vegetation", + "bands": [ + "G", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(2.0 * G - R - B) / (2.0 * G + R + B)", + "long_name": "Green Leaf Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://dx.doi.org/10.1080/10106040108542184", + "short_name": "GLI" + }, + "GM1": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "RE2/G", + "long_name": "Gitelson and Merzlyak Index 1", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(96)80284-7", + "short_name": "GM1" + }, + "GM2": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "RE2/RE1", + "long_name": "Gitelson and Merzlyak Index 2", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(96)80284-7", + "short_name": "GM2" + }, + "GNDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - G)/(N + G)", + "long_name": "Green Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(96)00072-7", + "short_name": "GNDVI" + }, + "GOSAVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(N - G) / (N + G + 0.16)", + "long_name": "Green Optimized Soil Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "GOSAVI" + }, + "GRNDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - (G + R))/(N + (G + R))", + "long_name": "Green-Red Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S1672-6308(07)60027-4", + "short_name": "GRNDVI" + }, + "GRVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "N/G", + "long_name": "Green Ratio Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "GRVI" + }, + "GSAVI": { + "application_domain": "vegetation", + "bands": [ + "L", + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(1.0 + L) * (N - G) / (N + G + L)", + "long_name": "Green Soil Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "GSAVI" + }, + "GVMI": { + "application_domain": "vegetation", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "((N + 0.1) - (S2 + 0.02)) / ((N + 0.1) + (S2 + 0.02))", + "long_name": "Global Vegetation Moisture Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00037-8", + "short_name": "GVMI" + }, + "IAVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "gamma", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(N - (R - gamma * (B - R)))/(N + (R - gamma * (B - R)))", + "long_name": "New Atmospherically Resistant Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://www.jipb.net/EN/abstract/abstract23925.shtml", + "short_name": "IAVI" + }, + "IBI": { + "application_domain": "urban", + "bands": [ + "S1", + "N", + "R", + "L", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-02-09", + "formula": "(((S1-N)/(S1+N))-(((N-R)*(1.0+L)/(N+R+L))+((G-S1)/(G+S1)))/2.0)/(((S1-N)/(S1+N))+(((N-R)*(1.0+L)/(N+R+L))+((G-S1)/(G+S1)))/2.0)", + "long_name": "Index-Based Built-Up Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160802039957", + "short_name": "IBI" + }, + "IKAW": { + "application_domain": "vegetation", + "bands": [ + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(R - B)/(R + B)", + "long_name": "Kawashima Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1006/anbo.1997.0544", + "short_name": "IKAW" + }, + "IPVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "N/(N + R)", + "long_name": "Infrared Percentage Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(90)90085-Z", + "short_name": "IPVI" + }, + "IRECI": { + "application_domain": "vegetation", + "bands": [ + "RE3", + "R", + "RE1", + "RE2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "(RE3 - R) / (RE1 / RE2)", + "long_name": "Inverted Red-Edge Chlorophyll Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.isprsjprs.2013.04.007", + "short_name": "IRECI" + }, + "LSWI": { + "application_domain": "water", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-20", + "formula": "(N - S1)/(N + S1)", + "long_name": "Land Surface Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2003.11.008", + "short_name": "LSWI" + }, + "MBI": { + "application_domain": "soil", + "bands": [ + "S1", + "S2", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "((S1 - S2 - N)/(S1 + S2 + N)) + 0.5", + "long_name": "Modified Bare Soil Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/land10030231", + "short_name": "MBI" + }, + "MBWI": { + "application_domain": "water", + "bands": [ + "omega", + "G", + "R", + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "(omega * G) - R - N - S1 - S2", + "long_name": "Multi-Band Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.jag.2018.01.018", + "short_name": "MBWI" + }, + "MCARI": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "((RE1 - R) - 0.2 * (RE1 - G)) * (RE1 / R)", + "long_name": "Modified Chlorophyll Absorption in Reflectance Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "http://dx.doi.org/10.1016/S0034-4257(00)00113-9", + "short_name": "MCARI" + }, + "MCARI1": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "1.2 * (2.5 * (N - R) - 1.3 * (N - G))", + "long_name": "Modified Chlorophyll Absorption in Reflectance Index 1", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2003.12.013", + "short_name": "MCARI1" + }, + "MCARI2": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(1.5 * (2.5 * (N - R) - 1.3 * (N - G))) / ((((2.0 * N + 1) ** 2) - (6.0 * N - 5 * (R ** 0.5)) - 0.5) ** 0.5)", + "long_name": "Modified Chlorophyll Absorption in Reflectance Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2003.12.013", + "short_name": "MCARI2" + }, + "MCARI705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "((RE2 - RE1) - 0.2 * (RE2 - G)) * (RE2 / RE1)", + "long_name": "Modified Chlorophyll Absorption in Reflectance Index (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.agrformet.2008.03.005", + "short_name": "MCARI705" + }, + "MCARIOSAVI": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R", + "G", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(((RE1 - R) - 0.2 * (RE1 - G)) * (RE1 / R)) / (1.16 * (N - R) / (N + R + 0.16))", + "long_name": "MCARI/OSAVI Ratio", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(00)00113-9", + "short_name": "MCARIOSAVI" + }, + "MCARIOSAVI705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(((RE2 - RE1) - 0.2 * (RE2 - G)) * (RE2 / RE1)) / (1.16 * (RE2 - RE1) / (RE2 + RE1 + 0.16))", + "long_name": "MCARI/OSAVI Ratio (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.agrformet.2008.03.005", + "short_name": "MCARIOSAVI705" + }, + "MGRVI": { + "application_domain": "vegetation", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(G ** 2.0 - R ** 2.0) / (G ** 2.0 + R ** 2.0)", + "long_name": "Modified Green Red Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.jag.2015.02.012", + "short_name": "MGRVI" + }, + "MIRBI": { + "application_domain": "burn", + "bands": [ + "S2", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "10.0 * S2 - 9.8 * S1 + 2.0", + "long_name": "Mid-Infrared Burn Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160110053185", + "short_name": "MIRBI" + }, + "MLSWI26": { + "application_domain": "water", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-20", + "formula": "(1.0 - N - S1)/(1.0 - N + S1)", + "long_name": "Modified Land Surface Water Index (MODIS Bands 2 and 6)", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs71215805", + "short_name": "MLSWI26" + }, + "MLSWI27": { + "application_domain": "water", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-20", + "formula": "(1.0 - N - S2)/(1.0 - N + S2)", + "long_name": "Modified Land Surface Water Index (MODIS Bands 2 and 7)", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs71215805", + "short_name": "MLSWI27" + }, + "MNDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - S2)/(N + S2)", + "long_name": "Modified Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/014311697216810", + "short_name": "MNDVI" + }, + "MNDWI": { + "application_domain": "water", + "bands": [ + "G", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(G - S1) / (G + S1)", + "long_name": "Modified Normalized Difference Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160600589179", + "short_name": "MNDWI" + }, + "MNLI": { + "application_domain": "vegetation", + "bands": [ + "L", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "(1 + L)*((N ** 2) - R)/((N ** 2) + R + L)", + "long_name": "Modified Non-Linear Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1109/TGRS.2003.812910", + "short_name": "MNLI" + }, + "MRBVI": { + "application_domain": "vegetation", + "bands": [ + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(R ** 2.0 - B ** 2.0)/(R ** 2.0 + B ** 2.0)", + "long_name": "Modified Red Blue Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.3390/s20185055", + "short_name": "MRBVI" + }, + "MSAVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "0.5 * (2.0 * N + 1 - (((2 * N + 1) ** 2) - 8 * (N - R)) ** 0.5)", + "long_name": "Modified Soil-Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(94)90134-1", + "short_name": "MSAVI" + }, + "MSI": { + "application_domain": "vegetation", + "bands": [ + "S1", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "S1/N", + "long_name": "Moisture Stress Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/0034-4257(89)90046-1", + "short_name": "MSI" + }, + "MSR": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(N / R - 1) / ((N / R + 1) ** 0.5)", + "long_name": "Modified Simple Ratio", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/07038992.1996.10855178", + "short_name": "MSR" + }, + "MSR705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(RE2 / RE1 - 1) / ((RE2 / RE1 + 1) ** 0.5)", + "long_name": "Modified Simple Ratio (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.agrformet.2008.03.005", + "short_name": "MSR705" + }, + "MTCI": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "(RE2 - RE1) / (RE1 - R)", + "long_name": "MERIS Terrestrial Chlorophyll Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1080/0143116042000274015", + "short_name": "MTCI" + }, + "MTVI1": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "1.2 * (1.2 * (N - G) - 2.5 * (R - G))", + "long_name": "Modified Triangular Vegetation Index 1", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2003.12.013", + "short_name": "MTVI1" + }, + "MTVI2": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(1.5 * (1.2 * (N - G) - 2.5 * (R - G))) / ((((2.0 * N + 1) ** 2) - (6.0 * N - 5 * (R ** 0.5)) - 0.5) ** 0.5)", + "long_name": "Modified Triangular Vegetation Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2003.12.013", + "short_name": "MTVI2" + }, + "MuWIR": { + "application_domain": "water", + "bands": [ + "B", + "G", + "N", + "S2", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "-4.0 * ((B - G)/(B + G)) + 2.0 * ((G - N)/(G + N)) + 2.0 * ((G - S2)/(G + S2)) - ((G - S1)/(G + S1))", + "long_name": "Revised Multi-Spectral Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs10101643", + "short_name": "MuWIR" + }, + "NBAI": { + "application_domain": "urban", + "bands": [ + "S2", + "S1", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "((S2 - S1)/G)/((S2 + S1)/G)", + "long_name": "Normalized Built-up Area Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.omicsonline.org/scientific-reports/JGRS-SR136.pdf", + "short_name": "NBAI" + }, + "NBLI": { + "application_domain": "soil", + "bands": [ + "R", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(R - T)/(R + T)", + "long_name": "Normalized Difference Bare Land Index", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.3390/rs9030249", + "short_name": "NBLI" + }, + "NBLIOLI": { + "application_domain": "soil", + "bands": [ + "R", + "T1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2023-03-12", + "formula": "(R - T1)/(R + T1)", + "long_name": "Normalized Difference Bare Land Index for Landsat-OLI", + "platforms": [ + "Landsat-OLI" + ], + "reference": "https://doi.org/10.3390/rs9030249", + "short_name": "NBLIOLI" + }, + "NBR": { + "application_domain": "burn", + "bands": [ + "N", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - S2) / (N + S2)", + "long_name": "Normalized Burn Ratio", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3133/ofr0211", + "short_name": "NBR" + }, + "NBR2": { + "application_domain": "burn", + "bands": [ + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(S1 - S2) / (S1 + S2)", + "long_name": "Normalized Burn Ratio 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.usgs.gov/core-science-systems/nli/landsat/landsat-normalized-burn-ratio-2", + "short_name": "NBR2" + }, + "NBRSWIR": { + "application_domain": "burn", + "bands": [ + "S2", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "(S2 - S1 - 0.02)/(S2 + S1 + 0.1)", + "long_name": "Normalized Burn Ratio SWIR", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/22797254.2020.1738900", + "short_name": "NBRSWIR" + }, + "NBRT1": { + "application_domain": "burn", + "bands": [ + "N", + "S2", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - (S2 * T / 10000.0)) / (N + (S2 * T / 10000.0))", + "long_name": "Normalized Burn Ratio Thermal 1", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160500239008", + "short_name": "NBRT1" + }, + "NBRT2": { + "application_domain": "burn", + "bands": [ + "N", + "T", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "((N / (T / 10000.0)) - S2) / ((N / (T / 10000.0)) + S2)", + "long_name": "Normalized Burn Ratio Thermal 2", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160500239008", + "short_name": "NBRT2" + }, + "NBRT3": { + "application_domain": "burn", + "bands": [ + "N", + "T", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "((N - (T / 10000.0)) - S2) / ((N - (T / 10000.0)) + S2)", + "long_name": "Normalized Burn Ratio Thermal 3", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160500239008", + "short_name": "NBRT3" + }, + "NBRplus": { + "application_domain": "burn", + "bands": [ + "S2", + "N2", + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "(S2 - N2 - G - B)/(S2 + N2 + G + B)", + "long_name": "Normalized Burn Ratio Plus", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/rs14071727", + "short_name": "NBRplus" + }, + "NBSIMS": { + "application_domain": "snow", + "bands": [ + "G", + "R", + "N", + "B", + "S2", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "0.36 * (G + R + N) - (((B + S2)/G) + S1)", + "long_name": "Non-Binary Snow Index for Multi-Component Surfaces", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs13142777", + "short_name": "NBSIMS" + }, + "NBUI": { + "application_domain": "urban", + "bands": [ + "S1", + "N", + "T", + "R", + "L", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "((S1 - N)/(10.0 * (T + S1) ** 0.5)) - (((N - R) * (1.0 + L))/(N - R + L)) - (G - S1)/(G + S1)", + "long_name": "New Built-Up Index", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://hdl.handle.net/1959.11/29500", + "short_name": "NBUI" + }, + "ND705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(RE2 - RE1)/(RE2 + RE1)", + "long_name": "Normalized Difference (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00010-X", + "short_name": "ND705" + }, + "NDBI": { + "application_domain": "urban", + "bands": [ + "S1", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(S1 - N) / (S1 + N)", + "long_name": "Normalized Difference Built-Up Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "http://dx.doi.org/10.1080/01431160304987", + "short_name": "NDBI" + }, + "NDBaI": { + "application_domain": "soil", + "bands": [ + "S1", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "(S1 - T) / (S1 + T)", + "long_name": "Normalized Difference Bareness Index", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1109/IGARSS.2005.1526319", + "short_name": "NDBaI" + }, + "NDCI": { + "application_domain": "water", + "bands": [ + "RE1", + "R" + ], + "contributor": "https://github.com/kalab-oto", + "date_of_addition": "2022-10-10", + "formula": "(RE1 - R)/(RE1 + R)", + "long_name": "Normalized Difference Chlorophyll Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.rse.2011.10.016", + "short_name": "NDCI" + }, + "NDDI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(((N - R)/(N + R)) - ((G - N)/(G + N)))/(((N - R)/(N + R)) + ((G - N)/(G + N)))", + "long_name": "Normalized Difference Drought Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1029/2006GL029127", + "short_name": "NDDI" + }, + "NDGI": { + "application_domain": "vegetation", + "bands": [ + "lambdaN", + "lambdaR", + "lambdaG", + "G", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-20", + "formula": "(((lambdaN - lambdaR)/(lambdaN - lambdaG)) * G + (1.0 - ((lambdaN - lambdaR)/(lambdaN - lambdaG))) * N - R)/(((lambdaN - lambdaR)/(lambdaN - lambdaG)) * G + (1.0 - ((lambdaN - lambdaR)/(lambdaN - lambdaG))) * N + R)", + "long_name": "Normalized Difference Greenness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2019.03.028", + "short_name": "NDGI" + }, + "NDGlaI": { + "application_domain": "snow", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(G - R)/(G + R)", + "long_name": "Normalized Difference Glacier Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431160802385459", + "short_name": "NDGlaI" + }, + "NDII": { + "application_domain": "vegetation", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-20", + "formula": "(N - S1)/(N + S1)", + "long_name": "Normalized Difference Infrared Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.asprs.org/wp-content/uploads/pers/1983journal/jan/1983_jan_77-83.pdf", + "short_name": "NDII" + }, + "NDISIb": { + "application_domain": "urban", + "bands": [ + "T", + "B", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(T - (B + N + S1) / 3.0)/(T + (B + N + S1) / 3.0)", + "long_name": "Normalized Difference Impervious Surface Index Blue", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.14358/PERS.76.5.557", + "short_name": "NDISIb" + }, + "NDISIg": { + "application_domain": "urban", + "bands": [ + "T", + "G", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(T - (G + N + S1) / 3.0)/(T + (G + N + S1) / 3.0)", + "long_name": "Normalized Difference Impervious Surface Index Green", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.14358/PERS.76.5.557", + "short_name": "NDISIg" + }, + "NDISImndwi": { + "application_domain": "urban", + "bands": [ + "T", + "G", + "S1", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(T - (((G - S1)/(G + S1)) + N + S1) / 3.0)/(T + (((G - S1)/(G + S1)) + N + S1) / 3.0)", + "long_name": "Normalized Difference Impervious Surface Index with MNDWI", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.14358/PERS.76.5.557", + "short_name": "NDISImndwi" + }, + "NDISIndwi": { + "application_domain": "urban", + "bands": [ + "T", + "G", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(T - (((G - N)/(G + N)) + N + S1) / 3.0)/(T + (((G - N)/(G + N)) + N + S1) / 3.0)", + "long_name": "Normalized Difference Impervious Surface Index with NDWI", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.14358/PERS.76.5.557", + "short_name": "NDISIndwi" + }, + "NDISIr": { + "application_domain": "urban", + "bands": [ + "T", + "R", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(T - (R + N + S1) / 3.0)/(T + (R + N + S1) / 3.0)", + "long_name": "Normalized Difference Impervious Surface Index Red", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.14358/PERS.76.5.557", + "short_name": "NDISIr" + }, + "NDMI": { + "application_domain": "vegetation", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/bpurinton", + "date_of_addition": "2021-12-01", + "formula": "(N - S1)/(N + S1)", + "long_name": "Normalized Difference Moisture Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00318-2", + "short_name": "NDMI" + }, + "NDPI": { + "application_domain": "vegetation", + "bands": [ + "N", + "alpha", + "R", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-20", + "formula": "(N - (alpha * R + (1.0 - alpha) * S1))/(N + (alpha * R + (1.0 - alpha) * S1))", + "long_name": "Normalized Difference Phenology Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2017.04.031", + "short_name": "NDPI" + }, + "NDPolI": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "(VV - VH)/(VV + VH)", + "long_name": "Normalized Difference Polarization Index", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://www.isprs.org/proceedings/XXXVII/congress/4_pdf/267.pdf", + "short_name": "NDPolI" + }, + "NDPonI": { + "application_domain": "water", + "bands": [ + "S1", + "G" + ], + "contributor": "https://github.com/CvenGeo", + "date_of_addition": "2022-10-03", + "formula": "(S1-G)/(S1+G)", + "long_name": "Normalized Difference Pond Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2006.07.012", + "short_name": "NDPonI" + }, + "NDREI": { + "application_domain": "vegetation", + "bands": [ + "N", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "(N - RE1) / (N + RE1)", + "long_name": "Normalized Difference Red Edge Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/1011-1344(93)06963-4", + "short_name": "NDREI" + }, + "NDSI": { + "application_domain": "snow", + "bands": [ + "G", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(G - S1) / (G + S1)", + "long_name": "Normalized Difference Snow Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1109/IGARSS.1994.399618", + "short_name": "NDSI" + }, + "NDSII": { + "application_domain": "snow", + "bands": [ + "G", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(G - N)/(G + N)", + "long_name": "Normalized Difference Snow Ice Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431160802385459", + "short_name": "NDSII" + }, + "NDSIWV": { + "application_domain": "soil", + "bands": [ + "G", + "Y" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2022-11-20", + "formula": "(G - Y)/(G + Y)", + "long_name": "WorldView Normalized Difference Soil Index", + "platforms": [], + "reference": "https://www.semanticscholar.org/paper/Using-WorldView-2-Vis-NIR-MSI-Imagery-to-Support-Wolf/5e5063ccc4ee76b56b721c866e871d47a77f9fb4", + "short_name": "NDSIWV" + }, + "NDSInw": { + "application_domain": "snow", + "bands": [ + "N", + "S1", + "beta" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(N - S1 - beta)/(N + S1)", + "long_name": "Normalized Difference Snow Index with no Water", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/w12051339", + "short_name": "NDSInw" + }, + "NDSWIR": { + "application_domain": "burn", + "bands": [ + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "(N - S1)/(N + S1)", + "long_name": "Normalized Difference SWIR", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1109/TGRS.2003.819190", + "short_name": "NDSWIR" + }, + "NDSaII": { + "application_domain": "snow", + "bands": [ + "R", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(R - S1) / (R + S1)", + "long_name": "Normalized Difference Snow and Ice Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1080/01431160119766", + "short_name": "NDSaII" + }, + "NDSoI": { + "application_domain": "soil", + "bands": [ + "S2", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(S2 - G)/(S2 + G)", + "long_name": "Normalized Difference Soil Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.jag.2015.02.010", + "short_name": "NDSoiI" + }, + "NDTI": { + "application_domain": "water", + "bands": [ + "R", + "G" + ], + "contributor": "https://github.com/CvenGeo", + "date_of_addition": "2022-10-03", + "formula": "(R-G)/(R+G)", + "long_name": "Normalized Difference Turbidity Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2006.07.012", + "short_name": "NDTI" + }, + "NDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - R)/(N + R)", + "long_name": "Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://ntrs.nasa.gov/citations/19740022614", + "short_name": "NDVI" + }, + "NDVI705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(RE2 - RE1) / (RE2 + RE1)", + "long_name": "Normalized Difference Vegetation Index (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(11)81633-0", + "short_name": "NDVI705" + }, + "NDVIMNDWI": { + "application_domain": "water", + "bands": [ + "N", + "R", + "G", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "((N - R)/(N + R)) - ((G - S1)/(G + S1))", + "long_name": "NDVI-MNDWI Model", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1007/978-3-662-45737-5_51", + "short_name": "NDVIMNDWI" + }, + "NDVIT": { + "application_domain": "burn", + "bands": [ + "N", + "R", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(N - (R * T / 10000.0))/(N + (R * T / 10000.0))", + "long_name": "Normalized Difference Vegetation Index Thermal", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160600954704", + "short_name": "NDVIT" + }, + "NDWI": { + "application_domain": "water", + "bands": [ + "G", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(G - N) / (G + N)", + "long_name": "Normalized Difference Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431169608948714", + "short_name": "NDWI" + }, + "NDWIns": { + "application_domain": "water", + "bands": [ + "G", + "alpha", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(G - alpha * N)/(G + N)", + "long_name": "Normalized Difference Water Index with no Snow Cover and Glaciers", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.3390/w12051339", + "short_name": "NDWIns" + }, + "NDYI": { + "application_domain": "vegetation", + "bands": [ + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(G - B) / (G + B)", + "long_name": "Normalized Difference Yellowness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2016.06.016", + "short_name": "NDYI" + }, + "NGRDI": { + "application_domain": "vegetation", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(G - R) / (G + R)", + "long_name": "Normalized Green Red Difference Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(79)90013-0", + "short_name": "NGRDI" + }, + "NHFD": { + "application_domain": "urban", + "bands": [ + "RE1", + "A" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "(RE1 - A) / (RE1 + A)", + "long_name": "Non-Homogeneous Feature Difference", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://www.semanticscholar.org/paper/Using-WorldView-2-Vis-NIR-MSI-Imagery-to-Support-Wolf/5e5063ccc4ee76b56b721c866e871d47a77f9fb4", + "short_name": "NHFD" + }, + "NIRv": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-16", + "formula": "((N - R) / (N + R)) * N", + "long_name": "Near-Infrared Reflectance of Vegetation", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.1602244", + "short_name": "NIRv" + }, + "NIRvH2": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "k", + "lambdaN", + "lambdaR" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "N - R - k * (lambdaN - lambdaR)", + "long_name": "Hyperspectral Near-Infrared Reflectance of Vegetation", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2021.112723", + "short_name": "NIRvH2" + }, + "NIRvP": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "PAR" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-18", + "formula": "((N - R) / (N + R)) * N * PAR", + "long_name": "Near-Infrared Reflectance of Vegetation and Incoming PAR", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.rse.2021.112763", + "short_name": "NIRvP" + }, + "NLI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "((N ** 2) - R)/((N ** 2) + R)", + "long_name": "Non-Linear Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/02757259409532252", + "short_name": "NLI" + }, + "NMDI": { + "application_domain": "vegetation", + "bands": [ + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "(N - (S1 - S2))/(N + (S1 - S2))", + "long_name": "Normalized Multi-band Drought Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1029/2007GL031021", + "short_name": "NMDI" + }, + "NRFIg": { + "application_domain": "vegetation", + "bands": [ + "G", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(G - S2) / (G + S2)", + "long_name": "Normalized Rapeseed Flowering Index Green", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs13010105", + "short_name": "NRFIg" + }, + "NRFIr": { + "application_domain": "vegetation", + "bands": [ + "R", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(R - S2) / (R + S2)", + "long_name": "Normalized Rapeseed Flowering Index Red", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs13010105", + "short_name": "NRFIr" + }, + "NSDS": { + "application_domain": "soil", + "bands": [ + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "(S1 - S2)/(S1 + S2)", + "long_name": "Normalized Shortwave Infrared Difference Soil-Moisture", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/land10030231", + "short_name": "NSDS" + }, + "NSDSI1": { + "application_domain": "soil", + "bands": [ + "S1", + "S2" + ], + "contributor": "https://github.com/CvenGeo", + "date_of_addition": "2022-10-03", + "formula": "(S1-S2)/S1", + "long_name": "Normalized Shortwave-Infrared Difference Bare Soil Moisture Index 1", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.isprsjprs.2019.06.012", + "short_name": "NSDSI1" + }, + "NSDSI2": { + "application_domain": "soil", + "bands": [ + "S1", + "S2" + ], + "contributor": "https://github.com/CvenGeo", + "date_of_addition": "2022-10-03", + "formula": "(S1-S2)/S2", + "long_name": "Normalized Shortwave-Infrared Difference Bare Soil Moisture Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.isprsjprs.2019.06.012", + "short_name": "NSDSI2" + }, + "NSDSI3": { + "application_domain": "soil", + "bands": [ + "S1", + "S2" + ], + "contributor": "https://github.com/CvenGeo", + "date_of_addition": "2022-10-03", + "formula": "(S1-S2)/(S1+S2)", + "long_name": "Normalized Shortwave-Infrared Difference Bare Soil Moisture Index 3", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.isprsjprs.2019.06.012", + "short_name": "NSDSI3" + }, + "NSTv1": { + "application_domain": "burn", + "bands": [ + "N", + "S2", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-06", + "formula": "((N-S2)/(N+S2))*T", + "long_name": "NIR-SWIR-Temperature Version 1", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1016/j.rse.2011.06.010", + "short_name": "NSTv1" + }, + "NSTv2": { + "application_domain": "burn", + "bands": [ + "N", + "S2", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-10-06", + "formula": "(N-(S2+T))/(N+(S2+T))", + "long_name": "NIR-SWIR-Temperature Version 2", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1016/j.rse.2011.06.010", + "short_name": "NSTv2" + }, + "NWI": { + "application_domain": "water", + "bands": [ + "B", + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "(B - (N + S1 + S2))/(B + (N + S1 + S2))", + "long_name": "New Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.11873/j.issn.1004-0323.2009.2.167", + "short_name": "NWI" + }, + "NormG": { + "application_domain": "vegetation", + "bands": [ + "G", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "G/(N + G + R)", + "long_name": "Normalized Green", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "NormG" + }, + "NormNIR": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "N/(N + G + R)", + "long_name": "Normalized NIR", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "NormNIR" + }, + "NormR": { + "application_domain": "vegetation", + "bands": [ + "R", + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "R/(N + G + R)", + "long_name": "Normalized Red", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2134/agronj2004.0314", + "short_name": "NormR" + }, + "OCVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R", + "cexp" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "(N / G) * (R / G) ** cexp", + "long_name": "Optimized Chlorophyll Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://dx.doi.org/10.1007/s11119-008-9075-z", + "short_name": "OCVI" + }, + "OSAVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "(N - R) / (N + R + 0.16)", + "long_name": "Optimized Soil-Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(95)00186-7", + "short_name": "OSAVI" + }, + "PISI": { + "application_domain": "urban", + "bands": [ + "B", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-18", + "formula": "0.8192 * B - 0.5735 * N + 0.0750", + "long_name": "Perpendicular Impervious Surface Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.3390/rs10101521", + "short_name": "PISI" + }, + "PSRI": { + "application_domain": "vegetation", + "bands": [ + "R", + "B", + "RE2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(R - B)/RE2", + "long_name": "Plant Senescing Reflectance Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1034/j.1399-3054.1999.106119.x", + "short_name": "PSRI" + }, + "QpRVI": { + "application_domain": "radar", + "bands": [ + "HV", + "HH", + "VV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-12-24", + "formula": "(8.0 * HV)/(HH + VV + 2.0 * HV)", + "long_name": "Quad-Polarized Radar Vegetation Index", + "platforms": [], + "reference": "https://doi.org/10.1109/IGARSS.2001.976856", + "short_name": "QpRVI" + }, + "RCC": { + "application_domain": "vegetation", + "bands": [ + "R", + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "R / (R + G + B)", + "long_name": "Red Chromatic Coordinate", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(87)90088-5", + "short_name": "RCC" + }, + "RDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(N - R) / ((N + R) ** 0.5)", + "long_name": "Renormalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(94)00114-3", + "short_name": "RDVI" + }, + "REDSI": { + "application_domain": "vegetation", + "bands": [ + "RE3", + "R", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "((705.0 - 665.0) * (RE3 - R) - (783.0 - 665.0) * (RE1 - R)) / (2.0 * R)", + "long_name": "Red-Edge Disease Stress Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/s18030868", + "short_name": "REDSI" + }, + "RENDVI": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "(RE2 - RE1)/(RE2 + RE1)", + "long_name": "Red Edge Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(11)81633-0", + "short_name": "RENDVI" + }, + "RFDI": { + "application_domain": "radar", + "bands": [ + "HH", + "HV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-12-25", + "formula": "(HH - HV)/(HH + HV)", + "long_name": "Radar Forest Degradation Index", + "platforms": [ + "Sentinel-1 (Dual Polarisation HH-HV)" + ], + "reference": "https://doi.org/10.5194/bg-9-179-2012", + "short_name": "RFDI" + }, + "RGBVI": { + "application_domain": "vegetation", + "bands": [ + "G", + "B", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(G ** 2.0 - B * R)/(G ** 2.0 + B * R)", + "long_name": "Red Green Blue Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.jag.2015.02.012", + "short_name": "RGBVI" + }, + "RGRI": { + "application_domain": "vegetation", + "bands": [ + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "R/G", + "long_name": "Red-Green Ratio Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.jag.2014.03.018", + "short_name": "RGRI" + }, + "RI": { + "application_domain": "vegetation", + "bands": [ + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "(R - G)/(R + G)", + "long_name": "Redness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://www.documentation.ird.fr/hor/fdi:34390", + "short_name": "RI" + }, + "RI4XS": { + "application_domain": "soil", + "bands": [ + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-11-20", + "formula": "(R**2.0)/(G**4.0)", + "long_name": "SPOT HRV XS-based Redness Index 4", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(98)00030-3", + "short_name": "RI4XS" + }, + "RVI": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "RE2 / R", + "long_name": "Ratio Vegetation Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.2134/agronj1968.00021962006000060016x", + "short_name": "RVI" + }, + "S2REP": { + "application_domain": "vegetation", + "bands": [ + "RE3", + "R", + "RE1", + "RE2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "705.0 + 35.0 * ((((RE3 + R) / 2.0) - RE1) / (RE2 - RE1))", + "long_name": "Sentinel-2 Red-Edge Position", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.isprsjprs.2013.04.007", + "short_name": "S2REP" + }, + "S2WI": { + "application_domain": "water", + "bands": [ + "RE1", + "S2" + ], + "contributor": "https://github.com/MATRIX4284", + "date_of_addition": "2022-03-06", + "formula": "(RE1 - S2)/(RE1 + S2)", + "long_name": "Sentinel-2 Water Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/w13121647", + "short_name": "S2WI" + }, + "S3": { + "application_domain": "snow", + "bands": [ + "N", + "R", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(N * (R - S1)) / ((N + R) * (N + S1))", + "long_name": "S3 Snow Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3178/jjshwr.12.28", + "short_name": "S3" + }, + "SARVI": { + "application_domain": "vegetation", + "bands": [ + "L", + "N", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-11", + "formula": "(1 + L)*(N - (R - (R - B))) / (N + (R - (R - B)) + L)", + "long_name": "Soil Adjusted and Atmospherically Resistant Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1109/36.134076", + "short_name": "SARVI" + }, + "SAVI": { + "application_domain": "vegetation", + "bands": [ + "L", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(1.0 + L) * (N - R) / (N + R + L)", + "long_name": "Soil-Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(88)90106-X", + "short_name": "SAVI" + }, + "SAVI2": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "slb", + "sla" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "N / (R + (slb / sla))", + "long_name": "Soil-Adjusted Vegetation Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431169008955053", + "short_name": "SAVI2" + }, + "SAVIT": { + "application_domain": "burn", + "bands": [ + "L", + "N", + "R", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(1.0 + L) * (N - (R * T / 10000.0)) / (N + (R * T / 10000.0) + L)", + "long_name": "Soil-Adjusted Vegetation Index Thermal", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160600954704", + "short_name": "SAVIT" + }, + "SEVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R", + "fdelta" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "(N/R) + fdelta * (1.0/R)", + "long_name": "Shadow-Eliminated Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/17538947.2018.1495770", + "short_name": "SEVI" + }, + "SI": { + "application_domain": "vegetation", + "bands": [ + "B", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "((1.0 - B) * (1.0 - G) * (1.0 - R)) ** (1/3)", + "long_name": "Shadow Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.465.8749&rep=rep1&type=pdf", + "short_name": "SI" + }, + "SIPI": { + "application_domain": "vegetation", + "bands": [ + "N", + "A", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-17", + "formula": "(N - A) / (N - R)", + "long_name": "Structure Insensitive Pigment Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI" + ], + "reference": "https://eurekamag.com/research/009/395/009395053.php", + "short_name": "SIPI" + }, + "SR": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "N/R", + "long_name": "Simple Ratio", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.2307/1936256", + "short_name": "SR" + }, + "SR2": { + "application_domain": "vegetation", + "bands": [ + "N", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "N/G", + "long_name": "Simple Ratio (800 and 550 nm)", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1080/01431169308904370", + "short_name": "SR2" + }, + "SR3": { + "application_domain": "vegetation", + "bands": [ + "N2", + "G", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "N2/(G * RE1)", + "long_name": "Simple Ratio (860, 550 and 708 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(98)00046-7", + "short_name": "SR3" + }, + "SR555": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "RE2 / G", + "long_name": "Simple Ratio (555 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(11)81633-0", + "short_name": "SR555" + }, + "SR705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "RE2 / RE1", + "long_name": "Simple Ratio (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0176-1617(11)81633-0", + "short_name": "SR705" + }, + "SWI": { + "application_domain": "snow", + "bands": [ + "G", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(G * (N - S1)) / ((G + N) * (N + S1))", + "long_name": "Snow Water Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs11232774", + "short_name": "SWI" + }, + "SWM": { + "application_domain": "water", + "bands": [ + "B", + "G", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-20", + "formula": "(B + G)/(N + S1)", + "long_name": "Sentinel Water Mask", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://eoscience.esa.int/landtraining2017/files/posters/MILCZAREK.pdf", + "short_name": "SWM" + }, + "SeLI": { + "application_domain": "vegetation", + "bands": [ + "N2", + "RE1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-08", + "formula": "(N2 - RE1) / (N2 + RE1)", + "long_name": "Sentinel-2 LAI Green Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/s19040904", + "short_name": "SeLI" + }, + "TCARI": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-13", + "formula": "3 * ((RE1 - R) - 0.2 * (RE1 - G) * (RE1 / R))", + "long_name": "Transformed Chlorophyll Absorption in Reflectance Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00018-4", + "short_name": "TCARI" + }, + "TCARIOSAVI": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R", + "G", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(3 * ((RE1 - R) - 0.2 * (RE1 - G) * (RE1 / R))) / (1.16 * (N - R) / (N + R + 0.16))", + "long_name": "TCARI/OSAVI Ratio", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00018-4", + "short_name": "TCARIOSAVI" + }, + "TCARIOSAVI705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1", + "G" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-11-06", + "formula": "(3 * ((RE2 - RE1) - 0.2 * (RE2 - G) * (RE2 / RE1))) / (1.16 * (RE2 - RE1) / (RE2 + RE1 + 0.16))", + "long_name": "TCARI/OSAVI Ratio (705 and 750 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/j.agrformet.2008.03.005", + "short_name": "TCARIOSAVI705" + }, + "TCI": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "1.2 * (RE1 - G) - 1.5 * (R - G) * (RE1 / R) ** 0.5", + "long_name": "Triangular Chlorophyll Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "http://dx.doi.org/10.1109/TGRS.2007.904836", + "short_name": "TCI" + }, + "TDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-09", + "formula": "1.5 * ((N - R)/((N ** 2.0 + R + 0.5) ** 0.5))", + "long_name": "Transformed Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1109/IGARSS.2002.1026867", + "short_name": "TDVI" + }, + "TGI": { + "application_domain": "vegetation", + "bands": [ + "R", + "G", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "- 0.5 * (190 * (R - G) - 120 * (R - B))", + "long_name": "Triangular Greenness Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://dx.doi.org/10.1016/j.jag.2012.07.020", + "short_name": "TGI" + }, + "TRRVI": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "R", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "((RE2 - R) / (RE2 + R)) / (((N - R) / (N + R)) + 1.0)", + "long_name": "Transformed Red Range Vegetation Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/rs12152359", + "short_name": "TRRVI" + }, + "TSAVI": { + "application_domain": "vegetation", + "bands": [ + "sla", + "N", + "R", + "slb" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "sla * (N - sla * R - slb) / (sla * N + R - sla * slb)", + "long_name": "Transformed Soil-Adjusted Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1109/IGARSS.1989.576128", + "short_name": "TSAVI" + }, + "TTVI": { + "application_domain": "vegetation", + "bands": [ + "RE3", + "RE2", + "N2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "0.5 * ((865.0 - 740.0) * (RE3 - RE2) - (N2 - RE2) * (783.0 - 740))", + "long_name": "Transformed Triangular Vegetation Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/rs12010016", + "short_name": "TTVI" + }, + "TVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(((N - R)/(N + R)) + 0.5) ** 0.5", + "long_name": "Transformed Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://ntrs.nasa.gov/citations/19740022614", + "short_name": "TVI" + }, + "TWI": { + "application_domain": "water", + "bands": [ + "RE1", + "RE2", + "G", + "S2", + "B", + "N" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2023-02-10", + "formula": "(2.84 * (RE1 - RE2) / (G + S2)) + ((1.25 * (G - B) - (N - B)) / (N + 1.25 * G - 0.25 * B))", + "long_name": "Triangle Water Index", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.3390/rs14215289", + "short_name": "TWI" + }, + "TriVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "0.5 * (120 * (N - G) - 200 * (R - G))", + "long_name": "Triangular Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "http://dx.doi.org/10.1016/S0034-4257(00)00197-8", + "short_name": "TriVI" + }, + "UI": { + "application_domain": "urban", + "bands": [ + "S2", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-02-07", + "formula": "(S2 - N)/(S2 + N)", + "long_name": "Urban Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://www.isprs.org/proceedings/XXXI/congress/part7/321_XXXI-part7.pdf", + "short_name": "UI" + }, + "VARI": { + "application_domain": "vegetation", + "bands": [ + "G", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(G - R) / (G + R - B)", + "long_name": "Visible Atmospherically Resistant Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00289-9", + "short_name": "VARI" + }, + "VARI700": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R", + "B" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(RE1 - 1.7 * R + 0.7 * B) / (RE1 + 1.3 * R - 1.3 * B)", + "long_name": "Visible Atmospherically Resistant Index (700 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00289-9", + "short_name": "VARI700" + }, + "VDDPI": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "(VV + VH)/VV", + "long_name": "Vertical Dual De-Polarization Index", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1016/j.rse.2018.09.003", + "short_name": "VDDPI" + }, + "VHVVD": { + "application_domain": "radar", + "bands": [ + "VH", + "VV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VH - VV", + "long_name": "VH-VV Difference", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.3390/app9040655", + "short_name": "VHVVD" + }, + "VHVVP": { + "application_domain": "radar", + "bands": [ + "VH", + "VV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VH * VV", + "long_name": "VH-VV Product", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1109/IGARSS47720.2021.9554099", + "short_name": "VHVVP" + }, + "VHVVR": { + "application_domain": "radar", + "bands": [ + "VH", + "VV" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VH/VV", + "long_name": "VH-VV Ratio", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1109/IGARSS47720.2021.9554099", + "short_name": "VHVVR" + }, + "VI6T": { + "application_domain": "burn", + "bands": [ + "N", + "T" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "(N - T/10000.0)/(N + T/10000.0)", + "long_name": "VI6T Index", + "platforms": [ + "Landsat-TM", + "Landsat-ETM+" + ], + "reference": "https://doi.org/10.1080/01431160500239008", + "short_name": "VI6T" + }, + "VI700": { + "application_domain": "vegetation", + "bands": [ + "RE1", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(RE1 - R) / (RE1 + R)", + "long_name": "Vegetation Index (700 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00289-9", + "short_name": "VI700" + }, + "VIBI": { + "application_domain": "urban", + "bands": [ + "N", + "R", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-09-22", + "formula": "((N-R)/(N+R))/(((N-R)/(N+R)) + ((S1-N)/(S1+N)))", + "long_name": "Vegetation Index Built-up Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "http://dx.doi.org/10.1080/01431161.2012.687842", + "short_name": "VIBI" + }, + "VIG": { + "application_domain": "vegetation", + "bands": [ + "G", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-20", + "formula": "(G - R) / (G + R)", + "long_name": "Vegetation Index Green", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/S0034-4257(01)00289-9", + "short_name": "VIG" + }, + "VVVHD": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VV - VH", + "long_name": "VV-VH Difference", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1109/IGARSS47720.2021.9554099", + "short_name": "VVVHD" + }, + "VVVHR": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VV/VH", + "long_name": "VV-VH Ratio", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.3390/app9040655", + "short_name": "VVVHR" + }, + "VVVHS": { + "application_domain": "radar", + "bands": [ + "VV", + "VH" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-19", + "formula": "VV + VH", + "long_name": "VV-VH Sum", + "platforms": [ + "Sentinel-1 (Dual Polarisation VV-VH)" + ], + "reference": "https://doi.org/10.1109/IGARSS47720.2021.9554099", + "short_name": "VVVHS" + }, + "VgNIRBI": { + "application_domain": "urban", + "bands": [ + "G", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-02-09", + "formula": "(G - N)/(G + N)", + "long_name": "Visible Green-Based Built-Up Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.ecolind.2015.03.037", + "short_name": "VgNIRBI" + }, + "VrNIRBI": { + "application_domain": "urban", + "bands": [ + "R", + "N" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-02-09", + "formula": "(R - N)/(R + N)", + "long_name": "Visible Red-Based Built-Up Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/j.ecolind.2015.03.037", + "short_name": "VrNIRBI" + }, + "WDRVI": { + "application_domain": "vegetation", + "bands": [ + "alpha", + "N", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "(alpha * N - R) / (alpha * N + R)", + "long_name": "Wide Dynamic Range Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1078/0176-1617-01176", + "short_name": "WDRVI" + }, + "WDVI": { + "application_domain": "vegetation", + "bands": [ + "N", + "sla", + "R" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-14", + "formula": "N - sla * R", + "long_name": "Weighted Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1016/0034-4257(89)90076-X", + "short_name": "WDVI" + }, + "WI1": { + "application_domain": "water", + "bands": [ + "G", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(G - S2) / (G + S2)", + "long_name": "Water Index 1", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs11182186", + "short_name": "WI1" + }, + "WI2": { + "application_domain": "water", + "bands": [ + "B", + "S2" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-09-18", + "formula": "(B - S2) / (B + S2)", + "long_name": "Water Index 2", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.3390/rs11182186", + "short_name": "WI2" + }, + "WI2015": { + "application_domain": "water", + "bands": [ + "G", + "R", + "N", + "S1", + "S2" + ], + "contributor": "https://github.com/remi-braun", + "date_of_addition": "2022-10-26", + "formula": "1.7204 + 171 * G + 3 * R - 70 * N - 45 * S1 - 71 * S2", + "long_name": "Water Index 2015", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1016/j.rse.2015.12.055", + "short_name": "WI2015" + }, + "WRI": { + "application_domain": "water", + "bands": [ + "G", + "R", + "N", + "S1" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-01-17", + "formula": "(G + R)/(N + S1)", + "long_name": "Water Ratio Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS" + ], + "reference": "https://doi.org/10.1109/GEOINFORMATICS.2010.5567762", + "short_name": "WRI" + }, + "kEVI": { + "application_domain": "kernel", + "bands": [ + "g", + "kNN", + "kNR", + "C1", + "C2", + "kNB", + "kNL" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-10", + "formula": "g * (kNN - kNR) / (kNN + C1 * kNR - C2 * kNB + kNL)", + "long_name": "Kernel Enhanced Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.abc7447", + "short_name": "kEVI" + }, + "kIPVI": { + "application_domain": "kernel", + "bands": [ + "kNN", + "kNR" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "kNN/(kNN + kNR)", + "long_name": "Kernel Infrared Percentage Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.abc7447", + "short_name": "kIPVI" + }, + "kNDVI": { + "application_domain": "kernel", + "bands": [ + "kNN", + "kNR" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "(kNN - kNR)/(kNN + kNR)", + "long_name": "Kernel Normalized Difference Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.abc7447", + "short_name": "kNDVI" + }, + "kRVI": { + "application_domain": "kernel", + "bands": [ + "kNN", + "kNR" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-04-07", + "formula": "kNN / kNR", + "long_name": "Kernel Ratio Vegetation Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.abc7447", + "short_name": "kRVI" + }, + "kVARI": { + "application_domain": "kernel", + "bands": [ + "kGG", + "kGR", + "kGB" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2021-05-10", + "formula": "(kGG - kGR) / (kGG + kGR - kGB)", + "long_name": "Kernel Visible Atmospherically Resistant Index", + "platforms": [ + "Sentinel-2", + "Landsat-OLI", + "Landsat-TM", + "Landsat-ETM+", + "MODIS", + "Planet-Fusion" + ], + "reference": "https://doi.org/10.1126/sciadv.abc7447", + "short_name": "kVARI" + }, + "mND705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "RE1", + "A" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(RE2 - RE1)/(RE2 + RE1 - A)", + "long_name": "Modified Normalized Difference (705, 750 and 445 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00010-X", + "short_name": "mND705" + }, + "mSR705": { + "application_domain": "vegetation", + "bands": [ + "RE2", + "A" + ], + "contributor": "https://github.com/davemlz", + "date_of_addition": "2022-04-08", + "formula": "(RE2 - A)/(RE2 + A)", + "long_name": "Modified Simple Ratio (705 and 445 nm)", + "platforms": [ + "Sentinel-2" + ], + "reference": "https://doi.org/10.1016/S0034-4257(02)00010-X", + "short_name": "mSR705" + } + } +} diff --git a/lib/openeo/extra/spectral_indices/resources/extra-indices-dict.json b/lib/openeo/extra/spectral_indices/resources/extra-indices-dict.json new file mode 100644 index 000000000..f8b0e55f7 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/resources/extra-indices-dict.json @@ -0,0 +1,98 @@ +{ + "SpectralIndices": { + "ANIR": { + "bands": + [ + "R", + "N", + "S1" + ], + "contributor": "vito", + "date_of_addition": "2021-10-27", + "formula": "exec('import numpy as np') or exec('from openeo.processes import clip') or np.arccos(clip((( np.sqrt( (0.8328 - 0.6646)**2 + (N - R)**2 )**2 + np.sqrt( (1.610 - 0.8328)**2 + (S1 - N)**2 )**2 - np.sqrt( (1.610 - 0.6646)**2 + (S1 - R)**2 )**2 ) / (2 * np.sqrt( (0.8328 - 0.6646)**2 + (N - R)**2 ) * np.sqrt( (1.610 - 0.8328)**2 + (S1 - N)**2 ))), -1,1)) * (1. / np.pi)", + "long_name": "Angle at Near InfraRed", + "reference": "", + "short_name": "ANIR", + "type": "vegetation" + }, + "NDRE1": { + "bands": [ + "N", + "RE1" + ], + "contributor": "vito", + "date_of_addition": "2021-10-27", + "formula": "(N - RE1) / (N + RE1)", + "long_name": "Normalized Difference Red Edge 1", + "reference": "", + "short_name": "NDRE1", + "type": "vegetation" + }, + "NDRE2": { + "bands": [ + "N", + "RE2" + ], + "contributor": "vito", + "date_of_addition": "2021-10-27", + "formula": "(N - RE2) / (N + RE2)", + "long_name": "Normalized Difference Red Edge 2", + "reference": "", + "short_name": "NDRE2", + "type": "vegetation" + }, + "NDRE5": { + "bands": [ + "RE1", + "RE3" + ], + "contributor": "vito", + "date_of_addition": "2021-10-27", + "formula": "(RE3 - RE1) / (RE3 + RE1)", + "long_name": "Normalized Difference Red Edge 5", + "reference": "", + "short_name": "NDRE5", + "type": "vegetation" + }, + "BI2": { + "bands": [ + "G", + "R", + "N" + ], + "contributor": "vito", + "date_of_addition": "2022-01-27", + "formula": "((R**2+N**2+G**2)**0.5)/3", + "long_name": "Brightness index 2", + "reference": "https://digifed.org/", + "short_name": "BI2", + "type": "soil" + }, + "BI_B08": { + "bands": [ + "R", + "N" + ], + "contributor": "vito", + "date_of_addition": "2022-01-27", + "formula": "(R**2+N**2)**0.5", + "long_name": "Brightness index B08", + "reference": "https://digifed.org/", + "short_name": "BI_B08", + "type": "soil" + }, + "LSWI_B12": { + "bands": [ + "N", + "S2" + ], + "contributor": "vito", + "date_of_addition": "2022-01-27", + "formula": "(N-S2)/(N+S2)", + "long_name": "Sentinel-2 land surface water index", + "reference": "https://digifed.org/", + "short_name": "LSWI_B12", + "type": "water" + } + } +} diff --git a/lib/openeo/extra/spectral_indices/spectral_indices.py b/lib/openeo/extra/spectral_indices/spectral_indices.py new file mode 100644 index 000000000..8ac3c0b93 --- /dev/null +++ b/lib/openeo/extra/spectral_indices/spectral_indices.py @@ -0,0 +1,475 @@ +import functools +import json +import re +from typing import Dict, List, Optional, Set + +from openeo import BaseOpenEoException +from openeo.processes import ProcessBuilder, array_create, array_modify +from openeo.rest.datacube import DataCube + +try: + import importlib_resources +except ImportError: + import importlib.resources as importlib_resources + + +@functools.lru_cache(maxsize=1) +def load_indices() -> Dict[str, dict]: + """Load set of supported spectral indices.""" + # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class? + specs = {} + + for path in [ + "resources/awesome-spectral-indices/spectral-indices-dict.json", + # TODO #506 Deprecate extra-indices-dict.json as a whole + # and provide an alternative mechanism to work with custom indices + "resources/extra-indices-dict.json", + ]: + with importlib_resources.files("openeo.extra.spectral_indices") / path as resource_path: + data = json.loads(resource_path.read_text(encoding="utf8")) + overwrites = set(specs.keys()).intersection(data["SpectralIndices"].keys()) + if overwrites: + raise RuntimeError(f"Duplicate spectral indices: {overwrites} from {path}") + specs.update(data["SpectralIndices"]) + + return specs + + +@functools.lru_cache(maxsize=1) +def load_constants() -> Dict[str, float]: + """Load constants defined by Awesome Spectral Indices.""" + # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class? + with importlib_resources.files( + "openeo.extra.spectral_indices" + ) / "resources/awesome-spectral-indices/constants.json" as resource_path: + data = json.loads(resource_path.read_text(encoding="utf8")) + + return {k: v["default"] for k, v in data.items() if isinstance(v["default"], (int, float))} + + +@functools.lru_cache(maxsize=1) +def _load_bands() -> Dict[str, dict]: + """Load band name mapping defined by Awesome Spectral Indices.""" + # TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class? + with importlib_resources.files( + "openeo.extra.spectral_indices" + ) / "resources/awesome-spectral-indices/bands.json" as resource_path: + data = json.loads(resource_path.read_text(encoding="utf8")) + return data + + +class BandMappingException(BaseOpenEoException): + """Failure to determine band-variable mapping.""" + + +class _BandMapping: + """ + Helper class to extract mappings between band names and variable names used in Awesome Spectral Indices formulas. + """ + + _EXTRA = { + "sentinel1": {"HH": "HH", "HV": "HV", "VH": "VH", "VV": "VV"}, + } + + def __init__(self): + # Load bands.json from Awesome Spectral Indices + self._band_data = _load_bands() + + @staticmethod + def _normalize_platform(platform: str) -> str: + platform = platform.lower().replace("-", "").replace(" ", "") + if platform in {"sentinel2a", "sentinel2b"}: + platform = "sentinel2" + return platform + + @staticmethod + def _normalize_band_name(band_name: str) -> str: + band_name = band_name.upper() + # Normalize band names like "B01" to "B1" + band_name = re.sub(r"^B0+(\d+)$", r"B\1", band_name) + return band_name + + @functools.lru_cache(maxsize=1) + def get_platforms(self) -> Set[str]: + """Get list of supported (normalized) satellite platforms.""" + platforms = {p for var_data in self._band_data.values() for p in var_data.get("platforms", {}).keys()} + platforms.update(self._EXTRA.keys()) + platforms.update({self._normalize_platform(p) for p in platforms}) + return platforms + + def guess_platform(self, name: str) -> str: + """Guess platform from given collection id or name.""" + # First check original id, then retry with removed separators as last resort. + for haystack in [name.lower(), re.sub("[_ -]", "", name.lower())]: + for platform in sorted(self.get_platforms(), key=len, reverse=True): + if platform in haystack: + return platform + raise BandMappingException(f"Unable to guess satellite platform from id {name!r}.") + + def variable_to_band_name_map(self, platform: str) -> Dict[str, str]: + """ + Build mapping from Awesome Spectral Indices variable names to (normalized) band names for given satellite platform. + """ + platform_normalized = self._normalize_platform(platform) + if platform_normalized in self._EXTRA: + return self._EXTRA[platform_normalized] + + var_to_band = { + var: pf_data["band"] + for var, var_data in self._band_data.items() + for pf, pf_data in var_data.get("platforms", {}).items() + if self._normalize_platform(pf) == platform_normalized + } + if not var_to_band: + raise BandMappingException(f"Empty band mapping derived for satellite platform {platform!r}") + return var_to_band + + def actual_band_name_to_variable_map(self, platform: str, band_names: List[str]) -> Dict[str, str]: + """Build mapping from actual band names (as given) to Awesome Spectral Indices variable names.""" + var_to_band = self.variable_to_band_name_map(platform=platform) + band_to_var = { + band_name: var + for var, normalized_band_name in var_to_band.items() + for band_name in band_names + if self._normalize_band_name(band_name) == normalized_band_name + } + return band_to_var + + +def list_indices() -> List[str]: + """List names of supported spectral indices""" + specs = load_indices() + return list(specs.keys()) + + +def _check_params(item, params): + range_vals = ["input_range", "output_range"] + if set(params) != set(range_vals): + raise ValueError( + f"You have set the parameters {params} on {item}, while the following are required {range_vals}" + ) + for rng in range_vals: + if params[rng] is None: + continue + if len(params[rng]) != 2: + raise ValueError( + f"The list of provided values {params[rng]} for parameter {rng} for {item} is not of length 2" + ) + # TODO: allow float too? + if not all(isinstance(val, int) for val in params[rng]): + raise ValueError("The ranges you supplied are not all of type int") + if (params["input_range"] is None) != (params["output_range"] is None): + raise ValueError(f"The index_range and output_range of {item} should either be both supplied, or both None") + + +def _check_validity_index_dict(index_dict: dict, index_specs: dict): + # TODO: this `index_dict` API needs some more rethinking: + # - the dictionary has no explicit order of indices, which can be important for end user + # - allow "collection" to be missing (e.g. if no rescaling is desired, or input data is not kept)? + # - option to define default output range, instead of having it to specify it for each index? + # - keep "rescaling" feature separate/orthogonal from "spectral indices" feature. It could be useful as + # a more generic machine learning data preparation feature + input_vals = ["collection", "indices"] + if set(index_dict.keys()) != set(input_vals): + raise ValueError( + f"The first level of the dictionary should contain the keys 'collection' and 'indices', but they contain {index_dict.keys()}" + ) + _check_params("collection", index_dict["collection"]) + for index, params in index_dict["indices"].items(): + if index not in index_specs.keys(): + raise NotImplementedError("Index " + index + " is not supported.") + _check_params(index, params) + + +def _callback( + x: ProcessBuilder, + index_dict: dict, + index_specs: dict, + append: bool, + band_names: List[str], + band_to_var: Dict[str, str], +) -> ProcessBuilder: + index_values = [] + x_res = x + + # TODO: use `label` parameter of `array_element` to avoid index based band references + variables = {band_to_var[bn]: x.array_element(i) for i, bn in enumerate(band_names) if bn in band_to_var} + eval_globals = { + **load_constants(), + **variables, + } + # TODO: user might want to control order of indices, which is tricky through a dictionary. + for index, params in index_dict["indices"].items(): + index_result = eval(index_specs[index]["formula"], eval_globals) + if params["input_range"] is not None: + index_result = index_result.linear_scale_range(*params["input_range"], *params["output_range"]) + index_values.append(index_result) + if index_dict["collection"]["input_range"] is not None: + x_res = x_res.linear_scale_range( + *index_dict["collection"]["input_range"], *index_dict["collection"]["output_range"] + ) + if append: + return array_modify(data=x_res, values=index_values, index=len(band_names)) + else: + return array_create(data=index_values) + + +def compute_and_rescale_indices( + datacube: DataCube, + index_dict: dict, + *, + append: bool = False, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Computes a list of indices from a data cube + + :param datacube: input data cube + :param index_dict: a dictionary that contains the input- and output range of the collection on which you calculate the indices + as well as the indices that you want to calculate with their responding input- and output ranges + It follows the following format:: + + { + "collection": { + "input_range": [0,8000], + "output_range": [0,250] + }, + "indices": { + "NDVI": { + "input_range": [-1,1], + "output_range": [0,250] + }, + } + } + + If you don't want to rescale your data, you can fill the input-, index- and output-range with ``None``. + + See `list_indices()` for supported indices. + + :param append: append the indices as bands to the given data cube + instead of creating a new cube with only the calculated indices + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: the datacube with the indices attached as bands + + .. warning:: this "rescaled" index helper uses an experimental API (e.g. `index_dict` argument) that is subject to change. + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + + """ + index_specs = load_indices() + + _check_validity_index_dict(index_dict, index_specs) + + if variable_map is None: + # Automatic band mapping + band_mapping = _BandMapping() + if platform is None: + if datacube.metadata and datacube.metadata.get("id"): + platform = band_mapping.guess_platform(name=datacube.metadata.get("id")) + else: + raise BandMappingException("Unable to determine satellite platform from data cube metadata") + band_to_var = band_mapping.actual_band_name_to_variable_map( + platform=platform, band_names=datacube.metadata.band_names + ) + else: + band_to_var = {b: v for v, b in variable_map.items()} + + res = datacube.apply_dimension( + dimension="bands", + process=lambda x: _callback( + x, + index_dict=index_dict, + index_specs=index_specs, + append=append, + band_names=datacube.metadata.band_names, + band_to_var=band_to_var, + ), + ) + if append: + return res.rename_labels("bands", target=datacube.metadata.band_names + list(index_dict["indices"].keys())) + else: + return res.rename_labels("bands", target=list(index_dict["indices"].keys())) + + +def append_and_rescale_indices( + datacube: DataCube, + index_dict: dict, + *, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Computes a list of indices from a datacube and appends them to the existing datacube + + :param datacube: input data cube + :param index_dict: a dictionary that contains the input- and output range of the collection on which you calculate the indices + as well as the indices that you want to calculate with their responding input- and output ranges + It follows the following format:: + + { + "collection": { + "input_range": [0,8000], + "output_range": [0,250] + }, + "indices": { + "NDVI": { + "input_range": [-1,1], + "output_range": [0,250] + }, + } + } + + See `list_indices()` for supported indices. + + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended indices + + .. warning:: this "rescaled" index helper uses an experimental API (e.g. `index_dict` argument) that is subject to change. + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + return compute_and_rescale_indices( + datacube=datacube, index_dict=index_dict, append=True, variable_map=variable_map, platform=platform + ) + + +def compute_indices( + datacube: DataCube, + indices: List[str], + *, + append: bool = False, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Compute multiple spectral indices from the given data cube. + + :param datacube: input data cube + :param indices: list of names of the indices to compute and append. See `list_indices()` for supported indices. + :param append: append the indices as bands to the given data cube + instead of creating a new cube with only the calculated indices + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube containing the indices as bands + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + # TODO: it's bit weird to have to specify all these None's in this structure + index_dict = { + "collection": { + "input_range": None, + "output_range": None, + }, + "indices": {index: {"input_range": None, "output_range": None} for index in indices}, + } + return compute_and_rescale_indices( + datacube=datacube, index_dict=index_dict, append=append, variable_map=variable_map, platform=platform + ) + + +def append_indices( + datacube: DataCube, + indices: List[str], + *, + variable_map: Optional[Dict[str, str]] = None, + platform: Optional[str] = None, +) -> DataCube: + """ + Compute multiple spectral indices and append them to the given data cube. + + :param datacube: input data cube + :param indices: list of names of the indices to compute and append. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended indices + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + + return compute_indices( + datacube=datacube, indices=indices, append=True, variable_map=variable_map, platform=platform + ) + + +def compute_index( + datacube: DataCube, index: str, *, variable_map: Optional[Dict[str, str]] = None, platform: Optional[str] = None +) -> DataCube: + """ + Compute a single spectral index from a data cube. + + :param datacube: input data cube + :param index: name of the index to compute. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube containing the index as band + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + # TODO: option to compute the index with `reduce_dimension` instead of `apply_dimension`? + return compute_indices( + datacube=datacube, indices=[index], append=False, variable_map=variable_map, platform=platform + ) + + +def append_index( + datacube: DataCube, index: str, *, variable_map: Optional[Dict[str, str]] = None, platform: Optional[str] = None +) -> DataCube: + """ + Compute a single spectral index and append it to the given data cube. + + :param cube: input data cube + :param index: name of the index to compute and append. See `list_indices()` for supported indices. + :param variable_map: (optional) mapping from Awesome Spectral Indices formula variable to actual cube band names. + To be specified if the given data cube has non-standard band names, + or the satellite platform can not be recognized from the data cube metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + :param platform: optionally specify the satellite platform (to determine band name mapping) + if the given data cube has no or an unhandled collection id in its metadata. + See :ref:`spectral_indices_manual_band_mapping` for more information. + + :return: data cube with appended index + + .. versionadded:: 0.26.0 + Added `variable_map` and `platform` arguments. + """ + return compute_indices( + datacube=datacube, indices=[index], append=True, variable_map=variable_map, platform=platform + ) diff --git a/lib/openeo/internal/__init__.py b/lib/openeo/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/openeo/internal/documentation.py b/lib/openeo/internal/documentation.py new file mode 100644 index 000000000..c7da614c4 --- /dev/null +++ b/lib/openeo/internal/documentation.py @@ -0,0 +1,60 @@ +""" +Utilities to build/automate/extend documentation +""" + +import collections +import inspect +import textwrap +from functools import partial +from typing import Callable, Optional, Tuple, TypeVar + +# TODO: give this a proper public API? +_process_registry = collections.defaultdict(list) + + +T = TypeVar("T", bound=Callable) + + +def openeo_process(f: Optional[T] = None, process_id: Optional[str] = None, mode: Optional[str] = None) -> T: + """ + Decorator for function or method to associate it with a standard openEO process + + :param f: function or method + :param process_id: openEO process_id (to be given when it can not be guessed from function name) + :return: + """ + # TODO: include openEO version? + # TODO: support non-standard/proposed/experimental? + # TODO: handling of `mode` (or something alike): apply/reduce_dimension/... callback, (band) math operator, ...? + # TODO: documentation test that "seealso" urls are valid + # TODO: inject more references/metadata in __doc__ + if f is None: + # Parameterized decorator call + return partial(openeo_process, process_id=process_id) + + process_id = process_id or f.__name__ + url = f"https://processes.openeo.org/#{process_id}" + seealso = f'.. seealso::\n openeo.org documentation on `process "{process_id}" <{url}>`_.' + f.__doc__ = textwrap.dedent(f.__doc__ or "") + "\n\n" + seealso + + _process_registry[process_id].append((f, mode)) + return f + + +def openeo_endpoint(endpoint: str) -> Callable[[Callable], Callable]: + """ + Parameterized decorator to annotate given function or method with the openEO endpoint it interacts with + + :param endpoint: REST endpoint (e.g. "GET /jobs", "POST /result", ...) + :return: + """ + # TODO: automatically parse/normalize endpoint (to method+path) + # TODO: wrap this in some markup/directive to make this more a "small print" note. + + def decorate(f: Callable) -> Callable: + is_method = list(inspect.signature(f).parameters.keys())[:1] == ["self"] + seealso = f"This {'method' if is_method else 'function'} uses openEO endpoint ``{endpoint}``" + f.__doc__ = textwrap.dedent(f.__doc__ or "") + "\n\n" + seealso + "\n" + return f + + return decorate diff --git a/lib/openeo/internal/graph_building.py b/lib/openeo/internal/graph_building.py new file mode 100644 index 000000000..6f5918ea2 --- /dev/null +++ b/lib/openeo/internal/graph_building.py @@ -0,0 +1,476 @@ +""" +Internal openEO process graph building utilities +'''''''''''''''''''''''''''''''''''''''''''''''''' + +Internal functionality for abstracting, building, manipulating and processing openEO process graphs. + +""" + +from __future__ import annotations + +import abc +import collections +import copy +import json +import sys +from contextlib import nullcontext +from pathlib import Path +from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union + +from openeo.api.process import Parameter +from openeo.internal.process_graph_visitor import ( + ProcessGraphUnflattener, + ProcessGraphVisitException, + ProcessGraphVisitor, +) +from openeo.util import dict_no_none, load_json_resource + + +class FlatGraphableMixin(metaclass=abc.ABCMeta): + """ + Mixin for classes that can be exported/converted to + a "flat graph" representation of an openEO process graph. + """ + + @abc.abstractmethod + def flat_graph(self) -> Dict[str, dict]: + ... + + def to_json(self, *, indent: Union[int, None] = 2, separators: Optional[Tuple[str, str]] = None) -> str: + """ + Get interoperable JSON representation of the process graph. + + See :py:meth:`DataCube.print_json` to directly print the JSON representation + and :ref:`process_graph_export` for more usage information. + + Also see ``json.dumps`` docs for more information on the JSON formatting options. + + :param indent: JSON indentation level. + :param separators: (optional) tuple of item/key separators. + :return: JSON string + """ + pg = {"process_graph": self.flat_graph()} + return json.dumps(pg, indent=indent, separators=separators) + + def print_json( + self, + *, + file=None, + indent: Union[int, None] = 2, + separators: Optional[Tuple[str, str]] = None, + end: str = "\n", + ): + """ + Print interoperable JSON representation of the process graph. + + See :py:meth:`DataCube.to_json` to get the JSON representation as a string + and :ref:`process_graph_export` for more usage information. + + Also see ``json.dumps`` docs for more information on the JSON formatting options. + + :param file: file-like object (stream) to print to (current ``sys.stdout`` by default). + Or a path (string or pathlib.Path) to a file to write to. + :param indent: JSON indentation level. + :param separators: (optional) tuple of item/key separators. + :param end: additional string to be printed at the end (newline by default). + + .. versionadded:: 0.12.0 + + .. versionadded:: 0.23.0 + added the ``end`` argument. + """ + pg = {"process_graph": self.flat_graph()} + if isinstance(file, (str, Path)): + # Create (new) file and automatically close it + file_ctx = Path(file).open("w", encoding="utf8") + else: + # Just use file as-is, but don't close it automatically. + file_ctx = nullcontext(enter_result=file or sys.stdout) + with file_ctx as f: + json.dump(pg, f, indent=indent, separators=separators) + if end: + f.write(end) + + +class _FromNodeMixin(abc.ABC): + """Mixin for classes that want to hook into the generation of a "from_node" reference.""" + + @abc.abstractmethod + def from_node(self) -> PGNode: + # TODO: "from_node" is a bit a confusing name: + # it refers to the "from_node" node reference in openEO process graphs, + # but as a method name here it reads like "construct from PGNode", + # while it is actually meant as "export as PGNode" (that can be used in a "from_node" reference). + pass + + +class PGNode(_FromNodeMixin, FlatGraphableMixin): + """ + A process node in a process graph: has at least a process_id and arguments. + + Note that a full openEO "process graph" is essentially a directed acyclic graph of nodes + pointing to each other. A full process graph is practically equivalent with its "result" node, + as it points (directly or indirectly) to all the other nodes it depends on. + + .. warning:: + This class is an implementation detail meant for internal use. + It is not recommended for general use in normal user code. + Instead, use process graph abstraction builders like + :py:meth:`Connection.load_collection() `, + :py:meth:`Connection.datacube_from_process() `, + :py:meth:`Connection.datacube_from_flat_graph() `, + :py:meth:`Connection.datacube_from_json() `, + :py:meth:`Connection.load_ml_model() `, + :py:func:`openeo.processes.process()`, + + """ + + __slots__ = ["_process_id", "_arguments", "_namespace"] + + def __init__(self, process_id: str, arguments: dict = None, namespace: Union[str, None] = None, **kwargs): + self._process_id = process_id + # Merge arguments dict and kwargs + arguments = dict(**(arguments or {}), **kwargs) + # Make sure direct PGNode arguments are properly wrapped in a "from_node" dict + for arg, value in arguments.items(): + if isinstance(value, _FromNodeMixin): + arguments[arg] = {"from_node": value.from_node()} + elif isinstance(value, list): + for index, arrayelement in enumerate(value): + if isinstance(arrayelement, _FromNodeMixin): + value[index] = {"from_node": arrayelement.from_node()} + # TODO: use a frozendict of some sort to ensure immutability? + self._arguments = arguments + self._namespace = namespace + + def from_node(self): + return self + + def __repr__(self): + return "<{c} {p!r} at 0x{m:x}>".format(c=self.__class__.__name__, p=self.process_id, m=id(self)) + + @property + def process_id(self) -> str: + return self._process_id + + @property + def arguments(self) -> dict: + return self._arguments + + @property + def namespace(self) -> Union[str, None]: + return self._namespace + + def update_arguments(self, **kwargs): + """ + Add/Update arguments of the process node. + + .. versionadded:: 0.10.1 + """ + self._arguments = {**self._arguments, **kwargs} + + def _as_tuple(self): + return (self._process_id, self._arguments, self._namespace) + + def __eq__(self, other): + return isinstance(other, type(self)) and self._as_tuple() == other._as_tuple() + + def to_dict(self) -> dict: + """ + Convert process graph to a nested dictionary structure. + Uses deep copy style: nodes that are reused in graph will be deduplicated + """ + + def _deep_copy(x): + """PGNode aware deep copy helper""" + if isinstance(x, PGNode): + return dict_no_none(process_id=x.process_id, arguments=_deep_copy(x.arguments), namespace=x.namespace) + if isinstance(x, Parameter): + return {"from_parameter": x.name} + elif isinstance(x, dict): + return {str(k): _deep_copy(v) for k, v in x.items()} + elif isinstance(x, (list, tuple)): + return type(x)(_deep_copy(v) for v in x) + elif isinstance(x, (str, int, float)) or x is None: + return x + else: + raise ValueError(repr(x)) + + return _deep_copy(self) + + def flat_graph(self) -> Dict[str, dict]: + """Get the process graph in internal flat dict representation.""" + return GraphFlattener().flatten(node=self) + + @staticmethod + def to_process_graph_argument(value: Union["PGNode", str, dict]) -> dict: + """ + Normalize given argument properly to a "process_graph" argument + to be used as reducer/subprocess for processes like + ``reduce_dimension``, ``aggregate_spatial``, ``apply``, ``merge_cubes``, ``resample_cube_temporal`` + """ + if isinstance(value, str): + # assume string with predefined reduce/apply process ("mean", "sum", ...) + # TODO: is this case still used? It's invalid anyway for 1.0 openEO spec I think? + return value + elif isinstance(value, PGNode): + return {"process_graph": value} + elif isinstance(value, dict) and isinstance(value.get("process_graph"), PGNode): + return value + else: + raise ValueError(value) + + @staticmethod + def from_flat_graph(flat_graph: dict, parameters: Optional[dict] = None) -> PGNode: + """Unflatten a given flat dict representation of a process graph and return result node.""" + return PGNodeGraphUnflattener.unflatten(flat_graph=flat_graph, parameters=parameters) + + + def walk_nodes(self) -> Iterator[PGNode]: + """Walk this node and all it's parents""" + # TODO: option to do deep walk (walk through child graphs too)? + yield self + + def walk(x) -> Iterator[PGNode]: + if isinstance(x, PGNode): + yield from x.walk_nodes() + elif isinstance(x, dict): + for v in x.values(): + yield from walk(v) + elif isinstance(x, (list, tuple)): + for v in x: + yield from walk(v) + + yield from walk(self.arguments) + + +def as_flat_graph(x: Union[dict, FlatGraphableMixin, Path, List[FlatGraphableMixin], Any]) -> Dict[str, dict]: + """ + Convert given object to a internal flat dict graph representation. + """ + # TODO: document or verify which process graph flavor this is: + # including `{"process": {"process_graph": {nodes}}` ("process graph with metadata") + # including `{"process_graph": {nodes}}` ("process graph") + # or just the raw process graph nodes? + if isinstance(x, dict): + # Assume given dict is already a flat graph representation + return x + elif isinstance(x, FlatGraphableMixin): + return x.flat_graph() + elif isinstance(x, (str, Path)): + # Assume a JSON resource (raw JSON, path to local file, JSON url, ...) + return load_json_resource(x) + elif isinstance(x, (list, tuple)) and all(isinstance(i, FlatGraphableMixin) for i in x): + return MultiLeafGraph(x).flat_graph() + raise ValueError(x) + + +class ReduceNode(PGNode): + """ + A process graph node for "reduce" processes (has a reducer sub-process-graph) + """ + + def __init__( + self, + data: _FromNodeMixin, + reducer: Union[PGNode, str, dict], + dimension: str, + context=None, + process_id="reduce_dimension", + band_math_mode: bool = False, + ): + assert process_id in ("reduce_dimension", "reduce_dimension_binary") + arguments = { + "data": data, + "reducer": self.to_process_graph_argument(reducer), + "dimension": dimension, + } + if context is not None: + arguments["context"] = context + super().__init__(process_id=process_id, arguments=arguments) + # TODO #123 is it (still) necessary to make "band" math a special case? + self.band_math_mode = band_math_mode + + @property + def dimension(self): + return self.arguments["dimension"] + + def reducer_process_graph(self) -> PGNode: + return self.arguments["reducer"]["process_graph"] + + def clone_with_new_reducer(self, reducer: PGNode) -> ReduceNode: + """Copy/clone this reduce node: keep input reference, but use new reducer""" + return ReduceNode( + data=self.arguments["data"]["from_node"], + reducer=reducer, + dimension=self.arguments["dimension"], + band_math_mode=self.band_math_mode, + context=self.arguments.get("context"), + ) + + +class FlatGraphNodeIdGenerator: + """ + Helper class to generate unique node ids (e.g. autoincrement style) + for processes in a flat process graph. + """ + + def __init__(self): + self._counters = collections.defaultdict(int) + + def generate(self, process_id: str): + """Generate new key for given process id.""" + self._counters[process_id] += 1 + return "{p}{c}".format(p=process_id.replace("_", ""), c=self._counters[process_id]) + + +class GraphFlattener(ProcessGraphVisitor): + + def __init__(self, node_id_generator: FlatGraphNodeIdGenerator = None, multi_input_mode: bool = False): + super().__init__() + self._node_id_generator = node_id_generator or FlatGraphNodeIdGenerator() + self._last_node_id = None + self._flattened: Dict[str, dict] = {} + self._argument_stack = [] + self._node_cache = {} + self._multi_input_mode = multi_input_mode + + def flatten(self, node: PGNode) -> Dict[str, dict]: + """Consume given nested process graph and return flat dict representation""" + if self._flattened and not self._multi_input_mode: + raise RuntimeError("Flattening multiple graphs, but not in multi-input mode") + self.accept_node(node) + assert len(self._argument_stack) == 0 + return self.flattened(set_result_flag=not self._multi_input_mode) + + def flattened(self, set_result_flag: bool = True) -> Dict[str, dict]: + flat_graph = copy.deepcopy(self._flattened) + if set_result_flag: + # TODO #583 an "end" node is not necessarily a "result" node + flat_graph[self._last_node_id]["result"] = True + return flat_graph + + def accept_node(self, node: PGNode): + # Process reused nodes only first time and remember node id. + node_id = id(node) + if node_id not in self._node_cache: + super()._accept_process(process_id=node.process_id, arguments=node.arguments, namespace=node.namespace) + self._node_cache[node_id] = self._last_node_id + else: + self._last_node_id = self._node_cache[node_id] + + def enterProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + self._argument_stack.append({}) + + def leaveProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + node_id = self._node_id_generator.generate(process_id) + self._flattened[node_id] = dict_no_none( + process_id=process_id, + arguments=self._argument_stack.pop(), + namespace=namespace, + ) + self._last_node_id = node_id + + def _store_argument(self, argument_id: str, value): + if isinstance(value, Parameter): + value = {"from_parameter": value.name} + self._argument_stack[-1][argument_id] = value + + def _store_array_element(self, value): + if isinstance(value, Parameter): + value = {"from_parameter": value.name} + self._argument_stack[-1].append(value) + + def enterArray(self, argument_id: str): + array = [] + self._store_argument(argument_id, array) + self._argument_stack.append(array) + + def leaveArray(self, argument_id: str): + self._argument_stack.pop() + + def arrayElementDone(self, value): + self._store_array_element(self._flatten_argument(value)) + + def constantArrayElement(self, value): + self._store_array_element(self._flatten_argument(value)) + + def _flatten_argument(self, value): + if isinstance(value, dict): + if "from_node" in value: + value = {"from_node": self._last_node_id} + elif "process_graph" in value: + pg = value["process_graph"] + if isinstance(pg, PGNode): + value = {"process_graph": GraphFlattener(node_id_generator=self._node_id_generator).flatten(pg)} + elif isinstance(pg, dict): + # Assume it is already a valid flat graph representation of a subprocess + value = {"process_graph": pg} + else: + raise ValueError(pg) + else: + value = {k: self._flatten_argument(v) for k, v in value.items()} + elif isinstance(value, Parameter): + value = {"from_parameter": value.name} + return value + + def leaveArgument(self, argument_id: str, value): + self._store_argument(argument_id, self._flatten_argument(value)) + + def constantArgument(self, argument_id: str, value): + self._store_argument(argument_id, value) + + +class PGNodeGraphUnflattener(ProcessGraphUnflattener): + """ + Unflatten a flat process graph to a graph of :py:class:`PGNode` objects + + Parameter substitution can also be performed, but is optional: + if the ``parameters=None`` is given, no parameter substitution is done, + if it is a dictionary (even an empty one) is given, every parameter encountered in the process + graph must have an entry for substitution. + """ + + def __init__(self, flat_graph: dict, parameters: Optional[dict] = None): + super().__init__(flat_graph=flat_graph) + self._parameters = parameters + + def _process_node(self, node: dict) -> PGNode: + return PGNode( + process_id=node["process_id"], + arguments=self._process_value(value=node["arguments"]), + namespace=node.get("namespace"), + ) + + def _process_from_node(self, key: str, node: dict) -> PGNode: + return self.get_node(key=key) + + def _process_from_parameter(self, name: str) -> Any: + if self._parameters is None: + return super()._process_from_parameter(name=name) + if name not in self._parameters: + raise ProcessGraphVisitException("No substitution value for parameter {p!r}.".format(p=name)) + return self._parameters[name] + + +class MultiLeafGraph(FlatGraphableMixin): + """ + Container for process graphs with multiple leaf/result nodes. + """ + + __slots__ = ["_leaves"] + + def __init__(self, leaves: Iterable[FlatGraphableMixin]): + self._leaves = list(leaves) + + def flat_graph(self) -> Dict[str, dict]: + flattener = GraphFlattener(multi_input_mode=True) + for leaf in self._leaves: + if isinstance(leaf, PGNode): + flattener.flatten(leaf) + elif isinstance(leaf, _FromNodeMixin): + flattener.flatten(leaf.from_node()) + else: + raise ValueError(f"Unsupported type {type(leaf)}") + + return flattener.flattened(set_result_flag=True) diff --git a/lib/openeo/internal/jupyter.py b/lib/openeo/internal/jupyter.py new file mode 100644 index 000000000..891e7e50a --- /dev/null +++ b/lib/openeo/internal/jupyter.py @@ -0,0 +1,173 @@ +import json +import os + +from openeo.rest import OpenEoApiError + +SCRIPT_URL = "https://cdn.jsdelivr.net/npm/@openeo/vue-components@2/assets/openeo.min.js" +COMPONENT_MAP = { + "collection": "data", + "data-table": "data", + "file-format": "format", + "file-formats": "formats", + "item": "data", + "job-estimate": "estimate", + "model-builder": "value", + "service-type": "service", + "service-types": "services", + "udf-runtime": "runtime", + "udf-runtimes": "runtimes", +} + +TABLE_COLUMNS = { + "jobs": { + "id": { + "name": "ID", + "primaryKey": True, + }, + "title": { + "name": "Title", + }, + "status": { + "name": "Status", + # 'stylable': True + }, + "created": { + "name": "Submitted", + "format": "Timestamp", + "sort": "desc", + }, + "updated": { + "name": "Last update", + "format": "Timestamp", + }, + }, + "services": { + "id": { + "name": "ID", + "primaryKey": True, + }, + "title": { + "name": "Title", + }, + "type": { + "name": "Type", + # 'format': value => typeof value === 'string' ? value.toUpperCase() : value, + }, + "enabled": { + "name": "Enabled", + }, + "created": { + "name": "Submitted", + "format": "Timestamp", + "sort": "desc", + }, + }, + "files": { + "path": { + "name": "Path", + "primaryKey": True, + # 'sortFn': Utils.sortByPath, + "sort": "asc", + }, + "size": { + "name": "Size", + "format": "FileSize", + "filterable": False, + }, + "modified": { + "name": "Last modified", + "format": "Timestamp", + }, + }, +} + + +def in_jupyter_context() -> bool: + """Check if we are running in an interactive Jupyter notebook context.""" + try: + from ipykernel.zmqshell import ZMQInteractiveShell + from IPython.core.getipython import get_ipython + except ImportError: + return False + return isinstance(get_ipython(), ZMQInteractiveShell) + + +def render_component(component: str, data=None, parameters: dict = None): + parameters = parameters or {} + # Special handling for batch job results, show either item or collection depending on the data + if component == "batch-job-result": + component = "item" if data["type"] == "Feature" else "collection" + + if component == "data-table": + parameters["columns"] = TABLE_COLUMNS[parameters["columns"]] + elif component in ["collection", "collections", "item", "items"]: + url = os.environ.get("OPENEO_BASEMAP_URL") + attribution = os.environ.get("OPENEO_BASEMAP_ATTRIBUTION") + parameters["mapOptions"] = {} + if url: + parameters["mapOptions"]["basemap"] = url + if attribution: + parameters["mapOptions"]["attribution"] = attribution + + # Set the data as the corresponding parameter in the Vue components + key = COMPONENT_MAP.get(component, component) + if data is not None: + if isinstance(data, list): + # TODO: make this `to_dict` usage more explicit with an internal API? + data = [(x.to_dict() if hasattr(x, "to_dict") else x) for x in data] + parameters[key] = data + + # Construct HTML, load Vue Components source files only if the openEO HTML tag is not yet defined + return """ + + + + + """.format( + script=SCRIPT_URL, component=component, props=json.dumps(parameters) + ) + + +def render_error(error: OpenEoApiError): + # ToDo: Once we have a dedicated log/error component, use that instead of description + output = """## Error `{code}`\n\n{message}""".format(code=error.code, message=error.message) + return render_component("description", data=output) + + +# These classes are proxies to visualize openEO responses nicely in Jupyter +# To show the actual list or dict in Jupyter, use repr() or print() + + +class VisualDict(dict): + + def __init__(self, component: str, data: dict, parameters: dict = None): + dict.__init__(self, data) + self.component = component + self.parameters = parameters or {} + + def _repr_html_(self): + return render_component(self.component, self, self.parameters) + + +class VisualList(list): + + def __init__(self, component: str, data: list, parameters: dict = None): + list.__init__(self, data) + self.component = component + self.parameters = parameters or {} + + def _repr_html_(self): + return render_component(self.component, self, self.parameters) diff --git a/lib/openeo/internal/process_graph_visitor.py b/lib/openeo/internal/process_graph_visitor.py new file mode 100644 index 000000000..7c42c2202 --- /dev/null +++ b/lib/openeo/internal/process_graph_visitor.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +import json +from abc import ABC +from typing import Any, Tuple, Union + +from openeo.internal.warnings import deprecated +from openeo.rest import OpenEoClientException + + +class ProcessGraphVisitException(OpenEoClientException): + pass + + +class ProcessGraphVisitor(ABC): + """ + Hierarchical Visitor for (nested) process graphs structures. + """ + + def __init__(self): + self.process_stack = [] + + @classmethod + def dereference_from_node_arguments(cls, process_graph: dict) -> str: + """ + Walk through the given (flat) process graph and replace (in-place) "from_node" references in + process arguments (dictionaries or lists) with the corresponding resolved subgraphs + + :param process_graph: process graph dictionary to be manipulated in-place + :return: name of the "result" node of the graph + """ + + # TODO avoid manipulating process graph in place? make it more explicit? work on a copy? + # TODO call it more something like "unflatten"?. Split this off of ProcessGraphVisitor? + # TODO implement this through `ProcessGraphUnflattener` ? + + def resolve_from_node(process_graph, node, from_node): + if from_node not in process_graph: + raise ProcessGraphVisitException( + "from_node {f!r} (referenced by {n!r}) not in process graph.".format(f=from_node, n=node) + ) + return process_graph[from_node] + + result_node = None + for node, node_dict in process_graph.items(): + if node_dict.get("result", False): + if result_node: + raise ProcessGraphVisitException("Multiple result nodes: {a}, {b}".format(a=result_node, b=node)) + result_node = node + arguments = node_dict.get("arguments", {}) + for arg in arguments.values(): + if isinstance(arg, dict): + if "from_node" in arg: + arg["node"] = resolve_from_node(process_graph, node, arg["from_node"]) + else: + for k, v in arg.items(): + if isinstance(v, dict) and "from_node" in v: + v["node"] = resolve_from_node(process_graph, node, v["from_node"]) + elif isinstance(arg, list): + for i, element in enumerate(arg): + if isinstance(element, dict) and "from_node" in element: + arg[i] = resolve_from_node(process_graph, node, element["from_node"]) + + if result_node is None: + dump = json.dumps(process_graph, indent=2) + raise ProcessGraphVisitException("No result node in process graph: " + dump[:1000]) + return result_node + + def accept_process_graph(self, graph: dict) -> ProcessGraphVisitor: + """ + Traverse a (flat) process graph + + :param graph: + :return: + """ + # TODO: this is driver specific functionality, working on flattened graph structures. Make this more clear? + top_level_node = self.dereference_from_node_arguments(graph) + self.accept_node(graph[top_level_node]) + return self + + @deprecated(reason="Use accept_node() instead", version="0.4.6") + def accept(self, node: dict): + self.accept_node(node) + + def accept_node(self, node: dict): + pid = node["process_id"] + arguments = node.get("arguments", {}) + namespace = node.get("namespace", None) + self._accept_process(process_id=pid, arguments=arguments, namespace=namespace) + + def _accept_process(self, process_id: str, arguments: dict, namespace: Union[str, None]): + self.process_stack.append(process_id) + self.enterProcess(process_id=process_id, arguments=arguments, namespace=namespace) + for arg_id, value in sorted(arguments.items()): + if isinstance(value, list): + self.enterArray(argument_id=arg_id) + self._accept_argument_list(value) + self.leaveArray(argument_id=arg_id) + elif isinstance(value, dict): + self.enterArgument(argument_id=arg_id, value=value) + self._accept_argument_dict(value) + self.leaveArgument(argument_id=arg_id, value=value) + else: + self.constantArgument(argument_id=arg_id, value=value) + self.leaveProcess(process_id=process_id, arguments=arguments, namespace=namespace) + assert self.process_stack.pop() == process_id + + def _accept_argument_list(self, elements: list): + for element in elements: + if isinstance(element, dict): + self._accept_argument_dict(element) + self.arrayElementDone(element) + else: + self.constantArrayElement(element) + + def _accept_argument_dict(self, value: dict): + if "node" in value and "from_node" in value: + # TODO: this looks bit weird (or at least very specific). + self.accept_node(value["node"]) + elif value.get("from_node"): + self.accept_node(value["from_node"]) + elif "process_id" in value: + self.accept_node(value) + elif "from_parameter" in value: + self.from_parameter(value["from_parameter"]) + else: + self._accept_dict(value) + + def _accept_dict(self, value: dict): + pass + + def from_parameter(self, parameter_id: str): + pass + + def enterProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + pass + + def leaveProcess(self, process_id: str, arguments: dict, namespace: Union[str, None]): + pass + + def enterArgument(self, argument_id: str, value): + pass + + def leaveArgument(self, argument_id: str, value): + pass + + def constantArgument(self, argument_id: str, value): + pass + + def enterArray(self, argument_id: str): + pass + + def leaveArray(self, argument_id: str): + pass + + def constantArrayElement(self, value): + pass + + def arrayElementDone(self, value: dict): + pass + + +def find_result_node(flat_graph: dict) -> Tuple[str, dict]: + """ + Find result node in flat graph + + :return: tuple with node id (str) and node dictionary of the result node. + """ + result_nodes = [(key, node) for (key, node) in flat_graph.items() if node.get("result")] + + if len(result_nodes) == 1: + return result_nodes[0] + elif len(result_nodes) == 0: + raise ProcessGraphVisitException("Found no result node in flat process graph") + else: + keys = [k for (k, n) in result_nodes] + raise ProcessGraphVisitException( + "Found multiple result nodes in flat process graph: {keys!r}".format(keys=keys) + ) + + +class ProcessGraphUnflattener: + """ + Base class to process a flat graph representation of a process graph + and unflatten it by resolving the "from_node" references. + Subclassing and overriding certain methods allows to build a desired unflattened graph structure. + """ + + # Sentinel object for flagging a node "under construction" and detect graph cycles. + _UNDER_CONSTRUCTION = object() + + def __init__(self, flat_graph: dict): + self._flat_graph = flat_graph + self._nodes = {} + + @classmethod + def unflatten(cls, flat_graph: dict, **kwargs): + """Class method helper to unflatten given flat process graph""" + return cls(flat_graph=flat_graph, **kwargs).process() + + def process(self): + """Process the flat process graph: unflatten it.""" + result_key, result_node = find_result_node(flat_graph=self._flat_graph) + return self.get_node(result_key) + + def get_node(self, key: str) -> Any: + """Get processed node by node key.""" + if key not in self._nodes: + self._nodes[key] = self._UNDER_CONSTRUCTION + node = self._process_node(self._flat_graph[key]) + self._nodes[key] = node + elif self._nodes[key] is self._UNDER_CONSTRUCTION: + raise ProcessGraphVisitException("Cycle in process graph") + return self._nodes[key] + + def _process_node(self, node: dict) -> Any: + """ + Overridable: generate process graph node from flat_graph data. + """ + # Default implementation: basic validation/whitelisting, and only traverse arguments + return dict( + process_id=node["process_id"], + arguments=self._process_value(value=node["arguments"]), + **{k: node[k] for k in ["namespace", "description", "result"] if k in node}, + ) + + def _process_from_node(self, key: str, node: dict) -> Any: + """ + Overridable: generate a node from a flat_graph "from_node" reference + """ + # Default/original implementation: keep "from_node" key and add resolved node under "node" key. + # TODO: just return `self.get_node(key=key)` + return {"from_node": key, "node": self.get_node(key=key)} + + def _process_from_parameter(self, name: str) -> Any: + """ + Overridable: generate a node from a flat_graph "from_parameter" reference + """ + # Default implementation: + return {"from_parameter": name} + + def _resolve_from_node(self, key: str) -> dict: + if key not in self._flat_graph: + raise ProcessGraphVisitException("from_node reference {k!r} not found in process graph".format(k=key)) + return self._flat_graph[key] + + def _process_value(self, value) -> Any: + if isinstance(value, dict): + if "from_node" in value: + key = value["from_node"] + node = self._resolve_from_node(key=key) + return self._process_from_node(key=key, node=node) + elif "from_parameter" in value: + name = value["from_parameter"] + return self._process_from_parameter(name=name) + elif "process_graph" in value: + # Don't traverse child process graphs + # TODO: should/can we? Can we know available parameters for validation, or do we skip validation? + return value + else: + return {k: self._process_value(v) for (k, v) in value.items()} + elif isinstance(value, (list, tuple)): + return [self._process_value(v) for v in value] + else: + return value diff --git a/lib/openeo/internal/processes/__init__.py b/lib/openeo/internal/processes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/openeo/internal/processes/builder.py b/lib/openeo/internal/processes/builder.py new file mode 100644 index 000000000..a2a156eb3 --- /dev/null +++ b/lib/openeo/internal/processes/builder.py @@ -0,0 +1,120 @@ +import inspect +import logging +import warnings +from typing import Any, Callable, Dict, List, Optional, Union + +from openeo.internal.graph_building import FlatGraphableMixin, PGNode, _FromNodeMixin +from openeo.rest import OpenEoClientException + +UNSET = object() +_log = logging.getLogger(__name__) + + +def _to_pgnode_data(value: Any) -> Union[PGNode, dict, Any]: + """Convert given value to valid process graph material""" + if isinstance(value, ProcessBuilderBase): + return value.pgnode + elif isinstance(value, list): + return [_to_pgnode_data(item) for item in value] + elif isinstance(value, Callable): + pg = convert_callable_to_pgnode(value) + return PGNode.to_process_graph_argument(pg) + else: + # Fallback: assume value is valid process graph material already. + return value + + +class ProcessBuilderBase(_FromNodeMixin, FlatGraphableMixin): + """ + Base implementation of a builder pattern that allows constructing process graphs + by calling functions. + """ + + # TODO: can this implementation be merged with PGNode directly? + + def __init__(self, pgnode: Union[PGNode, dict, list]): + self.pgnode = pgnode + + @classmethod + def process(cls, process_id: str, arguments: dict = None, namespace: Union[str, None] = None, **kwargs): + """ + Apply process, using given arguments + + :param process_id: process id of the process. + :param arguments: argument dictionary for the process. + :param namespace: process namespace (only necessary to specify for non-predefined or non-user-defined processes) + :return: new ProcessBuilder instance + """ + arguments = {**(arguments or {}), **kwargs} + arguments = {k: _to_pgnode_data(v) for k, v in arguments.items() if v is not UNSET} + return cls(PGNode(process_id=process_id, arguments=arguments, namespace=namespace)) + + def flat_graph(self) -> Dict[str, dict]: + """Get the process graph in internal flat dict representation.""" + return self.pgnode.flat_graph() + + def from_node(self) -> PGNode: + # _FromNodeMixin API + return self.pgnode + + +def get_parameter_names(process: Callable) -> List[str]: + """Get argument (aka parameter) names of given function/callable.""" + signature = inspect.signature(process) + return [ + p.name + for p in signature.parameters.values() + if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD) + ] + + +def convert_callable_to_pgnode(callback: Callable, parent_parameters: Optional[List[str]] = None) -> PGNode: + """ + Convert given process callback to a PGNode. + + >>> result = convert_callable_to_pgnode(lambda x: x + 5) + >>> assert isinstance(result, PGNode) + >>> result.flat_graph() + {"add1": {"process_id": "add", "arguments": {"x": {"from_parameter": "x"}, "y": 5}, "result": True}} + + """ + # TODO: eliminate local import (due to circular dependency)? + from openeo.processes import ProcessBuilder + + process_params = get_parameter_names(callback) + if parent_parameters is None: + # Due to lack of parent parameter information, + # we blindly use all callback's argument names as parameter names + # TODO #426: Instead of guessing: extract expected parent_parameters, e.g. based on parent process_id? + message = f"Blindly using callback parameter names from {callback!r} argument names: {process_params!r}" + if tuple(process_params) not in {(), ("x",), ("data",), ("x", "y")}: + warnings.warn(message) + else: + _log.info(message) + kwargs = {p: ProcessBuilder({"from_parameter": p}) for p in process_params} + elif parent_parameters == ["x", "y"] and (len(process_params) == 1 or process_params[:1] == ["data"]): + # Special case: wrap all parent parameters in an array + kwargs = {process_params[0]: ProcessBuilder([{"from_parameter": p} for p in parent_parameters])} + else: + # Check for direct correspondence between callback arguments and parent parameters (or subset thereof). + common = set(parent_parameters).intersection(process_params) + if common: + kwargs = {p: ProcessBuilder({"from_parameter": p}) for p in common} + elif min(len(parent_parameters), len(process_params)) == 0: + kwargs = {} + elif min(len(parent_parameters), len(process_params)) == 1: + # Fallback for common case of just one callback argument (pass the main parameter), + # or one parent parameter (just pass that one) + kwargs = {process_params[0]: ProcessBuilder({"from_parameter": parent_parameters[0]})} + else: + raise OpenEoClientException( + f"Callback argument mismatch: expected (prefix of) {parent_parameters}, but found found {process_params!r}" + ) + + # "Evaluate" the callback, which should give a ProcessBuilder again to extract pgnode from + result = callback(**kwargs) + if not isinstance(result, ProcessBuilderBase): + raise OpenEoClientException( + f"Callback {callback} did not evaluate to ProcessBuilderBase. Got {result!r} instead" + ) + return result.pgnode diff --git a/lib/openeo/internal/processes/generator.py b/lib/openeo/internal/processes/generator.py new file mode 100644 index 000000000..ee91d18b9 --- /dev/null +++ b/lib/openeo/internal/processes/generator.py @@ -0,0 +1,305 @@ +import argparse +import datetime +import keyword +import sys +import textwrap +from pathlib import Path +from typing import Iterator, List, Optional, Union + +from openeo.internal.processes.parse import Process, parse_all_from_dir + + +class PythonRenderer: + """Generator of Python function source code for a given openEO process""" + + DEFAULT_WIDTH = 115 + + def __init__( + self, + oo_mode: bool = False, + indent: str = " ", + body_template: str = "return _process({id!r}, {args})", + optional_default="None", + return_type_hint: Optional[str] = None, + decorator: Optional[str] = None, + ): + self.oo_mode = oo_mode + self.indent = indent + self.body_template = body_template + self.optional_default = optional_default + self.return_type_hint = return_type_hint + self.decorator = decorator + + def render_process(self, process: Process, prefix: str = None, width: int = DEFAULT_WIDTH) -> str: + if prefix is None: + prefix = " " if self.oo_mode else "" + + # TODO: add type hints + # TODO: width limit? + def_line = "def {id}({args}){th}:".format( + id=self._safe_name(process.id), + args=", ".join(self._def_arguments(process)), + th=" -> {t}".format(t=self.return_type_hint) if self.return_type_hint else "", + ) + + call_args = ", ".join(self._call_args(process)) + if len(call_args) > width: + # TODO: also include `id` placeholder in `self.body_format` + call_args = ( + "\n" + ",\n".join(self.indent + self.indent + a for a in self._call_args(process)) + "\n" + self.indent + ) + body = self.indent + self.body_template.format( + id=process.id, safe_name=self._safe_name(process.id), args=call_args + ) + + lines = ([self.decorator] if self.decorator else []) + [ + def_line, + self.render_docstring(process, width=width - len(prefix), prefix=self.indent), + body, + ] + return textwrap.indent("\n".join(lines), prefix=prefix) + + def _safe_name(self, name: str) -> str: + if keyword.iskeyword(name): + name += "_" + return name + + def _par_names(self, process: Process) -> List[str]: + """Names of the openEO process parameters""" + return [self._safe_name(p.name) for p in process.parameters] + + def _arg_names(self, process: Process) -> List[str]: + """Names of the arguments in the python function""" + arg_names = self._par_names(process) + if self.oo_mode and arg_names: + arg_names[0] = "self" + return arg_names + + def _call_args(self, process: Process) -> Iterator[str]: + for parameter, par_name, arg_name in zip( + process.parameters, self._par_names(process), self._arg_names(process) + ): + arg_expression = arg_name + if parameter.schema.is_process_graph(): + parent_parameters = [p["name"] for p in parameter.schema.schema["parameters"]] + arg_expression = f"build_child_callback({arg_expression}, parent_parameters={parent_parameters})" + if parameter.optional: + arg_expression = ( + f"({arg_expression} if {arg_name} not in [None, {self.optional_default}] else {arg_name})" + ) + yield f"{par_name}={arg_expression}" + + def _def_arguments(self, process: Process) -> Iterator[str]: + # TODO: add argument type hints? + for arg, param in zip(self._arg_names(process), process.parameters): + if param.optional: + yield "{a}={d}".format(a=arg, d=self.optional_default) + elif param.has_default(): + yield "{a}={d!r}".format(a=arg, d=param.default) + else: + yield arg + if self.oo_mode and len(process.parameters) == 0: + yield "self" + + def render_docstring(self, process: Process, prefix="", width: int = DEFAULT_WIDTH) -> str: + w = width - len(prefix) + # TODO: use description instead of summary? + doc = "\n\n".join(textwrap.fill(d, width=w) for d in process.summary.split("\n\n")) + params = "\n".join( + self._hanging_indent(":param {n}: {d}".format(n=arg, d=param.description), width=w) + for arg, param in zip(self._arg_names(process), process.parameters) + ) + returns = self._hanging_indent(":return: {d}".format(d=process.returns.description), width=w) + return textwrap.indent('"""\n' + doc + "\n\n" + (params + "\n\n" + returns).strip() + '\n"""', prefix=prefix) + + def _hanging_indent(self, paragraph: str, indent=" ", width: int = DEFAULT_WIDTH) -> str: + return textwrap.indent(textwrap.fill(paragraph, width=width - len(indent)), prefix=indent).lstrip() + + +def collect_processes(sources: List[Union[Path, str]]) -> List[Process]: + processes = {} + for src in [Path(s) for s in sources]: + if src.is_dir(): + to_add = parse_all_from_dir(src) + else: + to_add = [Process.from_json_file(src)] + for p in to_add: + if p.id in processes: + raise Exception(f"Duplicate source for process {p.id!r}") + processes[p.id] = p + return sorted(processes.values(), key=lambda p: p.id) + + +def generate_process_py(processes: List[Process], output=sys.stdout, argv=None): + oo_src = textwrap.dedent( + """ + from __future__ import annotations + + import builtins + + from openeo.internal.documentation import openeo_process + from openeo.internal.processes.builder import UNSET, ProcessBuilderBase + from openeo.rest._datacube import build_child_callback + + + class ProcessBuilder(ProcessBuilderBase): + \"\"\" + .. include:: api-processbuilder.rst + \"\"\" + + _ITERATION_LIMIT = 100 + + @openeo_process(process_id="add", mode="operator") + def __add__(self, other) -> ProcessBuilder: + return self.add(other) + + @openeo_process(process_id="add", mode="operator") + def __radd__(self, other) -> ProcessBuilder: + return add(other, self) + + @openeo_process(process_id="subtract", mode="operator") + def __sub__(self, other) -> ProcessBuilder: + return self.subtract(other) + + @openeo_process(process_id="subtract", mode="operator") + def __rsub__(self, other) -> ProcessBuilder: + return subtract(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __mul__(self, other) -> ProcessBuilder: + return self.multiply(other) + + @openeo_process(process_id="multiply", mode="operator") + def __rmul__(self, other) -> ProcessBuilder: + return multiply(other, self) + + @openeo_process(process_id="divide", mode="operator") + def __truediv__(self, other) -> ProcessBuilder: + return self.divide(other) + + @openeo_process(process_id="divide", mode="operator") + def __rtruediv__(self, other) -> ProcessBuilder: + return divide(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __neg__(self) -> ProcessBuilder: + return self.multiply(-1) + + @openeo_process(process_id="power", mode="operator") + def __pow__(self, other) -> ProcessBuilder: + return self.power(other) + + @openeo_process(process_id="array_element", mode="operator") + def __getitem__(self, key) -> ProcessBuilder: + if isinstance(key, builtins.int): + if key > self._ITERATION_LIMIT: + raise RuntimeError( + "Exceeded ProcessBuilder iteration limit. " + "Are you mistakenly using a Python builtin like `sum()` or `all()` in a callback " + "instead of the appropriate helpers from the `openeo.processes` module?" + ) + return self.array_element(index=key) + else: + return self.array_element(label=key) + + @openeo_process(process_id="eq", mode="operator") + def __eq__(self, other) -> ProcessBuilder: + return eq(self, other) + + @openeo_process(process_id="neq", mode="operator") + def __ne__(self, other) -> ProcessBuilder: + return neq(self, other) + + @openeo_process(process_id="lt", mode="operator") + def __lt__(self, other) -> ProcessBuilder: + return lt(self, other) + + @openeo_process(process_id="lte", mode="operator") + def __le__(self, other) -> ProcessBuilder: + return lte(self, other) + + @openeo_process(process_id="ge", mode="operator") + def __ge__(self, other) -> ProcessBuilder: + return gte(self, other) + + @openeo_process(process_id="gt", mode="operator") + def __gt__(self, other) -> ProcessBuilder: + return gt(self, other) + + """ + ) + fun_src = textwrap.dedent( + """ + # Public shortcut + process = ProcessBuilder.process + # Private shortcut that has lower chance to collide with a process argument named `process` + _process = ProcessBuilder.process + + + """ + ) + fun_renderer = PythonRenderer( + body_template="return _process({id!r}, {args})", + optional_default="UNSET", + return_type_hint="ProcessBuilder", + decorator="@openeo_process", + ) + oo_renderer = PythonRenderer( + oo_mode=True, + body_template="return {safe_name}({args})", + optional_default="UNSET", + return_type_hint="ProcessBuilder", + decorator="@openeo_process", + ) + for p in processes: + fun_src += fun_renderer.render_process(p) + "\n\n\n" + oo_src += oo_renderer.render_process(p) + "\n\n" + output.write( + textwrap.dedent( + """ + # Do not edit this file directly. + # It is automatically generated. + """ + ) + ) + if argv: + output.write( + textwrap.dedent( + """\ + # Used command line arguments: + # {cli} + """.format( + cli=" ".join(argv) + ) + ) + ) + output.write(f"# Generated on {datetime.date.today().isoformat()}\n") + + output.write(oo_src) + output.write(fun_src.rstrip() + "\n") + + +def main(): + # Usage example (from project root): + # # Update subrepos (with process specs) + # python specs/update-subrepos.py + # python openeo/internal/processes/generator.py specs/openeo-processes specs/openeo-processes/proposals --output openeo/processes.py + + argv = sys.argv + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument( + "source", nargs="+", help="""Source directories or files containing openEO process definitions in JSON format""" + ) + arg_parser.add_argument("--output", help="Path to output 'processes.py' file") + + arguments = arg_parser.parse_args(argv[1:]) + sources = arguments.source + output = arguments.output + + processes = collect_processes(sources) + with open(output, "w", encoding="utf-8") if output else sys.stdout as f: + generate_process_py(processes, output=f, argv=argv) + + +if __name__ == "__main__": + main() diff --git a/lib/openeo/internal/processes/parse.py b/lib/openeo/internal/processes/parse.py new file mode 100644 index 000000000..1e22ba6bc --- /dev/null +++ b/lib/openeo/internal/processes/parse.py @@ -0,0 +1,164 @@ +""" +Functionality and tools to process openEO processes. +For example: parse a bunch of JSON descriptions and generate Python (stub) functions. +""" + +from __future__ import annotations + +import json +import re +import typing +from pathlib import Path +from typing import Any, Iterator, List, Optional, Union + +import requests + + +class Schema(typing.NamedTuple): + """Schema description of an openEO process parameter or return value.""" + + schema: Union[dict, list] + + @classmethod + def from_dict(cls, data: dict) -> Schema: + return cls(schema=data) + + def is_process_graph(self) -> bool: + """Is this a {"type": "object", "subtype": "process-graph"} schema?""" + return ( + isinstance(self.schema, dict) + and self.schema.get("type") == "object" + and self.schema.get("subtype") == "process-graph" + ) + + def accepts_geojson(self) -> bool: + """Does this schema accept inline GeoJSON objects?""" + + def is_geojson_schema(schema) -> bool: + return isinstance(schema, dict) and schema.get("type") == "object" and schema.get("subtype") == "geojson" + + if isinstance(self.schema, dict): + return is_geojson_schema(self.schema) + elif isinstance(self.schema, list): + return any(is_geojson_schema(s) for s in self.schema) + return False + + +_NO_DEFAULT = object() + + +class Parameter(typing.NamedTuple): + """openEO process parameter""" + # TODO unify with openeo.api.process.Parameter? + + name: str + description: str + schema: Schema + default: Any = _NO_DEFAULT + optional: bool = False + + @classmethod + def from_dict(cls, data: dict) -> Parameter: + return cls( + name=data["name"], + description=data["description"], + schema=Schema.from_dict(data["schema"]), + default=data.get("default", _NO_DEFAULT), + optional=data.get("optional", False), + ) + + def has_default(self): + return self.default is not _NO_DEFAULT + + +class Returns: + """openEO process return description.""" + + def __init__(self, description: str, schema: Schema): + self.description = description + self.schema = schema + + @classmethod + def from_dict(cls, data: dict) -> Returns: + return cls(description=data["description"], schema=Schema.from_dict(data["schema"])) + + +class Process(typing.NamedTuple): + """ + Container for a opneEO process definition of an openEO process, + covering pre-defined processes, user-defined processes, + remote process definitions, etc. + """ + + # Common-denominator-wise only the process id is a required field in a process definition. + # Depending on the context in the openEO API, some other fields (e.g. "process_graph") + # may also be required. + id: str + parameters: Optional[List[Parameter]] = None + returns: Optional[Returns] = None + description: Optional[str] = None + summary: Optional[str] = None + # TODO: more properties? + + @classmethod + def from_dict(cls, data: dict) -> Process: + """Construct openEO process from dictionary values""" + return cls( + id=data["id"], + parameters=[Parameter.from_dict(d) for d in data["parameters"]] if "parameters" in data else None, + returns=Returns.from_dict(data["returns"]) if "returns" in data else None, + description=data.get("description"), + summary=data.get("summary"), + ) + + @classmethod + def from_json(cls, data: str) -> Process: + """Parse openEO process JSON description.""" + return cls.from_dict(json.loads(data)) + + @classmethod + def from_json_url(cls, url: str) -> Process: + """Parse openEO process JSON description from given URL.""" + return cls.from_dict(requests.get(url).json()) + + @classmethod + def from_json_file(cls, path: Union[str, Path]) -> Process: + """Parse openEO process JSON description file.""" + with Path(path).open("r") as f: + return cls.from_json(f.read()) + + +def parse_all_from_dir(path: Union[str, Path], pattern="*.json") -> Iterator[Process]: + """Parse all openEO process files in given directory""" + for p in sorted(Path(path).glob(pattern)): + yield Process.from_json_file(p) + + +def parse_remote_process_definition(namespace: str, process_id: Optional[str] = None) -> Process: + """ + Parse a process definition as defined by the "Remote Process Definition Extension" spec + https://github.com/Open-EO/openeo-api/tree/draft/extensions/remote-process-definition + """ + if not re.match("https?://", namespace): + raise ValueError(f"Expected absolute URL, but got {namespace!r}") + + resp = requests.get(url=namespace) + resp.raise_for_status() + data = resp.json() + assert isinstance(data, dict) + + if "id" not in data and "processes" in data and isinstance(data["processes"], list): + # Handle process listing: filter out right process + if not isinstance(process_id, str): + raise ValueError(f"Working with process listing, but got invalid process id {process_id!r}") + processes = [p for p in data["processes"] if p.get("id") == process_id] + if len(processes) != 1: + raise LookupError(f"Process {process_id!r} not found in process listing {namespace!r}") + (data,) = processes + + # Some final validation. + assert "id" in data, "Process definition should at least have an 'id' field" + if process_id is not None and data["id"] != process_id: + raise LookupError(f"Expected process id {process_id!r}, but found {data['id']!r}") + + return Process.from_dict(data) diff --git a/lib/openeo/internal/warnings.py b/lib/openeo/internal/warnings.py new file mode 100644 index 000000000..fe9d5489a --- /dev/null +++ b/lib/openeo/internal/warnings.py @@ -0,0 +1,95 @@ +import functools +import inspect +import warnings +from typing import Callable, Optional + +from deprecated.sphinx import deprecated as _deprecated + + +class UserDeprecationWarning(Warning): + """ + Python has a built-in `DeprecationWarning` class to warn about deprecated features, + but as the docs state (https://docs.python.org/3/library/warnings.html): + + when those warnings are intended for other Python developers + + Consequently, the default warning filters are set up to ignore (hide) these warnings + to the software end user. The developer is expected to explicitly set up + the warning filters to show the deprecation warnings again. + + In case of the openeo Python client however, this does not work because the client user + is usually the developer, but probably won't bother setting up warning filters properly. + + This custom warning class can be used as drop in replacement for `DeprecationWarning`, + where the deprecation warning should be visible by default. + """ + + pass + + +def test_warnings(stacklevel=1): + """Trigger some warnings (for test contexts).""" + for warning in [UserWarning, DeprecationWarning, UserDeprecationWarning]: + warnings.warn( + f"This is a {warning.__name__} (stacklevel {stacklevel})", category=warning, stacklevel=stacklevel + ) + + +def legacy_alias(orig: Callable, name: str, *, since: str, mode: str = "full"): + """ + Create legacy alias of given function/method/classmethod/staticmethod + + :param orig: function/method to create legacy alias for + :param name: original name of the alias + :param since: version since when this is alias is deprecated + :param mode: + - "full": raise warnings on calling, only have deprecation note as doc + - "soft": don't raise warning on calling, just add deprecation note to doc + :return: + """ + # TODO: drop `name` argument? + post_process = None + if isinstance(orig, classmethod): + post_process = classmethod + orig = orig.__func__ + kind = "class method" + elif isinstance(orig, staticmethod): + post_process = staticmethod + orig = orig.__func__ + kind = "static method" + elif inspect.ismethod(orig) or "self" in inspect.signature(orig).parameters: + kind = "method" + elif inspect.isfunction(orig): + kind = "function" + else: + raise ValueError(orig) + + # Create a "copy" by wrapping the original + @functools.wraps(orig) + def wrapper(*args, **kwargs): + return orig(*args, **kwargs) + + # Set deprecated name on the wrapper so that deprecation warnings use proper name. + wrapper.__name__ = name + + ref = f":py:{'meth' if 'method' in kind else 'func'}:`.{orig.__name__}`" + message = f"Usage of this legacy {kind} is deprecated. Use {ref} instead." + + if mode == "full": + # Drop original doc block, just show deprecation note. + wrapper.__doc__ = "" + wrapper = deprecated(reason=message, version=since)(wrapper) + elif mode == "soft": + # Only keep first paragraph of original doc block + wrapper.__doc__ = "\n\n".join(orig.__doc__.split("\n\n")[:1] + [f".. deprecated:: {since}\n {message}\n"]) + else: + raise ValueError(mode) + + if post_process: + wrapper = post_process(wrapper) + return wrapper + + +def deprecated(reason: str, version: str): + """Wrapper around `deprecated.sphinx.deprecated` to explicitly set the warning category.""" + return _deprecated(reason=reason, version=version, category=UserDeprecationWarning) diff --git a/lib/openeo/local/__init__.py b/lib/openeo/local/__init__.py new file mode 100644 index 000000000..bb84360b4 --- /dev/null +++ b/lib/openeo/local/__init__.py @@ -0,0 +1,3 @@ +from openeo.local.connection import LocalConnection + +__all__ = ["LocalConnection"] diff --git a/lib/openeo/local/collections.py b/lib/openeo/local/collections.py new file mode 100644 index 000000000..7e5e1b0f1 --- /dev/null +++ b/lib/openeo/local/collections.py @@ -0,0 +1,240 @@ +import logging +from pathlib import Path +from typing import List + +import rioxarray +import xarray as xr +from pyproj import Transformer + +_log = logging.getLogger(__name__) + + +def _get_dimension(dims: dict, candidates: List[str]): + for name in candidates: + if name in dims: + return name + error = f'Dimension matching one of the candidates {candidates} not found! The available ones are {dims}. Please rename the dimension accordingly and try again. This local collection will be skipped.' + raise Exception(error) + + +def _get_netcdf_zarr_metadata(file_path): + if '.zarr' in file_path.suffixes: + data = xr.open_dataset(file_path.as_posix(),chunks={},engine='zarr') + else: + data = xr.open_dataset(file_path.as_posix(),chunks={}) # Add decode_coords='all' if the crs as a band gives some issues + file_path = file_path.as_posix() + try: + t_dim = _get_dimension(data.dims, ['t', 'time', 'temporal', 'DATE']) + except Exception: + t_dim = None + try: + x_dim = _get_dimension(data.dims, ['x', 'X', 'lon', 'longitude']) + y_dim = _get_dimension(data.dims, ['y', 'Y', 'lat', 'latitude']) + except Exception as e: + _log.warning(e) + raise Exception(f'Error creating metadata for {file_path}') from e + metadata = {} + metadata['stac_version'] = '1.0.0-rc.2' + metadata['type'] = 'Collection' + metadata['id'] = file_path + data_attrs_lowercase = [x.lower() for x in data.attrs] + data_attrs_original = [x for x in data.attrs] + data_attrs = dict(zip(data_attrs_lowercase,data_attrs_original)) + if 'title' in data_attrs_lowercase: + metadata['title'] = data.attrs[data_attrs['title']] + else: + metadata['title'] = file_path + if 'description' in data_attrs_lowercase: + metadata['description'] = data.attrs[data_attrs['description']] + else: + metadata['description'] = '' + if 'license' in data_attrs_lowercase: + metadata['license'] = data.attrs[data_attrs['license']] + else: + metadata['license'] = '' + providers = [{'name':'', + 'roles':['producer'], + 'url':''}] + if 'providers' in data_attrs_lowercase: + providers[0]['name'] = data.attrs[data_attrs['providers']] + metadata['providers'] = providers + elif 'institution' in data_attrs_lowercase: + providers[0]['name'] = data.attrs[data_attrs['institution']] + metadata['providers'] = providers + else: + metadata['providers'] = providers + if 'links' in data_attrs_lowercase: + metadata['links'] = data.attrs[data_attrs['links']] + else: + metadata['links'] = '' + x_min = data[x_dim].min().item(0) + x_max = data[x_dim].max().item(0) + y_min = data[y_dim].min().item(0) + y_max = data[y_dim].max().item(0) + + crs_present = False + bands = list(data.data_vars) + if 'crs' in bands: + bands.remove('crs') + crs_present = True + extent = {} + if crs_present: + if "crs_wkt" in data.crs.attrs: + transformer = Transformer.from_crs(data.crs.attrs["crs_wkt"], "epsg:4326") + lat_min, lon_min = transformer.transform(x_min, y_min) + lat_max, lon_max = transformer.transform(x_max, y_max) + extent["spatial"] = {"bbox": [[lon_min, lat_min, lon_max, lat_max]]} + + if t_dim is not None: + t_min = str(data[t_dim].min().values) + t_max = str(data[t_dim].max().values) + extent['temporal'] = {'interval': [[t_min,t_max]]} + + metadata['extent'] = extent + + t_dimension = {} + if t_dim is not None: + t_dimension = {t_dim: {'type': 'temporal', 'extent':[t_min,t_max]}} + + x_dimension = {x_dim: {'type': 'spatial','axis':'x','extent':[x_min,x_max]}} + y_dimension = {y_dim: {'type': 'spatial','axis':'y','extent':[y_min,y_max]}} + if crs_present: + if 'crs_wkt' in data.crs.attrs: + x_dimension[x_dim]['reference_system'] = data.crs.attrs['crs_wkt'] + y_dimension[y_dim]['reference_system'] = data.crs.attrs['crs_wkt'] + + b_dimension = {} + if len(bands)>0: + b_dimension = {'bands': {'type': 'bands', 'values':bands}} + + metadata['cube:dimensions'] = {**t_dimension,**x_dimension,**y_dimension,**b_dimension} + + return metadata + + +def _get_geotiff_metadata(file_path): + data = rioxarray.open_rasterio(file_path.as_posix(),chunks={},band_as_variable=True) + file_path = file_path.as_posix() + try: + t_dim = _get_dimension(data.dims, ['t', 'time', 'temporal', 'DATE']) + except Exception: + t_dim = None + try: + x_dim = _get_dimension(data.dims, ['x', 'X', 'lon', 'longitude']) + y_dim = _get_dimension(data.dims, ['y', 'Y', 'lat', 'latitude']) + except Exception as e: + _log.warning(e) + raise Exception(f'Error creating metadata for {file_path}') from e + + metadata = {} + metadata['stac_version'] = '1.0.0-rc.2' + metadata['type'] = 'Collection' + metadata['id'] = file_path + data_attrs_lowercase = [x.lower() for x in data.attrs] + data_attrs_original = [x for x in data.attrs] + data_attrs = dict(zip(data_attrs_lowercase,data_attrs_original)) + if 'title' in data_attrs_lowercase: + metadata['title'] = data.attrs[data_attrs['title']] + else: + metadata['title'] = file_path + if 'description' in data_attrs_lowercase: + metadata['description'] = data.attrs[data_attrs['description']] + else: + metadata['description'] = '' + if 'license' in data_attrs_lowercase: + metadata['license'] = data.attrs[data_attrs['license']] + else: + metadata['license'] = '' + providers = [{'name':'', + 'roles':['producer'], + 'url':''}] + if 'providers' in data_attrs_lowercase: + providers[0]['name'] = data.attrs[data_attrs['providers']] + metadata['providers'] = providers + elif 'institution' in data_attrs_lowercase: + providers[0]['name'] = data.attrs[data_attrs['institution']] + metadata['providers'] = providers + else: + metadata['providers'] = providers + if 'links' in data_attrs_lowercase: + metadata['links'] = data.attrs[data_attrs['links']] + else: + metadata['links'] = '' + x_min = data[x_dim].min().item(0) + x_max = data[x_dim].max().item(0) + y_min = data[y_dim].min().item(0) + y_max = data[y_dim].max().item(0) + + crs_present = False + coords = list(data.coords) + if 'spatial_ref' in coords: + # bands.remove('crs') + crs_present = True + bands = [] + for d in data.data_vars: + data_attrs_lowercase = [x.lower() for x in data[d].attrs] + data_attrs_original = [x for x in data[d].attrs] + data_attrs = dict(zip(data_attrs_lowercase,data_attrs_original)) + if 'description' in data_attrs_lowercase: + bands.append(data[d].attrs[data_attrs['description']]) + else: + bands.append(d) + extent = {} + if crs_present: + if 'crs_wkt' in data.spatial_ref.attrs: + transformer = Transformer.from_crs(data.spatial_ref.attrs['crs_wkt'], 'epsg:4326') + lat_min,lon_min = transformer.transform(x_min,y_min) + lat_max,lon_max = transformer.transform(x_max,y_max) + extent['spatial'] = {'bbox': [[lon_min, lat_min, lon_max, lat_max]]} + + if t_dim is not None: + t_min = str(data[t_dim].min().values) + t_max = str(data[t_dim].max().values) + extent['temporal'] = {'interval': [[t_min,t_max]]} + + metadata['extent'] = extent + + t_dimension = {} + if t_dim is not None: + t_dimension = {t_dim: {'type': 'temporal', 'extent':[t_min,t_max]}} + + x_dimension = {x_dim: {'type': 'spatial','axis':'x','extent':[x_min,x_max]}} + y_dimension = {y_dim: {'type': 'spatial','axis':'y','extent':[y_min,y_max]}} + if crs_present: + if 'crs_wkt' in data.spatial_ref.attrs: + x_dimension[x_dim]['reference_system'] = data.spatial_ref.attrs['crs_wkt'] + y_dimension[y_dim]['reference_system'] = data.spatial_ref.attrs['crs_wkt'] + + b_dimension = {} + if len(bands)>0: + b_dimension = {'bands': {'type': 'bands', 'values':bands}} + + metadata['cube:dimensions'] = {**t_dimension,**x_dimension,**y_dimension,**b_dimension} + + return metadata + + +def _get_local_collections(local_collections_path): + if isinstance(local_collections_path,str): + local_collections_path = [local_collections_path] + local_collections_list = [] + for flds in local_collections_path: + local_collections_netcdf_zarr = [p for p in Path(flds).rglob('*') if p.suffix in ['.nc','.zarr']] + for local_file in local_collections_netcdf_zarr: + try: + metadata = _get_netcdf_zarr_metadata(local_file) + local_collections_list.append(metadata) + except Exception as e: + _log.error(e) + continue + local_collections_geotiffs = [p for p in Path(flds).rglob('*') if p.suffix in ['.tif','.tiff']] + for local_file in local_collections_geotiffs: + try: + metadata = _get_geotiff_metadata(local_file) + local_collections_list.append(metadata) + except Exception as e: + _log.error(e) + continue + local_collections_dict = {'collections':local_collections_list} + + return local_collections_dict diff --git a/lib/openeo/local/connection.py b/lib/openeo/local/connection.py new file mode 100644 index 000000000..7de3cd452 --- /dev/null +++ b/lib/openeo/local/connection.py @@ -0,0 +1,285 @@ +import datetime +import logging +from pathlib import Path +from typing import Callable, Dict, List, Optional, Union + +import numpy as np +import xarray as xr +from openeo_pg_parser_networkx.graph import OpenEOProcessGraph +from openeo_pg_parser_networkx.pg_schema import BoundingBox, TemporalInterval +from openeo_processes_dask.process_implementations.cubes import load_stac + +from openeo.internal.graph_building import PGNode, as_flat_graph +from openeo.internal.jupyter import VisualDict, VisualList +from openeo.local.collections import ( + _get_geotiff_metadata, + _get_local_collections, + _get_netcdf_zarr_metadata, +) +from openeo.local.processing import PROCESS_REGISTRY +from openeo.metadata import ( + Band, + BandDimension, + CollectionMetadata, + SpatialDimension, + TemporalDimension, +) +from openeo.rest.datacube import DataCube + +_log = logging.getLogger(__name__) + + +class LocalConnection(): + """ + Connection to no backend, for local processing. + """ + + def __init__(self,local_collections_path: Union[str,List]): + """ + Constructor of LocalConnection. + + :param local_collections_path: String or list of strings, path to the folder(s) with + the local collections in netCDF, geoTIFF or ZARR. + """ + self.local_collections_path = local_collections_path + + def list_collections(self) -> List[dict]: + """ + List basic metadata of all collections provided in the local collections folder. + + .. caution:: + :return: list of dictionaries with basic collection metadata. + """ + data = _get_local_collections(self.local_collections_path)["collections"] + return VisualList("collections", data=data) + + def describe_collection(self, collection_id: str) -> dict: + """ + Get full collection metadata for given collection id. + + .. seealso:: + + :py:meth:`~openeo.rest.connection.Connection.list_collection_ids` + to list all collection ids provided by the back-end. + + :param collection_id: collection id + :return: collection metadata. + """ + local_collection = Path(collection_id) + if '.nc' in local_collection.suffixes or '.zarr' in local_collection.suffixes: + data = _get_netcdf_zarr_metadata(local_collection) + elif '.tif' in local_collection.suffixes or '.tiff' in local_collection.suffixes: + data = _get_geotiff_metadata(local_collection) + return VisualDict("collection", data=data) + + def collection_metadata(self, name) -> CollectionMetadata: + # TODO: duplication with `Connection.describe_collection`: deprecate one or the other? + return CollectionMetadata(metadata=self.describe_collection(name)) + + def load_collection( + self, + collection_id: str, + spatial_extent: Optional[Dict[str, float]] = None, + temporal_extent: Optional[List[Union[str, datetime.datetime, datetime.date]]] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + fetch_metadata: bool = True, + ) -> DataCube: + """ + Load a DataCube by collection id. + + :param collection_id: image collection identifier + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval + :param bands: only add the specified bands + :param properties: limit data by metadata property predicates + :return: a datacube containing the requested data + """ + return DataCube.load_collection( + collection_id=collection_id, connection=self, + spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties, + fetch_metadata=fetch_metadata, + ) + + def datacube_from_process(self, process_id: str, namespace: Optional[str] = None, **kwargs) -> DataCube: + """ + Load a data cube from a (custom) process. + + :param process_id: The process id. + :param namespace: optional: process namespace + :param kwargs: The arguments of the custom process + :return: A :py:class:`DataCube`, without valid metadata, as the client is not aware of this custom process. + """ + graph = PGNode(process_id, namespace=namespace, arguments=kwargs) + return DataCube(graph=graph, connection=self) + + def load_stac( + self, + url: str, + spatial_extent: Optional[Dict[str, float]] = None, + temporal_extent: Optional[List[Union[str, datetime.datetime, datetime.date]]] = None, + bands: Optional[List[str]] = None, + properties: Optional[dict] = None, + ) -> DataCube: + """ + Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`. + A batch job result can be loaded by providing a reference to it. + + If supported by the underlying metadata and file format, the data that is added to the data cube can be + restricted with the parameters ``spatial_extent``, ``temporal_extent`` and ``bands``. + If no data is available for the given extents, a ``NoDataAvailable`` error is thrown. + + Remarks: + + * The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as + specified in the metadata if the ``bands`` parameter is set to ``null``. + * If no additional parameter is specified this would imply that the whole data set is expected to be loaded. + Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only + load the data that is actually required after evaluating subsequent processes such as filters. + This means that the values should be processed only after the data has been limited to the required extent + and as a consequence also to a manageable size. + + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) + or a specific STAC API Collection that allows to filter items and to download assets. + This includes batch job results, which itself are compliant to STAC. + For external URLs, authentication details such as API keys or tokens may need to be included in the URL. + + Batch job results can be specified in two ways: + + - For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with ``/jobs/{id}/results`` and ``{id}`` + is the corresponding batch job ID. + - For external results, a signed URL must be provided. Not all back-ends support signed URLs, + which are provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: + Limits the data to load to the specified bounding box or polygons. + + For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects + with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + + For vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. + + The GeoJSON can be one of the following feature types: + + * A ``Polygon`` or ``MultiPolygon`` geometry, + * a ``Feature`` with a ``Polygon`` or ``MultiPolygon`` geometry, or + * a ``FeatureCollection`` containing at least one ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometries. + + Set this parameter to ``None`` to set no limit for the spatial extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + + :param temporal_extent: + Limits the data to load to the specified left-closed temporal interval. + Applies to all temporal dimensions. + The interval has to be specified as an array with exactly two elements: + + 1. The first element is the start of the temporal interval. + The specified instance in time is **included** in the interval. + 2. The second element is the end of the temporal interval. + The specified instance in time is **excluded** from the interval. + + The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. + + Also supports open intervals by setting one of the boundaries to ``None``, but never both. + + Set this parameter to ``None`` to set no limit for the temporal extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_temporal()`` directly after loading unbounded data. + + :param bands: + Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. + + Either the unique band name (metadata field ``name`` in bands) or one of the common band names + (metadata field ``common_name`` in bands) can be specified. + If the unique band name and the common name conflict, the unique band name has a higher priority. + + The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. + + It is recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :param properties: + Limits the data by metadata properties to include only data in the data cube which + all given conditions return ``True`` for (AND operation). + + Specify key-value-pairs with the key being the name of the metadata property, + which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. + This parameter is not supported for static STAC. + + .. versionadded:: 0.21.0 + """ + arguments = {"url": url} + # TODO: more normalization/validation of extent/band parameters and `properties` + if spatial_extent is not None: + arguments["spatial_extent"] = spatial_extent + if temporal_extent is not None: + arguments["temporal_extent"] = DataCube._get_temporal_extent(extent=temporal_extent) + if bands is not None: + arguments["bands"] = bands + if properties is not None: + arguments["properties"] = properties + cube = self.datacube_from_process(process_id="load_stac", **arguments) + # detect actual metadata from URL + # run load_stac to get the datacube metadata + if spatial_extent is not None: + arguments["spatial_extent"] = BoundingBox.parse_obj(spatial_extent) + if temporal_extent is not None: + arguments["temporal_extent"] = TemporalInterval.parse_obj(temporal_extent) + xarray_cube = load_stac(**arguments) + attrs = xarray_cube.attrs + for at in attrs: + # allowed types: str, Number, ndarray, number, list, tuple + if not isinstance(attrs[at], (int, float, str, np.ndarray, list, tuple)): + attrs[at] = str(attrs[at]) + metadata = CollectionMetadata( + attrs, + dimensions=[ + SpatialDimension(name=xarray_cube.openeo.x_dim, extent=[]), + SpatialDimension(name=xarray_cube.openeo.y_dim, extent=[]), + TemporalDimension(name=xarray_cube.openeo.temporal_dims[0], extent=[]), + BandDimension( + name=xarray_cube.openeo.band_dims[0], + bands=[Band(name=x) for x in xarray_cube[xarray_cube.openeo.band_dims[0]].values], + ), + ], + ) + cube.metadata = metadata + return cube + + def list_udf_runtimes(self) -> dict: + """ + Loads all available UDF runtimes. + + :return: All available UDF runtimes + """ + runtimes = { + "Python": {"title": "Python 3", "type": "language", "versions": {"3": {"libraries": {}}}, "default": "3"} + } + return VisualDict("udf-runtimes", data=runtimes) + + def execute( + self, + process_graph: Union[dict, str, Path], + *, + validate: Optional[bool] = None, + auto_decode: bool = True, + ) -> xr.DataArray: + """ + Execute locally the process graph and return the result as an xarray.DataArray. + + :param process_graph: (flat) dict representing a process graph, or process graph as raw JSON string, + :return: a datacube containing the requested data + """ + if validate: + raise ValueError("LocalConnection does not support process graph validation") + if auto_decode is not True: + raise ValueError("LocalConnection requires auto_decode=True") + process_graph = as_flat_graph(process_graph) + return OpenEOProcessGraph(process_graph).to_callable(PROCESS_REGISTRY)() diff --git a/lib/openeo/local/processing.py b/lib/openeo/local/processing.py new file mode 100644 index 000000000..4adce909d --- /dev/null +++ b/lib/openeo/local/processing.py @@ -0,0 +1,82 @@ +import inspect +import logging +from pathlib import Path + +import openeo_processes_dask.process_implementations +import openeo_processes_dask.specs +import rasterio +import rioxarray +import xarray as xr +from openeo_pg_parser_networkx import ProcessRegistry +from openeo_pg_parser_networkx.process_registry import Process +from openeo_processes_dask.process_implementations.core import process +from openeo_processes_dask.process_implementations.data_model import RasterCube + +_log = logging.getLogger(__name__) + + +def init_process_registry(): + process_registry = ProcessRegistry(wrap_funcs=[process]) + + # Import these pre-defined processes from openeo_processes_dask and register them into registry + processes_from_module = [ + func + for _, func in inspect.getmembers( + openeo_processes_dask.process_implementations, + inspect.isfunction, + ) + ] + + specs = {} + for func in processes_from_module: + try: + specs[func.__name__] = getattr(openeo_processes_dask.specs, func.__name__) + except Exception: + continue + + for func in processes_from_module: + try: + process_registry[func.__name__] = Process( + spec=specs[func.__name__], implementation=func + ) + except Exception: + continue + return process_registry + + +PROCESS_REGISTRY = init_process_registry() + + +def load_local_collection(*args, **kwargs): + pretty_args = {k: repr(v)[:80] for k, v in kwargs.items()} + _log.info("Running process load_collection") + _log.debug( + f"Running process load_collection with resolved parameters: {pretty_args}" + ) + collection = Path(kwargs['id']) + if '.zarr' in collection.suffixes: + data = xr.open_dataset(kwargs['id'],chunks={},engine='zarr') + elif '.nc' in collection.suffixes: + data = xr.open_dataset(kwargs['id'],chunks={},decode_coords='all') # Add decode_coords='all' if the crs as a band gives some issues + crs = None + if 'crs' in data.coords: + if 'spatial_ref' in data.crs.attrs: + crs = data.crs.attrs['spatial_ref'] + elif 'crs_wkt' in data.crs.attrs: + crs = data.crs.attrs['crs_wkt'] + data = data.to_array(dim='bands') + if crs is not None: + data.rio.write_crs(crs,inplace=True) + elif '.tiff' in collection.suffixes or '.tif' in collection.suffixes: + data = rioxarray.open_rasterio(kwargs['id'],chunks={},band_as_variable=True) + for d in data.data_vars: + descriptions = [v for k, v in data[d].attrs.items() if k.lower() == "description"] + if descriptions: + data = data.rename({d: descriptions[0]}) + data = data.to_array(dim='bands') + return data + +PROCESS_REGISTRY["load_collection"] = Process( + spec=openeo_processes_dask.specs.load_collection, + implementation=load_local_collection, +) diff --git a/lib/openeo/metadata.py b/lib/openeo/metadata.py new file mode 100644 index 000000000..1de8c38b7 --- /dev/null +++ b/lib/openeo/metadata.py @@ -0,0 +1,706 @@ +from __future__ import annotations + +import functools +import logging +import warnings +from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Union + +import pystac +import pystac.extensions.datacube +import pystac.extensions.eo +import pystac.extensions.item_assets + +from openeo.internal.jupyter import render_component +from openeo.util import deep_get + +_log = logging.getLogger(__name__) + + +class MetadataException(Exception): + pass + + +class DimensionAlreadyExistsException(MetadataException): + pass + + +# TODO: make these dimension classes immutable data classes +class Dimension: + """Base class for dimensions.""" + + def __init__(self, type: str, name: str): + self.type = type + self.name = name + + def __repr__(self): + return "{c}({f})".format( + c=self.__class__.__name__, + f=", ".join("{k!s}={v!r}".format(k=k, v=v) for (k, v) in self.__dict__.items()) + ) + + def __eq__(self, other): + return self.__class__ == other.__class__ and self.__dict__ == other.__dict__ + + def rename(self, name) -> Dimension: + """Create new dimension with new name.""" + return Dimension(type=self.type, name=name) + + def rename_labels(self, target, source) -> Dimension: + """ + Rename labels, if the type of dimension allows it. + + :param target: List of target labels + :param source: Source labels, or empty list + :return: A new dimension with modified labels, or the same if no change is applied. + """ + # In general, we don't have/manage label info here, so do nothing. + return Dimension(type=self.type, name=self.name) + + +class SpatialDimension(Dimension): + DEFAULT_CRS = 4326 + + def __init__( + self, + name: str, + extent: Union[Tuple[float, float], List[float]], + crs: Union[str, int, dict] = DEFAULT_CRS, + step=None, + ): + """ + + @param name: + @param extent: + @param crs: + @param step: The space between the values. Use null for irregularly spaced steps. + """ + super().__init__(type="spatial", name=name) + self.extent = extent + self.crs = crs + self.step = step + + def rename(self, name) -> Dimension: + return SpatialDimension(name=name, extent=self.extent, crs=self.crs, step=self.step) + + +class TemporalDimension(Dimension): + def __init__(self, name: str, extent: Union[Tuple[str, str], List[str]]): + super().__init__(type="temporal", name=name) + self.extent = extent + + def rename(self, name) -> Dimension: + return TemporalDimension(name=name, extent=self.extent) + + def rename_labels(self, target, source) -> Dimension: + # TODO should we check if the extent has changed with the new labels? + return TemporalDimension(name=self.name, extent=self.extent) + + +class Band(NamedTuple): + """ + Simple container class for band metadata. + Based on https://github.com/stac-extensions/eo#band-object + """ + + name: str + common_name: Optional[str] = None + # wavelength in micrometer + wavelength_um: Optional[float] = None + aliases: Optional[List[str]] = None + # "openeo:gsd" field (https://github.com/Open-EO/openeo-stac-extensions#GSD-Object) + gsd: Optional[dict] = None + + +class BandDimension(Dimension): + # TODO #575 support unordered bands and avoid assumption that band order is known. + def __init__(self, name: str, bands: List[Band]): + super().__init__(type="bands", name=name) + self.bands = bands + + @property + def band_names(self) -> List[str]: + return [b.name for b in self.bands] + + @property + def band_aliases(self) -> List[List[str]]: + return [b.aliases for b in self.bands] + + @property + def common_names(self) -> List[str]: + return [b.common_name for b in self.bands] + + def band_index(self, band: Union[int, str]) -> int: + """ + Resolve a given band (common) name/index to band index + + :param band: band name, common name or index + :return int: band index + """ + band_names = self.band_names + if isinstance(band, int) and 0 <= band < len(band_names): + return band + elif isinstance(band, str): + common_names = self.common_names + # First try common names if possible + if band in common_names: + return common_names.index(band) + if band in band_names: + return band_names.index(band) + # Check band aliases to still support old band names + aliases = [True if aliases and band in aliases else False for aliases in self.band_aliases] + if any(aliases): + return aliases.index(True) + raise ValueError("Invalid band name/index {b!r}. Valid names: {n!r}".format(b=band, n=band_names)) + + def band_name(self, band: Union[str, int], allow_common=True) -> str: + """Resolve (common) name or index to a valid (common) name""" + if isinstance(band, str): + if band in self.band_names: + return band + elif band in self.common_names: + if allow_common: + return band + else: + return self.band_names[self.common_names.index(band)] + elif any([True if aliases and band in aliases else False for aliases in self.band_aliases]): + return self.band_names[self.band_index(band)] + elif isinstance(band, int) and 0 <= band < len(self.bands): + return self.band_names[band] + raise ValueError("Invalid band name/index {b!r}. Valid names: {n!r}".format(b=band, n=self.band_names)) + + def filter_bands(self, bands: List[Union[int, str]]) -> BandDimension: + """ + Construct new BandDimension with subset of bands, + based on given band indices or (common) names + """ + return BandDimension( + name=self.name, + bands=[self.bands[self.band_index(b)] for b in bands] + ) + + def append_band(self, band: Band) -> BandDimension: + """Create new BandDimension with appended band.""" + if band.name in self.band_names: + raise ValueError("Duplicate band {b!r}".format(b=band)) + + return BandDimension( + name=self.name, + bands=self.bands + [band] + ) + + def rename_labels(self, target, source) -> Dimension: + if source: + if len(target) != len(source): + raise ValueError( + "In rename_labels, `target` and `source` should have same number of labels, " + "but got: `target` {t} and `source` {s}".format(t=target, s=source) + ) + new_bands = self.bands.copy() + for old_name, new_name in zip(source, target): + band_index = self.band_index(old_name) + the_band = new_bands[band_index] + new_bands[band_index] = Band( + name=new_name, + common_name=the_band.common_name, + wavelength_um=the_band.wavelength_um, + aliases=the_band.aliases, + gsd=the_band.gsd, + ) + else: + new_bands = [Band(name=n) for n in target] + return BandDimension(name=self.name, bands=new_bands) + + def rename(self, name) -> Dimension: + return BandDimension(name=name, bands=self.bands) + +class CubeMetadata: + """ + Interface for metadata of a data cube. + + Allows interaction with the cube dimensions and their labels (if available). + """ + + def __init__(self, dimensions: Optional[List[Dimension]] = None): + # Original collection metadata (actual cube metadata might be altered through processes) + self._dimensions = dimensions + self._band_dimension = None + self._temporal_dimension = None + + if dimensions is not None: + for dim in self._dimensions: + # TODO: here we blindly pick last bands or temporal dimension if multiple. Let user choose? + # TODO: add spacial dimension handling? + if dim.type == "bands": + if isinstance(dim, BandDimension): + self._band_dimension = dim + else: + raise MetadataException("Invalid band dimension {d!r}".format(d=dim)) + if dim.type == "temporal": + if isinstance(dim, TemporalDimension): + self._temporal_dimension = dim + else: + raise MetadataException("Invalid temporal dimension {d!r}".format(d=dim)) + + def __eq__(self, o: Any) -> bool: + return isinstance(o, type(self)) and self._dimensions == o._dimensions + + def _clone_and_update(self, dimensions: Optional[List[Dimension]] = None, **kwargs) -> CubeMetadata: + """Create a new instance (of same class) with copied/updated fields.""" + cls = type(self) + if dimensions is None: + dimensions = self._dimensions + return cls(dimensions=dimensions, **kwargs) + + def dimension_names(self) -> List[str]: + return list(d.name for d in self._dimensions) + + def assert_valid_dimension(self, dimension: str) -> str: + """Make sure given dimension name is valid.""" + names = self.dimension_names() + if dimension not in names: + raise ValueError(f"Invalid dimension {dimension!r}. Should be one of {names}") + return dimension + + def has_band_dimension(self) -> bool: + return isinstance(self._band_dimension, BandDimension) + + @property + def band_dimension(self) -> BandDimension: + """Dimension corresponding to spectral/logic/thematic "bands".""" + if not self.has_band_dimension(): + raise MetadataException("No band dimension") + return self._band_dimension + + def has_temporal_dimension(self) -> bool: + return isinstance(self._temporal_dimension, TemporalDimension) + + @property + def temporal_dimension(self) -> TemporalDimension: + if not self.has_temporal_dimension(): + raise MetadataException("No temporal dimension") + return self._temporal_dimension + + @property + def spatial_dimensions(self) -> List[SpatialDimension]: + return [d for d in self._dimensions if isinstance(d, SpatialDimension)] + + @property + def bands(self) -> List[Band]: + """Get band metadata as list of Band metadata tuples""" + return self.band_dimension.bands + + @property + def band_names(self) -> List[str]: + """Get band names of band dimension""" + return self.band_dimension.band_names + + @property + def band_common_names(self) -> List[str]: + return self.band_dimension.common_names + + def get_band_index(self, band: Union[int, str]) -> int: + # TODO: eliminate this shortcut for smaller API surface + return self.band_dimension.band_index(band) + + def filter_bands(self, band_names: List[Union[int, str]]) -> CubeMetadata: + """ + Create new `CubeMetadata` with filtered band dimension + :param band_names: list of band names/indices to keep + :return: + """ + assert self.band_dimension + return self._clone_and_update( + dimensions=[d.filter_bands(band_names) if isinstance(d, BandDimension) else d for d in self._dimensions] + ) + + def append_band(self, band: Band) -> CubeMetadata: + """ + Create new `CubeMetadata` with given band added to band dimension. + """ + assert self.band_dimension + return self._clone_and_update( + dimensions=[d.append_band(band) if isinstance(d, BandDimension) else d for d in self._dimensions] + ) + + def rename_labels(self, dimension: str, target: list, source: list = None) -> CubeMetadata: + """ + Renames the labels of the specified dimension from source to target. + + :param dimension: Dimension name + :param target: The new names for the labels. + :param source: The names of the labels as they are currently in the data cube. + + :return: Updated metadata + """ + self.assert_valid_dimension(dimension) + loc = self.dimension_names().index(dimension) + new_dimensions = self._dimensions.copy() + new_dimensions[loc] = new_dimensions[loc].rename_labels(target, source) + + return self._clone_and_update(dimensions=new_dimensions) + + def rename_dimension(self, source: str, target: str) -> CubeMetadata: + """ + Rename source dimension into target, preserving other properties + """ + self.assert_valid_dimension(source) + loc = self.dimension_names().index(source) + new_dimensions = self._dimensions.copy() + new_dimensions[loc] = new_dimensions[loc].rename(target) + + return self._clone_and_update(dimensions=new_dimensions) + + def reduce_dimension(self, dimension_name: str) -> CubeMetadata: + """Create new CubeMetadata object by collapsing/reducing a dimension.""" + # TODO: option to keep reduced dimension (with a single value)? + # TODO: rename argument to `name` for more internal consistency + # TODO: merge with drop_dimension (which does the same). + self.assert_valid_dimension(dimension_name) + loc = self.dimension_names().index(dimension_name) + dimensions = self._dimensions[:loc] + self._dimensions[loc + 1 :] + return self._clone_and_update(dimensions=dimensions) + + def reduce_spatial(self) -> CubeMetadata: + """Create new CubeMetadata object by reducing the spatial dimensions.""" + dimensions = [d for d in self._dimensions if not isinstance(d, SpatialDimension)] + return self._clone_and_update(dimensions=dimensions) + + def add_dimension(self, name: str, label: Union[str, float], type: str = None) -> CubeMetadata: + """Create new CubeMetadata object with added dimension""" + if any(d.name == name for d in self._dimensions): + raise DimensionAlreadyExistsException(f"Dimension with name {name!r} already exists") + if type == "bands": + dim = BandDimension(name=name, bands=[Band(name=label)]) + elif type == "spatial": + dim = SpatialDimension(name=name, extent=[label, label]) + elif type == "temporal": + dim = TemporalDimension(name=name, extent=[label, label]) + else: + dim = Dimension(type=type or "other", name=name) + return self._clone_and_update(dimensions=self._dimensions + [dim]) + + def drop_dimension(self, name: str = None) -> CubeMetadata: + """Create new CubeMetadata object without dropped dimension with given name""" + dimension_names = self.dimension_names() + if name not in dimension_names: + raise ValueError("No dimension named {n!r} (valid names: {ns!r})".format(n=name, ns=dimension_names)) + return self._clone_and_update(dimensions=[d for d in self._dimensions if not d.name == name]) + + def __str__(self) -> str: + bands = self.band_names if self.has_band_dimension() else "no bands dimension" + return f"CubeMetadata({bands} - {self.dimension_names()})" + + +class CollectionMetadata(CubeMetadata): + """ + Wrapper for EO Data Collection metadata. + + Simplifies getting values from deeply nested mappings, + allows additional parsing and normalizing compatibility issues. + + Metadata is expected to follow format defined by + https://openeo.org/documentation/1.0/developers/api/reference.html#operation/describe-collection + (with partial support for older versions) + + """ + + def __init__(self, metadata: dict, dimensions: List[Dimension] = None): + self._orig_metadata = metadata + if dimensions is None: + dimensions = self._parse_dimensions(self._orig_metadata) + + super().__init__(dimensions=dimensions) + + @classmethod + def _parse_dimensions(cls, spec: dict, complain: Callable[[str], None] = warnings.warn) -> List[Dimension]: + """ + Extract data cube dimension metadata from STAC-like description of a collection. + + Dimension metadata comes from different places in spec: + - 'cube:dimensions' has dimension names (e.g. 'x', 'y', 't'), dimension extent info + and band names for band dimensions + - 'eo:bands' has more detailed band information like "common" name and wavelength info + + This helper tries to normalize/combine these sources. + + :param spec: STAC like collection metadata dict + :param complain: handler for warnings + :return list: list of `Dimension` objects + + """ + + # Dimension info is in `cube:dimensions` (or 0.4-style `properties/cube:dimensions`) + cube_dimensions = ( + deep_get(spec, "cube:dimensions", default=None) + or deep_get(spec, "properties", "cube:dimensions", default=None) + or {} + ) + if not cube_dimensions: + complain("No cube:dimensions metadata") + dimensions = [] + for name, info in cube_dimensions.items(): + dim_type = info.get("type") + if dim_type == "spatial": + dimensions.append( + SpatialDimension( + name=name, + extent=info.get("extent"), + crs=info.get("reference_system", SpatialDimension.DEFAULT_CRS), + step=info.get("step", None), + ) + ) + elif dim_type == "temporal": + dimensions.append(TemporalDimension(name=name, extent=info.get("extent"))) + elif dim_type == "bands": + bands = [Band(name=b) for b in info.get("values", [])] + if not bands: + complain("No band names in dimension {d!r}".format(d=name)) + dimensions.append(BandDimension(name=name, bands=bands)) + else: + complain("Unknown dimension type {t!r}".format(t=dim_type)) + dimensions.append(Dimension(name=name, type=dim_type)) + + # Detailed band information: `summaries/[eo|raster]:bands` (and 0.4 style `properties/eo:bands`) + eo_bands = ( + deep_get(spec, "summaries", "eo:bands", default=None) + or deep_get(spec, "summaries", "raster:bands", default=None) + or deep_get(spec, "properties", "eo:bands", default=None) + ) + if eo_bands: + # center_wavelength is in micrometer according to spec + bands_detailed = [ + Band( + name=b["name"], + common_name=b.get("common_name"), + wavelength_um=b.get("center_wavelength"), + aliases=b.get("aliases"), + gsd=b.get("openeo:gsd"), + ) + for b in eo_bands + ] + # Update band dimension with more detailed info + band_dimensions = [d for d in dimensions if d.type == "bands"] + if len(band_dimensions) == 1: + dim = band_dimensions[0] + # Update band values from 'cube:dimensions' with more detailed 'eo:bands' info + eo_band_names = [b.name for b in bands_detailed] + cube_dimension_band_names = [b.name for b in dim.bands] + if eo_band_names == cube_dimension_band_names: + dim.bands = bands_detailed + else: + complain("Band name mismatch: {a} != {b}".format(a=cube_dimension_band_names, b=eo_band_names)) + elif len(band_dimensions) == 0: + if len(dimensions) == 0: + complain("Assuming name 'bands' for anonymous band dimension.") + dimensions.append(BandDimension(name="bands", bands=bands_detailed)) + else: + complain("No 'bands' dimension in 'cube:dimensions' while having 'eo:bands' or 'raster:bands'") + else: + complain("Multiple dimensions of type 'bands'") + + return dimensions + + def _clone_and_update( + self, metadata: dict = None, dimensions: List[Dimension] = None, **kwargs + ) -> CollectionMetadata: + """ + Create a new instance (of same class) with copied/updated fields. + + This overrides the method in `CubeMetadata` to keep the original metadata. + """ + cls = type(self) + if metadata is None: + metadata = self._orig_metadata + if dimensions is None: + dimensions = self._dimensions + return cls(metadata=metadata, dimensions=dimensions, **kwargs) + + def get(self, *args, default=None): + return deep_get(self._orig_metadata, *args, default=default) + + @property + def extent(self) -> dict: + # TODO: is this currently used and relevant? + # TODO: check against extent metadata in dimensions + return self._orig_metadata.get("extent") + + def _repr_html_(self): + return render_component("collection", data=self._orig_metadata) + + def __str__(self) -> str: + bands = self.band_names if self.has_band_dimension() else "no bands dimension" + return f"CollectionMetadata({self.extent} - {bands} - {self.dimension_names()})" + + +def metadata_from_stac(url: str) -> CubeMetadata: + """ + Reads the band metadata a static STAC catalog or a STAC API Collection and returns it as a :py:class:`CubeMetadata` + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a specific STAC API Collection + :return: A :py:class:`CubeMetadata` containing the DataCube band metadata from the url. + """ + + # TODO move these nested functions and other logic to _StacMetadataParser + + def get_band_metadata(eo_bands_location: dict) -> List[Band]: + # TODO: return None iso empty list when no metadata? + return [ + Band(name=band["name"], common_name=band.get("common_name"), wavelength_um=band.get("center_wavelength")) + for band in eo_bands_location.get("eo:bands", []) + ] + + def get_band_names(bands: List[Band]) -> List[str]: + return [band.name for band in bands] + + def is_band_asset(asset: pystac.Asset) -> bool: + return "eo:bands" in asset.extra_fields + + stac_object = pystac.read_file(href=url) + + if isinstance(stac_object, pystac.Item): + item = stac_object + if "eo:bands" in item.properties: + eo_bands_location = item.properties + elif item.get_collection() is not None: + # TODO: Also do asset based band detection (like below)? + eo_bands_location = item.get_collection().summaries.lists + else: + eo_bands_location = {} + bands = get_band_metadata(eo_bands_location) + + elif isinstance(stac_object, pystac.Collection): + collection = stac_object + bands = get_band_metadata(collection.summaries.lists) + + # Summaries is not a required field in a STAC collection, so also check the assets + for itm in collection.get_items(): + band_assets = {asset_id: asset for asset_id, asset in itm.get_assets().items() if is_band_asset(asset)} + + for asset in band_assets.values(): + asset_bands = get_band_metadata(asset.extra_fields) + for asset_band in asset_bands: + if asset_band.name not in get_band_names(bands): + bands.append(asset_band) + if _PYSTAC_1_9_EXTENSION_INTERFACE and collection.ext.has("item_assets"): + # TODO #575 support unordered band names and avoid conversion to a list. + bands = list(_StacMetadataParser().get_bands_from_item_assets(collection.ext.item_assets)) + + elif isinstance(stac_object, pystac.Catalog): + catalog = stac_object + bands = get_band_metadata(catalog.extra_fields.get("summaries", {})) + else: + raise ValueError(stac_object) + + # TODO: conditionally include band dimension when there was actual indication of band metadata? + band_dimension = BandDimension(name="bands", bands=bands) + dimensions = [band_dimension] + + # TODO: is it possible to derive the actual name of temporal dimension that the backend will use? + temporal_dimension = _StacMetadataParser().get_temporal_dimension(stac_object) + if temporal_dimension: + dimensions.append(temporal_dimension) + + metadata = CubeMetadata(dimensions=dimensions) + return metadata + +# Sniff for PySTAC extension API since version 1.9.0 (which is not available below Python 3.9) +# TODO: remove this once support for Python 3.7 and 3.8 is dropped +_PYSTAC_1_9_EXTENSION_INTERFACE = hasattr(pystac.Item, "ext") + + +class _StacMetadataParser: + """ + Helper to extract openEO metadata from STAC metadata resource + """ + + def __init__(self): + # TODO: toggles for how to handle strictness, warnings, logging, etc + pass + + def _get_band_from_eo_bands_item(self, eo_band: Union[dict, pystac.extensions.eo.Band]) -> Band: + if isinstance(eo_band, pystac.extensions.eo.Band): + return Band( + name=eo_band.name, + common_name=eo_band.common_name, + wavelength_um=eo_band.center_wavelength, + ) + elif isinstance(eo_band, dict) and "name" in eo_band: + return Band( + name=eo_band["name"], + common_name=eo_band.get("common_name"), + wavelength_um=eo_band.get("center_wavelength"), + ) + else: + raise ValueError(eo_band) + + def get_bands_from_eo_bands(self, eo_bands: List[Union[dict, pystac.extensions.eo.Band]]) -> List[Band]: + """ + Extract bands from STAC `eo:bands` array + + :param eo_bands: List of band objects, as dict or `pystac.extensions.eo.Band` instances + """ + # TODO: option to skip bands that failed to parse in some way? + return [self._get_band_from_eo_bands_item(band) for band in eo_bands] + + def _get_bands_from_item_asset( + self, item_asset: pystac.extensions.item_assets.AssetDefinition, *, _warn: Callable[[str], None] = _log.warning + ) -> Union[List[Band], None]: + """Get bands from a STAC 'item_assets' asset definition.""" + if _PYSTAC_1_9_EXTENSION_INTERFACE and item_asset.ext.has("eo"): + if item_asset.ext.eo.bands is not None: + return self.get_bands_from_eo_bands(item_asset.ext.eo.bands) + elif "eo:bands" in item_asset.properties: + # TODO: skip this in strict mode? + if _PYSTAC_1_9_EXTENSION_INTERFACE: + _warn("Extracting band info from 'eo:bands' metadata, but 'eo' STAC extension was not declared.") + return self.get_bands_from_eo_bands(item_asset.properties["eo:bands"]) + + def get_bands_from_item_assets( + self, item_assets: Dict[str, pystac.extensions.item_assets.AssetDefinition] + ) -> Set[Band]: + """ + Get bands extracted from "item_assets" objects (defined by "item-assets" extension, + in combination with "eo" extension) at STAC Collection top-level, + + Note that "item_assets" in STAC is a mapping, so the band order is undefined, + which is why we return a set of bands here. + + :param item_assets: a STAC `item_assets` mapping + """ + bands = set() + # Trick to just warn once per collection + _warn = functools.lru_cache()(_log.warning) + for item_asset in item_assets.values(): + asset_bands = self._get_bands_from_item_asset(item_asset, _warn=_warn) + if asset_bands: + bands.update(asset_bands) + return bands + + def get_temporal_dimension(self, stac_obj: pystac.STACObject) -> Union[TemporalDimension, None]: + """ + Extract the temporal dimension from a STAC Collection/Item (if any) + """ + # TODO: also extract temporal dimension from assets? + if _PYSTAC_1_9_EXTENSION_INTERFACE: + if stac_obj.ext.has("cube") and hasattr(stac_obj.ext, "cube"): + temporal_dims = [ + (n, d.extent or [None, None]) + for (n, d) in stac_obj.ext.cube.dimensions.items() + if d.dim_type == pystac.extensions.datacube.DimensionType.TEMPORAL + ] + if len(temporal_dims) == 1: + name, extent = temporal_dims[0] + return TemporalDimension(name=name, extent=extent) + else: + if isinstance(stac_obj, pystac.Item): + cube_dimensions = stac_obj.properties.get("cube:dimensions", {}) + elif isinstance(stac_obj, pystac.Collection): + cube_dimensions = stac_obj.extra_fields.get("cube:dimensions", {}) + else: + cube_dimensions = {} + temporal_dims = [ + (n, d.get("extent", [None, None])) for (n, d) in cube_dimensions.items() if d.get("type") == "temporal" + ] + if len(temporal_dims) == 1: + name, extent = temporal_dims[0] + return TemporalDimension(name=name, extent=extent) diff --git a/lib/openeo/processes.py b/lib/openeo/processes.py new file mode 100644 index 000000000..fcc13312f --- /dev/null +++ b/lib/openeo/processes.py @@ -0,0 +1,5590 @@ + +# Do not edit this file directly. +# It is automatically generated. +# Used command line arguments: +# openeo/internal/processes/generator.py specs/openeo-processes specs/openeo-processes/proposals specs/openeo-processes-legacy --output openeo/processes.py +# Generated on 2024-01-09 + +from __future__ import annotations + +import builtins + +from openeo.internal.documentation import openeo_process +from openeo.internal.processes.builder import UNSET, ProcessBuilderBase +from openeo.rest._datacube import build_child_callback + + +class ProcessBuilder(ProcessBuilderBase): + """ + .. include:: api-processbuilder.rst + """ + + _ITERATION_LIMIT = 100 + + @openeo_process(process_id="add", mode="operator") + def __add__(self, other) -> ProcessBuilder: + return self.add(other) + + @openeo_process(process_id="add", mode="operator") + def __radd__(self, other) -> ProcessBuilder: + return add(other, self) + + @openeo_process(process_id="subtract", mode="operator") + def __sub__(self, other) -> ProcessBuilder: + return self.subtract(other) + + @openeo_process(process_id="subtract", mode="operator") + def __rsub__(self, other) -> ProcessBuilder: + return subtract(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __mul__(self, other) -> ProcessBuilder: + return self.multiply(other) + + @openeo_process(process_id="multiply", mode="operator") + def __rmul__(self, other) -> ProcessBuilder: + return multiply(other, self) + + @openeo_process(process_id="divide", mode="operator") + def __truediv__(self, other) -> ProcessBuilder: + return self.divide(other) + + @openeo_process(process_id="divide", mode="operator") + def __rtruediv__(self, other) -> ProcessBuilder: + return divide(other, self) + + @openeo_process(process_id="multiply", mode="operator") + def __neg__(self) -> ProcessBuilder: + return self.multiply(-1) + + @openeo_process(process_id="power", mode="operator") + def __pow__(self, other) -> ProcessBuilder: + return self.power(other) + + @openeo_process(process_id="array_element", mode="operator") + def __getitem__(self, key) -> ProcessBuilder: + if isinstance(key, builtins.int): + if key > self._ITERATION_LIMIT: + raise RuntimeError( + "Exceeded ProcessBuilder iteration limit. " + "Are you mistakenly using a Python builtin like `sum()` or `all()` in a callback " + "instead of the appropriate helpers from the `openeo.processes` module?" + ) + return self.array_element(index=key) + else: + return self.array_element(label=key) + + @openeo_process(process_id="eq", mode="operator") + def __eq__(self, other) -> ProcessBuilder: + return eq(self, other) + + @openeo_process(process_id="neq", mode="operator") + def __ne__(self, other) -> ProcessBuilder: + return neq(self, other) + + @openeo_process(process_id="lt", mode="operator") + def __lt__(self, other) -> ProcessBuilder: + return lt(self, other) + + @openeo_process(process_id="lte", mode="operator") + def __le__(self, other) -> ProcessBuilder: + return lte(self, other) + + @openeo_process(process_id="ge", mode="operator") + def __ge__(self, other) -> ProcessBuilder: + return gte(self, other) + + @openeo_process(process_id="gt", mode="operator") + def __gt__(self, other) -> ProcessBuilder: + return gt(self, other) + + @openeo_process + def absolute(self) -> ProcessBuilder: + """ + Absolute value + + :param self: A number. + + :return: The computed absolute value. + """ + return absolute(x=self) + + @openeo_process + def add(self, y) -> ProcessBuilder: + """ + Addition of two numbers + + :param self: The first summand. + :param y: The second summand. + + :return: The computed sum of the two numbers. + """ + return add(x=self, y=y) + + @openeo_process + def add_dimension(self, name, label, type=UNSET) -> ProcessBuilder: + """ + Add a new dimension + + :param self: A data cube to add the dimension to. + :param name: Name for the dimension. + :param label: A dimension label. + :param type: The type of dimension, defaults to `other`. + + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. + All other dimensions remain unchanged. + """ + return add_dimension(data=self, name=name, label=label, type=type) + + @openeo_process + def aggregate_spatial(self, geometries, reducer, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for geometries + + :param self: A raster data cube with at least two spatial dimensions. The data cube implicitly gets + restricted to the bounds of the geometries as if ``filter_spatial()`` would have been used with the + same values for the corresponding parameters immediately before this process. + :param geometries: Geometries for which the aggregation will be computed. Feature properties are + preserved for vector data cubes and all GeoJSON Features. One value will be computed per label in the + dimension of type `geometries`, GeoJSON `Feature` or `Geometry`. For a `FeatureCollection` multiple + values will be computed, one value per contained `Feature`. No values will be computed for empty + geometries. For example, a single value will be computed for a `MultiPolygon`, but two values will be + computed for a `FeatureCollection` containing two polygons. - For **polygons**, the process considers + all pixels for which the point at the pixel center intersects with the corresponding polygon (as + defined in the Simple Features standard by the OGC). - For **points**, the process considers the + closest pixel center. - For **lines** (line strings), the process considers all the pixels whose + centers are closest to at least one point on the line. Thus, pixels may be part of multiple geometries + and be part of multiple aggregations. No operation is applied to geometries that are outside of the + bounds of the data. + :param reducer: A reducer to be applied on all values of each geometry. A reducer is a single process + such as ``mean()`` or a set of processes, which computes a single value for a list of values, see the + category 'reducer' for such processes. + :param target_dimension: By default (which is `null`), the process only computes the results and + doesn't add a new dimension. If this parameter contains a new dimension name, the computation also + stores information about the total count of pixels (valid + invalid pixels) and the number of valid + pixels (see ``is_valid()``) for each computed value. These values are added as a new dimension. The new + dimension of type `other` has the dimension labels `value`, `total_count` and `valid_count`. Fails + with a `TargetDimensionExists` exception if a dimension with the specified name exists. + :param context: Additional data to be passed to the reducer. + + :return: A vector data cube with the computed results. Empty geometries still exist but without any + aggregated values (i.e. no-data). The spatial dimensions are replaced by a dimension of type + 'geometries' and if `target_dimension` is not `null`, a new dimension is added. + """ + return aggregate_spatial( + data=self, + geometries=geometries, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + target_dimension=target_dimension, + context=context + ) + + @openeo_process + def aggregate_spatial_window(self, reducer, size, boundary=UNSET, align=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for rectangular windows + + :param self: A raster data cube with exactly two horizontal spatial dimensions and an arbitrary number + of additional dimensions. The process is applied to all additional dimensions individually. + :param reducer: A reducer to be applied on the list of values, which contain all pixels covered by the + window. A reducer is a single process such as ``mean()`` or a set of processes, which computes a single + value for a list of values, see the category 'reducer' for such processes. + :param size: Window size in pixels along the horizontal spatial dimensions. The first value + corresponds to the `x` axis, the second value corresponds to the `y` axis. + :param boundary: Behavior to apply if the number of values for the axes `x` and `y` is not a multiple + of the corresponding value in the `size` parameter. Options are: - `pad` (default): pad the data cube + with the no-data value `null` to fit the required window size. - `trim`: trim the data cube to fit the + required window size. Set the parameter `align` to specifies to which corner the data is aligned to. + :param align: If the data requires padding or trimming (see parameter `boundary`), specifies to which + corner of the spatial extent the data is aligned to. For example, if the data is aligned to the upper + left, the process pads/trims at the lower-right. + :param context: Additional data to be passed to the reducer. + + :return: A raster data cube with the newly computed values and the same dimensions. The resolution + will change depending on the chosen values for the `size` and `boundary` parameter. It usually + decreases for the dimensions which have the corresponding parameter `size` set to values greater than + 1. The dimension labels will be set to the coordinate at the center of the window. The other dimension + properties (name, type and reference system) remain unchanged. + """ + return aggregate_spatial_window( + data=self, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + size=size, + boundary=boundary, + align=align, + context=context + ) + + @openeo_process + def aggregate_temporal(self, intervals, reducer, labels=UNSET, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations + + :param self: A data cube. + :param intervals: Left-closed temporal intervals, which are allowed to overlap. Each temporal interval + in the array has exactly two elements: 1. The first element is the start of the temporal interval. The + specified time instant is **included** in the interval. 2. The second element is the end of the + temporal interval. The specified time instant is **excluded** from the interval. The second element + must always be greater/later than the first element, except when using time without date. Otherwise, a + `TemporalExtentEmpty` exception is thrown. + :param reducer: A reducer to be applied for the values contained in each interval. A reducer is a + single process such as ``mean()`` or a set of processes, which computes a single value for a list of + values, see the category 'reducer' for such processes. Intervals may not contain any values, which for + most reducers leads to no-data (`null`) values by default. + :param labels: Distinct labels for the intervals, which can contain dates and/or times. Is only + required to be specified if the values for the start of the temporal intervals are not distinct and + thus the default labels would not be unique. The number of labels and the number of groups need to be + equal. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the given temporal dimension. + """ + return aggregate_temporal( + data=self, + intervals=intervals, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + labels=labels, + dimension=dimension, + context=context + ) + + @openeo_process + def aggregate_temporal_period(self, period, reducer, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations based on calendar hierarchies + + :param self: The source data cube. + :param period: The time intervals to aggregate. The following pre-defined values are available: * + `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten day periods, + counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third + dekad of the month can range from 8 to 11 days. For example, the third dekad of a year spans from + January 21 till January 31 (11 days), the fourth dekad spans from February 1 till February 10 (10 days) + and the sixth dekad spans from February 21 till February 28 or February 29 in a leap year (8 or 9 days + respectively). * `month`: Month of the year * `season`: Three month periods of the calendar seasons + (December - February, March - May, June - August, September - November). * `tropical-season`: Six month + periods of the tropical seasons (November - April, May - October). * `year`: Proleptic years * + `decade`: Ten year periods ([0-to-9 decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from + a year ending in a 0 to the next year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. + :param reducer: A reducer to be applied for the values contained in each period. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, + see the category 'reducer' for such processes. Periods may not contain any values, which for most + reducers leads to no-data (`null`) values by default. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the source data + cube is expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it + has more dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not + exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the given temporal dimension. The specified temporal dimension has the following dimension labels + (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: `YYYY-MM- + DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * `dekad`: + `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - February), + `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - November). * `tropical- + season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * `decade`: + `YYY0` * `decade-ad`: `YYY1` The dimension labels in the new data cube are complete for the whole + extent of the source data cube. For example, if `period` is set to `day` and the source data cube has + two dimension labels at the beginning of the year (`2020-01-01`) and the end of a year (`2020-12-31`), + the process returns a data cube with 365 dimension labels (`2020-001`, `2020-002`, ..., `2020-365`). In + contrast, if `period` is set to `day` and the source data cube has just one dimension label + `2020-01-05`, the process returns a data cube with just a single dimension label (`2020-005`). + """ + return aggregate_temporal_period( + data=self, + period=period, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def all(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Are all of the values true? + + :param self: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return all(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def and_(self, y) -> ProcessBuilder: + """ + Logical AND + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical AND. + """ + return and_(x=self, y=y) + + @openeo_process + def anomaly(self, normals, period) -> ProcessBuilder: + """ + Compute anomalies + + :param self: A data cube with exactly one temporal dimension and the following dimension labels for the + given period (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * + `hour`: `YYYY-MM-DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - + `YYYY-52` * `dekad`: `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` + (December - February), `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - + November). * `tropical-season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * + `year`: `YYYY` * `decade`: `YYY0` * `decade-ad`: `YYY1` * `single-period` / `climatology-period`: Any + ``aggregate_temporal_period()`` can compute such a data cube. + :param normals: A data cube with normals, e.g. daily, monthly or yearly values computed from a process + such as ``climatological_normal()``. Must contain exactly one temporal dimension with the following + dimension labels for the given period: * `hour`: `00` - `23` * `day`: `001` - `365` * `week`: `01` - + `52` * `dekad`: `00` - `36` * `month`: `01` - `12` * `season`: `djf` (December - February), `mam` + (March - May), `jja` (June - August), `son` (September - November) * `tropical-season`: `ndjfma` + (November - April), `mjjaso` (May - October) * `year`: Four-digit year numbers * `decade`: Four-digit + year numbers, the last digit being a `0` * `decade-ad`: Four-digit year numbers, the last digit being a + `1` * `single-period` / `climatology-period`: A single dimension label with any name is expected. + :param period: Specifies the time intervals available in the normals data cube. The following options + are available: * `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * + `dekad`: Ten day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - + end of month). The third dekad of the month can range from 8 to 11 days. For example, the fourth dekad + is Feb, 1 - Feb, 10 each year. * `month`: Month of the year * `season`: Three month periods of the + calendar seasons (December - February, March - May, June - August, September - November). * `tropical- + season`: Six month periods of the tropical seasons (November - April, May - October). * `year`: + Proleptic years * `decade`: Ten year periods ([0-to-9 + decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the next + year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. * `single-period` / + `climatology-period`: A single period of arbitrary length + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged. + """ + return anomaly(data=self, normals=normals, period=period) + + @openeo_process + def any(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Is at least one value true? + + :param self: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return any(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def apply(self, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each value + + :param self: A data cube. + :param process: A process that accepts and returns a single value and is applied on each individual + value in the data cube. The process may consist of multiple sub-processes and could, for example, + consist of processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply(data=self, process=build_child_callback(process, parent_parameters=['x', 'context']), context=context) + + @openeo_process + def apply_dimension(self, process, dimension, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to all values along a dimension + + :param self: A data cube. + :param process: Process to be applied on all values along the given dimension. The specified process + needs to accept an array and must return an array with at least one element. A process may consist of + multiple sub-processes. + :param dimension: The name of the source dimension to apply the process on. Fails with a + `DimensionNotAvailable` exception if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or `null` (the default) to use the source + dimension specified in the parameter `dimension`. By specifying a target dimension, the source + dimension is removed. The target dimension with the specified name and the type `other` (see + ``add_dimension()``) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. All dimensions stay the same, except for the + dimensions specified in corresponding parameters. There are three cases how the dimensions can change: + 1. The source dimension is the target dimension: - The (number of) dimensions remain unchanged as + the source dimension is the target dimension. - The source dimension properties name and type remain + unchanged. - The dimension labels, the reference system and the resolution are preserved only if the + number of values in the source dimension is equal to the number of values computed by the process. + Otherwise, all other dimension properties change as defined in the list below. 2. The source dimension + is not the target dimension. The target dimension exists with a single label only: - The number of + dimensions decreases by one as the source dimension is 'dropped' and the target dimension is filled + with the processed data that originates from the source dimension. - The target dimension properties + name and type remain unchanged. All other dimension properties change as defined in the list below. 3. + The source dimension is not the target dimension and the latter does not exist: - The number of + dimensions remain unchanged, but the source dimension is replaced with the target dimension. - The + target dimension has the specified name and the type other. All other dimension properties are set as + defined in the list below. Unless otherwise stated above, for the given (target) dimension the + following applies: - the number of dimension labels is equal to the number of values computed by the + process, - the dimension labels are incrementing integers starting from zero, - the resolution changes, + and - the reference system is undefined. + """ + return apply_dimension( + data=self, + process=build_child_callback(process, parent_parameters=['data', 'context']), + dimension=dimension, + target_dimension=target_dimension, + context=context + ) + + @openeo_process + def apply_kernel(self, kernel, factor=UNSET, border=UNSET, replace_invalid=UNSET) -> ProcessBuilder: + """ + Apply a spatial convolution with a kernel + + :param self: A raster data cube. + :param kernel: Kernel as a two-dimensional array of weights. The inner level of the nested array aligns + with the `x` axis and the outer level aligns with the `y` axis. Each level of the kernel must have an + uneven number of elements, otherwise the process throws a `KernelDimensionsUneven` exception. + :param factor: A factor that is multiplied to each value after the kernel has been applied. This is + basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often + required for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults + to fill the border with zeroes. The following options are available: * *numeric value* - fill with a + user-defined constant number `n`: `nnnnnn|abcdefgh|nnnnnn` (default, with `n` = 0) * `replicate` - + repeat the value from the pixel at the border: `aaaaaa|abcdefgh|hhhhhh` * `reflect` - mirror/reflect + from the border: `fedcba|abcdefgh|hgfedc` * `reflect_pixel` - mirror/reflect from the center of the + pixel at the border: `gfedcb|abcdefgh|gfedcb` * `wrap` - repeat/wrap the image: + `cdefgh|abcdefgh|abcdef` + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite + numerical values with. By default, those values are replaced with zeroes. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_kernel(data=self, kernel=kernel, factor=factor, border=border, replace_invalid=replace_invalid) + + @openeo_process + def apply_neighborhood(self, process, size, overlap=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to pixels in a n-dimensional neighborhood + + :param self: A raster data cube. + :param process: Process to be applied on all neighborhoods. + :param size: Neighborhood sizes along each dimension. This object maps dimension names to either a + physical measure (e.g. 100 m, 10 days) or pixels (e.g. 32 pixels). For dimensions not specified, the + default is to provide all values. Be aware that including all values from overly large dimensions may + not be processed at once. + :param overlap: Overlap of neighborhoods along each dimension to avoid border effects. By default no + overlap is provided. For instance a temporal dimension can add 1 month before and after a + neighborhood. In the spatial dimensions, this is often a number of pixels. The overlap specified is + added before and after, so an overlap of 8 pixels will add 8 pixels on both sides of the window, so 16 + in total. Be aware that large overlaps increase the need for computational resources and modifying + overlapping data in subsequent operations have no effect. + :param context: Additional data to be passed to the process. + + :return: A raster data cube with the newly computed values and the same dimensions. The dimension + properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_neighborhood( + data=self, + process=build_child_callback(process, parent_parameters=['data', 'context']), + size=size, + overlap=overlap, + context=context + ) + + @openeo_process + def apply_polygon(self, polygons, process, mask_value=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to segments of the data cube + + :param self: A data cube. + :param polygons: A vector data cube containing at least one polygon. The provided vector data can be + one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or + `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or + `MultiPolygon` geometries. * Empty geometries are ignored. + :param process: A process that accepts and returns a single data cube and is applied on each individual + sub data cube. The process may consist of multiple sub-processes. + :param mask_value: All pixels for which the point at the pixel center **does not** intersect with the + polygon are replaced with the given value, which defaults to `null` (no data). It can provide a + distinction between no data values within the polygon and masked pixels outside of it. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return apply_polygon( + data=self, + polygons=polygons, + process=build_child_callback(process, parent_parameters=['data', 'context']), + mask_value=mask_value, + context=context + ) + + @openeo_process + def arccos(self) -> ProcessBuilder: + """ + Inverse cosine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arccos(x=self) + + @openeo_process + def arcosh(self) -> ProcessBuilder: + """ + Inverse hyperbolic cosine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arcosh(x=self) + + @openeo_process + def arcsin(self) -> ProcessBuilder: + """ + Inverse sine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arcsin(x=self) + + @openeo_process + def arctan(self) -> ProcessBuilder: + """ + Inverse tangent + + :param self: A number. + + :return: The computed angle in radians. + """ + return arctan(x=self) + + @openeo_process + def arctan2(self, x) -> ProcessBuilder: + """ + Inverse tangent of two numbers + + :param self: A number to be used as the dividend. + :param x: A number to be used as the divisor. + + :return: The computed angle in radians. + """ + return arctan2(y=self, x=x) + + @openeo_process + def ard_normalized_radar_backscatter(self, elevation_model=UNSET, contributing_area=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant SAR NRB generation + + :param self: The source data cube containing SAR input. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options + will reduce portability. + + :return: Backscatter values expressed as gamma0 in linear scale. In addition to the bands + `contributing_area` and `ellipsoid_incidence_angle` that can optionally be added with corresponding + parameters, the following bands are always added to the data cube: - `mask`: A data mask that + indicates which values are valid (1), invalid (0) or contain no-data (null). - `local_incidence_angle`: + A band with DEM-based local incidence angles in degrees. The data returned is CARD4L compliant with + corresponding metadata. + """ + return ard_normalized_radar_backscatter( + data=self, + elevation_model=elevation_model, + contributing_area=contributing_area, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + @openeo_process + def ard_surface_reflectance(self, atmospheric_correction_method, cloud_detection_method, elevation_model=UNSET, atmospheric_correction_options=UNSET, cloud_detection_options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant Surface Reflectance generation + + :param self: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances. There must be a single dimension of type `bands` available. + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. Each method supports detecting + different atmospheric disturbances such as clouds, cloud shadows, aerosols, haze, ozone and/or water + vapour in optical imagery. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + Specifying proprietary options will reduce portability. + :param cloud_detection_options: Proprietary options for the cloud detection method. Specifying + proprietary options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances for each spectral band in the source + data cube, with atmospheric disturbances like clouds and cloud shadows removed. No-data values (null) + are directly set in the bands. Depending on the methods used, several additional bands will be added to + the data cube: Data cube containing bottom of atmosphere reflectances for each spectral band in the + source data cube, with atmospheric disturbances like clouds and cloud shadows removed. Depending on the + methods used, several additional bands will be added to the data cube: - `date` (optional): Specifies + per-pixel acquisition timestamps. - `incomplete-testing` (required): Identifies pixels with a value of + 1 for which the per-pixel tests (at least saturation, cloud and cloud shadows, see CARD4L specification + for details) have not all been successfully completed. Otherwise, the value is 0. - `saturation` + (required) / `saturation_{band}` (optional): Indicates where pixels in the input spectral bands are + saturated (1) or not (0). If the saturation is given per band, the band names are `saturation_{band}` + with `{band}` being the band name from the source data cube. - `cloud`, `shadow` (both + required),`aerosol`, `haze`, `ozone`, `water_vapor` (all optional): Indicates the probability of pixels + being an atmospheric disturbance such as clouds. All bands have values between 0 (clear) and 1, which + describes the probability that it is an atmospheric disturbance. - `snow-ice` (optional): Points to a + file that indicates whether a pixel is assessed as being snow/ice (1) or not (0). All values describe + the probability and must be between 0 and 1. - `land-water` (optional): Indicates whether a pixel is + assessed as being land (1) or water (0). All values describe the probability and must be between 0 and + 1. - `incidence-angle` (optional): Specifies per-pixel incidence angles in degrees. - `azimuth` + (optional): Specifies per-pixel azimuth angles in degrees. - `sun-azimuth:` (optional): Specifies per- + pixel sun azimuth angles in degrees. - `sun-elevation` (optional): Specifies per-pixel sun elevation + angles in degrees. - `terrain-shadow` (optional): Indicates with a value of 1 whether a pixel is not + directly illuminated due to terrain shadowing. Otherwise, the value is 0. - `terrain-occlusion` + (optional): Indicates with a value of 1 whether a pixel is not visible to the sensor due to terrain + occlusion during off-nadir viewing. Otherwise, the value is 0. - `terrain-illumination` (optional): + Contains coefficients used for terrain illumination correction are provided for each pixel. The data + returned is CARD4L compliant with corresponding metadata. + """ + return ard_surface_reflectance( + data=self, + atmospheric_correction_method=atmospheric_correction_method, + cloud_detection_method=cloud_detection_method, + elevation_model=elevation_model, + atmospheric_correction_options=atmospheric_correction_options, + cloud_detection_options=cloud_detection_options + ) + + @openeo_process + def array_append(self, value, label=UNSET) -> ProcessBuilder: + """ + Append a value to an array + + :param self: An array. + :param value: Value to append to the array. + :param label: If the given array is a labeled array, a new label for the new value should be given. If + not given or `null`, the array index as string is used as the label. If in any case the label exists, a + `LabelExists` exception is thrown. + + :return: The new array with the value being appended. + """ + return array_append(data=self, value=value, label=label) + + @openeo_process + def array_apply(self, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each array element + + :param self: An array. + :param process: A process that accepts and returns a single value and is applied on each individual + value in the array. The process may consist of multiple sub-processes and could, for example, consist + of processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: An array with the newly computed values. The number of elements are the same as for the + original array. + """ + return array_apply( + data=self, + process=build_child_callback(process, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + @openeo_process + def array_concat(self, array2) -> ProcessBuilder: + """ + Merge two arrays + + :param self: The first array. + :param array2: The second array. + + :return: The merged array. + """ + return array_concat(array1=self, array2=array2) + + @openeo_process + def array_contains(self, value) -> ProcessBuilder: + """ + Check whether the array contains a given value + + :param self: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `false`. + + :return: `true` if the list contains the value, false` otherwise. + """ + return array_contains(data=self, value=value) + + @openeo_process + def array_create(self=UNSET, repeat=UNSET) -> ProcessBuilder: + """ + Create an array + + :param self: A (native) array to fill the newly created array with. Defaults to an empty array. + :param repeat: The number of times the (native) array specified in `data` is repeatedly added after + each other to the new array being created. Defaults to `1`. + + :return: The newly created array. + """ + return array_create(data=self, repeat=repeat) + + @openeo_process + def array_create_labeled(self, labels) -> ProcessBuilder: + """ + Create a labeled array + + :param self: An array of values to be used. + :param labels: An array of labels to be used. + + :return: The newly created labeled array. + """ + return array_create_labeled(data=self, labels=labels) + + @openeo_process + def array_element(self, index=UNSET, label=UNSET, return_nodata=UNSET) -> ProcessBuilder: + """ + Get an element from an array + + :param self: An array. + :param index: The zero-based index of the element to retrieve. + :param label: The label of the element to retrieve. Throws an `ArrayNotLabeled` exception, if the given + array is not a labeled array and this parameter is set. + :param return_nodata: By default this process throws an `ArrayElementNotAvailable` exception if the + index or label is invalid. If you want to return `null` instead, set this flag to `true`. + + :return: The value of the requested element. + """ + return array_element(data=self, index=index, label=label, return_nodata=return_nodata) + + @openeo_process + def array_filter(self, condition, context=UNSET) -> ProcessBuilder: + """ + Filter an array based on a condition + + :param self: An array. + :param condition: A condition that is evaluated against each value, index and/or label in the array. + Only the array elements for which the condition returns `true` are preserved. + :param context: Additional data to be passed to the condition. + + :return: An array filtered by the specified condition. The number of elements are less than or equal + compared to the original array. + """ + return array_filter( + data=self, + condition=build_child_callback(condition, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + @openeo_process + def array_find(self, value, reverse=UNSET) -> ProcessBuilder: + """ + Get the index for a value in an array + + :param self: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `null`. + :param reverse: By default, this process finds the index of the first match. To return the index of the + last match instead, set this flag to `true`. + + :return: The index of the first element with the specified value. If no element was found, `null` is + returned. + """ + return array_find(data=self, value=value, reverse=reverse) + + @openeo_process + def array_find_label(self, label) -> ProcessBuilder: + """ + Get the index for a label in a labeled array + + :param self: List to find the label in. + :param label: Label to find in `data`. + + :return: The index of the element with the specified label assigned. If no such label was found, `null` + is returned. + """ + return array_find_label(data=self, label=label) + + @openeo_process + def array_interpolate_linear(self) -> ProcessBuilder: + """ + One-dimensional linear interpolation for arrays + + :param self: An array of numbers and no-data values. If the given array is a labeled array, the labels + must have a natural/inherent label order and the process expects the labels to be sorted accordingly. + This is the default behavior in openEO for spatial and temporal dimensions. + + :return: An array with no-data values being replaced with interpolated values. If not at least 2 + numerical values are available in the array, the array stays the same. + """ + return array_interpolate_linear(data=self) + + @openeo_process + def array_labels(self) -> ProcessBuilder: + """ + Get the labels for an array + + :param self: An array. + + :return: The labels or indices as array. + """ + return array_labels(data=self) + + @openeo_process + def array_modify(self, values, index, length=UNSET) -> ProcessBuilder: + """ + Change the content of an array (remove, insert, update) + + :param self: The array to modify. + :param values: The values to insert into the `data` array. + :param index: The index in the `data` array of the element to insert the value(s) before. If the index + is greater than the number of elements in the `data` array, the process throws an + `ArrayElementNotAvailable` exception. To insert after the last element, there are two options: 1. Use + the simpler processes ``array_append()`` to append a single value or ``array_concat()`` to append + multiple values. 2. Specify the number of elements in the array. You can retrieve the number of + elements with the process ``count()``, having the parameter `condition` set to `true`. + :param length: The number of elements in the `data` array to remove (or replace) starting from the + given index. If the array contains fewer elements, the process simply removes all elements up to the + end. + + :return: An array with values added, updated or removed. + """ + return array_modify(data=self, values=values, index=index, length=length) + + @openeo_process + def arsinh(self) -> ProcessBuilder: + """ + Inverse hyperbolic sine + + :param self: A number. + + :return: The computed angle in radians. + """ + return arsinh(x=self) + + @openeo_process + def artanh(self) -> ProcessBuilder: + """ + Inverse hyperbolic tangent + + :param self: A number. + + :return: The computed angle in radians. + """ + return artanh(x=self) + + @openeo_process + def atmospheric_correction(self, method, elevation_model=UNSET, options=UNSET) -> ProcessBuilder: + """ + Apply atmospheric correction + + :param self: Data cube containing multi-spectral optical top of atmosphere reflectances to be + corrected. + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but + reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param options: Proprietary options for the atmospheric correction method. Specifying proprietary + options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances. + """ + return atmospheric_correction(data=self, method=method, elevation_model=elevation_model, options=options) + + @openeo_process + def between(self, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison + + :param self: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return between(x=self, min=min, max=max, exclude_max=exclude_max) + + @openeo_process + def ceil(self) -> ProcessBuilder: + """ + Round fractions up + + :param self: A number to round up. + + :return: The number rounded up. + """ + return ceil(x=self) + + @openeo_process + def climatological_normal(self, period, climatology_period=UNSET) -> ProcessBuilder: + """ + Compute climatology normals + + :param self: A data cube with exactly one temporal dimension. The data cube must span at least the + temporal interval specified in the parameter `climatology-period`. Seasonal periods may span two + consecutive years, e.g. temporal winter that includes months December, January and February. If the + required months before the actual climate period are available, the season is taken into account. If + not available, the first season is not taken into account and the seasonal mean is based on one year + less than the other seasonal normals. The incomplete season at the end of the last year is never taken + into account. + :param period: The time intervals to aggregate the average value for. The following pre-defined + frequencies are supported: * `day`: Day of the year * `month`: Month of the year * `climatology- + period`: The period specified in the `climatology-period`. * `season`: Three month periods of the + calendar seasons (December - February, March - May, June - August, September - November). * `tropical- + season`: Six month periods of the tropical seasons (November - April, May - October). + :param climatology_period: The climatology period as a closed temporal interval. The first element of + the array is the first year to be fully included in the temporal interval. The second element is the + last year to be fully included in the temporal interval. The default climatology period is from 1981 + until 2010 (both inclusive) right now, but this might be updated over time to what is commonly used in + climatology. If you don't want to keep your research to be reproducible, please explicitly specify a + period. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the temporal + dimension. The temporal dimension has the following dimension labels: * `day`: `001` - `365` * + `month`: `01` - `12` * `climatology-period`: `climatology-period` * `season`: `djf` (December - + February), `mam` (March - May), `jja` (June - August), `son` (September - November) * `tropical- + season`: `ndjfma` (November - April), `mjjaso` (May - October) + """ + return climatological_normal(data=self, period=period, climatology_period=climatology_period) + + @openeo_process + def clip(self, min, max) -> ProcessBuilder: + """ + Clip a value between a minimum and a maximum + + :param self: A number. + :param min: Minimum value. If the value is lower than this value, the process will return the value of + this parameter. + :param max: Maximum value. If the value is greater than this value, the process will return the value + of this parameter. + + :return: The value clipped to the specified range. + """ + return clip(x=self, min=min, max=max) + + @openeo_process + def cloud_detection(self, method, options=UNSET) -> ProcessBuilder: + """ + Create cloud masks + + :param self: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances on which to perform cloud detection. + :param method: The cloud detection method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but + reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param options: Proprietary options for the cloud detection method. Specifying proprietary options will + reduce portability. + + :return: A data cube with bands for the atmospheric disturbances. Each of the masks contains values + between 0 and 1. The data cube has the same spatial and temporal dimensions as the source data cube and + a dimension that contains a dimension label for each of the supported/considered atmospheric + disturbance. + """ + return cloud_detection(data=self, method=method, options=options) + + @openeo_process + def constant(self) -> ProcessBuilder: + """ + Define a constant value + + :param self: The value of the constant. + + :return: The value of the constant. + """ + return constant(x=self) + + @openeo_process + def cos(self) -> ProcessBuilder: + """ + Cosine + + :param self: An angle in radians. + + :return: The computed cosine of `x`. + """ + return cos(x=self) + + @openeo_process + def cosh(self) -> ProcessBuilder: + """ + Hyperbolic cosine + + :param self: An angle in radians. + + :return: The computed hyperbolic cosine of `x`. + """ + return cosh(x=self) + + @openeo_process + def count(self, condition=UNSET, context=UNSET) -> ProcessBuilder: + """ + Count the number of elements + + :param self: An array with elements of any data type. + :param condition: A condition consists of one or more processes, which in the end return a boolean + value. It is evaluated against each element in the array. An element is counted only if the condition + returns `true`. Defaults to count valid elements in a list (see ``is_valid()``). Setting this parameter + to boolean `true` counts all elements in the list. `false` is not a valid value for this parameter. + :param context: Additional data to be passed to the condition. + + :return: The counted number of elements. + """ + return count(data=self, condition=condition, context=context) + + @openeo_process + def create_data_cube(self) -> ProcessBuilder: + """ + Create an empty data cube + + :return: An empty data cube with no dimensions. + """ + return create_data_cube() + + @openeo_process + def cummax(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative maxima + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative maxima. + """ + return cummax(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cummin(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative minima + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative minima. + """ + return cummin(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cumproduct(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative products + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative products. + """ + return cumproduct(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def cumsum(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative sums + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following + elements. + + :return: An array with the computed cumulative sums. + """ + return cumsum(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def date_between(self, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison for dates and times + + :param self: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return date_between(x=self, min=min, max=max, exclude_max=exclude_max) + + @openeo_process + def date_difference(self, date2, unit=UNSET) -> ProcessBuilder: + """ + Computes the difference between two time instants + + :param self: The base date, optionally with a time component. + :param date2: The other date, optionally with a time component. + :param unit: The unit for the returned value. The following units are available: - millisecond - + second - leap seconds are ignored in computations. - minute - hour - day - month - year + + :return: Returns the difference between date1 and date2 in the given unit (seconds by default), + including a fractional part if required. For comparison purposes this means: - If `date1` < `date2`, + the returned value is positive. - If `date1` = `date2`, the returned value is 0. - If `date1` > + `date2`, the returned value is negative. + """ + return date_difference(date1=self, date2=date2, unit=unit) + + @openeo_process + def date_shift(self, value, unit) -> ProcessBuilder: + """ + Manipulates dates and times by addition or subtraction + + :param self: The date (and optionally time) to manipulate. If the given date doesn't include the time, + the process assumes that the time component is `00:00:00Z` (i.e. midnight, in UTC). The millisecond + part of the time is optional and defaults to `0` if not given. + :param value: The period of time in the unit given that is added (positive numbers) or subtracted + (negative numbers). The value `0` doesn't have any effect. + :param unit: The unit for the value given. The following pre-defined units are available: - + millisecond: Milliseconds - second: Seconds - leap seconds are ignored in computations. - minute: + Minutes - hour: Hours - day: Days - changes only the the day part of a date - week: Weeks (equivalent + to 7 days) - month: Months - year: Years Manipulations with the unit `year`, `month`, `week` or `day` + do never change the time. If any of the manipulations result in an invalid date or time, the + corresponding part is rounded down to the next valid date or time respectively. For example, adding a + month to `2020-01-31` would result in `2020-02-29`. + + :return: The manipulated date. If a time component was given in the parameter `date`, the time + component is returned with the date. + """ + return date_shift(date=self, value=value, unit=unit) + + @openeo_process + def dimension_labels(self, dimension) -> ProcessBuilder: + """ + Get the dimension labels + + :param self: The data cube. + :param dimension: The name of the dimension to get the labels for. + + :return: The labels as an array. + """ + return dimension_labels(data=self, dimension=dimension) + + @openeo_process + def divide(self, y) -> ProcessBuilder: + """ + Division of two numbers + + :param self: The dividend. + :param y: The divisor. + + :return: The computed result. + """ + return divide(x=self, y=y) + + @openeo_process + def drop_dimension(self, name) -> ProcessBuilder: + """ + Remove a dimension + + :param self: The data cube to drop a dimension from. + :param name: Name of the dimension to drop. + + :return: A data cube without the specified dimension. The number of dimensions decreases by one, but + the dimension properties (name, type, labels, reference system and resolution) for all other dimensions + remain unchanged. + """ + return drop_dimension(data=self, name=name) + + @openeo_process + def e(self) -> ProcessBuilder: + """ + Euler's number (e) + + :return: The numerical value of Euler's number. + """ + return e() + + @openeo_process + def eq(self, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Equal to comparison + + :param self: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a + positive non-zero number the equality of two numbers is checked against a delta value. This is + especially useful to circumvent problems with floating-point inaccuracy in machine-based computation. + This option is basically an alias for the following computation: `lte(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be + disabled by setting this parameter to `false`. + + :return: `true` if `x` is equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return eq(x=self, y=y, delta=delta, case_sensitive=case_sensitive) + + @openeo_process + def exp(self) -> ProcessBuilder: + """ + Exponentiation to the base e + + :param self: The numerical exponent. + + :return: The computed value for *e* raised to the power of `p`. + """ + return exp(p=self) + + @openeo_process + def extrema(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum and maximum values + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that an array with two `null` values is + returned if any value is such a value. + + :return: An array containing the minimum and maximum values for the specified numbers. The first + element is the minimum, the second element is the maximum. If the input array is empty both elements + are set to `null`. + """ + return extrema(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def filter_bands(self, bands=UNSET, wavelengths=UNSET) -> ProcessBuilder: + """ + Filter the bands by names + + :param self: A data cube with bands. + :param bands: A list of band names. Either the unique band name (metadata field `name` in bands) or one + of the common band names (metadata field `common_name` in bands). If the unique band name and the + common name conflict, the unique band name has a higher priority. The order of the specified array + defines the order of the bands in the data cube. If multiple bands match a common name, all matched + bands are included in the original order. + :param wavelengths: A list of sub-lists with each sub-list consisting of two elements. The first + element is the minimum wavelength and the second element is the maximum wavelength. Wavelengths are + specified in micrometers (μm). The order of the specified array defines the order of the bands in the + data cube. If multiple bands match the wavelengths, all matched bands are included in the original + order. + + :return: A data cube limited to a subset of its original bands. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the dimension of + type `bands` has less (or the same) dimension labels. + """ + return filter_bands(data=self, bands=bands, wavelengths=wavelengths) + + @openeo_process + def filter_bbox(self, extent) -> ProcessBuilder: + """ + Spatial filter using a bounding box + + :param self: A data cube. + :param extent: A bounding box, which may include a vertical axis (see `base` and `height`). + + :return: A data cube restricted to the bounding box. The dimensions and dimension properties (name, + type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions + have less (or the same) dimension labels. + """ + return filter_bbox(data=self, extent=extent) + + @openeo_process + def filter_labels(self, condition, dimension, context=UNSET) -> ProcessBuilder: + """ + Filter dimension labels based on a condition + + :param self: A data cube. + :param condition: A condition that is evaluated against each dimension label in the specified + dimension. A dimension label and the corresponding data is preserved for the given dimension, if the + condition returns `true`. + :param dimension: The name of the dimension to filter on. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the condition. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that the given dimension has less (or the same) + dimension labels. + """ + return filter_labels( + data=self, + condition=build_child_callback(condition, parent_parameters=['value', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def filter_spatial(self, geometries) -> ProcessBuilder: + """ + Spatial filter raster data cubes using geometries + + :param self: A raster data cube. + :param geometries: One or more geometries used for filtering, given as GeoJSON or vector data cube. If + multiple geometries are provided, the union of them is used. Empty geometries are ignored. Limits the + data cube to the bounding box of the given geometries. No implicit masking gets applied. To mask the + pixels of the data cube use ``mask_polygon()``. + + :return: A raster data cube restricted to the specified geometries. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + spatial dimensions have less (or the same) dimension labels. + """ + return filter_spatial(data=self, geometries=geometries) + + @openeo_process + def filter_temporal(self, extent, dimension=UNSET) -> ProcessBuilder: + """ + Temporal filter based on temporal intervals + + :param self: A data cube. + :param extent: Left-closed temporal interval, i.e. an array with exactly two elements: 1. The first + element is the start of the temporal interval. The specified time instant is **included** in the + interval. 2. The second element is the end of the temporal interval. The specified time instant is + **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by + setting one of the boundaries to `null`, but never both. + :param dimension: The name of the temporal dimension to filter on. If no specific dimension is + specified, the filter applies to all temporal dimensions. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + + :return: A data cube restricted to the specified temporal extent. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + temporal dimensions (determined by `dimensions` parameter) may have less dimension labels. + """ + return filter_temporal(data=self, extent=extent, dimension=dimension) + + @openeo_process + def filter_vector(self, geometries, relation=UNSET) -> ProcessBuilder: + """ + Spatial vector filter using geometries + + :param self: A vector data cube with the candidate geometries. + :param geometries: One or more base geometries used for filtering, given as vector data cube. If + multiple base geometries are provided, the union of them is used. + :param relation: The spatial filter predicate for comparing the geometries provided through (a) + `geometries` (base geometries) and (b) `data` (candidate geometries). + + :return: A vector data cube restricted to the specified geometries. The dimensions and dimension + properties (name, type, labels, reference system and resolution) remain unchanged, except that the + geometries dimension has less (or the same) dimension labels. + """ + return filter_vector(data=self, geometries=geometries, relation=relation) + + @openeo_process + def first(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + First element + + :param self: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if the first value is + such a value. + + :return: The first element of the input array. + """ + return first(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def fit_curve(self, parameters, function, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Curve fitting + + :param self: A labeled array, the labels correspond to the variable `y` and the values correspond to + the variable `x`. + :param parameters: Defined the number of parameters for the model function and provides an initial + guess for them. At least one parameter is required. + :param function: The model function. It must take the parameters to fit as array through the first + argument and the independent variable `x` as the second argument. It is recommended to store the model + function as a user-defined process on the back-end to be able to re-use the model function with the + computed optimal values for the parameters afterwards. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is passed to the model function. + + :return: An array with the optimal values for the parameters. + """ + return fit_curve( + data=self, + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + ignore_nodata=ignore_nodata + ) + + @openeo_process + def flatten_dimensions(self, dimensions, target_dimension, label_separator=UNSET) -> ProcessBuilder: + """ + Combine multiple dimensions into a single dimension + + :param self: A data cube. + :param dimensions: The names of the dimension to combine. The order of the array defines the order in + which the dimension labels and values are combined (see the example in the process description). Fails + with a `DimensionNotAvailable` exception if at least one of the specified dimensions does not exist. + :param target_dimension: The name of the new target dimension. A new dimensions will be created with + the given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` + exception if a dimension with the specified name exists. + :param label_separator: The string that will be used as a separator for the concatenated dimension + labels. To unambiguously revert the dimension labels with the process ``unflatten_dimension()``, the + given string must not be contained in any of the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system + and resolution) for all other dimensions remain unchanged. + """ + return flatten_dimensions(data=self, dimensions=dimensions, target_dimension=target_dimension, label_separator=label_separator) + + @openeo_process + def floor(self) -> ProcessBuilder: + """ + Round fractions down + + :param self: A number to round down. + + :return: The number rounded down. + """ + return floor(x=self) + + @openeo_process + def gt(self, y) -> ProcessBuilder: + """ + Greater than comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly greater than `y` or `null` if any operand is `null`, otherwise + `false`. + """ + return gt(x=self, y=y) + + @openeo_process + def gte(self, y) -> ProcessBuilder: + """ + Greater than or equal to comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is greater than or equal to `y`, `null` if any operand is `null`, otherwise + `false`. + """ + return gte(x=self, y=y) + + @openeo_process + def if_(self, accept, reject=UNSET) -> ProcessBuilder: + """ + If-Then-Else conditional + + :param self: A boolean value. + :param accept: A value that is returned if the boolean value is `true`. + :param reject: A value that is returned if the boolean value is **not** `true`. Defaults to `null`. + + :return: Either the `accept` or `reject` argument depending on the given boolean value. + """ + return if_(value=self, accept=accept, reject=reject) + + @openeo_process + def inspect(self, message=UNSET, code=UNSET, level=UNSET) -> ProcessBuilder: + """ + Add information to the logs + + :param self: Data to log. + :param message: A message to send in addition to the data. + :param code: A label to help identify one or more log entries originating from this process in the list + of all log entries. It can help to group or filter log entries and is usually not unique. + :param level: The severity level of this message, defaults to `info`. + + :return: The data as passed to the `data` parameter without any modification. + """ + return inspect(data=self, message=message, code=code, level=level) + + @openeo_process + def int(self) -> ProcessBuilder: + """ + Integer part of a number + + :param self: A number. + + :return: Integer part of the number. + """ + return int(x=self) + + @openeo_process + def is_infinite(self) -> ProcessBuilder: + """ + Value is an infinite number + + :param self: The data to check. + + :return: `true` if the data is an infinite number, otherwise `false`. + """ + return is_infinite(x=self) + + @openeo_process + def is_nan(self) -> ProcessBuilder: + """ + Value is not a number + + :param self: The data to check. + + :return: Returns `true` for `NaN` and all non-numeric data types, otherwise returns `false`. + """ + return is_nan(x=self) + + @openeo_process + def is_nodata(self) -> ProcessBuilder: + """ + Value is a no-data value + + :param self: The data to check. + + :return: `true` if the data is a no-data value, otherwise `false`. + """ + return is_nodata(x=self) + + @openeo_process + def is_valid(self) -> ProcessBuilder: + """ + Value is valid data + + :param self: The data to check. + + :return: `true` if the data is valid, otherwise `false`. + """ + return is_valid(x=self) + + @openeo_process + def last(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Last element + + :param self: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if the last value is + such a value. + + :return: The last element of the input array. + """ + return last(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def linear_scale_range(self, inputMin, inputMax, outputMin=UNSET, outputMax=UNSET) -> ProcessBuilder: + """ + Linear transformation between two ranges + + :param self: A number to transform. The number gets clipped to the bounds specified in `inputMin` and + `inputMax`. + :param inputMin: Minimum value the input can obtain. + :param inputMax: Maximum value the input can obtain. + :param outputMin: Minimum value of the desired output range. + :param outputMax: Maximum value of the desired output range. + + :return: The transformed number. + """ + return linear_scale_range(x=self, inputMin=inputMin, inputMax=inputMax, outputMin=outputMin, outputMax=outputMax) + + @openeo_process + def ln(self) -> ProcessBuilder: + """ + Natural logarithm + + :param self: A number to compute the natural logarithm for. + + :return: The computed natural logarithm. + """ + return ln(x=self) + + @openeo_process + def load_collection(self, spatial_extent, temporal_extent, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Load a collection + + :param self: The collection id. + :param spatial_extent: Limits the data to load from the collection to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). * For vector data, the process loads the geometry into the data cube if the + geometry is fully *within* the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been + provided. The GeoJSON can be one of the following feature types: * A `Polygon` or `MultiPolygon` + geometry, * a `Feature` with a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` + containing at least one `Feature` with `Polygon` or `MultiPolygon` geometries. * Empty geometries are + ignored. Set this parameter to `null` to set no limit for the spatial extent. Be careful with this + when loading large datasets! It is recommended to use this parameter instead of using ``filter_bbox()`` + or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load from the collection to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array + with exactly two elements: 1. The first element is the start of the temporal interval. The specified + time instant is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified time instant is **excluded** from the interval. The second element must always + be greater/later than the first element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also + supports unbounded intervals by setting one of the boundaries to `null`, but never both. Set this + parameter to `null` to set no limit for the temporal extent. Be careful with this when loading large + datasets! It is recommended to use this parameter instead of using ``filter_temporal()`` directly after + loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + :param properties: Limits the data by metadata properties to include only data in the data cube which + all given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the + name of the metadata property, which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against the collection metadata, + see the example. + + :return: A data cube for further processing. The dimensions and dimension properties (name, type, + labels, reference system and resolution) correspond to the collection's metadata, but the dimension + labels are restricted as specified in the parameters. + """ + return load_collection(id=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + @openeo_process + def load_geojson(self, properties=UNSET) -> ProcessBuilder: + """ + Converts GeoJSON into a vector data cube + + :param self: A GeoJSON object to convert into a vector data cube. The GeoJSON type `GeometryCollection` + is not supported. Each geometry in the GeoJSON data results in a dimension label in the `geometries` + dimension. + :param properties: A list of properties from the GeoJSON file to construct an additional dimension + from. A new dimension with the name `properties` and type `other` is created if at least one property + is provided. Only applies for GeoJSON Features and FeatureCollections. Missing values are generally set + to no-data (`null`). Depending on the number of properties provided, the process creates the dimension + differently: - Single property with scalar values: A single dimension label with the name of the + property and a single value per geometry. - Single property of type array: The dimension labels + correspond to the array indices. There are as many values and labels per geometry as there are for the + largest array. - Multiple properties with scalar values: The dimension labels correspond to the + property names. There are as many values and labels per geometry as there are properties provided here. + + :return: A vector data cube containing the geometries, either one or two dimensional. + """ + return load_geojson(data=self, properties=properties) + + @openeo_process + def load_ml_model(self) -> ProcessBuilder: + """ + Load a ML model + + :param self: The STAC Item to load the machine learning model from. The STAC Item must implement the + `ml-model` extension. + + :return: A machine learning model to be used with machine learning processes such as + ``predict_random_forest()``. + """ + return load_ml_model(id=self) + + @openeo_process + def load_result(self, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET) -> ProcessBuilder: + """ + Load batch job results + + :param self: The id of a batch job with results. + :param spatial_extent: Limits the data to load from the batch job result to the specified bounding box + or polygons. * For raster data, the process loads the pixel into the data cube if the point at the + pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). * For vector data, the process loads the geometry into the data cube of the + geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features + standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been + provided. The GeoJSON can be one of the following feature types: * A `Polygon` or `MultiPolygon` + geometry, * a `Feature` with a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` + containing at least one `Feature` with `Polygon` or `MultiPolygon` geometries. Set this parameter to + `null` to set no limit for the spatial extent. Be careful with this when loading large datasets! It is + recommended to use this parameter instead of using ``filter_bbox()`` or ``filter_spatial()`` directly + after loading unbounded data. + :param temporal_extent: Limits the data to load from the batch job result to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array + with exactly two elements: 1. The first element is the start of the temporal interval. The specified + instance in time is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified instance in time is **excluded** from the interval. The specified temporal + strings follow [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html). Also supports open intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit + for the temporal extent. Be careful with this when loading large datasets! It is recommended to use + this parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + + :return: A data cube for further processing. + """ + return load_result(id=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands) + + @openeo_process + def load_stac(self, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Loads data from STAC + + :param self: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a + specific STAC API Collection that allows to filter items and to download assets. This includes batch + job results, which itself are compliant to STAC. For external URLs, authentication details such as API + keys or tokens may need to be included in the URL. Batch job results can be specified in two ways: - + For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with `/jobs/{id}/results` and `{id}` is the + corresponding batch job ID. - For external results, a signed URL must be provided. Not all back-ends + support signed URLs, which are provided as a link with the link relation `canonical` in the batch job + result metadata. + :param spatial_extent: Limits the data to load to the specified bounding box or polygons. * For raster + data, the process loads the pixel into the data cube if the point at the pixel center intersects with + the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). * For + vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty + geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be one + of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. Set this parameter to `null` to set no limit for the spatial + extent. Be careful with this when loading large datasets! It is recommended to use this parameter + instead of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load to the specified left-closed temporal interval. Applies + to all temporal dimensions. The interval has to be specified as an array with exactly two elements: 1. + The first element is the start of the temporal interval. The specified instance in time is **included** + in the interval. 2. The second element is the end of the temporal interval. The specified instance in + time is **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports open intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit + for the temporal extent. Be careful with this when loading large datasets! It is recommended to use + this parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. Either the unique band + name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in + bands) can be specified. If the unique band name and the common name conflict, the unique band name has + a higher priority. The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. It is + recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded + data. + :param properties: Limits the data by metadata properties to include only data in the data cube which + all given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the + name of the metadata property, which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. This parameter + is not supported for static STAC. + + :return: A data cube for further processing. + """ + return load_stac(url=self, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + @openeo_process + def load_uploaded_files(self, format, options=UNSET) -> ProcessBuilder: + """ + Load files from the user workspace + + :param self: The files to read. Folders can't be specified, specify all files instead. An exception is + thrown if a file can't be read. + :param format: The file format to read from. It must be one of the values that the server reports as + supported input file formats, which usually correspond to the short GDAL/OGR codes. If the format is + not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter is + *case insensitive*. + :param options: The file format parameters to be used to read the files. Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return load_uploaded_files(paths=self, format=format, options=options) + + @openeo_process + def load_url(self, format, options=UNSET) -> ProcessBuilder: + """ + Load data from a URL + + :param self: The URL to read from. Authentication details such as API keys or tokens may need to be + included in the URL. + :param format: The file format to use when loading the data. It must be one of the values that the + server reports as supported input file formats, which usually correspond to the short GDAL/OGR codes. + If the format is not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This + parameter is *case insensitive*. + :param options: The file format parameters to use when reading the data. Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return load_url(url=self, format=format, options=options) + + @openeo_process + def log(self, base) -> ProcessBuilder: + """ + Logarithm to a base + + :param self: A number to compute the logarithm for. + :param base: The numerical base. + + :return: The computed logarithm. + """ + return log(x=self, base=base) + + @openeo_process + def lt(self, y) -> ProcessBuilder: + """ + Less than comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly less than `y`, `null` if any operand is `null`, otherwise `false`. + """ + return lt(x=self, y=y) + + @openeo_process + def lte(self, y) -> ProcessBuilder: + """ + Less than or equal to comparison + + :param self: First operand. + :param y: Second operand. + + :return: `true` if `x` is less than or equal to `y`, `null` if any operand is `null`, otherwise + `false`. + """ + return lte(x=self, y=y) + + @openeo_process + def mask(self, mask, replacement=UNSET) -> ProcessBuilder: + """ + Apply a raster mask + + :param self: A raster data cube. + :param mask: A mask as a raster data cube. Every pixel in `data` must have a corresponding element in + `mask`. + :param replacement: The value used to replace masked values with. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, + labels, reference system and resolution) remain unchanged. + """ + return mask(data=self, mask=mask, replacement=replacement) + + @openeo_process + def mask_polygon(self, mask, replacement=UNSET, inside=UNSET) -> ProcessBuilder: + """ + Apply a polygon mask + + :param self: A raster data cube. + :param mask: A GeoJSON object or a vector data cube containing at least one polygon. The provided + vector data can be one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with + a `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` + with `Polygon` or `MultiPolygon` geometries. * Empty geometries are ignored. + :param replacement: The value used to replace masked values with. + :param inside: If set to `true` all pixels for which the point at the pixel center **does** intersect + with any polygon are replaced. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, + labels, reference system and resolution) remain unchanged. + """ + return mask_polygon(data=self, mask=mask, replacement=replacement, inside=inside) + + @openeo_process + def max(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Maximum value + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The maximum value. + """ + return max(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def mean(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Arithmetic mean (average) + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed arithmetic mean. + """ + return mean(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def median(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Statistical median + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed statistical median. + """ + return median(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def merge_cubes(self, cube2, overlap_resolver=UNSET, context=UNSET) -> ProcessBuilder: + """ + Merge two data cubes + + :param self: The base data cube. + :param cube2: The other data cube to be merged with the base data cube. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The + reducer must return a value of the same data type as the input values are. The reduction operator may + be a single process such as ``multiply()`` or consist of multiple sub-processes. `null` (the default) + can be specified if no overlap resolver is required. + :param context: Additional data to be passed to the overlap resolver. + + :return: The merged data cube. See the process description for details regarding the dimensions and + dimension properties (name, type, labels, reference system and resolution). + """ + return merge_cubes( + cube1=self, + cube2=cube2, + overlap_resolver=(build_child_callback(overlap_resolver, parent_parameters=['x', 'y', 'context']) if overlap_resolver not in [None, UNSET] else overlap_resolver), + context=context + ) + + @openeo_process + def min(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum value + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The minimum value. + """ + return min(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def mod(self, y) -> ProcessBuilder: + """ + Modulo + + :param self: A number to be used as the dividend. + :param y: A number to be used as the divisor. + + :return: The remainder after division. + """ + return mod(x=self, y=y) + + @openeo_process + def multiply(self, y) -> ProcessBuilder: + """ + Multiplication of two numbers + + :param self: The multiplier. + :param y: The multiplicand. + + :return: The computed product of the two numbers. + """ + return multiply(x=self, y=y) + + @openeo_process + def nan(self) -> ProcessBuilder: + """ + Not a Number (NaN) + + :return: Returns `NaN`. + """ + return nan() + + @openeo_process + def ndvi(self, nir=UNSET, red=UNSET, target_band=UNSET) -> ProcessBuilder: + """ + Normalized Difference Vegetation Index + + :param self: A raster data cube with two bands that have the common names `red` and `nir` assigned. + :param nir: The name of the NIR band. Defaults to the band that has the common name `nir` assigned. + Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata + field `common_name` in bands) can be specified. If the unique band name and the common name conflict, + the unique band name has a higher priority. + :param red: The name of the red band. Defaults to the band that has the common name `red` assigned. + Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata + field `common_name` in bands) can be specified. If the unique band name and the common name conflict, + the unique band name has a higher priority. + :param target_band: By default, the dimension of type `bands` is dropped. To keep the dimension specify + a new band name in this parameter so that a new dimension label with the specified name will be added + for the computed values. + + :return: A raster data cube containing the computed NDVI values. The structure of the data cube differs + depending on the value passed to `target_band`: * `target_band` is `null`: The data cube does not + contain the dimension of type `bands`, the number of dimensions decreases by one. The dimension + properties (name, type, labels, reference system and resolution) for all other dimensions remain + unchanged. * `target_band` is a string: The data cube keeps the same dimensions. The dimension + properties remain unchanged, but the number of dimension labels for the dimension of type `bands` + increases by one. The additional label is named as specified in `target_band`. + """ + return ndvi(data=self, nir=nir, red=red, target_band=target_band) + + @openeo_process + def neq(self, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Not equal to comparison + + :param self: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a + positive non-zero number the non-equality of two numbers is checked against a delta value. This is + especially useful to circumvent problems with floating-point inaccuracy in machine-based computation. + This option is basically an alias for the following computation: `gt(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be + disabled by setting this parameter to `false`. + + :return: `true` if `x` is *not* equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return neq(x=self, y=y, delta=delta, case_sensitive=case_sensitive) + + @openeo_process + def normalized_difference(self, y) -> ProcessBuilder: + """ + Normalized difference + + :param self: The value for the first band. + :param y: The value for the second band. + + :return: The computed normalized difference. + """ + return normalized_difference(x=self, y=y) + + @openeo_process + def not_(self) -> ProcessBuilder: + """ + Inverting a boolean + + :param self: Boolean value to invert. + + :return: Inverted boolean value. + """ + return not_(x=self) + + @openeo_process + def or_(self, y) -> ProcessBuilder: + """ + Logical OR + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical OR. + """ + return or_(x=self, y=y) + + @openeo_process + def order(self, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Get the order of array elements + + :param self: An array to compute the order for. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set + to `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The computed permutation. + """ + return order(data=self, asc=asc, nodata=nodata) + + @openeo_process + def pi(self) -> ProcessBuilder: + """ + Pi (π) + + :return: The numerical value of Pi. + """ + return pi() + + @openeo_process + def power(self, p) -> ProcessBuilder: + """ + Exponentiation + + :param self: The numerical base. + :param p: The numerical exponent. + + :return: The computed value for `base` raised to the power of `p`. + """ + return power(base=self, p=p) + + @openeo_process + def predict_curve(self, function, dimension, labels=UNSET) -> ProcessBuilder: + """ + Predict values + + :param self: A data cube with optimal values, e.g. computed by the process ``fit_curve()``. + :param function: The model function. It must take the parameters to fit as array through the first + argument and the independent variable `x` as the second argument. It is recommended to store the model + function as a user-defined process on the back-end. + :param dimension: The name of the dimension for predictions. + :param labels: The labels to predict values for. If no labels are given, predicts values only for no- + data (`null`) values in the data cube. + + :return: A data cube with the predicted values with the provided dimension `dimension` having as many + labels as provided through `labels`. + """ + return predict_curve( + parameters=self, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + dimension=dimension, + labels=labels + ) + + @openeo_process + def predict_random_forest(self, model) -> ProcessBuilder: + """ + Predict values based on a Random Forest model + + :param self: An array of numbers. + :param model: A model object that can be trained with the processes ``fit_regr_random_forest()`` + (regression) and ``fit_class_random_forest()`` (classification). + + :return: The predicted value. Returns `null` if any of the given values in the array is a no-data + value. + """ + return predict_random_forest(data=self, model=model) + + @openeo_process + def product(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the product by multiplying numbers + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed product of the sequence of numbers. + """ + return product(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def quantiles(self, probabilities=UNSET, q=UNSET, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Quantiles + + :param self: An array of numbers. + :param probabilities: Quantiles to calculate. Either a list of probabilities or the number of + intervals: * Provide an array with a sorted list of probabilities in ascending order to calculate + quantiles for. The probabilities must be between 0 and 1 (inclusive). If not sorted in ascending order, + an `AscendingProbabilitiesRequired` exception is thrown. * Provide an integer to specify the number of + intervals to calculate quantiles for. Calculates q-quantiles with equal-sized intervals. + :param q: Number of intervals to calculate quantiles for. Calculates q-quantiles with equal-sized + intervals. This parameter has been **deprecated**. Please use the parameter `probabilities` instead. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that an array with `null` values is returned + if any element is such a value. + + :return: An array with the computed quantiles. The list has either * as many elements as the given + list of `probabilities` had or * *`q`-1* elements. If the input array is empty the resulting array is + filled with as many `null` values as required according to the list above. See the 'Empty array' + example for an example. + """ + return quantiles(data=self, probabilities=probabilities, q=q, ignore_nodata=ignore_nodata) + + @openeo_process + def rearrange(self, order) -> ProcessBuilder: + """ + Sort an array based on a permutation + + :param self: The array to rearrange. + :param order: The permutation used for rearranging. + + :return: The rearranged array. + """ + return rearrange(data=self, order=order) + + @openeo_process + def reduce_dimension(self, reducer, dimension, context=UNSET) -> ProcessBuilder: + """ + Reduce dimensions + + :param self: A data cube. + :param reducer: A reducer to apply on the specified dimension. A reducer is a single process such as + ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param dimension: The name of the dimension over which to reduce. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the given dimension, the number of + dimensions decreases by one. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return reduce_dimension( + data=self, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + @openeo_process + def reduce_spatial(self, reducer, context=UNSET) -> ProcessBuilder: + """ + Reduce spatial dimensions 'x' and 'y' + + :param self: A raster data cube. + :param reducer: A reducer to apply on the horizontal spatial dimensions. A reducer is a single process + such as ``mean()`` or a set of processes, which computes a single value for a list of values, see the + category 'reducer' for such processes. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the horizontal spatial dimensions, + the number of dimensions decreases by two. The dimension properties (name, type, labels, reference + system and resolution) for all other dimensions remain unchanged. + """ + return reduce_spatial(data=self, reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), context=context) + + @openeo_process + def rename_dimension(self, source, target) -> ProcessBuilder: + """ + Rename a dimension + + :param self: The data cube. + :param source: The current name of the dimension. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a `DimensionExists` exception if a dimension + with the specified name exists. + + :return: A data cube with the same dimensions, but the name of one of the dimensions changes. The old + name can not be referred to any longer. The dimension properties (name, type, labels, reference system + and resolution) remain unchanged. + """ + return rename_dimension(data=self, source=source, target=target) + + @openeo_process + def rename_labels(self, dimension, target, source=UNSET) -> ProcessBuilder: + """ + Rename dimension labels + + :param self: The data cube. + :param dimension: The name of the dimension to rename the labels for. + :param target: The new names for the labels. If a target dimension label already exists in the data + cube, a `LabelExists` exception is thrown. + :param source: The original names of the labels to be renamed to corresponding array elements in the + parameter `target`. It is allowed to only specify a subset of labels to rename, as long as the `target` + and `source` parameter have the same length. The order of the labels doesn't need to match the order of + the dimension labels in the data cube. By default, the array is empty so that the dimension labels in + the data cube are expected to be enumerated. If the dimension labels are not enumerated and the given + array is empty, the `LabelsNotEnumerated` exception is thrown. If one of the source dimension labels + doesn't exist, the `LabelNotAvailable` exception is thrown. + + :return: The data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except that for the given dimension the labels + change. The old labels can not be referred to any longer. The number of labels remains the same. + """ + return rename_labels(data=self, dimension=dimension, target=target, source=source) + + @openeo_process + def resample_cube_spatial(self, target, method=UNSET) -> ProcessBuilder: + """ + Resample the spatial dimensions to match a target data cube + + :param self: A raster data cube. + :param target: A raster data cube that describes the spatial target resolution. + :param method: Resampling method to use. The following options are available and are meant to align + with [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average + (mean) resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling + * `cubic`: cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc + resampling * `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median + resampling, selects the median value of all valid pixels * `min`: minimum resampling, selects the + minimum value from all valid pixels * `mode`: mode resampling, selects the value which appears most + often of all the sampled points * `near`: nearest neighbour resampling (default) * `q1`: first quartile + resampling, selects the first quartile value of all valid pixels * `q3`: third quartile resampling, + selects the third quartile value of all valid pixels * `rms` root mean square (quadratic mean) of all + valid pixels * `sum`: compute the weighted sum of all valid pixels Valid pixels are determined based + on the function ``is_valid()``. + + :return: A raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of + the spatial dimensions. + """ + return resample_cube_spatial(data=self, target=target, method=method) + + @openeo_process + def resample_cube_temporal(self, target, dimension=UNSET, valid_within=UNSET) -> ProcessBuilder: + """ + Resample temporal dimensions to match a target data cube + + :param self: A data cube with one or more temporal dimensions. + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample, which must exist with this name in + both data cubes. If the dimension is not set or is set to `null`, the process resamples all temporal + dimensions that exist with the same names in both data cubes. The following exceptions may occur: * A + dimension is given, but it does not exist in any of the data cubes: `DimensionNotAvailable` * A + dimension is given, but one of them is not temporal: `DimensionMismatch` * No specific dimension name + is given and there are no temporal dimensions with the same name in the data: `DimensionMismatch` + :param valid_within: Setting this parameter to a numerical value enables that the process searches for + valid values within the given period of days before and after the target timestamps. Valid values are + determined based on the function ``is_valid()``. For example, the limit of `7` for the target + timestamps `2020-01-15 12:00:00` looks for a nearest neighbor after `2020-01-08 12:00:00` and before + `2020-01-22 12:00:00`. If no valid value is found within the given period, the value will be set to no- + data (`null`). + + :return: A data cube with the same dimensions and the same dimension properties (name, type, labels, + reference system and resolution) for all non-temporal dimensions. For the temporal dimension, the name + and type remain unchanged, but the dimension labels, resolution and reference system may change. + """ + return resample_cube_temporal(data=self, target=target, dimension=dimension, valid_within=valid_within) + + @openeo_process + def resample_spatial(self, resolution=UNSET, projection=UNSET, method=UNSET, align=UNSET) -> ProcessBuilder: + """ + Resample and warp the spatial dimensions + + :param self: A raster data cube. + :param resolution: Resamples the data cube to the target resolution, which can be specified either as + separate values for x and y or as a single value for both axes. Specified in the units of the target + projection. Doesn't change the resolution by default (`0`). + :param projection: Warps the data cube to the target projection, specified as as [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). By default (`null`), the projection + is not changed. + :param method: Resampling method to use. The following options are available and are meant to align + with [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average + (mean) resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling + * `cubic`: cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc + resampling * `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median + resampling, selects the median value of all valid pixels * `min`: minimum resampling, selects the + minimum value from all valid pixels * `mode`: mode resampling, selects the value which appears most + often of all the sampled points * `near`: nearest neighbour resampling (default) * `q1`: first quartile + resampling, selects the first quartile value of all valid pixels * `q3`: third quartile resampling, + selects the third quartile value of all valid pixels * `rms` root mean square (quadratic mean) of all + valid pixels * `sum`: compute the weighted sum of all valid pixels Valid pixels are determined based + on the function ``is_valid()``. + :param align: Specifies to which corner of the spatial extent the new resampled data is aligned to. + + :return: A raster data cube with values warped onto the new projection. It has the same dimensions and + the same dimension properties (name, type, labels, reference system and resolution) for all non-spatial + or vertical spatial dimensions. For the horizontal spatial dimensions the name and type remain + unchanged, but reference system, labels and resolution may change depending on the given parameters. + """ + return resample_spatial(data=self, resolution=resolution, projection=projection, method=method, align=align) + + @openeo_process + def round(self, p=UNSET) -> ProcessBuilder: + """ + Round to a specified precision + + :param self: A number to round. + :param p: A positive number specifies the number of digits after the decimal point to round to. A + negative number means rounding to a power of ten, so for example *-2* rounds to the nearest hundred. + Defaults to *0*. + + :return: The rounded number. + """ + return round(x=self, p=p) + + @openeo_process + def run_udf(self, udf, runtime, version=UNSET, context=UNSET) -> ProcessBuilder: + """ + Run a UDF + + :param self: The data to be passed to the UDF. + :param udf: Either source code, an absolute URL or a path to a UDF script. + :param runtime: A UDF runtime identifier available at the back-end. + :param version: An UDF runtime version. If set to `null`, the default runtime version specified for + each runtime is used. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can be of any data type and is exactly what + the UDF code returns. + """ + return run_udf(data=self, udf=udf, runtime=runtime, version=version, context=context) + + @openeo_process + def run_udf_externally(self, url, context=UNSET) -> ProcessBuilder: + """ + Run an externally hosted UDF container + + :param self: The data to be passed to the UDF. + :param url: Absolute URL to a remote UDF service. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can in principle be of any data type, but it + depends on what is returned by the UDF code. Please see the implemented UDF interface for details. + """ + return run_udf_externally(data=self, url=url, context=context) + + @openeo_process + def sar_backscatter(self, coefficient=UNSET, elevation_model=UNSET, mask=UNSET, contributing_area=UNSET, local_incidence_angle=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + Computes backscatter from SAR input + + :param self: The source data cube containing SAR input. + :param coefficient: Select the radiometric correction coefficient. The following options are available: + * `beta0`: radar brightness * `sigma0-ellipsoid`: ground area computed with ellipsoid earth model * + `sigma0-terrain`: ground area computed with terrain earth model * `gamma0-ellipsoid`: ground area + computed with ellipsoid earth model in sensor line of sight * `gamma0-terrain`: ground area computed + with terrain earth model in sensor line of sight (default) * `null`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the + back-end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. It indicates + which values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options + will reduce portability. + + :return: Backscatter values corresponding to the chosen parametrization. The values are given in linear + scale. + """ + return sar_backscatter( + data=self, + coefficient=coefficient, + elevation_model=elevation_model, + mask=mask, + contributing_area=contributing_area, + local_incidence_angle=local_incidence_angle, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + @openeo_process + def save_result(self, format, options=UNSET) -> ProcessBuilder: + """ + Save processed data + + :param self: The data to deliver in the given file format. + :param format: The file format to use. It must be one of the values that the server reports as + supported output file formats, which usually correspond to the short GDAL/OGR codes. This parameter is + *case insensitive*. * If the data cube is empty and the file format can't store empty data cubes, a + `DataCubeEmpty` exception is thrown. * If the file format is otherwise not suitable for storing the + underlying data structure, a `FormatUnsuitable` exception is thrown. + :param options: The file format parameters to be used to create the file(s). Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names + and valid values usually correspond to the GDAL/OGR format options. + + :return: Always returns `true` as in case of an error an exception is thrown which aborts the execution + of the process. + """ + return save_result(data=self, format=format, options=options) + + @openeo_process + def sd(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Standard deviation + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sample standard deviation. + """ + return sd(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def sgn(self) -> ProcessBuilder: + """ + Signum + + :param self: A number. + + :return: The computed signum value of `x`. + """ + return sgn(x=self) + + @openeo_process + def sin(self) -> ProcessBuilder: + """ + Sine + + :param self: An angle in radians. + + :return: The computed sine of `x`. + """ + return sin(x=self) + + @openeo_process + def sinh(self) -> ProcessBuilder: + """ + Hyperbolic sine + + :param self: An angle in radians. + + :return: The computed hyperbolic sine of `x`. + """ + return sinh(x=self) + + @openeo_process + def sort(self, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Sort data + + :param self: An array with data to sort. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set + to `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The sorted array. + """ + return sort(data=self, asc=asc, nodata=nodata) + + @openeo_process + def sqrt(self) -> ProcessBuilder: + """ + Square root + + :param self: A number. + + :return: The computed square root. + """ + return sqrt(x=self) + + @openeo_process + def subtract(self, y) -> ProcessBuilder: + """ + Subtraction of two numbers + + :param self: The minuend. + :param y: The subtrahend. + + :return: The computed result. + """ + return subtract(x=self, y=y) + + @openeo_process + def sum(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the sum by adding up numbers + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sum of the sequence of numbers. + """ + return sum(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def tan(self) -> ProcessBuilder: + """ + Tangent + + :param self: An angle in radians. + + :return: The computed tangent of `x`. + """ + return tan(x=self) + + @openeo_process + def tanh(self) -> ProcessBuilder: + """ + Hyperbolic tangent + + :param self: An angle in radians. + + :return: The computed hyperbolic tangent of `x`. + """ + return tanh(x=self) + + @openeo_process + def text_begins(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text begins with another text + + :param self: Text in which to find something at the beginning. + :param pattern: Text to find at the beginning of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` begins with `pattern`, false` otherwise. + """ + return text_begins(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def text_concat(self, separator=UNSET) -> ProcessBuilder: + """ + Concatenate elements to a single text + + :param self: A set of elements. Numbers, boolean values and null values get converted to their (lower + case) string representation. For example: `1` (integer), `-1.5` (number), `true` / `false` (boolean + values) + :param separator: A separator to put between each of the individual texts. Defaults to an empty string. + + :return: A string containing a string representation of all the array elements in the same order, with + the separator between each element. + """ + return text_concat(data=self, separator=separator) + + @openeo_process + def text_contains(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text contains another text + + :param self: Text in which to find something in. + :param pattern: Text to find in `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` contains the `pattern`, false` otherwise. + """ + return text_contains(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def text_ends(self, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text ends with another text + + :param self: Text in which to find something at the end. + :param pattern: Text to find at the end of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` ends with `pattern`, false` otherwise. + """ + return text_ends(data=self, pattern=pattern, case_sensitive=case_sensitive) + + @openeo_process + def trim_cube(self) -> ProcessBuilder: + """ + Remove dimension labels with no-data values + + :param self: A data cube to trim. + + :return: A trimmed data cube with the same dimensions. The dimension properties name, type, reference + system and resolution remain unchanged. The number of dimension labels may decrease. + """ + return trim_cube(data=self) + + @openeo_process + def unflatten_dimension(self, dimension, target_dimensions, label_separator=UNSET) -> ProcessBuilder: + """ + Split a single dimensions into multiple dimensions + + :param self: A data cube that is consistently structured so that operation can execute flawlessly (e.g. + the dimension labels need to contain the `label_separator` exactly 1 time for two target dimensions, 2 + times for three target dimensions etc.). + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the new target dimensions. New dimensions will be created with + the given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` + exception if any of the dimensions exists. The order of the array defines the order in which the + dimensions and dimension labels are added to the data cube (see the example in the process + description). + :param label_separator: The string that will be used as a separator to split the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system + and resolution) for all other dimensions remain unchanged. + """ + return unflatten_dimension(data=self, dimension=dimension, target_dimensions=target_dimensions, label_separator=label_separator) + + @openeo_process + def variance(self, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Variance + + :param self: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is returned if any value is such a + value. + + :return: The computed sample variance. + """ + return variance(data=self, ignore_nodata=ignore_nodata) + + @openeo_process + def vector_buffer(self, distance) -> ProcessBuilder: + """ + Buffer geometries by distance + + :param self: Geometries to apply the buffer on. Feature properties are preserved. + :param distance: The distance of the buffer in meters. A positive distance expands the geometries, + resulting in outward buffering (dilation), while a negative distance shrinks the geometries, resulting + in inward buffering (erosion). If the unit of the spatial reference system is not meters, a + `UnitMismatch` error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable + spatial reference system. + + :return: Returns a vector data cube with the computed new geometries of which some may be empty. + """ + return vector_buffer(geometries=self, distance=distance) + + @openeo_process + def vector_reproject(self, projection, dimension=UNSET) -> ProcessBuilder: + """ + Reprojects the geometry dimension + + :param self: A vector data cube. + :param projection: Coordinate reference system to reproject to. Specified as an [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). + :param dimension: The name of the geometry dimension to reproject. If no specific dimension is + specified, the filter applies to all geometry dimensions. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + + :return: A vector data cube with geometries projected to the new coordinate reference system. The + reference system of the geometry dimension changes, all other dimensions and properties remain + unchanged. + """ + return vector_reproject(data=self, projection=projection, dimension=dimension) + + @openeo_process + def vector_to_random_points(self, geometry_count=UNSET, total_count=UNSET, group=UNSET, seed=UNSET) -> ProcessBuilder: + """ + Sample random points from geometries + + :param self: Input geometries for sample extraction. + :param geometry_count: The maximum number of points to compute per geometry. Points in the input + geometries can be selected only once by the sampling. + :param total_count: The maximum number of points to compute overall. Throws a `CountMismatch` + exception if the specified value is less than the provided number of geometries. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a + `MultiPoint` per geometry given which keeps the original identifier if present. * Otherwise, each + sampled point is generated as a distinct `Point` geometry without identifier. + :param seed: A randomization seed to use for random sampling. If not given or `null`, no seed is used + and results may differ on subsequent use. + + :return: Returns a vector data cube with the sampled points. + """ + return vector_to_random_points(data=self, geometry_count=geometry_count, total_count=total_count, group=group, seed=seed) + + @openeo_process + def vector_to_regular_points(self, distance, group=UNSET) -> ProcessBuilder: + """ + Sample regular points from geometries + + :param self: Input geometries for sample extraction. + :param distance: Defines the minimum distance in meters that is required between two samples generated + *inside* a single geometry. If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. - For **polygons**, the distance defines the cell sizes of a regular grid that starts at the + upper-left bound of each polygon. The centroid of each cell is then a sample point. If the centroid is + not enclosed in the polygon, no point is sampled. If no point can be sampled for the geometry at all, + the first coordinate of the geometry is returned as point. - For **lines** (line strings), the sampling + starts with a point at the first coordinate of the line and then walks along the line and samples a new + point each time the distance to the previous point has been reached again. - For **points**, the point + is returned as given. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a + `MultiPoint` per geometry given which keeps the original identifier if present. * Otherwise, each + sampled point is generated as a distinct `Point` geometry without identifier. + + :return: Returns a vector data cube with the sampled points. + """ + return vector_to_regular_points(data=self, distance=distance, group=group) + + @openeo_process + def xor(self, y) -> ProcessBuilder: + """ + Logical XOR (exclusive or) + + :param self: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical XOR. + """ + return xor(x=self, y=y) + + +# Public shortcut +process = ProcessBuilder.process +# Private shortcut that has lower chance to collide with a process argument named `process` +_process = ProcessBuilder.process + + +@openeo_process +def absolute(x) -> ProcessBuilder: + """ + Absolute value + + :param x: A number. + + :return: The computed absolute value. + """ + return _process('absolute', x=x) + + +@openeo_process +def add(x, y) -> ProcessBuilder: + """ + Addition of two numbers + + :param x: The first summand. + :param y: The second summand. + + :return: The computed sum of the two numbers. + """ + return _process('add', x=x, y=y) + + +@openeo_process +def add_dimension(data, name, label, type=UNSET) -> ProcessBuilder: + """ + Add a new dimension + + :param data: A data cube to add the dimension to. + :param name: Name for the dimension. + :param label: A dimension label. + :param type: The type of dimension, defaults to `other`. + + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. All + other dimensions remain unchanged. + """ + return _process('add_dimension', data=data, name=name, label=label, type=type) + + +@openeo_process +def aggregate_spatial(data, geometries, reducer, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for geometries + + :param data: A raster data cube with at least two spatial dimensions. The data cube implicitly gets + restricted to the bounds of the geometries as if ``filter_spatial()`` would have been used with the same + values for the corresponding parameters immediately before this process. + :param geometries: Geometries for which the aggregation will be computed. Feature properties are preserved + for vector data cubes and all GeoJSON Features. One value will be computed per label in the dimension of + type `geometries`, GeoJSON `Feature` or `Geometry`. For a `FeatureCollection` multiple values will be + computed, one value per contained `Feature`. No values will be computed for empty geometries. For example, + a single value will be computed for a `MultiPolygon`, but two values will be computed for a + `FeatureCollection` containing two polygons. - For **polygons**, the process considers all pixels for + which the point at the pixel center intersects with the corresponding polygon (as defined in the Simple + Features standard by the OGC). - For **points**, the process considers the closest pixel center. - For + **lines** (line strings), the process considers all the pixels whose centers are closest to at least one + point on the line. Thus, pixels may be part of multiple geometries and be part of multiple aggregations. + No operation is applied to geometries that are outside of the bounds of the data. + :param reducer: A reducer to be applied on all values of each geometry. A reducer is a single process such + as ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param target_dimension: By default (which is `null`), the process only computes the results and doesn't + add a new dimension. If this parameter contains a new dimension name, the computation also stores + information about the total count of pixels (valid + invalid pixels) and the number of valid pixels (see + ``is_valid()``) for each computed value. These values are added as a new dimension. The new dimension of + type `other` has the dimension labels `value`, `total_count` and `valid_count`. Fails with a + `TargetDimensionExists` exception if a dimension with the specified name exists. + :param context: Additional data to be passed to the reducer. + + :return: A vector data cube with the computed results. Empty geometries still exist but without any + aggregated values (i.e. no-data). The spatial dimensions are replaced by a dimension of type 'geometries' + and if `target_dimension` is not `null`, a new dimension is added. + """ + return _process('aggregate_spatial', + data=data, + geometries=geometries, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + target_dimension=target_dimension, + context=context + ) + + +@openeo_process +def aggregate_spatial_window(data, reducer, size, boundary=UNSET, align=UNSET, context=UNSET) -> ProcessBuilder: + """ + Zonal statistics for rectangular windows + + :param data: A raster data cube with exactly two horizontal spatial dimensions and an arbitrary number of + additional dimensions. The process is applied to all additional dimensions individually. + :param reducer: A reducer to be applied on the list of values, which contain all pixels covered by the + window. A reducer is a single process such as ``mean()`` or a set of processes, which computes a single + value for a list of values, see the category 'reducer' for such processes. + :param size: Window size in pixels along the horizontal spatial dimensions. The first value corresponds to + the `x` axis, the second value corresponds to the `y` axis. + :param boundary: Behavior to apply if the number of values for the axes `x` and `y` is not a multiple of + the corresponding value in the `size` parameter. Options are: - `pad` (default): pad the data cube with + the no-data value `null` to fit the required window size. - `trim`: trim the data cube to fit the required + window size. Set the parameter `align` to specifies to which corner the data is aligned to. + :param align: If the data requires padding or trimming (see parameter `boundary`), specifies to which + corner of the spatial extent the data is aligned to. For example, if the data is aligned to the upper left, + the process pads/trims at the lower-right. + :param context: Additional data to be passed to the reducer. + + :return: A raster data cube with the newly computed values and the same dimensions. The resolution will + change depending on the chosen values for the `size` and `boundary` parameter. It usually decreases for the + dimensions which have the corresponding parameter `size` set to values greater than 1. The dimension + labels will be set to the coordinate at the center of the window. The other dimension properties (name, + type and reference system) remain unchanged. + """ + return _process('aggregate_spatial_window', + data=data, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + size=size, + boundary=boundary, + align=align, + context=context + ) + + +@openeo_process +def aggregate_temporal(data, intervals, reducer, labels=UNSET, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations + + :param data: A data cube. + :param intervals: Left-closed temporal intervals, which are allowed to overlap. Each temporal interval in + the array has exactly two elements: 1. The first element is the start of the temporal interval. The + specified time instant is **included** in the interval. 2. The second element is the end of the temporal + interval. The specified time instant is **excluded** from the interval. The second element must always be + greater/later than the first element, except when using time without date. Otherwise, a + `TemporalExtentEmpty` exception is thrown. + :param reducer: A reducer to be applied for the values contained in each interval. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, see + the category 'reducer' for such processes. Intervals may not contain any values, which for most reducers + leads to no-data (`null`) values by default. + :param labels: Distinct labels for the intervals, which can contain dates and/or times. Is only required to + be specified if the values for the start of the temporal intervals are not distinct and thus the default + labels would not be unique. The number of labels and the number of groups need to be equal. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the given + temporal dimension. + """ + return _process('aggregate_temporal', + data=data, + intervals=intervals, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + labels=labels, + dimension=dimension, + context=context + ) + + +@openeo_process +def aggregate_temporal_period(data, period, reducer, dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Temporal aggregations based on calendar hierarchies + + :param data: The source data cube. + :param period: The time intervals to aggregate. The following pre-defined values are available: * `hour`: + Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten day periods, counted per + year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month + can range from 8 to 11 days. For example, the third dekad of a year spans from January 21 till January 31 + (11 days), the fourth dekad spans from February 1 till February 10 (10 days) and the sixth dekad spans from + February 21 till February 28 or February 29 in a leap year (8 or 9 days respectively). * `month`: Month of + the year * `season`: Three month periods of the calendar seasons (December - February, March - May, June - + August, September - November). * `tropical-season`: Six month periods of the tropical seasons (November - + April, May - October). * `year`: Proleptic years * `decade`: Ten year periods ([0-to-9 + decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the next year + ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. + :param reducer: A reducer to be applied for the values contained in each period. A reducer is a single + process such as ``mean()`` or a set of processes, which computes a single value for a list of values, see + the category 'reducer' for such processes. Periods may not contain any values, which for most reducers + leads to no-data (`null`) values by default. + :param dimension: The name of the temporal dimension for aggregation. All data along the dimension is + passed through the specified reducer. If the dimension is not set or set to `null`, the source data cube is + expected to only have one temporal dimension. Fails with a `TooManyDimensions` exception if it has more + dimensions. Fails with a `DimensionNotAvailable` exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A new data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the given + temporal dimension. The specified temporal dimension has the following dimension labels (`YYYY` = four- + digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: `YYYY-MM-DD-00` - `YYYY-MM- + DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * `dekad`: `YYYY-00` - `YYYY-36` * + `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - February), `YYYY-mam` (March - May), + `YYYY-jja` (June - August), `YYYY-son` (September - November). * `tropical-season`: `YYYY-ndjfma` (November + - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * `decade`: `YYY0` * `decade-ad`: `YYY1` The + dimension labels in the new data cube are complete for the whole extent of the source data cube. For + example, if `period` is set to `day` and the source data cube has two dimension labels at the beginning of + the year (`2020-01-01`) and the end of a year (`2020-12-31`), the process returns a data cube with 365 + dimension labels (`2020-001`, `2020-002`, ..., `2020-365`). In contrast, if `period` is set to `day` and + the source data cube has just one dimension label `2020-01-05`, the process returns a data cube with just a + single dimension label (`2020-005`). + """ + return _process('aggregate_temporal_period', + data=data, + period=period, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + +@openeo_process +def all(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Are all of the values true? + + :param data: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return _process('all', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def and_(x, y) -> ProcessBuilder: + """ + Logical AND + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical AND. + """ + return _process('and', x=x, y=y) + + +@openeo_process +def anomaly(data, normals, period) -> ProcessBuilder: + """ + Compute anomalies + + :param data: A data cube with exactly one temporal dimension and the following dimension labels for the + given period (`YYYY` = four-digit year, `MM` = two-digit month, `DD` two-digit day of month): * `hour`: + `YYYY-MM-DD-00` - `YYYY-MM-DD-23` * `day`: `YYYY-001` - `YYYY-365` * `week`: `YYYY-01` - `YYYY-52` * + `dekad`: `YYYY-00` - `YYYY-36` * `month`: `YYYY-01` - `YYYY-12` * `season`: `YYYY-djf` (December - + February), `YYYY-mam` (March - May), `YYYY-jja` (June - August), `YYYY-son` (September - November). * + `tropical-season`: `YYYY-ndjfma` (November - April), `YYYY-mjjaso` (May - October). * `year`: `YYYY` * + `decade`: `YYY0` * `decade-ad`: `YYY1` * `single-period` / `climatology-period`: Any + ``aggregate_temporal_period()`` can compute such a data cube. + :param normals: A data cube with normals, e.g. daily, monthly or yearly values computed from a process such + as ``climatological_normal()``. Must contain exactly one temporal dimension with the following dimension + labels for the given period: * `hour`: `00` - `23` * `day`: `001` - `365` * `week`: `01` - `52` * `dekad`: + `00` - `36` * `month`: `01` - `12` * `season`: `djf` (December - February), `mam` (March - May), `jja` + (June - August), `son` (September - November) * `tropical-season`: `ndjfma` (November - April), `mjjaso` + (May - October) * `year`: Four-digit year numbers * `decade`: Four-digit year numbers, the last digit being + a `0` * `decade-ad`: Four-digit year numbers, the last digit being a `1` * `single-period` / `climatology- + period`: A single dimension label with any name is expected. + :param period: Specifies the time intervals available in the normals data cube. The following options are + available: * `hour`: Hour of the day * `day`: Day of the year * `week`: Week of the year * `dekad`: Ten + day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The + third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 + each year. * `month`: Month of the year * `season`: Three month periods of the calendar seasons (December - + February, March - May, June - August, September - November). * `tropical-season`: Six month periods of the + tropical seasons (November - April, May - October). * `year`: Proleptic years * `decade`: Ten year periods + ([0-to-9 decade](https://en.wikipedia.org/wiki/Decade#0-to-9_decade)), from a year ending in a 0 to the + next year ending in a 9. * `decade-ad`: Ten year periods ([1-to-0 + decade](https://en.wikipedia.org/wiki/Decade#1-to-0_decade)) better aligned with the anno Domini (AD) + calendar era, from a year ending in a 1 to the next year ending in a 0. * `single-period` / `climatology- + period`: A single period of arbitrary length + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged. + """ + return _process('anomaly', data=data, normals=normals, period=period) + + +@openeo_process +def any(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Is at least one value true? + + :param data: A set of boolean values. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + + :return: Boolean result of the logical operation. + """ + return _process('any', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def apply(data, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each value + + :param data: A data cube. + :param process: A process that accepts and returns a single value and is applied on each individual value + in the data cube. The process may consist of multiple sub-processes and could, for example, consist of + processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply', data=data, process=build_child_callback(process, parent_parameters=['x', 'context']), context=context) + + +@openeo_process +def apply_dimension(data, process, dimension, target_dimension=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to all values along a dimension + + :param data: A data cube. + :param process: Process to be applied on all values along the given dimension. The specified process needs + to accept an array and must return an array with at least one element. A process may consist of multiple + sub-processes. + :param dimension: The name of the source dimension to apply the process on. Fails with a + `DimensionNotAvailable` exception if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or `null` (the default) to use the source + dimension specified in the parameter `dimension`. By specifying a target dimension, the source dimension + is removed. The target dimension with the specified name and the type `other` (see ``add_dimension()``) is + created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. All dimensions stay the same, except for the + dimensions specified in corresponding parameters. There are three cases how the dimensions can change: 1. + The source dimension is the target dimension: - The (number of) dimensions remain unchanged as the + source dimension is the target dimension. - The source dimension properties name and type remain + unchanged. - The dimension labels, the reference system and the resolution are preserved only if the + number of values in the source dimension is equal to the number of values computed by the process. + Otherwise, all other dimension properties change as defined in the list below. 2. The source dimension is + not the target dimension. The target dimension exists with a single label only: - The number of + dimensions decreases by one as the source dimension is 'dropped' and the target dimension is filled with + the processed data that originates from the source dimension. - The target dimension properties name and + type remain unchanged. All other dimension properties change as defined in the list below. 3. The source + dimension is not the target dimension and the latter does not exist: - The number of dimensions remain + unchanged, but the source dimension is replaced with the target dimension. - The target dimension has + the specified name and the type other. All other dimension properties are set as defined in the list below. + Unless otherwise stated above, for the given (target) dimension the following applies: - the number of + dimension labels is equal to the number of values computed by the process, - the dimension labels are + incrementing integers starting from zero, - the resolution changes, and - the reference system is + undefined. + """ + return _process('apply_dimension', + data=data, + process=build_child_callback(process, parent_parameters=['data', 'context']), + dimension=dimension, + target_dimension=target_dimension, + context=context + ) + + +@openeo_process +def apply_kernel(data, kernel, factor=UNSET, border=UNSET, replace_invalid=UNSET) -> ProcessBuilder: + """ + Apply a spatial convolution with a kernel + + :param data: A raster data cube. + :param kernel: Kernel as a two-dimensional array of weights. The inner level of the nested array aligns + with the `x` axis and the outer level aligns with the `y` axis. Each level of the kernel must have an + uneven number of elements, otherwise the process throws a `KernelDimensionsUneven` exception. + :param factor: A factor that is multiplied to each value after the kernel has been applied. This is + basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required + for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults to + fill the border with zeroes. The following options are available: * *numeric value* - fill with a user- + defined constant number `n`: `nnnnnn|abcdefgh|nnnnnn` (default, with `n` = 0) * `replicate` - repeat the + value from the pixel at the border: `aaaaaa|abcdefgh|hhhhhh` * `reflect` - mirror/reflect from the border: + `fedcba|abcdefgh|hgfedc` * `reflect_pixel` - mirror/reflect from the center of the pixel at the border: + `gfedcb|abcdefgh|gfedcb` * `wrap` - repeat/wrap the image: `cdefgh|abcdefgh|abcdef` + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite numerical + values with. By default, those values are replaced with zeroes. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_kernel', data=data, kernel=kernel, factor=factor, border=border, replace_invalid=replace_invalid) + + +@openeo_process +def apply_neighborhood(data, process, size, overlap=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to pixels in a n-dimensional neighborhood + + :param data: A raster data cube. + :param process: Process to be applied on all neighborhoods. + :param size: Neighborhood sizes along each dimension. This object maps dimension names to either a + physical measure (e.g. 100 m, 10 days) or pixels (e.g. 32 pixels). For dimensions not specified, the + default is to provide all values. Be aware that including all values from overly large dimensions may not + be processed at once. + :param overlap: Overlap of neighborhoods along each dimension to avoid border effects. By default no + overlap is provided. For instance a temporal dimension can add 1 month before and after a neighborhood. In + the spatial dimensions, this is often a number of pixels. The overlap specified is added before and after, + so an overlap of 8 pixels will add 8 pixels on both sides of the window, so 16 in total. Be aware that + large overlaps increase the need for computational resources and modifying overlapping data in subsequent + operations have no effect. + :param context: Additional data to be passed to the process. + + :return: A raster data cube with the newly computed values and the same dimensions. The dimension + properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_neighborhood', + data=data, + process=build_child_callback(process, parent_parameters=['data', 'context']), + size=size, + overlap=overlap, + context=context + ) + + +@openeo_process +def apply_polygon(data, polygons, process, mask_value=UNSET, context=UNSET) -> ProcessBuilder: + """ + Apply a process to segments of the data cube + + :param data: A data cube. + :param polygons: A vector data cube containing at least one polygon. The provided vector data can be one of + the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or `MultiPolygon` + geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or `MultiPolygon` + geometries. * Empty geometries are ignored. + :param process: A process that accepts and returns a single data cube and is applied on each individual sub + data cube. The process may consist of multiple sub-processes. + :param mask_value: All pixels for which the point at the pixel center **does not** intersect with the + polygon are replaced with the given value, which defaults to `null` (no data). It can provide a + distinction between no data values within the polygon and masked pixels outside of it. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. The dimension properties + (name, type, labels, reference system and resolution) remain unchanged. + """ + return _process('apply_polygon', + data=data, + polygons=polygons, + process=build_child_callback(process, parent_parameters=['data', 'context']), + mask_value=mask_value, + context=context + ) + + +@openeo_process +def arccos(x) -> ProcessBuilder: + """ + Inverse cosine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arccos', x=x) + + +@openeo_process +def arcosh(x) -> ProcessBuilder: + """ + Inverse hyperbolic cosine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arcosh', x=x) + + +@openeo_process +def arcsin(x) -> ProcessBuilder: + """ + Inverse sine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arcsin', x=x) + + +@openeo_process +def arctan(x) -> ProcessBuilder: + """ + Inverse tangent + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arctan', x=x) + + +@openeo_process +def arctan2(y, x) -> ProcessBuilder: + """ + Inverse tangent of two numbers + + :param y: A number to be used as the dividend. + :param x: A number to be used as the divisor. + + :return: The computed angle in radians. + """ + return _process('arctan2', y=y, x=x) + + +@openeo_process +def ard_normalized_radar_backscatter(data, elevation_model=UNSET, contributing_area=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant SAR NRB generation + + :param data: The source data cube containing SAR input. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options will + reduce portability. + + :return: Backscatter values expressed as gamma0 in linear scale. In addition to the bands + `contributing_area` and `ellipsoid_incidence_angle` that can optionally be added with corresponding + parameters, the following bands are always added to the data cube: - `mask`: A data mask that indicates + which values are valid (1), invalid (0) or contain no-data (null). - `local_incidence_angle`: A band with + DEM-based local incidence angles in degrees. The data returned is CARD4L compliant with corresponding + metadata. + """ + return _process('ard_normalized_radar_backscatter', + data=data, + elevation_model=elevation_model, + contributing_area=contributing_area, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + +@openeo_process +def ard_surface_reflectance(data, atmospheric_correction_method, cloud_detection_method, elevation_model=UNSET, atmospheric_correction_options=UNSET, cloud_detection_options=UNSET) -> ProcessBuilder: + """ + CARD4L compliant Surface Reflectance generation + + :param data: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances. There must be a single dimension of type `bands` available. + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. Each method supports detecting different + atmospheric disturbances such as clouds, cloud shadows, aerosols, haze, ozone and/or water vapour in + optical imagery. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + Specifying proprietary options will reduce portability. + :param cloud_detection_options: Proprietary options for the cloud detection method. Specifying proprietary + options will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances for each spectral band in the source data + cube, with atmospheric disturbances like clouds and cloud shadows removed. No-data values (null) are + directly set in the bands. Depending on the methods used, several additional bands will be added to the + data cube: Data cube containing bottom of atmosphere reflectances for each spectral band in the source + data cube, with atmospheric disturbances like clouds and cloud shadows removed. Depending on the methods + used, several additional bands will be added to the data cube: - `date` (optional): Specifies per-pixel + acquisition timestamps. - `incomplete-testing` (required): Identifies pixels with a value of 1 for which + the per-pixel tests (at least saturation, cloud and cloud shadows, see CARD4L specification for details) + have not all been successfully completed. Otherwise, the value is 0. - `saturation` (required) / + `saturation_{band}` (optional): Indicates where pixels in the input spectral bands are saturated (1) or not + (0). If the saturation is given per band, the band names are `saturation_{band}` with `{band}` being the + band name from the source data cube. - `cloud`, `shadow` (both required),`aerosol`, `haze`, `ozone`, + `water_vapor` (all optional): Indicates the probability of pixels being an atmospheric disturbance such as + clouds. All bands have values between 0 (clear) and 1, which describes the probability that it is an + atmospheric disturbance. - `snow-ice` (optional): Points to a file that indicates whether a pixel is + assessed as being snow/ice (1) or not (0). All values describe the probability and must be between 0 and 1. + - `land-water` (optional): Indicates whether a pixel is assessed as being land (1) or water (0). All values + describe the probability and must be between 0 and 1. - `incidence-angle` (optional): Specifies per-pixel + incidence angles in degrees. - `azimuth` (optional): Specifies per-pixel azimuth angles in degrees. - `sun- + azimuth:` (optional): Specifies per-pixel sun azimuth angles in degrees. - `sun-elevation` (optional): + Specifies per-pixel sun elevation angles in degrees. - `terrain-shadow` (optional): Indicates with a value + of 1 whether a pixel is not directly illuminated due to terrain shadowing. Otherwise, the value is 0. - + `terrain-occlusion` (optional): Indicates with a value of 1 whether a pixel is not visible to the sensor + due to terrain occlusion during off-nadir viewing. Otherwise, the value is 0. - `terrain-illumination` + (optional): Contains coefficients used for terrain illumination correction are provided for each pixel. + The data returned is CARD4L compliant with corresponding metadata. + """ + return _process('ard_surface_reflectance', + data=data, + atmospheric_correction_method=atmospheric_correction_method, + cloud_detection_method=cloud_detection_method, + elevation_model=elevation_model, + atmospheric_correction_options=atmospheric_correction_options, + cloud_detection_options=cloud_detection_options + ) + + +@openeo_process +def array_append(data, value, label=UNSET) -> ProcessBuilder: + """ + Append a value to an array + + :param data: An array. + :param value: Value to append to the array. + :param label: If the given array is a labeled array, a new label for the new value should be given. If not + given or `null`, the array index as string is used as the label. If in any case the label exists, a + `LabelExists` exception is thrown. + + :return: The new array with the value being appended. + """ + return _process('array_append', data=data, value=value, label=label) + + +@openeo_process +def array_apply(data, process, context=UNSET) -> ProcessBuilder: + """ + Apply a process to each array element + + :param data: An array. + :param process: A process that accepts and returns a single value and is applied on each individual value + in the array. The process may consist of multiple sub-processes and could, for example, consist of + processes such as ``absolute()`` or ``linear_scale_range()``. + :param context: Additional data to be passed to the process. + + :return: An array with the newly computed values. The number of elements are the same as for the original + array. + """ + return _process('array_apply', + data=data, + process=build_child_callback(process, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + +@openeo_process +def array_concat(array1, array2) -> ProcessBuilder: + """ + Merge two arrays + + :param array1: The first array. + :param array2: The second array. + + :return: The merged array. + """ + return _process('array_concat', array1=array1, array2=array2) + + +@openeo_process +def array_contains(data, value) -> ProcessBuilder: + """ + Check whether the array contains a given value + + :param data: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `false`. + + :return: `true` if the list contains the value, false` otherwise. + """ + return _process('array_contains', data=data, value=value) + + +@openeo_process +def array_create(data=UNSET, repeat=UNSET) -> ProcessBuilder: + """ + Create an array + + :param data: A (native) array to fill the newly created array with. Defaults to an empty array. + :param repeat: The number of times the (native) array specified in `data` is repeatedly added after each + other to the new array being created. Defaults to `1`. + + :return: The newly created array. + """ + return _process('array_create', data=data, repeat=repeat) + + +@openeo_process +def array_create_labeled(data, labels) -> ProcessBuilder: + """ + Create a labeled array + + :param data: An array of values to be used. + :param labels: An array of labels to be used. + + :return: The newly created labeled array. + """ + return _process('array_create_labeled', data=data, labels=labels) + + +@openeo_process +def array_element(data, index=UNSET, label=UNSET, return_nodata=UNSET) -> ProcessBuilder: + """ + Get an element from an array + + :param data: An array. + :param index: The zero-based index of the element to retrieve. + :param label: The label of the element to retrieve. Throws an `ArrayNotLabeled` exception, if the given + array is not a labeled array and this parameter is set. + :param return_nodata: By default this process throws an `ArrayElementNotAvailable` exception if the index + or label is invalid. If you want to return `null` instead, set this flag to `true`. + + :return: The value of the requested element. + """ + return _process('array_element', data=data, index=index, label=label, return_nodata=return_nodata) + + +@openeo_process +def array_filter(data, condition, context=UNSET) -> ProcessBuilder: + """ + Filter an array based on a condition + + :param data: An array. + :param condition: A condition that is evaluated against each value, index and/or label in the array. Only + the array elements for which the condition returns `true` are preserved. + :param context: Additional data to be passed to the condition. + + :return: An array filtered by the specified condition. The number of elements are less than or equal + compared to the original array. + """ + return _process('array_filter', + data=data, + condition=build_child_callback(condition, parent_parameters=['x', 'index', 'label', 'context']), + context=context + ) + + +@openeo_process +def array_find(data, value, reverse=UNSET) -> ProcessBuilder: + """ + Get the index for a value in an array + + :param data: List to find the value in. + :param value: Value to find in `data`. If the value is `null`, this process returns always `null`. + :param reverse: By default, this process finds the index of the first match. To return the index of the + last match instead, set this flag to `true`. + + :return: The index of the first element with the specified value. If no element was found, `null` is + returned. + """ + return _process('array_find', data=data, value=value, reverse=reverse) + + +@openeo_process +def array_find_label(data, label) -> ProcessBuilder: + """ + Get the index for a label in a labeled array + + :param data: List to find the label in. + :param label: Label to find in `data`. + + :return: The index of the element with the specified label assigned. If no such label was found, `null` is + returned. + """ + return _process('array_find_label', data=data, label=label) + + +@openeo_process +def array_interpolate_linear(data) -> ProcessBuilder: + """ + One-dimensional linear interpolation for arrays + + :param data: An array of numbers and no-data values. If the given array is a labeled array, the labels + must have a natural/inherent label order and the process expects the labels to be sorted accordingly. This + is the default behavior in openEO for spatial and temporal dimensions. + + :return: An array with no-data values being replaced with interpolated values. If not at least 2 numerical + values are available in the array, the array stays the same. + """ + return _process('array_interpolate_linear', data=data) + + +@openeo_process +def array_labels(data) -> ProcessBuilder: + """ + Get the labels for an array + + :param data: An array. + + :return: The labels or indices as array. + """ + return _process('array_labels', data=data) + + +@openeo_process +def array_modify(data, values, index, length=UNSET) -> ProcessBuilder: + """ + Change the content of an array (remove, insert, update) + + :param data: The array to modify. + :param values: The values to insert into the `data` array. + :param index: The index in the `data` array of the element to insert the value(s) before. If the index is + greater than the number of elements in the `data` array, the process throws an `ArrayElementNotAvailable` + exception. To insert after the last element, there are two options: 1. Use the simpler processes + ``array_append()`` to append a single value or ``array_concat()`` to append multiple values. 2. Specify the + number of elements in the array. You can retrieve the number of elements with the process ``count()``, + having the parameter `condition` set to `true`. + :param length: The number of elements in the `data` array to remove (or replace) starting from the given + index. If the array contains fewer elements, the process simply removes all elements up to the end. + + :return: An array with values added, updated or removed. + """ + return _process('array_modify', data=data, values=values, index=index, length=length) + + +@openeo_process +def arsinh(x) -> ProcessBuilder: + """ + Inverse hyperbolic sine + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('arsinh', x=x) + + +@openeo_process +def artanh(x) -> ProcessBuilder: + """ + Inverse hyperbolic tangent + + :param x: A number. + + :return: The computed angle in radians. + """ + return _process('artanh', x=x) + + +@openeo_process +def atmospheric_correction(data, method, elevation_model=UNSET, options=UNSET) -> ProcessBuilder: + """ + Apply atmospheric correction + + :param data: Data cube containing multi-spectral optical top of atmosphere reflectances to be corrected. + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a + specific method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce + reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param options: Proprietary options for the atmospheric correction method. Specifying proprietary options + will reduce portability. + + :return: Data cube containing bottom of atmosphere reflectances. + """ + return _process('atmospheric_correction', data=data, method=method, elevation_model=elevation_model, options=options) + + +@openeo_process +def between(x, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison + + :param x: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return _process('between', x=x, min=min, max=max, exclude_max=exclude_max) + + +@openeo_process +def ceil(x) -> ProcessBuilder: + """ + Round fractions up + + :param x: A number to round up. + + :return: The number rounded up. + """ + return _process('ceil', x=x) + + +@openeo_process +def climatological_normal(data, period, climatology_period=UNSET) -> ProcessBuilder: + """ + Compute climatology normals + + :param data: A data cube with exactly one temporal dimension. The data cube must span at least the temporal + interval specified in the parameter `climatology-period`. Seasonal periods may span two consecutive years, + e.g. temporal winter that includes months December, January and February. If the required months before the + actual climate period are available, the season is taken into account. If not available, the first season + is not taken into account and the seasonal mean is based on one year less than the other seasonal normals. + The incomplete season at the end of the last year is never taken into account. + :param period: The time intervals to aggregate the average value for. The following pre-defined frequencies + are supported: * `day`: Day of the year * `month`: Month of the year * `climatology-period`: The period + specified in the `climatology-period`. * `season`: Three month periods of the calendar seasons (December - + February, March - May, June - August, September - November). * `tropical-season`: Six month periods of the + tropical seasons (November - April, May - October). + :param climatology_period: The climatology period as a closed temporal interval. The first element of the + array is the first year to be fully included in the temporal interval. The second element is the last year + to be fully included in the temporal interval. The default climatology period is from 1981 until 2010 + (both inclusive) right now, but this might be updated over time to what is commonly used in climatology. If + you don't want to keep your research to be reproducible, please explicitly specify a period. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except for the resolution and dimension labels of the temporal + dimension. The temporal dimension has the following dimension labels: * `day`: `001` - `365` * `month`: + `01` - `12` * `climatology-period`: `climatology-period` * `season`: `djf` (December - February), `mam` + (March - May), `jja` (June - August), `son` (September - November) * `tropical-season`: `ndjfma` (November + - April), `mjjaso` (May - October) + """ + return _process('climatological_normal', data=data, period=period, climatology_period=climatology_period) + + +@openeo_process +def clip(x, min, max) -> ProcessBuilder: + """ + Clip a value between a minimum and a maximum + + :param x: A number. + :param min: Minimum value. If the value is lower than this value, the process will return the value of this + parameter. + :param max: Maximum value. If the value is greater than this value, the process will return the value of + this parameter. + + :return: The value clipped to the specified range. + """ + return _process('clip', x=x, min=min, max=max) + + +@openeo_process +def cloud_detection(data, method, options=UNSET) -> ProcessBuilder: + """ + Create cloud masks + + :param data: The source data cube containing multi-spectral optical top of the atmosphere (TOA) + reflectances on which to perform cloud detection. + :param method: The cloud detection method to use. To get reproducible results, you have to set a specific + method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce + reproducibility as you *may* get different results if you run the processes multiple times. + :param options: Proprietary options for the cloud detection method. Specifying proprietary options will + reduce portability. + + :return: A data cube with bands for the atmospheric disturbances. Each of the masks contains values between + 0 and 1. The data cube has the same spatial and temporal dimensions as the source data cube and a dimension + that contains a dimension label for each of the supported/considered atmospheric disturbance. + """ + return _process('cloud_detection', data=data, method=method, options=options) + + +@openeo_process +def constant(x) -> ProcessBuilder: + """ + Define a constant value + + :param x: The value of the constant. + + :return: The value of the constant. + """ + return _process('constant', x=x) + + +@openeo_process +def cos(x) -> ProcessBuilder: + """ + Cosine + + :param x: An angle in radians. + + :return: The computed cosine of `x`. + """ + return _process('cos', x=x) + + +@openeo_process +def cosh(x) -> ProcessBuilder: + """ + Hyperbolic cosine + + :param x: An angle in radians. + + :return: The computed hyperbolic cosine of `x`. + """ + return _process('cosh', x=x) + + +@openeo_process +def count(data, condition=UNSET, context=UNSET) -> ProcessBuilder: + """ + Count the number of elements + + :param data: An array with elements of any data type. + :param condition: A condition consists of one or more processes, which in the end return a boolean value. + It is evaluated against each element in the array. An element is counted only if the condition returns + `true`. Defaults to count valid elements in a list (see ``is_valid()``). Setting this parameter to boolean + `true` counts all elements in the list. `false` is not a valid value for this parameter. + :param context: Additional data to be passed to the condition. + + :return: The counted number of elements. + """ + return _process('count', data=data, condition=condition, context=context) + + +@openeo_process +def create_data_cube() -> ProcessBuilder: + """ + Create an empty data cube + + :return: An empty data cube with no dimensions. + """ + return _process('create_data_cube', ) + + +@openeo_process +def cummax(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative maxima + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative maxima. + """ + return _process('cummax', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def cummin(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative minima + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative minima. + """ + return _process('cummin', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def cumproduct(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative products + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative products. + """ + return _process('cumproduct', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def cumsum(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Cumulative sums + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not and ignores them by default. + Setting this flag to `false` considers no-data values so that `null` is set for all the following elements. + + :return: An array with the computed cumulative sums. + """ + return _process('cumsum', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def date_between(x, min, max, exclude_max=UNSET) -> ProcessBuilder: + """ + Between comparison for dates and times + + :param x: The value to check. + :param min: Lower boundary (inclusive) to check against. + :param max: Upper boundary (inclusive) to check against. + :param exclude_max: Exclude the upper boundary `max` if set to `true`. Defaults to `false`. + + :return: `true` if `x` is between the specified bounds, otherwise `false`. + """ + return _process('date_between', x=x, min=min, max=max, exclude_max=exclude_max) + + +@openeo_process +def date_difference(date1, date2, unit=UNSET) -> ProcessBuilder: + """ + Computes the difference between two time instants + + :param date1: The base date, optionally with a time component. + :param date2: The other date, optionally with a time component. + :param unit: The unit for the returned value. The following units are available: - millisecond - second - + leap seconds are ignored in computations. - minute - hour - day - month - year + + :return: Returns the difference between date1 and date2 in the given unit (seconds by default), including a + fractional part if required. For comparison purposes this means: - If `date1` < `date2`, the returned + value is positive. - If `date1` = `date2`, the returned value is 0. - If `date1` > `date2`, the returned + value is negative. + """ + return _process('date_difference', date1=date1, date2=date2, unit=unit) + + +@openeo_process +def date_shift(date, value, unit) -> ProcessBuilder: + """ + Manipulates dates and times by addition or subtraction + + :param date: The date (and optionally time) to manipulate. If the given date doesn't include the time, the + process assumes that the time component is `00:00:00Z` (i.e. midnight, in UTC). The millisecond part of the + time is optional and defaults to `0` if not given. + :param value: The period of time in the unit given that is added (positive numbers) or subtracted (negative + numbers). The value `0` doesn't have any effect. + :param unit: The unit for the value given. The following pre-defined units are available: - millisecond: + Milliseconds - second: Seconds - leap seconds are ignored in computations. - minute: Minutes - hour: Hours + - day: Days - changes only the the day part of a date - week: Weeks (equivalent to 7 days) - month: Months + - year: Years Manipulations with the unit `year`, `month`, `week` or `day` do never change the time. If + any of the manipulations result in an invalid date or time, the corresponding part is rounded down to the + next valid date or time respectively. For example, adding a month to `2020-01-31` would result in + `2020-02-29`. + + :return: The manipulated date. If a time component was given in the parameter `date`, the time component is + returned with the date. + """ + return _process('date_shift', date=date, value=value, unit=unit) + + +@openeo_process +def dimension_labels(data, dimension) -> ProcessBuilder: + """ + Get the dimension labels + + :param data: The data cube. + :param dimension: The name of the dimension to get the labels for. + + :return: The labels as an array. + """ + return _process('dimension_labels', data=data, dimension=dimension) + + +@openeo_process +def divide(x, y) -> ProcessBuilder: + """ + Division of two numbers + + :param x: The dividend. + :param y: The divisor. + + :return: The computed result. + """ + return _process('divide', x=x, y=y) + + +@openeo_process +def drop_dimension(data, name) -> ProcessBuilder: + """ + Remove a dimension + + :param data: The data cube to drop a dimension from. + :param name: Name of the dimension to drop. + + :return: A data cube without the specified dimension. The number of dimensions decreases by one, but the + dimension properties (name, type, labels, reference system and resolution) for all other dimensions remain + unchanged. + """ + return _process('drop_dimension', data=data, name=name) + + +@openeo_process +def e() -> ProcessBuilder: + """ + Euler's number (e) + + :return: The numerical value of Euler's number. + """ + return _process('e', ) + + +@openeo_process +def eq(x, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Equal to comparison + + :param x: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a positive + non-zero number the equality of two numbers is checked against a delta value. This is especially useful to + circumvent problems with floating-point inaccuracy in machine-based computation. This option is basically + an alias for the following computation: `lte(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be disabled + by setting this parameter to `false`. + + :return: `true` if `x` is equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('eq', x=x, y=y, delta=delta, case_sensitive=case_sensitive) + + +@openeo_process +def exp(p) -> ProcessBuilder: + """ + Exponentiation to the base e + + :param p: The numerical exponent. + + :return: The computed value for *e* raised to the power of `p`. + """ + return _process('exp', p=p) + + +@openeo_process +def extrema(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum and maximum values + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that an array with two `null` values is returned if any + value is such a value. + + :return: An array containing the minimum and maximum values for the specified numbers. The first element is + the minimum, the second element is the maximum. If the input array is empty both elements are set to + `null`. + """ + return _process('extrema', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def filter_bands(data, bands=UNSET, wavelengths=UNSET) -> ProcessBuilder: + """ + Filter the bands by names + + :param data: A data cube with bands. + :param bands: A list of band names. Either the unique band name (metadata field `name` in bands) or one of + the common band names (metadata field `common_name` in bands). If the unique band name and the common name + conflict, the unique band name has a higher priority. The order of the specified array defines the order + of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the + original order. + :param wavelengths: A list of sub-lists with each sub-list consisting of two elements. The first element is + the minimum wavelength and the second element is the maximum wavelength. Wavelengths are specified in + micrometers (μm). The order of the specified array defines the order of the bands in the data cube. If + multiple bands match the wavelengths, all matched bands are included in the original order. + + :return: A data cube limited to a subset of its original bands. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the dimension of type + `bands` has less (or the same) dimension labels. + """ + return _process('filter_bands', data=data, bands=bands, wavelengths=wavelengths) + + +@openeo_process +def filter_bbox(data, extent) -> ProcessBuilder: + """ + Spatial filter using a bounding box + + :param data: A data cube. + :param extent: A bounding box, which may include a vertical axis (see `base` and `height`). + + :return: A data cube restricted to the bounding box. The dimensions and dimension properties (name, type, + labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less (or + the same) dimension labels. + """ + return _process('filter_bbox', data=data, extent=extent) + + +@openeo_process +def filter_labels(data, condition, dimension, context=UNSET) -> ProcessBuilder: + """ + Filter dimension labels based on a condition + + :param data: A data cube. + :param condition: A condition that is evaluated against each dimension label in the specified dimension. A + dimension label and the corresponding data is preserved for the given dimension, if the condition returns + `true`. + :param dimension: The name of the dimension to filter on. Fails with a `DimensionNotAvailable` exception if + the specified dimension does not exist. + :param context: Additional data to be passed to the condition. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that the given dimension has less (or the same) dimension + labels. + """ + return _process('filter_labels', + data=data, + condition=build_child_callback(condition, parent_parameters=['value', 'context']), + dimension=dimension, + context=context + ) + + +@openeo_process +def filter_spatial(data, geometries) -> ProcessBuilder: + """ + Spatial filter raster data cubes using geometries + + :param data: A raster data cube. + :param geometries: One or more geometries used for filtering, given as GeoJSON or vector data cube. If + multiple geometries are provided, the union of them is used. Empty geometries are ignored. Limits the data + cube to the bounding box of the given geometries. No implicit masking gets applied. To mask the pixels of + the data cube use ``mask_polygon()``. + + :return: A raster data cube restricted to the specified geometries. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions + have less (or the same) dimension labels. + """ + return _process('filter_spatial', data=data, geometries=geometries) + + +@openeo_process +def filter_temporal(data, extent, dimension=UNSET) -> ProcessBuilder: + """ + Temporal filter based on temporal intervals + + :param data: A data cube. + :param extent: Left-closed temporal interval, i.e. an array with exactly two elements: 1. The first + element is the start of the temporal interval. The specified time instant is **included** in the interval. + 2. The second element is the end of the temporal interval. The specified time instant is **excluded** from + the interval. The second element must always be greater/later than the first element. Otherwise, a + `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by setting one of the + boundaries to `null`, but never both. + :param dimension: The name of the temporal dimension to filter on. If no specific dimension is specified, + the filter applies to all temporal dimensions. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + + :return: A data cube restricted to the specified temporal extent. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the temporal dimensions + (determined by `dimensions` parameter) may have less dimension labels. + """ + return _process('filter_temporal', data=data, extent=extent, dimension=dimension) + + +@openeo_process +def filter_vector(data, geometries, relation=UNSET) -> ProcessBuilder: + """ + Spatial vector filter using geometries + + :param data: A vector data cube with the candidate geometries. + :param geometries: One or more base geometries used for filtering, given as vector data cube. If multiple + base geometries are provided, the union of them is used. + :param relation: The spatial filter predicate for comparing the geometries provided through (a) + `geometries` (base geometries) and (b) `data` (candidate geometries). + + :return: A vector data cube restricted to the specified geometries. The dimensions and dimension properties + (name, type, labels, reference system and resolution) remain unchanged, except that the geometries + dimension has less (or the same) dimension labels. + """ + return _process('filter_vector', data=data, geometries=geometries, relation=relation) + + +@openeo_process +def first(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + First element + + :param data: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if the first value is such a + value. + + :return: The first element of the input array. + """ + return _process('first', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def fit_curve(data, parameters, function, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Curve fitting + + :param data: A labeled array, the labels correspond to the variable `y` and the values correspond to the + variable `x`. + :param parameters: Defined the number of parameters for the model function and provides an initial guess + for them. At least one parameter is required. + :param function: The model function. It must take the parameters to fit as array through the first argument + and the independent variable `x` as the second argument. It is recommended to store the model function as + a user-defined process on the back-end to be able to re-use the model function with the computed optimal + values for the parameters afterwards. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is passed to the model function. + + :return: An array with the optimal values for the parameters. + """ + return _process('fit_curve', + data=data, + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + ignore_nodata=ignore_nodata + ) + + +@openeo_process +def flatten_dimensions(data, dimensions, target_dimension, label_separator=UNSET) -> ProcessBuilder: + """ + Combine multiple dimensions into a single dimension + + :param data: A data cube. + :param dimensions: The names of the dimension to combine. The order of the array defines the order in which + the dimension labels and values are combined (see the example in the process description). Fails with a + `DimensionNotAvailable` exception if at least one of the specified dimensions does not exist. + :param target_dimension: The name of the new target dimension. A new dimensions will be created with the + given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` exception if a + dimension with the specified name exists. + :param label_separator: The string that will be used as a separator for the concatenated dimension labels. + To unambiguously revert the dimension labels with the process ``unflatten_dimension()``, the given string + must not be contained in any of the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('flatten_dimensions', data=data, dimensions=dimensions, target_dimension=target_dimension, label_separator=label_separator) + + +@openeo_process +def floor(x) -> ProcessBuilder: + """ + Round fractions down + + :param x: A number to round down. + + :return: The number rounded down. + """ + return _process('floor', x=x) + + +@openeo_process +def gt(x, y) -> ProcessBuilder: + """ + Greater than comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly greater than `y` or `null` if any operand is `null`, otherwise `false`. + """ + return _process('gt', x=x, y=y) + + +@openeo_process +def gte(x, y) -> ProcessBuilder: + """ + Greater than or equal to comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is greater than or equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('gte', x=x, y=y) + + +@openeo_process +def if_(value, accept, reject=UNSET) -> ProcessBuilder: + """ + If-Then-Else conditional + + :param value: A boolean value. + :param accept: A value that is returned if the boolean value is `true`. + :param reject: A value that is returned if the boolean value is **not** `true`. Defaults to `null`. + + :return: Either the `accept` or `reject` argument depending on the given boolean value. + """ + return _process('if', value=value, accept=accept, reject=reject) + + +@openeo_process +def inspect(data, message=UNSET, code=UNSET, level=UNSET) -> ProcessBuilder: + """ + Add information to the logs + + :param data: Data to log. + :param message: A message to send in addition to the data. + :param code: A label to help identify one or more log entries originating from this process in the list of + all log entries. It can help to group or filter log entries and is usually not unique. + :param level: The severity level of this message, defaults to `info`. + + :return: The data as passed to the `data` parameter without any modification. + """ + return _process('inspect', data=data, message=message, code=code, level=level) + + +@openeo_process +def int(x) -> ProcessBuilder: + """ + Integer part of a number + + :param x: A number. + + :return: Integer part of the number. + """ + return _process('int', x=x) + + +@openeo_process +def is_infinite(x) -> ProcessBuilder: + """ + Value is an infinite number + + :param x: The data to check. + + :return: `true` if the data is an infinite number, otherwise `false`. + """ + return _process('is_infinite', x=x) + + +@openeo_process +def is_nan(x) -> ProcessBuilder: + """ + Value is not a number + + :param x: The data to check. + + :return: Returns `true` for `NaN` and all non-numeric data types, otherwise returns `false`. + """ + return _process('is_nan', x=x) + + +@openeo_process +def is_nodata(x) -> ProcessBuilder: + """ + Value is a no-data value + + :param x: The data to check. + + :return: `true` if the data is a no-data value, otherwise `false`. + """ + return _process('is_nodata', x=x) + + +@openeo_process +def is_valid(x) -> ProcessBuilder: + """ + Value is valid data + + :param x: The data to check. + + :return: `true` if the data is valid, otherwise `false`. + """ + return _process('is_valid', x=x) + + +@openeo_process +def last(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Last element + + :param data: An array with elements of any data type. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if the last value is such a value. + + :return: The last element of the input array. + """ + return _process('last', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def linear_scale_range(x, inputMin, inputMax, outputMin=UNSET, outputMax=UNSET) -> ProcessBuilder: + """ + Linear transformation between two ranges + + :param x: A number to transform. The number gets clipped to the bounds specified in `inputMin` and + `inputMax`. + :param inputMin: Minimum value the input can obtain. + :param inputMax: Maximum value the input can obtain. + :param outputMin: Minimum value of the desired output range. + :param outputMax: Maximum value of the desired output range. + + :return: The transformed number. + """ + return _process('linear_scale_range', x=x, inputMin=inputMin, inputMax=inputMax, outputMin=outputMin, outputMax=outputMax) + + +@openeo_process +def ln(x) -> ProcessBuilder: + """ + Natural logarithm + + :param x: A number to compute the natural logarithm for. + + :return: The computed natural logarithm. + """ + return _process('ln', x=x) + + +@openeo_process +def load_collection(id, spatial_extent, temporal_extent, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Load a collection + + :param id: The collection id. + :param spatial_extent: Limits the data to load from the collection to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard + by the OGC). * For vector data, the process loads the geometry into the data cube if the geometry is fully + *within* the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be + one of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. * Empty geometries are ignored. Set this parameter to `null` to + set no limit for the spatial extent. Be careful with this when loading large datasets! It is recommended to + use this parameter instead of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading + unbounded data. + :param temporal_extent: Limits the data to load from the collection to the specified left-closed temporal + interval. Applies to all temporal dimensions. The interval has to be specified as an array with exactly two + elements: 1. The first element is the start of the temporal interval. The specified time instant is + **included** in the interval. 2. The second element is the end of the temporal interval. The specified time + instant is **excluded** from the interval. The second element must always be greater/later than the first + element. Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports unbounded intervals by + setting one of the boundaries to `null`, but never both. Set this parameter to `null` to set no limit for + the temporal extent. Be careful with this when loading large datasets! It is recommended to use this + parameter instead of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + :param properties: Limits the data by metadata properties to include only data in the data cube which all + given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the name of + the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value + must be a condition (user-defined process) to be evaluated against the collection metadata, see the + example. + + :return: A data cube for further processing. The dimensions and dimension properties (name, type, labels, + reference system and resolution) correspond to the collection's metadata, but the dimension labels are + restricted as specified in the parameters. + """ + return _process('load_collection', id=id, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + +@openeo_process +def load_geojson(data, properties=UNSET) -> ProcessBuilder: + """ + Converts GeoJSON into a vector data cube + + :param data: A GeoJSON object to convert into a vector data cube. The GeoJSON type `GeometryCollection` is + not supported. Each geometry in the GeoJSON data results in a dimension label in the `geometries` + dimension. + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. A + new dimension with the name `properties` and type `other` is created if at least one property is provided. + Only applies for GeoJSON Features and FeatureCollections. Missing values are generally set to no-data + (`null`). Depending on the number of properties provided, the process creates the dimension differently: + - Single property with scalar values: A single dimension label with the name of the property and a single + value per geometry. - Single property of type array: The dimension labels correspond to the array indices. + There are as many values and labels per geometry as there are for the largest array. - Multiple properties + with scalar values: The dimension labels correspond to the property names. There are as many values and + labels per geometry as there are properties provided here. + + :return: A vector data cube containing the geometries, either one or two dimensional. + """ + return _process('load_geojson', data=data, properties=properties) + + +@openeo_process +def load_ml_model(id) -> ProcessBuilder: + """ + Load a ML model + + :param id: The STAC Item to load the machine learning model from. The STAC Item must implement the `ml- + model` extension. + + :return: A machine learning model to be used with machine learning processes such as + ``predict_random_forest()``. + """ + return _process('load_ml_model', id=id) + + +@openeo_process +def load_result(id, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET) -> ProcessBuilder: + """ + Load batch job results + + :param id: The id of a batch job with results. + :param spatial_extent: Limits the data to load from the batch job result to the specified bounding box or + polygons. * For raster data, the process loads the pixel into the data cube if the point at the pixel + center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard + by the OGC). * For vector data, the process loads the geometry into the data cube of the geometry is fully + within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. The GeoJSON can be + one of the following feature types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a + `Polygon` or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with + `Polygon` or `MultiPolygon` geometries. Set this parameter to `null` to set no limit for the spatial + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load from the batch job result to the specified left-closed + temporal interval. Applies to all temporal dimensions. The interval has to be specified as an array with + exactly two elements: 1. The first element is the start of the temporal interval. The specified instance + in time is **included** in the interval. 2. The second element is the end of the temporal interval. The + specified instance in time is **excluded** from the interval. The specified temporal strings follow [RFC + 3339](https://www.rfc-editor.org/rfc/rfc3339.html). Also supports open intervals by setting one of the + boundaries to `null`, but never both. Set this parameter to `null` to set no limit for the temporal + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :return: A data cube for further processing. + """ + return _process('load_result', id=id, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands) + + +@openeo_process +def load_stac(url, spatial_extent=UNSET, temporal_extent=UNSET, bands=UNSET, properties=UNSET) -> ProcessBuilder: + """ + Loads data from STAC + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) or a specific + STAC API Collection that allows to filter items and to download assets. This includes batch job results, + which itself are compliant to STAC. For external URLs, authentication details such as API keys or tokens + may need to be included in the URL. Batch job results can be specified in two ways: - For Batch job + results at the same back-end, a URL pointing to the corresponding batch job results endpoint should be + provided. The URL usually ends with `/jobs/{id}/results` and `{id}` is the corresponding batch job ID. - + For external results, a signed URL must be provided. Not all back-ends support signed URLs, which are + provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: Limits the data to load to the specified bounding box or polygons. * For raster + data, the process loads the pixel into the data cube if the point at the pixel center intersects with the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). * For vector + data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or + any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be + in the data cube if no spatial extent has been provided. The GeoJSON can be one of the following feature + types: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` or `MultiPolygon` + geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or `MultiPolygon` + geometries. Set this parameter to `null` to set no limit for the spatial extent. Be careful with this when + loading large datasets! It is recommended to use this parameter instead of using ``filter_bbox()`` or + ``filter_spatial()`` directly after loading unbounded data. + :param temporal_extent: Limits the data to load to the specified left-closed temporal interval. Applies to + all temporal dimensions. The interval has to be specified as an array with exactly two elements: 1. The + first element is the start of the temporal interval. The specified instance in time is **included** in the + interval. 2. The second element is the end of the temporal interval. The specified instance in time is + **excluded** from the interval. The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. Also supports open intervals by setting one of the + boundaries to `null`, but never both. Set this parameter to `null` to set no limit for the temporal + extent. Be careful with this when loading large datasets! It is recommended to use this parameter instead + of using ``filter_temporal()`` directly after loading unbounded data. + :param bands: Only adds the specified bands into the data cube so that bands that don't match the list of + band names are not available. Applies to all dimensions of type `bands`. Either the unique band name + (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands) + can be specified. If the unique band name and the common name conflict, the unique band name has a higher + priority. The order of the specified array defines the order of the bands in the data cube. If multiple + bands match a common name, all matched bands are included in the original order. It is recommended to use + this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + :param properties: Limits the data by metadata properties to include only data in the data cube which all + given conditions return `true` for (AND operation). Specify key-value-pairs with the key being the name of + the metadata property, which can be retrieved with the openEO Data Discovery for Collections. The value + must be a condition (user-defined process) to be evaluated against a STAC API. This parameter is not + supported for static STAC. + + :return: A data cube for further processing. + """ + return _process('load_stac', url=url, spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=bands, properties=properties) + + +@openeo_process +def load_uploaded_files(paths, format, options=UNSET) -> ProcessBuilder: + """ + Load files from the user workspace + + :param paths: The files to read. Folders can't be specified, specify all files instead. An exception is + thrown if a file can't be read. + :param format: The file format to read from. It must be one of the values that the server reports as + supported input file formats, which usually correspond to the short GDAL/OGR codes. If the format is not + suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter is *case + insensitive*. + :param options: The file format parameters to be used to read the files. Must correspond to the parameters + that the server reports as supported parameters for the chosen `format`. The parameter names and valid + values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return _process('load_uploaded_files', paths=paths, format=format, options=options) + + +@openeo_process +def load_url(url, format, options=UNSET) -> ProcessBuilder: + """ + Load data from a URL + + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included + in the URL. + :param format: The file format to use when loading the data. It must be one of the values that the server + reports as supported input file formats, which usually correspond to the short GDAL/OGR codes. If the + format is not suitable for loading the data, a `FormatUnsuitable` exception will be thrown. This parameter + is *case insensitive*. + :param options: The file format parameters to use when reading the data. Must correspond to the parameters + that the server reports as supported parameters for the chosen `format`. The parameter names and valid + values usually correspond to the GDAL/OGR format options. + + :return: A data cube for further processing. + """ + return _process('load_url', url=url, format=format, options=options) + + +@openeo_process +def log(x, base) -> ProcessBuilder: + """ + Logarithm to a base + + :param x: A number to compute the logarithm for. + :param base: The numerical base. + + :return: The computed logarithm. + """ + return _process('log', x=x, base=base) + + +@openeo_process +def lt(x, y) -> ProcessBuilder: + """ + Less than comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is strictly less than `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('lt', x=x, y=y) + + +@openeo_process +def lte(x, y) -> ProcessBuilder: + """ + Less than or equal to comparison + + :param x: First operand. + :param y: Second operand. + + :return: `true` if `x` is less than or equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('lte', x=x, y=y) + + +@openeo_process +def mask(data, mask, replacement=UNSET) -> ProcessBuilder: + """ + Apply a raster mask + + :param data: A raster data cube. + :param mask: A mask as a raster data cube. Every pixel in `data` must have a corresponding element in + `mask`. + :param replacement: The value used to replace masked values with. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged. + """ + return _process('mask', data=data, mask=mask, replacement=replacement) + + +@openeo_process +def mask_polygon(data, mask, replacement=UNSET, inside=UNSET) -> ProcessBuilder: + """ + Apply a polygon mask + + :param data: A raster data cube. + :param mask: A GeoJSON object or a vector data cube containing at least one polygon. The provided vector + data can be one of the following: * A `Polygon` or `MultiPolygon` geometry, * a `Feature` with a `Polygon` + or `MultiPolygon` geometry, or * a `FeatureCollection` containing at least one `Feature` with `Polygon` or + `MultiPolygon` geometries. * Empty geometries are ignored. + :param replacement: The value used to replace masked values with. + :param inside: If set to `true` all pixels for which the point at the pixel center **does** intersect with + any polygon are replaced. + + :return: A masked raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged. + """ + return _process('mask_polygon', data=data, mask=mask, replacement=replacement, inside=inside) + + +@openeo_process +def max(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Maximum value + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The maximum value. + """ + return _process('max', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def mean(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Arithmetic mean (average) + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed arithmetic mean. + """ + return _process('mean', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def median(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Statistical median + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed statistical median. + """ + return _process('median', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def merge_cubes(cube1, cube2, overlap_resolver=UNSET, context=UNSET) -> ProcessBuilder: + """ + Merge two data cubes + + :param cube1: The base data cube. + :param cube2: The other data cube to be merged with the base data cube. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The reducer + must return a value of the same data type as the input values are. The reduction operator may be a single + process such as ``multiply()`` or consist of multiple sub-processes. `null` (the default) can be specified + if no overlap resolver is required. + :param context: Additional data to be passed to the overlap resolver. + + :return: The merged data cube. See the process description for details regarding the dimensions and + dimension properties (name, type, labels, reference system and resolution). + """ + return _process('merge_cubes', + cube1=cube1, + cube2=cube2, + overlap_resolver=(build_child_callback(overlap_resolver, parent_parameters=['x', 'y', 'context']) if overlap_resolver not in [None, UNSET] else overlap_resolver), + context=context + ) + + +@openeo_process +def min(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Minimum value + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The minimum value. + """ + return _process('min', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def mod(x, y) -> ProcessBuilder: + """ + Modulo + + :param x: A number to be used as the dividend. + :param y: A number to be used as the divisor. + + :return: The remainder after division. + """ + return _process('mod', x=x, y=y) + + +@openeo_process +def multiply(x, y) -> ProcessBuilder: + """ + Multiplication of two numbers + + :param x: The multiplier. + :param y: The multiplicand. + + :return: The computed product of the two numbers. + """ + return _process('multiply', x=x, y=y) + + +@openeo_process +def nan() -> ProcessBuilder: + """ + Not a Number (NaN) + + :return: Returns `NaN`. + """ + return _process('nan', ) + + +@openeo_process +def ndvi(data, nir=UNSET, red=UNSET, target_band=UNSET) -> ProcessBuilder: + """ + Normalized Difference Vegetation Index + + :param data: A raster data cube with two bands that have the common names `red` and `nir` assigned. + :param nir: The name of the NIR band. Defaults to the band that has the common name `nir` assigned. Either + the unique band name (metadata field `name` in bands) or one of the common band names (metadata field + `common_name` in bands) can be specified. If the unique band name and the common name conflict, the unique + band name has a higher priority. + :param red: The name of the red band. Defaults to the band that has the common name `red` assigned. Either + the unique band name (metadata field `name` in bands) or one of the common band names (metadata field + `common_name` in bands) can be specified. If the unique band name and the common name conflict, the unique + band name has a higher priority. + :param target_band: By default, the dimension of type `bands` is dropped. To keep the dimension specify a + new band name in this parameter so that a new dimension label with the specified name will be added for the + computed values. + + :return: A raster data cube containing the computed NDVI values. The structure of the data cube differs + depending on the value passed to `target_band`: * `target_band` is `null`: The data cube does not contain + the dimension of type `bands`, the number of dimensions decreases by one. The dimension properties (name, + type, labels, reference system and resolution) for all other dimensions remain unchanged. * `target_band` + is a string: The data cube keeps the same dimensions. The dimension properties remain unchanged, but the + number of dimension labels for the dimension of type `bands` increases by one. The additional label is + named as specified in `target_band`. + """ + return _process('ndvi', data=data, nir=nir, red=red, target_band=target_band) + + +@openeo_process +def neq(x, y, delta=UNSET, case_sensitive=UNSET) -> ProcessBuilder: + """ + Not equal to comparison + + :param x: First operand. + :param y: Second operand. + :param delta: Only applicable for comparing two numbers. If this optional parameter is set to a positive + non-zero number the non-equality of two numbers is checked against a delta value. This is especially useful + to circumvent problems with floating-point inaccuracy in machine-based computation. This option is + basically an alias for the following computation: `gt(abs(minus([x, y]), delta)` + :param case_sensitive: Only applicable for comparing two strings. Case sensitive comparison can be disabled + by setting this parameter to `false`. + + :return: `true` if `x` is *not* equal to `y`, `null` if any operand is `null`, otherwise `false`. + """ + return _process('neq', x=x, y=y, delta=delta, case_sensitive=case_sensitive) + + +@openeo_process +def normalized_difference(x, y) -> ProcessBuilder: + """ + Normalized difference + + :param x: The value for the first band. + :param y: The value for the second band. + + :return: The computed normalized difference. + """ + return _process('normalized_difference', x=x, y=y) + + +@openeo_process +def not_(x) -> ProcessBuilder: + """ + Inverting a boolean + + :param x: Boolean value to invert. + + :return: Inverted boolean value. + """ + return _process('not', x=x) + + +@openeo_process +def or_(x, y) -> ProcessBuilder: + """ + Logical OR + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical OR. + """ + return _process('or', x=x, y=y) + + +@openeo_process +def order(data, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Get the order of array elements + + :param data: An array to compute the order for. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set to + `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The computed permutation. + """ + return _process('order', data=data, asc=asc, nodata=nodata) + + +@openeo_process +def pi() -> ProcessBuilder: + """ + Pi (π) + + :return: The numerical value of Pi. + """ + return _process('pi', ) + + +@openeo_process +def power(base, p) -> ProcessBuilder: + """ + Exponentiation + + :param base: The numerical base. + :param p: The numerical exponent. + + :return: The computed value for `base` raised to the power of `p`. + """ + return _process('power', base=base, p=p) + + +@openeo_process +def predict_curve(parameters, function, dimension, labels=UNSET) -> ProcessBuilder: + """ + Predict values + + :param parameters: A data cube with optimal values, e.g. computed by the process ``fit_curve()``. + :param function: The model function. It must take the parameters to fit as array through the first argument + and the independent variable `x` as the second argument. It is recommended to store the model function as + a user-defined process on the back-end. + :param dimension: The name of the dimension for predictions. + :param labels: The labels to predict values for. If no labels are given, predicts values only for no-data + (`null`) values in the data cube. + + :return: A data cube with the predicted values with the provided dimension `dimension` having as many + labels as provided through `labels`. + """ + return _process('predict_curve', + parameters=parameters, + function=build_child_callback(function, parent_parameters=['x', 'parameters']), + dimension=dimension, + labels=labels + ) + + +@openeo_process +def predict_random_forest(data, model) -> ProcessBuilder: + """ + Predict values based on a Random Forest model + + :param data: An array of numbers. + :param model: A model object that can be trained with the processes ``fit_regr_random_forest()`` + (regression) and ``fit_class_random_forest()`` (classification). + + :return: The predicted value. Returns `null` if any of the given values in the array is a no-data value. + """ + return _process('predict_random_forest', data=data, model=model) + + +@openeo_process +def product(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the product by multiplying numbers + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed product of the sequence of numbers. + """ + return _process('product', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def quantiles(data, probabilities=UNSET, q=UNSET, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Quantiles + + :param data: An array of numbers. + :param probabilities: Quantiles to calculate. Either a list of probabilities or the number of intervals: * + Provide an array with a sorted list of probabilities in ascending order to calculate quantiles for. The + probabilities must be between 0 and 1 (inclusive). If not sorted in ascending order, an + `AscendingProbabilitiesRequired` exception is thrown. * Provide an integer to specify the number of + intervals to calculate quantiles for. Calculates q-quantiles with equal-sized intervals. + :param q: Number of intervals to calculate quantiles for. Calculates q-quantiles with equal-sized + intervals. This parameter has been **deprecated**. Please use the parameter `probabilities` instead. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that an array with `null` values is returned if any + element is such a value. + + :return: An array with the computed quantiles. The list has either * as many elements as the given list of + `probabilities` had or * *`q`-1* elements. If the input array is empty the resulting array is filled with + as many `null` values as required according to the list above. See the 'Empty array' example for an + example. + """ + return _process('quantiles', data=data, probabilities=probabilities, q=q, ignore_nodata=ignore_nodata) + + +@openeo_process +def rearrange(data, order) -> ProcessBuilder: + """ + Sort an array based on a permutation + + :param data: The array to rearrange. + :param order: The permutation used for rearranging. + + :return: The rearranged array. + """ + return _process('rearrange', data=data, order=order) + + +@openeo_process +def reduce_dimension(data, reducer, dimension, context=UNSET) -> ProcessBuilder: + """ + Reduce dimensions + + :param data: A data cube. + :param reducer: A reducer to apply on the specified dimension. A reducer is a single process such as + ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param dimension: The name of the dimension over which to reduce. Fails with a `DimensionNotAvailable` + exception if the specified dimension does not exist. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the given dimension, the number of + dimensions decreases by one. The dimension properties (name, type, labels, reference system and resolution) + for all other dimensions remain unchanged. + """ + return _process('reduce_dimension', + data=data, + reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), + dimension=dimension, + context=context + ) + + +@openeo_process +def reduce_spatial(data, reducer, context=UNSET) -> ProcessBuilder: + """ + Reduce spatial dimensions 'x' and 'y' + + :param data: A raster data cube. + :param reducer: A reducer to apply on the horizontal spatial dimensions. A reducer is a single process such + as ``mean()`` or a set of processes, which computes a single value for a list of values, see the category + 'reducer' for such processes. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the newly computed values. It is missing the horizontal spatial dimensions, the + number of dimensions decreases by two. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('reduce_spatial', data=data, reducer=build_child_callback(reducer, parent_parameters=['data', 'context']), context=context) + + +@openeo_process +def rename_dimension(data, source, target) -> ProcessBuilder: + """ + Rename a dimension + + :param data: The data cube. + :param source: The current name of the dimension. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a `DimensionExists` exception if a dimension with + the specified name exists. + + :return: A data cube with the same dimensions, but the name of one of the dimensions changes. The old name + can not be referred to any longer. The dimension properties (name, type, labels, reference system and + resolution) remain unchanged. + """ + return _process('rename_dimension', data=data, source=source, target=target) + + +@openeo_process +def rename_labels(data, dimension, target, source=UNSET) -> ProcessBuilder: + """ + Rename dimension labels + + :param data: The data cube. + :param dimension: The name of the dimension to rename the labels for. + :param target: The new names for the labels. If a target dimension label already exists in the data cube, + a `LabelExists` exception is thrown. + :param source: The original names of the labels to be renamed to corresponding array elements in the + parameter `target`. It is allowed to only specify a subset of labels to rename, as long as the `target` and + `source` parameter have the same length. The order of the labels doesn't need to match the order of the + dimension labels in the data cube. By default, the array is empty so that the dimension labels in the data + cube are expected to be enumerated. If the dimension labels are not enumerated and the given array is + empty, the `LabelsNotEnumerated` exception is thrown. If one of the source dimension labels doesn't exist, + the `LabelNotAvailable` exception is thrown. + + :return: The data cube with the same dimensions. The dimension properties (name, type, labels, reference + system and resolution) remain unchanged, except that for the given dimension the labels change. The old + labels can not be referred to any longer. The number of labels remains the same. + """ + return _process('rename_labels', data=data, dimension=dimension, target=target, source=source) + + +@openeo_process +def resample_cube_spatial(data, target, method=UNSET) -> ProcessBuilder: + """ + Resample the spatial dimensions to match a target data cube + + :param data: A raster data cube. + :param target: A raster data cube that describes the spatial target resolution. + :param method: Resampling method to use. The following options are available and are meant to align with + [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average (mean) + resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling * `cubic`: + cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc resampling * + `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median resampling, + selects the median value of all valid pixels * `min`: minimum resampling, selects the minimum value from + all valid pixels * `mode`: mode resampling, selects the value which appears most often of all the sampled + points * `near`: nearest neighbour resampling (default) * `q1`: first quartile resampling, selects the + first quartile value of all valid pixels * `q3`: third quartile resampling, selects the third quartile + value of all valid pixels * `rms` root mean square (quadratic mean) of all valid pixels * `sum`: compute + the weighted sum of all valid pixels Valid pixels are determined based on the function ``is_valid()``. + + :return: A raster data cube with the same dimensions. The dimension properties (name, type, labels, + reference system and resolution) remain unchanged, except for the resolution and dimension labels of the + spatial dimensions. + """ + return _process('resample_cube_spatial', data=data, target=target, method=method) + + +@openeo_process +def resample_cube_temporal(data, target, dimension=UNSET, valid_within=UNSET) -> ProcessBuilder: + """ + Resample temporal dimensions to match a target data cube + + :param data: A data cube with one or more temporal dimensions. + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample, which must exist with this name in both + data cubes. If the dimension is not set or is set to `null`, the process resamples all temporal dimensions + that exist with the same names in both data cubes. The following exceptions may occur: * A dimension is + given, but it does not exist in any of the data cubes: `DimensionNotAvailable` * A dimension is given, but + one of them is not temporal: `DimensionMismatch` * No specific dimension name is given and there are no + temporal dimensions with the same name in the data: `DimensionMismatch` + :param valid_within: Setting this parameter to a numerical value enables that the process searches for + valid values within the given period of days before and after the target timestamps. Valid values are + determined based on the function ``is_valid()``. For example, the limit of `7` for the target timestamps + `2020-01-15 12:00:00` looks for a nearest neighbor after `2020-01-08 12:00:00` and before `2020-01-22 + 12:00:00`. If no valid value is found within the given period, the value will be set to no-data (`null`). + + :return: A data cube with the same dimensions and the same dimension properties (name, type, labels, + reference system and resolution) for all non-temporal dimensions. For the temporal dimension, the name and + type remain unchanged, but the dimension labels, resolution and reference system may change. + """ + return _process('resample_cube_temporal', data=data, target=target, dimension=dimension, valid_within=valid_within) + + +@openeo_process +def resample_spatial(data, resolution=UNSET, projection=UNSET, method=UNSET, align=UNSET) -> ProcessBuilder: + """ + Resample and warp the spatial dimensions + + :param data: A raster data cube. + :param resolution: Resamples the data cube to the target resolution, which can be specified either as + separate values for x and y or as a single value for both axes. Specified in the units of the target + projection. Doesn't change the resolution by default (`0`). + :param projection: Warps the data cube to the target projection, specified as as [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). By default (`null`), the projection is + not changed. + :param method: Resampling method to use. The following options are available and are meant to align with + [`gdalwarp`](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r): * `average`: average (mean) + resampling, computes the weighted average of all valid pixels * `bilinear`: bilinear resampling * `cubic`: + cubic resampling * `cubicspline`: cubic spline resampling * `lanczos`: Lanczos windowed sinc resampling * + `max`: maximum resampling, selects the maximum value from all valid pixels * `med`: median resampling, + selects the median value of all valid pixels * `min`: minimum resampling, selects the minimum value from + all valid pixels * `mode`: mode resampling, selects the value which appears most often of all the sampled + points * `near`: nearest neighbour resampling (default) * `q1`: first quartile resampling, selects the + first quartile value of all valid pixels * `q3`: third quartile resampling, selects the third quartile + value of all valid pixels * `rms` root mean square (quadratic mean) of all valid pixels * `sum`: compute + the weighted sum of all valid pixels Valid pixels are determined based on the function ``is_valid()``. + :param align: Specifies to which corner of the spatial extent the new resampled data is aligned to. + + :return: A raster data cube with values warped onto the new projection. It has the same dimensions and the + same dimension properties (name, type, labels, reference system and resolution) for all non-spatial or + vertical spatial dimensions. For the horizontal spatial dimensions the name and type remain unchanged, but + reference system, labels and resolution may change depending on the given parameters. + """ + return _process('resample_spatial', data=data, resolution=resolution, projection=projection, method=method, align=align) + + +@openeo_process +def round(x, p=UNSET) -> ProcessBuilder: + """ + Round to a specified precision + + :param x: A number to round. + :param p: A positive number specifies the number of digits after the decimal point to round to. A negative + number means rounding to a power of ten, so for example *-2* rounds to the nearest hundred. Defaults to + *0*. + + :return: The rounded number. + """ + return _process('round', x=x, p=p) + + +@openeo_process +def run_udf(data, udf, runtime, version=UNSET, context=UNSET) -> ProcessBuilder: + """ + Run a UDF + + :param data: The data to be passed to the UDF. + :param udf: Either source code, an absolute URL or a path to a UDF script. + :param runtime: A UDF runtime identifier available at the back-end. + :param version: An UDF runtime version. If set to `null`, the default runtime version specified for each + runtime is used. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can be of any data type and is exactly what the + UDF code returns. + """ + return _process('run_udf', data=data, udf=udf, runtime=runtime, version=version, context=context) + + +@openeo_process +def run_udf_externally(data, url, context=UNSET) -> ProcessBuilder: + """ + Run an externally hosted UDF container + + :param data: The data to be passed to the UDF. + :param url: Absolute URL to a remote UDF service. + :param context: Additional data such as configuration options to be passed to the UDF. + + :return: The data processed by the UDF. The returned value can in principle be of any data type, but it + depends on what is returned by the UDF code. Please see the implemented UDF interface for details. + """ + return _process('run_udf_externally', data=data, url=url, context=context) + + +@openeo_process +def sar_backscatter(data, coefficient=UNSET, elevation_model=UNSET, mask=UNSET, contributing_area=UNSET, local_incidence_angle=UNSET, ellipsoid_incidence_angle=UNSET, noise_removal=UNSET, options=UNSET) -> ProcessBuilder: + """ + Computes backscatter from SAR input + + :param data: The source data cube containing SAR input. + :param coefficient: Select the radiometric correction coefficient. The following options are available: * + `beta0`: radar brightness * `sigma0-ellipsoid`: ground area computed with ellipsoid earth model * + `sigma0-terrain`: ground area computed with terrain earth model * `gamma0-ellipsoid`: ground area computed + with ellipsoid earth model in sensor line of sight * `gamma0-terrain`: ground area computed with terrain + earth model in sensor line of sight (default) * `null`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `null` (the default) to allow the back- + end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. It indicates which + values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named + `contributing_area` is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes + noise. + :param options: Proprietary options for the backscatter computations. Specifying proprietary options will + reduce portability. + + :return: Backscatter values corresponding to the chosen parametrization. The values are given in linear + scale. + """ + return _process('sar_backscatter', + data=data, + coefficient=coefficient, + elevation_model=elevation_model, + mask=mask, + contributing_area=contributing_area, + local_incidence_angle=local_incidence_angle, + ellipsoid_incidence_angle=ellipsoid_incidence_angle, + noise_removal=noise_removal, + options=options + ) + + +@openeo_process +def save_result(data, format, options=UNSET) -> ProcessBuilder: + """ + Save processed data + + :param data: The data to deliver in the given file format. + :param format: The file format to use. It must be one of the values that the server reports as supported + output file formats, which usually correspond to the short GDAL/OGR codes. This parameter is *case + insensitive*. * If the data cube is empty and the file format can't store empty data cubes, a + `DataCubeEmpty` exception is thrown. * If the file format is otherwise not suitable for storing the + underlying data structure, a `FormatUnsuitable` exception is thrown. + :param options: The file format parameters to be used to create the file(s). Must correspond to the + parameters that the server reports as supported parameters for the chosen `format`. The parameter names and + valid values usually correspond to the GDAL/OGR format options. + + :return: Always returns `true` as in case of an error an exception is thrown which aborts the execution of + the process. + """ + return _process('save_result', data=data, format=format, options=options) + + +@openeo_process +def sd(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Standard deviation + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sample standard deviation. + """ + return _process('sd', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def sgn(x) -> ProcessBuilder: + """ + Signum + + :param x: A number. + + :return: The computed signum value of `x`. + """ + return _process('sgn', x=x) + + +@openeo_process +def sin(x) -> ProcessBuilder: + """ + Sine + + :param x: An angle in radians. + + :return: The computed sine of `x`. + """ + return _process('sin', x=x) + + +@openeo_process +def sinh(x) -> ProcessBuilder: + """ + Hyperbolic sine + + :param x: An angle in radians. + + :return: The computed hyperbolic sine of `x`. + """ + return _process('sinh', x=x) + + +@openeo_process +def sort(data, asc=UNSET, nodata=UNSET) -> ProcessBuilder: + """ + Sort data + + :param data: An array with data to sort. + :param asc: The default sort order is ascending, with smallest values first. To sort in reverse + (descending) order, set this parameter to `false`. + :param nodata: Controls the handling of no-data values (`null`). By default, they are removed. If set to + `true`, missing values in the data are put last; if set to `false`, they are put first. + + :return: The sorted array. + """ + return _process('sort', data=data, asc=asc, nodata=nodata) + + +@openeo_process +def sqrt(x) -> ProcessBuilder: + """ + Square root + + :param x: A number. + + :return: The computed square root. + """ + return _process('sqrt', x=x) + + +@openeo_process +def subtract(x, y) -> ProcessBuilder: + """ + Subtraction of two numbers + + :param x: The minuend. + :param y: The subtrahend. + + :return: The computed result. + """ + return _process('subtract', x=x, y=y) + + +@openeo_process +def sum(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Compute the sum by adding up numbers + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sum of the sequence of numbers. + """ + return _process('sum', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def tan(x) -> ProcessBuilder: + """ + Tangent + + :param x: An angle in radians. + + :return: The computed tangent of `x`. + """ + return _process('tan', x=x) + + +@openeo_process +def tanh(x) -> ProcessBuilder: + """ + Hyperbolic tangent + + :param x: An angle in radians. + + :return: The computed hyperbolic tangent of `x`. + """ + return _process('tanh', x=x) + + +@openeo_process +def text_begins(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text begins with another text + + :param data: Text in which to find something at the beginning. + :param pattern: Text to find at the beginning of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` begins with `pattern`, false` otherwise. + """ + return _process('text_begins', data=data, pattern=pattern, case_sensitive=case_sensitive) + + +@openeo_process +def text_concat(data, separator=UNSET) -> ProcessBuilder: + """ + Concatenate elements to a single text + + :param data: A set of elements. Numbers, boolean values and null values get converted to their (lower case) + string representation. For example: `1` (integer), `-1.5` (number), `true` / `false` (boolean values) + :param separator: A separator to put between each of the individual texts. Defaults to an empty string. + + :return: A string containing a string representation of all the array elements in the same order, with the + separator between each element. + """ + return _process('text_concat', data=data, separator=separator) + + +@openeo_process +def text_contains(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text contains another text + + :param data: Text in which to find something in. + :param pattern: Text to find in `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` contains the `pattern`, false` otherwise. + """ + return _process('text_contains', data=data, pattern=pattern, case_sensitive=case_sensitive) + + +@openeo_process +def text_ends(data, pattern, case_sensitive=UNSET) -> ProcessBuilder: + """ + Text ends with another text + + :param data: Text in which to find something at the end. + :param pattern: Text to find at the end of `data`. Regular expressions are not supported. + :param case_sensitive: Case sensitive comparison can be disabled by setting this parameter to `false`. + + :return: `true` if `data` ends with `pattern`, false` otherwise. + """ + return _process('text_ends', data=data, pattern=pattern, case_sensitive=case_sensitive) + + +@openeo_process +def trim_cube(data) -> ProcessBuilder: + """ + Remove dimension labels with no-data values + + :param data: A data cube to trim. + + :return: A trimmed data cube with the same dimensions. The dimension properties name, type, reference + system and resolution remain unchanged. The number of dimension labels may decrease. + """ + return _process('trim_cube', data=data) + + +@openeo_process +def unflatten_dimension(data, dimension, target_dimensions, label_separator=UNSET) -> ProcessBuilder: + """ + Split a single dimensions into multiple dimensions + + :param data: A data cube that is consistently structured so that operation can execute flawlessly (e.g. the + dimension labels need to contain the `label_separator` exactly 1 time for two target dimensions, 2 times + for three target dimensions etc.). + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the new target dimensions. New dimensions will be created with the + given names and type `other` (see ``add_dimension()``). Fails with a `TargetDimensionExists` exception if + any of the dimensions exists. The order of the array defines the order in which the dimensions and + dimension labels are added to the data cube (see the example in the process description). + :param label_separator: The string that will be used as a separator to split the dimension labels. + + :return: A data cube with the new shape. The dimension properties (name, type, labels, reference system and + resolution) for all other dimensions remain unchanged. + """ + return _process('unflatten_dimension', data=data, dimension=dimension, target_dimensions=target_dimensions, label_separator=label_separator) + + +@openeo_process +def variance(data, ignore_nodata=UNSET) -> ProcessBuilder: + """ + Variance + + :param data: An array of numbers. + :param ignore_nodata: Indicates whether no-data values are ignored or not. Ignores them by default. Setting + this flag to `false` considers no-data values so that `null` is returned if any value is such a value. + + :return: The computed sample variance. + """ + return _process('variance', data=data, ignore_nodata=ignore_nodata) + + +@openeo_process +def vector_buffer(geometries, distance) -> ProcessBuilder: + """ + Buffer geometries by distance + + :param geometries: Geometries to apply the buffer on. Feature properties are preserved. + :param distance: The distance of the buffer in meters. A positive distance expands the geometries, + resulting in outward buffering (dilation), while a negative distance shrinks the geometries, resulting in + inward buffering (erosion). If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. + + :return: Returns a vector data cube with the computed new geometries of which some may be empty. + """ + return _process('vector_buffer', geometries=geometries, distance=distance) + + +@openeo_process +def vector_reproject(data, projection, dimension=UNSET) -> ProcessBuilder: + """ + Reprojects the geometry dimension + + :param data: A vector data cube. + :param projection: Coordinate reference system to reproject to. Specified as an [EPSG + code](http://www.epsg-registry.org/) or [WKT2 CRS + string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). + :param dimension: The name of the geometry dimension to reproject. If no specific dimension is specified, + the filter applies to all geometry dimensions. Fails with a `DimensionNotAvailable` exception if the + specified dimension does not exist. + + :return: A vector data cube with geometries projected to the new coordinate reference system. The reference + system of the geometry dimension changes, all other dimensions and properties remain unchanged. + """ + return _process('vector_reproject', data=data, projection=projection, dimension=dimension) + + +@openeo_process +def vector_to_random_points(data, geometry_count=UNSET, total_count=UNSET, group=UNSET, seed=UNSET) -> ProcessBuilder: + """ + Sample random points from geometries + + :param data: Input geometries for sample extraction. + :param geometry_count: The maximum number of points to compute per geometry. Points in the input + geometries can be selected only once by the sampling. + :param total_count: The maximum number of points to compute overall. Throws a `CountMismatch` exception if + the specified value is less than the provided number of geometries. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a `MultiPoint` + per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is + generated as a distinct `Point` geometry without identifier. + :param seed: A randomization seed to use for random sampling. If not given or `null`, no seed is used and + results may differ on subsequent use. + + :return: Returns a vector data cube with the sampled points. + """ + return _process('vector_to_random_points', data=data, geometry_count=geometry_count, total_count=total_count, group=group, seed=seed) + + +@openeo_process +def vector_to_regular_points(data, distance, group=UNSET) -> ProcessBuilder: + """ + Sample regular points from geometries + + :param data: Input geometries for sample extraction. + :param distance: Defines the minimum distance in meters that is required between two samples generated + *inside* a single geometry. If the unit of the spatial reference system is not meters, a `UnitMismatch` + error is thrown. Use ``vector_reproject()`` to convert the geometries to a suitable spatial reference + system. - For **polygons**, the distance defines the cell sizes of a regular grid that starts at the + upper-left bound of each polygon. The centroid of each cell is then a sample point. If the centroid is not + enclosed in the polygon, no point is sampled. If no point can be sampled for the geometry at all, the first + coordinate of the geometry is returned as point. - For **lines** (line strings), the sampling starts with a + point at the first coordinate of the line and then walks along the line and samples a new point each time + the distance to the previous point has been reached again. - For **points**, the point is returned as + given. + :param group: Specifies whether the sampled points should be grouped by input geometry (default) or be + generated as independent points. * If the sampled points are grouped, the process generates a `MultiPoint` + per geometry given which keeps the original identifier if present. * Otherwise, each sampled point is + generated as a distinct `Point` geometry without identifier. + + :return: Returns a vector data cube with the sampled points. + """ + return _process('vector_to_regular_points', data=data, distance=distance, group=group) + + +@openeo_process +def xor(x, y) -> ProcessBuilder: + """ + Logical XOR (exclusive or) + + :param x: A boolean value. + :param y: A boolean value. + + :return: Boolean result of the logical XOR. + """ + return _process('xor', x=x, y=y) diff --git a/lib/openeo/rest/__init__.py b/lib/openeo/rest/__init__.py new file mode 100644 index 000000000..22fbdb71b --- /dev/null +++ b/lib/openeo/rest/__init__.py @@ -0,0 +1,96 @@ +from typing import Optional + +from openeo import BaseOpenEoException + +# TODO: get from config file +DEFAULT_DOWNLOAD_CHUNK_SIZE = 10_000_000 # 10MB + + +class OpenEoClientException(BaseOpenEoException): + """Base class for OpenEO client exceptions""" + pass + + +class CapabilitiesException(OpenEoClientException): + """Back-end does not support certain openEO feature or endpoint.""" + + +class JobFailedException(OpenEoClientException): + """A synchronous batch job failed. This exception references its corresponding job so the client can e.g. + retrieve its logs. + """ + + def __init__(self, message, job): + super().__init__(message) + self.job = job + + +class OperatorException(OpenEoClientException): + """Invalid (mathematical) operator usage.""" + pass + + +class BandMathException(OperatorException): + """Invalid "band math" usage.""" + pass + + +class OpenEoRestError(OpenEoClientException): + pass + + +class OpenEoApiPlainError(OpenEoRestError): + """ + Base class for openEO API error responses, not necessarily following the openEO API specification + (e.g. not properly JSON encoded, missing required fields, ...) + + :param message: the direct error message from the response + :param http_status_code: the HTTP status code of the response + :param error_message: the error message to show when the exception is rendered + (by default a combination of the HTTP status code and the message) + + .. versionadded:: 0.25.0 + """ + + __slots__ = ("http_status_code", "message") + + def __init__( + self, + message: str, + *, + http_status_code: Optional[int] = None, + error_message: Optional[str] = None, + ): + super().__init__(error_message or f"[{http_status_code}] {message}") + self.http_status_code = http_status_code + self.message = message + + +class OpenEoApiError(OpenEoApiPlainError): + """ + Exception for API error responses following the openEO API specification + (https://api.openeo.org/#section/API-Principles/Error-Handling): + JSON-encoded body, some expected fields like "code" and "message", ... + """ + + __slots__ = ("http_status_code", "code", "message", "id", "url") + + def __init__( + self, + *, + http_status_code: int, + code: str, + message: str, + id: Optional[str] = None, + url: Optional[str] = None, + ): + super().__init__( + message=message, + http_status_code=http_status_code, + error_message=f"[{http_status_code}] {code}: {message}" + (f" (ref: {id})" if id else ""), + ) + self.http_status_code = http_status_code + self.code = code + self.message = message + self.id = id + self.url = url diff --git a/lib/openeo/rest/_datacube.py b/lib/openeo/rest/_datacube.py new file mode 100644 index 000000000..79fe5d5ea --- /dev/null +++ b/lib/openeo/rest/_datacube.py @@ -0,0 +1,358 @@ +from __future__ import annotations + +import logging +import pathlib +import re +import typing +import uuid +import warnings +from typing import Dict, List, Optional, Tuple, Union + +import requests + +from openeo.internal.graph_building import FlatGraphableMixin, PGNode, _FromNodeMixin +from openeo.internal.jupyter import render_component +from openeo.internal.processes.builder import ( + convert_callable_to_pgnode, + get_parameter_names, +) +from openeo.internal.warnings import UserDeprecationWarning +from openeo.rest import OpenEoClientException +from openeo.util import dict_no_none, str_truncate + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo.rest.connection import Connection + +log = logging.getLogger(__name__) + +# Sentinel object to refer to "current" cube in chained cube processing expressions. +THIS = object() + + +class _ProcessGraphAbstraction(_FromNodeMixin, FlatGraphableMixin): + """ + Base class for client-side abstractions/wrappers + for structures that are represented by a openEO process graph: + raster data cubes, vector cubes, ML models, ... + """ + + def __init__(self, pgnode: PGNode, connection: Union[Connection, None]): + self._pg = pgnode + # TODO: now that connection can officially be None: + # improve exceptions in cases where is it still assumed to be a real connection (download, create_job, ...) + self._connection = connection + + def __str__(self): + return "{t}({pg})".format(t=self.__class__.__name__, pg=self._pg) + + def flat_graph(self) -> Dict[str, dict]: + """ + Get the process graph in internal flat dict representation. + + .. warning:: This method is mainly intended for internal use. + It is not recommended for general use and is *subject to change*. + + Instead, it is recommended to use + :py:meth:`to_json()` or :py:meth:`print_json()` + to obtain a standardized, interoperable JSON representation of the process graph. + See :ref:`process_graph_export` for more information. + """ + # TODO: wrap in {"process_graph":...} by default/optionally? + return self._pg.flat_graph() + + @property + def _api_version(self): + return self._connection.capabilities().api_version_check + + @property + def connection(self) -> Connection: + return self._connection + + def result_node(self) -> PGNode: + """ + Get the current result node (:py:class:`PGNode`) of the process graph. + + .. versionadded:: 0.10.1 + """ + return self._pg + + def from_node(self): + # _FromNodeMixin API + return self._pg + + def _build_pgnode( + self, + process_id: str, + arguments: Optional[dict] = None, + namespace: Optional[str] = None, + **kwargs + ) -> PGNode: + """ + Helper to build a PGNode from given argument dict and/or kwargs, + and possibly resolving the `THIS` reference. + """ + arguments = {**(arguments or {}), **kwargs} + for k, v in arguments.items(): + if v is THIS: + arguments[k] = self + # TODO: also necessary to traverse lists/dictionaries? + return PGNode(process_id=process_id, arguments=arguments, namespace=namespace) + + # TODO #278 also move process graph "execution" methods here: `download`, `execute`, `execute_batch`, `create_job`, `save_udf`, ... + + def _repr_html_(self): + process = {"process_graph": self.flat_graph()} + parameters = { + "id": uuid.uuid4().hex, + "explicit-zoom": True, + "height": "400px", + } + return render_component("model-builder", data=process, parameters=parameters) + + +class UDF: + """ + Helper class to load UDF code (e.g. from file) and embed them as "callback" or child process in a process graph. + + Usage example: + + .. code-block:: python + + udf = UDF.from_file("my-udf-code.py") + cube = cube.apply(process=udf) + + + .. versionchanged:: 0.13.0 + Added auto-detection of ``runtime``. + Specifying the ``data`` argument is not necessary anymore, and actually deprecated. + Added :py:meth:`from_file` to simplify loading UDF code from a file. + See :ref:`old_udf_api` for more background about the changes. + """ + + # TODO: eliminate dependency on `openeo.rest.connection` and move to somewhere under `openeo.internal`? + + __slots__ = ["code", "_runtime", "version", "context", "_source"] + + def __init__( + self, + code: str, + runtime: Optional[str] = None, + data=None, # TODO #181 remove `data` argument + version: Optional[str] = None, + context: Optional[dict] = None, + _source=None, + ): + """ + Construct a UDF object from given code string and other argument related to the ``run_udf`` process. + + :param code: UDF source code string (Python, R, ...) + :param runtime: optional UDF runtime identifier, will be autodetected from source code if omitted. + :param data: unused leftover from old API. Don't use this argument, it will be removed in a future release. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + :param _source: (for internal use) source identifier + """ + # TODO: automatically dedent code (when literal string) ? + self.code = code + self._runtime = runtime + self.version = version + self.context = context + self._source = _source + if data is not None: + # TODO #181 remove `data` argument + warnings.warn( + f"The `data` argument of `{self.__class__.__name__}` is deprecated, unused and will be removed in a future release.", + category=UserDeprecationWarning, + stacklevel=2, + ) + + def __repr__(self): + return f"<{type(self).__name__} runtime={self._runtime!r} code={str_truncate(self.code, width=200)!r}>" + + def get_runtime(self, connection: Optional[Connection] = None) -> str: + return self._runtime or self._guess_runtime(connection=connection) + + @classmethod + def from_file( + cls, + path: Union[str, pathlib.Path], + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> UDF: + """ + Load a UDF from a local file. + + .. seealso:: + :py:meth:`from_url` for loading from a URL. + + :param path: path to the local file with UDF source code + :param runtime: optional UDF runtime identifier, will be auto-detected from source code if omitted. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + """ + path = pathlib.Path(path) + code = path.read_text(encoding="utf-8") + return cls( + code=code, runtime=runtime, version=version, context=context, _source=path + ) + + @classmethod + def from_url( + cls, + url: str, + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> UDF: + """ + Load a UDF from a URL. + + .. seealso:: + :py:meth:`from_file` for loading from a local file. + + :param url: URL path to load the UDF source code from + :param runtime: optional UDF runtime identifier, will be auto-detected from source code if omitted. + :param version: optional UDF runtime version string + :param context: optional additional UDF context data + """ + resp = requests.get(url) + resp.raise_for_status() + code = resp.text + return cls( + code=code, runtime=runtime, version=version, context=context, _source=url + ) + + def _guess_runtime(self, connection: Optional[Connection] = None) -> str: + """Guess UDF runtime from UDF source (path) or source code.""" + # First, guess UDF language + language = None + if isinstance(self._source, pathlib.Path): + language = self._guess_runtime_from_suffix(self._source.suffix) + elif isinstance(self._source, str): + url_match = re.match( + r"https?://.*?(?P\.\w+)([&#].*)?$", self._source + ) + if url_match: + language = self._guess_runtime_from_suffix(url_match.group("suffix")) + if not language: + # Guess language from UDF code + if re.search(r"^def [\w0-9_]+\(", self.code, flags=re.MULTILINE): + language = "Python" + # TODO: detection heuristics for R and other languages? + if not language: + raise OpenEoClientException("Failed to detect language of UDF code.") + runtime = language + if connection: + # Some additional best-effort validation/normalization of the runtime + # TODO: this just does some case-normalization, just drop that all together to eliminate + # the dependency on a connection object. See https://github.com/Open-EO/openeo-api/issues/510 + runtimes = {k.lower(): k for k in connection.list_udf_runtimes().keys()} + runtime = runtimes.get(runtime.lower(), runtime) + return runtime + + def _guess_runtime_from_suffix(self, suffix: str) -> Union[str]: + return { + ".py": "Python", + ".r": "R", + }.get(suffix.lower()) + + def get_run_udf_callback(self, connection: Optional[Connection] = None, data_parameter: str = "data") -> PGNode: + """ + For internal use: construct `run_udf` node to be used as callback in `apply`, `reduce_dimension`, ... + """ + arguments = dict_no_none( + data={"from_parameter": data_parameter}, + udf=self.code, + runtime=self.get_runtime(connection=connection), + version=self.version, + context=self.context, + ) + return PGNode(process_id="run_udf", arguments=arguments) + + +def build_child_callback( + process: Union[str, PGNode, typing.Callable, UDF], + parent_parameters: List[str], + connection: Optional[Connection] = None, +) -> dict: + """ + Build a "callback" process: a user defined process that is used by another process (such + as `apply`, `apply_dimension`, `reduce`, ....) + + :param process: process id string, PGNode or callable that uses the ProcessBuilder mechanism to build a process + :param parent_parameters: list of parameter names defined for child process + :param connection: optional connection object to improve runtime validation for UDFs + :return: + """ + # TODO: move this to more generic process graph building utility module + # TODO: autodetect the parameters defined by parent process? + # TODO: eliminate need for connection object (also see `UDF._guess_runtime`) + # TODO: when `openeo.rest` deps are gone: move this helper to somewhere under `openeo.internal` + if isinstance(process, PGNode): + # Assume this is already a valid callback process + pg = process + elif isinstance(process, str): + # Assume given reducer is a simple predefined reduce process_id + # TODO: avoid local import (workaround for circular import issue) + import openeo.processes + if process in openeo.processes.__dict__: + process_params = get_parameter_names(openeo.processes.__dict__[process]) + # TODO: switch to "Callable" handling here + else: + # Best effort guess + process_params = parent_parameters + if parent_parameters == ["x", "y"] and (len(process_params) == 1 or process_params[:1] == ["data"]): + # Special case: wrap all parent parameters in an array + arguments = {process_params[0]: [{"from_parameter": p} for p in parent_parameters]} + else: + # Only pass parameters that correspond with an arg name + common = set(process_params).intersection(parent_parameters) + arguments = {p: {"from_parameter": p} for p in common} + pg = PGNode(process_id=process, arguments=arguments) + elif isinstance(process, typing.Callable): + pg = convert_callable_to_pgnode(process, parent_parameters=parent_parameters) + elif isinstance(process, UDF): + pg = process.get_run_udf_callback(connection=connection, data_parameter=parent_parameters[0]) + elif isinstance(process, dict) and isinstance(process.get("process_graph"), PGNode): + pg = process["process_graph"] + else: + raise ValueError(process) + + return PGNode.to_process_graph_argument(pg) + + +def _ensure_save_result( + cube: _ProcessGraphAbstraction, + *, + format: Optional[str] = None, + options: Optional[dict] = None, + weak_format: Optional[str] = None, + default_format: str, + method: str, +) -> _ProcessGraphAbstraction: + """ + Make sure there is a`save_result` node in the process graph. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :param weak_format: (optional) weak format indicator guessed from file name + :param default_format: default format for data type to use when no format is specified by user + :return: + """ + # TODO #278 instead of standalone helper function, move this to common base class for raster cubes, vector cubes, ... + save_result_nodes = [n for n in cube.result_node().walk_nodes() if n.process_id == "save_result"] + + if not save_result_nodes: + # No `save_result` node yet: automatically add it. + # TODO: the `save_result` method is not defined on _ProcessGraphAbstraction, but it is on DataCube and VectorCube + cube = cube.save_result(format=format or weak_format or default_format, options=options) + elif format or options: + raise OpenEoClientException( + f"{method} with explicit output {'format' if format else 'options'} {format or options!r}," + f" but the process graph already has `save_result` node(s)" + f" which is ambiguous and should not be combined." + ) + + return cube diff --git a/lib/openeo/rest/_testing.py b/lib/openeo/rest/_testing.py new file mode 100644 index 000000000..7dc079d76 --- /dev/null +++ b/lib/openeo/rest/_testing.py @@ -0,0 +1,338 @@ +from __future__ import annotations + +import collections +import json +import re +from typing import Callable, Iterable, Iterator, Optional, Sequence, Tuple, Union + +from openeo import Connection, DataCube +from openeo.rest.vectorcube import VectorCube + +OPENEO_BACKEND = "https://openeo.test/" + + +class OpeneoTestingException(Exception): + pass + + +class DummyBackend: + """ + Dummy backend that handles sync/batch execution requests + and allows inspection of posted process graphs + """ + + # TODO: move to openeo.testing + + __slots__ = ( + "_requests_mock", + "connection", + "file_formats", + "sync_requests", + "batch_jobs", + "validation_requests", + "next_result", + "next_validation_errors", + "job_status_updater", + "extra_job_metadata_fields", + ) + + # Default result (can serve both as JSON or binary data) + DEFAULT_RESULT = b'{"what?": "Result data"}' + + def __init__( + self, + requests_mock, + connection: Connection, + ): + self._requests_mock = requests_mock + self.connection = connection + self.file_formats = {"input": {}, "output": {}} + self.sync_requests = [] + self.batch_jobs = {} + self.validation_requests = [] + self.next_result = self.DEFAULT_RESULT + self.next_validation_errors = [] + self.extra_job_metadata_fields = [] + + # Job status update hook: + # callable that is called on starting a job, and getting job metadata + # allows to dynamically change how the status of a job evolves + # By default: immediately set to "finished" once job is started + self.job_status_updater = lambda job_id, current_status: "finished" + + requests_mock.post( + connection.build_url("/result"), + content=self._handle_post_result, + ) + requests_mock.post( + connection.build_url("/jobs"), + content=self._handle_post_jobs, + ) + requests_mock.post( + re.compile(connection.build_url(r"/jobs/(job-\d+)/results$")), content=self._handle_post_job_results + ) + requests_mock.get(re.compile(connection.build_url(r"/jobs/(job-\d+)$")), json=self._handle_get_job) + requests_mock.get( + re.compile(connection.build_url(r"/jobs/(job-\d+)/results$")), json=self._handle_get_job_results + ) + requests_mock.get( + re.compile(connection.build_url("/jobs/(.*?)/results/result.data$")), + content=self._handle_get_job_result_asset, + ) + requests_mock.post(connection.build_url("/validation"), json=self._handle_post_validation) + + @classmethod + def at_url(cls, root_url: str, *, requests_mock, capabilities: Optional[dict] = None) -> DummyBackend: + """ + Factory to build dummy backend from given root URL + including creation of connection and mocking of capabilities doc + """ + root_url = root_url.rstrip("/") + "/" + requests_mock.get(root_url, json=build_capabilities(**(capabilities or None))) + connection = Connection(root_url) + return cls(requests_mock=requests_mock, connection=connection) + + def setup_collection( + self, + collection_id: str, + *, + temporal: Union[bool, Tuple[str, str]] = True, + bands: Sequence[str] = ("B1", "B2", "B3"), + ): + # TODO: also mock `/collections` overview + # TODO: option to override cube_dimensions as a whole, or override dimension names + cube_dimensions = { + "x": {"type": "spatial"}, + "y": {"type": "spatial"}, + } + + if temporal: + cube_dimensions["t"] = { + "type": "temporal", + "extent": temporal if isinstance(temporal, tuple) else [None, None], + } + if bands: + cube_dimensions["bands"] = {"type": "bands", "values": list(bands)} + + self._requests_mock.get( + self.connection.build_url(f"/collections/{collection_id}"), + # TODO: add more metadata? + json={ + "id": collection_id, + # define temporal and band dim + "cube:dimensions": {"t": {"type": "temporal"}, "bands": {"type": "bands"}}, + }, + ) + return self + + def setup_file_format(self, name: str, type: str = "output", gis_data_types: Iterable[str] = ("raster",)): + self.file_formats[type][name] = { + "title": name, + "gis_data_types": list(gis_data_types), + "parameters": {}, + } + self._requests_mock.get(self.connection.build_url("/file_formats"), json=self.file_formats) + return self + + def _handle_post_result(self, request, context): + """handler of `POST /result` (synchronous execute)""" + pg = request.json()["process"]["process_graph"] + self.sync_requests.append(pg) + result = self.next_result + if isinstance(result, (dict, list)): + result = json.dumps(result).encode("utf-8") + elif isinstance(result, str): + result = result.encode("utf-8") + assert isinstance(result, bytes) + return result + + def _handle_post_jobs(self, request, context): + """handler of `POST /jobs` (create batch job)""" + post_data = request.json() + pg = post_data["process"]["process_graph"] + job_id = f"job-{len(self.batch_jobs):03d}" + job_data = {"job_id": job_id, "pg": pg, "status": "created"} + for field in ["title", "description"]: + if field in post_data: + job_data[field] = post_data[field] + for field in self.extra_job_metadata_fields: + job_data[field] = post_data.get(field) + self.batch_jobs[job_id] = job_data + context.status_code = 201 + context.headers["openeo-identifier"] = job_id + + def _get_job_id(self, request) -> str: + match = re.match(r"^/jobs/(job-\d+)(/|$)", request.path) + if not match: + raise OpeneoTestingException(f"Failed to extract job_id from {request.path}") + job_id = match.group(1) + assert job_id in self.batch_jobs + return job_id + + def _handle_post_job_results(self, request, context): + """Handler of `POST /job/{job_id}/results` (start batch job).""" + job_id = self._get_job_id(request) + assert self.batch_jobs[job_id]["status"] == "created" + self.batch_jobs[job_id]["status"] = self.job_status_updater( + job_id=job_id, current_status=self.batch_jobs[job_id]["status"] + ) + context.status_code = 202 + + def _handle_get_job(self, request, context): + """Handler of `GET /job/{job_id}` (get batch job status and metadata).""" + job_id = self._get_job_id(request) + # Allow updating status with `job_status_setter` once job got past status "created" + if self.batch_jobs[job_id]["status"] != "created": + self.batch_jobs[job_id]["status"] = self.job_status_updater( + job_id=job_id, current_status=self.batch_jobs[job_id]["status"] + ) + return {"id": job_id, "status": self.batch_jobs[job_id]["status"]} + + def _handle_get_job_results(self, request, context): + """Handler of `GET /job/{job_id}/results` (list batch job results).""" + job_id = self._get_job_id(request) + assert self.batch_jobs[job_id]["status"] == "finished" + return { + "id": job_id, + "assets": {"result.data": {"href": self.connection.build_url(f"/jobs/{job_id}/results/result.data")}}, + } + + def _handle_get_job_result_asset(self, request, context): + """Handler of `GET /job/{job_id}/results/result.data` (get batch job result asset).""" + job_id = self._get_job_id(request) + assert self.batch_jobs[job_id]["status"] == "finished" + return self.next_result + + def _handle_post_validation(self, request, context): + """Handler of `POST /validation` (validate process graph).""" + pg = request.json()["process_graph"] + self.validation_requests.append(pg) + return {"errors": self.next_validation_errors} + + def get_sync_pg(self) -> dict: + """Get one and only synchronous process graph""" + assert len(self.sync_requests) == 1 + return self.sync_requests[0] + + def get_batch_pg(self) -> dict: + """ + Get process graph of the one and only batch job. + Fails when there is none or more than one. + """ + assert len(self.batch_jobs) == 1 + return self.batch_jobs[max(self.batch_jobs.keys())]["pg"] + + def get_validation_pg(self) -> dict: + """ + Get process graph of the one and only validation request. + """ + assert len(self.validation_requests) == 1 + return self.validation_requests[0] + + def get_pg(self, process_id: Optional[str] = None) -> dict: + """ + Get one and only batch process graph (sync or batch) + + :param process_id: just return single process graph node with this process_id + :return: process graph (flat graph representation) or process graph node + """ + pgs = self.sync_requests + [b["pg"] for b in self.batch_jobs.values()] + if len(pgs) != 1: + raise OpeneoTestingException(f"Expected single process graph, but collected {len(pgs)}") + pg = pgs[0] + if process_id: + # Just return single node (by process_id) + found = [node for node in pg.values() if node.get("process_id") == process_id] + if len(found) != 1: + raise OpeneoTestingException( + f"Expected single process graph node with process_id {process_id!r}, but found {len(found)}: {found}" + ) + return found[0] + return pg + + def execute(self, cube: Union[DataCube, VectorCube], process_id: Optional[str] = None) -> dict: + """ + Execute given cube (synchronously) and return observed process graph (or subset thereof). + + :param cube: cube to execute on dummy back-end + :param process_id: just return single process graph node with this process_id + :return: process graph (flat graph representation) or process graph node + """ + cube.execute() + return self.get_pg(process_id=process_id) + + def setup_simple_job_status_flow(self, *, queued: int = 1, running: int = 4, final: str = "finished"): + """ + Set up simple job status flow: + queued (a couple of times) -> running (a couple of times) -> finished/error. + """ + template = ["queued"] * queued + ["running"] * running + [final] + job_stacks = collections.defaultdict(template.copy) + + def get_status(job_id: str, current_status: str) -> str: + stack = job_stacks[job_id] + # Pop first item each time, but repeat the last one at the end + return stack.pop(0) if len(stack) > 1 else stack[0] + + self.job_status_updater = get_status + + +def build_capabilities( + *, + api_version: str = "1.0.0", + stac_version: str = "0.9.0", + basic_auth: bool = True, + oidc_auth: bool = True, + collections: bool = True, + processes: bool = True, + sync_processing: bool = True, + validation: bool = False, + batch_jobs: bool = True, + udp: bool = False, +) -> dict: + """Build a dummy capabilities document for testing purposes.""" + + endpoints = [] + if basic_auth: + endpoints.append({"path": "/credentials/basic", "methods": ["GET"]}) + if oidc_auth: + endpoints.append({"path": "/credentials/oidc", "methods": ["GET"]}) + if basic_auth or oidc_auth: + endpoints.append({"path": "/me", "methods": ["GET"]}) + + if collections: + endpoints.append({"path": "/collections", "methods": ["GET"]}) + endpoints.append({"path": "/collections/{collection_id}", "methods": ["GET"]}) + if processes: + endpoints.append({"path": "/processes", "methods": ["GET"]}) + if sync_processing: + endpoints.append({"path": "/result", "methods": ["POST"]}) + if validation: + endpoints.append({"path": "/validation", "methods": ["POST"]}) + if batch_jobs: + endpoints.extend( + [ + {"path": "/jobs", "methods": ["GET", "POST"]}, + {"path": "/jobs/{job_id}", "methods": ["GET", "DELETE"]}, + {"path": "/jobs/{job_id}/results", "methods": ["GET", "POST", "DELETE"]}, + {"path": "/jobs/{job_id}/logs", "methods": ["GET"]}, + ] + ) + if udp: + endpoints.extend( + [ + {"path": "/process_graphs", "methods": ["GET"]}, + {"path": "/process_graphs/{process_graph_id", "methods": ["GET", "PUT", "DELETE"]}, + ] + ) + + capabilities = { + "api_version": api_version, + "stac_version": stac_version, + "id": "dummy", + "title": "Dummy openEO back-end", + "description": "Dummy openeEO back-end", + "endpoints": endpoints, + "links": [], + } + return capabilities diff --git a/lib/openeo/rest/auth/__init__.py b/lib/openeo/rest/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/openeo/rest/auth/auth.py b/lib/openeo/rest/auth/auth.py new file mode 100644 index 000000000..1eff400fa --- /dev/null +++ b/lib/openeo/rest/auth/auth.py @@ -0,0 +1,54 @@ +import collections +from typing import Optional + +from requests import Request +from requests.auth import AuthBase + + +class OpenEoApiAuthBase(AuthBase): + """ + Base class for authentication with the OpenEO REST API. + + Follows the authentication approach of the requests library: + an auth object is a callable object that can be passed with get/post request + to manipulate this request (typically setting headers). + """ + + def __call__(self, req: Request) -> Request: + # Do nothing by default + return req + + +class NullAuth(OpenEoApiAuthBase): + """No authentication""" + + pass + + +class BearerAuth(OpenEoApiAuthBase): + """ + Requests are authenticated through a bearer token + https://open-eo.github.io/openeo-api/apireference/#section/Authentication/Bearer + """ + + def __init__(self, bearer: str): + self.bearer = bearer + + def __call__(self, req: Request) -> Request: + # Add bearer authorization header. + req.headers["Authorization"] = "Bearer {b}".format(b=self.bearer) + return req + + +class BasicBearerAuth(BearerAuth): + """Bearer token for Basic Auth (openEO API 1.0.0 style)""" + + def __init__(self, access_token: str): + super().__init__(bearer="basic//{t}".format(t=access_token)) + + +class OidcBearerAuth(BearerAuth): + """Bearer token for OIDC Auth (openEO API 1.0.0 style)""" + + def __init__(self, provider_id: str, access_token: str): + super().__init__(bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token)) diff --git a/lib/openeo/rest/auth/cli.py b/lib/openeo/rest/auth/cli.py new file mode 100644 index 000000000..8c4068f30 --- /dev/null +++ b/lib/openeo/rest/auth/cli.py @@ -0,0 +1,376 @@ +import argparse +import builtins +import json +import logging +import sys +from collections import OrderedDict +from getpass import getpass +from pathlib import Path +from typing import List, Tuple + +from openeo import Connection, connect +from openeo.capabilities import ApiVersionException +from openeo.rest.auth.config import AuthConfig, RefreshTokenStore +from openeo.rest.auth.oidc import OidcProviderInfo + +_log = logging.getLogger(__name__) + + +class CliToolException(RuntimeError): + pass + + +_OIDC_FLOW_CHOICES = [ + "auth-code", + "device", + # TODO: add client credentials flow? +] + + +def main(argv=None): + root_parser = argparse.ArgumentParser(description="Tool to manage openEO related authentication and configuration.") + root_parser.add_argument( + "--verbose", "-v", action="count", default=0, help="Increase logging verbosity. Can be given multiple times." + ) + root_subparsers = root_parser.add_subparsers(title="Subcommands", dest="subparser_name") + + # Command: paths + paths_parser = root_subparsers.add_parser("paths", help="Show paths to config/token files.") + paths_parser.set_defaults(func=main_paths) + + # Command: config-dump + config_dump_parser = root_subparsers.add_parser("config-dump", help="Dump config file.", aliases=["config"]) + config_dump_parser.set_defaults(func=main_config_dump) + config_dump_parser.add_argument("--show-secrets", action="store_true", help="Don't redact secrets in the dump.") + + # Command: token-dump + token_dump_parser = root_subparsers.add_parser( + "token-dump", help="Dump OpenID Connect refresh tokens file.", aliases=["tokens"] + ) + token_dump_parser.set_defaults(func=main_token_dump) + token_dump_parser.add_argument("--show-secrets", action="store_true", help="Don't redact secrets in the dump.") + + # Command: token-clear + token_clear_parser = root_subparsers.add_parser("token-clear", help="Remove OpenID Connect refresh tokens file.") + token_clear_parser.set_defaults(func=main_token_clear) + token_clear_parser.add_argument("--force", "-f", action="store_true", help="Remove without asking confirmation.") + + # Command: add-basic + add_basic_parser = root_subparsers.add_parser("add-basic", help="Add or update config entry for basic auth.") + add_basic_parser.set_defaults(func=main_add_basic) + add_basic_parser.add_argument("backend", help="OpenEO Backend URL.") + add_basic_parser.add_argument("--username", help="Basic auth username.") + add_basic_parser.add_argument( + "--no-try", + dest="try_auth", + action="store_false", + help="Don't try out the credentials against the backend, just store them.", + ) + + # Command: add-oidc + add_oidc_parser = root_subparsers.add_parser("add-oidc", help="Add or update config entry for OpenID Connect.") + add_oidc_parser.set_defaults(func=main_add_oidc) + add_oidc_parser.add_argument("backend", help="OpenEO Backend URL.") + add_oidc_parser.add_argument("--provider-id", help="Provider ID to use.") + add_oidc_parser.add_argument("--client-id", help="Client ID to use.") + add_oidc_parser.add_argument( + "--no-client-secret", + dest="ask_client_secret", + default=True, + action="store_false", + help="Don't ask for secret (because client does not need one).", + ) + add_oidc_parser.add_argument( + "--use-default-client", action="store_true", help="Use default client (as provided by backend)." + ) + + # Command: oidc-auth + oidc_auth_parser = root_subparsers.add_parser( + "oidc-auth", help="Do OpenID Connect authentication flow and store refresh tokens." + ) + oidc_auth_parser.set_defaults(func=main_oidc_auth) + oidc_auth_parser.add_argument("backend", help="OpenEO Backend URL.") + oidc_auth_parser.add_argument("--provider-id", help="Provider ID to use.") + oidc_auth_parser.add_argument( + "--flow", choices=_OIDC_FLOW_CHOICES, default="device", help="OpenID Connect flow to use (default: device)." + ) + oidc_auth_parser.add_argument( + "--timeout", type=int, default=60, help="Timeout in seconds to wait for (user) response." + ) + + # Parse arguments and execute sub-command + args = root_parser.parse_args(argv) + logging.basicConfig(level={0: logging.WARN, 1: logging.INFO}.get(args.verbose, logging.DEBUG)) + _log.debug(repr(args)) + if args.subparser_name: + args.func(args) + else: + root_parser.print_help() + + +def main_paths(args): + """ + Print paths of auth config file and refresh token cache file. + """ + + def describe(p: Path): + if p.exists(): + return "perms: 0o{p:o}, size: {s}B".format(p=p.stat().st_mode & 0o777, s=p.stat().st_size) + else: + return "does not exist" + + config_path = AuthConfig().path + print("openEO auth config: {p} ({d})".format(p=str(config_path), d=describe(config_path))) + tokens_path = RefreshTokenStore().path + print("openEO OpenID Connect refresh token store: {p} ({d})".format(p=str(tokens_path), d=describe(tokens_path))) + + +def _redact(d: dict, keys_to_redact: List[str]): + """Redact secrets in given dict in-place.""" + for k, v in d.items(): + if k in keys_to_redact: + d[k] = "" + elif isinstance(v, dict): + _redact(v, keys_to_redact=keys_to_redact) + + +def main_config_dump(args): + """ + Dump auth config file + """ + config = AuthConfig() + print("### {p} ".format(p=str(config.path)).ljust(80, "#")) + data = config.load(empty_on_file_not_found=False) + if not args.show_secrets: + _redact(data, keys_to_redact=["client_secret", "password", "refresh_token"]) + json.dump(data, fp=sys.stdout, indent=2) + print() + + +def main_token_dump(args): + """ + Dump refresh token file + """ + tokens = RefreshTokenStore() + print("### {p} ".format(p=str(tokens.path)).ljust(80, "#")) + data = tokens.load(empty_on_file_not_found=False) + if not args.show_secrets: + _redact(data, keys_to_redact=["client_secret", "password", "refresh_token"]) + json.dump(data, fp=sys.stdout, indent=2) + print() + + +def main_token_clear(args): + """ + Remove refresh token file + """ + tokens = RefreshTokenStore() + path = tokens.path + if path.exists(): + if not args.force: + answer = builtins.input(f"Remove refresh token file {path}? 'y' or 'n': ") + if answer.lower()[:1] != "y": + print("Keeping refresh token file.") + return + tokens.remove() + print(f"Removed refresh token file {path}.") + else: + print(f"No refresh token file at {path}.") + + +def main_add_basic(args): + """ + Add a config entry for basic auth + """ + backend = args.backend + username = args.username + try_auth = args.try_auth + config = AuthConfig() + + print("Will add basic auth config for backend URL {b!r}".format(b=backend)) + print("to config file: {c!r}".format(c=str(config.path))) + + # Find username and password + if not username: + username = builtins.input("Enter username and press enter: ") + print("Using username {u!r}".format(u=username)) + password = getpass("Enter password and press enter: ") or None + + if try_auth: + print("Trying to authenticate with {b!r}".format(b=backend)) + con = connect(backend) + con.authenticate_basic(username, password) + print("Successfully authenticated {u!r}".format(u=username)) + + config.set_basic_auth(backend=backend, username=username, password=password) + print("Saved credentials to {p!r}".format(p=str(config.path))) + + +def _interactive_choice(title: str, options: List[Tuple[str, str]], attempts=10) -> str: + """ + Let user choose between options (given as dict) and return chosen key + """ + print(title) + for c, (k, v) in enumerate(options): + print("[{c:d}] {v}".format(c=c + 1, v=v)) + for _ in range(attempts): + try: + entered = builtins.input("Choose one (enter index): ") + return options[int(entered) - 1][0] + except Exception: + pass + raise CliToolException("Failed to pick valid option.") + + +def show_warning(message: str): + _log.warning(message) + + +def main_add_oidc(args): + """ + Add a config entry for OIDC auth + """ + backend = args.backend + provider_id = args.provider_id + client_id = args.client_id + ask_client_secret = args.ask_client_secret + use_default_client = args.use_default_client + config = AuthConfig() + + print("Will add OpenID Connect auth config for backend URL {b!r}".format(b=backend)) + print("to config file: {c!r}".format(c=str(config.path))) + + con = connect(backend) + con.capabilities().api_version_check.require_at_least("1.0.0") + + # Find provider ID + oidc_info = con.get("/credentials/oidc", expected_status=200).json() + providers = OrderedDict((p["id"], OidcProviderInfo.from_dict(p)) for p in oidc_info["providers"]) + + if not providers: + raise CliToolException("No OpenID Connect providers listed by backend {b!r}.".format(b=backend)) + if not provider_id: + if len(providers) == 1: + provider_id = list(providers.keys())[0] + else: + provider_id = _interactive_choice( + title="Backend {b!r} has multiple OpenID Connect providers.".format(b=backend), + options=[(p.id, "{t} (issuer {s})".format(t=p.title, s=p.issuer)) for p in providers.values()], + ) + if provider_id not in providers: + raise CliToolException( + "Invalid provider ID {p!r}. Should be one of {o}.".format(p=provider_id, o=list(providers.keys())) + ) + provider = providers[provider_id] + print("Using provider ID {p!r} (issuer {i!r})".format(p=provider_id, i=provider.issuer)) + + # Get client_id and client_secret (if necessary) + if use_default_client: + if not provider.default_clients: + show_warning("No default clients declared for provider {p!r}".format(p=provider_id)) + client_id, client_secret = None, None + else: + if not client_id: + if provider.default_clients: + client_prompt = "Enter client_id or leave empty to use default client, and press enter: " + else: + client_prompt = "Enter client_id and press enter: " + client_id = builtins.input(client_prompt).strip() or None + print("Using client ID {u!r}".format(u=client_id)) + if not client_id and not provider.default_clients: + show_warning("Given client ID was empty.") + + if client_id and ask_client_secret: + client_secret = getpass("Enter client_secret or leave empty to not use a secret, and press enter: ") or None + else: + client_secret = None + + config.set_oidc_client_config( + backend=backend, + provider_id=provider_id, + client_id=client_id, + client_secret=client_secret, + issuer=provider.issuer, + ) + print("Saved client information to {p!r}".format(p=str(config.path))) + + +_webbrowser_open = None + + +def main_oidc_auth(args): + """ + Do OIDC auth flow and store refresh tokens. + """ + backend = args.backend + oidc_flow = args.flow + provider_id = args.provider_id + timeout = args.timeout + + config = AuthConfig() + + print("Will do OpenID Connect flow to authenticate with backend {b!r}.".format(b=backend)) + print("Using config {c!r}.".format(c=str(config.path))) + + # Determine provider + provider_configs = config.get_oidc_provider_configs(backend=backend) + _log.debug("Provider configs: {c!r}".format(c=provider_configs)) + if not provider_id: + if len(provider_configs) == 0: + print("Will try to use default provider_id.") + provider_id = None + elif len(provider_configs) == 1: + provider_id = list(provider_configs.keys())[0] + else: + provider_id = _interactive_choice( + title="Multiple OpenID Connect providers available for backend {b!r}".format(b=backend), + options=sorted( + (k, "{k}: issuer {s}".format(k=k, s=v.get("issuer", "n/a"))) for k, v in provider_configs.items() + ), + ) + if not (provider_id is None or provider_id in provider_configs): + raise CliToolException( + "Invalid provider ID {p!r}. Should be `None` or one of {o}.".format( + p=provider_id, o=list(provider_configs.keys()) + ) + ) + print("Using provider ID {p!r}.".format(p=provider_id)) + + # Get client id and secret + client_id, client_secret = config.get_oidc_client_configs(backend=backend, provider_id=provider_id) + if client_id: + print("Using client ID {c!r}.".format(c=client_id)) + else: + print("Will try to use default client.") + + refresh_token_store = RefreshTokenStore() + con = Connection(backend, refresh_token_store=refresh_token_store) + if oidc_flow == "auth-code": + print("Starting OpenID Connect authorization code flow:") + print( + "a browser window should open allowing you to log in with the identity provider\n" + "and grant access to the client {c!r} (timeout: {t}s).".format(c=client_id, t=timeout) + ) + con.authenticate_oidc_authorization_code( + client_id=client_id, + client_secret=client_secret, + provider_id=provider_id, + timeout=timeout, + store_refresh_token=True, + webbrowser_open=_webbrowser_open, + ) + print("The OpenID Connect authorization code flow was successful.") + elif oidc_flow == "device": + print("Starting OpenID Connect device flow.") + con.authenticate_oidc_device( + client_id=client_id, client_secret=client_secret, provider_id=provider_id, store_refresh_token=True + ) + print("The OpenID Connect device flow was successful.") + else: + raise CliToolException("Invalid flow {f!r}".format(f=oidc_flow)) + + print("Stored refresh token in {p!r}".format(p=str(refresh_token_store.path))) + + +if __name__ == "__main__": + main() diff --git a/lib/openeo/rest/auth/config.py b/lib/openeo/rest/auth/config.py new file mode 100644 index 000000000..8890b88ac --- /dev/null +++ b/lib/openeo/rest/auth/config.py @@ -0,0 +1,240 @@ +""" +Functionality to store and retrieve authentication settings (usernames, passwords, client ids, ...) +from local config files. +""" + +# TODO: also allow to set client_id, client_secret, refresh_token through env variables? + + +import json +import logging +import platform +import stat +from datetime import datetime +from pathlib import Path +from typing import Dict, Tuple, Union + +from openeo import __version__ +from openeo.config import get_user_config_dir, get_user_data_dir +from openeo.util import deep_get, deep_set, rfc3339 + +try: + # Use oschmod when available (fall back to POSIX-only functionality from stdlib otherwise) + # TODO: enforce oschmod as dependency for all platforms? + import oschmod +except ImportError: + oschmod = None + + +PRIVATE_PERMISSIONS = stat.S_IRUSR | stat.S_IWUSR + +log = logging.getLogger(__name__) + + +def get_file_mode(path: Path) -> int: + """Get the file permission bits in a way that works on both *nix and Windows platforms.""" + if oschmod: + return oschmod.get_mode(str(path)) + return path.stat().st_mode + + +def set_file_mode(path: Path, mode: int): + """Set the file permission bits in a way that works on both *nix and Windows platforms.""" + if oschmod: + oschmod.set_mode(str(path), mode=mode) + else: + path.chmod(mode=mode) + + +def assert_private_file(path: Path): + """Check that given file is only readable by user.""" + mode = get_file_mode(path) + if (mode & stat.S_IRWXG) or (mode & stat.S_IRWXO): + message = "File {p} could be readable by others: mode {a:o} (expected: {e:o}).".format( + p=path, a=mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO), e=PRIVATE_PERMISSIONS + ) + if platform.system() == "Windows": + log.info(message) + else: + raise PermissionError(message) + + +def utcnow_rfc3339() -> str: + """Current datetime formatted as RFC-3339 string.""" + return rfc3339.datetime(datetime.utcnow()) + + +def _normalize_url(url: str) -> str: + """Normalize a url (trim trailing slash), to simplify equality checking.""" + return url.rstrip("/") or "/" + + +class PrivateJsonFile: + """ + Base class for private config/data files in JSON format. + """ + + DEFAULT_FILENAME = "private.json" + + def __init__(self, path: Path = None): + if path is None: + path = self.default_path() + if path.is_dir(): + path = path / self.DEFAULT_FILENAME + self._path = path + + @property + def path(self) -> Path: + return self._path + + @classmethod + def default_path(cls) -> Path: + return get_user_config_dir(auto_create=True) / cls.DEFAULT_FILENAME + + def load(self, empty_on_file_not_found=True) -> dict: + """Load all data from file""" + if not self._path.exists(): + if empty_on_file_not_found: + return {} + raise FileNotFoundError(self._path) + assert_private_file(self._path) + log.debug("Loading private JSON file {p}".format(p=self._path)) + # TODO: add file locking to avoid race conditions? + try: + with self._path.open("r", encoding="utf8") as f: + return json.load(f) + except Exception as e: + raise RuntimeError(f"Failed to load {type(self).__name__} from {self._path!r}: {e!r}") from e + + def _write(self, data: dict): + """Write whole data to file.""" + log.debug("Writing private JSON file {p}".format(p=self._path)) + # TODO: add file locking to avoid race conditions? + with self._path.open("w", encoding="utf8") as f: + json.dump(data, f, indent=2) + set_file_mode(self._path, mode=PRIVATE_PERMISSIONS) + assert_private_file(self._path) + + def get(self, *keys, default=None) -> Union[dict, str, int]: + """Load JSON file and do deep get with given keys.""" + result = deep_get(self.load(), *keys, default=default) + if isinstance(result, Exception) or (isinstance(result, type) and issubclass(result, Exception)): + # pylint: disable=raising-bad-type + raise result + return result + + def set(self, *keys, value): + data = self.load() + deep_set(data, *keys, value=value) + self._write(data) + + def remove(self): + if self._path.exists(): + log.debug(f"Removing {self._path}") + self._path.unlink() + + +class AuthConfig(PrivateJsonFile): + DEFAULT_FILENAME = "auth-config.json" + + @classmethod + def default_path(cls) -> Path: + return get_user_config_dir(auto_create=True) / cls.DEFAULT_FILENAME + + def _write(self, data: dict): + # When starting fresh: add some metadata and defaults + if "metadata" not in data: + data["metadata"] = { + "type": "AuthConfig", + "created": utcnow_rfc3339(), + "created_by": "openeo-python-client {v}".format(v=__version__), + "version": 1, + } + data.setdefault("general", {}) + data.setdefault("backends", {}) + return super()._write(data=data) + + def get_basic_auth(self, backend: str) -> Tuple[Union[None, str], Union[None, str]]: + """Get username/password combo for given backend. Values will be None when no config is available.""" + basic = self.get("backends", _normalize_url(backend), "basic", default={}) + username = basic.get("username") + password = basic.get("password") if username else None + return username, password + + def set_basic_auth(self, backend: str, username: str, password: Union[str, None]): + data = self.load() + keys = ( + "backends", + _normalize_url(backend), + "basic", + ) + # TODO: support multiple basic auth credentials? (pick latest by default for example) + deep_set(data, *keys, "date", value=utcnow_rfc3339()) + deep_set(data, *keys, "username", value=username) + if password: + deep_set(data, *keys, "password", value=password) + self._write(data) + + def get_oidc_provider_configs(self, backend: str) -> Dict[str, dict]: + """ + Get provider config items for given backend. + + Returns a dict mapping provider_id to dicts with "client_id" and "client_secret" items + """ + return self.get("backends", _normalize_url(backend), "oidc", "providers", default={}) + + def get_oidc_client_configs(self, backend: str, provider_id: str) -> Tuple[str, str]: + """ + Get client_id and client_secret for given backend+provider_id. Values will be None when no config is available. + """ + client = self.get("backends", _normalize_url(backend), "oidc", "providers", provider_id, default={}) + client_id = client.get("client_id") + client_secret = client.get("client_secret") if client_id else None + return client_id, client_secret + + def set_oidc_client_config( + self, + backend: str, + provider_id: str, + client_id: Union[str, None], + client_secret: Union[str, None] = None, + issuer: Union[str, None] = None, + ): + data = self.load() + keys = ("backends", _normalize_url(backend), "oidc", "providers", provider_id) + # TODO: support multiple clients? (pick latest by default for example) + deep_set(data, *keys, "date", value=utcnow_rfc3339()) + deep_set(data, *keys, "client_id", value=client_id) + deep_set(data, *keys, "client_secret", value=client_secret) + if issuer: + deep_set(data, *keys, "issuer", value=issuer) + self._write(data) + + +class RefreshTokenStore(PrivateJsonFile): + """ + Basic JSON-file based storage of refresh tokens. + """ + + DEFAULT_FILENAME = "refresh-tokens.json" + + @classmethod + def default_path(cls) -> Path: + return get_user_data_dir(auto_create=True) / cls.DEFAULT_FILENAME + + def get_refresh_token(self, issuer: str, client_id: str) -> Union[str, None]: + return self.get(_normalize_url(issuer), client_id, "refresh_token", default=None) + + def set_refresh_token(self, issuer: str, client_id: str, refresh_token: str): + data = self.load() + log.info("Storing refresh token for issuer {i!r} (client {c!r})".format(i=issuer, c=client_id)) + deep_set( + data, + _normalize_url(issuer), + client_id, + value={ + "date": utcnow_rfc3339(), + "refresh_token": refresh_token, + }, + ) + self._write(data) diff --git a/lib/openeo/rest/auth/oidc.py b/lib/openeo/rest/auth/oidc.py new file mode 100644 index 000000000..b7a79c80b --- /dev/null +++ b/lib/openeo/rest/auth/oidc.py @@ -0,0 +1,943 @@ +""" +OpenID Connect related functionality and helpers. + +""" + +from __future__ import annotations + +import base64 +import contextlib +import enum +import functools +import hashlib +import http.server +import inspect +import json +import logging +import random +import string +import threading +import time +import urllib.parse +import warnings +import webbrowser +from queue import Empty, Queue +from typing import Callable, List, NamedTuple, Optional, Tuple, Union + +import requests +import requests.exceptions + +import openeo +from openeo.internal.jupyter import in_jupyter_context +from openeo.rest import OpenEoClientException +from openeo.util import SimpleProgressBar, clip, dict_no_none, url_join + +log = logging.getLogger(__name__) + + +class QueuingRequestHandler(http.server.BaseHTTPRequestHandler): + """ + Base class for simple HTTP request handlers to be used in threaded context. + The handler puts the requested paths in a thread-safe queue + """ + + def __init__(self, *args, **kwargs): + self._queue = kwargs.pop("queue", None) or Queue() + super().__init__(*args, **kwargs) + + def do_GET(self): + log.debug("{c} GET {p}".format(c=self.__class__.__name__, p=self.path)) + status, body, headers = self.queue(self.path) + self.send_response(status) + self.send_header("Content-Length", str(len(body))) + for k, v in headers.items(): + self.send_header(k, v) + self.end_headers() + self.wfile.write(body.encode("utf-8")) + + def queue(self, path: str): + self._queue.put(path) + return 200, "queued", {} + + @classmethod + def with_queue(cls, queue: Queue): + """Create a factory for this object pre-bound with given queue object""" + return functools.partial(cls, queue=queue) + + def log_message(self, format, *args): + # Override the default implementation, which is a hardcoded `sys.stderr.write` + log.debug(format % args) + + +class OAuthRedirectRequestHandler(QueuingRequestHandler): + """Request handler for OAuth redirects""" + + PATH = "/callback" + + TEMPLATE = """ + + openEO OIDC auth + + {content} +

openEO Python client {version}

+ + + """ + + def queue(self, path: str): + if path.startswith(self.PATH + "?"): + super().queue(path) + # TODO: auto-close browser tab/window? + # TODO: make it a nicer page and bit more of metadata? + status = 200 + content = "

OIDC Redirect URL request received.

You can close this browser tab now.

" + else: + status = 404 + content = "

Not found.

" + body = self.TEMPLATE.format(content=content, version=openeo.client_version()) + return status, body, {"Content-Type": "text/html; charset=UTF-8"} + + +class HttpServerThread(threading.Thread): + """ + Thread that runs a HTTP server (`http.server.HTTPServer`) + """ + + def __init__(self, RequestHandlerClass, server_address: Tuple[str, int] = None): + # Make it a daemon to minimize potential shutdown issues due to `serve_forever` + super().__init__(daemon=True) + self._RequestHandlerClass = RequestHandlerClass + # Server address ('', 0): listen on all ips and let OS pick a free port + self._server_address = server_address or ("", 0) + self._server = None + + def start(self): + self._server = http.server.HTTPServer(self._server_address, self._RequestHandlerClass) + self._log_status("start thread") + super().start() + + def run(self): + self._log_status("start serving") + self._server.serve_forever() + self._log_status("stop serving") + + def shutdown(self): + self._log_status("shut down thread") + self._server.shutdown() + + def server_address_info(self) -> Tuple[int, str, str]: + """ + Get server address info: (port, host_address, fully_qualified_domain_name) + """ + if self._server is None: + raise RuntimeError("Server is not set up yet") + return self._server.server_port, self._server.server_address[0], self._server.server_name + + def _log_status(self, message): + port, host, fqdn = self.server_address_info() + log.info("{c}: {m} (at {h}:{p}, {f})".format(c=self.__class__.__name__, m=message, h=host, p=port, f=fqdn)) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + self.join() + self._log_status("thread joined") + + +def create_timer() -> Callable[[], float]: + """Create a timer function that returns elapsed time since creation of the timer function""" + start = time.time() + + def elapsed(): + return time.time() - start + + return elapsed + + +def drain_queue( + queue: Queue, initial_timeout: float = 10, item_minimum: int = 1, tail_timeout=5, on_empty=lambda **kwargs: None +): + """ + Drain the given queue, requiring at least a given number of items (within an initial timeout). + + :param queue: queue to drain + :param initial_timeout: time in seconds within which a minimum number of items should be fetched + :param item_minimum: minimum number of items to fetch + :param tail_timeout: additional timeout to abort when queue doesn't get empty + :param on_empty: callable to call when/while queue is empty + :return: generator of items from the queue + """ + elapsed = create_timer() + + count = 0 + while True: + try: + yield queue.get(timeout=initial_timeout / 10) + count += 1 + except Empty: + on_empty(elapsed=elapsed(), count=count) + + if elapsed() > initial_timeout and count < item_minimum: + raise TimeoutError( + "Items after initial {t} timeout: {c} (<{m})".format(c=count, m=item_minimum, t=initial_timeout) + ) + if queue.empty() and count >= item_minimum: + break + if elapsed() > initial_timeout + tail_timeout: + warnings.warn("Queue still not empty after overall timeout: aborting.") + break + + +def random_string(length=32, characters: str = None): + """ + Build a random string from given characters (alphanumeric by default) + """ + # TODO: move this to a utils module? + characters = characters or (string.ascii_letters + string.digits) + return "".join(random.choice(characters) for _ in range(length)) + + +class OidcException(OpenEoClientException): + pass + + +class AccessTokenResult(NamedTuple): + """Container for result of access_token request.""" + + access_token: str + id_token: Optional[str] = None + refresh_token: Optional[str] = None + + +def jwt_decode(token: str) -> Tuple[dict, dict]: + """ + Poor man's JWT decoding + TODO: use a real library that also handles verification properly? + """ + + def _decode(data: str) -> dict: + decoded = base64.b64decode(data + "=" * (4 - len(data) % 4)).decode("ascii") + return json.loads(decoded) + + header, payload, signature = token.split(".") + return _decode(header), _decode(payload) + + +class DefaultOidcClientGrant(enum.Enum): + """ + Enum with possible values for "grant_types" field of default OIDC clients provided by backend. + """ + + IMPLICIT = "implicit" + AUTH_CODE = "authorization_code" + AUTH_CODE_PKCE = "authorization_code+pkce" + DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" + DEVICE_CODE_PKCE = "urn:ietf:params:oauth:grant-type:device_code+pkce" + REFRESH_TOKEN = "refresh_token" + + +# Type hint for function that checks if given list of OIDC grant types (DefaultOidcClientGrant enum values) +# fulfills a criterion. +GrantsChecker = Union[List[DefaultOidcClientGrant], Callable[[List[DefaultOidcClientGrant]], bool]] + + +class OidcProviderInfo: + """OpenID Connect Provider information, as provided by an openEO back-end (endpoint `/credentials/oidc`)""" + + def __init__( + self, + issuer: str = None, + discovery_url: str = None, + scopes: List[str] = None, + provider_id: str = None, + title: str = None, + default_clients: Union[List[dict], None] = None, + requests_session: Optional[requests.Session] = None, + ): + # TODO: id and title are required in the openEO API spec. + self.id = provider_id + self.title = title + if discovery_url: + self.discovery_url = discovery_url + elif issuer: + self.discovery_url = url_join(issuer, "/.well-known/openid-configuration") + else: + raise ValueError("At least `issuer` or `discovery_url` should be specified") + if not requests_session: + requests_session = requests.Session() + discovery_resp = requests_session.get(self.discovery_url, timeout=20) + discovery_resp.raise_for_status() + self.config = discovery_resp.json() + self.issuer = issuer or self.config["issuer"] + # Minimal set of scopes to request + self._supported_scopes = self.config.get("scopes_supported", ["openid"]) + self._scopes = {"openid"}.union(scopes or []).intersection(self._supported_scopes) + log.debug(f"Scopes: provider supported {self._supported_scopes} & backend desired {scopes} -> {self._scopes}") + self.default_clients = default_clients + + @classmethod + def from_dict(cls, data: dict) -> OidcProviderInfo: + return cls( + provider_id=data["id"], + title=data["title"], + issuer=data["issuer"], + scopes=data.get("scopes"), + default_clients=data.get("default_clients"), + ) + + def get_scopes_string(self, request_refresh_token: bool = False) -> str: + """ + Build "scope" string for authentication request. + + :param request_refresh_token: include "offline_access" scope (if supported), + which some OIDC providers require in order to return refresh token + :return: space separated scope listing as single string + """ + scopes = self._scopes + if request_refresh_token and "offline_access" in self._supported_scopes: + scopes = scopes | {"offline_access"} + log.debug("Using scopes: {s}".format(s=scopes)) + return " ".join(sorted(scopes)) + + def get_default_client_id(self, grant_check: GrantsChecker) -> Union[str, None]: + """ + Get first default client that supports (as stated by provider's `grant_types`) + the desired grant types (as implemented by `grant_check`) + """ + if isinstance(grant_check, list): + # Simple `grant_check` mode: just provide list of grants that all must be supported. + desired_grants = grant_check + grant_check = lambda grants: all(g in grants for g in desired_grants) + + def normalize_grants(grants: List[str]): + for grant in grants: + try: + yield DefaultOidcClientGrant(grant) + except ValueError: + log.warning(f"Invalid OIDC grant type {grant!r}.") + + for client in self.default_clients or []: + client_id = client.get("id") + supported_grants = client.get("grant_types") + supported_grants = list(normalize_grants(supported_grants)) + if client_id and supported_grants and grant_check(supported_grants): + return client_id + + +class OidcClientInfo: + """ + Simple container holding basic info of an OIDC client + """ + + __slots__ = ["client_id", "provider", "client_secret"] + + def __init__(self, client_id: str, provider: OidcProviderInfo, client_secret: Optional[str] = None): + self.client_id = client_id + self.provider = provider + self.client_secret = client_secret + # TODO: also info client type (desktop app, web app, SPA, ...)? + + # TODO: load from config file + + def guess_device_flow_pkce_support(self): + """Best effort guess if PKCE should be used for device auth grant""" + # Check if this client is also defined as default client with device_code+pkce + default_clients = [c for c in self.provider.default_clients or [] if c["id"] == self.client_id] + grant_types = set(g for c in default_clients for g in c.get("grant_types", [])) + return any("device_code+pkce" in g for g in grant_types) + + +class OidcAuthenticator: + """ + Base class for OpenID Connect authentication flows. + """ + + grant_type = NotImplemented + + def __init__( + self, + client_info: OidcClientInfo, + requests_session: Optional[requests.Session] = None, + ): + self._client_info = client_info + self._provider_config = client_info.provider.config + # TODO: check provider config (e.g. if grant type is supported) + self._requests = requests_session or requests.Session() + + @property + def client_info(self) -> OidcClientInfo: + return self._client_info + + @property + def client_id(self) -> str: + return self._client_info.client_id + + @property + def client_secret(self) -> str: + return self._client_info.client_secret + + @property + def provider_info(self) -> OidcProviderInfo: + return self._client_info.provider + + def get_tokens(self, request_refresh_token: bool = False) -> AccessTokenResult: + """Get access_token and possibly id_token+refresh_token.""" + result = self._do_token_post_request(post_data=self._get_token_endpoint_post_data()) + return self._get_access_token_result(result) + + def _get_token_endpoint_post_data(self) -> dict: + """Build POST data dict to send to token endpoint""" + return { + "grant_type": self.grant_type, + "client_id": self.client_id, + } + + def _do_token_post_request(self, post_data: dict) -> dict: + """Do POST to token endpoint to get access token""" + token_endpoint = self._provider_config["token_endpoint"] + log.info( + "Doing {g!r} token request {u!r} with post data fields {p!r} (client_id {c!r})".format( + g=self.grant_type, c=self.client_id, u=token_endpoint, p=list(post_data.keys()) + ) + ) + try: + resp = self._requests.post(url=token_endpoint, data=post_data) + except requests.exceptions.RequestException as e: + raise OidcException(f"Failed to retrieve access token at {token_endpoint!r}: {e!r}") from e + if resp.status_code != 200: + # TODO: are other status_code values valid too? + raise OidcException( + "Failed to retrieve access token at {u!r}: {s} {r!r} {t!r}".format( + s=resp.status_code, r=resp.reason, u=resp.url, t=resp.text + ) + ) + + result = resp.json() + return result + + def _get_access_token_result(self, data: dict, expected_nonce: str = None) -> AccessTokenResult: + """Parse JSON result from token request""" + redacted = { + k: v if k in ["expires_in", "refresh_expires_in", "token_type", "scope"] else "" + for k, v in data.items() + } + log.debug(f"Extracting access token result from token response {redacted}") + return AccessTokenResult( + access_token=self._extract_token(data, "access_token"), + id_token=self._extract_token(data, "id_token", expected_nonce=expected_nonce, allow_absent=True), + refresh_token=self._extract_token(data, "refresh_token", allow_absent=True), + ) + + @staticmethod + def _extract_token(data: dict, key: str, expected_nonce: str = None, allow_absent=False) -> Union[str, None]: + """ + Extract token of given type ("access_token", "id_token", "refresh_token") from a token JSON response + """ + try: + token = data[key] + except KeyError: + if allow_absent: + return + raise OidcException("No {k!r} in response".format(k=key)) + if expected_nonce: + # TODO: verify the JWT properly? + _, payload = jwt_decode(token) + if payload["nonce"] != expected_nonce: + raise OidcException("Invalid nonce in {k}".format(k=key)) + return token + + +class PkceCode: + """ + Simple container for PKCE code verifier and code challenge. + + PKCE, pronounced "pixy", is short for "Proof Key for Code Exchange". + Also see https://tools.ietf.org/html/rfc7636 + """ + + __slots__ = ["code_verifier", "code_challenge", "code_challenge_method"] + + def __init__(self): + self.code_verifier = random_string(64) + # Only SHA256 is supported for now. + self.code_challenge_method = "S256" + self.code_challenge = PkceCode.sha256_hash(self.code_verifier) + + @staticmethod + def sha256_hash(code: str) -> str: + """Apply SHA256 hash to code verifier to get code challenge""" + data = hashlib.sha256(code.encode("ascii")).digest() + return base64.urlsafe_b64encode(data).decode("ascii").replace("=", "") + + +class AuthCodeResult(NamedTuple): + auth_code: str + nonce: str + code_verifier: str + redirect_uri: str + + +class OidcAuthCodePkceAuthenticator(OidcAuthenticator): + """ + Implementation of OpenID Connect authentication using OAuth Authorization Code Flow with PKCE. + + This flow is to be used for interactive use cases (e.g. user is working in a Jupyter/IPython notebook). + + It goes roughly like this: + - A short living HTTP server is started in a side-thread to serve the redirect URI + that is required in this flow. + - A browser window/tab is opened showing the (third party) Identity Provider authorization endpoint + - (if not already:) User authenticates with the Identity Provider (e.g. with username and password) + - Identity Provider forwards to the redirect URI (which is served locally by the side-thread), + sending an authorization code (among others) along + - The request handler in the side thread captures the redirect and passes it to the main thread (through a queue) + - The main extracts the necessary information from the redirect request (like the authorization code) + and shuts down the side thread + - The authorization code is exchanged for an access code and id token + - The access code can be used as bearer token for subsequent API calls + + .. deprecated:: 0.19.0 + Usage of the Authorization Code flow is deprecated (because of its complexity) and will be removed. + """ + + grant_type = "authorization_code" + + TIMEOUT_DEFAULT = 60 + + def __init__( + self, + client_info: OidcClientInfo, + webbrowser_open: Callable = None, + timeout: int = None, + server_address: Tuple[str, int] = None, + requests_session: Optional[requests.Session] = None, + ): + super().__init__(client_info=client_info, requests_session=requests_session) + self._webbrowser_open = webbrowser_open or webbrowser.open + self._authentication_timeout = timeout or self.TIMEOUT_DEFAULT + self._server_address = server_address + + def _get_auth_code(self, request_refresh_token: bool = False) -> AuthCodeResult: + """ + Do OAuth authentication request and catch redirect to extract authentication code + :return: + """ + state = random_string(32) + nonce = random_string(21) + pkce = PkceCode() + + # Set up HTTP server (in separate thread) to catch OAuth redirect URL + callback_queue = Queue() + RequestHandlerClass = OAuthRedirectRequestHandler.with_queue(callback_queue) + http_server_thread = HttpServerThread( + RequestHandlerClass=RequestHandlerClass, server_address=self._server_address + ) + with http_server_thread: + port, host, fqdn = http_server_thread.server_address_info() + # TODO: use fully qualified domain name instead of "localhost"? + # Otherwise things won't work when the client is for example + # running in a remotely hosted Jupyter setup. + # Maybe even FQDN will not resolve properly in the user's browser + # and we need additional means to get a working hostname? + redirect_uri = "http://localhost:{p}".format(f=fqdn, p=port) + OAuthRedirectRequestHandler.PATH + log.info("Using OAuth redirect URI {u!r}".format(u=redirect_uri)) + + # Build authentication URL + auth_url = "{endpoint}?{params}".format( + endpoint=self._provider_config["authorization_endpoint"], + params=urllib.parse.urlencode( + { + "response_type": "code", + "client_id": self.client_id, + "scope": self._client_info.provider.get_scopes_string( + request_refresh_token=request_refresh_token + ), + "redirect_uri": redirect_uri, + "state": state, + "nonce": nonce, + "code_challenge": pkce.code_challenge, + "code_challenge_method": pkce.code_challenge_method, + } + ), + ) + log.info("Sending user to auth URL {u!r}".format(u=auth_url)) + # Open browser window/tab with authentication URL + self._webbrowser_open(auth_url) + + # TODO: show some feedback here that we are waiting browser based interaction here? + + try: + # Collect data from redirect uri + log.info("Waiting for request to redirect URI (timeout {t}s)".format(t=self._authentication_timeout)) + # TODO: When authentication fails (e.g. identity provider is down), this might hang the client + # (e.g. jupyter notebook). Is there a way to abort this? use signals? handle "abort" request? + callbacks = list( + drain_queue( + callback_queue, + initial_timeout=self._authentication_timeout, + on_empty=lambda **kwargs: log.info( + "No result yet (elapsed: {e:.2f}s)".format(e=kwargs.get("elapsed", 0)) + ), + ) + ) + except TimeoutError: + raise OidcException( + "Timeout: no request to redirect URI after {t}s".format(t=self._authentication_timeout) + ) + + if len(callbacks) != 1: + raise OidcException("Expected 1 OAuth redirect request, but got: {c}".format(c=len(callbacks))) + + # Parse OAuth redirect URL + redirect_request = callbacks[0] + log.debug("Parsing redirect request {r}".format(r=redirect_request)) + redirect_params = urllib.parse.parse_qs(urllib.parse.urlparse(redirect_request).query) + log.debug("Parsed redirect request: {p}".format(p=redirect_params)) + if "state" not in redirect_params or redirect_params["state"] != [state]: + raise OidcException("Invalid state") + if "code" not in redirect_params: + raise OidcException("No auth code in redirect") + auth_code = redirect_params["code"][0] + + return AuthCodeResult( + auth_code=auth_code, nonce=nonce, code_verifier=pkce.code_verifier, redirect_uri=redirect_uri + ) + + def get_tokens(self, request_refresh_token: bool = False) -> AccessTokenResult: + """ + Do OpenID authentication flow with PKCE: + get auth code and exchange for access and id token + """ + # Get auth code from authentication provider + auth_code_result = self._get_auth_code(request_refresh_token=request_refresh_token) + + # Exchange authentication code for access token + result = self._do_token_post_request( + post_data=dict_no_none( + grant_type=self.grant_type, + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uri=auth_code_result.redirect_uri, + code=auth_code_result.auth_code, + code_verifier=auth_code_result.code_verifier, + ) + ) + + return self._get_access_token_result(result, expected_nonce=auth_code_result.nonce) + + +class OidcClientCredentialsAuthenticator(OidcAuthenticator): + """ + Implementation of "Client Credentials" Flow. + """ + + grant_type = "client_credentials" + + def _get_token_endpoint_post_data(self) -> dict: + data = super()._get_token_endpoint_post_data() + data["client_secret"] = self.client_secret + data["scope"] = self._client_info.provider.get_scopes_string() + return data + + +class OidcResourceOwnerPasswordAuthenticator(OidcAuthenticator): + """ + Implementation of "Resource Owner Password Credentials" (ROPC) grant type. + + Note: This flow should only be used when end user owns (or highly trusts) the client code + and the password can be handled/stored/retrieved in a secure manner. + """ + + grant_type = "password" + + def __init__( + self, + client_info: OidcClientInfo, + username: str, + password: str, + requests_session: Optional[requests.Session] = None, + ): + super().__init__(client_info=client_info, requests_session=requests_session) + self._username = username + self._password = password + + def _get_token_endpoint_post_data(self) -> dict: + data = super()._get_token_endpoint_post_data() + data["client_secret"] = self.client_secret + data["scope"] = self._client_info.provider.get_scopes_string() + data["username"] = self._username + data["password"] = self._password + return data + + +class OidcRefreshTokenAuthenticator(OidcAuthenticator): + """ + Implementation of obtaining a new OpenID Connect access token through a refresh token. + """ + + grant_type = "refresh_token" + + def __init__( + self, + client_info: OidcClientInfo, + refresh_token: str, + requests_session: Optional[requests.Session] = None, + ): + super().__init__(client_info=client_info, requests_session=requests_session) + self._refresh_token = refresh_token + + def _get_token_endpoint_post_data(self) -> dict: + data = super()._get_token_endpoint_post_data() + if self.client_secret: + data["client_secret"] = self.client_secret + data["refresh_token"] = self._refresh_token + return data + + +class VerificationInfo(NamedTuple): + verification_uri: str + verification_uri_complete: Optional[str] + device_code: str + user_code: str + interval: int + + +def _like_print(display: Callable) -> Callable: + """Ensure that display function supports an `end` argument like `print`""" + if display is print or "end" in inspect.signature(display).parameters: + return display + else: + return lambda *args, end="\n", **kwargs: display(*args, **kwargs) + + +class _BasicDeviceCodePollUi: + """ + Basic (print + carriage return) implementation of the device code + polling loop UI (e.g. show progress bar and status). + """ + + def __init__( + self, + timeout: float, + elapsed: Callable[[], float], + max_width: int = 80, + display: Callable = print, + ): + self.timeout = timeout + self.elapsed = elapsed + self._max_width = max_width + self._status = "Authorization pending" + self._display = _like_print(display) + self._progress_bar = SimpleProgressBar(width=(max_width - 1) // 2) + + def _instructions(self, info: VerificationInfo) -> str: + if info.verification_uri_complete: + return f"Visit {info.verification_uri_complete} to authenticate." + else: + return f"Visit {info.verification_uri} and enter user code {info.user_code!r} to authenticate." + + def show_instructions(self, info: VerificationInfo) -> None: + self._display(self._instructions(info=info)) + + def set_status(self, status: str): + self._status = status + + def show_progress(self, status: Optional[str] = None, include_bar: bool = True): + if status: + self.set_status(status) + text = self._status + if include_bar: + progress_bar = self._progress_bar.get(fraction=1.0 - self.elapsed() / self.timeout) + text = f"{progress_bar} {text}" + self._display(f"{text[:self._max_width]: <{self._max_width}s}", end="\r") + + def close(self): + self._display("", end="\n") + + +class _JupyterDeviceCodePollUi(_BasicDeviceCodePollUi): + def __init__( + self, + timeout: float, + elapsed: Callable[[], float], + max_width: int = 80, + ): + super().__init__(timeout=timeout, elapsed=elapsed, max_width=max_width) + import IPython.display + + self._instructions_display = IPython.display.display({"text/html": " "}, raw=True, display_id=True) + self._progress_display = IPython.display.display({"text/html": " "}, raw=True, display_id=True) + + def _instructions(self, info: VerificationInfo) -> str: + url = info.verification_uri_complete if info.verification_uri_complete else info.verification_uri + instructions = ( + f'Visit {url}' + ) + instructions += f' 📋' + if not info.verification_uri_complete: + instructions += f" and enter user code {info.user_code!r}" + instructions += " to authenticate." + return instructions + + def show_instructions(self, info: VerificationInfo) -> None: + self._instructions_display.update({"text/html": self._instructions(info=info)}, raw=True) + + def show_progress(self, status: Optional[str] = None, include_bar: bool = True): + if status: + self.set_status(status) + icon = self._status_icon(self._status) + text = f"{icon} {self._status}" + if include_bar: + progress_bar = self._progress_bar.get(fraction=1.0 - self.elapsed() / self.timeout) + text = f"{progress_bar} {text}" + self._progress_display.update({"text/html": text}, raw=True) + + def _status_icon(self, status: str) -> str: + status = status.lower() + if "polling" in status or "pending" in status: + return "\u231B" # Hourglass + elif "success" in status: + return "\u2705" # Green check mark + elif "timed out" in status: + return "\u274C" # Red cross mark + else: + return "" + + def close(self): + pass + + +class OidcDeviceCodePollTimeout(OidcException): + pass + + +class OidcDeviceAuthenticator(OidcAuthenticator): + """ + Implementation of OAuth Device Authorization grant/flow + """ + + grant_type = "urn:ietf:params:oauth:grant-type:device_code" + + DEFAULT_MAX_POLL_TIME = 5 * 60 + + def __init__( + self, + client_info: OidcClientInfo, + display: Callable[[str], None] = print, + device_code_url: Optional[str] = None, + max_poll_time: float = DEFAULT_MAX_POLL_TIME, + use_pkce: Optional[bool] = None, + requests_session: Optional[requests.Session] = None, + ): + super().__init__(client_info=client_info, requests_session=requests_session) + self._display = display + # Allow to specify/override device code URL for cases when it is not available in OIDC discovery doc. + self._device_code_url = device_code_url or self._provider_config.get("device_authorization_endpoint") + if not self._device_code_url: + raise OidcException("No support for device authorization grant") + self._max_poll_time = max_poll_time + if use_pkce is None: + use_pkce = client_info.client_secret is None and client_info.guess_device_flow_pkce_support() + self._pkce = PkceCode() if use_pkce else None + + def _get_verification_info(self, request_refresh_token: bool = False) -> VerificationInfo: + """Get verification URL and user code""" + post_data = { + "client_id": self.client_id, + "scope": self._client_info.provider.get_scopes_string(request_refresh_token=request_refresh_token), + } + if self._pkce: + post_data["code_challenge"] = (self._pkce.code_challenge,) + post_data["code_challenge_method"] = self._pkce.code_challenge_method + resp = self._requests.post(url=self._device_code_url, data=post_data) + if resp.status_code != 200: + raise OidcException( + "Failed to get verification URL and user code from {u!r}: {s} {r!r} {t!r}".format( + s=resp.status_code, r=resp.reason, u=resp.url, t=resp.text + ) + ) + try: + data = resp.json() + verification_info = VerificationInfo( + # Google OAuth/OIDC implementation uses non standard "verification_url" instead of "verification_uri" + verification_uri=data["verification_uri"] if "verification_uri" in data else data["verification_url"], + # verification_uri_complete is optional, will be None if this key is not present + verification_uri_complete=data.get("verification_uri_complete"), + device_code=data["device_code"], + user_code=data["user_code"], + interval=data.get("interval", 5), + ) + except Exception as e: + raise OidcException("Failed to parse device authorization request: {e!r}".format(e=e)) + log.debug("Verification info: %r", verification_info) + return verification_info + + def get_tokens(self, request_refresh_token: bool = False) -> AccessTokenResult: + # Get verification url and user code + verification_info = self._get_verification_info(request_refresh_token=request_refresh_token) + + # Poll token endpoint + token_endpoint = self._provider_config["token_endpoint"] + post_data = { + "client_id": self.client_id, + "device_code": verification_info.device_code, + "grant_type": self.grant_type, + } + if self._pkce: + post_data["code_verifier"] = self._pkce.code_verifier + else: + post_data["client_secret"] = self.client_secret + + poll_interval = verification_info.interval + log.debug("Start polling token endpoint (interval {i}s)".format(i=poll_interval)) + + elapsed = create_timer() + next_poll = elapsed() + poll_interval + # TODO: let poll UI determine sleep interval? + sleep = clip(self._max_poll_time / 100, min=1, max=5) + + if in_jupyter_context(): + poll_ui = _JupyterDeviceCodePollUi(timeout=self._max_poll_time, elapsed=elapsed) + else: + poll_ui = _BasicDeviceCodePollUi(timeout=self._max_poll_time, elapsed=elapsed, display=self._display) + poll_ui.show_instructions(info=verification_info) + + with contextlib.closing(poll_ui): + while elapsed() <= self._max_poll_time: + poll_ui.show_progress() + time.sleep(sleep) + + if elapsed() >= next_poll: + log.debug( + f"Doing {self.grant_type!r} token request {token_endpoint!r} with post data fields {list(post_data.keys())!r} (client_id {self.client_id!r})" + ) + poll_ui.show_progress(status="Polling") + # TODO: skip occasional failing requests? (e.g. see `SkipIntermittentFailures` from openeo-aggregator) + resp = self._requests.post(url=token_endpoint, data=post_data, timeout=5) + if resp.status_code == 200: + log.info(f"[{elapsed():5.1f}s] Authorized successfully.") + poll_ui.show_progress(status="Authorized successfully", include_bar=False) + return self._get_access_token_result(data=resp.json()) + else: + try: + error = resp.json()["error"] + except Exception: + error = "unknown" + log.info(f"[{elapsed():5.1f}s] not authorized yet: {error}") + if error == "authorization_pending": + poll_ui.show_progress(status="Authorization pending") + elif error == "slow_down": + poll_ui.show_progress(status="Slowing down") + poll_interval += 5 + else: + # TODO: skip occasional glitches (e.g. see `SkipIntermittentFailures` from openeo-aggregator) + raise OidcException( + f"Failed to retrieve access token at {token_endpoint!r}: {resp.status_code} {resp.reason!r} {resp.text!r}" + ) + next_poll = elapsed() + poll_interval + + poll_ui.show_progress(status="Timed out", include_bar=False) + raise OidcDeviceCodePollTimeout(f"Timeout ({self._max_poll_time:.1f}s) while polling for access token.") diff --git a/lib/openeo/rest/auth/testing.py b/lib/openeo/rest/auth/testing.py new file mode 100644 index 000000000..651abd21f --- /dev/null +++ b/lib/openeo/rest/auth/testing.py @@ -0,0 +1,292 @@ +""" +Helpers, mocks for testing (OIDC) authentication +""" + +import base64 +import contextlib +import json +import urllib.parse +import uuid +from typing import List, Optional, Union +from unittest import mock + +import requests +import requests_mock.request + +from openeo.rest.auth.oidc import PkceCode, random_string +from openeo.util import dict_no_none, url_join + +DEVICE_CODE_POLL_INTERVAL = 2 + + +# Sentinel object to indicate that a field should be absent. +ABSENT = object() + + +class OidcMock: + """ + Fixture/mock to act as stand-in OIDC provider to test OIDC flows + """ + + def __init__( + self, + requests_mock: requests_mock.Mocker, + *, + expected_grant_type: Optional[str] = None, + oidc_issuer: str = "https://oidc.test", + expected_client_id: str = "myclient", + expected_fields: dict = None, + state: dict = None, + scopes_supported: List[str] = None, + device_code_flow_support: bool = True, + oidc_discovery_url: Optional[str] = None, + support_verification_uri_complete: bool = False, + ): + self.requests_mock = requests_mock + self.oidc_issuer = oidc_issuer + self.expected_grant_type = expected_grant_type + self.grant_request_history = [] + self.expected_client_id = expected_client_id + self.expected_fields = expected_fields or {} + self.expected_authorization_code = None + self.authorization_endpoint = url_join(self.oidc_issuer, "/auth") + self.token_endpoint = url_join(self.oidc_issuer, "/token") + self.device_code_endpoint = url_join(self.oidc_issuer, "/device_code") if device_code_flow_support else None + self.state = state or {} + self.scopes_supported = scopes_supported or ["openid", "email", "profile"] + self.support_verification_uri_complete = support_verification_uri_complete + self.mocks = {} + + oidc_discovery_url = oidc_discovery_url or url_join(oidc_issuer, "/.well-known/openid-configuration") + self.mocks["oidc_discovery"] = self.requests_mock.get( + oidc_discovery_url, + text=json.dumps( + dict_no_none( + { + # Rudimentary OpenID Connect discovery document + "issuer": self.oidc_issuer, + "authorization_endpoint": self.authorization_endpoint, + "token_endpoint": self.token_endpoint, + "device_authorization_endpoint": self.device_code_endpoint, + "scopes_supported": self.scopes_supported, + } + ) + ), + ) + self.mocks["token_endpoint"] = self.requests_mock.post(self.token_endpoint, text=self.token_callback) + + if self.device_code_endpoint: + self.mocks["device_code_endpoint"] = self.requests_mock.post( + self.device_code_endpoint, text=self.device_code_callback + ) + + def webbrowser_open(self, url: str): + """Doing fake browser and Oauth Provider handling here""" + assert url.startswith(self.authorization_endpoint) + params = self._get_query_params(url=url) + assert params["client_id"] == self.expected_client_id + assert params["response_type"] == "code" + assert params["scope"] == self.expected_fields["scope"] + for key in ["state", "nonce", "code_challenge", "redirect_uri", "scope"]: + self.state[key] = params[key] + redirect_uri = params["redirect_uri"] + # Don't mock the request to the redirect URI (it is hosted by the temporary web server in separate thread) + self.requests_mock.get(redirect_uri, real_http=True) + self.expected_authorization_code = "6uthc0d3" + requests.get( + redirect_uri, + params={"state": params["state"], "code": self.expected_authorization_code}, + ) + + def token_callback(self, request: requests_mock.request._RequestObjectProxy, context): + params = self._get_query_params(query=request.text) + grant_type = params["grant_type"] + self.grant_request_history.append({"grant_type": grant_type}) + if self.expected_grant_type: + assert grant_type == self.expected_grant_type + callback = { + "authorization_code": self.token_callback_authorization_code, + "client_credentials": self.token_callback_client_credentials, + "password": self.token_callback_resource_owner_password_credentials, + "urn:ietf:params:oauth:grant-type:device_code": self.token_callback_device_code, + "refresh_token": self.token_callback_refresh_token, + }[grant_type] + result = callback(params=params, context=context) + try: + result_decoded = json.loads(result) + self.grant_request_history[-1]["response"] = result_decoded + except json.JSONDecodeError: + self.grant_request_history[-1]["response"] = result + return result + + def token_callback_authorization_code(self, params: dict, context): + """Fake code to token exchange by Oauth Provider""" + assert params["client_id"] == self.expected_client_id + assert params["grant_type"] == "authorization_code" + assert self.state["code_challenge"] == PkceCode.sha256_hash(params["code_verifier"]) + assert params["code"] == self.expected_authorization_code + assert params["redirect_uri"] == self.state["redirect_uri"] + return self._build_token_response() + + def token_callback_client_credentials(self, params: dict, context): + assert params["client_id"] == self.expected_client_id + assert params["grant_type"] == "client_credentials" + assert params["scope"] == self.expected_fields["scope"] + assert params["client_secret"] == self.expected_fields["client_secret"] + return self._build_token_response(include_id_token=False, include_refresh_token=False) + + def token_callback_resource_owner_password_credentials(self, params: dict, context): + assert params["client_id"] == self.expected_client_id + assert params["grant_type"] == "password" + assert params["client_secret"] == self.expected_fields["client_secret"] + assert params["username"] == self.expected_fields["username"] + assert params["password"] == self.expected_fields["password"] + assert params["scope"] == self.expected_fields["scope"] + return self._build_token_response() + + def device_code_callback(self, request: requests_mock.request._RequestObjectProxy, context): + params = self._get_query_params(query=request.text) + assert params["client_id"] == self.expected_client_id + assert params["scope"] == self.expected_fields["scope"] + self.state["device_code"] = random_string() + self.state["user_code"] = random_string(length=6).upper() + self.state["scope"] = params["scope"] + if "code_challenge" in self.expected_fields: + expect_code_challenge = self.expected_fields.get("code_challenge") + if expect_code_challenge in [True]: + assert "code_challenge" in params + self.state["code_challenge"] = params["code_challenge"] + elif expect_code_challenge in [False, ABSENT]: + assert "code_challenge" not in params + else: + raise ValueError(expect_code_challenge) + + response = { + # TODO: also verification_url (google tweak) + "verification_uri": url_join(self.oidc_issuer, "/dc"), + "device_code": self.state["device_code"], + "user_code": self.state["user_code"], + "interval": DEVICE_CODE_POLL_INTERVAL, + } + if self.support_verification_uri_complete: + response["verification_uri_complete"] = ( + response["verification_uri"] + f"?user_code={self.state['user_code']}" + ) + return json.dumps(response) + + def token_callback_device_code(self, params: dict, context): + assert params["client_id"] == self.expected_client_id + expected_client_secret = self.expected_fields.get("client_secret") + if expected_client_secret: + assert params["client_secret"] == expected_client_secret + else: + assert "client_secret" not in params + expect_code_verifier = self.expected_fields.get("code_verifier") + if expect_code_verifier in [True]: + assert PkceCode.sha256_hash(params["code_verifier"]) == self.state["code_challenge"] + self.state["code_verifier"] = params["code_verifier"] + elif expect_code_verifier in [False, None, ABSENT]: + assert "code_verifier" not in params + assert "code_challenge" not in self.state + else: + raise ValueError(expect_code_verifier) + assert params["device_code"] == self.state["device_code"] + assert params["grant_type"] == "urn:ietf:params:oauth:grant-type:device_code" + # Fail with pending/too fast? + try: + result = self.state["device_code_callback_timeline"].pop(0) + except Exception: + result = "rest in peace" + if result == "great success": + return self._build_token_response() + else: + context.status_code = 400 + return json.dumps({"error": result}) + + def token_callback_refresh_token(self, params: dict, context): + assert params["client_id"] == self.expected_client_id + assert params["grant_type"] == "refresh_token" + if "client_secret" in self.expected_fields: + assert params["client_secret"] == self.expected_fields["client_secret"] + if params["refresh_token"] != self.expected_fields["refresh_token"]: + context.status_code = 401 + return json.dumps({"error": "invalid refresh token"}) + assert params["refresh_token"] == self.expected_fields["refresh_token"] + return self._build_token_response(include_id_token=False, include_refresh_token=False) + + @staticmethod + def _get_query_params(*, url=None, query=None): + """Helper to extract query params from an url or query string""" + if not query: + query = urllib.parse.urlparse(url).query + params = {} + for param, values in urllib.parse.parse_qs(query).items(): + assert len(values) == 1 + params[param] = values[0] + return params + + @staticmethod + def _jwt_encode(header: dict, payload: dict, signature="s1gn6tur3"): + """Poor man's JWT encoding (just for unit testing purposes)""" + + def encode(d): + return base64.urlsafe_b64encode(json.dumps(d).encode("ascii")).decode("ascii").replace("=", "") + + return ".".join([encode(header), encode(payload), signature]) + + def _build_token_response( + self, + sub="123", + name="john", + include_id_token=True, + include_refresh_token: Optional[bool] = None, + ) -> str: + """Build JSON serialized access/id/refresh token response (and store tokens for use in assertions)""" + access_token = self._jwt_encode( + header={}, + payload=dict_no_none( + sub=sub, + name=name, + nonce=self.state.get("nonce"), + _uuid=uuid.uuid4().hex, + ), + ) + res = {"access_token": access_token} + + # Attempt to simulate real world refresh token support. + if include_refresh_token is None: + if "offline_access" in self.scopes_supported: + # "offline_access" scope as suggested in spec + # (https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess) + # Implemented by Microsoft, EGI Check-in + include_refresh_token = "offline_access" in self.state.get("scope", "").split(" ") + else: + # Google OAuth style: no support for "offline_access", return refresh token automatically? + include_refresh_token = True + if include_refresh_token: + res["refresh_token"] = self._jwt_encode(header={}, payload={"foo": "refresh", "_uuid": uuid.uuid4().hex}) + if include_id_token: + res["id_token"] = access_token + self.state.update(res) + self.state.update(name=name, sub=sub) + return json.dumps(res) + + def validate_access_token(self, access_token: str): + if access_token == self.state["access_token"]: + return {"user_id": self.state["name"], "sub": self.state["sub"]} + raise LookupError("Invalid access token") + + def invalidate_access_token(self): + self.state["access_token"] = "***invalidated***" + + def get_request_history( + self, url: Optional[str] = None, method: Optional[str] = None + ) -> List[requests_mock.request._RequestObjectProxy]: + """Get mocked request history: requests with given method/url.""" + if url and url.startswith("/"): + url = url_join(self.oidc_issuer, url) + return [ + r + for r in self.requests_mock.request_history + if (method is None or method.lower() == r.method.lower()) and (url is None or url == r.url) + ] diff --git a/lib/openeo/rest/connection.py b/lib/openeo/rest/connection.py new file mode 100644 index 000000000..79ee478f8 --- /dev/null +++ b/lib/openeo/rest/connection.py @@ -0,0 +1,2015 @@ +""" +This module provides a Connection object to manage and persist settings when interacting with the OpenEO API. +""" +from __future__ import annotations + +import datetime +import json +import logging +import os +import shlex +import sys +import warnings +from collections import OrderedDict +from pathlib import Path, PurePosixPath +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) + +import requests +import shapely.geometry.base +from requests import Response +from requests.auth import AuthBase, HTTPBasicAuth + +import openeo +from openeo.capabilities import ApiVersionException, ComparableVersion +from openeo.config import config_log, get_config_option +from openeo.internal.documentation import openeo_process +from openeo.internal.graph_building import FlatGraphableMixin, PGNode, as_flat_graph +from openeo.internal.jupyter import VisualDict, VisualList +from openeo.internal.processes.builder import ProcessBuilderBase +from openeo.internal.warnings import deprecated, legacy_alias +from openeo.metadata import ( + Band, + BandDimension, + CollectionMetadata, + SpatialDimension, + TemporalDimension, +) +from openeo.rest import ( + DEFAULT_DOWNLOAD_CHUNK_SIZE, + CapabilitiesException, + OpenEoApiError, + OpenEoApiPlainError, + OpenEoClientException, + OpenEoRestError, +) +from openeo.rest._datacube import _ProcessGraphAbstraction, build_child_callback +from openeo.rest.auth.auth import BasicBearerAuth, BearerAuth, NullAuth, OidcBearerAuth +from openeo.rest.auth.config import AuthConfig, RefreshTokenStore +from openeo.rest.auth.oidc import ( + DefaultOidcClientGrant, + GrantsChecker, + OidcAuthCodePkceAuthenticator, + OidcAuthenticator, + OidcClientCredentialsAuthenticator, + OidcClientInfo, + OidcDeviceAuthenticator, + OidcException, + OidcProviderInfo, + OidcRefreshTokenAuthenticator, + OidcResourceOwnerPasswordAuthenticator, +) +from openeo.rest.datacube import DataCube, InputDate +from openeo.rest.graph_building import CollectionProperty +from openeo.rest.job import BatchJob, RESTJob +from openeo.rest.mlmodel import MlModel +from openeo.rest.rest_capabilities import RESTCapabilities +from openeo.rest.service import Service +from openeo.rest.udp import Parameter, RESTUserDefinedProcess +from openeo.rest.userfile import UserFile +from openeo.rest.vectorcube import VectorCube +from openeo.util import ( + ContextTimer, + LazyLoadCache, + dict_no_none, + ensure_list, + load_json_resource, + repr_truncate, + rfc3339, + str_truncate, + url_join, +) + +_log = logging.getLogger(__name__) + +# Default timeouts for requests +# TODO: get default_timeout from config? +DEFAULT_TIMEOUT = 20 * 60 +DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE = 30 * 60 + + +class RestApiConnection: + """Base connection class implementing generic REST API request functionality""" + + def __init__( + self, + root_url: str, + auth: Optional[AuthBase] = None, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + slow_response_threshold: Optional[float] = None, + ): + self._root_url = root_url + self.auth = auth or NullAuth() + self.session = session or requests.Session() + self.default_timeout = default_timeout or DEFAULT_TIMEOUT + self.default_headers = { + "User-Agent": "openeo-python-client/{cv} {py}/{pv} {pl}".format( + cv=openeo.client_version(), + py=sys.implementation.name, pv=".".join(map(str, sys.version_info[:3])), + pl=sys.platform + ) + } + self.slow_response_threshold = slow_response_threshold + + @property + def root_url(self): + return self._root_url + + def build_url(self, path: str): + return url_join(self._root_url, path) + + def _merged_headers(self, headers: dict) -> dict: + """Merge default headers with given headers""" + result = self.default_headers.copy() + if headers: + result.update(headers) + return result + + def _is_external(self, url: str) -> bool: + """Check if given url is external (not under root url)""" + root = self.root_url.rstrip("/") + return not (url == root or url.startswith(root + '/')) + + def request( + self, + method: str, + path: str, + *, + headers: Optional[dict] = None, + auth: Optional[AuthBase] = None, + check_error: bool = True, + expected_status: Optional[Union[int, Iterable[int]]] = None, + **kwargs, + ): + """Generic request send""" + url = self.build_url(path) + # Don't send default auth headers to external domains. + auth = auth or (self.auth if not self._is_external(url) else None) + slow_response_threshold = kwargs.pop("slow_response_threshold", self.slow_response_threshold) + if _log.isEnabledFor(logging.DEBUG): + _log.debug("Request `{m} {u}` with headers {h}, auth {a}, kwargs {k}".format( + m=method.upper(), u=url, h=headers and headers.keys(), a=type(auth).__name__, k=list(kwargs.keys())) + ) + with ContextTimer() as timer: + resp = self.session.request( + method=method, + url=url, + headers=self._merged_headers(headers), + auth=auth, + timeout=kwargs.pop("timeout", self.default_timeout), + **kwargs + ) + if slow_response_threshold and timer.elapsed() > slow_response_threshold: + _log.warning("Slow response: `{m} {u}` took {e:.2f}s (>{t:.2f}s)".format( + m=method.upper(), u=str_truncate(url, width=64), + e=timer.elapsed(), t=slow_response_threshold + )) + if _log.isEnabledFor(logging.DEBUG): + _log.debug( + f"openEO request `{resp.request.method} {resp.request.path_url}` -> response {resp.status_code} headers {resp.headers!r}" + ) + # Check for API errors and unexpected HTTP status codes as desired. + status = resp.status_code + expected_status = ensure_list(expected_status) if expected_status else [] + if check_error and status >= 400 and status not in expected_status: + self._raise_api_error(resp) + if expected_status and status not in expected_status: + raise OpenEoRestError("Got status code {s!r} for `{m} {p}` (expected {e!r}) with body {body}".format( + m=method.upper(), p=path, s=status, e=expected_status, body=resp.text) + ) + return resp + + def _raise_api_error(self, response: requests.Response): + """Convert API error response to Python exception""" + status_code = response.status_code + try: + info = response.json() + except Exception: + info = None + + # Valid JSON object with "code" and "message" fields indicates a proper openEO API error. + if isinstance(info, dict): + error_code = info.get("code") + error_message = info.get("message") + if error_code and isinstance(error_code, str) and error_message and isinstance(error_message, str): + raise OpenEoApiError( + http_status_code=status_code, + code=error_code, + message=error_message, + id=info.get("id"), + url=info.get("url"), + ) + + # Failed to parse it as a compliant openEO API error: show body as-is in the exception. + text = response.text + error_message = None + _log.warning(f"Failed to parse API error response: [{status_code}] {text!r} (headers: {response.headers})") + + # TODO: eliminate this VITO-backend specific error massaging? + if status_code == 502 and "Proxy Error" in text: + error_message = ( + "Received 502 Proxy Error." + " This typically happens when a synchronous openEO processing request takes too long and is aborted." + " Consider using a batch job instead." + ) + + raise OpenEoApiPlainError(message=text, http_status_code=status_code, error_message=error_message) + + def get(self, path: str, stream: bool = False, auth: Optional[AuthBase] = None, **kwargs) -> Response: + """ + Do GET request to REST API. + + :param path: API path (without root url) + :param stream: True if the get request should be streamed, else False + :param auth: optional custom authentication to use instead of the default one + :return: response: Response + """ + return self.request("get", path=path, stream=stream, auth=auth, **kwargs) + + def post(self, path: str, json: Optional[dict] = None, **kwargs) -> Response: + """ + Do POST request to REST API. + + :param path: API path (without root url) + :param json: Data (as dictionary) to be posted with JSON encoding) + :return: response: Response + """ + return self.request("post", path=path, json=json, allow_redirects=False, **kwargs) + + def delete(self, path: str, **kwargs) -> Response: + """ + Do DELETE request to REST API. + + :param path: API path (without root url) + :return: response: Response + """ + return self.request("delete", path=path, allow_redirects=False, **kwargs) + + def patch(self, path: str, **kwargs) -> Response: + """ + Do PATCH request to REST API. + + :param path: API path (without root url) + :return: response: Response + """ + return self.request("patch", path=path, allow_redirects=False, **kwargs) + + def put(self, path: str, headers: Optional[dict] = None, data: Optional[dict] = None, **kwargs) -> Response: + """ + Do PUT request to REST API. + + :param path: API path (without root url) + :param headers: headers that gets added to the request. + :param data: data that gets added to the request. + :return: response: Response + """ + return self.request("put", path=path, data=data, headers=headers, allow_redirects=False, **kwargs) + + def __repr__(self): + return "<{c} to {r!r} with {a}>".format(c=type(self).__name__, r=self._root_url, a=type(self.auth).__name__) + + +class Connection(RestApiConnection): + """ + Connection to an openEO backend. + + :param url: Backend root url + :param session: Optional ``requests.Session`` object to use for requests. + :param default_timeout: Default timeout for requests in seconds. + :param auto_validate: toggle to automatically validate process graphs before execution + :param slow_response_threshold: Optional threshold in seconds + to consider a response as slow and log a warning. + :param auth_config: Optional :class:`AuthConfig` object + to fetch authentication related configuration from. + :param refresh_token_store: For advanced usage: + custom :class:`RefreshTokenStore` object + to use for storing/loading refresh tokens. + :param oidc_auth_renewer: For advanced usage: + optional :class:`OidcAuthenticator` object to use for renewing OIDC tokens. + :param auth: Optional ``requests.auth.AuthBase`` object to use for requests. + Usage of this parameter is deprecated, use the specific authentication methods instead. + """ + + _MINIMUM_API_VERSION = ComparableVersion("1.0.0") + + def __init__( + self, + url: str, + *, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auto_validate: bool = True, + slow_response_threshold: Optional[float] = None, + auth_config: Optional[AuthConfig] = None, + refresh_token_store: Optional[RefreshTokenStore] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + auth: Optional[AuthBase] = None, + ): + if "://" not in url: + url = "https://" + url + self._orig_url = url + super().__init__( + root_url=self.version_discovery(url, session=session, timeout=default_timeout), + auth=auth, session=session, default_timeout=default_timeout, + slow_response_threshold=slow_response_threshold, + ) + self._capabilities_cache = LazyLoadCache() + + # Initial API version check. + self._api_version.require_at_least(self._MINIMUM_API_VERSION) + + self._auth_config = auth_config + self._refresh_token_store = refresh_token_store + self._oidc_auth_renewer = oidc_auth_renewer + self._auto_validate = auto_validate + + @classmethod + def version_discovery( + cls, url: str, session: Optional[requests.Session] = None, timeout: Optional[int] = None + ) -> str: + """ + Do automatic openEO API version discovery from given url, using a "well-known URI" strategy. + + :param url: initial backend url (not including "/.well-known/openeo") + :return: root url of highest supported backend version + """ + try: + connection = RestApiConnection(url, session=session) + well_known_url_response = connection.get("/.well-known/openeo", timeout=timeout) + assert well_known_url_response.status_code == 200 + versions = well_known_url_response.json()["versions"] + supported_versions = [v for v in versions if cls._MINIMUM_API_VERSION <= v["api_version"]] + assert supported_versions + production_versions = [v for v in supported_versions if v.get("production", True)] + highest_version = max(production_versions or supported_versions, key=lambda v: v["api_version"]) + _log.debug("Highest supported version available in backend: %s" % highest_version) + return highest_version['url'] + except Exception: + # Be very lenient about failing on the well-known URI strategy. + return url + + def _get_auth_config(self) -> AuthConfig: + if self._auth_config is None: + self._auth_config = AuthConfig() + return self._auth_config + + def _get_refresh_token_store(self) -> RefreshTokenStore: + if self._refresh_token_store is None: + self._refresh_token_store = RefreshTokenStore() + return self._refresh_token_store + + def authenticate_basic(self, username: Optional[str] = None, password: Optional[str] = None) -> Connection: + """ + Authenticate a user to the backend using basic username and password. + + :param username: User name + :param password: User passphrase + """ + if not self.capabilities().supports_endpoint("/credentials/basic", method="GET"): + raise OpenEoClientException("This openEO back-end does not support basic authentication.") + if username is None: + username, password = self._get_auth_config().get_basic_auth(backend=self._orig_url) + if username is None: + raise OpenEoClientException("No username/password given or found.") + + resp = self.get( + '/credentials/basic', + # /credentials/basic is the only endpoint that expects a Basic HTTP auth + auth=HTTPBasicAuth(username, password) + ).json() + # Switch to bearer based authentication in further requests. + self.auth = BasicBearerAuth(access_token=resp["access_token"]) + return self + + def _get_oidc_provider( + self, provider_id: Union[str, None] = None, parse_info: bool = True + ) -> Tuple[str, Union[OidcProviderInfo, None]]: + """ + Get provider id and info, based on context. + If provider_id is given, verify it against backend's list of providers. + If not given, find a suitable provider based on env vars, config or backend's default. + + :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). + Can be None if there is just one provider. + :param parse_info: whether to parse the provider info into an :py:class:`OidcProviderInfo` object + (which involves a ".well-known/openid-configuration" request) + :return: resolved/verified provider_id and provider info object (unless ``parse_info`` is False) + """ + oidc_info = self.get("/credentials/oidc", expected_status=200).json() + providers = OrderedDict((p["id"], p) for p in oidc_info["providers"]) + if len(providers) < 1: + raise OpenEoClientException("Backend lists no OIDC providers.") + _log.info("Found OIDC providers: {p}".format(p=list(providers.keys()))) + + # TODO: also support specifying provider through issuer URL? + provider_id_from_env = os.environ.get("OPENEO_AUTH_PROVIDER_ID") + + if provider_id: + if provider_id not in providers: + raise OpenEoClientException( + "Requested OIDC provider {r!r} not available. Should be one of {p}.".format( + r=provider_id, p=list(providers.keys()) + ) + ) + provider = providers[provider_id] + elif provider_id_from_env and provider_id_from_env in providers: + _log.info(f"Using provider_id {provider_id_from_env!r} from OPENEO_AUTH_PROVIDER_ID env var") + provider_id = provider_id_from_env + provider = providers[provider_id] + elif len(providers) == 1: + provider_id, provider = providers.popitem() + _log.info( + f"No OIDC provider given, but only one available: {provider_id!r}. Using that one." + ) + else: + # Check if there is a single provider in the config to use. + backend = self._orig_url + provider_configs = self._get_auth_config().get_oidc_provider_configs( + backend=backend + ) + intersection = set(provider_configs.keys()).intersection(providers.keys()) + if len(intersection) == 1: + provider_id = intersection.pop() + provider = providers[provider_id] + _log.info( + f"No OIDC provider given, but only one in config (for backend {backend!r}): {provider_id!r}. Using that one." + ) + else: + provider_id, provider = providers.popitem(last=False) + _log.info( + f"No OIDC provider given. Using first provider {provider_id!r} as advertised by backend." + ) + + provider_info = OidcProviderInfo.from_dict(provider) if parse_info else None + + return provider_id, provider_info + + def _get_oidc_provider_and_client_info( + self, + provider_id: str, + client_id: Union[str, None] = None, + client_secret: Union[str, None] = None, + default_client_grant_check: Union[None, GrantsChecker] = None, + ) -> Tuple[str, OidcClientInfo]: + """ + Resolve provider_id and client info (as given or from config) + + :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). + Can be None if there is just one provider. + + :return: OIDC provider id and client info + """ + provider_id, provider = self._get_oidc_provider(provider_id) + + if client_id is None: + _log.debug("No client_id: checking config for preferred client_id") + client_id, client_secret = self._get_auth_config().get_oidc_client_configs( + backend=self._orig_url, provider_id=provider_id + ) + if client_id: + _log.info("Using client_id {c!r} from config (provider {p!r})".format(c=client_id, p=provider_id)) + if client_id is None and default_client_grant_check: + # Try "default_clients" from backend's provider info. + _log.debug("No client_id given: checking default clients in backend's provider info") + client_id = provider.get_default_client_id(grant_check=default_client_grant_check) + if client_id: + _log.info("Using default client_id {c!r} from OIDC provider {p!r} info.".format( + c=client_id, p=provider_id + )) + if client_id is None: + raise OpenEoClientException("No client_id found.") + + client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider) + + return provider_id, client_info + + def _authenticate_oidc( + self, + authenticator: OidcAuthenticator, + *, + provider_id: str, + store_refresh_token: bool = False, + fallback_refresh_token_to_store: Optional[str] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + ) -> Connection: + """ + Authenticate through OIDC and set up bearer token (based on OIDC access_token) for further requests. + """ + tokens = authenticator.get_tokens(request_refresh_token=store_refresh_token) + _log.info("Obtained tokens: {t}".format(t=[k for k, v in tokens._asdict().items() if v])) + if store_refresh_token: + refresh_token = tokens.refresh_token or fallback_refresh_token_to_store + if refresh_token: + self._get_refresh_token_store().set_refresh_token( + issuer=authenticator.provider_info.issuer, + client_id=authenticator.client_id, + refresh_token=refresh_token + ) + if not oidc_auth_renewer: + oidc_auth_renewer = OidcRefreshTokenAuthenticator( + client_info=authenticator.client_info, refresh_token=refresh_token + ) + else: + _log.warning("No OIDC refresh token to store.") + token = tokens.access_token + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token) + self._oidc_auth_renewer = oidc_auth_renewer + return self + + def authenticate_oidc_authorization_code( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + timeout: Optional[int] = None, + server_address: Optional[Tuple[str, int]] = None, + webbrowser_open: Optional[Callable] = None, + store_refresh_token=False, + ) -> Connection: + """ + OpenID Connect Authorization Code Flow (with PKCE). + + .. deprecated:: 0.19.0 + Usage of the Authorization Code flow is deprecated (because of its complexity) and will be removed. + It is recommended to use the Device Code flow with :py:meth:`authenticate_oidc_device` + or Client Credentials flow with :py:meth:`authenticate_oidc_client_credentials`. + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=[DefaultOidcClientGrant.AUTH_CODE_PKCE], + ) + authenticator = OidcAuthCodePkceAuthenticator( + client_info=client_info, + webbrowser_open=webbrowser_open, timeout=timeout, server_address=server_address + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token) + + def authenticate_oidc_client_credentials( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + ) -> Connection: + """ + Authenticate with :ref:`OIDC Client Credentials flow ` + + Client id, secret and provider id can be specified directly through the available arguments. + It is also possible to leave these arguments empty and specify them through + environment variables ``OPENEO_AUTH_CLIENT_ID``, + ``OPENEO_AUTH_CLIENT_SECRET`` and ``OPENEO_AUTH_PROVIDER_ID`` respectively + as discussed in :ref:`authenticate_oidc_client_credentials_env_vars`. + + :param client_id: client id to use + :param client_secret: client secret to use + :param provider_id: provider id to use + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + + .. versionchanged:: 0.18.0 Allow specifying client id, secret and provider id through environment variables. + """ + # TODO: option to get client id/secret from a config file too? + if client_id is None and "OPENEO_AUTH_CLIENT_ID" in os.environ and "OPENEO_AUTH_CLIENT_SECRET" in os.environ: + client_id = os.environ.get("OPENEO_AUTH_CLIENT_ID") + client_secret = os.environ.get("OPENEO_AUTH_CLIENT_SECRET") + _log.debug(f"Getting client id ({client_id}) and secret from environment") + + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret + ) + authenticator = OidcClientCredentialsAuthenticator(client_info=client_info) + return self._authenticate_oidc( + authenticator, provider_id=provider_id, store_refresh_token=False, oidc_auth_renewer=authenticator + ) + + def authenticate_oidc_resource_owner_password_credentials( + self, + username: str, + password: str, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + store_refresh_token: bool = False, + ) -> Connection: + """ + OpenId Connect Resource Owner Password Credentials + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret + ) + # TODO: also get username and password from config? + authenticator = OidcResourceOwnerPasswordAuthenticator( + client_info=client_info, username=username, password=password + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token) + + def authenticate_oidc_refresh_token( + self, + client_id: Optional[str] = None, + refresh_token: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + *, + store_refresh_token: bool = False, + ) -> Connection: + """ + Authenticate with :ref:`OIDC Refresh Token flow ` + + :param client_id: client id to use + :param refresh_token: refresh token to use + :param client_secret: client secret to use + :param provider_id: provider id to use. + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + :param store_refresh_token: whether to store the received refresh token automatically + + .. versionchanged:: 0.19.0 Support fallback provider id through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + """ + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=[DefaultOidcClientGrant.REFRESH_TOKEN], + ) + + if refresh_token is None: + refresh_token = self._get_refresh_token_store().get_refresh_token( + issuer=client_info.provider.issuer, + client_id=client_info.client_id + ) + if refresh_token is None: + raise OpenEoClientException("No refresh token given or found") + + authenticator = OidcRefreshTokenAuthenticator(client_info=client_info, refresh_token=refresh_token) + return self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + fallback_refresh_token_to_store=refresh_token, + oidc_auth_renewer=authenticator, + ) + + def authenticate_oidc_device( + self, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + provider_id: Optional[str] = None, + *, + store_refresh_token: bool = False, + use_pkce: Optional[bool] = None, + max_poll_time: float = OidcDeviceAuthenticator.DEFAULT_MAX_POLL_TIME, + **kwargs, + ) -> Connection: + """ + Authenticate with the :ref:`OIDC Device Code flow ` + + :param client_id: client id to use instead of the default one + :param client_secret: client secret to use instead of the default one + :param provider_id: provider id to use. + Fallback value can be set through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + :param store_refresh_token: whether to store the received refresh token automatically + :param use_pkce: Use PKCE instead of client secret. + If not set explicitly to `True` (use PKCE) or `False` (use client secret), + it will be attempted to detect the best mode automatically. + Note that PKCE for device code is not widely supported among OIDC providers. + :param max_poll_time: maximum time in seconds to keep polling for successful authentication. + + .. versionchanged:: 0.5.1 Add :py:obj:`use_pkce` argument + .. versionchanged:: 0.17.0 Add :py:obj:`max_poll_time` argument + .. versionchanged:: 0.19.0 Support fallback provider id through environment variable ``OPENEO_AUTH_PROVIDER_ID``. + """ + _g = DefaultOidcClientGrant # alias for compactness + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=(lambda grants: _g.DEVICE_CODE in grants or _g.DEVICE_CODE_PKCE in grants), + ) + authenticator = OidcDeviceAuthenticator( + client_info=client_info, use_pkce=use_pkce, max_poll_time=max_poll_time, **kwargs + ) + return self._authenticate_oidc(authenticator, provider_id=provider_id, store_refresh_token=store_refresh_token) + + def authenticate_oidc( + self, + provider_id: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + *, + store_refresh_token: bool = True, + use_pkce: Optional[bool] = None, + display: Callable[[str], None] = print, + max_poll_time: float = OidcDeviceAuthenticator.DEFAULT_MAX_POLL_TIME, + ): + """ + Generic method to do OpenID Connect authentication. + + In the context of interactive usage, this method first tries to use refresh tokens + and falls back on device code flow. + + For non-interactive, machine-to-machine contexts, it is also possible to trigger + the usage of the "client_credentials" flow through environment variables. + Assuming you have set up a OIDC client (with a secret): + set ``OPENEO_AUTH_METHOD`` to ``client_credentials``, + set ``OPENEO_AUTH_CLIENT_ID`` to the client id, + and set ``OPENEO_AUTH_CLIENT_SECRET`` to the client secret. + + See :ref:`authenticate_oidc_automatic` for more details. + + :param provider_id: provider id to use + :param client_id: client id to use + :param client_secret: client secret to use + :param max_poll_time: maximum time in seconds to keep polling for successful authentication. + + .. versionadded:: 0.6.0 + .. versionchanged:: 0.17.0 Add :py:obj:`max_poll_time` argument + .. versionchanged:: 0.18.0 Add support for client credentials flow. + """ + # TODO: unify `os.environ.get` with `get_config_option`? + # TODO also support OPENEO_AUTH_CLIENT_ID, ... env vars for refresh token and device code auth? + + auth_method = os.environ.get("OPENEO_AUTH_METHOD") + if auth_method == "client_credentials": + _log.debug("authenticate_oidc: going for 'client_credentials' authentication") + return self.authenticate_oidc_client_credentials( + client_id=client_id, client_secret=client_secret, provider_id=provider_id + ) + elif auth_method: + raise ValueError(f"Unhandled auth method {auth_method}") + + _g = DefaultOidcClientGrant # alias for compactness + provider_id, client_info = self._get_oidc_provider_and_client_info( + provider_id=provider_id, client_id=client_id, client_secret=client_secret, + default_client_grant_check=lambda grants: ( + _g.REFRESH_TOKEN in grants and (_g.DEVICE_CODE in grants or _g.DEVICE_CODE_PKCE in grants) + ) + ) + + # Try refresh token first. + refresh_token = self._get_refresh_token_store().get_refresh_token( + issuer=client_info.provider.issuer, + client_id=client_info.client_id + ) + if refresh_token: + try: + _log.info("Found refresh token: trying refresh token based authentication.") + authenticator = OidcRefreshTokenAuthenticator(client_info=client_info, refresh_token=refresh_token) + con = self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + fallback_refresh_token_to_store=refresh_token, + ) + # TODO: pluggable/jupyter-aware display function? + print("Authenticated using refresh token.") + return con + except OidcException as e: + _log.info("Refresh token based authentication failed: {e}.".format(e=e)) + + # Fall back on device code flow + # TODO: make it possible to do other fallback flows too? + _log.info("Trying device code flow.") + authenticator = OidcDeviceAuthenticator( + client_info=client_info, use_pkce=use_pkce, display=display, max_poll_time=max_poll_time + ) + con = self._authenticate_oidc( + authenticator, + provider_id=provider_id, + store_refresh_token=store_refresh_token, + ) + print("Authenticated using device code flow.") + return con + + def authenticate_oidc_access_token(self, access_token: str, provider_id: Optional[str] = None) -> Connection: + """ + Set up authorization headers directly with an OIDC access token. + + :py:class:`Connection` provides multiple methods to handle various OIDC authentication flows end-to-end. + If you already obtained a valid OIDC access token in another "out-of-band" way, you can use this method to + set up the authorization headers appropriately. + + :param access_token: OIDC access token + :param provider_id: id of the OIDC provider as listed by the openEO backend (``/credentials/oidc``). + If not specified, the first (default) OIDC provider will be used. + :param skip_verification: Skip clients-side verification of the provider_id + against the backend's list of providers to avoid and related OIDC configuration + + .. versionadded:: 0.31.0 + + .. versionchanged:: 0.33.0 + Return connection object to support chaining. + """ + provider_id, _ = self._get_oidc_provider(provider_id=provider_id, parse_info=False) + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=access_token) + self._oidc_auth_renewer = None + return self + + def request( + self, + method: str, + path: str, + headers: Optional[dict] = None, + auth: Optional[AuthBase] = None, + check_error: bool = True, + expected_status: Optional[Union[int, Iterable[int]]] = None, + **kwargs, + ): + # Do request, but with retry when access token has expired and refresh token is available. + def _request(): + return super(Connection, self).request( + method=method, path=path, headers=headers, auth=auth, + check_error=check_error, expected_status=expected_status, **kwargs, + ) + + try: + # Initial request attempt + return _request() + except OpenEoApiError as api_exc: + if api_exc.http_status_code in {401, 403} and api_exc.code == "TokenInvalid": + # Auth token expired: can we refresh? + if isinstance(self.auth, OidcBearerAuth) and self._oidc_auth_renewer: + msg = f"OIDC access token expired ({api_exc.http_status_code} {api_exc.code})." + try: + self._authenticate_oidc( + authenticator=self._oidc_auth_renewer, + provider_id=self._oidc_auth_renewer.provider_info.id, + store_refresh_token=False, + oidc_auth_renewer=self._oidc_auth_renewer, + ) + _log.info(f"{msg} Obtained new access token (grant {self._oidc_auth_renewer.grant_type!r}).") + except OpenEoClientException as auth_exc: + _log.error( + f"{msg} Failed to obtain new access token (grant {self._oidc_auth_renewer.grant_type!r}): {auth_exc!r}." + ) + else: + # Retry request. + return _request() + raise + + def describe_account(self) -> dict: + """ + Describes the currently authenticated user account. + """ + return self.get('/me', expected_status=200).json() + + @deprecated("use :py:meth:`list_jobs` instead", version="0.4.10") + def user_jobs(self) -> List[dict]: + return self.list_jobs() + + def list_collections(self) -> List[dict]: + """ + List basic metadata of all collections provided by the back-end. + + .. caution:: + + Only the basic collection metadata will be returned. + To obtain full metadata of a particular collection, + it is recommended to use :py:meth:`~openeo.rest.connection.Connection.describe_collection` instead. + + :return: list of dictionaries with basic collection metadata. + """ + # TODO: add caching #383 + data = self.get('/collections', expected_status=200).json()["collections"] + return VisualList("collections", data=data) + + def list_collection_ids(self) -> List[str]: + """ + List all collection ids provided by the back-end. + + .. seealso:: + + :py:meth:`~openeo.rest.connection.Connection.describe_collection` + to get the metadata of a particular collection. + + :return: list of collection ids + """ + return [collection['id'] for collection in self.list_collections() if 'id' in collection] + + def capabilities(self) -> RESTCapabilities: + """ + Loads all available capabilities. + """ + return self._capabilities_cache.get( + "capabilities", + load=lambda: RESTCapabilities(data=self.get('/', expected_status=200).json(), url=self._orig_url) + ) + + def list_input_formats(self) -> dict: + return self.list_file_formats().get("input", {}) + + def list_output_formats(self) -> dict: + return self.list_file_formats().get("output", {}) + + list_file_types = legacy_alias( + list_output_formats, "list_file_types", since="0.4.6" + ) + + def list_file_formats(self) -> dict: + """ + Get available input and output formats + """ + formats = self._capabilities_cache.get( + key="file_formats", + load=lambda: self.get('/file_formats', expected_status=200).json() + ) + return VisualDict("file-formats", data=formats) + + def list_service_types(self) -> dict: + """ + Loads all available service types. + + :return: data_dict: Dict All available service types + """ + types = self._capabilities_cache.get( + key="service_types", + load=lambda: self.get('/service_types', expected_status=200).json() + ) + return VisualDict("service-types", data=types) + + def list_udf_runtimes(self) -> dict: + """ + List information about the available UDF runtimes. + + :return: A dictionary with metadata about each available UDF runtime. + """ + runtimes = self._capabilities_cache.get( + key="udf_runtimes", + load=lambda: self.get('/udf_runtimes', expected_status=200).json() + ) + return VisualDict("udf-runtimes", data=runtimes) + + def list_services(self) -> dict: + """ + Loads all available services of the authenticated user. + + :return: data_dict: Dict All available services + """ + # TODO return parsed service objects + services = self.get('/services', expected_status=200).json()["services"] + return VisualList("data-table", data=services, parameters={'columns': 'services'}) + + def describe_collection(self, collection_id: str) -> dict: + """ + Get full collection metadata for given collection id. + + .. seealso:: + + :py:meth:`~openeo.rest.connection.Connection.list_collection_ids` + to list all collection ids provided by the back-end. + + :param collection_id: collection id + :return: collection metadata. + """ + # TODO: duplication with `Connection.collection_metadata`: deprecate one or the other? + # TODO: add caching #383 + data = self.get(f"/collections/{collection_id}", expected_status=200).json() + return VisualDict("collection", data=data) + + def collection_items( + self, + name, + spatial_extent: Optional[List[float]] = None, + temporal_extent: Optional[List[Union[str, datetime.datetime]]] = None, + limit: Optional[int] = None, + ) -> Iterator[dict]: + """ + Loads items for a specific image collection. + May not be available for all collections. + + This is an experimental API and is subject to change. + + :param name: String Id of the collection + :param spatial_extent: Limits the items to the given bounding box in WGS84: + 1. Lower left corner, coordinate axis 1 + 2. Lower left corner, coordinate axis 2 + 3. Upper right corner, coordinate axis 1 + 4. Upper right corner, coordinate axis 2 + + :param temporal_extent: Limits the items to the specified temporal interval. + :param limit: The amount of items per request/page. If None, the back-end decides. + The interval has to be specified as an array with exactly two elements (start, end). + Also supports open intervals by setting one of the boundaries to None, but never both. + + :return: data_list: List A list of items + """ + url = '/collections/{}/items'.format(name) + params = {} + if spatial_extent: + params["bbox"] = ",".join(str(c) for c in spatial_extent) + if temporal_extent: + params["datetime"] = "/".join(".." if t is None else rfc3339.normalize(t) for t in temporal_extent) + if limit is not None and limit > 0: + params['limit'] = limit + + return paginate(self, url, params, lambda response, page: VisualDict("items", data = response, parameters = {'show-map': True, 'heading': 'Page {} - Items'.format(page)})) + + def collection_metadata(self, name) -> CollectionMetadata: + # TODO: duplication with `Connection.describe_collection`: deprecate one or the other? + return CollectionMetadata(metadata=self.describe_collection(name)) + + def list_processes(self, namespace: Optional[str] = None) -> List[dict]: + # TODO: Maybe format the result dictionary so that the process_id is the key of the dictionary. + """ + Loads all available processes of the back end. + + :param namespace: The namespace for which to list processes. + + :return: processes_dict: Dict All available processes of the back end. + """ + if namespace is None: + processes = self._capabilities_cache.get( + key=("processes", "backend"), + load=lambda: self.get('/processes', expected_status=200).json()["processes"] + ) + else: + processes = self.get('/processes/' + namespace, expected_status=200).json()["processes"] + return VisualList("processes", data=processes, parameters={'show-graph': True, 'provide-download': False}) + + def describe_process(self, id: str, namespace: Optional[str] = None) -> dict: + """ + Returns a single process from the back end. + + :param id: The id of the process. + :param namespace: The namespace of the process. + + :return: The process definition. + """ + + processes = self.list_processes(namespace) + for process in processes: + if process["id"] == id: + return VisualDict("process", data=process, parameters={'show-graph': True, 'provide-download': False}) + + raise OpenEoClientException("Process does not exist.") + + def list_jobs(self) -> List[dict]: + """ + Lists all jobs of the authenticated user. + + :return: job_list: Dict of all jobs of the user. + """ + # TODO: Parse the result so that there get Job classes returned? + resp = self.get('/jobs', expected_status=200).json() + if resp.get("federation:missing"): + _log.warning("Partial user job listing due to missing federation components: {c}".format( + c=",".join(resp["federation:missing"]) + )) + jobs = resp["jobs"] + return VisualList("data-table", data=jobs, parameters={'columns': 'jobs'}) + + def assert_user_defined_process_support(self): + """ + Capabilities document based verification that back-end supports user-defined processes. + + .. versionadded:: 0.23.0 + """ + if not self.capabilities().supports_endpoint("/process_graphs"): + raise CapabilitiesException("Backend does not support user-defined processes.") + + def save_user_defined_process( + self, user_defined_process_id: str, + process_graph: Union[dict, ProcessBuilderBase], + parameters: List[Union[dict, Parameter]] = None, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ) -> RESTUserDefinedProcess: + """ + Store a process graph and its metadata on the backend as a user-defined process for the authenticated user. + + :param user_defined_process_id: unique identifier for the user-defined process + :param process_graph: a process graph + :param parameters: a list of parameters + :param public: visible to other users? + :param summary: A short summary of what the process does. + :param description: Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation. + :param returns: Description and schema of the return value. + :param categories: A list of categories. + :param examples: A list of examples. + :param links: A list of links. + :return: a RESTUserDefinedProcess instance + """ + self.assert_user_defined_process_support() + if user_defined_process_id in set(p["id"] for p in self.list_processes()): + warnings.warn("Defining user-defined process {u!r} with same id as a pre-defined process".format( + u=user_defined_process_id)) + if not parameters: + warnings.warn("Defining user-defined process {u!r} without parameters".format(u=user_defined_process_id)) + udp = RESTUserDefinedProcess(user_defined_process_id=user_defined_process_id, connection=self) + udp.store( + process_graph=process_graph, parameters=parameters, public=public, + summary=summary, description=description, + returns=returns, categories=categories, examples=examples, links=links + ) + return udp + + def list_user_defined_processes(self) -> List[dict]: + """ + Lists all user-defined processes of the authenticated user. + """ + self.assert_user_defined_process_support() + data = self.get("/process_graphs", expected_status=200).json()["processes"] + return VisualList("processes", data=data, parameters={'show-graph': True, 'provide-download': False}) + + def user_defined_process(self, user_defined_process_id: str) -> RESTUserDefinedProcess: + """ + Get the user-defined process based on its id. The process with the given id should already exist. + + :param user_defined_process_id: the id of the user-defined process + :return: a RESTUserDefinedProcess instance + """ + return RESTUserDefinedProcess(user_defined_process_id=user_defined_process_id, connection=self) + + def validate_process_graph( + self, process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]] + ) -> List[dict]: + """ + Validate a process graph without executing it. + + :param process_graph: openEO-style (flat) process graph representation, + or an object that can be converted to such a representation: + a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object, + a string with a JSON representation, + a local file path or URL to a JSON representation, + a :py:class:`~openeo.rest.multiresult.MultiResult` object, ... + + :return: list of errors (dictionaries with "code" and "message" fields) + """ + pg_with_metadata = self._build_request_with_process_graph(process_graph)["process"] + return self.post(path="/validation", json=pg_with_metadata, expected_status=200).json()["errors"] + + @property + def _api_version(self) -> ComparableVersion: + # TODO make this a public property (it's also useful outside the Connection class) + return self.capabilities().api_version_check + + def vectorcube_from_paths( + self, paths: List[str], format: str, options: dict = {} + ) -> VectorCube: + """ + Loads one or more files referenced by url or path that is accessible by the backend. + + :param paths: The files to read. + :param format: The file format to read from. It must be one of the values that the server reports as supported input file formats. + :param options: The file format parameters to be used to read the files. Must correspond to the parameters that the server reports as supported parameters for the chosen format. + + :return: A :py:class:`VectorCube`. + + .. versionadded:: 0.14.0 + """ + # TODO #457 deprecate this in favor of `load_url` and standard support for `load_uploaded_files` + graph = PGNode( + "load_uploaded_files", + arguments=dict(paths=paths, format=format, options=options), + ) + # TODO: load_uploaded_files might also return a raster data cube. Determine this based on format? + return VectorCube(graph=graph, connection=self) + + def datacube_from_process(self, process_id: str, namespace: Optional[str] = None, **kwargs) -> DataCube: + """ + Load a data cube from a (custom) process. + + :param process_id: The process id. + :param namespace: optional: process namespace + :param kwargs: The arguments of the custom process + :return: A :py:class:`DataCube`, without valid metadata, as the client is not aware of this custom process. + """ + graph = PGNode(process_id, namespace=namespace, arguments=kwargs) + return DataCube(graph=graph, connection=self) + + def datacube_from_flat_graph(self, flat_graph: dict, parameters: Optional[dict] = None) -> DataCube: + """ + Construct a :py:class:`DataCube` from a flat dictionary representation of a process graph. + + .. seealso:: :ref:`datacube_from_json`, :py:meth:`~openeo.rest.connection.Connection.datacube_from_json` + + :param flat_graph: flat dictionary representation of a process graph + or a process dictionary with such a flat process graph under a "process_graph" field + (and optionally parameter metadata under a "parameters" field). + :param parameters: Optional dictionary mapping parameter names to parameter values + to use for parameters occurring in the process graph (e.g. as used in user-defined processes) + :return: A :py:class:`DataCube` corresponding with the operations encoded in the process graph + """ + parameters = parameters or {} + + if "process_graph" in flat_graph: + # `flat_graph` is a "process" structure + # Extract defaults from declared parameters. + for param in flat_graph.get("parameters") or []: + if "default" in param: + parameters.setdefault(param["name"], param["default"]) + + flat_graph = flat_graph["process_graph"] + + pgnode = PGNode.from_flat_graph(flat_graph=flat_graph, parameters=parameters or {}) + return DataCube(graph=pgnode, connection=self) + + def datacube_from_json(self, src: Union[str, Path], parameters: Optional[dict] = None) -> DataCube: + """ + Construct a :py:class:`DataCube` from JSON resource containing (flat) process graph representation. + + .. seealso:: :ref:`datacube_from_json`, :py:meth:`~openeo.rest.connection.Connection.datacube_from_flat_graph` + + :param src: raw JSON string, URL to JSON resource or path to local JSON file + :param parameters: Optional dictionary mapping parameter names to parameter values + to use for parameters occurring in the process graph (e.g. as used in user-defined processes) + :return: A :py:class:`DataCube` corresponding with the operations encoded in the process graph + """ + return self.datacube_from_flat_graph(load_json_resource(src), parameters=parameters) + + @openeo_process + def load_collection( + self, + collection_id: Union[str, Parameter], + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Union[None, List[str], Parameter] = None, + properties: Union[ + None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty + ] = None, + max_cloud_cover: Optional[float] = None, + fetch_metadata: bool = True, + ) -> DataCube: + """ + Load a DataCube by collection id. + + :param collection_id: image collection identifier + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands. + :param properties: limit data by collection metadata property predicates. + See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of such predicates. + :param max_cloud_cover: shortcut to set maximum cloud cover ("eo:cloud_cover" collection property) + :return: a datacube containing the requested data + + .. versionadded:: 0.13.0 + added the ``max_cloud_cover`` argument. + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + + .. versionchanged:: 0.26.0 + Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument. + """ + return DataCube.load_collection( + collection_id=collection_id, + connection=self, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + max_cloud_cover=max_cloud_cover, + fetch_metadata=fetch_metadata, + ) + + # TODO: remove this #100 #134 0.4.10 + imagecollection = legacy_alias( + load_collection, name="imagecollection", since="0.4.10" + ) + + @openeo_process + def load_result( + self, + id: str, + spatial_extent: Optional[Dict[str, float]] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + ) -> DataCube: + """ + Loads batch job results by job id from the server-side user workspace. + The job must have been stored by the authenticated user on the back-end currently connected to. + + :param id: The id of a batch job with results. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands + + :return: a :py:class:`DataCube` + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + """ + # TODO: add check that back-end supports `load_result` process? + cube = self.datacube_from_process( + process_id="load_result", + id=id, + **dict_no_none( + spatial_extent=spatial_extent, + temporal_extent=temporal_extent and DataCube._get_temporal_extent(extent=temporal_extent), + bands=bands, + ), + ) + return cube + + @openeo_process + def load_stac( + self, + url: str, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + ) -> DataCube: + """ + Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`. + A batch job result can be loaded by providing a reference to it. + + If supported by the underlying metadata and file format, the data that is added to the data cube can be + restricted with the parameters ``spatial_extent``, ``temporal_extent`` and ``bands``. + If no data is available for the given extents, a ``NoDataAvailable`` error is thrown. + + Remarks: + + * The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as + specified in the metadata if the ``bands`` parameter is set to ``null``. + * If no additional parameter is specified this would imply that the whole data set is expected to be loaded. + Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only + load the data that is actually required after evaluating subsequent processes such as filters. + This means that the values should be processed only after the data has been limited to the required extent + and as a consequence also to a manageable size. + + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) + or a specific STAC API Collection that allows to filter items and to download assets. + This includes batch job results, which itself are compliant to STAC. + For external URLs, authentication details such as API keys or tokens may need to be included in the URL. + + Batch job results can be specified in two ways: + + - For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with ``/jobs/{id}/results`` and ``{id}`` + is the corresponding batch job ID. + - For external results, a signed URL must be provided. Not all back-ends support signed URLs, + which are provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: + Limits the data to load to the specified bounding box or polygons. + + For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects + with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + + For vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. + + The GeoJSON can be one of the following feature types: + + * A ``Polygon`` or ``MultiPolygon`` geometry, + * a ``Feature`` with a ``Polygon`` or ``MultiPolygon`` geometry, or + * a ``FeatureCollection`` containing at least one ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometries. + + Set this parameter to ``None`` to set no limit for the spatial extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + + :param temporal_extent: + Limits the data to load to the specified left-closed temporal interval. + Applies to all temporal dimensions. + The interval has to be specified as an array with exactly two elements: + + 1. The first element is the start of the temporal interval. + The specified instance in time is **included** in the interval. + 2. The second element is the end of the temporal interval. + The specified instance in time is **excluded** from the interval. + + The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. + + Also supports open intervals by setting one of the boundaries to ``None``, but never both. + + Set this parameter to ``None`` to set no limit for the temporal extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_temporal()`` directly after loading unbounded data. + + :param bands: + Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. + + Either the unique band name (metadata field ``name`` in bands) or one of the common band names + (metadata field ``common_name`` in bands) can be specified. + If the unique band name and the common name conflict, the unique band name has a higher priority. + + The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. + + It is recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :param properties: + Limits the data by metadata properties to include only data in the data cube which + all given conditions return ``True`` for (AND operation). + + Specify key-value-pairs with the key being the name of the metadata property, + which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. + This parameter is not supported for static STAC. + + .. versionadded:: 0.17.0 + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + """ + return DataCube.load_stac( + url=url, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + connection=self, + ) + + def load_stac_from_job( + self, + job: Union[BatchJob, str], + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + ) -> DataCube: + """ + Convenience function to directly load the results of a finished openEO job + (as a STAC collection) with :py:meth:`load_stac` in a new openEO process graph. + + When available, the "canonical" link (signed URL) of the job results will be used. + + :param job: a :py:class:`~openeo.rest.job.BatchJob` or job id pointing to a finished job. + Note that the :py:class:`~openeo.rest.job.BatchJob` approach allows to point + to a batch job on a different back-end. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + :param bands: limit data to the specified bands + + .. versionadded:: 0.30.0 + """ + # TODO #634 add option to require or avoid the canonical link + if isinstance(job, str): + job = BatchJob(job_id=job, connection=self) + elif not isinstance(job, BatchJob): + raise ValueError("job must be a BatchJob or job id") + + try: + job_results = job.get_results() + + canonical_links = [ + link["href"] + for link in job_results.get_metadata().get("links", []) + if link.get("rel") == "canonical" and "href" in link + ] + if len(canonical_links) == 0: + _log.warning("No canonical link found in job results metadata. Using job results URL instead.") + stac_link = job.get_results_metadata_url(full=True) + else: + if len(canonical_links) > 1: + _log.warning( + f"Multiple canonical links found in job results metadata: {canonical_links}. Picking first one." + ) + stac_link = canonical_links[0] + except OpenEoApiError as e: + _log.warning(f"Failed to get the canonical job results: {e!r}. Using job results URL instead.") + stac_link = job.get_results_metadata_url(full=True) + + return self.load_stac( + url=stac_link, + spatial_extent=spatial_extent, + temporal_extent=temporal_extent, + bands=bands, + properties=properties, + ) + + def load_ml_model(self, id: Union[str, BatchJob]) -> MlModel: + """ + Loads a machine learning model from a STAC Item. + + :param id: STAC item reference, as URL, batch job (id) or user-uploaded file + :return: + + .. versionadded:: 0.10.0 + """ + return MlModel.load_ml_model(connection=self, id=id) + + @openeo_process + def load_geojson( + self, + data: Union[dict, str, Path, shapely.geometry.base.BaseGeometry, Parameter], + properties: Optional[List[str]] = None, + ): + """ + Converts GeoJSON data as defined by RFC 7946 into a vector data cube. + + :param data: the geometry to load. One of: + + - GeoJSON-style data structure: e.g. a dictionary with ``"type": "Polygon"`` and ``"coordinates"`` fields + - a path to a local GeoJSON file + - a GeoJSON string + - a shapely geometry object + + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + return VectorCube.load_geojson(connection=self, data=data, properties=properties) + + @openeo_process + def load_url(self, url: str, format: str, options: Optional[dict] = None): + """ + Loads a file from a URL + + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL. + :param format: The file format to use when loading the data. + :param options: The file format parameters to use when reading the data. + Must correspond to the parameters that the server reports as supported parameters for the chosen ``format`` + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + if format not in self.list_input_formats(): + # TODO: make this an error? + _log.warning(f"Format {format!r} not listed in back-end input formats") + # TODO: Inspect format's gis_data_type to see if we need to load a VectorCube or classic raster DataCube + return VectorCube.load_url(connection=self, url=url, format=format, options=options) + + def create_service(self, graph: dict, type: str, **kwargs) -> Service: + # TODO: type hint for graph: is it a nested or a flat one? + pg_with_metadata = self._build_request_with_process_graph(process_graph=graph, type=type, **kwargs) + self._preflight_validation(pg_with_metadata=pg_with_metadata) + response = self.post(path="/services", json=pg_with_metadata, expected_status=201) + service_id = response.headers.get("OpenEO-Identifier") + return Service(service_id, self) + + @deprecated("Use :py:meth:`openeo.rest.service.Service.delete_service` instead.", version="0.8.0") + def remove_service(self, service_id: str): + """ + Stop and remove a secondary web service. + + :param service_id: service identifier + :return: + """ + Service(service_id, self).delete_service() + + @deprecated("Use :py:meth:`openeo.rest.job.BatchJob.get_results` instead.", version="0.4.10") + def job_results(self, job_id) -> dict: + """Get batch job results metadata.""" + return BatchJob(job_id=job_id, connection=self).list_results() + + @deprecated("Use :py:meth:`openeo.rest.job.BatchJob.logs` instead.", version="0.4.10") + def job_logs(self, job_id, offset) -> list: + """Get batch job logs.""" + return BatchJob(job_id=job_id, connection=self).logs(offset=offset) + + def list_files(self) -> List[UserFile]: + """ + Lists all user-uploaded files in the user workspace on the back-end. + + :return: List of the user-uploaded files. + """ + files = self.get('/files', expected_status=200).json()['files'] + files = [UserFile.from_metadata(metadata=f, connection=self) for f in files] + return VisualList("data-table", data=files, parameters={'columns': 'files'}) + + def get_file( + self, path: Union[str, PurePosixPath], metadata: Optional[dict] = None + ) -> UserFile: + """ + Gets a handle to a user-uploaded file in the user workspace on the back-end. + + :param path: The path on the user workspace. + """ + return UserFile(path=path, connection=self, metadata=metadata) + + def upload_file( + self, + source: Union[Path, str], + target: Optional[Union[str, PurePosixPath]] = None, + ) -> UserFile: + """ + Uploads a file to the given target location in the user workspace on the back-end. + + If a file at the target path exists in the user workspace it will be replaced. + + :param source: A path to a file on the local file system to upload. + :param target: The desired path (which can contain a folder structure if desired) on the user workspace. + If not set: defaults to the original filename (without any folder structure) of the local file . + """ + source = Path(source) + target = target or source.name + # TODO: support other non-path sources too: bytes, open file, url, ... + with source.open("rb") as f: + resp = self.put(f"/files/{target!s}", expected_status=200, data=f) + metadata = resp.json() + return UserFile.from_metadata(metadata=metadata, connection=self) + + def _build_request_with_process_graph( + self, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + **kwargs, + ) -> dict: + """ + Prepare a json payload with a process graph to submit to /result, /services, /jobs, ... + :param process_graph: flat dict representing a "process graph with metadata" ({"process": {"process_graph": ...}, ...}) + """ + # TODO: make this a more general helper (like `as_flat_graph`) + connections = extract_connections(process_graph) + if any(c != self for c in connections): + raise OpenEoClientException(f"Mixing different connections: {self} and {connections}.") + result = kwargs + process_graph = as_flat_graph(process_graph) + if "process_graph" not in process_graph: + process_graph = {"process_graph": process_graph} + # TODO: also check if `process_graph` already has "process" key (i.e. is a "process graph with metadata" already) + result["process"] = process_graph + return result + + def _preflight_validation(self, pg_with_metadata: dict, *, validate: Optional[bool] = None): + """ + Preflight validation of process graph to execute. + + :param pg_with_metadata: flat dict representation of process graph with metadata, + e.g. as produced by `_build_request_with_process_graph` + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + + :return: + """ + if validate is None: + validate = self._auto_validate + if validate and self.capabilities().supports_endpoint("/validation", "POST"): + # At present, the intention is that a failed validation does not block + # the job from running, it is only reported as a warning. + # Therefor we also want to continue when something *else* goes wrong + # *during* the validation. + try: + resp = self.post(path="/validation", json=pg_with_metadata["process"], expected_status=200) + validation_errors = resp.json()["errors"] + if validation_errors: + _log.warning( + "Preflight process graph validation raised: " + + (" ".join(f"[{e.get('code')}] {e.get('message')}" for e in validation_errors)) + ) + except Exception as e: + _log.error(f"Preflight process graph validation failed: {e}") + + # TODO: additional validation and sanity checks: e.g. is there a result node, are all process_ids valid, ...? + + # TODO: unify `download` and `execute` better: e.g. `download` always writes to disk, `execute` returns result (raw or as JSON decoded dict) + def download( + self, + graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + outputfile: Union[Path, str, None] = None, + *, + timeout: Optional[int] = None, + validate: Optional[bool] = None, + chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE, + ) -> Union[None, bytes]: + """ + Downloads the result of a process graph synchronously, + and save the result to the given file or return bytes object if no outputfile is specified. + This method is useful to export binary content such as images. For json content, the execute method is recommended. + + :param graph: (flat) dict representing a process graph, or process graph as raw JSON string, + or as local file path or URL + :param outputfile: output file + :param timeout: timeout to wait for response + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param chunk_size: chunk size for streaming response. + """ + pg_with_metadata = self._build_request_with_process_graph(process_graph=graph) + self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate) + response = self.post( + path="/result", + json=pg_with_metadata, + expected_status=200, + stream=True, + timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, + ) + + if outputfile is not None: + with Path(outputfile).open(mode="wb") as f: + for chunk in response.iter_content(chunk_size=chunk_size): + f.write(chunk) + else: + return response.content + + def execute( + self, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + *, + 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. 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 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) + response = self.post( + path="/result", + json=pg_with_metadata, + expected_status=200, + timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, + ) + 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, + process_graph: Union[dict, FlatGraphableMixin, str, Path, List[FlatGraphableMixin]], + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + additional: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + """ + Create a new job from given process graph on the back-end. + + :param process_graph: openEO-style (flat) process graph representation, + or an object that can be converted to such a representation: + a dictionary, a :py:class:`~openeo.rest.datacube.DataCube` object, + a string with a JSON representation, + a local file path or URL to a JSON representation, + a :py:class:`~openeo.rest.multiresult.MultiResult` object, ... + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param additional: additional job options to pass to the backend + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :return: Created job + + .. versionchanged:: 0.35.0 + Add :ref:`multi-result support `. + """ + # TODO move all this (BatchJob factory) logic to BatchJob? + + pg_with_metadata = self._build_request_with_process_graph( + process_graph=process_graph, + **dict_no_none(title=title, description=description, plan=plan, budget=budget) + ) + if additional: + # TODO: get rid of this non-standard field? https://github.com/Open-EO/openeo-api/issues/276 + pg_with_metadata["job_options"] = additional + + self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate) + response = self.post("/jobs", json=pg_with_metadata, expected_status=201) + + job_id = None + if "openeo-identifier" in response.headers: + job_id = response.headers['openeo-identifier'].strip() + elif "location" in response.headers: + _log.warning("Backend did not explicitly respond with job id, will guess it from redirect URL.") + job_id = response.headers['location'].split("/")[-1] + if not job_id: + raise OpenEoClientException("Job creation response did not contain a valid job id") + return BatchJob(job_id=job_id, connection=self) + + def job(self, job_id: str) -> BatchJob: + """ + Get the job based on the id. The job with the given id should already exist. + + Use :py:meth:`openeo.rest.connection.Connection.create_job` to create new jobs + + :param job_id: the job id of an existing job + :return: A job object. + """ + return BatchJob(job_id=job_id, connection=self) + + def service(self, service_id: str) -> Service: + """ + Get the secondary web service based on the id. The service with the given id should already exist. + + Use :py:meth:`openeo.rest.connection.Connection.create_service` to create new services + + :param job_id: the service id of an existing secondary web service + :return: A service object. + """ + return Service(service_id, connection=self) + + @deprecated( + reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.", + version="0.25.0") + def load_disk_collection( + self, format: str, glob_pattern: str, options: Optional[dict] = None + ) -> DataCube: + """ + Loads image data from disk as a :py:class:`DataCube`. + + This is backed by a non-standard process ('load_disk_data'). This will eventually be replaced by standard options such as + :py:meth:`openeo.rest.connection.Connection.load_stac` or https://processes.openeo.org/#load_uploaded_files + + :param format: the file format, e.g. 'GTiff' + :param glob_pattern: a glob pattern that matches the files to load from disk + :param options: options specific to the file format + """ + return DataCube.load_disk_collection( + self, format, glob_pattern, **(options or {}) + ) + + def as_curl( + self, + data: Union[dict, DataCube, FlatGraphableMixin], + path="/result", + method="POST", + obfuscate_auth: bool = False, + ) -> str: + """ + Build curl command to evaluate given process graph or data cube + (including authorization and content-type headers). + + >>> print(connection.as_curl(cube)) + curl -i -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer ...' \\ + --data '{"process":{"process_graph":{...}}' \\ + https://openeo.example/openeo/1.1/result + + :param data: something that is convertable to an openEO process graph: a dictionary, + a :py:class:`~openeo.rest.datacube.DataCube` object, + a :py:class:`~openeo.processes.ProcessBuilder`, ... + :param path: endpoint to send request to: typically ``"/result"`` (default) for synchronous requests + or ``"/jobs"`` for batch jobs + :param method: HTTP method to use (typically ``"POST"``) + :param obfuscate_auth: don't show actual bearer token + + :return: curl command as a string + """ + cmd = ["curl", "-i", "-X", method] + cmd += ["-H", "Content-Type: application/json"] + if isinstance(self.auth, BearerAuth): + cmd += ["-H", f"Authorization: Bearer {'...' if obfuscate_auth else self.auth.bearer}"] + pg_with_metadata = self._build_request_with_process_graph(data) + if path == "/validation": + pg_with_metadata = pg_with_metadata["process"] + post_json = json.dumps(pg_with_metadata, separators=(",", ":")) + cmd += ["--data", post_json] + cmd += [self.build_url(path)] + return " ".join(shlex.quote(c) for c in cmd) + + def version_info(self): + """List version of the openEO client, API, back-end, etc.""" + capabilities = self.capabilities() + return { + "client": openeo.client_version(), + "api": capabilities.api_version(), + "backend": dict_no_none({ + "root_url": self.root_url, + "version": capabilities.get("backend_version"), + "processing:software": capabilities.get("processing:software"), + }), + } + + +def connect( + url: Optional[str] = None, + *, + auth_type: Optional[str] = None, + auth_options: Optional[dict] = None, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auto_validate: bool = True, +) -> Connection: + """ + This method is the entry point to OpenEO. + You typically create one connection object in your script or application + and re-use it for all calls to that backend. + + If the backend requires authentication, you can pass authentication data directly to this function, + but it could be easier to authenticate as follows: + + >>> # For basic authentication + >>> conn = connect(url).authenticate_basic(username="john", password="foo") + >>> # For OpenID Connect authentication + >>> conn = connect(url).authenticate_oidc(client_id="myclient") + + :param url: The http url of the OpenEO back-end. + :param auth_type: Which authentication to use: None, "basic" or "oidc" (for OpenID Connect) + :param auth_options: Options/arguments specific to the authentication type + :param default_timeout: default timeout (in seconds) for requests + :param auto_validate: toggle to automatically validate process graphs before execution + + .. versionadded:: 0.24.0 + added ``auto_validate`` argument + """ + + def _config_log(message): + _log.info(message) + config_log(message) + + if url is None: + default_backend = get_config_option("connection.default_backend") + if default_backend: + url = default_backend + _config_log(f"Using default back-end URL {url!r} (from config)") + default_backend_auto_auth = get_config_option("connection.default_backend.auto_authenticate") + if default_backend_auto_auth and default_backend_auto_auth.lower() in {"basic", "oidc"}: + auth_type = default_backend_auto_auth.lower() + _config_log(f"Doing auto-authentication {auth_type!r} (from config)") + + if auth_type is None: + auto_authenticate = get_config_option("connection.auto_authenticate") + if auto_authenticate and auto_authenticate.lower() in {"basic", "oidc"}: + auth_type = auto_authenticate.lower() + _config_log(f"Doing auto-authentication {auth_type!r} (from config)") + + if not url: + raise OpenEoClientException("No openEO back-end URL given or known to connect to.") + connection = Connection(url, session=session, default_timeout=default_timeout, auto_validate=auto_validate) + + auth_type = auth_type.lower() if isinstance(auth_type, str) else auth_type + if auth_type in {None, False, 'null', 'none'}: + pass + elif auth_type == "basic": + connection.authenticate_basic(**(auth_options or {})) + elif auth_type in {"oidc", "openid"}: + connection.authenticate_oidc(**(auth_options or {})) + else: + raise ValueError("Unknown auth type {a!r}".format(a=auth_type)) + return connection + + +@deprecated("Use :py:func:`openeo.connect` instead", version="0.0.9") +def session(userid=None, endpoint: str = "https://openeo.org/openeo") -> Connection: + """ + This method is the entry point to OpenEO. You typically create one session object in your script or application, per back-end. + and re-use it for all calls to that backend. + If the backend requires authentication, you should set pass your credentials. + + :param endpoint: The http url of an OpenEO endpoint. + :rtype: openeo.sessions.Session + """ + return connect(url=endpoint) + + +def paginate(con: Connection, url: str, params: Optional[dict] = None, callback: Callable = lambda resp, page: resp): + # TODO: make this a method `get_paginated` on `RestApiConnection`? + # TODO: is it necessary to have `callback`? It's only used just before yielding, + # so it's probably cleaner (even for the caller) to to move it outside. + page = 1 + while True: + response = con.get(url, params=params).json() + yield callback(response, page) + next_links = [link for link in response.get("links", []) if link.get("rel") == "next" and "href" in link] + if not next_links: + break + url = next_links[0]["href"] + page += 1 + params = {} + + +def extract_connections( + data: Union[_ProcessGraphAbstraction, Sequence[_ProcessGraphAbstraction], Any] +) -> Set[Connection]: + """ + Extract the :py:class:`Connection` object(s) linked from a given data construct. + Typical use case is to get the connection from a :py:class:`DataCube`, + but can also extract multiple connections from a list of data cubes. + """ + connections = set() + # TODO: define some kind of "Connected" interface/mixin/protocol + # for objects that contain a connection instead of just checking for _ProcessGraphAbstraction + # TODO: also support extracting connections from other objects like BatchJob, ... + if isinstance(data, _ProcessGraphAbstraction) and data.connection: + connections.add(data.connection) + elif isinstance(data, (list, tuple, set)): + for item in data: + if isinstance(item, _ProcessGraphAbstraction) and item.connection: + connections.add(item.connection) + + return connections diff --git a/lib/openeo/rest/conversions.py b/lib/openeo/rest/conversions.py new file mode 100644 index 000000000..6268bed1a --- /dev/null +++ b/lib/openeo/rest/conversions.py @@ -0,0 +1,124 @@ +""" +Helpers for data conversions between Python ecosystem data types and openEO data structures. +""" + +from __future__ import annotations + +import typing + +import numpy as np +import pandas + +from openeo.internal.warnings import deprecated + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + import xarray + + from openeo.udf import XarrayDataCube + + +class InvalidTimeSeriesException(ValueError): + pass + + +def timeseries_json_to_pandas(timeseries: dict, index: str = "date", auto_collapse=True) -> pandas.DataFrame: + """ + Convert a timeseries JSON object as returned by the `aggregate_spatial` process to a pandas DataFrame object + + This timeseries data has three dimensions in general: date, polygon index and band index. + One of these will be used as index of the resulting dataframe (as specified by the `index` argument), + and the other two will be used as multilevel columns. + When there is just a single polygon or band in play, the dataframe will be simplified + by removing the corresponding dimension if `auto_collapse` is enabled (on by default). + + :param timeseries: dictionary as returned by `aggregate_spatial` + :param index: which dimension should be used for the DataFrame index: 'date' or 'polygon' + :param auto_collapse: whether single band or single polygon cases should be simplified automatically + + :return: pandas DataFrame or Series + """ + # The input timeseries dictionary is assumed to have this structure: + # {dict mapping date -> [list with one item per polygon: [list with one float/None per band or empty list]]} + # TODO is this format of `aggregate_spatial` standardized across backends? Or can we detect the structure? + # TODO: option to pass a path to a JSON file as input? + + # Some quick checks + if len(timeseries) == 0: + raise InvalidTimeSeriesException("Empty data set") + polygon_counts = set(len(polygon_data) for polygon_data in timeseries.values()) + if polygon_counts == {0}: + raise InvalidTimeSeriesException("No polygon data for each date") + elif 0 in polygon_counts: + # TODO: still support this use case? + raise InvalidTimeSeriesException("No polygon data for some dates ({p})".format(p=polygon_counts)) + elif len(polygon_counts) > 1: + raise InvalidTimeSeriesException("Inconsistent polygon counts: {p}".format(p=polygon_counts)) + # Count the number of bands in the timeseries, so we can provide a fallback array for missing data + band_counts = set(len(band_data) for polygon_data in timeseries.values() for band_data in polygon_data) + if band_counts == {0}: + raise InvalidTimeSeriesException("Zero bands everywhere") + band_counts.discard(0) + if len(band_counts) != 1: + raise InvalidTimeSeriesException("Inconsistent band counts: {b}".format(b=band_counts)) + band_count = band_counts.pop() + band_data_fallback = [np.nan] * band_count + # Load the timeseries data in a pandas Series with multi-index ["date", "polygon", "band"] + s = pandas.DataFrame.from_records( + ( + (date, polygon_index, band_index, value) + for (date, polygon_data) in timeseries.items() + for polygon_index, band_data in enumerate(polygon_data) + for band_index, value in enumerate(band_data or band_data_fallback) + ), + columns=["date", "polygon", "band", "value"], + index=["date", "polygon", "band"] + )["value"].rename(None) + # TODO convert date to real date index? + + if auto_collapse: + if s.index.levshape[2] == 1: + # Single band case + s.index = s.index.droplevel("band") + if s.index.levshape[1] == 1: + # Single polygon case + s.index = s.index.droplevel("polygon") + + # Reshape as desired + if index == "date": + if len(s.index.names) > 1: + return s.unstack("date").T + else: + return s + elif index == "polygon": + return s.unstack("polygon").T + else: + raise ValueError(index) + + +@deprecated("Use :py:meth:`XarrayDataCube.from_file` instead.", version="0.7.0") +def datacube_from_file(filename, fmt="netcdf") -> XarrayDataCube: + from openeo.udf.xarraydatacube import XarrayDataCube + return XarrayDataCube.from_file(path=filename, fmt=fmt) + + +@deprecated("Use :py:meth:`XarrayDataCube.save_to_file` instead.", version="0.7.0") +def datacube_to_file(datacube: XarrayDataCube, filename, fmt="netcdf"): + return datacube.save_to_file(path=filename, fmt=fmt) + + +@deprecated("Use :py:meth:`XarrayIO.to_json_file` instead", version="0.7.0") +def _save_DataArray_to_JSON(filename, array: xarray.DataArray): + from openeo.udf.xarraydatacube import XarrayIO + return XarrayIO.to_json_file(array=array, path=filename) + + +@deprecated("Use :py:meth:`XarrayIO.to_netcdf_file` instead", version="0.7.0") +def _save_DataArray_to_NetCDF(filename, array: xarray.DataArray): + from openeo.udf.xarraydatacube import XarrayIO + return XarrayIO.to_netcdf_file(array=array, path=filename) + + +@deprecated("Use :py:meth:`XarrayDataCube.plot` instead.", version="0.7.0") +def datacube_plot(datacube: XarrayDataCube, *args, **kwargs): + datacube.plot(*args, **kwargs) diff --git a/lib/openeo/rest/datacube.py b/lib/openeo/rest/datacube.py new file mode 100644 index 000000000..c91fb722a --- /dev/null +++ b/lib/openeo/rest/datacube.py @@ -0,0 +1,2769 @@ +""" +The main module for creating earth observation processes. It aims to easily build complex process chains, that can +be evaluated by an openEO backend. + +.. data:: THIS + + Symbolic reference to the current data cube, to be used as argument in :py:meth:`DataCube.process()` calls + +""" +from __future__ import annotations + +import datetime +import logging +import pathlib +import typing +import warnings +from builtins import staticmethod +from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union + +import numpy as np +import requests +import shapely.geometry +import shapely.geometry.base +from shapely.geometry import MultiPolygon, Polygon, mapping + +from openeo.api.process import Parameter +from openeo.dates import get_temporal_extent +from openeo.internal.documentation import openeo_process +from openeo.internal.graph_building import PGNode, ReduceNode, _FromNodeMixin +from openeo.internal.jupyter import in_jupyter_context +from openeo.internal.processes.builder import ( + ProcessBuilderBase, + convert_callable_to_pgnode, + get_parameter_names, +) +from openeo.internal.warnings import UserDeprecationWarning, deprecated, legacy_alias +from openeo.metadata import ( + Band, + BandDimension, + CollectionMetadata, + SpatialDimension, + TemporalDimension, + metadata_from_stac, +) +from openeo.processes import ProcessBuilder +from openeo.rest import BandMathException, OpenEoClientException, OperatorException +from openeo.rest._datacube import ( + THIS, + UDF, + _ensure_save_result, + _ProcessGraphAbstraction, + build_child_callback, +) +from openeo.rest.graph_building import CollectionProperty +from openeo.rest.job import BatchJob, RESTJob +from openeo.rest.mlmodel import MlModel +from openeo.rest.service import Service +from openeo.rest.udp import RESTUserDefinedProcess +from openeo.rest.vectorcube import VectorCube +from openeo.util import dict_no_none, guess_format, normalize_crs, rfc3339 + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + import xarray + + from openeo.rest.connection import Connection + from openeo.udf import XarrayDataCube + + +log = logging.getLogger(__name__) + + +# Type annotation aliases +InputDate = Union[str, datetime.date, Parameter, PGNode, ProcessBuilderBase, None] + + +class DataCube(_ProcessGraphAbstraction): + """ + Class representing a openEO (raster) data cube. + + The data cube is represented by its corresponding openeo "process graph" + and this process graph can be "grown" to a desired workflow by calling the appropriate methods. + """ + + # TODO: set this based on back-end or user preference? + _DEFAULT_RASTER_FORMAT = "GTiff" + + def __init__( + self, graph: PGNode, connection: Optional[Connection] = None, metadata: Optional[CollectionMetadata] = None + ): + super().__init__(pgnode=graph, connection=connection) + self.metadata: Optional[CollectionMetadata] = metadata + + def process( + self, + process_id: str, + arguments: Optional[dict] = None, + metadata: Optional[CollectionMetadata] = None, + namespace: Optional[str] = None, + **kwargs, + ) -> DataCube: + """ + Generic helper to create a new DataCube by applying a process. + + :param process_id: process id of the process. + :param arguments: argument dictionary for the process. + :param metadata: optional: metadata to override original cube metadata (e.g. when reducing dimensions) + :param namespace: optional: process namespace + :return: new DataCube instance + """ + pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs) + return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata) + + graph_add_node = legacy_alias(process, "graph_add_node", since="0.1.1") + + def process_with_node(self, pg: PGNode, metadata: Optional[CollectionMetadata] = None) -> DataCube: + """ + Generic helper to create a new DataCube by applying a process (given as process graph node) + + :param pg: process graph node (containing process id and arguments) + :param metadata: optional: metadata to override original cube metadata (e.g. when reducing dimensions) + :return: new DataCube instance + """ + # TODO: deep copy `self.metadata` instead of using same instance? + # TODO: cover more cases where metadata has to be altered + # TODO: deprecate `process_with_node``: little added value over just calling DataCube() directly + return DataCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata) + + def _do_metadata_normalization(self) -> bool: + """Do metadata-based normalization/validation of dimension names, band names, ...""" + return isinstance(self.metadata, CollectionMetadata) + + def _assert_valid_dimension_name(self, name: str) -> str: + if self._do_metadata_normalization(): + self.metadata.assert_valid_dimension(name) + return name + + @classmethod + @openeo_process + def load_collection( + cls, + collection_id: Union[str, Parameter], + connection: Optional[Connection] = None, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Union[None, List[str], Parameter] = None, + fetch_metadata: bool = True, + properties: Union[ + None, Dict[str, Union[str, PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty + ] = None, + max_cloud_cover: Optional[float] = None, + ) -> DataCube: + """ + Create a new Raster Data cube. + + :param collection_id: image collection identifier + :param connection: The backend connection to use. + Can be ``None`` to work without connection and collection metadata. + :param spatial_extent: limit data to specified bounding box or polygons + :param temporal_extent: limit data to specified temporal interval. + Typically, just a two-item list or tuple containing start and end date. + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + :param bands: only add the specified bands. + :param properties: limit data by metadata property predicates. + See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of such predicates. + :param max_cloud_cover: shortcut to set maximum cloud cover ("eo:cloud_cover" collection property) + :return: new DataCube containing the collection + + .. versionchanged:: 0.13.0 + added the ``max_cloud_cover`` argument. + + .. versionchanged:: 0.23.0 + Argument ``temporal_extent``: add support for year/month shorthand notation + as discussed at :ref:`date-shorthand-handling`. + + .. versionchanged:: 0.26.0 + Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument. + """ + if temporal_extent: + temporal_extent = cls._get_temporal_extent(extent=temporal_extent) + + if isinstance(spatial_extent, Parameter): + if spatial_extent.schema.get("type") != "object": + warnings.warn( + "Unexpected parameterized `spatial_extent` in `load_collection`:" + f" expected schema with type 'object' but got {spatial_extent.schema!r}." + ) + arguments = { + 'id': collection_id, + # TODO: spatial_extent could also be a "geojson" subtype object, so we might want to allow (and convert) shapely shapes as well here. + 'spatial_extent': spatial_extent, + 'temporal_extent': temporal_extent, + } + if isinstance(collection_id, Parameter): + fetch_metadata = False + metadata: Optional[CollectionMetadata] = ( + connection.collection_metadata(collection_id) if connection and fetch_metadata else None + ) + if bands: + if isinstance(bands, str): + bands = [bands] + elif isinstance(bands, Parameter): + metadata = None + if metadata: + bands = [b if isinstance(b, str) else metadata.band_dimension.band_name(b) for b in bands] + metadata = metadata.filter_bands(bands) + arguments['bands'] = bands + + if isinstance(properties, list): + # TODO: warn about items that are not CollectionProperty objects instead of silently dropping them. + properties = {p.name: p.from_node() for p in properties if isinstance(p, CollectionProperty)} + if isinstance(properties, CollectionProperty): + properties = {properties.name: properties.from_node()} + elif properties is None: + properties = {} + if max_cloud_cover: + properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover + if properties: + summaries = metadata and metadata.get("summaries") or {} + undefined_properties = set(properties.keys()).difference(summaries.keys()) + if undefined_properties: + warnings.warn( + f"{collection_id} property filtering with properties that are undefined " + f"in the collection metadata (summaries): {', '.join(undefined_properties)}.", + stacklevel=2, + ) + arguments["properties"] = { + prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items() + } + + pg = PGNode( + process_id='load_collection', + arguments=arguments + ) + return cls(graph=pg, connection=connection, metadata=metadata) + + create_collection = legacy_alias( + load_collection, name="create_collection", since="0.4.6" + ) + + @classmethod + @deprecated(reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.",version="0.25.0") + def load_disk_collection(cls, connection: Connection, file_format: str, glob_pattern: str, **options) -> DataCube: + """ + Loads image data from disk as a DataCube. + This is backed by a non-standard process ('load_disk_data'). This will eventually be replaced by standard options such as + :py:meth:`openeo.rest.connection.Connection.load_stac` or https://processes.openeo.org/#load_uploaded_files + + + :param connection: The connection to use to connect with the backend. + :param file_format: the file format, e.g. 'GTiff' + :param glob_pattern: a glob pattern that matches the files to load from disk + :param options: options specific to the file format + :return: the data as a DataCube + """ + pg = PGNode( + process_id='load_disk_data', + arguments={ + 'format': file_format, + 'glob_pattern': glob_pattern, + 'options': options + } + ) + return cls(graph=pg, connection=connection) + + @classmethod + def load_stac( + cls, + url: str, + spatial_extent: Union[Dict[str, float], Parameter, None] = None, + temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None, + bands: Optional[List[str]] = None, + properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None, + connection: Optional[Connection] = None, + ) -> DataCube: + """ + Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`. + A batch job result can be loaded by providing a reference to it. + + If supported by the underlying metadata and file format, the data that is added to the data cube can be + restricted with the parameters ``spatial_extent``, ``temporal_extent`` and ``bands``. + If no data is available for the given extents, a ``NoDataAvailable`` error is thrown. + + Remarks: + + * The bands (and all dimensions that specify nominal dimension labels) are expected to be ordered as + specified in the metadata if the ``bands`` parameter is set to ``null``. + * If no additional parameter is specified this would imply that the whole data set is expected to be loaded. + Due to the large size of many data sets, this is not recommended and may be optimized by back-ends to only + load the data that is actually required after evaluating subsequent processes such as filters. + This means that the values should be processed only after the data has been limited to the required extent + and as a consequence also to a manageable size. + + + :param url: The URL to a static STAC catalog (STAC Item, STAC Collection, or STAC Catalog) + or a specific STAC API Collection that allows to filter items and to download assets. + This includes batch job results, which itself are compliant to STAC. + For external URLs, authentication details such as API keys or tokens may need to be included in the URL. + + Batch job results can be specified in two ways: + + - For Batch job results at the same back-end, a URL pointing to the corresponding batch job results + endpoint should be provided. The URL usually ends with ``/jobs/{id}/results`` and ``{id}`` + is the corresponding batch job ID. + - For external results, a signed URL must be provided. Not all back-ends support signed URLs, + which are provided as a link with the link relation `canonical` in the batch job result metadata. + :param spatial_extent: + Limits the data to load to the specified bounding box or polygons. + + For raster data, the process loads the pixel into the data cube if the point at the pixel center intersects + with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + + For vector data, the process loads the geometry into the data cube if the geometry is fully within the + bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). + Empty geometries may only be in the data cube if no spatial extent has been provided. + + The GeoJSON can be one of the following feature types: + + * A ``Polygon`` or ``MultiPolygon`` geometry, + * a ``Feature`` with a ``Polygon`` or ``MultiPolygon`` geometry, or + * a ``FeatureCollection`` containing at least one ``Feature`` with ``Polygon`` or ``MultiPolygon`` geometries. + + Set this parameter to ``None`` to set no limit for the spatial extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_bbox()`` or ``filter_spatial()`` directly after loading unbounded data. + + :param temporal_extent: + Limits the data to load to the specified left-closed temporal interval. + Applies to all temporal dimensions. + The interval has to be specified as an array with exactly two elements: + + 1. The first element is the start of the temporal interval. + The specified instance in time is **included** in the interval. + 2. The second element is the end of the temporal interval. + The specified instance in time is **excluded** from the interval. + + The second element must always be greater/later than the first element. + Otherwise, a `TemporalExtentEmpty` exception is thrown. + + Also supports open intervals by setting one of the boundaries to ``None``, but never both. + + Set this parameter to ``None`` to set no limit for the temporal extent. + Be careful with this when loading large datasets. It is recommended to use this parameter instead of + using ``filter_temporal()`` directly after loading unbounded data. + + :param bands: + Only adds the specified bands into the data cube so that bands that don't match the list + of band names are not available. Applies to all dimensions of type `bands`. + + Either the unique band name (metadata field ``name`` in bands) or one of the common band names + (metadata field ``common_name`` in bands) can be specified. + If the unique band name and the common name conflict, the unique band name has a higher priority. + + The order of the specified array defines the order of the bands in the data cube. + If multiple bands match a common name, all matched bands are included in the original order. + + It is recommended to use this parameter instead of using ``filter_bands()`` directly after loading unbounded data. + + :param properties: + Limits the data by metadata properties to include only data in the data cube which + all given conditions return ``True`` for (AND operation). + + Specify key-value-pairs with the key being the name of the metadata property, + which can be retrieved with the openEO Data Discovery for Collections. + The value must be a condition (user-defined process) to be evaluated against a STAC API. + This parameter is not supported for static STAC. + + :param connection: The connection to use to connect with the backend. + + .. versionadded:: 0.33.0 + + """ + arguments = {"url": url} + # TODO #425 more normalization/validation of extent/band parameters + if spatial_extent: + arguments["spatial_extent"] = spatial_extent + if temporal_extent: + arguments["temporal_extent"] = DataCube._get_temporal_extent(extent=temporal_extent) + if bands: + arguments["bands"] = bands + if properties: + arguments["properties"] = { + prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items() + } + graph = PGNode("load_stac", arguments=arguments) + try: + metadata = metadata_from_stac(url) + except Exception: + log.warning(f"Failed to extract cube metadata from STAC URL {url}", exc_info=True) + metadata = None + return cls(graph=graph, connection=connection, metadata=metadata) + + @classmethod + def _get_temporal_extent( + cls, + *args, + start_date: InputDate = None, + end_date: InputDate = None, + extent: Union[Sequence[InputDate], Parameter, str, None] = None, + ) -> Union[List[Union[str, Parameter, PGNode, None]], Parameter]: + """Parameter aware temporal_extent normalizer""" + # TODO: move this outside of DataCube class + # TODO: return extent as tuple instead of list + if len(args) == 1 and isinstance(args[0], Parameter): + assert start_date is None and end_date is None and extent is None + return args[0] + elif len(args) == 0 and isinstance(extent, Parameter): + assert start_date is None and end_date is None + # TODO: warn about unexpected parameter schema + return extent + else: + def convertor(d: Any) -> Any: + # TODO: can this be generalized through _FromNodeMixin? + if isinstance(d, Parameter) or isinstance(d, PGNode): + # TODO: warn about unexpected parameter schema + return d + elif isinstance(d, ProcessBuilderBase): + return d.pgnode + else: + return rfc3339.normalize(d) + + return list( + get_temporal_extent(*args, start_date=start_date, end_date=end_date, extent=extent, convertor=convertor) + ) + + @openeo_process + def filter_temporal( + self, + *args, + start_date: InputDate = None, + end_date: InputDate = None, + extent: Union[Sequence[InputDate], Parameter, str, None] = None, + ) -> DataCube: + """ + Limit the DataCube to a certain date range, which can be specified in several ways: + + >>> cube.filter_temporal("2019-07-01", "2019-08-01") + >>> cube.filter_temporal(["2019-07-01", "2019-08-01"]) + >>> cube.filter_temporal(extent=["2019-07-01", "2019-08-01"]) + >>> cube.filter_temporal(start_date="2019-07-01", end_date="2019-08-01"]) + + See :ref:`filtering-on-temporal-extent-section` for more details on temporal extent handling and shorthand notation. + + :param start_date: start date of the filter (inclusive), as a string or date object + :param end_date: end date of the filter (exclusive), as a string or date object + :param extent: temporal extent. + Typically, specified as a two-item list or tuple containing start and end date. + + .. versionchanged:: 0.23.0 + Arguments ``start_date``, ``end_date`` and ``extent``: + add support for year/month shorthand notation as discussed at :ref:`date-shorthand-handling`. + """ + if len(args) == 1 and isinstance(args[0], (str)): + raise OpenEoClientException( + f"filter_temporal() with a single string argument ({args[0]!r}) is ambiguous." + f" If you want a half-unbounded interval, use something like filter_temporal({args[0]!r}, None) or use explicit keyword arguments." + f" If you want the full interval covering all of {args[0]!r}, use something like filter_temporal(extent={args[0]!r})." + ) + return self.process( + process_id='filter_temporal', + arguments={ + 'data': THIS, + 'extent': self._get_temporal_extent(*args, start_date=start_date, end_date=end_date, extent=extent) + } + ) + + @openeo_process + def filter_bbox( + self, + *args, + west: Optional[float] = None, + south: Optional[float] = None, + east: Optional[float] = None, + north: Optional[float] = None, + crs: Optional[Union[int, str]] = None, + base: Optional[float] = None, + height: Optional[float] = None, + bbox: Optional[Sequence[float]] = None, + ) -> DataCube: + """ + Limits the data cube to the specified bounding box. + + The bounding box can be specified in multiple ways. + + - With keyword arguments:: + + >>> cube.filter_bbox(west=3, south=51, east=4, north=52, crs=4326) + + - With a (west, south, east, north) list or tuple + (note that EPSG:4326 is the default CRS, so it's not necessary to specify it explicitly):: + + >>> cube.filter_bbox([3, 51, 4, 52]) + >>> cube.filter_bbox(bbox=[3, 51, 4, 52]) + + - With a bbox dictionary:: + + >>> bbox = {"west": 3, "south": 51, "east": 4, "north": 52, "crs": 4326} + >>> cube.filter_bbox(bbox) + >>> cube.filter_bbox(bbox=bbox) + >>> cube.filter_bbox(**bbox) + + - With a shapely geometry (of which the bounding box will be used):: + + >>> cube.filter_bbox(geometry) + >>> cube.filter_bbox(bbox=geometry) + + - Passing a parameter:: + + >>> bbox_param = Parameter(name="my_bbox", schema="object") + >>> cube.filter_bbox(bbox_param) + >>> cube.filter_bbox(bbox=bbox_param) + + - With a CRS other than EPSG 4326:: + + >>> cube.filter_bbox( + ... west=652000, east=672000, north=5161000, south=5181000, + ... crs=32632 + ... ) + + - Deprecated: positional arguments are also supported, + but follow a non-standard order for legacy reasons:: + + >>> west, east, north, south = 3, 4, 52, 51 + >>> cube.filter_bbox(west, east, north, south) + + :param crs: value describing the coordinate reference system. + Typically just an int (interpreted as EPSG code, e.g. ``4326``) + or a string (handled as authority string, e.g. ``"EPSG:4326"``). + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + """ + if args and any(k is not None for k in (west, south, east, north, bbox)): + raise ValueError("Don't mix positional arguments with keyword arguments.") + if bbox and any(k is not None for k in (west, south, east, north)): + raise ValueError("Don't mix `bbox` with `west`/`south`/`east`/`north` keyword arguments.") + + if args: + if 4 <= len(args) <= 5: + # Handle old-style west-east-north-south order + # TODO remove handling of this legacy order? + warnings.warn("Deprecated argument order usage: `filter_bbox(west, east, north, south)`." + " Use keyword arguments or tuple/list argument instead.") + west, east, north, south = args[:4] + if len(args) > 4: + crs = normalize_crs(args[4]) + elif len(args) == 1 and (isinstance(args[0], (list, tuple)) and len(args[0]) == 4 + or isinstance(args[0], (dict, shapely.geometry.base.BaseGeometry, Parameter))): + bbox = args[0] + else: + raise ValueError(args) + + if isinstance(bbox, Parameter): + if bbox.schema.get("type") != "object": + warnings.warn( + "Unexpected parameterized `extent` in `filter_bbox`:" + f" expected schema with type 'object' but got {bbox.schema!r}." + ) + extent = bbox + else: + if bbox: + if isinstance(bbox, shapely.geometry.base.BaseGeometry): + west, south, east, north = bbox.bounds + elif isinstance(bbox, (list, tuple)) and len(bbox) == 4: + west, south, east, north = bbox[:4] + elif isinstance(bbox, dict): + west, south, east, north = (bbox[k] for k in ["west", "south", "east", "north"]) + if "crs" in bbox: + crs = bbox["crs"] + else: + raise ValueError(bbox) + + extent = {'west': west, 'east': east, 'north': north, 'south': south} + extent.update(dict_no_none(crs=crs, base=base, height=height)) + + return self.process( + process_id='filter_bbox', + arguments={ + 'data': THIS, + 'extent': extent + } + ) + + @openeo_process + def filter_spatial(self, geometries) -> DataCube: + """ + Limits the data cube over the spatial dimensions to the specified geometries. + + - For polygons, the filter retains a pixel in the data cube if the point at the pixel center intersects with + at least one of the polygons (as defined in the Simple Features standard by the OGC). + - For points, the process considers the closest pixel center. + - For lines (line strings), the process considers all the pixels whose centers are closest to at least one + point on the line. + + More specifically, pixels outside of the bounding box of the given geometry will not be available after filtering. + All pixels inside the bounding box that are not retained will be set to null (no data). + + :param geometries: One or more geometries used for filtering, specified as GeoJSON in EPSG:4326. + :return: A data cube restricted to the specified geometries. The dimensions and dimension properties (name, + type, labels, reference system and resolution) remain unchanged, except that the spatial dimensions have less + (or the same) dimension labels. + """ + valid_geojson_types = [ + "Point", "MultiPoint", "LineString", "MultiLineString", + "Polygon", "MultiPolygon", "GeometryCollection", "FeatureCollection" + ] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types, crs=None) + return self.process( + process_id='filter_spatial', + arguments={ + 'data': THIS, + 'geometries': geometries + } + ) + + @openeo_process + def filter_bands(self, bands: Union[List[Union[str, int]], str]) -> DataCube: + """ + Filter the data cube by the given bands + + :param bands: list of band names, common names or band indices. Single band name can also be given as string. + :return: a DataCube instance + """ + if isinstance(bands, str): + bands = [bands] + if self._do_metadata_normalization(): + bands = [self.metadata.band_dimension.band_name(b) for b in bands] + cube = self.process( + process_id="filter_bands", + arguments={"data": THIS, "bands": bands}, + metadata=self.metadata.filter_bands(bands) if self.metadata else None, + ) + return cube + + @openeo_process + def filter_labels( + self, condition: Union[PGNode, Callable], dimension: str, context: Optional[dict] = None + ) -> DataCube: + """ + Filters the dimension labels in the data cube for the given dimension. + Only the dimension labels that match the specified condition are preserved, + all other labels with their corresponding data get removed. + + :param condition: the "child callback" which will be given a single label value (number or string) + and returns a boolean expressing if the label should be preserved. + Also see :ref:`callbackfunctions`. + :param dimension: The name of the dimension to filter on. + + .. versionadded:: 0.27.0 + """ + condition = build_child_callback(condition, parent_parameters=["value"]) + return self.process( + process_id="filter_labels", + arguments=dict_no_none(data=THIS, condition=condition, dimension=dimension, context=context), + ) + + band_filter = legacy_alias(filter_bands, "band_filter", since="0.1.0") + + def band(self, band: Union[str, int]) -> DataCube: + """ + Filter out a single band + + :param band: band name, band common name or band index. + :return: a DataCube instance + """ + if self._do_metadata_normalization(): + band = self.metadata.band_dimension.band_index(band) + arguments = {"data": {"from_parameter": "data"}} + if isinstance(band, int): + arguments["index"] = band + else: + arguments["label"] = band + return self.reduce_bands(reducer=PGNode(process_id="array_element", arguments=arguments)) + + @openeo_process + def resample_spatial( + self, resolution: Union[float, Tuple[float, float]], projection: Union[int, str] = None, + method: str = 'near', align: str = 'upper-left' + ) -> DataCube: + return self.process('resample_spatial', { + 'data': THIS, + 'resolution': resolution, + 'projection': projection, + 'method': method, + 'align': align + }) + + def resample_cube_spatial(self, target: DataCube, method: str = "near") -> DataCube: + """ + Resamples the spatial dimensions (x,y) from a source data cube to align with the corresponding + dimensions of the given target data cube. + Returns a new data cube with the resampled dimensions. + + To resample a data cube to a specific resolution or projection regardless of an existing target + data cube, refer to :py:meth:`resample_spatial`. + + :param target: A data cube that describes the spatial target resolution. + :param method: Resampling method to use. + :return: + """ + return self.process("resample_cube_spatial", {"data": self, "target": target, "method": method}) + + @openeo_process + def resample_cube_temporal( + self, target: DataCube, dimension: Optional[str] = None, valid_within: Optional[int] = None + ) -> DataCube: + """ + Resamples one or more given temporal dimensions from a source data cube to align with the corresponding + dimensions of the given target data cube using the nearest neighbor method. + Returns a new data cube with the resampled dimensions. + + By default, this process simply takes the nearest neighbor independent of the value (including values such as + no-data / ``null``). Depending on the data cubes this may lead to values being assigned to two target timestamps. + To only consider valid values in a specific range around the target timestamps, use the parameter ``valid_within``. + + The rare case of ties is resolved by choosing the earlier timestamps. + + :param target: A data cube that describes the temporal target resolution. + :param dimension: The name of the temporal dimension to resample. + :param valid_within: + :return: + + .. versionadded:: 0.10.0 + """ + return self.process( + "resample_cube_temporal", + dict_no_none({"data": self, "target": target, "dimension": dimension, "valid_within": valid_within}) + ) + + def _operator_binary(self, operator: str, other: Union[DataCube, int, float], reverse=False) -> DataCube: + """Generic handling of (mathematical) binary operator""" + band_math_mode = self._in_bandmath_mode() + if band_math_mode: + if isinstance(other, (int, float)): + return self._bandmath_operator_binary_scalar(operator, other, reverse=reverse) + elif isinstance(other, DataCube): + return self._bandmath_operator_binary_cubes(operator, other) + else: + if isinstance(other, DataCube): + return self._merge_operator_binary_cubes(operator, other) + elif isinstance(other, (int, float)): + # "`apply` math" mode + return self._apply_operator( + operator=operator, other=other, reverse=reverse + ) + raise OperatorException( + f"Unsupported operator {operator!r} with `other` type {type(other)!r} (band math mode={band_math_mode})" + ) + + def _operator_unary(self, operator: str, **kwargs) -> DataCube: + band_math_mode = self._in_bandmath_mode() + if band_math_mode: + return self._bandmath_operator_unary(operator, **kwargs) + else: + return self._apply_operator(operator=operator, extra_arguments=kwargs) + + def _apply_operator( + self, + operator: str, + other: Optional[Union[int, float]] = None, + reverse: Optional[bool] = None, + extra_arguments: Optional[dict] = None, + ) -> DataCube: + """ + Apply a unary or binary operator/process, + by appending to existing `apply` node, or starting a new one. + + :param operator: process id of operator + :param other: for binary operators: "other" argument + :param reverse: for binary operators: "self" and "other" should be swapped (reflected operator mode) + """ + if self.result_node().process_id == "apply": + # Append to existing `apply` node + orig_apply = self.result_node() + data = orig_apply.arguments["data"] + x = {"from_node": orig_apply.arguments["process"]["process_graph"]} + context = orig_apply.arguments.get("context") + else: + # Start new `apply` node. + data = self + x = {"from_parameter": "x"} + context = None + # Build args for child callback. + args = {"x": x, **(extra_arguments or {})} + if other is not None: + # Binary operator mode + args["y"] = other + if reverse: + args["x"], args["y"] = args["y"], args["x"] + child_pg = PGNode(process_id=operator, arguments=args) + return self.process_with_node( + PGNode( + process_id="apply", + arguments=dict_no_none( + data=data, + process={"process_graph": child_pg}, + context=context, + ), + ) + ) + + @openeo_process(mode="operator") + def add(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("add", other, reverse=reverse) + + @openeo_process(mode="operator") + def subtract(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("subtract", other, reverse=reverse) + + @openeo_process(mode="operator") + def divide(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("divide", other, reverse=reverse) + + @openeo_process(mode="operator") + def multiply(self, other: Union[DataCube, int, float], reverse=False) -> DataCube: + return self._operator_binary("multiply", other, reverse=reverse) + + @openeo_process + def normalized_difference(self, other: DataCube) -> DataCube: + # This DataCube method is only a convenience function when in band math mode + assert self._in_bandmath_mode() + assert other._in_bandmath_mode() + return self._operator_binary("normalized_difference", other) + + @openeo_process(process_id="or", mode="operator") + def logical_or(self, other: DataCube) -> DataCube: + """ + Apply element-wise logical `or` operation + + :param other: + :return: logical_or(this, other) + """ + return self._operator_binary("or", other) + + @openeo_process(process_id="and", mode="operator") + def logical_and(self, other: DataCube) -> DataCube: + """ + Apply element-wise logical `and` operation + + :param other: + :return: logical_and(this, other) + """ + return self._operator_binary("and", other) + + @openeo_process(process_id="not", mode="operator") + def __invert__(self) -> DataCube: + return self._operator_unary("not") + + @openeo_process(process_id="neq", mode="operator") + def __ne__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("neq", other) + + @openeo_process(process_id="eq", mode="operator") + def __eq__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pixelwise comparison of this data cube with another cube or constant. + + :param other: Another data cube, or a constant + :return: + """ + return self._operator_binary("eq", other) + + @openeo_process(process_id="gt", mode="operator") + def __gt__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pairwise comparison of the bands in this data cube with the bands in the 'other' data cube. + + :param other: + :return: this > other + """ + return self._operator_binary("gt", other) + + @openeo_process(process_id="ge", mode="operator") + def __ge__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("gte", other) + + @openeo_process(process_id="lt", mode="operator") + def __lt__(self, other: Union[DataCube, int, float]) -> DataCube: + """ + Pairwise comparison of the bands in this data cube with the bands in the 'other' data cube. + The number of bands in both data cubes has to be the same. + + :param other: + :return: this < other + """ + return self._operator_binary("lt", other) + + @openeo_process(process_id="le", mode="operator") + def __le__(self, other: Union[DataCube, int, float]) -> DataCube: + return self._operator_binary("lte", other) + + @openeo_process(process_id="add", mode="operator") + def __add__(self, other) -> DataCube: + return self.add(other) + + @openeo_process(process_id="add", mode="operator") + def __radd__(self, other) -> DataCube: + return self.add(other, reverse=True) + + @openeo_process(process_id="subtract", mode="operator") + def __sub__(self, other) -> DataCube: + return self.subtract(other) + + @openeo_process(process_id="subtract", mode="operator") + def __rsub__(self, other) -> DataCube: + return self.subtract(other, reverse=True) + + @openeo_process(process_id="multiply", mode="operator") + def __neg__(self) -> DataCube: + return self.multiply(-1) + + @openeo_process(process_id="multiply", mode="operator") + def __mul__(self, other) -> DataCube: + return self.multiply(other) + + @openeo_process(process_id="multiply", mode="operator") + def __rmul__(self, other) -> DataCube: + return self.multiply(other, reverse=True) + + @openeo_process(process_id="divide", mode="operator") + def __truediv__(self, other) -> DataCube: + return self.divide(other) + + @openeo_process(process_id="divide", mode="operator") + def __rtruediv__(self, other) -> DataCube: + return self.divide(other, reverse=True) + + @openeo_process(process_id="power", mode="operator") + def __rpow__(self, other) -> DataCube: + return self._power(other, reverse=True) + + @openeo_process(process_id="power", mode="operator") + def __pow__(self, other) -> DataCube: + return self._power(other, reverse=False) + + def _power(self, other, reverse=False): + node = self._get_bandmath_node() + x = node.reducer_process_graph() + y = other + if reverse: + x, y = y, x + return self.process_with_node(node.clone_with_new_reducer( + PGNode(process_id="power", base=x, p=y) + )) + + @openeo_process(process_id="power", mode="operator") + def power(self, p: float): + return self._power(other=p, reverse=False) + + @openeo_process(process_id="ln", mode="operator") + def ln(self) -> DataCube: + return self._operator_unary("ln") + + @openeo_process(process_id="log", mode="operator") + def logarithm(self, base: float) -> DataCube: + return self._operator_unary("log", base=base) + + @openeo_process(process_id="log", mode="operator") + def log2(self) -> DataCube: + return self.logarithm(base=2) + + @openeo_process(process_id="log", mode="operator") + def log10(self) -> DataCube: + return self.logarithm(base=10) + + @openeo_process(process_id="or", mode="operator") + def __or__(self, other) -> DataCube: + return self.logical_or(other) + + @openeo_process(process_id="and", mode="operator") + def __and__(self, other): + return self.logical_and(other) + + def _bandmath_operator_binary_cubes( + self, operator, other: DataCube, left_arg_name="x", right_arg_name="y" + ) -> DataCube: + """Band math binary operator with cube as right hand side argument""" + left = self._get_bandmath_node() + right = other._get_bandmath_node() + if left.arguments["data"] != right.arguments["data"]: + raise BandMathException("'Band math' between bands of different data cubes is not supported yet.") + + # Build reducer's sub-processgraph + merged = PGNode( + process_id=operator, + arguments={ + left_arg_name: {"from_node": left.reducer_process_graph()}, + right_arg_name: {"from_node": right.reducer_process_graph()}, + }, + ) + return self.process_with_node(left.clone_with_new_reducer(merged)) + + def _bandmath_operator_binary_scalar(self, operator: str, other: Union[int, float], reverse=False) -> DataCube: + """Band math binary operator with scalar value (int or float) as right hand side argument""" + node = self._get_bandmath_node() + x = {'from_node': node.reducer_process_graph()} + y = other + if reverse: + x, y = y, x + return self.process_with_node(node.clone_with_new_reducer( + PGNode(operator, x=x, y=y) + )) + + def _bandmath_operator_unary(self, operator: str, **kwargs) -> DataCube: + node = self._get_bandmath_node() + return self.process_with_node(node.clone_with_new_reducer( + PGNode(operator, x={'from_node': node.reducer_process_graph()}, **kwargs) + )) + + def _in_bandmath_mode(self) -> bool: + """So-called "band math" mode: current result node is reduce_dimension along "bands" dimension.""" + # TODO #123 is it (still) necessary to make "band" math a special case? + return isinstance(self._pg, ReduceNode) and self._pg.band_math_mode + + def _get_bandmath_node(self) -> ReduceNode: + """Check we are in bandmath mode and return the node""" + if not self._in_bandmath_mode(): + raise BandMathException("Must be in band math mode already") + return self._pg + + def _merge_operator_binary_cubes( + self, operator: str, other: DataCube, left_arg_name="x", right_arg_name="y" + ) -> DataCube: + """Merge two cubes with given operator as overlap_resolver.""" + # TODO #123 reuse an existing merge_cubes process graph if it already exists? + return self.merge_cubes(other, overlap_resolver=PGNode( + process_id=operator, + arguments={ + left_arg_name: {"from_parameter": "x"}, + right_arg_name: {"from_parameter": "y"}, + } + )) + + def _get_geometry_argument( + self, + geometry: Union[ + shapely.geometry.base.BaseGeometry, + dict, + str, + pathlib.Path, + Parameter, + _FromNodeMixin, + ], + valid_geojson_types: List[str], + crs: Optional[str] = None, + ) -> Union[dict, Parameter, PGNode]: + """ + Convert input to a geometry as "geojson" subtype object. + + :param crs: value that encodes a coordinate reference system. + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + """ + if isinstance(geometry, (str, pathlib.Path)): + # Assumption: `geometry` is path to polygon is a path to vector file at backend. + # TODO #104: `read_vector` is non-standard process. + # TODO: If path exists client side: load it client side? + return PGNode(process_id="read_vector", arguments={"filename": str(geometry)}) + elif isinstance(geometry, Parameter): + return geometry + elif isinstance(geometry, _FromNodeMixin): + return geometry.from_node() + + if isinstance(geometry, shapely.geometry.base.BaseGeometry): + geometry = mapping(geometry) + if not isinstance(geometry, dict): + raise OpenEoClientException("Invalid geometry argument: {g!r}".format(g=geometry)) + + if geometry.get("type") not in valid_geojson_types: + raise OpenEoClientException("Invalid geometry type {t!r}, must be one of {s}".format( + t=geometry.get("type"), s=valid_geojson_types + )) + if crs: + # TODO: don't warn when the crs is Lon-Lat like EPSG:4326? + warnings.warn(f"Geometry with non-Lon-Lat CRS {crs!r} is only supported by specific back-ends.") + # TODO #204 alternative for non-standard CRS in GeoJSON object? + epsg_code = normalize_crs(crs) + if epsg_code is not None: + # proj did recognize the CRS + crs_name = f"EPSG:{epsg_code}" + else: + # proj did not recognise this CRS + warnings.warn(f"non-Lon-Lat CRS {crs!r} is not known to the proj library and might not be supported.") + crs_name = crs + geometry["crs"] = {"type": "name", "properties": {"name": crs_name}} + return geometry + + @openeo_process + def aggregate_spatial( + self, + geometries: Union[ + shapely.geometry.base.BaseGeometry, + dict, + str, + pathlib.Path, + Parameter, + VectorCube, + ], + reducer: Union[str, typing.Callable, PGNode], + target_dimension: Optional[str] = None, + crs: Optional[Union[int, str]] = None, + context: Optional[dict] = None, + # TODO arguments: target dimension, context + ) -> VectorCube: + """ + Aggregates statistics for one or more geometries (e.g. zonal statistics for polygons) + over the spatial dimensions. + + :param geometries: a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute ` (:ref:`predefined openEO process function `) + - ``lambda data: data.min()`` (function or lambda) + + :param target_dimension: The new dimension name to be used for storing the results. + :param crs: The spatial reference system of the provided polygon. + By default, longitude-latitude (EPSG:4326) is assumed. + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + + :param context: Additional data to be passed to the reducer process. + + .. note:: this ``crs`` argument is a non-standard/experimental feature, only supported by specific back-ends. + See https://github.com/Open-EO/openeo-processes/issues/235 for details. + """ + valid_geojson_types = [ + "Point", "MultiPoint", "LineString", "MultiLineString", + "Polygon", "MultiPolygon", "GeometryCollection", "Feature", "FeatureCollection" + ] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types, crs=crs) + reducer = build_child_callback(reducer, parent_parameters=["data"]) + return VectorCube( + graph=self._build_pgnode( + process_id="aggregate_spatial", + data=THIS, + geometries=geometries, + reducer=reducer, + arguments=dict_no_none( + target_dimension=target_dimension, context=context + ), + ), + connection=self._connection, + # TODO: also add new "geometry" dimension #457 + metadata=None if self.metadata is None else self.metadata.reduce_spatial(), + ) + + @openeo_process + def aggregate_spatial_window( + self, + reducer: Union[str, typing.Callable, PGNode], + size: List[int], + boundary: str = "pad", + align: str = "upper-left", + context: Optional[dict] = None, + # TODO arguments: target dimension, context + ) -> DataCube: + """ + Aggregates statistics over the horizontal spatial dimensions (axes x and y) of the data cube. + + The pixel grid for the axes x and y is divided into non-overlapping windows with the size + specified in the parameter size. If the number of values for the axes x and y is not a multiple + of the corresponding window size, the behavior specified in the parameters boundary and align + is applied. For each of these windows, the reducer process computes the result. + + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + :param size: Window size in pixels along the horizontal spatial dimensions. + The first value corresponds to the x axis, the second value corresponds to the y axis. + :param boundary: Behavior to apply if the number of values for the axes x and y is not a + multiple of the corresponding value in the size parameter. + Options are: + + - ``pad`` (default): pad the data cube with the no-data value null to fit the required window size. + - ``trim``: trim the data cube to fit the required window size. + + Use the parameter ``align`` to align the data to the desired corner. + + :param align: If the data requires padding or trimming (see parameter ``boundary``), specifies + to which corner of the spatial extent the data is aligned to. For example, if the data is + aligned to the upper left, the process pads/trims at the lower-right. + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values and the same dimensions. + """ + valid_boundary_types = ["pad", "trim"] + valid_align_types = ["lower-left", "upper-left", "lower-right", "upper-right"] + if boundary not in valid_boundary_types: + raise ValueError(f"Provided boundary type not supported. Please use one of {valid_boundary_types} .") + if align not in valid_align_types: + raise ValueError(f"Provided align type not supported. Please use one of {valid_align_types} .") + if len(size) != 2: + raise ValueError(f"Provided size not supported. Please provide a list of 2 integer values.") + + reducer = build_child_callback(reducer, parent_parameters=["data"]) + arguments = { + "data": THIS, + "boundary": boundary, + "align": align, + "size": size, + "reducer": reducer, + "context": context, + } + return self.process(process_id="aggregate_spatial_window", arguments=arguments) + + @openeo_process + def apply_dimension( + self, + code: Optional[str] = None, + runtime=None, + # TODO: drop None default of process (when `code` and `runtime` args can be dropped) + process: Union[str, typing.Callable, UDF, PGNode] = None, + version: Optional[str] = None, + # TODO: dimension has no default (per spec)? + dimension: str = "t", + target_dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a process to all pixel values along a dimension of a raster data cube. For example, + if the temporal dimension is specified the process will work on a time series of pixel values. + + The process to apply is specified by either `code` and `runtime` in case of a UDF, or by providing a callback function + in the `process` argument. + + The process reduce_dimension also applies a process to pixel values along a dimension, but drops + the dimension afterwards. The process apply applies a process to each pixel value in the data cube. + + The target dimension is the source dimension if not specified otherwise in the target_dimension parameter. + The pixel values in the target dimension get replaced by the computed pixel values. The name, type and + reference system are preserved. + + The dimension labels are preserved when the target dimension is the source dimension and the number of + pixel values in the source dimension is equal to the number of values computed by the process. Otherwise, + the dimension labels will be incrementing integers starting from zero, which can be changed using + rename_labels afterwards. The number of labels will equal to the number of values computed by the process. + + :param code: [**deprecated**] UDF code or process identifier (optional) + :param runtime: [**deprecated**] UDF runtime to use (optional) + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns an array of numerical values. + For example: + + - ``"sort"`` (string) + - :py:func:`sort ` (:ref:`predefined openEO process function `) + - ``lambda data: data.concat([42, -3])`` (function or lambda) + + + :param version: [**deprecated**] Version of the UDF runtime to use + :param dimension: The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or null (the default) to use the source dimension + specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. + The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A datacube with the UDF applied to the given dimension. + :raises: DimensionNotAvailable + + .. versionchanged:: 0.13.0 + arguments ``code``, ``runtime`` and ``version`` are deprecated if favor of the standard approach + of using an :py:class:`UDF ` object in the ``process`` argument. + See :ref:`old_udf_api` for more background about the changes. + + """ + # TODO #137 #181 #312 remove support for code/runtime/version + if runtime or (isinstance(code, str) and "\n" in code) or version: + if process: + raise ValueError( + "Cannot specify `process` argument together with deprecated `code`/`runtime`/`version` arguments." + ) + else: + warnings.warn( + "Specifying UDF code through `code`, `runtime` and `version` arguments is deprecated. " + "Instead create an `openeo.UDF` object and pass that to the `process` argument.", + category=UserDeprecationWarning, + stacklevel=2, + ) + process = UDF(code=code, runtime=runtime, version=version, context=context) + else: + process = process or code + process = build_child_callback( + process=process, parent_parameters=["data", "context"], connection=self.connection + ) + arguments = { + "data": THIS, + "process": process, + "dimension": self._assert_valid_dimension_name(dimension), + } + + metadata = self.metadata + if target_dimension is not None: + arguments["target_dimension"] = target_dimension + metadata = self.metadata.reduce_dimension(dimension_name=dimension) if self.metadata else None + if(not target_dimension in self.metadata.dimension_names()): + metadata = self.metadata.add_dimension(target_dimension, label="unknown") + if context is not None: + arguments["context"] = context + result_cube = self.process(process_id="apply_dimension", arguments=arguments, metadata = metadata) + + return result_cube + + @openeo_process + def reduce_dimension( + self, + dimension: str, + reducer: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + process_id="reduce_dimension", + band_math_mode: bool = False, + ) -> DataCube: + """ + Add a reduce process with given reducer callback along given dimension + + :param dimension: the label of the dimension to reduce + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute ` (:ref:`predefined openEO process function `) + - ``lambda data: data.min()`` (function or lambda) + + :param context: Additional data to be passed to the process. + """ + # TODO: check if dimension is valid according to metadata? #116 + # TODO: #125 use/test case for `reduce_dimension_binary`? + reducer = build_child_callback( + process=reducer, parent_parameters=["data", "context"], connection=self.connection + ) + + return self.process_with_node( + ReduceNode( + process_id=process_id, + data=self, + reducer=reducer, + dimension=self._assert_valid_dimension_name(dimension), + context=context, + # TODO #123 is it (still) necessary to make "band" math a special case? + band_math_mode=band_math_mode, + ), + metadata=self.metadata.reduce_dimension(dimension_name=dimension) if self.metadata else None, + ) + + @openeo_process + def reduce_spatial( + self, + reducer: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + ) -> "DataCube": + """ + Add a reduce process with given reducer callback along the spatial dimensions + + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute ` (:ref:`predefined openEO process function `) + - ``lambda data: data.min()`` (function or lambda) + + :param context: Additional data to be passed to the process. + """ + reducer = build_child_callback( + process=reducer, parent_parameters=["data", "context"], connection=self.connection + ) + return self.process( + process_id="reduce_spatial", + data=self, + reducer=reducer, + context=context, + metadata=self.metadata.reduce_spatial(), + ) + + @deprecated("Use :py:meth:`apply_polygon`.", version="0.26.0") + def chunk_polygon( + self, + chunks: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + process: Union[str, PGNode, typing.Callable, UDF], + mask_value: float = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Apply a process to spatial chunks of a data cube. + + .. warning:: experimental process: not generally supported, API subject to change. + + :param chunks: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param process: "child callback" function, see :ref:`callbackfunctions` + :param mask_value: The value used for cells outside the polygon. + This provides a distinction between NoData cells within the polygon (due to e.g. clouds) + and masked cells outside it. If no value is provided, NoData cells are used outside the polygon. + :param context: Additional data to be passed to the process. + """ + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = [ + "Polygon", + "MultiPolygon", + "GeometryCollection", + "Feature", + "FeatureCollection", + ] + chunks = self._get_geometry_argument( + chunks, valid_geojson_types=valid_geojson_types + ) + mask_value = float(mask_value) if mask_value is not None else None + return self.process( + process_id="chunk_polygon", + data=THIS, + chunks=chunks, + process=process, + arguments=dict_no_none( + mask_value=mask_value, + context=context, + ), + ) + + @openeo_process + def apply_polygon( + self, + geometries: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube] = None, + process: Union[str, PGNode, typing.Callable, UDF] = None, + mask_value: Optional[float] = None, + context: Optional[dict] = None, + **kwargs, + ) -> DataCube: + """ + Apply a process to segments of the data cube that are defined by the given polygons. + For each polygon provided, all pixels for which the point at the pixel center intersects + with the polygon (as defined in the Simple Features standard by the OGC) are collected into sub data cubes. + If a pixel is part of multiple of the provided polygons (e.g., when the polygons overlap), + the GeometriesOverlap exception is thrown. + Each sub data cube is passed individually to the given process. + + :param geometries: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param process: "child callback" function, see :ref:`callbackfunctions` + :param mask_value: The value used for pixels outside the polygon. + :param context: Additional data to be passed to the process. + + .. warning:: experimental process: not generally supported, API subject to change. + + .. versionchanged:: 0.32.0 + Argument ``polygons`` was renamed to ``geometries``. + While deprecated, the old name ``polygons`` is still supported + as keyword argument for backwards compatibility. + """ + # TODO drop support for legacy `polygons` argument: + # remove `kwargs, remove default `None` value for `geometries` and `process` + # and the related backwards compatibility code + geometries_parameter = "geometries" + if geometries is None and "polygons" in kwargs: + geometries = kwargs.pop("polygons") + geometries_parameter = "polygons" + warnings.warn( + "In `apply_polygon` use argument `geometries` instead of deprecated 'polygons'.", + category=UserDeprecationWarning, + stacklevel=2, + ) + if kwargs: + raise ValueError(f"Unexpected keyword arguments: {kwargs!r}") + if not geometries: + raise ValueError("No geometries provided.") + + # Note: the `process` argument was given a default value `None` (with the `polygons`/`geometries` argument rename) + # to keep support for legacy `cube.apply_polygon(polygons=..., process=...)` usage: + # `geometries` had to be given a default value, and so did `process` as it comes after it. + # TODO: remove default value for `process` when dropping support for legacy `polygons` argument + assert process is not None + + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = ["Polygon", "MultiPolygon", "Feature", "FeatureCollection"] + geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types) + mask_value = float(mask_value) if mask_value is not None else None + return self.process( + process_id="apply_polygon", + data=THIS, + **{geometries_parameter: geometries}, + process=process, + arguments=dict_no_none( + mask_value=mask_value, + context=context, + ), + ) + + def reduce_bands(self, reducer: Union[str, PGNode, typing.Callable, UDF]) -> DataCube: + """ + Shortcut for :py:meth:`reduce_dimension` along the band dimension + + :param reducer: "child callback" function, see :ref:`callbackfunctions` + """ + return self.reduce_dimension( + dimension=self.metadata.band_dimension.name if self.metadata else "bands", + reducer=reducer, + band_math_mode=True, + ) + + def reduce_temporal(self, reducer: Union[str, PGNode, typing.Callable, UDF]) -> DataCube: + """ + Shortcut for :py:meth:`reduce_dimension` along the temporal dimension + + :param reducer: "child callback" function, see :ref:`callbackfunctions` + """ + return self.reduce_dimension( + dimension=self.metadata.temporal_dimension.name if self.metadata else "t", + reducer=reducer, + ) + + @deprecated( + "Use :py:meth:`reduce_bands` with :py:class:`UDF ` as reducer.", + version="0.13.0", + ) + def reduce_bands_udf(self, code: str, runtime: Optional[str] = None, version: Optional[str] = None) -> DataCube: + """ + Use `reduce_dimension` process with given UDF along band/spectral dimension. + """ + # TODO #181 #312 drop this deprecated pattern + return self.reduce_bands(reducer=UDF(code=code, runtime=runtime, version=version)) + + @openeo_process + def add_dimension(self, name: str, label: str, type: Optional[str] = None): + """ + Adds a new named dimension to the data cube. + Afterwards, the dimension can be referenced with the specified name. If a dimension with the specified name exists, + the process fails with a DimensionExists error. The dimension label of the dimension is set to the specified label. + + This call does not modify the datacube in place, but returns a new datacube with the additional dimension. + + :param name: The name of the dimension to add + :param label: The dimension label. + :param type: Dimension type, allowed values: 'spatial', 'temporal', 'bands', 'other', default value is 'other' + :return: The data cube with a newly added dimension. The new dimension has exactly one dimension label. All other dimensions remain unchanged. + """ + return self.process( + process_id="add_dimension", + arguments=dict_no_none({"data": self, "name": name, "label": label, "type": type}), + metadata=self.metadata.add_dimension(name=name, label=label, type=type) if self.metadata else None, + ) + + @openeo_process + def drop_dimension(self, name: str): + """ + Drops a dimension from the data cube. + Dropping a dimension only works on dimensions with a single dimension label left, otherwise the process fails + with a DimensionLabelCountMismatch exception. Dimension values can be reduced to a single value with a filter + such as filter_bands or the reduce_dimension process. If a dimension with the specified name does not exist, + the process fails with a DimensionNotAvailable exception. + + :param name: The name of the dimension to drop + :return: The data cube with the given dimension dropped. + """ + return self.process( + process_id="drop_dimension", + arguments={"data": self, "name": name}, + metadata=self.metadata.drop_dimension(name=name) if self.metadata else None, + ) + + @deprecated( + "Use :py:meth:`reduce_temporal` with :py:class:`UDF ` as reducer", + version="0.13.0", + ) + def reduce_temporal_udf(self, code: str, runtime="Python", version="latest"): + """ + Apply reduce (`reduce_dimension`) process with given UDF along temporal dimension. + + :param code: The UDF code, compatible with the given runtime and version + :param runtime: The UDF runtime + :param version: The UDF runtime version + """ + # TODO #181 #312 drop this deprecated pattern + return self.reduce_temporal(reducer=UDF(code=code, runtime=runtime, version=version)) + + reduce_tiles_over_time = legacy_alias( + reduce_temporal_udf, name="reduce_tiles_over_time", since="0.1.1" + ) + + @openeo_process + def apply_neighborhood( + self, + process: Union[str, PGNode, typing.Callable, UDF], + size: List[Dict], + overlap: List[dict] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a focal process to a data cube. + + A focal process is a process that works on a 'neighbourhood' of pixels. The neighbourhood can extend into multiple dimensions, this extent is specified by the `size` argument. It is not only (part of) the size of the input window, but also the size of the output for a given position of the sliding window. The sliding window moves with multiples of `size`. + + An overlap can be specified so that neighbourhoods can have overlapping boundaries. This allows for continuity of the output. The values included in the data cube as overlap can't be modified by the given `process`. + + The neighbourhood size should be kept small enough, to avoid running beyond computational resources, but a too small size will result in a larger number of process invocations, which may slow down processing. Window sizes for spatial dimensions typically are in the range of 64 to 512 pixels, while overlaps of 8 to 32 pixels are common. + + The process must not add new dimensions, or remove entire dimensions, but the result can have different dimension labels. + + For the special case of 2D convolution, it is recommended to use ``apply_kernel()``. + + :param size: + :param overlap: + :param process: a callback function that creates a process graph, see :ref:`callbackfunctions` + :param context: Additional data to be passed to the process. + + :return: + """ + return self.process( + process_id="apply_neighborhood", + arguments=dict_no_none( + data=THIS, + process=build_child_callback(process=process, parent_parameters=["data"], connection=self.connection), + size=size, + overlap=overlap, + context=context, + ) + ) + + @openeo_process + def apply( + self, + process: Union[str, typing.Callable, UDF, PGNode], + context: Optional[dict] = None, + ) -> DataCube: + """ + Applies a unary process (a local operation) to each value of the specified or all dimensions in the data cube. + + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives a single numerical value + and returns a single numerical value. + For example: + + - ``"absolute"`` (string) + - :py:func:`absolute ` (:ref:`predefined openEO process function `) + - ``lambda x: x * 2 + 3`` (function or lambda) + + :param context: Additional data to be passed to the process. + + :return: A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube. + """ + return self.process( + process_id="apply", + arguments=dict_no_none( + { + "data": THIS, + "process": build_child_callback(process, parent_parameters=["x"], connection=self.connection), + "context": context, + } + ), + ) + + reduce_temporal_simple = legacy_alias( + reduce_temporal, "reduce_temporal_simple", since="0.13.0" + ) + + @openeo_process(process_id="min", mode="reduce_dimension") + def min_time(self) -> DataCube: + """ + Finds the minimum value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("min") + + @openeo_process(process_id="max", mode="reduce_dimension") + def max_time(self) -> DataCube: + """ + Finds the maximum value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("max") + + @openeo_process(process_id="mean", mode="reduce_dimension") + def mean_time(self) -> DataCube: + """ + Finds the mean value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("mean") + + @openeo_process(process_id="median", mode="reduce_dimension") + def median_time(self) -> DataCube: + """ + Finds the median value of a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("median") + + @openeo_process(process_id="count", mode="reduce_dimension") + def count_time(self) -> DataCube: + """ + Counts the number of images with a valid mask in a time series for all bands of the input dataset. + + :return: a DataCube instance + """ + return self.reduce_temporal("count") + + @openeo_process + def aggregate_temporal( + self, + intervals: List[list], + reducer: Union[str, typing.Callable, PGNode], + labels: Optional[List[str]] = None, + dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Computes a temporal aggregation based on an array of date and/or time intervals. + + Calendar hierarchies such as year, month, week etc. must be transformed into specific intervals by the clients. For each interval, all data along the dimension will be passed through the reducer. The computed values will be projected to the labels, so the number of labels and the number of intervals need to be equal. + + If the dimension is not set, the data cube is expected to only have one temporal dimension. + + :param intervals: Temporal left-closed intervals so that the start time is contained, but not the end time. + :param reducer: the "child callback": + the name of a single openEO process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns a single numerical value. + For example: + + - ``"mean"`` (string) + - :py:func:`absolute ` (:ref:`predefined openEO process function `) + - ``lambda data: data.min()`` (function or lambda) + + :param labels: Labels for the intervals. The number of labels and the number of groups need to be equal. + :param dimension: The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension. + :param context: Additional data to be passed to the reducer. Not set by default. + + :return: A :py:class:`DataCube` containing a result for each time window + """ + return self.process( + process_id="aggregate_temporal", + arguments=dict_no_none( + data=THIS, + intervals=intervals, + labels=labels, + dimension=dimension, + reducer=build_child_callback(reducer, parent_parameters=["data"]), + context=context, + ), + ) + + @openeo_process + def aggregate_temporal_period( + self, + period: str, + reducer: Union[str, PGNode, typing.Callable], + dimension: Optional[str] = None, + context: Optional[Dict] = None, + ) -> DataCube: + """ + Computes a temporal aggregation based on calendar hierarchies such as years, months or seasons. For other calendar hierarchies aggregate_temporal can be used. + + For each interval, all data along the dimension will be passed through the reducer. + + If the dimension is not set or is set to null, the data cube is expected to only have one temporal dimension. + + The period argument specifies the time intervals to aggregate. The following pre-defined values are available: + + - hour: Hour of the day + - day: Day of the year + - week: Week of the year + - dekad: Ten day periods, counted per year with three periods per month (day 1 - 10, 11 - 20 and 21 - end of month). The third dekad of the month can range from 8 to 11 days. For example, the fourth dekad is Feb, 1 - Feb, 10 each year. + - month: Month of the year + - season: Three month periods of the calendar seasons (December - February, March - May, June - August, September - November). + - tropical-season: Six month periods of the tropical seasons (November - April, May - October). + - year: Proleptic years + - decade: Ten year periods (0-to-9 decade), from a year ending in a 0 to the next year ending in a 9. + - decade-ad: Ten year periods (1-to-0 decade) better aligned with the Anno Domini (AD) calendar era, from a year ending in a 1 to the next year ending in a 0. + + + :param period: The period of the time intervals to aggregate. + :param reducer: A reducer to be applied on all values along the specified dimension. The reducer must be a callable process (or a set processes) that accepts an array and computes a single return value of the same type as the input values, for example median. + :param dimension: The temporal dimension for aggregation. All data along the dimension will be passed through the specified reducer. If the dimension is not set, the data cube is expected to only have one temporal dimension. + :param context: Additional data to be passed to the reducer. + + :return: A data cube with the same dimensions. The dimension properties (name, type, labels, reference system and resolution) remain unchanged. + """ + return self.process( + process_id="aggregate_temporal_period", + arguments=dict_no_none( + data=THIS, + period=period, + dimension=dimension, + reducer=build_child_callback(reducer, parent_parameters=["data"]), + context=context, + ), + ) + + @openeo_process + def ndvi(self, nir: str = None, red: str = None, target_band: str = None) -> DataCube: + """ + Normalized Difference Vegetation Index (NDVI) + + :param nir: (optional) name of NIR band + :param red: (optional) name of red band + :param target_band: (optional) name of the newly created band + + :return: a DataCube instance + """ + if self.metadata is None: + metadata = None + elif target_band is None: + metadata = self.metadata.reduce_dimension(self.metadata.band_dimension.name) + else: + # TODO: first drop "bands" dim and re-add it with single "ndvi" band + metadata = self.metadata.append_band(Band(name=target_band, common_name="ndvi")) + return self.process( + process_id="ndvi", + arguments=dict_no_none( + data=THIS, nir=nir, red=red, target_band=target_band + ), + metadata=metadata, + ) + + @openeo_process + def rename_dimension(self, source: str, target: str): + """ + Renames a dimension in the data cube while preserving all other properties. + + :param source: The current name of the dimension. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target: A new Name for the dimension. Fails with a DimensionExists error if a dimension with the specified name exists. + + :return: A new datacube with the dimension renamed. + """ + if self._do_metadata_normalization() and target in self.metadata.dimension_names(): + raise ValueError('Target dimension name conflicts with existing dimension: %s.' % target) + return self.process( + process_id="rename_dimension", + arguments=dict_no_none( + data=THIS, + source=self._assert_valid_dimension_name(source), + target=target, + ), + metadata=self.metadata.rename_dimension(source, target) if self.metadata else None, + ) + + @openeo_process + def rename_labels(self, dimension: str, target: list, source: list = None) -> DataCube: + """ + Renames the labels of the specified dimension in the data cube from source to target. + + :param dimension: Dimension name + :param target: The new names for the labels. + :param source: The names of the labels as they are currently in the data cube. + + :return: An DataCube instance + """ + return self.process( + process_id="rename_labels", + arguments=dict_no_none( + data=THIS, + dimension=self._assert_valid_dimension_name(dimension), + target=target, + source=source, + ), + metadata=self.metadata.rename_labels(dimension, target, source) if self.metadata else None, + ) + + @openeo_process(mode="apply") + def linear_scale_range(self, input_min, input_max, output_min, output_max) -> DataCube: + """ + Performs a linear transformation between the input and output range. + + The given number in x is clipped to the bounds specified in inputMin and inputMax so that the underlying formula + + ((x - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin) + outputMin + + never returns any value lower than outputMin or greater than outputMax. + + Potential use case include scaling values to the 8-bit range (0 - 255) often used for numeric representation of + values in one of the channels of the RGB colour model or calculating percentages (0 - 100). + + The no-data value null is passed through and therefore gets propagated. + + :param input_min: Minimum input value + :param input_max: Maximum input value + :param output_min: Minimum value of the desired output range. + :param output_max: Maximum value of the desired output range. + :return: a DataCube instance + """ + + return self.apply(lambda x: x.linear_scale_range(input_min, input_max, output_min, output_max)) + + @openeo_process + def mask(self, mask: DataCube = None, replacement=None) -> DataCube: + """ + Applies a mask to a raster data cube. To apply a vector mask use `mask_polygon`. + + A mask is a raster data cube for which corresponding pixels among `data` and `mask` + are compared and those pixels in `data` are replaced whose pixels in `mask` are non-zero + (for numbers) or true (for boolean values). + The pixel values are replaced with the value specified for `replacement`, + which defaults to null (no data). + + :param mask: the raster mask + :param replacement: the value to replace the masked pixels with + """ + return self.process( + process_id="mask", + arguments=dict_no_none(data=self, mask=mask, replacement=replacement), + ) + + @openeo_process + def mask_polygon( + self, + mask: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + srs: str = None, + replacement=None, + inside: bool = None, + ) -> DataCube: + """ + Applies a polygon mask to a raster data cube. To apply a raster mask use `mask`. + + All pixels for which the point at the pixel center does not intersect with any + polygon (as defined in the Simple Features standard by the OGC) are replaced. + This behaviour can be inverted by setting the parameter `inside` to true. + + The pixel values are replaced with the value specified for `replacement`, + which defaults to `no data`. + + :param mask: The geometry to mask with: a shapely geometry, a GeoJSON-style dictionary, + a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file. + :param srs: The spatial reference system of the provided polygon. + By default longitude-latitude (EPSG:4326) is assumed. + + .. note:: this ``srs`` argument is a non-standard/experimental feature, only supported by specific back-ends. + See https://github.com/Open-EO/openeo-processes/issues/235 for details. + :param replacement: the value to replace the masked pixels with + """ + valid_geojson_types = ["Polygon", "MultiPolygon", "GeometryCollection", "Feature", "FeatureCollection"] + mask = self._get_geometry_argument(mask, valid_geojson_types=valid_geojson_types, crs=srs) + return self.process( + process_id="mask_polygon", + arguments=dict_no_none( + data=THIS, + mask=mask, + replacement=replacement, + inside=inside + ) + ) + + @openeo_process + def merge_cubes( + self, + other: DataCube, + overlap_resolver: Union[str, PGNode, typing.Callable] = None, + context: Optional[dict] = None, + ) -> DataCube: + """ + Merging two data cubes + + The data cubes have to be compatible. A merge operation without overlap should be reversible with (a set of) filter operations for each of the two cubes. The process performs the join on overlapping dimensions, with the same name and type. + An overlapping dimension has the same name, type, reference system and resolution in both dimensions, but can have different labels. One of the dimensions can have different labels, for all other dimensions the labels must be equal. If data overlaps, the parameter overlap_resolver must be specified to resolve the overlap. + + Examples for merging two data cubes: + + #. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first cube and B3 and B4. An overlap resolver is not needed. The merged data cube has the dimensions x, y, t and bands and the dimension bands has four dimension labels: B1, B2, B3, B4. + #. Data cubes with the dimensions x, y, t and bands have the same dimension labels in x,y and t, but the labels for the dimension bands are B1 and B2 for the first data cube and B2 and B3 for the second. An overlap resolver is required to resolve overlap in band B2. The merged data cube has the dimensions x, y, t and bands and the dimension bands has three dimension labels: B1, B2, B3. + #. Data cubes with the dimensions x, y and t have the same dimension labels in x,y and t. There are two options: + * Keep the overlapping values separately in the merged data cube: An overlap resolver is not needed, but for each data cube you need to add a new dimension using add_dimension. The new dimensions must be equal, except that the labels for the new dimensions must differ by name. The merged data cube has the same dimensions and labels as the original data cubes, plus the dimension added with add_dimension, which has the two dimension labels after the merge. + * Combine the overlapping values into a single value: An overlap resolver is required to resolve the overlap for all pixels. The merged data cube has the same dimensions and labels as the original data cubes, but all pixel values have been processed by the overlap resolver. + #. Merging a data cube with dimensions x, y, t with another cube with dimensions x, y will join on the x, y dimension, so the lower dimension cube is merged with each time step in the higher dimensional cube. This can for instance be used to apply a digital elevation model to a spatiotemporal data cube. + + :param other: The data cube to merge with. + :param overlap_resolver: A reduction operator that resolves the conflict if the data overlaps. The reducer must return a value of the same data type as the input values are. The reduction operator may be a single process such as multiply or consist of multiple sub-processes. null (the default) can be specified if no overlap resolver is required. + :param context: Additional data to be passed to the process. + + :return: The merged data cube. + """ + arguments = {"cube1": self, "cube2": other} + if overlap_resolver: + arguments["overlap_resolver"] = build_child_callback(overlap_resolver, parent_parameters=["x", "y"]) + if ( + self.metadata + and self.metadata.has_band_dimension() + and isinstance(other, DataCube) + and other.metadata + and other.metadata.has_band_dimension() + ): + # Minimal client side metadata merging + merged_metadata = self.metadata + for b in other.metadata.band_dimension.bands: + if b not in merged_metadata.bands: + merged_metadata = merged_metadata.append_band(b) + else: + merged_metadata = None + # Overlapping bands without overlap resolver will give an error in the backend + if context: + arguments["context"] = context + return self.process(process_id="merge_cubes", arguments=arguments, metadata=merged_metadata) + + merge = legacy_alias(merge_cubes, name="merge", since="0.4.6") + + @openeo_process + def apply_kernel( + self, kernel: Union[np.ndarray, List[List[float]]], factor=1.0, border=0, + replace_invalid=0 + ) -> DataCube: + """ + Applies a focal operation based on a weighted kernel to each value of the specified dimensions in the data cube. + + The border parameter determines how the data is extended when the kernel overlaps with the borders. + The following options are available: + + * numeric value - fill with a user-defined constant number n: nnnnnn|abcdefgh|nnnnnn (default, with n = 0) + * replicate - repeat the value from the pixel at the border: aaaaaa|abcdefgh|hhhhhh + * reflect - mirror/reflect from the border: fedcba|abcdefgh|hgfedc + * reflect_pixel - mirror/reflect from the center of the pixel at the border: gfedcb|abcdefgh|gfedcb + * wrap - repeat/wrap the image: cdefgh|abcdefgh|abcdef + + + :param kernel: The kernel to be applied on the data cube. The kernel has to be as many dimensions as the data cube has dimensions. + :param factor: A factor that is multiplied to each value computed by the focal operation. This is basically a shortcut for explicitly multiplying each value by a factor afterwards, which is often required for some kernel-based algorithms such as the Gaussian blur. + :param border: Determines how the data is extended when the kernel overlaps with the borders. Defaults to fill the border with zeroes. + :param replace_invalid: This parameter specifies the value to replace non-numerical or infinite numerical values with. By default, those values are replaced with zeroes. + :return: A data cube with the newly computed values. The resolution, cardinality and the number of dimensions are the same as for the original data cube. + """ + return self.process('apply_kernel', { + 'data': THIS, + 'kernel': kernel.tolist() if isinstance(kernel, np.ndarray) else kernel, + 'factor': factor, + 'border': border, + 'replace_invalid': replace_invalid + }) + + @openeo_process + def resolution_merge( + self, high_resolution_bands: List[str], low_resolution_bands: List[str], method: str = None + ) -> DataCube: + """ + Resolution merging algorithms try to improve the spatial resolution of lower resolution bands + (e.g. Sentinel-2 20M) based on higher resolution bands. (e.g. Sentinel-2 10M). + + External references: + + `Pansharpening explained `_ + + `Example publication: 'Improving the Spatial Resolution of Land Surface Phenology by Fusing Medium- and + Coarse-Resolution Inputs' `_ + + .. warning:: experimental process: not generally supported, API subject to change. + + :param high_resolution_bands: A list of band names to use as 'high-resolution' band. Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will remain unmodified. + :param low_resolution_bands: A list of band names for which the spatial resolution should be increased. Either the unique band name (metadata field `name` in bands) or one of the common band names (metadata field `common_name` in bands). If unique band name and common name conflict, the unique band name has higher priority. The order of the specified array defines the order of the bands in the data cube. If multiple bands match a common name, all matched bands are included in the original order. These bands will be modified by the process. + :param method: The method to use. The supported algorithms can vary between back-ends. Set to `null` (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility.. + :return: A datacube with the same bands and metadata as the input, but algorithmically increased spatial resolution for the selected bands. + """ + return self.process('resolution_merge', { + 'data': THIS, + 'high_resolution_bands': high_resolution_bands, + 'low_resolution_bands': low_resolution_bands, + 'method': method, + + }) + + def raster_to_vector(self) -> VectorCube: + """ + Converts this raster data cube into a :py:class:`~openeo.rest.vectorcube.VectorCube`. + The bounding polygon of homogenous areas of pixels is constructed. + + .. warning:: experimental process: not generally supported, API subject to change. + + :return: a :py:class:`~openeo.rest.vectorcube.VectorCube` + """ + pg_node = PGNode(process_id="raster_to_vector", arguments={"data": self}) + return VectorCube(pg_node, connection=self._connection) + + ####VIEW methods ####### + + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'mean'``.", version="0.10.0" + ) + def polygonal_mean_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a mean time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="mean") + + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'histogram'``.", + version="0.10.0", + ) + def polygonal_histogram_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a histogram time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="histogram") + + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'median'``.", version="0.10.0" + ) + def polygonal_median_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a median time series for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="median") + + @deprecated( + "Use :py:meth:`aggregate_spatial` with reducer ``'sd'``.", version="0.10.0" + ) + def polygonal_standarddeviation_timeseries( + self, polygon: Union[Polygon, MultiPolygon, str] + ) -> VectorCube: + """ + Extract a time series of standard deviations for the given (multi)polygon. Its points are + expected to be in the EPSG:4326 coordinate + reference system. + + :param polygon: The (multi)polygon; or a file path or HTTP URL to a GeoJSON file or shape file + """ + return self.aggregate_spatial(geometries=polygon, reducer="sd") + + @openeo_process + def ard_surface_reflectance( + self, atmospheric_correction_method: str, cloud_detection_method: str, elevation_model: str = None, + atmospheric_correction_options: dict = None, cloud_detection_options: dict = None, + ) -> DataCube: + """ + Computes CARD4L compliant surface reflectance values from optical input. + + :param atmospheric_correction_method: The atmospheric correction method to use. + :param cloud_detection_method: The cloud detection method to use. + :param elevation_model: The digital elevation model to use, leave empty to allow the back-end to make a suitable choice. + :param atmospheric_correction_options: Proprietary options for the atmospheric correction method. + :param cloud_detection_options: Proprietary options for the cloud detection method. + :return: Data cube containing bottom of atmosphere reflectances with atmospheric disturbances like clouds and cloud shadows removed. The data returned is CARD4L compliant and contains metadata. + """ + return self.process('ard_surface_reflectance', { + 'data': THIS, + 'atmospheric_correction_method': atmospheric_correction_method, + 'cloud_detection_method': cloud_detection_method, + 'elevation_model': elevation_model, + 'atmospheric_correction_options': atmospheric_correction_options or {}, + 'cloud_detection_options': cloud_detection_options or {}, + }) + + @openeo_process + def atmospheric_correction(self, method: str = None, elevation_model: str = None, options: dict = None) -> DataCube: + """ + Applies an atmospheric correction that converts top of atmosphere reflectance values into bottom of atmosphere/top of canopy reflectance values. + + Note that multiple atmospheric methods exist, but may not be supported by all backends. The method parameter gives + you the option of requiring a specific method, but this may result in an error if the backend does not support it. + + :param method: The atmospheric correction method to use. To get reproducible results, you have to set a specific method. Set to `null` to allow the back-end to choose, which will improve portability, but reduce reproducibility as you *may* get different results if you run the processes multiple times. + :param elevation_model: The digital elevation model to use, leave empty to allow the back-end to make a suitable choice. + :param options: Proprietary options for the atmospheric correction method. + :return: datacube with bottom of atmosphere reflectances + """ + return self.process('atmospheric_correction', { + 'data': THIS, + 'method': method, + 'elevation_model': elevation_model, + 'options': options or {}, + }) + + @openeo_process + def save_result( + self, + format: str = _DEFAULT_RASTER_FORMAT, + options: Optional[dict] = None, + ) -> DataCube: + if self._connection: + formats = set(self._connection.list_output_formats().keys()) + # TODO: map format to correct casing too? + if format.lower() not in {f.lower() for f in formats}: + raise ValueError("Invalid format {f!r}. Should be one of {s}".format(f=format, s=formats)) + return self.process( + process_id="save_result", + arguments={ + "data": THIS, + "format": format, + # TODO: leave out options if unset? + "options": options or {} + } + ) + + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + ) -> Union[None, bytes]: + """ + Execute synchronously and download the raster data cube, e.g. as GeoTIFF. + + If outputfile is provided, the result is stored on disk locally, otherwise, a bytes object is returned. + The bytes object can be passed on to a suitable decoder for decoding. + + :param outputfile: Optional, an output file if the result needs to be stored on disk. + :param format: Optional, an output format supported by the backend. + :param options: Optional, file format options + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: None if the result is stored to disk, or a bytes object returned by the backend. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=format, + options=options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.download()", + ) + return self._connection.download(cube.flat_graph(), outputfile, validate=validate) + + def validate(self) -> List[dict]: + """ + Validate a process graph without executing it. + + :return: list of errors (dictionaries with "code" and "message" fields) + """ + return self._connection.validate_process_graph(self.flat_graph()) + + def tiled_viewing_service(self, type: str, **kwargs) -> Service: + return self._connection.create_service(self.flat_graph(), type=type, **kwargs) + + def _get_spatial_extent_from_load_collection(self): + pg = self.flat_graph() + for node in pg: + if pg[node]["process_id"] == "load_collection": + if "spatial_extent" in pg[node]["arguments"] and all( + cd in pg[node]["arguments"]["spatial_extent"] for cd in ["east", "west", "south", "north"] + ): + return pg[node]["arguments"]["spatial_extent"] + return None + + def preview( + self, + center: Union[Iterable, None] = None, + zoom: Union[int, None] = None, + ): + """ + Creates a service with the process graph and displays a map widget. Only supports XYZ. + + :param center: (optional) Map center. Default is (0,0). + :param zoom: (optional) Zoom level of the map. Default is 1. + + :return: ipyleaflet Map object and the displayed Service + + .. warning:: experimental feature, subject to change. + .. versionadded:: 0.19.0 + """ + if "XYZ" not in self.connection.list_service_types(): + raise OpenEoClientException("Backend does not support service type 'XYZ'.") + + if not in_jupyter_context(): + raise Exception("On-demand preview only supported in Jupyter notebooks!") + try: + import ipyleaflet + except ImportError: + raise Exception( + "Additional modules must be installed for on-demand preview. Run `pip install openeo[jupyter]` or refer to the documentation." + ) + + service = self.tiled_viewing_service("XYZ") + service_metadata = service.describe_service() + + m = ipyleaflet.Map( + center=center or (0, 0), + zoom=zoom or 1, + scroll_wheel_zoom=True, + basemap=ipyleaflet.basemaps.OpenStreetMap.Mapnik, + ) + service_layer = ipyleaflet.TileLayer(url=service_metadata["url"]) + m.add(service_layer) + + if center is None and zoom is None: + spatial_extent = self._get_spatial_extent_from_load_collection() + if spatial_extent is not None: + m.fit_bounds( + [ + [spatial_extent["south"], spatial_extent["west"]], + [spatial_extent["north"], spatial_extent["east"]], + ] + ) + + class Preview: + """ + On-demand preview instance holding the associated XYZ service and ipyleaflet Map + """ + + def __init__(self, service: Service, ipyleaflet_map: ipyleaflet.Map): + self.service = service + self.map = ipyleaflet_map + + def _repr_html_(self): + from IPython.display import display + + display(self.map) + + def delete_service(self): + self.service.delete_service() + + return Preview(service, m) + + def execute_batch( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print: typing.Callable[[str], None] = print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: deprecate `format_options` as keyword arguments + **format_options, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long-running jobs, you probably do not want to keep the client running. + + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) File format to use for the job result. + :param job_options: + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: start showing deprecation warnings about these inconsistent argument names + if "format" in format_options and not out_format: + out_format = format_options["format"] # align with 'download' call arg name + + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.execute_batch()", + ) + + job = cube.create_job( + title=title, + description=description, + plan=plan, + budget=budget, + job_options=job_options, + validate=validate, + auto_add_save_result=False, + ) + return job.run_synchronous( + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + ) + + def create_job( + self, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: avoid `format_options` as keyword arguments + **format_options, + ) -> BatchJob: + """ + Sends the datacube's process graph as a batch job to the back-end + and return a :py:class:`~openeo.rest.job.BatchJob` instance. + + Note that the batch job will just be created at the back-end, + it still needs to be started and tracked explicitly. + Use :py:meth:`execute_batch` instead to have the openEO Python client take care of that job management. + + :param out_format: output file format. + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: custom job options. + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: Created job. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: add option to also automatically start the job? + # TODO: avoid using all kwargs as format_options + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options or None, + default_format=self._DEFAULT_RASTER_FORMAT, + method="DataCube.create_job()", + ) + return self._connection.create_job( + process_graph=cube.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + validate=validate, + additional=job_options, + ) + + send_job = legacy_alias(create_job, name="send_job", since="0.10.0") + + def save_user_defined_process( + self, + user_defined_process_id: str, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ) -> RESTUserDefinedProcess: + """ + Saves this process graph in the backend as a user-defined process for the authenticated user. + + :param user_defined_process_id: unique identifier for the process + :param public: visible to other users? + :param summary: A short summary of what the process does. + :param description: Detailed description to explain the entity. CommonMark 0.29 syntax MAY be used for rich text representation. + :param returns: Description and schema of the return value. + :param categories: A list of categories. + :param examples: A list of examples. + :param links: A list of links. + :return: a RESTUserDefinedProcess instance + """ + return self._connection.save_user_defined_process( + user_defined_process_id=user_defined_process_id, + process_graph=self.flat_graph(), public=public, summary=summary, description=description, + returns=returns, categories=categories, examples=examples, links=links, + ) + + 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") + def execute_local_udf(udf: str, datacube: Union[str, 'xarray.DataArray', 'XarrayDataCube'] = None, fmt='netcdf'): + import openeo.udf.run_code + return openeo.udf.run_code.execute_local_udf(udf=udf, datacube=datacube, fmt=fmt) + + @openeo_process + def ard_normalized_radar_backscatter( + self, elevation_model: str = None, contributing_area=False, + ellipsoid_incidence_angle: bool = False, noise_removal: bool = True + ) -> DataCube: + """ + Computes CARD4L compliant backscatter (gamma0) from SAR input. + This method is a variant of :py:meth:`~openeo.rest.datacube.DataCube.sar_backscatter`, + with restricted parameters to generate backscatter according to CARD4L specifications. + + Note that backscatter computation may require instrument specific metadata that is tightly coupled to the original SAR products. + As a result, this process may only work in combination with loading data from specific collections, not with general data cubes. + + :param elevation_model: The digital elevation model to use. Set to None (the default) to allow the back-end to choose, which will improve portability, but reduce reproducibility. + :param contributing_area: If set to `true`, a DEM-based local contributing area band named `contributing_area` + is added. The values are given in square meters. + :param ellipsoid_incidence_angle: If set to `True`, an ellipsoidal incidence angle band named `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `True`, which removes noise. + + :return: Backscatter values expressed as gamma0. The data returned is CARD4L compliant and contains metadata. By default, the backscatter values are given in linear scale. + """ + return self.process(process_id="ard_normalized_radar_backscatter", arguments={ + "data": THIS, + "elevation_model": elevation_model, + "contributing_area": contributing_area, + "ellipsoid_incidence_angle": ellipsoid_incidence_angle, + "noise_removal": noise_removal + }) + + @openeo_process + def sar_backscatter( + self, + coefficient: Union[str, None] = "gamma0-terrain", + elevation_model: Union[str, None] = None, + mask: bool = False, + contributing_area: bool = False, + local_incidence_angle: bool = False, + ellipsoid_incidence_angle: bool = False, + noise_removal: bool = True, + options: Optional[dict] = None + ) -> DataCube: + """ + Computes backscatter from SAR input. + + Note that backscatter computation may require instrument specific metadata that is tightly coupled to the + original SAR products. As a result, this process may only work in combination with loading data from + specific collections, not with general data cubes. + + :param coefficient: Select the radiometric correction coefficient. + The following options are available: + + - `"beta0"`: radar brightness + - `"sigma0-ellipsoid"`: ground area computed with ellipsoid earth model + - `"sigma0-terrain"`: ground area computed with terrain earth model + - `"gamma0-ellipsoid"`: ground area computed with ellipsoid earth model in sensor line of sight + - `"gamma0-terrain"`: ground area computed with terrain earth model in sensor line of sight (default) + - `None`: non-normalized backscatter + :param elevation_model: The digital elevation model to use. Set to `None` (the default) to allow + the back-end to choose, which will improve portability, but reduce reproducibility. + :param mask: If set to `true`, a data mask is added to the bands with the name `mask`. + It indicates which values are valid (1), invalid (0) or contain no-data (null). + :param contributing_area: If set to `true`, a DEM-based local contributing area band named `contributing_area` + is added. The values are given in square meters. + :param local_incidence_angle: If set to `true`, a DEM-based local incidence angle band named + `local_incidence_angle` is added. The values are given in degrees. + :param ellipsoid_incidence_angle: If set to `true`, an ellipsoidal incidence angle band named + `ellipsoid_incidence_angle` is added. The values are given in degrees. + :param noise_removal: If set to `false`, no noise removal is applied. Defaults to `true`, which removes noise. + :param options: dictionary with additional (backend-specific) options. + :return: + + .. versionadded:: 0.4.9 + .. versionchanged:: 0.4.10 replace `orthorectify` and `rtc` arguments with `coefficient`. + """ + coefficient_options = [ + "beta0", "sigma0-ellipsoid", "sigma0-terrain", "gamma0-ellipsoid", "gamma0-terrain", None + ] + if coefficient not in coefficient_options: + raise OpenEoClientException("Invalid `sar_backscatter` coefficient {c!r}. Should be one of {o}".format( + c=coefficient, o=coefficient_options + )) + arguments = { + "data": THIS, + "coefficient": coefficient, + "elevation_model": elevation_model, + "mask": mask, + "contributing_area": contributing_area, + "local_incidence_angle": local_incidence_angle, + "ellipsoid_incidence_angle": ellipsoid_incidence_angle, + "noise_removal": noise_removal, + } + if options: + arguments["options"] = options + return self.process(process_id="sar_backscatter", arguments=arguments) + + @openeo_process + def fit_curve(self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str): + """ + Use non-linear least squares to fit a model function `y = f(x, parameters)` to data. + + The process throws an `InvalidValues` exception if invalid values are encountered. + Invalid values are finite numbers (see also ``is_valid()``). + + .. warning:: experimental process: not generally supported, API subject to change. + https://github.com/Open-EO/openeo-processes/pull/240 + + :param parameters: + :param function: "child callback" function, see :ref:`callbackfunctions` + :param dimension: + """ + # TODO: does this return a `DataCube`? Shouldn't it just return an array (wrapper)? + return self.process( + process_id="fit_curve", + arguments={ + "data": THIS, + "parameters": parameters, + "function": build_child_callback(function, parent_parameters=["x", "parameters"]), + "dimension": dimension, + }, + ) + + @openeo_process + def predict_curve( + self, parameters: list, function: Union[str, PGNode, typing.Callable], dimension: str, + labels=None + ): + """ + Predict values using a model function and pre-computed parameters. + + .. warning:: experimental process: not generally supported, API subject to change. + https://github.com/Open-EO/openeo-processes/pull/240 + + :param parameters: + :param function: "child callback" function, see :ref:`callbackfunctions` + :param dimension: + """ + return self.process( + process_id="predict_curve", + arguments={ + "data": THIS, + "parameters": parameters, + "function": build_child_callback(function, parent_parameters=["x", "parameters"]), + "dimension": dimension, + "labels": labels, + }, + ) + + @openeo_process(mode="reduce_dimension") + def predict_random_forest(self, model: Union[str, BatchJob, MlModel], dimension: str = "bands"): + """ + Apply ``reduce_dimension`` process with a ``predict_random_forest`` reducer. + + :param model: a reference to a trained model, one of + + - a :py:class:`~openeo.rest.mlmodel.MlModel` instance (e.g. loaded from :py:meth:`Connection.load_ml_model`) + - a :py:class:`~openeo.rest.job.BatchJob` instance of a batch job that saved a single random forest model + - a job id (``str``) of a batch job that saved a single random forest model + - a STAC item URL (``str``) to load the random forest from. + (The STAC Item must implement the `ml-model` extension.) + :param dimension: dimension along which to apply the ``reduce_dimension`` process. + + .. versionadded:: 0.10.0 + """ + if not isinstance(model, MlModel): + model = MlModel.load_ml_model(connection=self.connection, id=model) + reducer = PGNode( + process_id="predict_random_forest", data={"from_parameter": "data"}, model={"from_parameter": "context"} + ) + return self.reduce_dimension(dimension=dimension, reducer=reducer, context=model) + + @openeo_process + def dimension_labels(self, dimension: str) -> DataCube: + """ + Gives all labels for a dimension in the data cube. The labels have the same order as in the data cube. + + :param dimension: The name of the dimension to get the labels for. + """ + if self._do_metadata_normalization(): + dimension_names = self.metadata.dimension_names() + if dimension_names and dimension not in dimension_names: + raise ValueError(f"Invalid dimension name {dimension!r}, should be one of {dimension_names}") + return self.process(process_id="dimension_labels", arguments={"data": THIS, "dimension": dimension}) + + @openeo_process + def flatten_dimensions(self, dimensions: List[str], target_dimension: str, label_separator: Optional[str] = None): + """ + Combines multiple given dimensions into a single dimension by flattening the values + and merging the dimension labels with the given `label_separator`. Non-string dimension labels will + be converted to strings. This process is the opposite of the process :py:meth:`unflatten_dimension()` + but executing both processes subsequently doesn't necessarily create a data cube that + is equal to the original data cube. + + :param dimensions: The names of the dimension to combine. + :param target_dimension: The name of a target dimension with a single dimension label to replace. + :param label_separator: The string that will be used as a separator for the concatenated dimension labels. + :return: A data cube with the new shape. + + .. warning:: experimental process: not generally supported, API subject to change. + .. versionadded:: 0.10.0 + """ + return self.process( + process_id="flatten_dimensions", + arguments=dict_no_none( + data=THIS, + dimensions=dimensions, + target_dimension=target_dimension, + label_separator=label_separator, + ), + ) + + @openeo_process + def unflatten_dimension(self, dimension: str, target_dimensions: List[str], label_separator: Optional[str] = None): + """ + Splits a single dimension into multiple dimensions by systematically extracting values and splitting + the dimension labels by the given `label_separator`. + This process is the opposite of the process :py:meth:`flatten_dimensions()` but executing both processes + subsequently doesn't necessarily create a data cube that is equal to the original data cube. + + :param dimension: The name of the dimension to split. + :param target_dimensions: The names of the target dimensions. + :param label_separator: The string that will be used as a separator to split the dimension labels. + :return: A data cube with the new shape. + + .. warning:: experimental process: not generally supported, API subject to change. + .. versionadded:: 0.10.0 + """ + return self.process( + process_id="unflatten_dimension", + arguments=dict_no_none( + data=THIS, + dimension=dimension, + target_dimensions=target_dimensions, + label_separator=label_separator, + ), + ) diff --git a/lib/openeo/rest/graph_building.py b/lib/openeo/rest/graph_building.py new file mode 100644 index 000000000..d05eae930 --- /dev/null +++ b/lib/openeo/rest/graph_building.py @@ -0,0 +1,78 @@ +""" +Public openEO process graph building utilities +''''''''''''''''''''''''''''''''''''''''''''''' + +""" +from __future__ import annotations + +from typing import Optional + +from openeo.internal.graph_building import PGNode, _FromNodeMixin +from openeo.processes import ProcessBuilder + + +class CollectionProperty(_FromNodeMixin): + """ + Helper object to easily create simple collection metadata property filters + to be used with :py:meth:`Connection.load_collection() `. + + .. note:: This class should not be used directly by end user code. + Use the :py:func:`~openeo.rest.graph_building.collection_property` factory instead. + + .. warning:: this is an experimental feature, naming might change. + """ + + def __init__(self, name: str, _builder: Optional[ProcessBuilder] = None): + self.name = name + self._builder = _builder or ProcessBuilder(pgnode={"from_parameter": "value"}) + + def from_node(self) -> PGNode: + return self._builder.from_node() + + def __eq__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder == other) + + def __ne__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder != other) + + def __gt__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder > other) + + def __ge__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder >= other) + + def __lt__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder < other) + + def __le__(self, other) -> CollectionProperty: + return CollectionProperty(self.name, _builder=self._builder <= other) + + +def collection_property(name: str) -> CollectionProperty: + """ + Helper to easily create simple collection metadata property filters + to be used with :py:meth:`Connection.load_collection() `. + + Usage example: + + .. code-block:: python + + from openeo import collection_property + ... + + connection.load_collection( + ... + properties=[ + collection_property("eo:cloud_cover") <= 75, + collection_property("platform") == "Sentinel-2B", + ] + ) + + .. warning:: this is an experimental feature, naming might change. + + .. versionadded:: 0.26.0 + + :param name: name of the collection property to filter on + :return: an object that supports operators like ``<=``, ``==`` to easily build simple property filters. + """ + return CollectionProperty(name=name) diff --git a/lib/openeo/rest/job.py b/lib/openeo/rest/job.py new file mode 100644 index 000000000..e3f307a71 --- /dev/null +++ b/lib/openeo/rest/job.py @@ -0,0 +1,546 @@ +from __future__ import annotations + +import datetime +import json +import logging +import time +import typing +from pathlib import Path +from typing import Dict, List, Optional, Union + +import requests + +from openeo.api.logs import LogEntry, log_level_name, normalize_log_level +from openeo.internal.documentation import openeo_endpoint +from openeo.internal.jupyter import ( + VisualDict, + VisualList, + render_component, + render_error, +) +from openeo.internal.warnings import deprecated, legacy_alias +from openeo.rest import ( + DEFAULT_DOWNLOAD_CHUNK_SIZE, + JobFailedException, + OpenEoApiError, + OpenEoApiPlainError, + OpenEoClientException, +) +from openeo.util import ensure_dir + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo.rest.connection import Connection + +logger = logging.getLogger(__name__) + + +DEFAULT_JOB_RESULTS_FILENAME = "job-results.json" + + +class BatchJob: + """ + Handle for an openEO batch job, allowing it to describe, start, cancel, inspect results, etc. + + .. versionadded:: 0.11.0 + This class originally had the more cryptic name :py:class:`RESTJob`, + which is still available as legacy alias, + but :py:class:`BatchJob` is recommended since version 0.11.0. + + """ + + # TODO #425 method to bootstrap `load_stac` directly from a BatchJob object + + def __init__(self, job_id: str, connection: Connection): + self.job_id = job_id + """Unique identifier of the batch job (string).""" + + self.connection = connection + + def __repr__(self): + return '<{c} job_id={i!r}>'.format(c=self.__class__.__name__, i=self.job_id) + + def _repr_html_(self): + data = self.describe() + currency = self.connection.capabilities().currency() + return render_component('job', data=data, parameters={'currency': currency}) + + @openeo_endpoint("GET /jobs/{job_id}") + def describe(self) -> dict: + """ + Get detailed metadata about a submitted batch job + (title, process graph, status, progress, ...). + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`describe_job`. + """ + return self.connection.get(f"/jobs/{self.job_id}", expected_status=200).json() + + describe_job = legacy_alias(describe, name="describe_job", since="0.20.0", mode="soft") + + def status(self) -> str: + """ + Get the status of the batch job + + :return: batch job status, one of "created", "queued", "running", "canceled", "finished" or "error". + """ + return self.describe().get("status", "N/A") + + @openeo_endpoint("DELETE /jobs/{job_id}") + def delete(self): + """ + Delete this batch job. + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`delete_job`. + """ + self.connection.delete(f"/jobs/{self.job_id}", expected_status=204) + + delete_job = legacy_alias(delete, name="delete_job", since="0.20.0", mode="soft") + + @openeo_endpoint("GET /jobs/{job_id}/estimate") + def estimate(self): + """Calculate time/cost estimate for a job.""" + data = self.connection.get( + f"/jobs/{self.job_id}/estimate", expected_status=200 + ).json() + currency = self.connection.capabilities().currency() + return VisualDict('job-estimate', data=data, parameters={'currency': currency}) + + estimate_job = legacy_alias(estimate, name="estimate_job", since="0.20.0", mode="soft") + + @openeo_endpoint("POST /jobs/{job_id}/results") + def start(self) -> BatchJob: + """ + Start this batch job. + + :return: Started batch job + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`start_job`. + """ + self.connection.post(f"/jobs/{self.job_id}/results", expected_status=202) + return self + + start_job = legacy_alias(start, name="start_job", since="0.20.0", mode="soft") + + @openeo_endpoint("DELETE /jobs/{job_id}/results") + def stop(self): + """ + Stop this batch job. + + .. versionadded:: 0.20.0 + This method was previously called :py:meth:`stop_job`. + """ + self.connection.delete(f"/jobs/{self.job_id}/results", expected_status=204) + + stop_job = legacy_alias(stop, name="stop_job", since="0.20.0", mode="soft") + + def get_results_metadata_url(self, *, full: bool = False) -> str: + """Get results metadata URL""" + url = f"/jobs/{self.job_id}/results" + if full: + url = self.connection.build_url(url) + return url + + @deprecated("Use :py:meth:`~BatchJob.get_results` instead.", version="0.4.10") + def list_results(self) -> dict: + """Get batch job results metadata.""" + return self.get_results().get_metadata() + + def download_result(self, target: Union[str, Path] = None) -> Path: + """ + Download single job result to the target file path or into folder (current working dir by default). + + Fails if there are multiple result files. + + :param target: String or path where the file should be downloaded to. + """ + return self.get_results().download_file(target=target) + + @deprecated( + "Instead use :py:meth:`BatchJob.get_results` and the more flexible download functionality of :py:class:`JobResults`", + version="0.4.10") + def download_results(self, target: Union[str, Path] = None) -> Dict[Path, dict]: + """ + Download all job result files into given folder (current working dir by default). + + The names of the files are taken directly from the backend. + + :param target: String/path, folder where to put the result files. + :return: file_list: Dict containing the downloaded file path as value and asset metadata + """ + return self.get_result().download_files(target) + + @deprecated("Use :py:meth:`BatchJob.get_results` instead.", version="0.4.10") + def get_result(self): + return _Result(self) + + def get_results(self) -> JobResults: + """ + Get handle to batch job results for result metadata inspection or downloading resulting assets. + + .. versionadded:: 0.4.10 + """ + return JobResults(job=self) + + def logs( + self, offset: Optional[str] = None, level: Optional[Union[str, int]] = None + ) -> List[LogEntry]: + """Retrieve job logs. + + :param offset: The last identifier (property ``id`` of a LogEntry) the client has received. + + If provided, the back-ends only sends the entries that occurred after the specified identifier. + If not provided or empty, start with the first entry. + + Defaults to None. + + :param level: Minimum log level to retrieve. + + You can use either constants from Python's standard module ``logging`` + or their names (case-insensitive). + + For example: + ``logging.INFO``, ``"info"`` or ``"INFO"`` can all be used to show the messages + for level ``logging.INFO`` and above, i.e. also ``logging.WARNING`` and + ``logging.ERROR`` will be included. + + Default is to show all log levels, in other words ``logging.DEBUG``. + This is also the result when you explicitly pass log_level=None or log_level="". + + :return: A list containing the log entries for the batch job. + """ + url = f"/jobs/{self.job_id}/logs" + params = {} + if offset is not None: + params["offset"] = offset + if level is not None: + params["level"] = log_level_name(level) + response = self.connection.get(url, params=params, expected_status=200) + logs = response.json()["logs"] + + # Only filter logs when specified. + # We should still support client-side log_level filtering because not all backends + # support the minimum log level parameter. + if level is not None: + log_level = normalize_log_level(level) + logs = ( + log + for log in logs + if normalize_log_level(log.get("level")) >= log_level + ) + + entries = [LogEntry(log) for log in logs] + return VisualList("logs", data=entries) + + def run_synchronous( + self, outputfile: Union[str, Path, None] = None, + print=print, max_poll_interval=60, connection_retry_interval=30 + ) -> BatchJob: + """Start the job, wait for it to finish and download result""" + self.start_and_wait( + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + ) + # TODO #135 support multi file result sets too? + if outputfile is not None: + self.download_result(outputfile) + return self + + def start_and_wait( + self, print=print, max_poll_interval: int = 60, connection_retry_interval: int = 30, soft_error_max=10 + ) -> BatchJob: + """ + Start the batch job, poll its status and wait till it finishes (or fails) + + :param print: print/logging function to show progress/status + :param max_poll_interval: maximum number of seconds to sleep between status polls + :param connection_retry_interval: how long to wait when status poll failed due to connection issue + :param soft_error_max: maximum number of soft errors (e.g. temporary connection glitches) to allow + :return: + """ + # TODO rename `connection_retry_interval` to something more generic? + start_time = time.time() + + def elapsed() -> str: + return str(datetime.timedelta(seconds=time.time() - start_time)).rsplit(".")[0] + + def print_status(msg: str): + print("{t} Job {i!r}: {m}".format(t=elapsed(), i=self.job_id, m=msg)) + + # TODO: make `max_poll_interval`, `connection_retry_interval` class constants or instance properties? + print_status("send 'start'") + self.start() + + # TODO: also add `wait` method so you can track a job that already has started explicitly + # or just rename this method to `wait` and automatically do start if not started yet? + + # Start with fast polling. + poll_interval = min(5, max_poll_interval) + status = None + _soft_error_count = 0 + + def soft_error(message: str): + """Non breaking error (unless we had too much of them)""" + nonlocal _soft_error_count + _soft_error_count += 1 + if _soft_error_count > soft_error_max: + raise OpenEoClientException("Excessive soft errors") + print_status(message) + time.sleep(connection_retry_interval) + + while True: + # TODO: also allow a hard time limit on this infinite poll loop? + try: + job_info = self.describe() + except requests.ConnectionError as e: + soft_error("Connection error while polling job status: {e}".format(e=e)) + continue + except OpenEoApiPlainError as e: + if e.http_status_code in [502, 503]: + soft_error("Service availability error while polling job status: {e}".format(e=e)) + continue + else: + raise + + status = job_info.get("status", "N/A") + progress = '{p}%'.format(p=job_info["progress"]) if "progress" in job_info else "N/A" + print_status("{s} (progress {p})".format(s=status, p=progress)) + if status not in ('submitted', 'created', 'queued', 'running'): + break + + # Sleep for next poll (and adaptively make polling less frequent) + time.sleep(poll_interval) + poll_interval = min(1.25 * poll_interval, max_poll_interval) + + if status != "finished": + # TODO: allow to disable this printing logs (e.g. in non-interactive contexts)? + # TODO: render logs jupyter-aware in a notebook context? + print(f"Your batch job {self.job_id!r} failed. Error logs:") + print(self.logs(level=logging.ERROR)) + print( + f"Full logs can be inspected in an openEO (web) editor or with `connection.job({self.job_id!r}).logs()`." + ) + raise JobFailedException( + f"Batch job {self.job_id!r} didn't finish successfully. Status: {status} (after {elapsed()}).", + job=self, + ) + + return self + + +@deprecated(reason="Use :py:class:`BatchJob` instead", version="0.11.0") +class RESTJob(BatchJob): + """ + Legacy alias for :py:class:`BatchJob`. + """ + + +class ResultAsset: + """ + Result asset of a batch job (e.g. a GeoTIFF or JSON file) + + .. versionadded:: 0.4.10 + """ + + def __init__(self, job: BatchJob, name: str, href: str, metadata: dict): + self.job = job + + self.name = name + """Asset name as advertised by the backend.""" + + self.href = href + """Download URL of the asset.""" + + self.metadata = metadata + """Asset metadata provided by the backend, possibly containing keys "type" (for media type), "roles", "title", "description".""" + + def __repr__(self): + return "".format( + n=self.name, t=self.metadata.get("type", "unknown"), h=self.href + ) + + def download( + self, target: Optional[Union[Path, str]] = None, *, chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE + ) -> Path: + """ + Download asset to given location + + :param target: download target path. Can be an existing folder + (in which case the filename advertised by backend will be used) + or full file name. By default, the working directory will be used. + :param chunk_size: chunk size for streaming response. + """ + target = Path(target or Path.cwd()) + if target.is_dir(): + target = target / self.name + ensure_dir(target.parent) + logger.info("Downloading Job result asset {n!r} from {h!s} to {t!s}".format(n=self.name, h=self.href, t=target)) + response = self._get_response(stream=True) + with target.open("wb") as f: + for block in response.iter_content(chunk_size=chunk_size): + f.write(block) + return target + + def _get_response(self, stream=True) -> requests.Response: + return self.job.connection.get(self.href, stream=stream) + + def load_json(self) -> dict: + """Load asset in memory and parse as JSON.""" + if not (self.name.lower().endswith(".json") or self.metadata.get("type") == "application/json"): + logger.warning("Asset might not be JSON") + return self._get_response().json() + + def load_bytes(self) -> bytes: + """Load asset in memory as raw bytes.""" + return self._get_response().content + + # TODO: more `load` methods e.g.: load GTiff asset directly as numpy array + + +class MultipleAssetException(OpenEoClientException): + pass + + +class JobResults: + """ + Results of a batch job: listing of one or more output files (assets) + and some metadata. + + .. versionadded:: 0.4.10 + """ + + def __init__(self, job: BatchJob): + self._job = job + self._results = None + + def __repr__(self): + return "".format(j=self._job.job_id) + + def get_job_id(self) -> str: + return self._job.job_id + + def _repr_html_(self): + try: + response = self.get_metadata() + return render_component("batch-job-result", data = response) + except OpenEoApiError as error: + return render_error(error) + + def get_metadata(self, force=False) -> dict: + """Get batch job results metadata (parsed JSON)""" + if self._results is None or force: + self._results = self._job.connection.get( + self._job.get_results_metadata_url(), expected_status=200 + ).json() + return self._results + + # TODO: provide methods for `stac_version`, `id`, `geometry`, `properties`, `links`, ...? + + def get_assets(self) -> List[ResultAsset]: + """ + Get all assets from the job results. + """ + # TODO: add arguments to filter on metadata, e.g. to only get assets of type "image/tiff" + metadata = self.get_metadata() + # API 1.0 style: dictionary mapping filenames to metadata dict (with at least a "href" field) + assets = metadata.get("assets", {}) + if not assets: + logger.warning("No assets found in job result metadata.") + return [ + ResultAsset(job=self._job, name=name, href=asset["href"], metadata=asset) + for name, asset in assets.items() + ] + + def get_asset(self, name: str = None) -> ResultAsset: + """ + Get single asset by name or without name if there is only one. + """ + # TODO: also support getting a single asset by type or role? + assets = self.get_assets() + if len(assets) == 0: + raise OpenEoClientException("No assets in result.") + if name is None: + if len(assets) == 1: + return assets[0] + else: + raise MultipleAssetException("Multiple result assets for job {j}: {a}".format( + j=self._job.job_id, a=[a.name for a in assets] + )) + else: + try: + return next(a for a in assets if a.name == name) + except StopIteration: + raise OpenEoClientException( + "No asset {n!r} in: {a}".format(n=name, a=[a.name for a in assets]) + ) + + def download_file(self, target: Union[Path, str] = None, name: str = None) -> Path: + """ + Download single asset. Can be used when there is only one asset in the + :py:class:`JobResults`, or when the desired asset name is given explicitly. + + :param target: path to download to. Can be an existing directory + (in which case the filename advertised by backend will be used) + or full file name. By default, the working directory will be used. + :param name: asset name to download (not required when there is only one asset) + :return: path of downloaded asset + """ + try: + return self.get_asset(name=name).download(target=target) + except MultipleAssetException: + raise OpenEoClientException( + "Can not use `download_file` with multiple assets. Use `download_files` instead.") + + def download_files(self, target: Union[Path, str] = None, include_stac_metadata: bool = True) -> List[Path]: + """ + Download all assets to given folder. + + :param target: path to folder to download to (must be a folder if it already exists) + :param include_stac_metadata: whether to download the job result metadata as a STAC (JSON) file. + :return: list of paths to the downloaded assets. + """ + target = Path(target or Path.cwd()) + if target.exists() and not target.is_dir(): + raise OpenEoClientException(f"Target argument {target} exists but isn't a folder.") + ensure_dir(target) + + downloaded = [a.download(target) for a in self.get_assets()] + + if include_stac_metadata: + # TODO #184: convention for metadata file name? + metadata_file = target / DEFAULT_JOB_RESULTS_FILENAME + # TODO #184: rewrite references to locally downloaded assets? + metadata_file.write_text(json.dumps(self.get_metadata())) + downloaded.append(metadata_file) + + return downloaded + + +@deprecated(reason="Use :py:class:`JobResults` instead", version="0.4.10") +class _Result: + """ + Wrapper around `JobResults` to adapt old deprecated "Result" API. + + .. deprecated:: 0.4.10 + """ + + # TODO: deprecated: remove this + + def __init__(self, job): + self.results = JobResults(job=job) + + def download_file(self, target: Union[str, Path] = None) -> Path: + return self.results.download_file(target=target) + + def download_files(self, target: Union[str, Path] = None) -> Dict[Path, dict]: + target = Path(target or Path.cwd()) + if target.exists() and not target.is_dir(): + raise OpenEoClientException(f"Target argument {target} exists but isn't a folder.") + return {a.download(target): a.metadata for a in self.results.get_assets()} + + def load_json(self) -> dict: + return self.results.get_asset().load_json() + + def load_bytes(self) -> bytes: + return self.results.get_asset().load_bytes() diff --git a/lib/openeo/rest/mlmodel.py b/lib/openeo/rest/mlmodel.py new file mode 100644 index 000000000..b6214569c --- /dev/null +++ b/lib/openeo/rest/mlmodel.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import logging +import pathlib +import typing +from typing import Optional, Union + +from openeo.internal.documentation import openeo_process +from openeo.internal.graph_building import PGNode +from openeo.rest._datacube import _ProcessGraphAbstraction +from openeo.rest.job import BatchJob + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo import Connection + +_log = logging.getLogger(__name__) + + +class MlModel(_ProcessGraphAbstraction): + """ + A machine learning model. + + It is the result of a training procedure, e.g. output of a ``fit_...`` process, + and can be used for prediction (classification or regression) with the corresponding ``predict_...`` process. + + .. versionadded:: 0.10.0 + """ + + def __init__(self, graph: PGNode, connection: Union[Connection, None]): + super().__init__(pgnode=graph, connection=connection) + + def save_ml_model(self, options: Optional[dict] = None): + """ + Saves a machine learning model as part of a batch job. + + :param options: Additional parameters to create the file(s). + """ + pgnode = PGNode( + process_id="save_ml_model", + arguments={"data": self, "options": options or {}} + ) + return MlModel(graph=pgnode, connection=self._connection) + + @staticmethod + @openeo_process + def load_ml_model(connection: Connection, id: Union[str, BatchJob]) -> MlModel: + """ + Loads a machine learning model from a STAC Item. + + :param connection: connection object + :param id: STAC item reference, as URL, batch job (id) or user-uploaded file + :return: + + .. versionadded:: 0.10.0 + """ + if isinstance(id, BatchJob): + id = id.job_id + return MlModel(graph=PGNode(process_id="load_ml_model", id=id), connection=connection) + + def execute_batch( + self, + outputfile: Union[str, pathlib.Path], + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print=print, + max_poll_interval=60, + connection_retry_interval=30, + job_options=None, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long running jobs, you probably do not want to keep the client running. + + :param job_options: + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) Format of the job result. + :param format_options: String Parameters for the job result format + """ + job = self.create_job(title=title, description=description, plan=plan, budget=budget, job_options=job_options) + return job.run_synchronous( + # TODO #135 support multi file result sets too + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + ) + + def create_job( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + ) -> BatchJob: + """ + Sends a job to the backend and returns a ClientJob instance. + + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: A dictionary containing (custom) job options + :param format_options: String Parameters for the job result format + :return: Created job. + """ + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + pg = self + if pg.result_node().process_id not in {"save_ml_model"}: + _log.warning("Process graph has no final `save_ml_model`. Adding it automatically.") + pg = pg.save_ml_model() + return self._connection.create_job( + process_graph=pg.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + additional=job_options, + ) diff --git a/lib/openeo/rest/multiresult.py b/lib/openeo/rest/multiresult.py new file mode 100644 index 000000000..20733b68d --- /dev/null +++ b/lib/openeo/rest/multiresult.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from typing import Dict, List, Optional + +from openeo import BatchJob +from openeo.internal.graph_building import FlatGraphableMixin, MultiLeafGraph +from openeo.rest import OpenEoClientException +from openeo.rest.connection import Connection, extract_connections + + +class MultiResult(FlatGraphableMixin): + """ + Helper to create and run batch jobs with process graphs + that contain multiple result nodes + or, more generally speaking, multiple process graph "leaf" nodes. + + Provide multiple + :py:class:`~openeo.rest.datacube.DataCube`/:py:class:`~openeo.rest.vectorcube.VectorCube` + instances to the constructor, + and start a batch job from that, + for example as follows: + + .. code-block:: python + + from openeo import MultiResult + + cube1 = ... + cube2 = ... + multi_result = MultiResult([cube1, cube2]) + job = multi_result.create_job() + + .. seealso:: + + :ref:`multi-result-process-graphs` + + .. versionadded:: 0.35.0 + """ + + __slots__ = ("_multi_leaf_graph", "_connection") + + def __init__(self, leaves: List[FlatGraphableMixin], connection: Optional[Connection] = None): + """ + Build a :py:class:`MultiResult` instance from multiple leaf nodes + + :param leaves: list of objects that can be + converted to an openEO-style (flat) process graph representation, + typically :py:class:`~openeo.rest.datacube.DataCube` + or :py:class:`~openeo.rest.vectorcube.VectorCube` instances. + :param connection: Optional connection to use for creating/starting batch jobs, + for special use cases where the provided leaf instances + are not already associated with a connection. + """ + self._multi_leaf_graph = MultiLeafGraph(leaves=leaves) + self._connection = self._extract_connection(leaves=leaves, connection=connection) + + @staticmethod + def _extract_connection(leaves: List[FlatGraphableMixin], connection: Optional[Connection] = None) -> Connection: + """ + Extract common connection from leaves and/or explicitly provided connection. + Fails if there are multiple or none. + """ + connections = set() + if connection: + connections.add(connection) + connections.update(extract_connections(leaves)) + + if len(connections) == 1: + return connections.pop() + elif len(connections) == 0: + raise OpenEoClientException("No connection in any of the MultiResult leaves") + else: + raise OpenEoClientException("MultiResult with multiple different connections") + + def flat_graph(self) -> Dict[str, dict]: + return self._multi_leaf_graph.flat_graph() + + def create_job( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + return self._connection.create_job( + process_graph=self._multi_leaf_graph, + title=title, + description=description, + additional=job_options, + validate=validate, + ) + + def execute_batch( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + ) -> BatchJob: + job = self.create_job(title=title, description=description, job_options=job_options, validate=validate) + return job.run_synchronous() diff --git a/lib/openeo/rest/rest_capabilities.py b/lib/openeo/rest/rest_capabilities.py new file mode 100644 index 000000000..00922c261 --- /dev/null +++ b/lib/openeo/rest/rest_capabilities.py @@ -0,0 +1,54 @@ +from typing import List, Optional + +from openeo.capabilities import Capabilities +from openeo.internal.jupyter import render_component +from openeo.util import deep_get + + +class RESTCapabilities(Capabilities): + """Represents REST capabilities of a connection / back end.""" + + def __init__(self, data: dict, url: str = None): + super(RESTCapabilities, self).__init__(data) + self.capabilities = data + self.url = url + + def get(self, key: str, default=None): + return self.capabilities.get(key, default) + + def deep_get(self, *keys, default=None): + return deep_get(self.capabilities, *keys, default=default) + + def api_version(self) -> str: + """ Get openEO version.""" + if 'api_version' in self.capabilities: + return self.capabilities.get('api_version') + else: + # Legacy/deprecated + return self.capabilities.get('version') + + def list_features(self): + """ List all supported features / endpoints.""" + return self.capabilities.get('endpoints') + + def has_features(self, method_name): + """ Check whether a feature / endpoint is supported.""" + # Field: endpoints > ... TODO + pass + + def supports_endpoint(self, path: str, method="GET"): + return any( + endpoint.get("path") == path and method.upper() in endpoint.get("methods", []) + for endpoint in self.capabilities.get("endpoints", []) + ) + + def currency(self) -> Optional[str]: + """Get default billing currency.""" + return self.deep_get("billing", "currency", default=None) + + def list_plans(self) -> List[dict]: + """List all billing plans.""" + return self.deep_get("billing", "plans", default=[]) + + def _repr_html_(self): + return render_component("capabilities", data = self.capabilities, parameters = {"url": self.url}) diff --git a/lib/openeo/rest/service.py b/lib/openeo/rest/service.py new file mode 100644 index 000000000..a12383695 --- /dev/null +++ b/lib/openeo/rest/service.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import typing +from typing import List, Optional, Union + +from openeo.api.logs import LogEntry, log_level_name +from openeo.internal.jupyter import VisualDict, VisualList + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo.rest.connection import Connection + + +class Service: + """Represents a secondary web service in openeo.""" + + def __init__(self, service_id: str, connection: Connection): + # Unique identifier of the secondary web service (string) + self.service_id = service_id + self.connection = connection + + def __repr__(self): + return '<{c} service_id={i!r}>'.format(c=self.__class__.__name__, i=self.service_id) + + def _repr_html_(self): + data = self.describe_service() + currency = self.connection.capabilities().currency() + return VisualDict('service', data = data, parameters = {'currency': currency}) + + def describe_service(self): + """ Get all information about a secondary web service.""" + # GET /services/{service_id} + return self.connection.get("/services/{}".format(self.service_id), expected_status=200).json() + + def update_service(self, process_graph=None, title=None, description=None, enabled=None, configuration=None, plan=None, budget=None, additional=None): + """ Update a secondary web service.""" + # PATCH /services/{service_id} + raise NotImplementedError + + def delete_service(self): + """ Delete a secondary web service.""" + # DELETE /services/{service_id} + self.connection.delete("/services/{}".format(self.service_id), expected_status=204) + + def logs( + self, offset: Optional[str] = None, level: Optional[Union[str, int]] = None + ) -> List[LogEntry]: + """Retrieve service logs.""" + url = f"/service/{self.service_id}/logs" + params = {} + if offset is not None: + params["offset"] = offset + if level is not None: + params["level"] = log_level_name(level) + resp = self.connection.get(url, params=params, expected_status=200) + logs = resp.json()["logs"] + entries = [LogEntry(log) for log in logs] + return VisualList("logs", data=entries) diff --git a/lib/openeo/rest/udp.py b/lib/openeo/rest/udp.py new file mode 100644 index 000000000..0df9015ab --- /dev/null +++ b/lib/openeo/rest/udp.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import typing +from pathlib import Path +from typing import List, Optional, Union + +from openeo.api.process import Parameter +from openeo.internal.graph_building import FlatGraphableMixin, as_flat_graph +from openeo.internal.jupyter import render_component +from openeo.internal.processes.builder import ProcessBuilderBase +from openeo.internal.warnings import deprecated +from openeo.util import dict_no_none + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo.rest.connection import Connection + + +def build_process_dict( + process_graph: Union[dict, FlatGraphableMixin, Path, List[FlatGraphableMixin]], + process_id: Optional[str] = None, + summary: Optional[str] = None, + description: Optional[str] = None, + parameters: Optional[List[Union[Parameter, dict]]] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, +) -> dict: + """ + Build a dictionary describing a process with metadaa (`process_graph`, `parameters`, `description`, ...) + + :param process_graph: dict or builder representing a process graph + :param process_id: identifier of the process + :param summary: short summary of what the process does + :param description: detailed description + :param parameters: list of process parameters (which have name, schema, default value, ...) + :param returns: description and schema of what the process returns + :param categories: list of categories + :param examples: list of examples, may be used for unit tests + :param links: list of links related to the process + :return: dictionary in openEO "process graph with metadata" format + """ + process = dict_no_none( + process_graph=as_flat_graph(process_graph), + id=process_id, + summary=summary, + description=description, + returns=returns, + categories=categories, + examples=examples, + links=links + ) + if parameters is not None: + process["parameters"] = [ + (p if isinstance(p, Parameter) else Parameter(**p)).to_dict() + for p in parameters + ] + return process + + +class RESTUserDefinedProcess: + """ + Wrapper for a user-defined process stored (or to be stored) on an openEO back-end + """ + + def __init__(self, user_defined_process_id: str, connection: Connection): + self.user_defined_process_id = user_defined_process_id + self._connection = connection + self._connection.assert_user_defined_process_support() + + def _repr_html_(self): + process = self.describe() + return render_component('process', data=process, parameters = {'show-graph': True, 'provide-download': False}) + + def store( + self, + process_graph: Union[dict, FlatGraphableMixin], + parameters: Optional[List[Union[Parameter, dict]]] = None, + public: bool = False, + summary: Optional[str] = None, + description: Optional[str] = None, + returns: Optional[dict] = None, + categories: Optional[List[str]] = None, + examples: Optional[List[dict]] = None, + links: Optional[List[dict]] = None, + ): + """Store a process graph and its metadata on the backend as a user-defined process""" + process = build_process_dict( + process_graph=process_graph, parameters=parameters, + summary=summary, description=description, returns=returns, + categories=categories, examples=examples, links=links, + ) + + # TODO: this "public" flag is not standardized yet EP-3609, https://github.com/Open-EO/openeo-api/issues/310 + process["public"] = public + + self._connection._preflight_validation(pg_with_metadata={"process": process}) + self._connection.put( + path="/process_graphs/{}".format(self.user_defined_process_id), json=process, expected_status=200 + ) + + @deprecated( + "Use `store` instead. Method `update` is misleading: OpenEO API does not provide (partial) updates" + " of user-defined processes, only fully overwriting 'store' operations.", + version="0.4.11") + def update( + self, process_graph: Union[dict, ProcessBuilderBase], parameters: List[Union[Parameter, dict]] = None, + public: bool = False, summary: str = None, description: str = None + ): + self.store(process_graph=process_graph, parameters=parameters, public=public, summary=summary, + description=description) + + def describe(self) -> dict: + """Get metadata of this user-defined process.""" + # TODO: parse the "parameters" to Parameter objects? + return self._connection.get(path="/process_graphs/{}".format(self.user_defined_process_id)).json() + + def delete(self) -> None: + """Remove user-defined process from back-end""" + self._connection.delete(path="/process_graphs/{}".format(self.user_defined_process_id), expected_status=204) + + def validate(self) -> None: + raise NotImplementedError diff --git a/lib/openeo/rest/userfile.py b/lib/openeo/rest/userfile.py new file mode 100644 index 000000000..5e0e94e03 --- /dev/null +++ b/lib/openeo/rest/userfile.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import typing +from pathlib import Path, PurePosixPath +from typing import Any, Dict, Optional, Union + +from openeo.rest import DEFAULT_DOWNLOAD_CHUNK_SIZE +from openeo.util import ensure_dir + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo.rest.connection import Connection + + +class UserFile: + """ + Handle to a (user-uploaded) file in the user workspace on a openEO back-end. + """ + + def __init__( + self, + path: Union[str, PurePosixPath, None], + *, + connection: Connection, + metadata: Optional[dict] = None, + ): + if path: + pass + elif metadata and metadata.get("path"): + path = metadata.get("path") + else: + raise ValueError( + "File path should be specified through `path` or `metadata` argument." + ) + + self.path = PurePosixPath(path) + self.metadata = metadata or {"path": path} + self.connection = connection + + @classmethod + def from_metadata(cls, metadata: dict, connection: Connection) -> UserFile: + """Build :py:class:`UserFile` from a workspace file metadata dictionary.""" + return cls(path=None, connection=connection, metadata=metadata) + + def __repr__(self): + return "<{c} file={i!r}>".format(c=self.__class__.__name__, i=self.path) + + def _get_endpoint(self) -> str: + return f"/files/{self.path!s}" + + def download(self, target: Union[Path, str] = None) -> Path: + """ + Downloads a user-uploaded file from the user workspace on the back-end + locally to the given location. + + :param target: local download target path. Can be an existing folder + (in which case the file name advertised by backend will be used) + or full file name. By default, the working directory will be used. + """ + response = self.connection.get( + self._get_endpoint(), expected_status=200, stream=True + ) + + target = Path(target or Path.cwd()) + if target.is_dir(): + target = target / self.path.name + ensure_dir(target.parent) + + with target.open(mode="wb") as f: + for chunk in response.iter_content(chunk_size=DEFAULT_DOWNLOAD_CHUNK_SIZE): + f.write(chunk) + + return target + + def upload(self, source: Union[Path, str]) -> UserFile: + """ + Uploads a local file to the path corresponding to this :py:class:`UserFile` in the user workspace + and returns new :py:class:`UserFile` of newly uploaded file. + + .. tip:: + Usually you'll just need + :py:meth:`Connection.upload_file() ` + instead of this :py:class:`UserFile` method. + + If the file exists in the user workspace it will be replaced. + + :param source: A path to a file on the local file system to upload. + :return: new :py:class:`UserFile` instance of the newly uploaded file + """ + return self.connection.upload_file(source, target=self.path) + + def delete(self): + """Delete the user-uploaded file from the user workspace on the back-end.""" + self.connection.delete(self._get_endpoint(), expected_status=204) + + def to_dict(self) -> Dict[str, Any]: + """Returns the provided metadata as dict.""" + # This is used in internal/jupyter.py to detect and get the original metadata. + # TODO: make this more explicit with an internal API? + return self.metadata diff --git a/lib/openeo/rest/vectorcube.py b/lib/openeo/rest/vectorcube.py new file mode 100644 index 000000000..09ed77537 --- /dev/null +++ b/lib/openeo/rest/vectorcube.py @@ -0,0 +1,610 @@ +from __future__ import annotations + +import json +import pathlib +import typing +from typing import Callable, List, Optional, Tuple, Union + +import shapely.geometry.base + +import openeo.rest.datacube +from openeo.api.process import Parameter +from openeo.internal.documentation import openeo_process +from openeo.internal.graph_building import PGNode +from openeo.internal.warnings import legacy_alias +from openeo.metadata import CollectionMetadata, CubeMetadata, Dimension +from openeo.rest._datacube import ( + THIS, + UDF, + _ensure_save_result, + _ProcessGraphAbstraction, + build_child_callback, +) +from openeo.rest.job import BatchJob +from openeo.rest.mlmodel import MlModel +from openeo.util import InvalidBBoxException, dict_no_none, guess_format, to_bbox_dict + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + from openeo import Connection + + +class VectorCube(_ProcessGraphAbstraction): + """ + A Vector Cube, or 'Vector Collection' is a data structure containing 'Features': + https://www.w3.org/TR/sdw-bp/#dfn-feature + + The features in this cube are restricted to have a geometry. Geometries can be points, lines, polygons etcetera. + A geometry is specified in a 'coordinate reference system'. https://www.w3.org/TR/sdw-bp/#dfn-coordinate-reference-system-(crs) + """ + + _DEFAULT_VECTOR_FORMAT = "GeoJSON" + + def __init__(self, graph: PGNode, connection: Union[Connection, None], metadata: Optional[CubeMetadata] = None): + super().__init__(pgnode=graph, connection=connection) + self.metadata = metadata + + @classmethod + def _build_metadata(cls, add_properties: bool = False) -> CollectionMetadata: + """Helper to build a (minimal) `CollectionMetadata` object.""" + # Vector cubes have at least a "geometry" dimension + dimensions = [Dimension(name="geometry", type="geometry")] + if add_properties: + dimensions.append(Dimension(name="properties", type="other")) + # TODO #464: use a more generic metadata container than "collection" metadata + return CollectionMetadata(metadata={}, dimensions=dimensions) + + def process( + self, + process_id: str, + arguments: dict = None, + metadata: Optional[CollectionMetadata] = None, + namespace: Optional[str] = None, + **kwargs, + ) -> VectorCube: + """ + Generic helper to create a new VectorCube by applying a process. + + :param process_id: process id of the process. + :param args: argument dictionary for the process. + :return: new VectorCube instance + """ + pg = self._build_pgnode(process_id=process_id, arguments=arguments, namespace=namespace, **kwargs) + return VectorCube(graph=pg, connection=self._connection, metadata=metadata or self.metadata) + + @classmethod + @openeo_process + def load_geojson( + cls, + connection: Connection, + data: Union[dict, str, pathlib.Path, shapely.geometry.base.BaseGeometry, Parameter], + properties: Optional[List[str]] = None, + ) -> VectorCube: + """ + Converts GeoJSON data as defined by RFC 7946 into a vector data cube. + + :param connection: the connection to use to connect with the openEO back-end. + :param data: the geometry to load. One of: + + - GeoJSON-style data structure: e.g. a dictionary with ``"type": "Polygon"`` and ``"coordinates"`` fields + - a path to a local GeoJSON file + - a GeoJSON string + - a shapely geometry object + + :param properties: A list of properties from the GeoJSON file to construct an additional dimension from. + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + # TODO: unify with `DataCube._get_geometry_argument` + # TODO #457 also support client side fetching of GeoJSON from URL? + if isinstance(data, str) and data.strip().startswith("{"): + # Assume JSON dump + geometry = json.loads(data) + elif isinstance(data, (str, pathlib.Path)): + # Assume local file + with pathlib.Path(data).open(mode="r", encoding="utf-8") as f: + geometry = json.load(f) + assert isinstance(geometry, dict) + elif isinstance(data, shapely.geometry.base.BaseGeometry): + geometry = shapely.geometry.mapping(data) + elif isinstance(data, Parameter): + geometry = data + elif isinstance(data, dict): + geometry = data + else: + raise ValueError(data) + # TODO #457 client side verification of GeoJSON construct: valid type, valid structure, presence of CRS, ...? + + pg = PGNode(process_id="load_geojson", data=geometry, properties=properties or []) + # TODO #457 always a "properties" dimension? https://github.com/Open-EO/openeo-processes/issues/448 + metadata = cls._build_metadata(add_properties=True) + return cls(graph=pg, connection=connection, metadata=metadata) + + @classmethod + @openeo_process + def load_url(cls, connection: Connection, url: str, format: str, options: Optional[dict] = None) -> VectorCube: + """ + Loads a file from a URL + + :param connection: the connection to use to connect with the openEO back-end. + :param url: The URL to read from. Authentication details such as API keys or tokens may need to be included in the URL. + :param format: The file format to use when loading the data. + :param options: The file format parameters to use when reading the data. + Must correspond to the parameters that the server reports as supported parameters for the chosen ``format`` + :return: new VectorCube instance + + .. warning:: EXPERIMENTAL: this process is experimental with the potential for major things to change. + + .. versionadded:: 0.22.0 + """ + pg = PGNode(process_id="load_url", arguments=dict_no_none(url=url, format=format, options=options)) + # TODO #457 always a "properties" dimension? https://github.com/Open-EO/openeo-processes/issues/448 + metadata = cls._build_metadata(add_properties=True) + return cls(graph=pg, connection=connection, metadata=metadata) + + @openeo_process + def run_udf( + self, + udf: Union[str, UDF], + runtime: Optional[str] = None, + version: Optional[str] = None, + context: Optional[dict] = None, + ) -> VectorCube: + """ + Run a UDF on the vector cube. + + It is recommended to provide the UDF just as :py:class:`UDF ` instance. + (the other arguments could be used to override UDF parameters if necessary). + + :param udf: UDF code as a string or :py:class:`UDF ` instance + :param runtime: UDF runtime + :param version: UDF version + :param context: UDF context + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + .. versionadded:: 0.10.0 + + .. versionchanged:: 0.16.0 + Added support to pass self-contained :py:class:`UDF ` instance. + """ + if isinstance(udf, UDF): + # `UDF` instance is preferred usage pattern, but allow overriding. + version = version or udf.version + context = context or udf.context + runtime = runtime or udf.get_runtime(connection=self.connection) + udf = udf.code + else: + if not runtime: + raise ValueError("Argument `runtime` must be specified") + return self.process( + process_id="run_udf", + data=self, udf=udf, runtime=runtime, + arguments=dict_no_none({"version": version, "context": context}), + ) + + @openeo_process + def save_result(self, format: Union[str, None] = "GeoJSON", options: dict = None): + # TODO #401: guard against duplicate save_result nodes? + return self.process( + process_id="save_result", + arguments={ + "data": self, + "format": format or "GeoJSON", + "options": options or {}, + }, + ) + + def execute(self, *, validate: Optional[bool] = None) -> dict: + """Executes the process graph.""" + return self._connection.execute(self.flat_graph(), validate=validate) + + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + ) -> Union[None, bytes]: + """ + Execute synchronously and download the vector cube. + + The result will be stored to the output path, when specified. + If no output path (or ``None``) is given, the raw download content will be returned as ``bytes`` object. + + :param outputfile: (optional) output file to store the result to + :param format: (optional) output format to use. + :param options: (optional) additional output format options. + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=format, + options=options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.download()", + ) + return self._connection.download(cube.flat_graph(), outputfile=outputfile, validate=validate) + + def execute_batch( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + print=print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + # TODO: avoid using kwargs as format options + **format_options, + ) -> BatchJob: + """ + Evaluate the process graph by creating a batch job, and retrieving the results when it is finished. + This method is mostly recommended if the batch job is expected to run in a reasonable amount of time. + + For very long running jobs, you probably do not want to keep the client running. + + :param job_options: + :param outputfile: The path of a file to which a result can be written + :param out_format: (optional) output format to use. + :param format_options: (optional) additional output format options + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options, + weak_format=guess_format(outputfile) if outputfile else None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.execute_batch()", + ) + job = cube.create_job( + title=title, + description=description, + plan=plan, + budget=budget, + job_options=job_options, + validate=validate, + auto_add_save_result=False, + ) + return job.run_synchronous( + # TODO #135 support multi file result sets too + outputfile=outputfile, + print=print, max_poll_interval=max_poll_interval, connection_retry_interval=connection_retry_interval + ) + + def create_job( + self, + out_format: Optional[str] = None, + *, + title: Optional[str] = None, + description: Optional[str] = None, + plan: Optional[str] = None, + budget: Optional[float] = None, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + auto_add_save_result: bool = True, + **format_options, + ) -> BatchJob: + """ + Sends a job to the backend and returns a ClientJob instance. + + :param out_format: String Format of the job result. + :param title: job title + :param description: job description + :param plan: The billing plan to process and charge the job with + :param budget: Maximum budget to be spent on executing the job. + Note that some backends do not honor this limit. + :param job_options: A dictionary containing (custom) job options + :param format_options: String Parameters for the job result format + :param validate: Optional toggle to enable/prevent validation of the process graphs before execution + (overruling the connection's ``auto_validate`` setting). + :param auto_add_save_result: Automatically add a ``save_result`` node to the process graph if there is none yet. + + :return: Created job. + + .. versionchanged:: 0.32.0 + Added ``auto_add_save_result`` option + """ + # TODO: avoid using all kwargs as format_options + # TODO #278 centralize download/create_job/execute_job logic in DataCube, VectorCube, MlModel, ... + cube = self + if auto_add_save_result: + cube = _ensure_save_result( + cube=cube, + format=out_format, + options=format_options or None, + default_format=self._DEFAULT_VECTOR_FORMAT, + method="VectorCube.create_job()", + ) + return self._connection.create_job( + process_graph=cube.flat_graph(), + title=title, + description=description, + plan=plan, + budget=budget, + additional=job_options, + validate=validate, + ) + + send_job = legacy_alias(create_job, name="send_job", since="0.10.0") + + @openeo_process + def filter_bands(self, bands: List[str]) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + return self.process( + process_id="filter_bands", + arguments={"data": THIS, "bands": bands}, + ) + + @openeo_process + def filter_bbox( + self, + *, + west: Optional[float] = None, + south: Optional[float] = None, + east: Optional[float] = None, + north: Optional[float] = None, + extent: Optional[Union[dict, List[float], Tuple[float, float, float, float], Parameter]] = None, + crs: Optional[int] = None, + ) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + if any(c is not None for c in [west, south, east, north]): + if extent is not None: + raise InvalidBBoxException("Don't specify both west/south/east/north and extent") + extent = dict_no_none(west=west, south=south, east=east, north=north) + + if isinstance(extent, Parameter): + pass + else: + extent = to_bbox_dict(extent, crs=crs) + return self.process( + process_id="filter_bbox", + arguments={"data": THIS, "extent": extent}, + ) + + @openeo_process + def filter_labels( + self, condition: Union[PGNode, Callable], dimension: str, context: Optional[dict] = None + ) -> VectorCube: + """ + Filters the dimension labels in the data cube for the given dimension. + Only the dimension labels that match the specified condition are preserved, + all other labels with their corresponding data get removed. + + :param condition: the "child callback" which will be given a single label value (number or string) + and returns a boolean expressing if the label should be preserved. + Also see :ref:`callbackfunctions`. + :param dimension: The name of the dimension to filter on. + + .. versionadded:: 0.22.0 + """ + condition = build_child_callback(condition, parent_parameters=["value"]) + return self.process( + process_id="filter_labels", + arguments=dict_no_none(data=THIS, condition=condition, dimension=dimension, context=context), + ) + + @openeo_process + def filter_vector( + self, geometries: Union["VectorCube", shapely.geometry.base.BaseGeometry, dict], relation: str = "intersects" + ) -> VectorCube: + """ + .. versionadded:: 0.22.0 + """ + # TODO #459 docs + if not isinstance(geometries, (VectorCube, Parameter)): + geometries = self.load_geojson(connection=self.connection, data=geometries) + return self.process( + process_id="filter_vector", + arguments={"data": THIS, "geometries": geometries, "relation": relation}, + ) + + @openeo_process + def fit_class_random_forest( + self, + # TODO #279 #293: target type should be `VectorCube` (with adapters for GeoJSON FeatureCollection, GeoPandas, ...) + target: dict, + # TODO #293 max_variables officially has no default + max_variables: Optional[int] = None, + num_trees: int = 100, + seed: Optional[int] = None, + ) -> MlModel: + """ + Executes the fit of a random forest classification based on the user input of target and predictors. + The Random Forest classification model is based on the approach by Breiman (2001). + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + :param target: The training sites for the classification model as a vector data cube. This is associated with the target + variable for the Random Forest model. The geometry has to be associated with a value to predict (e.g. fractional + forest canopy cover). + :param max_variables: Specifies how many split variables will be used at a node. Default value is `null`, which corresponds to the + number of predictors divided by 3. + :param num_trees: The number of trees build within the Random Forest classification. + :param seed: A randomization seed to use for the random sampling in training. + + .. versionadded:: 0.16.0 + Originally added in version 0.10.0 as :py:class:`DataCube ` method, + but moved to :py:class:`VectorCube` in version 0.16.0. + """ + pgnode = PGNode( + process_id="fit_class_random_forest", + arguments=dict_no_none( + predictors=self, + # TODO #279 strictly per-spec, target should be a `vector-cube`, but due to lack of proper support we are limited to inline GeoJSON for now + target=target, + max_variables=max_variables, + num_trees=num_trees, + seed=seed, + ), + ) + model = MlModel(graph=pgnode, connection=self._connection) + return model + + @openeo_process + def fit_regr_random_forest( + self, + # TODO #279 #293: target type should be `VectorCube` (with adapters for GeoJSON FeatureCollection, GeoPandas, ...) + target: dict, + # TODO #293 max_variables officially has no default + max_variables: Optional[int] = None, + num_trees: int = 100, + seed: Optional[int] = None, + ) -> MlModel: + """ + Executes the fit of a random forest regression based on training data. + The Random Forest regression model is based on the approach by Breiman (2001). + + .. warning:: EXPERIMENTAL: not generally supported, API subject to change. + + :param target: The training sites for the regression model as a vector data cube. + This is associated with the target variable for the Random Forest model. + The geometry has to associated with a value to predict (e.g. fractional forest canopy cover). + :param max_variables: Specifies how many split variables will be used at a node. Default value is `null`, which corresponds to the + number of predictors divided by 3. + :param num_trees: The number of trees build within the Random Forest classification. + :param seed: A randomization seed to use for the random sampling in training. + + .. versionadded:: 0.16.0 + Originally added in version 0.10.0 as :py:class:`DataCube ` method, + but moved to :py:class:`VectorCube` in version 0.16.0. + """ + # TODO #279 #293: `fit_class_random_forest` should be defined on VectorCube instead of DataCube + pgnode = PGNode( + process_id="fit_regr_random_forest", + arguments=dict_no_none( + predictors=self, + # TODO #279 strictly per-spec, target should be a `vector-cube`, but due to lack of proper support we are limited to inline GeoJSON for now + target=target, + max_variables=max_variables, + num_trees=num_trees, + seed=seed, + ), + ) + model = MlModel(graph=pgnode, connection=self._connection) + return model + + @openeo_process + def apply_dimension( + self, + process: Union[str, typing.Callable, UDF, PGNode], + dimension: str, + target_dimension: Optional[str] = None, + context: Optional[dict] = None, + ) -> VectorCube: + """ + Applies a process to all values along a dimension of a data cube. + For example, if the temporal dimension is specified the process will work on the values of a time series. + + The process to apply is specified by providing a callback function in the `process` argument. + + :param process: the "child callback": + the name of a single process, + or a callback function as discussed in :ref:`callbackfunctions`, + or a :py:class:`UDF ` instance. + + The callback should correspond to a process that + receives an array of numerical values + and returns an array of numerical values. + For example: + + - ``"sort"`` (string) + - :py:func:`sort ` (:ref:`predefined openEO process function `) + - ``lambda data: data.concat([42, -3])`` (function or lambda) + + + :param dimension: The name of the source dimension to apply the process on. Fails with a DimensionNotAvailable error if the specified dimension does not exist. + :param target_dimension: The name of the target dimension or null (the default) to use the source dimension + specified in the parameter dimension. By specifying a target dimension, the source dimension is removed. + The target dimension with the specified name and the type other (see add_dimension) is created, if it doesn't exist yet. + :param context: Additional data to be passed to the process. + + :return: A datacube with the UDF applied to the given dimension. + :raises: DimensionNotAvailable + + .. versionadded:: 0.22.0 + """ + process = build_child_callback( + process=process, parent_parameters=["data", "context"], connection=self.connection + ) + arguments = dict_no_none( + { + "data": THIS, + "process": process, + "dimension": dimension, + "target_dimension": target_dimension, + "context": context, + } + ) + return self.process(process_id="apply_dimension", arguments=arguments) + + def vector_to_raster(self, target: openeo.rest.datacube.DataCube) -> openeo.rest.datacube.DataCube: + """ + Converts this vector cube (:py:class:`VectorCube`) into a raster data cube (:py:class:`~openeo.rest.datacube.DataCube`). + The bounding polygon of homogenous areas of pixels is constructed. + + :param target: a reference raster data cube to adopt the CRS/projection/resolution from. + + .. warning:: ``vector_to_raster`` is an experimental, non-standard process. It is not widely supported, and its API is subject to change. + + .. versionadded:: 0.28.0 + + """ + # TODO: this parameter sniffing is a temporary workaround until + # the `target` parameter name rename has fully settled + # https://github.com/Open-EO/openeo-python-driver/issues/274 + # After that has settled, it is still useful to verify assumptions about this non-standard process. + try: + process_spec = self.connection.describe_process("vector_to_raster") + target_parameter = process_spec["parameters"][1]["name"] + assert "target" in target_parameter + except Exception: + target_parameter = "target" + + pg_node = PGNode( + process_id="vector_to_raster", + arguments={"data": self, target_parameter: target}, + ) + # TODO: the correct metadata has to be passed here: + # replace "geometry" dimension with spatial dimensions of the target cube + return openeo.rest.datacube.DataCube(pg_node, connection=self._connection, metadata=self.metadata) diff --git a/lib/openeo/testing/__init__.py b/lib/openeo/testing/__init__.py new file mode 100644 index 000000000..8ad898cba --- /dev/null +++ b/lib/openeo/testing/__init__.py @@ -0,0 +1,37 @@ +""" +Utilities for testing of openEO client workflows. +""" + +import json +from pathlib import Path +from typing import Callable, Optional, Union + + +class TestDataLoader: + """ + Helper to resolve paths to test data files, load them as JSON, optionally preprocess them, etc. + + It's intended to be used as a pytest fixture, e.g. from ``conftest.py``: + + .. code-block:: python + + @pytest.fixture + def test_data() -> TestDataLoader: + return TestDataLoader(root=Path(__file__).parent / "data") + + .. versionadded:: 0.30.0 + """ + + def __init__(self, root: Union[str, Path]): + self.data_root = Path(root) + + def get_path(self, filename: Union[str, Path]) -> Path: + """Get absolute path to a test data file""" + return self.data_root / filename + + def load_json(self, filename: Union[str, Path], preprocess: Optional[Callable[[str], str]] = None) -> dict: + """Parse data from a test JSON file""" + data = self.get_path(filename).read_text(encoding="utf8") + if preprocess: + data = preprocess(data) + return json.loads(data) diff --git a/lib/openeo/testing/results.py b/lib/openeo/testing/results.py new file mode 100644 index 000000000..633ddaf58 --- /dev/null +++ b/lib/openeo/testing/results.py @@ -0,0 +1,386 @@ +""" +Assert functions for comparing actual (batch job) results against expected reference data. +""" + +import json +import logging +import tempfile +from pathlib import Path +from typing import List, Optional, Union + +import xarray +import xarray.testing + +from openeo.rest.job import DEFAULT_JOB_RESULTS_FILENAME, BatchJob, JobResults +from openeo.util import repr_truncate + +_log = logging.getLogger(__name__) + + +_DEFAULT_RTOL = 1e-6 +_DEFAULT_ATOL = 1e-6 + + +def _load_xarray_netcdf(path: Union[str, Path], **kwargs) -> xarray.Dataset: + """ + Load a netCDF file as Xarray Dataset + """ + _log.debug(f"_load_xarray_netcdf: {path!r}") + return xarray.load_dataset(path, **kwargs) + + +def _load_rioxarray_geotiff(path: Union[str, Path], **kwargs) -> xarray.DataArray: + """ + Load a GeoTIFF file as Xarray DataArray (using `rioxarray` extension). + """ + _log.debug(f"_load_rioxarray_geotiff: {path!r}") + try: + import rioxarray + except ImportError as e: + raise ImportError("This feature requires 'rioxarray` as optional dependency.") from e + return rioxarray.open_rasterio(path, **kwargs) + + +def _load_xarray(path: Union[str, Path], **kwargs) -> Union[xarray.Dataset, xarray.DataArray]: + """ + Generically load a netCDF/GeoTIFF file as Xarray Dataset/DataArray. + """ + path = Path(path) + if path.suffix.lower() in {".nc", ".netcdf"}: + return _load_xarray_netcdf(path, **kwargs) + elif path.suffix.lower() in {".tif", ".tiff", ".gtiff", ".geotiff"}: + return _load_rioxarray_geotiff(path, **kwargs) + raise ValueError(f"Unsupported file type: {path}") + + +def _load_json(path: Union[str, Path]) -> dict: + """ + Load a JSON file. + """ + with Path(path).open("r", encoding="utf-8") as f: + return json.load(f) + + +def _as_xarray_dataset(data: Union[str, Path, xarray.Dataset]) -> xarray.Dataset: + """ + Get data as Xarray Dataset (loading from file if needed). + """ + if isinstance(data, (str, Path)): + data = _load_xarray(data) + # TODO auto-convert DataArray to Dataset? + if not isinstance(data, xarray.Dataset): + raise ValueError(f"Unsupported type: {type(data)}") + return data + + +def _as_xarray_dataarray(data: Union[str, Path, xarray.DataArray]) -> xarray.DataArray: + """ + Convert a path to a NetCDF/GeoTIFF file to an Xarray DataArray. + + :param data: path to a NetCDF/GeoTIFF file or Xarray DataArray + :return: Xarray DataArray + """ + if isinstance(data, (str, Path)): + data = _load_xarray(data) + # TODO: auto-convert Dataset to DataArray? + if not isinstance(data, xarray.DataArray): + raise ValueError(f"Unsupported type: {type(data)}") + return data + + +def _compare_xarray_dataarray( + actual: Union[xarray.DataArray, str, Path], + expected: Union[xarray.DataArray, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +) -> List[str]: + """ + Compare two xarray DataArrays with tolerance and report mismatch issues (as strings) + + Checks that are done (with tolerance): + - (optional) Check fraction of mismatching pixels (difference exceeding some tolerance). + If fraction is below a given threshold, ignore these mismatches in subsequent comparisons. + If fraction is above the threshold, report this issue. + - Compare actual and expected data with `xarray.testing.assert_allclose` and specified tolerances. + + :return: list of issues (empty if no issues) + """ + # TODO: make this a public function? + # TODO: option for nodata fill value? + # TODO: option to include data type check? + # TODO: option to cast to some data type (or even rescale) before comparison? + # TODO: also compare attributes of the DataArray? + actual = _as_xarray_dataarray(actual) + expected = _as_xarray_dataarray(expected) + issues = [] + + # `xarray.testing.assert_allclose` currently does not always + # provides detailed information about shape/dimension mismatches + # so we enrich the issue listing with some more details + if actual.dims != expected.dims: + issues.append(f"Dimension mismatch: {actual.dims} != {expected.dims}") + for dim in sorted(set(expected.dims).intersection(actual.dims)): + acs = actual.coords[dim].values + ecs = expected.coords[dim].values + if not (acs.shape == ecs.shape and (acs == ecs).all()): + issues.append(f"Coordinates mismatch for dimension {dim!r}: {acs} != {ecs}") + if actual.shape != expected.shape: + issues.append(f"Shape mismatch: {actual.shape} != {expected.shape}") + + try: + xarray.testing.assert_allclose(a=actual, b=expected, rtol=rtol, atol=atol) + except AssertionError as e: + # TODO: message of `assert_allclose` is typically multiline, split it again or make it one line? + issues.append(str(e).strip()) + + return issues + + +def assert_xarray_dataarray_allclose( + actual: Union[xarray.DataArray, str, Path], + expected: Union[xarray.DataArray, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataArray`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file. + :param expected: expected or reference data, provided as Xarray DataArray object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_xarray_dataarray(actual=actual, expected=expected, rtol=rtol, atol=atol) + if issues: + raise AssertionError("\n".join(issues)) + + +def _compare_xarray_datasets( + actual: Union[xarray.Dataset, str, Path], + expected: Union[xarray.Dataset, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +) -> List[str]: + """ + Compare two xarray ``DataSet``s with tolerance and report mismatch issues (as strings) + + :return: list of issues (empty if no issues) + """ + # TODO: make this a public function? + actual = _as_xarray_dataset(actual) + expected = _as_xarray_dataset(expected) + + all_issues = [] + # TODO: just leverage DataSet support in xarray.testing.assert_allclose for all this? + actual_vars = set(actual.data_vars) + expected_vars = set(expected.data_vars) + _log.debug(f"_compare_xarray_datasets: actual_vars={actual_vars!r} expected_vars={expected_vars!r}") + if actual_vars != expected_vars: + all_issues.append(f"Xarray DataSet variables mismatch: {actual_vars} != {expected_vars}") + for var in expected_vars.intersection(actual_vars): + _log.debug(f"_compare_xarray_datasets: comparing variable {var!r}") + issues = _compare_xarray_dataarray(actual[var], expected[var], rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for variable {var!r}:") + all_issues.extend(issues) + return all_issues + + +def assert_xarray_dataset_allclose( + actual: Union[xarray.Dataset, str, Path], + expected: Union[xarray.Dataset, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataSet`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file + :param expected: expected or reference data, provided as Xarray Dataset object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_xarray_datasets(actual=actual, expected=expected, rtol=rtol, atol=atol) + if issues: + raise AssertionError("\n".join(issues)) + + +def assert_xarray_allclose( + actual: Union[xarray.Dataset, xarray.DataArray, str, Path], + expected: Union[xarray.Dataset, xarray.DataArray, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +): + """ + Assert that two Xarray ``DataSet`` or ``DataArray`` instances are equal (with tolerance). + + :param actual: actual data, provided as Xarray object or path to NetCDF/GeoTIFF file. + :param expected: expected or reference data, provided as Xarray object or path to NetCDF/GeoTIFF file. + :param rtol: relative tolerance + :param atol: absolute tolerance + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + if isinstance(actual, (str, Path)): + actual = _load_xarray(actual) + if isinstance(expected, (str, Path)): + expected = _load_xarray(expected) + + if isinstance(actual, xarray.Dataset) and isinstance(expected, xarray.Dataset): + assert_xarray_dataset_allclose(actual, expected, rtol=rtol, atol=atol) + elif isinstance(actual, xarray.DataArray) and isinstance(expected, xarray.DataArray): + assert_xarray_dataarray_allclose(actual, expected, rtol=rtol, atol=atol) + else: + raise ValueError(f"Unsupported types: {type(actual)} and {type(expected)}") + + +def _as_job_results_download( + job_results: Union[BatchJob, JobResults, str, Path], tmp_path: Optional[Path] = None +) -> Path: + """ + Produce a directory with downloaded job results assets and metadata. + + :param job_results: a batch job, job results metadata object or a path + :param tmp_path: root temp path to download results if needed + :return: + """ + # TODO: support download/copy from other sources (e.g. S3, ...) + if isinstance(job_results, BatchJob): + job_results = job_results.get_results() + if isinstance(job_results, JobResults): + download_dir = tempfile.mkdtemp(dir=tmp_path, prefix=job_results.get_job_id() + "-") + _log.info(f"Downloading results from job {job_results.get_job_id()} to {download_dir}") + job_results.download_files(target=download_dir) + job_results = download_dir + if isinstance(job_results, (str, Path)): + return Path(job_results) + else: + raise ValueError(f"Unsupported type: {type(job_results)}") + + +def _compare_job_results( + actual: Union[BatchJob, JobResults, str, Path], + expected: Union[BatchJob, JobResults, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, + tmp_path: Optional[Path] = None, +) -> List[str]: + """ + Compare two job results sets (directories with downloaded assets and metadata, + e.g. as produced by ``JobResults.download_files()``) + + :return: list of issues (empty if no issues) + """ + actual_dir = _as_job_results_download(actual, tmp_path=tmp_path) + expected_dir = _as_job_results_download(expected, tmp_path=tmp_path) + _log.info(f"Comparing job results: {actual_dir!r} vs {expected_dir!r}") + + all_issues = [] + + actual_filenames = set(p.name for p in actual_dir.glob("*") if p.is_file()) + expected_filenames = set(p.name for p in expected_dir.glob("*") if p.is_file()) + if actual_filenames != expected_filenames: + all_issues.append(f"File set mismatch: {actual_filenames} != {expected_filenames}") + + for filename in expected_filenames.intersection(actual_filenames): + actual_path = actual_dir / filename + expected_path = expected_dir / filename + if filename == DEFAULT_JOB_RESULTS_FILENAME: + issues = _compare_job_result_metadata(actual=actual_path, expected=expected_path) + if issues: + all_issues.append(f"Issues for metadata file {filename!r}:") + all_issues.extend(issues) + elif expected_path.suffix.lower() in {".nc", ".netcdf"}: + issues = _compare_xarray_datasets(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for file {filename!r}:") + all_issues.extend(issues) + elif expected_path.suffix.lower() in {".tif", ".tiff", ".gtiff", ".geotiff"}: + issues = _compare_xarray_dataarray(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for file {filename!r}:") + all_issues.extend(issues) + else: + _log.warning(f"Unhandled job result asset {filename!r}") + + return all_issues + + +def _compare_job_result_metadata( + actual: Union[str, Path], + expected: Union[str, Path], +) -> List[str]: + issues = [] + actual_metadata = _load_json(actual) + expected_metadata = _load_json(expected) + + # Check "derived_from" links + actual_derived_from = set(k["href"] for k in actual_metadata.get("links", []) if k["rel"] == "derived_from") + expected_derived_from = set(k["href"] for k in expected_metadata.get("links", []) if k["rel"] == "derived_from") + + if actual_derived_from != expected_derived_from: + actual_only = actual_derived_from - expected_derived_from + expected_only = expected_derived_from - actual_derived_from + common = actual_derived_from.intersection(expected_derived_from) + issues.append( + f"Differing 'derived_from' links ({len(common)} common, {len(actual_only)} only in actual, {len(expected_only)} only in expected):\n" + f" only in actual: {repr_truncate(actual_only, width=1000)}\n" + f" only in expected: {repr_truncate(expected_only, width=1000)}." + ) + + # TODO: more metadata checks (e.g. spatial and temporal extents)? + + return issues + + +def assert_job_results_allclose( + actual: Union[BatchJob, JobResults, str, Path], + expected: Union[BatchJob, JobResults, str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, + tmp_path: Optional[Path] = None, +): + """ + Assert that two job results sets are equal (with tolerance). + + :param actual: actual job results, provided as :py:class:`~openeo.rest.job.BatchJob` object, + :py:meth:`~openeo.rest.job.JobResults` object or path to directory with downloaded assets. + :param expected: expected job results, provided as :py:class:`~openeo.rest.job.BatchJob` object, + :py:meth:`~openeo.rest.job.JobResults` object or path to directory with downloaded assets. + :param rtol: relative tolerance + :param atol: absolute tolerance + :param tmp_path: root temp path to download results if needed. + It's recommended to pass pytest's `tmp_path` fixture here + :raises AssertionError: if not equal within the given tolerance + + .. versionadded:: 0.31.0 + + .. warning:: + This function is experimental and subject to change. + """ + issues = _compare_job_results(actual, expected, rtol=rtol, atol=atol, tmp_path=tmp_path) + if issues: + raise AssertionError("\n".join(issues)) diff --git a/lib/openeo/testing/stac.py b/lib/openeo/testing/stac.py new file mode 100644 index 000000000..4f0b455a8 --- /dev/null +++ b/lib/openeo/testing/stac.py @@ -0,0 +1,110 @@ +from typing import List, Optional, Union + + +class StacDummyBuilder: + """ + Helper to compactly produce STAC Item/Collection/Catalog/... dicts for test purposes + + .. warning:: + This is an experimental API subject to change. + """ + + _EXT_DATACUBE = "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" + + @classmethod + def item( + cls, + *, + id: str = "item123", + stac_version="1.0.0", + datetime: str = "2024-03-08", + properties: Optional[dict] = None, + cube_dimensions: Optional[dict] = None, + stac_extensions: Optional[List[str]] = None, + **kwargs, + ) -> dict: + """Create a STAC Item represented as dictionary.""" + properties = properties or {} + properties.setdefault("datetime", datetime) + + if cube_dimensions is not None: + properties["cube:dimensions"] = cube_dimensions + stac_extensions = cls._add_stac_extension(stac_extensions, cls._EXT_DATACUBE) + + d = { + "type": "Feature", + "stac_version": stac_version, + "id": id, + "geometry": None, + "properties": properties, + "links": [], + "assets": {}, + **kwargs, + } + + if stac_extensions is not None: + d["stac_extensions"] = stac_extensions + return d + + @classmethod + def _add_stac_extension(cls, stac_extensions: Union[List[str], None], stac_extension: str) -> List[str]: + stac_extensions = list(stac_extensions or []) + if stac_extension not in stac_extensions: + stac_extensions.append(stac_extension) + return stac_extensions + + @classmethod + def collection( + cls, + *, + id: str = "collection123", + description: str = "Collection 123", + stac_version: str = "1.0.0", + stac_extensions: Optional[List[str]] = None, + license: str = "proprietary", + extent: Optional[dict] = None, + cube_dimensions: Optional[dict] = None, + summaries: Optional[dict] = None, + ) -> dict: + """Create a STAC Collection represented as dictionary.""" + if extent is None: + extent = {"spatial": {"bbox": [[3, 4, 5, 6]]}, "temporal": {"interval": [["2024-01-01", "2024-05-05"]]}} + + d = { + "type": "Collection", + "stac_version": stac_version, + "id": id, + "description": description, + "license": license, + "extent": extent, + "links": [], + } + if cube_dimensions is not None: + d["cube:dimensions"] = cube_dimensions + stac_extensions = cls._add_stac_extension(stac_extensions, cls._EXT_DATACUBE) + if summaries is not None: + d["summaries"] = summaries + if stac_extensions is not None: + d["stac_extensions"] = stac_extensions + return d + + @classmethod + def catalog( + cls, + *, + id: str = "catalog123", + stac_version: str = "1.0.0", + description: str = "Catalog 123", + stac_extensions: Optional[List[str]] = None, + ) -> dict: + """Create a STAC Catalog represented as dictionary.""" + d = { + "type": "Catalog", + "stac_version": stac_version, + "id": id, + "description": description, + "links": [], + } + if stac_extensions is not None: + d["stac_extensions"] = stac_extensions + return d diff --git a/lib/openeo/udf/__init__.py b/lib/openeo/udf/__init__.py new file mode 100644 index 000000000..387b8bc3d --- /dev/null +++ b/lib/openeo/udf/__init__.py @@ -0,0 +1,13 @@ +from openeo import BaseOpenEoException + + +class OpenEoUdfException(BaseOpenEoException): + pass + + +from openeo.udf.debug import inspect +from openeo.udf.feature_collection import FeatureCollection +from openeo.udf.run_code import execute_local_udf, run_udf_code +from openeo.udf.structured_data import StructuredData +from openeo.udf.udf_data import UdfData +from openeo.udf.xarraydatacube import XarrayDataCube diff --git a/lib/openeo/udf/_compat.py b/lib/openeo/udf/_compat.py new file mode 100644 index 000000000..72a73a020 --- /dev/null +++ b/lib/openeo/udf/_compat.py @@ -0,0 +1,65 @@ +import json +import re +from typing import Union + +# TODO #465 move this to a more general utility subpackage? + +try: + import tomllib +except ImportError: + try: + import tomli as tomllib + except ImportError: + # Will be assigned with fallback implementation below + tomllib = None + + +class FlimsyTomlParser: + """ + This is a rudimentary, low-tech, incomplete implementation of TOML parsing functionality + for simple TOML use cases where the dependency on a full-fledged TOML library is not justified. + For these simple uses cases, it should act as a best-effort drop-in replacement + for the `loads()` functionality from full-fledged TOML libraries + like `tomllib` (part of standard library since Python 3.11) + or `tomli` (`tomllib` backport for earlier Python versions). + """ + + class TomlParseError(ValueError): + pass + + KEY_PAIR_REGEX = re.compile( + r"(?P^[a-z0-9_-]+)\s*=\s*(?P.*(\s+^\s+.*)*(\s+^])?)", + flags=re.MULTILINE | re.VERBOSE | re.IGNORECASE, + ) + + @classmethod + def loads(cls, data: str) -> dict: + if re.search(r"^\[", data, flags=re.MULTILINE): + raise cls.TomlParseError("Tables are not supported") + if re.search(r"^[a-z0-9_-]+\.[a-z0-9_.-]+\s*=", data, flags=re.MULTILINE | re.IGNORECASE): + raise cls.TomlParseError("Dotted keys are not supported") + return { + match.group("key"): cls._parse_toml_value_like_json(match.group("value")) + for match in cls.KEY_PAIR_REGEX.finditer(data) + } + + @classmethod + def _parse_toml_value_like_json(cls, value: str) -> Union[int, float, list]: + """ + Try to parse a TOML value by pretending it's (almost) JSON, + which covers the basics (simple strings, numbers, arrays, a bit of nesting, ...) + """ + # A bit of preprocessing to make it more JSON-like (strip comments, strip trailing commas) + value = re.sub(r"#.*$", "", value, flags=re.MULTILINE) + value = re.sub(r",\s*\]", "]", value) + # Rudimentarily convert single quote strings to double quotes. + value = re.sub("'([^'\"]*)'", r'"\1"', value) + try: + data = json.loads(value) + except json.JSONDecodeError as e: + raise cls.TomlParseError(f"Failed to parse TOML value {value!r}") from e + return data + + +if tomllib is None: + tomllib = FlimsyTomlParser diff --git a/lib/openeo/udf/debug.py b/lib/openeo/udf/debug.py new file mode 100644 index 000000000..3cb408494 --- /dev/null +++ b/lib/openeo/udf/debug.py @@ -0,0 +1,30 @@ +""" +Debug utilities for UDFs +""" +import logging +import os +import sys + +_log = logging.getLogger(__name__) +_user_log = logging.getLogger(os.environ.get("OPENEO_UDF_USER_LOGGER", f"{__name__}.user")) + + +def inspect(data=None, message: str = "", code: str = "User", level: str = "info"): + """ + Implementation of the openEO `inspect` process for UDF contexts. + + Note that it is up to the back-end implementation to properly capture this logging + and include it in the batch job logs. + + :param data: data to log + :param message: message to send in addition to the data + :param code: A label to help identify one or more log entries + :param level: The severity level of this message. Allowed values: "error", "warning", "info", "debug" + + .. versionadded:: 0.10.1 + + .. seealso:: :ref:`udf_logging_with_inspect` + """ + extra = {"data": data, "code": code} + kwargs = {"stacklevel": 2} if sys.version_info >= (3, 8) else {} + _user_log.log(level=logging.getLevelName(level.upper()), msg=message, extra=extra, **kwargs) diff --git a/lib/openeo/udf/feature_collection.py b/lib/openeo/udf/feature_collection.py new file mode 100644 index 000000000..329c618cc --- /dev/null +++ b/lib/openeo/udf/feature_collection.py @@ -0,0 +1,110 @@ +""" + +""" + +# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) +from __future__ import annotations + +from typing import Any, List, Optional, Union + +import pandas +import shapely.geometry + +# Geopandas is optional dependency for now +try: + from geopandas import GeoDataFrame +except ImportError: + class GeoDataFrame: + pass + + +class FeatureCollection: + """ + A feature collection that represents a subset or a whole feature collection + where single vector features may have time stamps assigned. + """ + + def __init__( + self, + id: str, + data: GeoDataFrame, + start_times: Optional[Union[pandas.DatetimeIndex, List[str]]] = None, + end_times: Optional[Union[pandas.DatetimeIndex, List[str]]] = None + ): + """ + Constructor of the of a vector collection + + :param id: The unique id of the vector collection + :param data: A GeoDataFrame with geometry column and attribute data + :param start_times: The vector with start times for each spatial x,y slice + :param end_times: The pandas.DateTimeIndex vector with end times + for each spatial x,y slice, if no + end times are defined, then time instances are assumed not intervals + """ + # TODO #455 `id` is first and a required argument, but it's unclear what it can/should be used for. Can we eliminate it? + self.id = id + self._data = data + # TODO #455 why not include these datetimes directly in the dataframe? + self._start_times = self._as_datetimeindex(start_times, expected_length=len(self.data)) + self._end_times = self._as_datetimeindex(end_times, expected_length=len(self.data)) + + def __repr__(self): + return f"<{type(self).__name__} with {type(self._data).__name__}>" + + @staticmethod + def _as_datetimeindex(dates: Any, expected_length: int = None) -> Union[pandas.DatetimeIndex, None]: + if dates is None: + return dates + if not isinstance(dates, pandas.DatetimeIndex): + dates = pandas.DatetimeIndex(dates) + if expected_length is not None and expected_length != len(dates): + raise ValueError("Expected size {e} but got {a}: {d}".format(e=expected_length, a=len(dates), d=dates)) + return dates + + @property + def data(self) -> GeoDataFrame: + """ + Get the geopandas.GeoDataFrame that contains the geometry column and any number of attribute columns + + :return: A data frame that contains the geometry column and any number of attribute columns + """ + return self._data + + @property + def start_times(self) -> Union[pandas.DatetimeIndex, None]: + return self._start_times + + @property + def end_times(self) -> Union[pandas.DatetimeIndex, None]: + return self._end_times + + def to_dict(self) -> dict: + """ + Convert this FeatureCollection into a dictionary that can be converted into + a valid JSON representation + """ + data = { + "id": self.id, + "data": shapely.geometry.mapping(self.data), + } + if self.start_times is not None: + data["start_times"] = [t.isoformat() for t in self.start_times] + if self.end_times is not None: + data["end_times"] = [t.isoformat() for t in self.end_times] + return data + + @classmethod + def from_dict(cls, data: dict) -> FeatureCollection: + """ + Create a feature collection from a python dictionary that was created from + the JSON definition of the FeatureCollection + + :param data: The dictionary that contains the feature collection definition + :return: A new FeatureCollection object + """ + return cls( + id=data["id"], + data=GeoDataFrame.from_features(data["data"]), + start_times=data.get("start_times"), + end_times=data.get("end_times"), + ) diff --git a/lib/openeo/udf/run_code.py b/lib/openeo/udf/run_code.py new file mode 100644 index 000000000..6c0657dd1 --- /dev/null +++ b/lib/openeo/udf/run_code.py @@ -0,0 +1,328 @@ +""" + +Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) +""" + +import functools +import inspect +import logging +import math +import pathlib +import re +from typing import Callable, List, Union + +import numpy +import pandas +import shapely +import xarray +from pandas import Series + +import openeo +from openeo import UDF +from openeo.udf import OpenEoUdfException +from openeo.udf._compat import tomllib +from openeo.udf.feature_collection import FeatureCollection +from openeo.udf.structured_data import StructuredData +from openeo.udf.udf_data import UdfData +from openeo.udf.xarraydatacube import XarrayDataCube + +_log = logging.getLogger(__name__) + + +def _build_default_execution_context(): + # TODO: is it really necessary to "pre-load" these modules? Isn't user going to import them explicitly in their script anyway? + context = { + "numpy": numpy, "np": numpy, + "xarray": xarray, + "pandas": pandas, "pd": pandas, + "shapely": shapely, + "math": math, + "UdfData": UdfData, + "XarrayDataCube": XarrayDataCube, + "DataCube": XarrayDataCube, # Legacy alias + "StructuredData": StructuredData, + "FeatureCollection": FeatureCollection, + # "SpatialExtent": SpatialExtent, # TODO? + # "MachineLearnModel": MachineLearnModelConfig, # TODO? + } + + + return context + + +@functools.lru_cache(maxsize=100) +def load_module_from_string(code: str) -> dict: + """ + Experimental: avoid loading same UDF module more than once, to make caching inside the udf work. + @param code: + @return: + """ + globals = _build_default_execution_context() + exec(code, globals) + return globals + + +def _get_annotation_str(annotation: Union[str, type]) -> str: + """Get parameter annotation as a string""" + if isinstance(annotation, str): + return annotation + elif isinstance(annotation, type): + mod = annotation.__module__ + return (mod + "." if mod != str.__module__ else "") + annotation.__name__ + else: + return str(annotation) + + +def _annotation_is_pandas_series(annotation) -> bool: + return annotation in {pandas.Series, _get_annotation_str(pandas.Series)} + + +def _annotation_is_udf_datacube(annotation) -> bool: + return annotation is XarrayDataCube or _get_annotation_str(annotation) in { + _get_annotation_str(XarrayDataCube), + 'openeo_udf.api.datacube.DataCube', # Legacy `openeo_udf` annotation + } + +def _annotation_is_data_array(annotation) -> bool: + return annotation is xarray.DataArray or _get_annotation_str(annotation) in { + _get_annotation_str(xarray.DataArray) + } + +def _annotation_is_udf_data(annotation) -> bool: + return annotation is UdfData or _get_annotation_str(annotation) in { + _get_annotation_str(UdfData), + 'openeo_udf.api.udf_data.UdfData' # Legacy `openeo_udf` annotation + } + + +def _apply_timeseries_xarray(array: xarray.DataArray, callback: Callable[[Series], Series]) -> xarray.DataArray: + """ + Apply timeseries callback to given xarray data array + along its time dimension (named "t" or "time") + + :param array: array to transform + :param callback: function that transforms a timeseries in another (same size) + :return: transformed array + """ + # Make time dimension the last one, and flatten the rest + # to create a 1D sequence of input time series (also 1D). + [time_position] = [i for (i, d) in enumerate(array.dims) if d in ["t", "time"]] + input_series = numpy.moveaxis(array.values, time_position, -1) + orig_shape = input_series.shape + input_series = input_series.reshape((-1, input_series.shape[-1])) + + applied = numpy.asarray([callback(s) for s in input_series]) + + # Reshape to original shape + applied = applied.reshape(orig_shape) + applied = numpy.moveaxis(applied, -1, time_position) + assert applied.shape == array.shape + + return xarray.DataArray(applied, coords=array.coords, dims=array.dims, name=array.name) + + +def apply_timeseries_generic( + udf_data: UdfData, + callback: Callable[[Series, dict], Series] +) -> UdfData: + """ + Implements the UDF contract by calling a user provided time series transformation function. + + :param udf_data: + :param callback: callable that takes a pandas Series and context dict and returns a pandas Series. + See template :py:func:`openeo.udf.udf_signatures.apply_timeseries` + :return: + """ + callback = functools.partial(callback, context=udf_data.user_context) + datacubes = [ + XarrayDataCube(_apply_timeseries_xarray(array=cube.array, callback=callback)) + for cube in udf_data.get_datacube_list() + ] + # Insert the new tiles as list of raster collection tiles in the input object. The new tiles will + # replace the original input tiles. + udf_data.set_datacube_list(datacubes) + return udf_data + + +def run_udf_code(code: str, data: UdfData) -> UdfData: + # TODO: current implementation uses first match directly, first check for multiple matches? + module = load_module_from_string(code) + functions = ((k, v) for (k, v) in module.items() if callable(v)) + + for (fn_name, func) in functions: + try: + sig = inspect.signature(func) + except ValueError: + continue + params = sig.parameters + first_param = next(iter(params.values()), None) + + if ( + fn_name == 'apply_timeseries' + and 'series' in params and 'context' in params + and _annotation_is_pandas_series(params["series"].annotation) + and _annotation_is_pandas_series(sig.return_annotation) + ): + _log.info("Found timeseries mapping UDF `{n}` {f!r}".format(n=fn_name, f=func)) + return apply_timeseries_generic(data, func) + elif ( + fn_name in ['apply_hypercube', 'apply_datacube'] + and 'cube' in params and 'context' in params + and _annotation_is_udf_datacube(params["cube"].annotation) + and _annotation_is_udf_datacube(sig.return_annotation) + ): + _log.info("Found datacube mapping UDF `{n}` {f!r}".format(n=fn_name, f=func)) + if len(data.get_datacube_list()) != 1: + raise ValueError("The provided UDF expects exactly one datacube, but {c} were provided.".format( + c=len(data.get_datacube_list()) + )) + # TODO: also support calls without user context? + result_cube = func(cube=data.get_datacube_list()[0], context=data.user_context) + data.set_datacube_list([result_cube]) + return data + elif ( + fn_name in ['apply_datacube'] + and 'cube' in params and 'context' in params + and _annotation_is_data_array(params["cube"].annotation) + and _annotation_is_data_array(sig.return_annotation) + ): + _log.info("Found datacube mapping UDF `{n}` {f!r}".format(n=fn_name, f=func)) + if len(data.get_datacube_list()) != 1: + raise ValueError("The provided UDF expects exactly one datacube, but {c} were provided.".format( + c=len(data.get_datacube_list()) + )) + # TODO: also support calls without user context? + result_cube: xarray.DataArray = func(cube=data.get_datacube_list()[0].get_array(), context=data.user_context) + data.set_datacube_list([XarrayDataCube(result_cube)]) + return data + elif ( + fn_name in ["apply_vectorcube"] + and "geometries" in params + and _get_annotation_str(params["geometries"].annotation) == "geopandas.geodataframe.GeoDataFrame" + and "cube" in params + and _annotation_is_data_array(params["cube"].annotation) + ): + if data.get_feature_collection_list is None or data.get_datacube_list() is None: + raise ValueError( + "The provided UDF expects a FeatureCollection and a datacube, but received {f} and {c}".format( + f=data.get_feature_collection_list(), c=data.get_datacube_list() + ) + ) + if len(data.get_feature_collection_list()) != 1: + raise ValueError( + "The provided UDF expects exactly one FeatureCollection, but {c} were provided.".format( + c=len(data.get_feature_collection_list()) + ) + ) + if len(data.get_datacube_list()) != 1: + raise ValueError( + "The provided UDF expects exactly one datacube, but {c} were provided.".format( + c=len(data.get_datacube_list()) + ) + ) + # TODO: geopandas is optional dependency. + input_geoms = data.get_feature_collection_list()[0].data + input_cube = data.get_datacube_list()[0].get_array() + result_geoms, result_cube = func(geometries=input_geoms, cube=input_cube, context=data.user_context) + data.set_datacube_list([XarrayDataCube(result_cube)]) + data.set_feature_collection_list([FeatureCollection(id="udf_result", data=result_geoms)]) + return data + elif len(params) == 1 and _annotation_is_udf_data(first_param.annotation): + _log.info("Found generic UDF `{n}` {f!r}".format(n=fn_name, f=func)) + func(data) + return data + + raise OpenEoUdfException("No UDF found.") + + +def execute_local_udf(udf: Union[str, openeo.UDF], datacube: Union[str, xarray.DataArray, XarrayDataCube], fmt='netcdf'): + """ + Locally executes an user defined function on a previously downloaded datacube. + + :param udf: the code of the user defined function + :param datacube: the path to the downloaded data in disk or a DataCube + :param fmt: format of the file if datacube is string + :return: the resulting DataCube + """ + if isinstance(udf, openeo.UDF): + udf = udf.code + + if isinstance(datacube, (str, pathlib.Path)): + d = XarrayDataCube.from_file(path=datacube, fmt=fmt) + elif isinstance(datacube, XarrayDataCube): + d = datacube + elif isinstance(datacube, xarray.DataArray): + d = XarrayDataCube(datacube) + else: + raise ValueError(datacube) + d_array = d.get_array() + expected_order = ("t", "bands", "y", "x") + dims = [d for d in expected_order if d in d_array.dims] + + # TODO #472: skip going through XarrayDataCube above, we only need xarray.DataArray here anyway. + d = XarrayDataCube( + d_array.transpose(*dims) + # TODO: this float conversion was in original implementation (0962e00e03) but is that actually necessary? + .astype(numpy.float64) + ) + # wrap to udf_data + udf_data = UdfData(datacube_list=[d]) + + # TODO: enrich to other types like time series, vector data,... probalby by adding named arguments + # signature: UdfData(proj, datacube_list, feature_collection_list, structured_data_list, ml_model_list, metadata) + + # run the udf through the same routine as it would have been parsed in the backend + result = run_udf_code(udf, udf_data) + return result + + +def extract_udf_dependencies(udf: Union[str, UDF]) -> Union[List[str], None]: + """ + Extract dependencies from UDF code declared in a top-level comment block + following the `inline script metadata specification (PEP 508) `_. + + Basic example UDF snippet declaring expected dependencies as embedded metadata + in a comment block: + + .. code-block:: python + + # /// script + # dependencies = [ + # "geojson", + # ] + # /// + + import geojson + + def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + ... + + .. seealso:: :ref:`python-udf-dependency-declaration` for more in-depth information. + + :param udf: UDF code as a string or :py:class:`~openeo.rest._datacube.UDF` object + :return: List of extracted dependencies or ``None`` when no valid metadata block with dependencies was found. + + .. versionadded:: 0.30.0 + """ + udf_code = udf.code if isinstance(udf, UDF) else udf + + # Extract "script" blocks + script_type = "script" + block_regex = re.compile( + r"^# /// (?P[a-zA-Z0-9-]+)\s*$\s(?P(^#(| .*)$\s)+)^# ///$", flags=re.MULTILINE + ) + script_blocks = [ + match.group("content") for match in block_regex.finditer(udf_code) if match.group("type") == script_type + ] + + if len(script_blocks) > 1: + raise ValueError(f"Multiple {script_type!r} blocks found in top-level comment") + elif len(script_blocks) == 0: + return None + + # Extract dependencies from "script" block + content = "".join( + line[2:] if line.startswith("# ") else line[1:] for line in script_blocks[0].splitlines(keepends=True) + ) + + return tomllib.loads(content).get("dependencies") diff --git a/lib/openeo/udf/structured_data.py b/lib/openeo/udf/structured_data.py new file mode 100644 index 000000000..038bb37be --- /dev/null +++ b/lib/openeo/udf/structured_data.py @@ -0,0 +1,47 @@ +""" + +""" + +# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) + +from __future__ import annotations + +import builtins +from typing import Union + + +class StructuredData: + """ + This class represents structured data that is produced by an UDF and can not be represented + as a raster or vector data cube. For example: the result of a statistical + computation. + + Usage example:: + + >>> StructuredData([3, 5, 8, 13]) + >>> StructuredData({"mean": 5, "median": 8}) + >>> StructuredData([('col_1', 'col_2'), (1, 2), (2, 3)], type="table") + """ + + def __init__(self, data: Union[list, dict], description: str = None, type: str = None): + self.data = data + self.type = type or builtins.type(data).__name__ + self.description = description or self.type + + def __repr__(self): + return f"<{type(self).__name__} with {self.type}>" + + def to_dict(self) -> dict: + return dict( + data=self.data, + description=self.description, + type=self.type, + ) + + @classmethod + def from_dict(cls, data: dict) -> StructuredData: + return cls( + data=data["data"], + description=data.get("description"), + type=data.get("type") + ) diff --git a/lib/openeo/udf/udf_data.py b/lib/openeo/udf/udf_data.py new file mode 100644 index 000000000..e07ccdf8b --- /dev/null +++ b/lib/openeo/udf/udf_data.py @@ -0,0 +1,135 @@ +""" + +""" + +# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) + +from __future__ import annotations + +from typing import List, Optional, Union + +from openeo.udf.feature_collection import FeatureCollection +from openeo.udf.structured_data import StructuredData +from openeo.udf.xarraydatacube import XarrayDataCube + + +class UdfData: + """ + Container for data passed to a user defined function (UDF) + """ + + # TODO: original implementation in `openeo_udf` project had `get_datacube_by_id`, `get_feature_collection_by_id`: is it still useful to provide this? + # TODO: original implementation in `openeo_udf` project had `server_context`: is it still useful to provide this? + + def __init__( + self, + proj: dict = None, + datacube_list: Optional[List[XarrayDataCube]] = None, + feature_collection_list: Optional[List[FeatureCollection]] = None, + structured_data_list: Optional[List[StructuredData]] = None, + user_context: Optional[dict] = None, + ): + """ + The constructor of the UDF argument class that stores all data required by the + user defined function. + + :param proj: A dictionary of form {"proj type string": "projection description"} e.g. {"EPSG": 4326} + :param datacube_list: A list of data cube objects + :param feature_collection_list: A list of VectorTile objects + :param structured_data_list: A list of structured data objects + """ + self.datacube_list = datacube_list + self.feature_collection_list = feature_collection_list + self.structured_data_list = structured_data_list + self.proj = proj + self._user_context = user_context or {} + + def __repr__(self) -> str: + fields = " ".join( + f"{f}:{getattr(self, f)!r}" for f in + ["datacube_list", "feature_collection_list", "structured_data_list"] + ) + return f"<{type(self).__name__} {fields}>" + + @property + def user_context(self) -> dict: + """Return the user context that was passed to the run_udf function""" + return self._user_context + + def get_datacube_list(self) -> Union[List[XarrayDataCube], None]: + """Get the data cube list""" + return self._datacube_list + + def set_datacube_list(self, datacube_list: Union[List[XarrayDataCube], None]): + """ + Set the data cube list + + :param datacube_list: A list of data cubes + """ + self._datacube_list = datacube_list + + datacube_list = property(fget=get_datacube_list, fset=set_datacube_list) + + def get_feature_collection_list(self) -> Union[List[FeatureCollection], None]: + """get all feature collections as list""" + return self._feature_collection_list + + def set_feature_collection_list(self, feature_collection_list: Union[List[FeatureCollection], None]): + self._feature_collection_list = feature_collection_list + + feature_collection_list = property(fget=get_feature_collection_list, fset=set_feature_collection_list) + + def get_structured_data_list(self) -> Union[List[StructuredData], None]: + """ + Get all structured data entries + + :return: A list of StructuredData objects + """ + return self._structured_data_list + + def set_structured_data_list(self, structured_data_list: Union[List[StructuredData], None]): + """ + Set the list of structured data + + :param structured_data_list: A list of StructuredData objects + """ + self._structured_data_list = structured_data_list + + structured_data_list = property(fget=get_structured_data_list, fset=set_structured_data_list) + + def to_dict(self) -> dict: + """ + Convert this UdfData object into a dictionary that can be converted into + a valid JSON representation + """ + return { + "datacubes": [x.to_dict() for x in self.datacube_list] \ + if self.datacube_list else None, + "feature_collection_list": [x.to_dict() for x in self.feature_collection_list] \ + if self.feature_collection_list else None, + "structured_data_list": [x.to_dict() for x in self.structured_data_list] \ + if self.structured_data_list else None, + "proj": self.proj, + "user_context": self.user_context, + } + + @classmethod + def from_dict(cls, udf_dict: dict) -> UdfData: + """ + Create a udf data object from a python dictionary that was created from + the JSON definition of the UdfData class + + :param udf_dict: The dictionary that contains the udf data definition + """ + + datacubes = [XarrayDataCube.from_dict(x) for x in udf_dict.get("datacubes", [])] + feature_collection_list = [FeatureCollection.from_dict(x) for x in udf_dict.get("feature_collection_list", [])] + structured_data_list = [StructuredData.from_dict(x) for x in udf_dict.get("structured_data_list", [])] + udf_data = cls( + proj=udf_dict.get("proj"), + datacube_list=datacubes, + feature_collection_list=feature_collection_list, + structured_data_list=structured_data_list, + user_context=udf_dict.get("user_context") + ) + return udf_data diff --git a/lib/openeo/udf/udf_signatures.py b/lib/openeo/udf/udf_signatures.py new file mode 100644 index 000000000..7afe36a6a --- /dev/null +++ b/lib/openeo/udf/udf_signatures.py @@ -0,0 +1,109 @@ +""" +This module defines a number of function signatures that can be implemented by UDF's. +Both the name of the function and the argument types are/can be used by the backend to validate if the provided UDF +is compatible with the calling context of the process graph in which it is used. + +""" +# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) + +import xarray +from pandas import Series + +from openeo.metadata import CollectionMetadata +from openeo.udf.udf_data import UdfData +from openeo.udf.xarraydatacube import XarrayDataCube + +try: + # Geopandas is an optional dependency, but one of the signatures uses it as type annotation + import geopandas +except ImportError: + pass + + +def apply_timeseries(series: Series, context: dict) -> Series: + """ + Process a timeseries of values, without changing the time instants. + + This can for instance be used for smoothing or gap-filling. + + :param series: A Pandas Series object with a date-time index. + :param context: A dictionary containing user context. + :return: A Pandas Series object with the same datetime index. + """ + # TODO: do we need geospatial coordinates for the series? + return series + + +def apply_datacube(cube: XarrayDataCube, context: dict) -> XarrayDataCube: + """ + Map a :py:class:`XarrayDataCube` to another :py:class:`XarrayDataCube`. + + Depending on the context in which this function is used, the :py:class:`XarrayDataCube` dimensions + have to be retained or can be chained. + For instance, in the context of a reducing operation along a dimension, + that dimension will have to be reduced to a single value. + In the context of a 1 to 1 mapping operation, all dimensions have to be retained. + + :param cube: input data cube + :param context: A dictionary containing user context. + :return: output data cube + """ + return cube + + +def apply_udf_data(data: UdfData): + """ + Generic UDF function that directly manipulates a :py:class:`UdfData` object + + :param data: :py:class:`UdfData` object to manipulate in-place + """ + pass + + +def apply_metadata(metadata: CollectionMetadata, context: dict) -> CollectionMetadata: + """ + .. warning:: + This signature is not yet fully standardized and subject to change. + + Returns the expected cube metadata, after applying this UDF, based on input metadata. + The provided metadata represents the whole raster or vector cube. This function does not need to be called for every data chunk. + + When this function is not implemented by the UDF, the backend may still be able to infer correct metadata by running the + UDF, but this can result in reduced performance or errors. + + This function does not need to be provided when using the UDF in combination with processes that by design have a clear + effect on cube metadata, such as :py:meth:`~openeo.rest.datacube.DataCube.reduce_dimension()` + + :param metadata: the collection metadata of the input data cube + :param context: A dictionary containing user context. + + :return: output metadata: the expected metadata of the cube, after applying the udf + + Examples + -------- + + An example for a UDF that is applied on the 'bands' dimension, and returns a new set of bands with different labels. + + >>> def apply_metadata(metadata: CollectionMetadata, context: dict) -> CollectionMetadata: + ... return metadata.rename_labels( + ... dimension="bands", + ... target=["computed_band_1", "computed_band_2"] + ... ) + + """ + pass + + +def apply_vectorcube( + geometries: "geopandas.geodataframe.GeoDataFrame", cube: xarray.DataArray, context: dict +) -> ("geopandas.geodataframe.GeoDataFrame", xarray.DataArray): + """ + Map a vector cube to another vector cube. + + :param geometries: input geometries as a geopandas.GeoDataFrame. This contains the actual shapely geometries and optional properties. + :param cube: a data cube with dimensions (geometries, time, bands) where time and bands are optional. + The coordinates for the geometry dimension are integers and match the index of the geometries in the geometries parameter. + :param context: A dictionary containing user context. + :return: output geometries, output data cube + """ + pass diff --git a/lib/openeo/udf/xarraydatacube.py b/lib/openeo/udf/xarraydatacube.py new file mode 100644 index 000000000..05dd5311d --- /dev/null +++ b/lib/openeo/udf/xarraydatacube.py @@ -0,0 +1,381 @@ +""" + +""" + +# Note: this module was initially developed under the ``openeo-udf`` project (https://github.com/Open-EO/openeo-udf) + +from __future__ import annotations + +import collections +import json +import typing +from pathlib import Path +from typing import Optional, Union + +import numpy +import xarray + +from openeo.udf import OpenEoUdfException +from openeo.util import deep_get, dict_no_none + +if typing.TYPE_CHECKING: + # Imports for type checking only (circular import issue at runtime). + import matplotlib.colors + + +class XarrayDataCube: + """ + This is a thin wrapper around :py:class:`xarray.DataArray` + providing a basic "DataCube" interface for openEO UDF usage around multi-dimensional data. + """ + + # TODO #472 This class, just wrapping an array.DataArray, seems to make things more complicated/confusing than necessary. + + def __init__(self, array: xarray.DataArray): + if not isinstance(array, xarray.DataArray): + raise OpenEoUdfException("Argument data must be of type xarray.DataArray") + self._array = array + + def __repr__(self): + return f"<{type(self).__name__} shape:{self._array.shape}>" + + def get_array(self) -> xarray.DataArray: + """ + Get the :py:class:`xarray.DataArray` that contains the data and dimension definition + """ + return self._array + + array = property(fget=get_array) + + @property + def id(self): + return self._array.name + + def to_dict(self) -> dict: + """ + Convert this hypercube into a dictionary that can be converted into + a valid JSON representation + + >>> example = { + ... "id": "test_data", + ... "data": [ + ... [[0.0, 0.1], [0.2, 0.3]], + ... [[0.0, 0.1], [0.2, 0.3]], + ... ], + ... "dimension": [ + ... {"name": "time", "coordinates": ["2001-01-01", "2001-01-02"]}, + ... {"name": "X", "coordinates": [50.0, 60.0]}, + ... {"name": "Y"}, + ... ], + ... } + """ + xd = self._array.to_dict() + return dict_no_none({ + "id": xd.get("name"), + "data": xd.get("data"), + "description": deep_get(xd, "attrs", "description", default=None), + "dimensions": [ + dict_no_none( + name=dim, + coordinates=deep_get(xd, "coords", dim, "data", default=None) + ) + for dim in xd.get("dims", []) + ] + }) + + @classmethod + def from_dict(cls, xdc_dict: dict) -> XarrayDataCube: + """ + Create a :py:class:`XarrayDataCube` from a Python dictionary that was created from + the JSON definition of the data cube + + :param data: The dictionary that contains the data cube definition + """ + + if "data" not in xdc_dict: + raise OpenEoUdfException("Missing data in dictionary") + + data = numpy.asarray(xdc_dict["data"]) + + if "dimensions" in xdc_dict: + dims = [dim["name"] for dim in xdc_dict["dimensions"]] + coords = {dim["name"]: dim["coordinates"] for dim in xdc_dict["dimensions"] if "coordinates" in dim} + else: + dims = None + coords = None + + x = xarray.DataArray(data, dims=dims, coords=coords, name=xdc_dict.get("id")) + + if "description" in xdc_dict: + x.attrs["description"] = xdc_dict["description"] + + return cls(array=x) + + @staticmethod + def _guess_format(path: Union[str, Path]) -> str: + """Guess file format from file name.""" + suffix = Path(path).suffix.lower() + if suffix in [".nc", ".netcdf"]: + return "netcdf" + elif suffix in [".json"]: + return "json" + else: + raise ValueError("Can not guess format of {p}".format(p=path)) + + @classmethod + def from_file(cls, path: Union[str, Path], fmt=None, **kwargs) -> XarrayDataCube: + """ + Load data file as :py:class:`XarrayDataCube` in memory + + :param path: the file on disk + :param fmt: format to load from, e.g. "netcdf" or "json" + (will be auto-detected when not specified) + + :return: loaded data cube + """ + fmt = fmt or cls._guess_format(path) + if fmt.lower() == 'netcdf': + return cls(array=XarrayIO.from_netcdf_file(path=path, **kwargs)) + elif fmt.lower() == 'json': + return cls(array=XarrayIO.from_json_file(path=path)) + else: + raise ValueError("invalid format {f}".format(f=fmt)) + + def save_to_file(self, path: Union[str, Path], fmt=None, **kwargs): + """ + Store :py:class:`XarrayDataCube` to file + + :param path: destination file on disk + :param fmt: format to save as, e.g. "netcdf" or "json" + (will be auto-detected when not specified) + """ + fmt = fmt or self._guess_format(path) + if fmt.lower() == 'netcdf': + XarrayIO.to_netcdf_file(array=self.get_array(), path=path, **kwargs) + elif fmt.lower() == 'json': + XarrayIO.to_json_file(array=self.get_array(), path=path) + else: + raise ValueError(fmt) + + def plot( + self, + title: str = None, + limits=None, + show_bandnames: bool = True, + show_dates: bool = True, + show_axeslabels: bool = False, + fontsize: float = 10., + oversample: float = 1, + cmap: Union[str, 'matplotlib.colors.Colormap'] = 'RdYlBu_r', + cbartext: str = None, + to_file: str = None, + to_show: bool = True + ): + """ + Visualize a :py:class:`XarrayDataCube` with matplotlib + + :param datacube: data to plot + :param title: title text drawn in the top left corner (default: nothing) + :param limits: range of the contour plot as a tuple(min,max) (default: None, in which case the min/max is computed from the data) + :param show_bandnames: whether to plot the column names (default: True) + :param show_dates: whether to show the dates for each row (default: True) + :param show_axeslabels: whether to show the labels on the axes (default: False) + :param fontsize: font size in pixels (default: 10) + :param oversample: one value is plotted into oversample x oversample number of pixels (default: 1 which means each value is plotted as a single pixel) + :param cmap: built-in matplotlib color map name or ColorMap object (default: RdYlBu_r which is a blue-yellow-red rainbow) + :param cbartext: text on top of the legend (default: nothing) + :param to_file: filename to save the image to (default: None, which means no file is generated) + :param to_show: whether to show the image in a matplotlib window (default: True) + + :return: None + """ + from matplotlib import pyplot + + data = self.get_array() + if limits is None: + vmin = data.min() + vmax = data.max() + else: + vmin = limits[0] + vmax = limits[1] + + # fill bands and t if missing + if 'bands' not in data.dims: + data = data.expand_dims(dim={'bands': ['band0']}) + if 't' not in data.dims: + data = data.expand_dims(dim={'t': [numpy.datetime64('today')]}) + if 'bands' not in data.coords: + data['bands'] = ['band0'] + if 't' not in data.coords: + data['t'] = [numpy.datetime64('today')] + + # align with plot + data = data.transpose('t', 'bands', 'y', 'x') + dpi = 100 + xres = len(data.x) / dpi + yres = len(data.y) / dpi + fs = fontsize / oversample + frame = 0.33 + + nrow = data.shape[0] + ncol = data.shape[1] + + fig = pyplot.figure(figsize=((ncol + frame) * xres * 1.1, (nrow + frame) * yres), dpi=int(dpi * oversample)) + gs = pyplot.GridSpec(nrow, ncol, wspace=0., hspace=0., top=nrow / (nrow + frame), bottom=0., + left=frame / (ncol + frame), right=1.) + + xmin = data.x.min() + xmax = data.x.max() + ymin = data.y.min() + ymax = data.y.max() + + # flip around if incorrect, this is in harmony with origin='lower' + if (data.x[0] > data.x[-1]): + data = data.reindex(x=list(reversed(data.x))) + if (data.y[0] > data.y[-1]): + data = data.reindex(y=list(reversed(data.y))) + + extent = (data.x[0], data.x[-1], data.y[0], data.y[-1]) + + for i in range(nrow): + for j in range(ncol): + im = data[i, j] + ax = pyplot.subplot(gs[i, j]) + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) + img = ax.imshow(im, vmin=vmin, vmax=vmax, cmap=cmap, origin='lower', extent=extent) + ax.xaxis.set_tick_params(labelsize=fs) + ax.yaxis.set_tick_params(labelsize=fs) + if not show_axeslabels: + ax.set_axis_off() + ax.set_xticklabels([]) + ax.set_yticklabels([]) + if show_bandnames: + if i == 0: ax.text(0.5, 1.08, data.bands.values[j] + " (" + str(data.dtype) + ")", size=fs, + va="center", + ha="center", transform=ax.transAxes) + if show_dates: + if j == 0: ax.text(-0.08, 0.5, data.t.dt.strftime("%Y-%m-%d").values[i], size=fs, va="center", + ha="center", rotation=90, transform=ax.transAxes) + + if title is not None: + fig.text(0., 1., title.split('/')[-1], size=fs, va="top", ha="left", weight='bold') + + cbar_ax = fig.add_axes([0.01, 0.1, 0.04, 0.5]) + if cbartext is not None: + fig.text(0.06, 0.62, cbartext, size=fs, va="bottom", ha="center") + cbar = fig.colorbar(img, cax=cbar_ax) + cbar.ax.tick_params(labelsize=fs) + cbar.outline.set_visible(False) + cbar.ax.tick_params(size=0) + cbar.ax.yaxis.set_tick_params(pad=0) + + if to_file is not None: + pyplot.savefig(str(to_file)) + if to_show: + pyplot.show() + + pyplot.close() + + +class XarrayIO: + """ + Helpers to load/store :py:cass:`xarray.DataArray` objects, + with some conventions about expected dimensions/bands + """ + + @classmethod + def from_json_file(cls, path: Union[str, Path]) -> xarray.DataArray: + with Path(path).open() as f: + return cls.from_json(json.load(f)) + + @classmethod + def from_json(cls, d: dict) -> xarray.DataArray: + d['data'] = numpy.array(d['data'], dtype=numpy.dtype(d['attrs']['dtype'])) + for k, v in d['coords'].items(): + # prepare coordinate + d['coords'][k]['data'] = numpy.array(v['data'], dtype=v['attrs']['dtype']) + # remove dtype and shape, because that is included for helping the user + if d['coords'][k].get('attrs', None) is not None: + d['coords'][k]['attrs'].pop('dtype', None) + d['coords'][k]['attrs'].pop('shape', None) + + # remove dtype and shape, because that is included for helping the user + if d.get('attrs', None) is not None: + d['attrs'].pop('dtype', None) + d['attrs'].pop('shape', None) + # convert to xarray + r = xarray.DataArray.from_dict(d) + + # build dimension list in proper order + dims = list(filter(lambda i: i != 't' and i != 'bands' and i != 'x' and i != 'y', r.dims)) + if 't' in r.dims: dims += ['t'] + if 'bands' in r.dims: dims += ['bands'] + if 'x' in r.dims: dims += ['x'] + if 'y' in r.dims: dims += ['y'] + # return the resulting data array + return r.transpose(*dims) + + @classmethod + def from_netcdf_file(cls, path: Union[str, Path], engine: Optional[str] = None) -> xarray.DataArray: + # load the dataset and convert to data array + ds = xarray.open_dataset(path, engine=engine) + + # Skip non-numerical variables (like "crs") + band_vars = [k for k, v in ds.data_vars.items() if v.dtype.kind in {"b", "i", "u", "f"} and len(v.dims) > 0] + ds = ds[band_vars] + + r = ds.to_array(dim='bands') + + # Reorder dims to proper order (t-bands-x-y at the end) + expected_order = ("t", "bands", "x", "y") + dims = [d for d in r.dims if d not in expected_order] + [d for d in expected_order if d in r.dims] + + return r.transpose(*dims) + + @classmethod + def to_json_file(cls, array: xarray.DataArray, path: Union[str, Path]): + # to deserialized json + jsonarray = array.to_dict() + # add attributes that needed for re-creating xarray from json + jsonarray['attrs']['dtype'] = str(array.values.dtype) + jsonarray['attrs']['shape'] = list(array.values.shape) + for i in array.coords.values(): + jsonarray['coords'][i.name]['attrs']['dtype'] = str(i.dtype) + jsonarray['coords'][i.name]['attrs']['shape'] = list(i.shape) + # custom print so resulting json file is humanly easy to read + # TODO: make this human friendly JSON format optional and allow compact JSON too. + with Path(path).open("w", encoding="utf-8") as f: + def custom_print(data_structure, indent=1): + f.write("{\n") + needs_comma = False + for key, value in data_structure.items(): + if needs_comma: + f.write(',\n') + needs_comma = True + f.write(' ' * indent + json.dumps(key) + ':') + if isinstance(value, dict): + custom_print(value, indent + 1) + else: + json.dump(value, f, default=str, separators=(',', ':')) + f.write('\n' + ' ' * (indent - 1) + "}") + + custom_print(jsonarray) + + @classmethod + def to_netcdf_file(cls, array: xarray.DataArray, path: Union[str, Path], engine: Optional[str] = None): + # temp reference to avoid modifying the original array + result = array + # rearrange in a basic way because older xarray versions have a bug and ellipsis don't work in xarray.transpose() + if result.dims[-2] == 'x' and result.dims[-1] == 'y': + l = list(result.dims[:-2]) + result = result.transpose(*(l + ['y', 'x'])) + # turn it into a dataset where each band becomes a variable + if not 'bands' in result.dims: + result = result.expand_dims(dim=collections.OrderedDict({'bands': ['band_0']})) + else: + if not 'bands' in result.coords: + labels = ['band_' + str(i) for i in range(result.shape[result.dims.index('bands')])] + result = result.assign_coords(bands=labels) + result = result.to_dataset('bands') + result.to_netcdf(path, engine=engine) diff --git a/lib/openeo/util.py b/lib/openeo/util.py new file mode 100644 index 000000000..6bbd4d897 --- /dev/null +++ b/lib/openeo/util.py @@ -0,0 +1,689 @@ +""" +Various utilities and helpers. +""" + +# TODO #465 split this kitchen-sink in thematic submodules + +from __future__ import annotations + +import datetime as dt +import functools +import json +import logging +import re +import sys +import time +from collections import OrderedDict +from enum import Enum +from pathlib import Path +from typing import Any, Callable, List, Optional, Tuple, Union +from urllib.parse import urljoin + +import requests +import shapely.geometry.base +from deprecated import deprecated + +try: + # pyproj is an optional dependency + import pyproj +except ImportError: + pyproj = None + + +logger = logging.getLogger(__name__) + + +class Rfc3339: + """ + Formatter for dates according to RFC-3339. + + Parses date(time)-like input and formats according to RFC-3339. Some examples: + + >>> rfc3339.date("2020:03:17") + "2020-03-17" + >>> rfc3339.date(2020, 3, 17) + "2020-03-17" + >>> rfc3339.datetime("2020/03/17/12/34/56") + "2020-03-17T12:34:56Z" + >>> rfc3339.datetime([2020, 3, 17, 12, 34, 56]) + "2020-03-17T12:34:56Z" + >>> rfc3339.datetime(2020, 3, 17) + "2020-03-17T00:00:00Z" + >>> rfc3339.datetime(datetime(2020, 3, 17, 12, 34, 56)) + "2020-03-17T12:34:56Z" + + Or just normalize (automatically preserve date/datetime resolution): + + >>> rfc3339.normalize("2020/03/17") + "2020-03-17" + >>> rfc3339.normalize("2020-03-17-12-34-56") + "2020-03-17T12:34:56Z" + + Also see https://tools.ietf.org/html/rfc3339#section-5.6 + """ + # TODO: currently we hard code timezone 'Z' for simplicity. Add real time zone support? + _FMT_DATE = '%Y-%m-%d' + _FMT_TIME = '%H:%M:%SZ' + _FMT_DATETIME = _FMT_DATE + "T" + _FMT_TIME + + _regex_datetime = re.compile(r""" + ^(?P\d{4})[:/_-](?P\d{2})[:/_-](?P\d{2})[T :/_-]? + (?:(?P\d{2})[:/_-](?P\d{2})(?:[:/_-](?P\d{2}))?)?""", re.VERBOSE) + + def __init__(self, propagate_none: bool = False): + self._propagate_none = propagate_none + + def datetime(self, x: Any, *args) -> Union[str, None]: + """ + Format given date(time)-like object as RFC-3339 datetime string. + """ + if args: + return self.datetime((x,) + args) + elif isinstance(x, dt.datetime): + return self._format_datetime(x) + elif isinstance(x, dt.date): + return self._format_datetime(dt.datetime.combine(x, dt.time())) + elif isinstance(x, str): + return self._format_datetime(dt.datetime(*self._parse_datetime(x))) + elif isinstance(x, (tuple, list)): + return self._format_datetime(dt.datetime(*(int(v) for v in x))) + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + def date(self, x: Any, *args) -> Union[str, None]: + """ + Format given date-like object as RFC-3339 date string. + """ + if args: + return self.date((x,) + args) + elif isinstance(x, (dt.date, dt.datetime)): + return self._format_date(x) + elif isinstance(x, str): + return self._format_date(dt.datetime(*self._parse_datetime(x))) + elif isinstance(x, (tuple, list)): + return self._format_date(dt.datetime(*(int(v) for v in x))) + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + def normalize(self, x: Any, *args) -> Union[str, None]: + """ + Format given date(time)-like object as RFC-3339 date or date-time string depending on given resolution + + >>> rfc3339.normalize("2020/03/17") + "2020-03-17" + >>> rfc3339.normalize("2020/03/17/12/34/56") + "2020-03-17T12:34:56Z" + """ + if args: + return self.normalize((x,) + args) + elif isinstance(x, dt.datetime): + return self.datetime(x) + elif isinstance(x, dt.date): + return self.date(x) + elif isinstance(x, str): + x = self._parse_datetime(x) + return self.date(x) if len(x) <= 3 else self.datetime(x) + elif isinstance(x, (tuple, list)): + return self.date(x) if len(x) <= 3 else self.datetime(x) + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + def parse_date(self, x: Union[str, None]) -> Union[dt.date, None]: + """Parse given string as RFC3339 date.""" + if isinstance(x, str): + return dt.datetime.strptime(x, "%Y-%m-%d").date() + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + def parse_datetime( + self, x: Union[str, None], with_timezone: bool = False + ) -> Union[dt.datetime, None]: + """Parse given string as RFC3339 date-time.""" + if isinstance(x, str): + # TODO: Also support parsing other timezones than UTC (Z) + if re.search(r":\d+\.\d+", x): + res = dt.datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%fZ") + else: + res = dt.datetime.strptime(x, "%Y-%m-%dT%H:%M:%SZ") + if with_timezone: + res = res.replace(tzinfo=dt.timezone.utc) + return res + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + def parse_date_or_datetime( + self, x: Union[str, None], with_timezone: bool = False + ) -> Union[dt.date, dt.datetime, None]: + """Parse given string as RFC3339 date or date-time.""" + if isinstance(x, str): + if len(x) > 10: + return self.parse_datetime(x, with_timezone=with_timezone) + else: + return self.parse_date(x) + elif x is None and self._propagate_none: + return None + raise ValueError(x) + + @classmethod + def _format_datetime(cls, d: dt.datetime) -> str: + """Format given datetime as RFC-3339 date-time string.""" + if d.tzinfo not in {None, dt.timezone.utc}: + # TODO: add support for non-UTC timezones? + raise ValueError(f"No support for non-UTC timezone {d.tzinfo}") + return d.strftime(cls._FMT_DATETIME) + + @classmethod + def _format_date(cls, d: dt.date) -> str: + """Format given datetime as RFC-3339 date-time string.""" + return d.strftime(cls._FMT_DATE) + + @classmethod + def _parse_datetime(cls, s: str) -> Tuple[int]: + """Try to parse string to a date(time) tuple""" + try: + return tuple(int(v) for v in cls._regex_datetime.match(s).groups() if v is not None) + except Exception: + raise ValueError("Can not parse as date: {s}".format(s=s)) + + def today(self) -> str: + """Today (date) in RFC3339 format""" + return self.date(dt.date.today()) + + def utcnow(self) -> str: + """Current UTC datetime in RFC3339 format.""" + # Current time in UTC timezone (instead of naive `datetime.datetime.utcnow()`, per `datetime` documentation) + now = dt.datetime.now(tz=dt.timezone.utc) + return self.datetime(now) + + +# Default RFC3339 date-time formatter +rfc3339 = Rfc3339() + + +@deprecated("Use `rfc3339.normalize`, `rfc3339.date` or `rfc3339.datetime` instead") +def date_to_rfc3339(d: Any) -> str: + """ + Convert date-like object to a RFC 3339 formatted date string + + see https://tools.ietf.org/html/rfc3339#section-5.6 + """ + return rfc3339.normalize(d) + + +def dict_no_none(*args, **kwargs) -> dict: + """ + Helper to build a dict containing given key-value pairs where the value is not None. + """ + return { + k: v + for k, v in dict(*args, **kwargs).items() + if v is not None + } + + +def first_not_none(*args): + """Return first item from given arguments that is not None.""" + for item in args: + if item is not None: + return item + raise ValueError("No not-None values given.") + + +def ensure_dir(path: Union[str, Path]) -> Path: + """Create directory if it doesn't exist.""" + path = Path(path) + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + assert path.is_dir() + return path + + +def ensure_list(x): + """Convert given data structure to a list.""" + try: + return list(x) + except TypeError: + return [x] + + +class ContextTimer: + """ + Context manager to measure the "wall clock" time (in seconds) inside/for a block of code. + + Usage example: + + with ContextTimer() as timer: + # Inside code block: currently elapsed time + print(timer.elapsed()) + + # Outside code block: elapsed time when block ended + print(timer.elapsed()) + + """ + + __slots__ = ["start", "end"] + + # Function that returns current time in seconds (overridable for unit tests) + _clock = time.time + + def __init__(self): + self.start = None + self.end = None + + def elapsed(self) -> float: + """Elapsed time (in seconds) inside or at the end of wrapped context.""" + if self.start is None: + raise RuntimeError("Timer not started.") + if self.end is not None: + # Elapsed time when exiting context. + return self.end - self.start + else: + # Currently elapsed inside context. + return self._clock() - self.start + + def __enter__(self) -> ContextTimer: + self.start = self._clock() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.end = self._clock() + + +class TimingLogger: + """ + Context manager for quick and easy logging of start time, end time and elapsed time of some block of code + + Usage example: + + >>> with TimingLogger("Doing batch job"): + ... do_batch_job() + + At start of the code block the current time will be logged + and at end of the code block the end time and elapsed time will be logged. + + Can also be used as a function/method decorator, for example: + + >>> @TimingLogger("Calculation going on") + ... def add(x, y): + ... return x + y + """ + + # Function that returns current datetime (overridable for unit tests) + _now = dt.datetime.now + + def __init__(self, title: str = "Timing", logger: Union[logging.Logger, str, Callable] = logger): + """ + :param title: the title to use in the logging + :param logger: how the timing should be logged. + Can be specified as a logging.Logger object (in which case the INFO log level will be used), + as a string (name of the logging.Logger object to construct), + or as callable (e.g. to use the `print` function, or the `.debug` method of an existing logger) + """ + self.title = title + if isinstance(logger, str): + logger = logging.getLogger(logger) + if isinstance(logger, (logging.Logger, logging.LoggerAdapter)): + self._log = logger.info + elif callable(logger): + self._log = logger + else: + raise ValueError("Invalid logger {l!r}".format(l=logger)) + + self.start_time = self.end_time = self.elapsed = None + + def __enter__(self): + self.start_time = self._now() + self._log("{t}: start {s}".format(t=self.title, s=self.start_time)) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.end_time = self._now() + self.elapsed = self.end_time - self.start_time + self._log("{t}: {s} {e}, elapsed {d}".format( + t=self.title, + s="fail" if exc_type else "end", + e=self.end_time, d=self.elapsed + )) + + def __call__(self, f: Callable): + """ + Use TimingLogger as function/method decorator + """ + + @functools.wraps(f) + def wrapper(*args, **kwargs): + with self: + return f(*args, **kwargs) + + return wrapper + + +class DeepKeyError(LookupError): + def __init__(self, key, keys): + super(DeepKeyError, self).__init__("{k!r} (from deep key {s!r})".format(k=key, s=keys)) + + +# Sentinel object for `default` argument of `deep_get` +_deep_get_default_undefined = object() + + +def deep_get(data: dict, *keys, default=_deep_get_default_undefined): + """ + Get value deeply from nested dictionaries/lists/tuples + + :param data: nested data structure of dicts, lists, tuples + :param keys: sequence of keys/indexes to traverse + :param default: default value when a key is missing. + By default a DeepKeyError will be raised. + :return: + """ + for key in keys: + if isinstance(data, dict) and key in data: + data = data[key] + elif isinstance(data, (list, tuple)) and isinstance(key, int) and 0 <= key < len(data): + data = data[key] + else: + if default is _deep_get_default_undefined: + raise DeepKeyError(key, keys) + else: + return default + return data + + +def deep_set(data: dict, *keys, value): + """ + Set a value deeply in nested dictionary + + :param data: nested data structure of dicts, lists, tuples + :param keys: sequence of keys/indexes to traverse + :param value: value to set + """ + if len(keys) == 1: + data[keys[0]] = value + elif len(keys) > 1: + if isinstance(data, dict): + deep_set(data.setdefault(keys[0], OrderedDict()), *keys[1:], value=value) + elif isinstance(data, (list, tuple)): + deep_set(data[keys[0]], *keys[1:], value=value) + else: + ValueError(data) + else: + raise ValueError("No keys given") + + +def guess_format(filename: Union[str, Path]) -> Union[str, None]: + """ + Guess the output format from a given filename and return the corrected format. + Any names not in the dict get passed through. + """ + extension = Path(filename).suffix + if not extension: + return None + extension = extension[1:].lower() + + format_map = { + "gtiff": "GTiff", + "geotiff": "GTiff", + "geotif": "GTiff", + "tiff": "GTiff", + "tif": "GTiff", + "nc": "netCDF", + "netcdf": "netCDF", + "geojson": "GeoJSON", + } + + return format_map.get(extension, extension.upper()) + + +def load_json(path: Union[Path, str]) -> dict: + with Path(path).open("r", encoding="utf-8") as f: + return json.load(f) + + +def load_json_resource(src: Union[str, Path]) -> dict: + """ + Helper to load some kind of JSON resource + + :param src: a JSON resource: a raw JSON string, + a path to (local) JSON file, or a URL to a remote JSON resource + :return: data structured parsed from JSON + """ + if isinstance(src, str) and src.strip().startswith("{"): + # Assume source is a raw JSON string + return json.loads(src) + elif isinstance(src, str) and re.match(r"^https?://", src, flags=re.I): + # URL to remote JSON resource + return requests.get(src).json() + elif isinstance(src, Path) or (isinstance(src, str) and src.endswith(".json")): + # Assume source is a local JSON file path + return load_json(src) + raise ValueError(src) + + +class LazyLoadCache: + """Simple cache that allows to (lazy) load on cache miss.""" + + def __init__(self): + self._cache = {} + + def get(self, key: Union[str, tuple], load: Callable[[], Any]): + if key not in self._cache: + self._cache[key] = load() + return self._cache[key] + + +def str_truncate(text: str, width: int = 64, ellipsis: str = "...") -> str: + """Shorten a string (with an ellipsis) if it is longer than certain length.""" + width = max(0, int(width)) + if len(text) <= width: + return text + if len(ellipsis) > width: + ellipsis = ellipsis[:width] + return text[:max(0, (width - len(ellipsis)))] + ellipsis + + +def repr_truncate(obj: Any, width: int = 64, ellipsis: str = "...") -> str: + """Do `repr` rendering of an object, but truncate string if it is too long .""" + if isinstance(obj, str) and width > len(ellipsis) + 2: + # Special case: put ellipsis inside quotes + return repr(str_truncate(text=obj, width=width - 2, ellipsis=ellipsis)) + else: + # General case: just put ellipsis at end + return str_truncate(text=repr(obj), width=width, ellipsis=ellipsis) + + +def in_interactive_mode() -> bool: + """Detect if we are running in interactive mode (Jupyter/IPython/repl)""" + # Based on https://stackoverflow.com/a/64523765 + return hasattr(sys, "ps1") + + +class InvalidBBoxException(ValueError): + pass + + +class BBoxDict(dict): + """ + Dictionary based helper to easily create/work with bounding box dictionaries + (having keys "west", "south", "east", "north", and optionally "crs"). + + :param crs: value describing the coordinate reference system. + Typically just an int (interpreted as EPSG code, e.g. ``4326``) + or a string (handled as authority string, e.g. ``"EPSG:4326"``). + See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument. + + .. versionadded:: 0.10.1 + """ + + def __init__(self, *, west: float, south: float, east: float, north: float, crs: Optional[Union[str, int]] = None): + super().__init__(west=west, south=south, east=east, north=north) + if crs is not None: + self.update(crs=normalize_crs(crs)) + + # TODO: provide west, south, east, north, crs as @properties? Read-only or read-write? + + @classmethod + def from_any(cls, x: Any, *, crs: Optional[str] = None) -> BBoxDict: + if isinstance(x, dict): + if crs and "crs" in x and crs != x["crs"]: + raise InvalidBBoxException(f"Two CRS values specified: {crs} and {x['crs']}") + return cls.from_dict({"crs": crs, **x}) + elif isinstance(x, (list, tuple)): + return cls.from_sequence(x, crs=crs) + elif isinstance(x, shapely.geometry.base.BaseGeometry): + return cls.from_sequence(x.bounds, crs=crs) + # TODO: support other input? E.g.: WKT string, GeoJson-style dictionary (Polygon, FeatureCollection, ...) + else: + raise InvalidBBoxException(f"Can not construct BBoxDict from {x!r}") + + @classmethod + def from_dict(cls, data: dict) -> BBoxDict: + """Build from dictionary with at least keys "west", "south", "east", and "north".""" + expected_fields = {"west", "south", "east", "north"} + # TODO: also support upper case fields? + # TODO: optional support for parameterized bbox fields? + missing = expected_fields.difference(data.keys()) + if missing: + raise InvalidBBoxException(f"Missing bbox fields {sorted(missing)}") + invalid = {k: data[k] for k in expected_fields if not isinstance(data[k], (int, float))} + if invalid: + raise InvalidBBoxException(f"Non-numerical bbox fields {invalid}.") + return cls(west=data["west"], south=data["south"], east=data["east"], north=data["north"], crs=data.get("crs")) + + @classmethod + def from_sequence(cls, seq: Union[list, tuple], crs: Optional[str] = None) -> BBoxDict: + """Build from sequence of 4 bounds (west, south, east and north).""" + if len(seq) != 4: + raise InvalidBBoxException(f"Expected sequence with 4 items, but got {len(seq)}.") + return cls(west=seq[0], south=seq[1], east=seq[2], north=seq[3], crs=crs) + + +def to_bbox_dict(x: Any, *, crs: Optional[Union[str, int]] = None) -> BBoxDict: + """ + Convert given data or object to a bounding box dictionary + (having keys "west", "south", "east", "north", and optionally "crs"). + + Supports various input types/formats: + + - list/tuple (assumed to be in west-south-east-north order) + + >>> to_bbox_dict([3, 50, 4, 51]) + {'west': 3, 'south': 50, 'east': 4, 'north': 51} + + - dictionary (unnecessary items will be stripped) + + >>> to_bbox_dict({ + ... "color": "red", "shape": "triangle", + ... "west": 1, "south": 2, "east": 3, "north": 4, "crs": "EPSG:4326", + ... }) + {'west': 1, 'south': 2, 'east': 3, 'north': 4, 'crs': 'EPSG:4326'} + + - a shapely geometry + + .. versionadded:: 0.10.1 + + :param x: input data that describes west-south-east-north bounds in some way, e.g. as a dictionary, + a list, a tuple, ashapely geometry, ... + :param crs: (optional) CRS field + :return: dictionary (subclass) with keys "west", "south", "east", "north", and optionally "crs". + """ + return BBoxDict.from_any(x=x, crs=crs) + + +def url_join(root_url: str, path: str): + """Join a base url and sub path properly.""" + return urljoin(root_url.rstrip("/") + "/", path.lstrip("/")) + + +def clip(x: float, min: float, max: float) -> float: + """Clip given value between minimum and maximum value""" + return min if x < min else (x if x < max else max) + + +class SimpleProgressBar: + """Simple ASCII-based progress bar helper.""" + + __slots__ = ["width", "bar", "fill", "left", "right"] + + def __init__(self, width: int = 40, *, bar: str = "#", fill: str = "-", left: str = "[", right: str = "]"): + self.width = int(width) + self.bar = bar[0] + self.fill = fill[0] + self.left = left + self.right = right + + def get(self, fraction: float) -> str: + width = self.width - len(self.left) - len(self.right) + bar = self.bar * int(round(width * clip(fraction, min=0, max=1))) + return f"{self.left}{bar:{self.fill}<{width}s}{self.right}" + + +def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]: + """ + Normalize the given value (describing a CRS or Coordinate Reference System) + to an openEO compatible EPSG code (int) or WKT2 CRS string. + + At minimum, the following input values are handled: + + - an integer value (e.g. ``4326``) is interpreted as an EPSG code + - a string that just contains an integer (e.g. ``"4326"``) + or with and additional ``"EPSG:"`` prefix (e.g. ``"EPSG:4326"``) + will also be interpreted as an EPSG value + + Additional support and behavior depends on the availability of the ``pyproj`` library: + + - When available, it will be used for parsing and validation: + everything supported by `pyproj.CRS.from_user_input `_ is allowed. + See the ``pyproj`` docs for more details. + - Otherwise, some best effort validation is done: + EPSG looking integer or string values will be parsed as such as discussed above. + Other strings will be assumed to be WKT2 already. + Other data structures will not be accepted. + + :param crs: value that encodes a coordinate reference system, typically just an int (EPSG code) or string (authority string). + If the ``pyproj`` library is available, everything supported by it is allowed. + + :param use_pyproj: whether ``pyproj`` should be leveraged at all + (mainly useful for testing the "no pyproj available" code path) + + :return: EPSG code as int, or WKT2 string. Or None if input was empty. + + :raises ValueError: + When the given CRS data can not be parsed/converted/normalized. + + """ + if crs in (None, "", {}): + return None + + if pyproj and use_pyproj: + try: + # (if available:) let pyproj do the validation/parsing + crs_obj = pyproj.CRS.from_user_input(crs) + # Convert back to EPSG int or WKT2 string + crs = crs_obj.to_epsg() or crs_obj.to_wkt() + except pyproj.ProjError as e: + raise ValueError(f"Failed to normalize CRS data with pyproj: {crs!r}") from e + else: + # Best effort simple validation/normalization + if isinstance(crs, int) and crs > 0: + # Assume int is already valid EPSG code + pass + elif isinstance(crs, str): + # Parse as EPSG int code if it looks like that, + # otherwise: leave it as-is, assuming it is a valid WKT2 CRS string + if re.match(r"^(epsg:)?\d+$", crs.strip(), flags=re.IGNORECASE): + crs = int(crs.split(":")[-1]) + elif "GEOGCRS[" in crs: + # Very simple WKT2 CRS detection heuristic + logger.warning(f"Assuming this is a valid WK2 CRS string: {repr_truncate(crs)}") + else: + raise ValueError(f"Can not normalize CRS string {repr_truncate(crs)}") + else: + raise ValueError(f"Can not normalize CRS data {type(crs)}") + + return crs diff --git a/machine_learning.html b/machine_learning.html new file mode 100644 index 000000000..fa4a17518 --- /dev/null +++ b/machine_learning.html @@ -0,0 +1,244 @@ + + + + + + + + Machine Learning — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Machine Learning

+
+

Warning

+

This API and documentation is experimental, +under heavy development and subject to change.

+
+
+

Added in version 0.10.0.

+
+
+

Random Forest based Classification and Regression

+

openEO defines a couple of processes for random forest based machine learning +for Earth Observation applications:

+
    +
  • fit_class_random_forest for training a random forest based classification model

  • +
  • fit_regr_random_forest for training a random forest based regression model

  • +
  • predict_random_forest for inference/prediction

  • +
+

The openEO Python Client library provides the necessary functionality to set up +and execute training and inference workflows.

+
+

Training

+

Let’s focus on training a classification model, where we try to predict +a class like a land cover type or crop type based on predictors +we derive from EO data. +For example, assume we have a GeoJSON FeatureCollection +of sample points and a corresponding classification target value as follows:

+
feature_collection = {"type": "FeatureCollection", "features": [
+    {
+        "type": "Feature",
+        "properties": {"id": "b3dw-wd23", "target": 3},
+        "geometry": {"type": "Point", "coordinates": [3.4, 51.1]}
+    },
+    {
+        "type": "Feature",
+        "properties": {"id": "r8dh-3jkd", "target": 5},
+        "geometry": {"type": "Point", "coordinates": [3.6, 51.2]}
+    },
+    ...
+
+
+
+

Note

+

Confusingly, the concept “feature” has somewhat conflicting meanings +for different audiences. GIS/EO people use “feature” to refer to the “rows” +in this feature collection. +For the machine learning community however, the properties (the “columns”) +are the features. +To avoid confusion in this discussion we will avoid the term “feature” +and instead use “sample point” for the former and “predictor” for the latter.

+
+

We first build a datacube of “predictor” bands. +For simplicity, we will just use the raw B02/B03/B04 band values here +and use the temporal mean to eliminate the time dimension:

+
cube = connection.load_collection(
+    "SENTINEL2",
+    temporal_extent=[start, end],
+    spatial_extent=bbox,
+    bands=["B02", "B03", "B04"]
+)
+cube = cube.reduce_dimension(dimension="t", reducer="mean")
+
+
+

We now use aggregate_spatial to sample this raster data cube at the sample points +and get a vector cube where we have the temporal mean of the B02/B03/B04 bands as predictor values:

+
predictors = cube.aggregate_spatial(feature_collection, reducer="mean")
+
+
+

We can now train a Random Forest model by calling the +fit_class_random_forest() method on the predictor vector cube +and passing the original target class data:

+
model = predictors.fit_class_random_forest(
+    target=feature_collection,
+)
+# Save the model as a batch job result asset
+# so that we can load it in another job.
+model = model.save_ml_model()
+
+
+

Finally execute this whole training flow as a batch job:

+
training_job = model.create_job()
+training_job.start_and_wait()
+
+
+
+
+

Inference

+

When the batch job finishes successfully, the trained model can then be used +with the predict_random_forest process on the raster data cube +(or another cube with the same band structure) to classify all the pixels.

+

Technically, the openEO predict_random_forest process has to be used as a reducer function +inside a reduce_dimension call, but the openEO Python client library makes it +a bit easier by providing a predict_random_forest() method +directly on the DataCube class, so that you can just do:

+
predicted = cube.predict_random_forest(
+    model=training_job.job_id,
+    dimension="bands"
+)
+
+predicted.download("predicted.GTiff")
+
+
+

We specified the model here by batch job id (string), +but it can also be specified in other ways: +as BatchJob instance, +as URL to the corresponding STAC Item that implements the ml-model extension, +or as MlModel instance (e.g. loaded through +load_ml_model()).

+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..ab807adf79778420da2a97e1dc0d5f36a94f01b7 GIT binary patch literal 5924 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 GHuE%45R?c2 literal 0 HcmV?d00001 diff --git a/process_mapping.html b/process_mapping.html new file mode 100644 index 000000000..d88fcbc2d --- /dev/null +++ b/process_mapping.html @@ -0,0 +1,609 @@ + + + + + + + + openEO Process Mapping — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

openEO Process Mapping

+

The table below maps openEO processes to the corresponding +method or function in the openEO Python Client Library.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

openEO process

openEO Python Client Method

absolute

ProcessBuilder.absolute(), absolute()

add

ProcessBuilder.__add__(), ProcessBuilder.__radd__(), ProcessBuilder.add(), add(), DataCube.add(), DataCube.__add__(), DataCube.__radd__()

add_dimension

ProcessBuilder.add_dimension(), add_dimension(), DataCube.add_dimension()

aggregate_spatial

ProcessBuilder.aggregate_spatial(), aggregate_spatial(), DataCube.aggregate_spatial()

aggregate_spatial_window

ProcessBuilder.aggregate_spatial_window(), aggregate_spatial_window(), DataCube.aggregate_spatial_window()

aggregate_temporal

ProcessBuilder.aggregate_temporal(), aggregate_temporal(), DataCube.aggregate_temporal()

aggregate_temporal_period

ProcessBuilder.aggregate_temporal_period(), aggregate_temporal_period(), DataCube.aggregate_temporal_period()

all

ProcessBuilder.all(), all()

and

DataCube.logical_and(), DataCube.__and__()

and_

ProcessBuilder.and_(), and_()

anomaly

ProcessBuilder.anomaly(), anomaly()

any

ProcessBuilder.any(), any()

apply

ProcessBuilder.apply(), apply(), DataCube.apply()

apply_dimension

ProcessBuilder.apply_dimension(), apply_dimension(), DataCube.apply_dimension()

apply_kernel

ProcessBuilder.apply_kernel(), apply_kernel(), DataCube.apply_kernel()

apply_neighborhood

ProcessBuilder.apply_neighborhood(), apply_neighborhood(), DataCube.apply_neighborhood()

arccos

ProcessBuilder.arccos(), arccos()

arcosh

ProcessBuilder.arcosh(), arcosh()

arcsin

ProcessBuilder.arcsin(), arcsin()

arctan

ProcessBuilder.arctan(), arctan()

arctan2

ProcessBuilder.arctan2(), arctan2()

ard_normalized_radar_backscatter

ProcessBuilder.ard_normalized_radar_backscatter(), ard_normalized_radar_backscatter(), DataCube.ard_normalized_radar_backscatter()

ard_surface_reflectance

ProcessBuilder.ard_surface_reflectance(), ard_surface_reflectance(), DataCube.ard_surface_reflectance()

array_append

ProcessBuilder.array_append(), array_append()

array_apply

ProcessBuilder.array_apply(), array_apply()

array_concat

ProcessBuilder.array_concat(), array_concat()

array_contains

ProcessBuilder.array_contains(), array_contains()

array_create

ProcessBuilder.array_create(), array_create()

array_create_labeled

ProcessBuilder.array_create_labeled(), array_create_labeled()

array_element

ProcessBuilder.__getitem__(), ProcessBuilder.array_element(), array_element()

array_filter

ProcessBuilder.array_filter(), array_filter()

array_find

ProcessBuilder.array_find(), array_find()

array_find_label

ProcessBuilder.array_find_label(), array_find_label()

array_interpolate_linear

ProcessBuilder.array_interpolate_linear(), array_interpolate_linear()

array_labels

ProcessBuilder.array_labels(), array_labels()

array_modify

ProcessBuilder.array_modify(), array_modify()

arsinh

ProcessBuilder.arsinh(), arsinh()

artanh

ProcessBuilder.artanh(), artanh()

atmospheric_correction

ProcessBuilder.atmospheric_correction(), atmospheric_correction(), DataCube.atmospheric_correction()

between

ProcessBuilder.between(), between()

ceil

ProcessBuilder.ceil(), ceil()

climatological_normal

ProcessBuilder.climatological_normal(), climatological_normal()

clip

ProcessBuilder.clip(), clip()

cloud_detection

ProcessBuilder.cloud_detection(), cloud_detection()

constant

ProcessBuilder.constant(), constant()

cos

ProcessBuilder.cos(), cos()

cosh

ProcessBuilder.cosh(), cosh()

count

ProcessBuilder.count(), count(), DataCube.count_time()

create_raster_cube

ProcessBuilder.create_raster_cube(), create_raster_cube()

cummax

ProcessBuilder.cummax(), cummax()

cummin

ProcessBuilder.cummin(), cummin()

cumproduct

ProcessBuilder.cumproduct(), cumproduct()

cumsum

ProcessBuilder.cumsum(), cumsum()

date_shift

ProcessBuilder.date_shift(), date_shift()

dimension_labels

ProcessBuilder.dimension_labels(), dimension_labels(), DataCube.dimension_labels()

divide

ProcessBuilder.__truediv__(), ProcessBuilder.__rtruediv__(), ProcessBuilder.divide(), divide(), DataCube.divide(), DataCube.__truediv__(), DataCube.__rtruediv__()

drop_dimension

ProcessBuilder.drop_dimension(), drop_dimension(), DataCube.drop_dimension()

e

ProcessBuilder.e(), e()

eq

ProcessBuilder.__eq__(), ProcessBuilder.eq(), eq(), DataCube.__eq__()

exp

ProcessBuilder.exp(), exp()

extrema

ProcessBuilder.extrema(), extrema()

filter_bands

ProcessBuilder.filter_bands(), filter_bands(), DataCube.filter_bands()

filter_bbox

ProcessBuilder.filter_bbox(), filter_bbox(), DataCube.filter_bbox()

filter_labels

ProcessBuilder.filter_labels(), filter_labels()

filter_spatial

ProcessBuilder.filter_spatial(), filter_spatial(), DataCube.filter_spatial()

filter_temporal

ProcessBuilder.filter_temporal(), filter_temporal(), DataCube.filter_temporal()

first

ProcessBuilder.first(), first()

fit_class_random_forest

ProcessBuilder.fit_class_random_forest(), fit_class_random_forest(), VectorCube.fit_class_random_forest()

fit_curve

ProcessBuilder.fit_curve(), fit_curve(), DataCube.fit_curve()

fit_regr_random_forest

ProcessBuilder.fit_regr_random_forest(), fit_regr_random_forest(), VectorCube.fit_regr_random_forest()

flatten_dimensions

ProcessBuilder.flatten_dimensions(), flatten_dimensions(), DataCube.flatten_dimensions()

floor

ProcessBuilder.floor(), floor()

ge

ProcessBuilder.__ge__(), DataCube.__ge__()

gt

ProcessBuilder.__gt__(), ProcessBuilder.gt(), gt(), DataCube.__gt__()

gte

ProcessBuilder.gte(), gte()

if_

ProcessBuilder.if_(), if_()

inspect

ProcessBuilder.inspect(), inspect()

int

ProcessBuilder.int(), int()

is_infinite

ProcessBuilder.is_infinite(), is_infinite()

is_nan

ProcessBuilder.is_nan(), is_nan()

is_nodata

ProcessBuilder.is_nodata(), is_nodata()

is_valid

ProcessBuilder.is_valid(), is_valid()

last

ProcessBuilder.last(), last()

le

DataCube.__le__()

linear_scale_range

ProcessBuilder.linear_scale_range(), linear_scale_range(), DataCube.linear_scale_range()

ln

ProcessBuilder.ln(), ln(), DataCube.ln()

load_collection

ProcessBuilder.load_collection(), load_collection(), DataCube.load_collection(), Connection.load_collection()

load_geojson

VectorCube.load_geojson(), Connection.load_geojson()

load_ml_model

ProcessBuilder.load_ml_model(), load_ml_model(), MlModel.load_ml_model()

load_result

ProcessBuilder.load_result(), load_result(), Connection.load_result()

load_stac

Connection.load_stac()

load_uploaded_files

ProcessBuilder.load_uploaded_files(), load_uploaded_files()

log

ProcessBuilder.log(), log(), DataCube.logarithm(), DataCube.log2(), DataCube.log10()

lt

ProcessBuilder.__lt__(), ProcessBuilder.lt(), lt(), DataCube.__lt__()

lte

ProcessBuilder.__le__(), ProcessBuilder.lte(), lte()

mask

ProcessBuilder.mask(), mask(), DataCube.mask()

mask_polygon

ProcessBuilder.mask_polygon(), mask_polygon(), DataCube.mask_polygon()

max

ProcessBuilder.max(), max(), DataCube.max_time()

mean

ProcessBuilder.mean(), mean(), DataCube.mean_time()

median

ProcessBuilder.median(), median(), DataCube.median_time()

merge_cubes

ProcessBuilder.merge_cubes(), merge_cubes(), DataCube.merge_cubes()

min

ProcessBuilder.min(), min(), DataCube.min_time()

mod

ProcessBuilder.mod(), mod()

multiply

ProcessBuilder.__mul__(), ProcessBuilder.__rmul__(), ProcessBuilder.__neg__(), ProcessBuilder.multiply(), multiply(), DataCube.multiply(), DataCube.__neg__(), DataCube.__mul__(), DataCube.__rmul__()

nan

ProcessBuilder.nan(), nan()

ndvi

ProcessBuilder.ndvi(), ndvi(), DataCube.ndvi()

neq

ProcessBuilder.__ne__(), ProcessBuilder.neq(), neq(), DataCube.__ne__()

normalized_difference

ProcessBuilder.normalized_difference(), normalized_difference(), DataCube.normalized_difference()

not

DataCube.__invert__()

not_

ProcessBuilder.not_(), not_()

or

DataCube.logical_or(), DataCube.__or__()

or_

ProcessBuilder.or_(), or_()

order

ProcessBuilder.order(), order()

pi

ProcessBuilder.pi(), pi()

power

ProcessBuilder.__pow__(), ProcessBuilder.power(), power(), DataCube.__rpow__(), DataCube.__pow__(), DataCube.power()

predict_curve

ProcessBuilder.predict_curve(), predict_curve(), DataCube.predict_curve()

predict_random_forest

ProcessBuilder.predict_random_forest(), predict_random_forest(), DataCube.predict_random_forest()

product

ProcessBuilder.product(), product()

quantiles

ProcessBuilder.quantiles(), quantiles()

rearrange

ProcessBuilder.rearrange(), rearrange()

reduce_dimension

ProcessBuilder.reduce_dimension(), reduce_dimension(), DataCube.reduce_dimension()

reduce_spatial

ProcessBuilder.reduce_spatial(), reduce_spatial()

rename_dimension

ProcessBuilder.rename_dimension(), rename_dimension(), DataCube.rename_dimension()

rename_labels

ProcessBuilder.rename_labels(), rename_labels(), DataCube.rename_labels()

resample_cube_spatial

ProcessBuilder.resample_cube_spatial(), resample_cube_spatial()

resample_cube_temporal

ProcessBuilder.resample_cube_temporal(), resample_cube_temporal(), DataCube.resample_cube_temporal()

resample_spatial

ProcessBuilder.resample_spatial(), resample_spatial(), DataCube.resample_spatial()

resolution_merge

DataCube.resolution_merge()

round

ProcessBuilder.round(), round()

run_udf

ProcessBuilder.run_udf(), run_udf(), VectorCube.run_udf()

run_udf_externally

ProcessBuilder.run_udf_externally(), run_udf_externally()

sar_backscatter

ProcessBuilder.sar_backscatter(), sar_backscatter(), DataCube.sar_backscatter()

save_ml_model

ProcessBuilder.save_ml_model(), save_ml_model()

save_result

ProcessBuilder.save_result(), save_result(), VectorCube.save_result(), DataCube.save_result()

sd

ProcessBuilder.sd(), sd()

sgn

ProcessBuilder.sgn(), sgn()

sin

ProcessBuilder.sin(), sin()

sinh

ProcessBuilder.sinh(), sinh()

sort

ProcessBuilder.sort(), sort()

sqrt

ProcessBuilder.sqrt(), sqrt()

subtract

ProcessBuilder.__sub__(), ProcessBuilder.__rsub__(), ProcessBuilder.subtract(), subtract(), DataCube.subtract(), DataCube.__sub__(), DataCube.__rsub__()

sum

ProcessBuilder.sum(), sum()

tan

ProcessBuilder.tan(), tan()

tanh

ProcessBuilder.tanh(), tanh()

text_begins

ProcessBuilder.text_begins(), text_begins()

text_concat

ProcessBuilder.text_concat(), text_concat()

text_contains

ProcessBuilder.text_contains(), text_contains()

text_ends

ProcessBuilder.text_ends(), text_ends()

trim_cube

ProcessBuilder.trim_cube(), trim_cube()

unflatten_dimension

ProcessBuilder.unflatten_dimension(), unflatten_dimension(), DataCube.unflatten_dimension()

variance

ProcessBuilder.variance(), variance()

vector_buffer

ProcessBuilder.vector_buffer(), vector_buffer()

vector_to_random_points

ProcessBuilder.vector_to_random_points(), vector_to_random_points()

vector_to_regular_points

ProcessBuilder.vector_to_regular_points(), vector_to_regular_points()

xor

ProcessBuilder.xor(), xor()

+

(Table autogenerated on 2023-08-07)

+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/processes.html b/processes.html new file mode 100644 index 000000000..e05658ba6 --- /dev/null +++ b/processes.html @@ -0,0 +1,539 @@ + + + + + + + + Working with processes — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Working with processes

+

In openEO, a process is an operation that performs a specific task on +a set of parameters and returns a result. +For example, with the add process you can add two numbers, in openEO’s JSON notation:

+
{
+    "process_id": "add",
+    "arguments": {"x": 3, "y": 5}
+}
+
+
+

A process is similar to a function in common programming languages, +and likewise, multiple processes can be combined or chained together +into new, more complex operations.

+
+

A bit of terminology

+

A pre-defined process is a process provided out of the box by a given back-end. +These are often the centrally defined openEO processes, +such as common mathematical (sum, divide, sqrt, …), +statistical (mean, max, …) and +image processing (mask, apply_kernel, …) +operations. +Back-ends are expected to support most of these standard ones, +but are free to pre-define additional ones too.

+

Processes can be combined into a larger pipeline, parameterized +and stored on the back-end as a so called user-defined process. +This allows you to build a library of reusable building blocks +that can be be inserted easily in multiple other places. +See User-Defined Processes (UDP) for more information.

+

How processes are combined into a larger unit +is internally represented by a so-called process graph. +It describes how the inputs and outputs of processes +should be linked together. +A user of the Python client should normally not worry about +the details of a process graph structure, as most of these aspects +are hidden behind regular Python functions, classes and methods.

+
+
+

Using common pre-defined processes

+

The listing of pre-defined processes provided by a back-end +can be inspected with list_processes(). +For example, to get a list of the process names (process ids):

+
>>> process_ids = [process["id"] for process in connection.list_processes()]
+>>> print(process_ids[:16])
+['arccos', 'arcosh', 'power', 'last', 'subtract', 'not', 'cosh', 'artanh',
+'is_valid', 'first', 'median', 'eq', 'absolute', 'arctan2', 'divide','is_nan']
+
+
+

More information about the processes, like a description +or expected parameters, can be queried like that, +but it is often easier to look them up on the +official openEO process documentation

+

A single pre-defined process can be retrieved with +describe_process().

+
+

Convenience methods

+

Most of the important pre-defined processes are covered directly by methods +on classes like DataCube or +VectorCube.

+
+

See also

+

See openEO Process Mapping for a mapping of openEO processes +the corresponding methods in the openEO Python Client library.

+
+

For example, to apply the filter_temporal process to a raster data cube:

+
cube = cube.filter_temporal("2020-02-20", "2020-06-06")
+
+
+

Being regular Python methods, you get usual function call features +you’re accustomed to: default values, keyword arguments, kwargs usage, … +For example, to use a bounding box dictionary with kwargs-expansion:

+
bbox = {
+    "west": 5.05, "south": 51.20, "east": 5.10, "north": 51.23
+}
+cube = cube.filter_bbox(**bbox)
+
+
+

Note that some methods try to be more flexible and convenient to use +than how the official process definition prescribes. +For example, the filter_temporal process expects an extent array +with 2 items (the start and end date), +but you can call the corresponding client method in multiple equivalent ways:

+
cube.filter_temporal("2019-07-01", "2019-08-01")
+cube.filter_temporal(["2019-07-01", "2019-08-01"])
+cube.filter_temporal(extent=["2019-07-01", "2019-08-01"])
+cube.filter_temporal(start_date="2019-07-01", end_date="2019-08-01"])
+
+
+
+
+

Advanced argument tweaking

+
+

Added in version 0.10.1.

+
+

In some situations, you may want to finetune what the (convenience) methods generate. +For example, you want to play with non-standard, experimental arguments, +or there is a problem with a automatic argument handling/conversion feature.

+

You can tweak the arguments of your current result node as follows. +Say, we want to add some non-standard feature_flags argument to the load_collection process node. +We first get the current result node with result_node() and use update_arguments() to add an additional argument to it:

+
# `Connection.load_collection` does not support `feature_flags` argument
+cube = connection.load_collection(...)
+
+# Add `feature_flag` argument `load_collection` process graph node
+cube.result_node().update_arguments(feature_flags="rXPk")
+
+# The resulting process graph will now contain this non-standard argument:
+#     {
+#         "process_id": "load_collection",
+#         "arguments": {
+#             ...
+#             "feature_flags": "rXPk",
+
+
+
+
+
+

Generic API for adding processes

+

An openEO back-end may offer processes that are not part of the core API, +or the client may not (yet) have a corresponding method +for a process that you wish to use. +In that case, you can fall back to a more generic API +that allows you to add processes directly.

+
+

Basics

+

To add a simple process to the graph, use +the process() method +on a DataCube. +You have to specify the process id and arguments +(as a single dictionary or through keyword arguments **kwargs). +It will return a new DataCube with the new process appended +to the internal process graph.

+

A very simple example using the mean process and a +literal list in an arguments dictionary:

+
arguments= {
+    "data": [1, 3, -1]
+}
+res = cube.process("mean", arguments)
+
+
+

or equivalently, leveraging keyword arguments:

+
res = cube.process("mean", data=[1, 3, -1])
+
+
+
+
+

Passing data cube arguments

+

The example above is a bit convoluted however in the sense that +you start from a given data cube cube, you add a mean process +that works on a given data array, while completely ignoring the original cube. +In reality you typically want to apply the process on the cube. +This is possible by passing a data cube object directly as argument, +for example with the ndvi process that at least expects +a data cube as data argument

+
res = cube.process("ndvi", data=cube)
+
+
+

Note that you have to specify cube twice here: +a first time to call the method and a second time as argument. +Moreover, it requires you to define a Python variable for the data +cube, which is annoying if you want to use a chained expressions. +To solve these issues, you can use the THIS +constant as symbolic reference to the “current” cube:

+
from openeo.rest.datacube import THIS
+
+res = (
+    cube
+        .process("filter_bands", data=THIS)
+        .process("mask", data=THIS, mask=mask)
+        .process("ndvi", data=THIS)
+)
+
+
+
+
+

Passing results from other process calls as arguments

+

Another use case of generically applying (custom) processes is +passing a process result as argument to another process working on a cube. +For example, assume we have a custom process load_my_vector_cube +to load a vector cube from an online resource. +We can use this vector cube as geometry for +DataCube.aggregate_spatial() +using openeo.processes.process() as follows:

+
from openeo.processes import process
+
+res = cube.aggregate_spatial(
+    geometries=process("load_my_vector_cube", url="https://geo.example/features.db"),
+    reducer="mean"
+)
+
+
+
+
+
+

Processes with child “callbacks”

+

Some openEO processes expect some kind of sub-process +to be invoked on a subset or slice of the datacube. +For example:

+
    +
  • process apply requires a transformation that will be applied +to each pixel in the cube (separately), e.g. in pseudocode

    +
    cube.apply(
    +    given a pixel value
    +    => scale it with factor 0.01
    +)
    +
    +
    +
  • +
  • process reduce_dimension requires an aggregation function to convert +an array of pixel values (along a given dimension) to a single value, +e.g. in pseudocode

    +
    cube.reduce_dimension(
    +    given a pixel timeseries (array) for a (x,y)-location
    +    => temporal mean of that array
    +)
    +
    +
    +
  • +
  • process aggregate_spatial requires a function to aggregate the values +in one or more geometries

  • +
+

These transformation functions are usually called “callbacks” +because instead of being called explicitly by the user, +they are called and managed by their “parent” process +(the apply, reduce_dimension and aggregate_spatial in the examples)

+

The openEO Python Client Library currently provides a couple of DataCube methods +that expect such a callback, most commonly:

+ +

The openEO Python Client Library supports several ways +to specify the desired callback for these functions:

+ +
+

Callback as string

+

The easiest way is passing a process name as a string, +for example:

+
# Take the absolute value of each pixel
+cube.apply("absolute")
+
+# Reduce a cube along the temporal dimension by taking the maximum value
+cube.reduce_dimension(reducer="max", dimension="t")
+
+
+

This approach is only possible if the desired transformation is available +as a single process. If not, use one of the methods below.

+

It’s also important to note that the “signature” of the provided callback process +should correspond properly with what the parent process expects. +For example: apply requires a callback process that receives a +number and returns one (like absolute or sqrt), +while reduce_dimension requires a callback process that receives +an array of numbers and returns a single number (like max or mean).

+
+
+

Callback as a callable

+

You can also specify the callback as a “callable”: +which is a fancy word for a Python object that can be called, +but just think of it like a function you can call.

+

You can use a regular Python function, like this:

+
def transform(x):
+    return x * 2 + 3
+
+cube.apply(transform)
+
+
+

or, more compactly, a “lambda” +(a construct in Python to create anonymous inline functions):

+
cube.apply(lambda x: x * 2 + 3)
+
+
+

The openEO Python Client Library implements most of the official openEO processes as +functions in the “openeo.processes” module, +which can be used directly as callback:

+
from openeo.processes import absolute, max
+
+cube.apply(absolute)
+cube.reduce_dimension(reducer=max, dimension="t")
+
+
+

The argument that will be passed to all these callback functions is +a ProcessBuilder instance. +This is a helper object with predefined methods for all standard openEO processes, +allowing to use an object oriented coding style to define the callback. +For example:

+
from openeo.processes import ProcessBuilder
+
+def avg(data: ProcessBuilder):
+    return data.mean()
+
+cube.reduce_dimension(reducer=avg, dimension="t")
+
+
+

These methods also return ProcessBuilder objects, +which also allows writing callbacks in chained fashion:

+
cube.apply(
+    lambda x: x.absolute().cos().add(y=1.23)
+)
+
+
+

All this gives a lot of flexibility to define callbacks compactly +in a desired coding style. +The following examples result in the same callback:

+
from openeo.processes import ProcessBuilder, mean, cos, add
+
+# Chained methods
+cube.reduce_dimension(
+    lambda data: data.mean().cos().add(y=1.23),
+    dimension="t"
+)
+
+# Functions
+cube.reduce_dimension(
+    lambda data: add(x=cos(mean(data)), y=1.23),
+    dimension="t"
+)
+
+# Mixing methods, functions and operators
+cube.reduce_dimension(
+    lambda data: cos(data.mean())) + 1.23,
+    dimension="t"
+)
+
+
+
+

Caveats

+

Specifying callbacks through Python functions (or lambdas) +looks intuitive and straightforward, but it should be noted +that not everything is allowed in these functions. +You should just limit yourself to calling +openeo.processes functions, +ProcessBuilder methods +and basic math operators. +Don’t call functions from other libraries like numpy or scipy. +Don’t use Python control flow statements like if/else constructs +or for loops.

+

The reason for this is that the openEO Python Client Library +does not translate the function source code itself +to an openEO process graph. +Instead, when building the openEO process graph, +it passes a special object to the function +and keeps track of which openeo.processes functions +were called to assemble the corresponding process graph. +If you use control flow statements or use numpy functions for example, +this procedure will incorrectly detect what you want to do in the callback.

+

For example, if you mistakenly use the Python builtin sum() function +in a callback instead of openeo.processes.sum(), you will run into trouble. +Luckily the openEO Python client Library should raise an error if it detects that:

+
>>> # Wrongly using builtin `sum` function
+>>> cube.reduce_dimension(dimension="t", reducer=sum)
+RuntimeError: Exceeded ProcessBuilder iteration limit.
+Are you mistakenly using a builtin like `sum()` or `all()` in a callback
+instead of the appropriate helpers from `openeo.processes`?
+
+>>> # Explicit usage of `openeo.processes.sum`
+>>> import openeo.processes
+>>> cube.reduce_dimension(dimension="t", reducer=openeo.processes.sum)
+<openeo.rest.datacube.DataCube at 0x7f6505a40d00>
+
+
+
+
+
+

Callback as PGNode

+

You can also pass a PGNode object as callback.

+
+

Attention

+

This approach should generally not be used in normal use cases. +The other options discussed above should be preferred. +It’s mainly intended for internal use and an occasional, advanced use case. +It requires in-depth knowledge of the openEO API +and openEO Python Client Library to construct correctly.

+
+

Some examples:

+
from openeo.internal.graph_building import PGNode
+
+cube.apply(PGNode(
+    "add",
+    x=PGNode(
+        "cos",
+        x=PGNode("absolute", x={"from_parameter": "x"})
+    ),
+    y=1.23
+))
+
+cube.reduce_dimension(
+    reducer=PGNode("max", data={"from_parameter": "data"}),
+    dimension="bands"
+)
+
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 000000000..7b082c160 --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,272 @@ + + + + + + + Python Module Index — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ o +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ o
+ openeo +
    + openeo.api.logs +
    + openeo.api.process +
    + openeo.extra.spectral_indices +
    + openeo.internal.graph_building +
    + openeo.metadata +
    + openeo.processes +
    + openeo.rest._datacube +
    + openeo.rest.connection +
    + openeo.rest.conversions +
    + openeo.rest.datacube +
    + openeo.rest.graph_building +
    + openeo.rest.job +
    + openeo.rest.mlmodel +
    + openeo.rest.multiresult +
    + openeo.rest.udp +
    + openeo.rest.userfile +
    + openeo.rest.vectorcube +
    + openeo.testing +
    + openeo.testing.results +
    + openeo.udf.debug +
    + openeo.udf.run_code +
    + openeo.udf.structured_data +
    + openeo.udf.udf_data +
    + openeo.udf.udf_signatures +
    + openeo.udf.xarraydatacube +
    + openeo.util +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 000000000..7ed67b42c --- /dev/null +++ b/search.html @@ -0,0 +1,144 @@ + + + + + + + Search — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 000000000..57dfc7e56 --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"A bit of terminology": [[24, "a-bit-of-terminology"]], "A first example: apply with an UDF to rescale pixel values": [[25, "a-first-example-apply-with-an-udf-to-rescale-pixel-values"]], "API": [[11, "api"], [14, "module-openeo.extra.spectral_indices"]], "API (General)": [[0, null]], "API: openeo.processes": [[2, null]], "Ad-hoc dependency handling": [[25, "ad-hoc-dependency-handling"]], "Added": [[7, "added"], [7, "id5"], [7, "id8"], [7, "id12"], [7, "id14"], [7, "id17"], [7, "id20"], [7, "id24"], [7, "id27"], [7, "id34"], [7, "id37"], [7, "id42"], [7, "id48"], [7, "id52"], [7, "id55"], [7, "id58"], [7, "id60"], [7, "id62"], [7, "id67"], [7, "id72"], [7, "id75"], [7, "id82"], [7, "id86"], [7, "id89"], [7, "id91"], [7, "id96"], [7, "id98"], [7, "id102"], [7, "id107"], [7, "id111"], [7, "id114"], [7, "id120"], [7, "id123"], [7, "id127"], [7, "id131"], [7, "id134"], [7, "id137"], [7, "id142"], [7, "id147"], [7, "id150"]], "Advanced argument tweaking": [[24, "advanced-argument-tweaking"]], "Aggregated EVI timeseries": [[4, "aggregated-evi-timeseries"]], "Alternative development installation": [[19, "alternative-development-installation"]], "Analysis Ready Data generation": [[9, null]], "Applicability and Constraints": [[25, "applicability-and-constraints"]], "Applying a cloud mask": [[4, "applying-a-cloud-mask"]], "Atmospheric correction": [[9, "atmospheric-correction"]], "Auth config files and openeo-auth helper tool": [[3, "auth-config-files-and-openeo-auth-helper-tool"]], "Authentication": [[4, "authentication"]], "Authentication and Account Management": [[3, null]], "Authentication for long-running applications and non-interactive contexts": [[3, "authentication-for-long-running-applications-and-non-interactive-contexts"]], "Automatic band mapping": [[14, "automatic-band-mapping"]], "Automatic batch job log printing": [[5, "automatic-batch-job-log-printing"]], "Background": [[12, "background"]], "Background and inspiration": [[6, "background-and-inspiration"]], "Band mapping": [[14, "band-mapping"]], "Band math": [[4, "band-math"]], "Basic HTTP Auth": [[3, "basic-http-auth"]], "Basic HTTP Auth config": [[3, "basic-http-auth-config"]], "Basic ProcessBasedJobCreator example": [[11, "basic-processbasedjobcreator-example"]], "Basic install": [[21, "basic-install"]], "Basics": [[24, "basics"]], "Batch Jobs": [[5, null]], "Batch Jobs (asynchronous execution)": [[4, "batch-jobs-asynchronous-execution"]], "Batch job logs": [[5, "batch-job-logs"]], "Batch job object": [[5, "batch-job-object"]], "Best Practices and Troubleshooting Tips": [[3, "best-practices-and-troubleshooting-tips"]], "Best practices, coding style and general tips": [[6, null]], "Building and storing user-defined process": [[26, "building-and-storing-user-defined-process"]], "Building process graphs with multiple result nodes": [[18, "building-process-graphs-with-multiple-result-nodes"]], "Building the documentation": [[19, "building-the-documentation"]], "Callback as PGNode": [[24, "callback-as-pgnode"]], "Callback as a callable": [[24, "callback-as-a-callable"]], "Callback as string": [[24, "callback-as-string"]], "Caveats": [[24, "caveats"]], "Changed": [[7, "changed"], [7, "id9"], [7, "id18"], [7, "id21"], [7, "id25"], [7, "id31"], [7, "id35"], [7, "id38"], [7, "id43"], [7, "id49"], [7, "id53"], [7, "id63"], [7, "id68"], [7, "id73"], [7, "id76"], [7, "id78"], [7, "id83"], [7, "id87"], [7, "id92"], [7, "id99"], [7, "id103"], [7, "id108"], [7, "id112"], [7, "id115"], [7, "id118"], [7, "id124"], [7, "id128"], [7, "id135"], [7, "id138"], [7, "id143"]], "Changelog": [[7, null]], "Clear the refresh token file": [[3, "clear-the-refresh-token-file"]], "Client-side (local) processing": [[12, null]], "Code reuse with user-defined processes": [[26, "code-reuse-with-user-defined-processes"]], "Collection discovery": [[4, "collection-discovery"]], "Computing multiple statistics": [[4, "computing-multiple-statistics"]], "Configuration": [[8, null]], "Configuration files": [[8, "configuration-files"]], "Configuration options": [[8, "configuration-options"]], "Connect to an openEO back-end": [[4, "connect-to-an-openeo-back-end"]], "Construct DataCube from process": [[18, "construct-datacube-from-process"]], "Construct a DataCube from JSON": [[18, "construct-a-datacube-from-json"]], "Contents:": [[10, null]], "Contributing code": [[19, "contributing-code"]], "Convenience methods": [[24, "convenience-methods"]], "Create a batch job": [[5, "create-a-batch-job"]], "Create, start and wait in one go": [[5, "create-start-and-wait-in-one-go"]], "Creating a release": [[19, "creating-a-release"]], "Data discovery": [[17, "data-discovery"]], "DataCube construction": [[18, null]], "Dataset sampling": [[13, null]], "Declaration of UDF dependencies": [[25, "declaration-of-udf-dependencies"]], "Declaring Parameters": [[26, "declaring-parameters"]], "Default openEO back-end URL and auto-authentication": [[3, "default-openeo-back-end-url-and-auto-authentication"]], "Deprecated": [[7, "deprecated"], [7, "id144"]], "Development Installation on Windows": [[19, "development-installation-on-windows"]], "Development and maintenance": [[19, null]], "Directly load batch job results": [[5, "directly-load-batch-job-results"]], "Download (synchronously)": [[4, "download-synchronously"]], "Download all assets": [[5, "download-all-assets"]], "Download batch job results": [[5, "download-batch-job-results"]], "Download single asset": [[5, "download-single-asset"]], "Downloading a datacube and executing an UDF locally": [[25, "downloading-a-datacube-and-executing-an-udf-locally"]], "EODC back-end": [[9, "eodc-back-end"], [9, "id5"]], "Enabling additional features": [[21, "enabling-additional-features"]], "Evaluate user-defined processes": [[26, "evaluate-user-defined-processes"]], "Example": [[25, "example"]], "Example use case: EVI map and timeseries": [[4, "example-use-case-evi-map-and-timeseries"]], "Example: Smoothing timeseries with a user defined function (UDF)": [[25, "example-smoothing-timeseries-with-a-user-defined-function-udf"]], "Example: apply_dimension with a UDF": [[25, "example-apply-dimension-with-a-udf"]], "Example: apply_neighborhood with a UDF": [[25, "example-apply-neighborhood-with-a-udf"]], "Example: reduce_dimension with a UDF": [[25, "example-reduce-dimension-with-a-udf"]], "Examples": [[25, "examples"]], "Execute a process graph directly from raw JSON": [[15, "execute-a-process-graph-directly-from-raw-json"]], "Export a process graph": [[15, "export-a-process-graph"]], "Filter on collection properties": [[17, "filter-on-collection-properties"]], "Filter on spatial extent": [[17, "filter-on-spatial-extent"]], "Filter on temporal extent": [[17, "filter-on-temporal-extent"]], "Finding and loading data": [[17, null]], "Fine-grained asset downloads": [[5, "fine-grained-asset-downloads"]], "Fixed": [[7, "fixed"], [7, "id3"], [7, "id6"], [7, "id10"], [7, "id15"], [7, "id22"], [7, "id26"], [7, "id29"], [7, "id32"], [7, "id40"], [7, "id44"], [7, "id46"], [7, "id50"], [7, "id56"], [7, "id59"], [7, "id61"], [7, "id65"], [7, "id70"], [7, "id74"], [7, "id80"], [7, "id88"], [7, "id94"], [7, "id100"], [7, "id116"], [7, "id125"], [7, "id132"], [7, "id140"], [7, "id148"], [7, "id151"]], "Format": [[8, "format"]], "From a parameterized data cube": [[26, "from-a-parameterized-data-cube"]], "Functions in openeo.processes": [[2, "functions-in-openeo-processes"]], "General code style recommendations": [[6, "general-code-style-recommendations"]], "General options": [[3, "general-options"]], "Generic API for adding processes": [[24, "generic-api-for-adding-processes"]], "Geotrellis back-end": [[9, "geotrellis-back-end"], [9, "id6"]], "Getting Started": [[4, null]], "Graph building": [[0, "graph-building"]], "Guidelines and tips": [[3, "guidelines-and-tips"]], "Handling large vector data sets": [[17, "handling-large-vector-data-sets"]], "High level Interface": [[0, "high-level-interface"]], "Important files": [[19, "important-files"]], "Indices and tables": [[20, "indices-and-tables"]], "Inference": [[22, "inference"]], "Initial exploration of an openEO collection": [[17, "initial-exploration-of-an-openeo-collection"]], "Installation": [[12, "installation"], [21, null]], "Installation with Conda": [[21, "installation-with-conda"]], "Installation with pip": [[21, "installation-with-pip"]], "Internal openEO process graph building utilities": [[0, "internal-openeo-process-graph-building-utilities"]], "Job creation based on parameterized processes": [[11, "job-creation-based-on-parameterized-processes"]], "Jupyter integration": [[5, "jupyter-integration"]], "Jupyter(lab) tips and tricks": [[6, "jupyter-lab-tips-and-tricks"]], "Left-closed intervals: start included, end excluded": [[17, "left-closed-intervals-start-included-end-excluded"]], "Like a Pro": [[19, "like-a-pro"]], "Line (length) management": [[6, "line-length-management"]], "List your batch jobs": [[5, "list-your-batch-jobs"]], "Loading a data cube from a collection": [[17, "loading-a-data-cube-from-a-collection"]], "Loading a published user-defined process as DataCube": [[16, "loading-a-published-user-defined-process-as-datacube"]], "Loading an initial data cube": [[4, "loading-an-initial-data-cube"]], "Local Collections": [[12, "local-collections"]], "Local Processing": [[12, "local-processing"]], "Location": [[8, "location"]], "Logging from a UDF": [[25, "logging-from-a-udf"]], "Machine Learning": [[22, null]], "Manual band mapping": [[14, "manual-band-mapping"]], "Miscellaneous tips and tricks": [[15, null]], "Module openeo.udf.udf_signatures": [[25, "module-openeo.udf.udf_signatures"]], "More advanced parameter schemas": [[26, "more-advanced-parameter-schemas"]], "Multi Backend Job Manager": [[11, null]], "OIDC Authentication: Client Credentials Flow": [[3, "oidc-authentication-client-credentials-flow"]], "OIDC Authentication: Device Code Flow": [[3, "oidc-authentication-device-code-flow"]], "OIDC Authentication: Dynamic Method Selection": [[3, "oidc-authentication-dynamic-method-selection"]], "OIDC Authentication: Refresh Token Flow": [[3, "oidc-authentication-refresh-token-flow"]], "OIDC Client Credentials Using Environment Variables": [[3, "oidc-client-credentials-using-environment-variables"]], "OpenID Connect Based Authentication": [[3, "openid-connect-based-authentication"]], "OpenID Connect configs": [[3, "openid-connect-configs"]], "OpenID Connect refresh tokens": [[3, "openid-connect-refresh-tokens"]], "Optional dependencies": [[21, "optional-dependencies"]], "Parameterization": [[18, "parameterization"]], "Passing data cube arguments": [[24, "passing-data-cube-arguments"]], "Passing results from other process calls as arguments": [[24, "passing-results-from-other-process-calls-as-arguments"]], "Performance & scalability": [[13, "performance-scalability"]], "Pre-commit for basic code quality checks": [[19, "pre-commit-for-basic-code-quality-checks"]], "Pre-commit set up": [[19, "pre-commit-set-up"]], "Pre-commit usage": [[19, "pre-commit-usage"]], "Prerequisites": [[19, "prerequisites"]], "Procedure": [[19, "procedure"]], "Process Parameters": [[26, "process-parameters"]], "ProcessBasedJobCreator with geometry handling": [[11, "processbasedjobcreator-with-geometry-handling"]], "ProcessBuilder helper class": [[2, "processbuilder-helper-class"]], "Processes with child \u201ccallbacks\u201d": [[24, "processes-with-child-callbacks"]], "Profile a process server-side": [[25, "profile-a-process-server-side"]], "Public openEO process graph building utilities": [[0, "public-openeo-process-graph-building-utilities"]], "Publicly publishing a user-defined process.": [[16, "publicly-publishing-a-user-defined-process"]], "Pull requests": [[19, "pull-requests"]], "Quick and easy": [[19, "quick-and-easy"]], "Random Forest based Classification and Regression": [[22, "random-forest-based-classification-and-regression"]], "Re-parameterization": [[18, "re-parameterization"]], "Reconnecting to a batch job": [[5, "reconnecting-to-a-batch-job"]], "Reference implementations": [[9, "reference-implementations"], [9, "id4"]], "Removed": [[7, "removed"], [7, "id2"], [7, "id28"], [7, "id39"], [7, "id64"], [7, "id79"], [7, "id84"], [7, "id90"], [7, "id93"], [7, "id104"], [7, "id109"], [7, "id121"], [7, "id129"], [7, "id139"], [7, "id145"]], "Rounding down periods to dates": [[17, "rounding-down-periods-to-dates"]], "Run a batch job": [[5, "run-a-batch-job"]], "Running the unit tests": [[19, "running-the-unit-tests"]], "SAR backscatter": [[9, "sar-backscatter"]], "STAC Collections and Items": [[12, "stac-collections-and-items"]], "Sampling at scale": [[13, "sampling-at-scale"]], "Sections:": [[2, "sections"]], "Sharing of user-defined processes": [[16, null]], "Single string temporal extents": [[17, "single-string-temporal-extents"]], "Some examples": [[18, "some-examples"]], "Source or development install": [[21, "source-or-development-install"]], "Spectral Indices": [[14, null]], "Standard for declaring Python UDF dependencies": [[25, "standard-for-declaring-python-udf-dependencies"]], "Store to a file": [[26, "store-to-a-file"]], "Table of contents": [[20, "table-of-contents"]], "Testing": [[0, "testing"]], "The load_collection process": [[18, "the-load-collection-process"]], "Through \u201cprocess functions\u201d": [[26, "through-process-functions"]], "Training": [[22, "training"]], "UDF dependency management": [[25, "udf-dependency-management"]], "UDF function names and signatures": [[25, "udf-function-names-and-signatures"]], "UDF script": [[25, "udf-script"]], "UDFs as apply/reduce \u201ccallbacks\u201d": [[25, "udfs-as-apply-reduce-callbacks"]], "UDF\u2019s that transform cube metadata": [[25, "udf-s-that-transform-cube-metadata"]], "UDP Example: EVI timeseries": [[26, "udp-example-evi-timeseries"]], "Update of generated files": [[19, "update-of-generated-files"]], "Usage": [[12, "usage"], [25, "usage"]], "Usage example": [[20, "usage-example"]], "User-Defined Functions (UDF) explained": [[25, null]], "User-Defined Processes (UDP)": [[26, null]], "Using a predefined dictionary": [[26, "using-a-predefined-dictionary"]], "Using a public UDP through URL based \u201cnamespace\u201d": [[16, "using-a-public-udp-through-url-based-namespace"]], "Using common pre-defined processes": [[24, "using-common-pre-defined-processes"]], "Verification": [[19, "verification"], [25, "verification"]], "Verifying and troubleshooting": [[21, "verifying-and-troubleshooting"]], "Viewing profiling information": [[25, "viewing-profiling-information"]], "Wait for a batch job to finish": [[5, "wait-for-a-batch-job-to-finish"]], "Workflow script": [[25, "workflow-script"]], "Working with processes": [[24, null]], "Year/month shorthand notation": [[17, "year-month-shorthand-notation"]], "[0.10.0] - 2022-04-08 - \u201cSRR3\u201d release": [[7, "srr3-release"]], "[0.10.1] - 2022-05-18 - \u201cLPS22\u201d release": [[7, "lps22-release"]], "[0.11.0] - 2022-07-02": [[7, "id85"]], "[0.12.0] - 2022-09-09": [[7, "id81"]], "[0.12.1] - 2022-09-15": [[7, "id77"]], "[0.13.0] - 2022-10-10 - \u201cUDF UX\u201d release": [[7, "udf-ux-release"]], "[0.14.0] - 2023-02-01": [[7, "id71"]], "[0.14.1] - 2023-02-06": [[7, "id69"]], "[0.15.0] - 2023-03-03": [[7, "id66"]], "[0.16.0] - 2023-04-17 - \u201cSRR5\u201d release": [[7, "srr5-release"]], "[0.17.0] and [0.17.1] - 2023-05-16": [[7, "and-0-17-1-2023-05-16"]], "[0.18.0] - 2023-05-31": [[7, "id57"]], "[0.19.0] - 2023-06-16": [[7, "id54"]], "[0.20.0] - 2023-06-30": [[7, "id51"]], "[0.21.0] - 2023-07-19": [[7, "id47"]], "[0.21.1] - 2023-07-19": [[7, "id45"]], "[0.22.0] - 2023-08-09": [[7, "id41"]], "[0.23.0] - 2023-10-02": [[7, "id36"]], "[0.24.0] - 2023-10-27": [[7, "id33"]], "[0.25.0] - 2023-11-02": [[7, "id30"]], "[0.26.0] - 2023-11-27 - \u201cSRR6\u201d release": [[7, "srr6-release"]], "[0.27.0] - 2024-01-12": [[7, "id23"]], "[0.28.0] - 2024-03-18": [[7, "id19"]], "[0.29.0] - 2024-05-03": [[7, "id16"]], "[0.30.0] - 2024-06-18": [[7, "id13"]], "[0.31.0] - 2024-07-26": [[7, "id11"]], "[0.32.0] - 2024-09-27": [[7, "id7"]], "[0.33.0] - 2024-10-18": [[7, "id4"]], "[0.34.0] - 2024-10-31": [[7, "id1"]], "[0.4.10] - 2021-02-26": [[7, "id126"]], "[0.4.4] - 2020-08-20": [[7, "id149"]], "[0.4.5] - 2020-10-01": [[7, "id146"]], "[0.4.6] - 2020-10-15": [[7, "id141"]], "[0.4.7] - 2020-10-22": [[7, "id136"]], "[0.4.8] - 2020-11-17": [[7, "id133"]], "[0.4.9] - 2021-01-29": [[7, "id130"]], "[0.5.0] - 2021-03-17": [[7, "id122"]], "[0.6.0] - 2021-03-26": [[7, "id119"]], "[0.6.1] - 2021-03-29": [[7, "id117"]], "[0.7.0] - 2021-04-21": [[7, "id113"]], "[0.8.0] - 2021-06-25": [[7, "id110"]], "[0.8.1] - 2021-08-24": [[7, "id106"]], "[0.8.2] - 2021-08-24": [[7, "id105"]], "[0.9.0] - 2021-10-11": [[7, "id101"]], "[0.9.1] - 2021-11-16": [[7, "id97"]], "[0.9.2] - 2022-01-14": [[7, "id95"]], "[Unreleased]": [[7, "unreleased"]], "openEO CookBook": [[10, null]], "openEO Process Mapping": [[23, null]], "openEO Python Client": [[20, null]], "openeo": [[0, "openeo"]], "openeo.UDF API and usage changes in version 0.13.0": [[25, "openeo-udf-api-and-usage-changes-in-version-0-13-0"]], "openeo.api.logs": [[0, "module-openeo.api.logs"]], "openeo.api.process": [[0, "module-openeo.api.process"]], "openeo.metadata": [[0, "module-openeo.metadata"]], "openeo.processes": [[0, "openeo-processes"]], "openeo.rest.connection": [[0, "module-openeo.rest.connection"]], "openeo.rest.conversions": [[0, "module-openeo.rest.conversions"]], "openeo.rest.datacube": [[0, "module-openeo.rest.datacube"]], "openeo.rest.job": [[0, "module-openeo.rest.job"]], "openeo.rest.mlmodel": [[0, "module-openeo.rest.mlmodel"]], "openeo.rest.multiresult": [[0, "module-openeo.rest.multiresult"]], "openeo.rest.udp": [[0, "module-openeo.rest.udp"]], "openeo.rest.userfile": [[0, "module-openeo.rest.userfile"]], "openeo.rest.vectorcube": [[0, "module-openeo.rest.vectorcube"]], "openeo.testing": [[0, "module-openeo.testing"]], "openeo.testing.results": [[0, "module-openeo.testing.results"]], "openeo.udf": [[0, "module-openeo.udf.udf_data"]], "openeo.util": [[0, "module-openeo.util"]], "to do": [[11, null]]}, "docnames": ["api", "api-processbuilder", "api-processes", "auth", "basics", "batch_jobs", "best_practices", "changelog", "configuration", "cookbook/ard", "cookbook/index", "cookbook/job_manager", "cookbook/localprocessing", "cookbook/sampling", "cookbook/spectral_indices", "cookbook/tricks", "cookbook/udp_sharing", "data_access", "datacube_construction", "development", "index", "installation", "machine_learning", "process_mapping", "processes", "udf", "udp"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.viewcode": 1}, "filenames": ["api.rst", "api-processbuilder.rst", "api-processes.rst", "auth.rst", "basics.rst", "batch_jobs.rst", "best_practices.rst", "changelog.md", "configuration.rst", "cookbook/ard.rst", "cookbook/index.rst", "cookbook/job_manager.rst", "cookbook/localprocessing.rst", "cookbook/sampling.md", "cookbook/spectral_indices.rst", "cookbook/tricks.rst", "cookbook/udp_sharing.rst", "data_access.rst", "datacube_construction.rst", "development.rst", "index.rst", "installation.rst", "machine_learning.rst", "process_mapping.rst", "processes.rst", "udf.rst", "udp.rst"], "indexentries": {"__call__() (openeo.extra.job_management.processbasedjobcreator method)": [[11, "openeo.extra.job_management.ProcessBasedJobCreator.__call__", false]], "__init__() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.__init__", false]], "__init__() (openeo.rest.multiresult.multiresult method)": [[0, "openeo.rest.multiresult.MultiResult.__init__", false]], "absolute() (in module openeo.processes)": [[2, "openeo.processes.absolute", false]], "add() (in module openeo.processes)": [[2, "openeo.processes.add", false]], "add() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.add", false]], "add_backend() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.add_backend", false]], "add_dimension() (in module openeo.processes)": [[2, "openeo.processes.add_dimension", false]], "add_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.add_dimension", false]], "aggregate_spatial() (in module openeo.processes)": [[2, "openeo.processes.aggregate_spatial", false]], "aggregate_spatial() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.aggregate_spatial", false]], "aggregate_spatial_window() (in module openeo.processes)": [[2, "openeo.processes.aggregate_spatial_window", false]], "aggregate_spatial_window() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.aggregate_spatial_window", false]], "aggregate_temporal() (in module openeo.processes)": [[2, "openeo.processes.aggregate_temporal", false]], "aggregate_temporal() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.aggregate_temporal", false]], "aggregate_temporal_period() (in module openeo.processes)": [[2, "openeo.processes.aggregate_temporal_period", false]], "aggregate_temporal_period() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.aggregate_temporal_period", false]], "all() (in module openeo.processes)": [[2, "openeo.processes.all", false]], "and_() (in module openeo.processes)": [[2, "openeo.processes.and_", false]], "anomaly() (in module openeo.processes)": [[2, "openeo.processes.anomaly", false]], "any() (in module openeo.processes)": [[2, "openeo.processes.any", false]], "append_and_rescale_indices() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.append_and_rescale_indices", false]], "append_band() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.append_band", false]], "append_index() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.append_index", false]], "append_indices() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.append_indices", false]], "apply() (in module openeo.processes)": [[2, "openeo.processes.apply", false]], "apply() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.apply", false]], "apply_datacube() (in module openeo.udf.udf_signatures)": [[25, "openeo.udf.udf_signatures.apply_datacube", false]], "apply_dimension() (in module openeo.processes)": [[2, "openeo.processes.apply_dimension", false]], "apply_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.apply_dimension", false]], "apply_dimension() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.apply_dimension", false]], "apply_kernel() (in module openeo.processes)": [[2, "openeo.processes.apply_kernel", false]], "apply_kernel() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.apply_kernel", false]], "apply_metadata() (in module openeo.udf.udf_signatures)": [[25, "openeo.udf.udf_signatures.apply_metadata", false]], "apply_neighborhood() (in module openeo.processes)": [[2, "openeo.processes.apply_neighborhood", false]], "apply_neighborhood() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.apply_neighborhood", false]], "apply_polygon() (in module openeo.processes)": [[2, "openeo.processes.apply_polygon", false]], "apply_polygon() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.apply_polygon", false]], "apply_timeseries() (in module openeo.udf.udf_signatures)": [[25, "openeo.udf.udf_signatures.apply_timeseries", false]], "apply_udf_data() (in module openeo.udf.udf_signatures)": [[25, "openeo.udf.udf_signatures.apply_udf_data", false]], "apply_vectorcube() (in module openeo.udf.udf_signatures)": [[25, "openeo.udf.udf_signatures.apply_vectorcube", false]], "arccos() (in module openeo.processes)": [[2, "openeo.processes.arccos", false]], "arcosh() (in module openeo.processes)": [[2, "openeo.processes.arcosh", false]], "arcsin() (in module openeo.processes)": [[2, "openeo.processes.arcsin", false]], "arctan() (in module openeo.processes)": [[2, "openeo.processes.arctan", false]], "arctan2() (in module openeo.processes)": [[2, "openeo.processes.arctan2", false]], "ard_normalized_radar_backscatter() (in module openeo.processes)": [[2, "openeo.processes.ard_normalized_radar_backscatter", false]], "ard_normalized_radar_backscatter() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.ard_normalized_radar_backscatter", false]], "ard_surface_reflectance() (in module openeo.processes)": [[2, "openeo.processes.ard_surface_reflectance", false]], "ard_surface_reflectance() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.ard_surface_reflectance", false]], "array (openeo.udf.xarraydatacube.xarraydatacube property)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.array", false]], "array() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.array", false]], "array_append() (in module openeo.processes)": [[2, "openeo.processes.array_append", false]], "array_apply() (in module openeo.processes)": [[2, "openeo.processes.array_apply", false]], "array_concat() (in module openeo.processes)": [[2, "openeo.processes.array_concat", false]], "array_contains() (in module openeo.processes)": [[2, "openeo.processes.array_contains", false]], "array_create() (in module openeo.processes)": [[2, "openeo.processes.array_create", false]], "array_create_labeled() (in module openeo.processes)": [[2, "openeo.processes.array_create_labeled", false]], "array_element() (in module openeo.processes)": [[2, "openeo.processes.array_element", false]], "array_filter() (in module openeo.processes)": [[2, "openeo.processes.array_filter", false]], "array_find() (in module openeo.processes)": [[2, "openeo.processes.array_find", false]], "array_find_label() (in module openeo.processes)": [[2, "openeo.processes.array_find_label", false]], "array_interpolate_linear() (in module openeo.processes)": [[2, "openeo.processes.array_interpolate_linear", false]], "array_labels() (in module openeo.processes)": [[2, "openeo.processes.array_labels", false]], "array_modify() (in module openeo.processes)": [[2, "openeo.processes.array_modify", false]], "arsinh() (in module openeo.processes)": [[2, "openeo.processes.arsinh", false]], "artanh() (in module openeo.processes)": [[2, "openeo.processes.artanh", false]], "as_curl() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.as_curl", false]], "assert_job_results_allclose() (in module openeo.testing.results)": [[0, "openeo.testing.results.assert_job_results_allclose", false]], "assert_user_defined_process_support() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.assert_user_defined_process_support", false]], "assert_xarray_allclose() (in module openeo.testing.results)": [[0, "openeo.testing.results.assert_xarray_allclose", false]], "assert_xarray_dataarray_allclose() (in module openeo.testing.results)": [[0, "openeo.testing.results.assert_xarray_dataarray_allclose", false]], "assert_xarray_dataset_allclose() (in module openeo.testing.results)": [[0, "openeo.testing.results.assert_xarray_dataset_allclose", false]], "atmospheric_correction() (in module openeo.processes)": [[2, "openeo.processes.atmospheric_correction", false]], "atmospheric_correction() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.atmospheric_correction", false]], "authenticate_basic() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_basic", false]], "authenticate_oidc() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc", false]], "authenticate_oidc_access_token() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_access_token", false]], "authenticate_oidc_authorization_code() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_authorization_code", false]], "authenticate_oidc_client_credentials() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_client_credentials", false]], "authenticate_oidc_device() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_device", false]], "authenticate_oidc_refresh_token() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_refresh_token", false]], "authenticate_oidc_resource_owner_password_credentials() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.authenticate_oidc_resource_owner_password_credentials", false]], "band() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.band", false]], "band_filter() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.band_filter", false]], "band_index() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.band_index", false]], "band_name() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.band_name", false]], "banddimension (class in openeo.metadata)": [[0, "openeo.metadata.BandDimension", false]], "batch job": [[5, "index-0", false], [5, "index-1", false], [5, "index-2", false], [5, "index-3", false], [5, "index-4", false], [5, "index-5", false], [5, "index-6", false], [5, "index-7", false], [5, "index-8", false]], "batchjob (class in openeo.rest.job)": [[0, "openeo.rest.job.BatchJob", false]], "bboxdict (class in openeo.util)": [[0, "openeo.util.BBoxDict", false]], "between() (in module openeo.processes)": [[2, "openeo.processes.between", false]], "boolean() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.boolean", false]], "bounding_box() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.bounding_box", false]], "build_process_dict() (in module openeo.rest.udp)": [[0, "openeo.rest.udp.build_process_dict", false]], "capabilities() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.capabilities", false]], "ceil() (in module openeo.processes)": [[2, "openeo.processes.ceil", false]], "chunk_polygon() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.chunk_polygon", false]], "chunking": [[25, "index-2", false]], "climatological_normal() (in module openeo.processes)": [[2, "openeo.processes.climatological_normal", false]], "clip() (in module openeo.processes)": [[2, "openeo.processes.clip", false]], "cloud_detection() (in module openeo.processes)": [[2, "openeo.processes.cloud_detection", false]], "collection_items() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.collection_items", false]], "collection_property() (in module openeo.rest.graph_building)": [[0, "openeo.rest.graph_building.collection_property", false]], "collectionmetadata (class in openeo.metadata)": [[0, "openeo.metadata.CollectionMetadata", false]], "collectionproperty (class in openeo.rest.graph_building)": [[0, "openeo.rest.graph_building.CollectionProperty", false]], "compute_and_rescale_indices() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.compute_and_rescale_indices", false]], "compute_index() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.compute_index", false]], "compute_indices() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.compute_indices", false]], "connect() (in module openeo)": [[0, "openeo.connect", false]], "connection (class in openeo.rest.connection)": [[0, "openeo.rest.connection.Connection", false]], "constant() (in module openeo.processes)": [[2, "openeo.processes.constant", false]], "cos() (in module openeo.processes)": [[2, "openeo.processes.cos", false]], "cosh() (in module openeo.processes)": [[2, "openeo.processes.cosh", false]], "count() (in module openeo.processes)": [[2, "openeo.processes.count", false]], "count_by_status() (openeo.extra.job_management.jobdatabaseinterface method)": [[11, "openeo.extra.job_management.JobDatabaseInterface.count_by_status", false]], "count_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.count_time", false]], "create": [[5, "index-1", false]], "create_collection() (openeo.rest.datacube.datacube class method)": [[0, "openeo.rest.datacube.DataCube.create_collection", false]], "create_data_cube() (in module openeo.processes)": [[2, "openeo.processes.create_data_cube", false]], "create_job() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.create_job", false]], "create_job() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.create_job", false]], "create_job() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.create_job", false]], "create_job() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.create_job", false]], "csvjobdatabase (class in openeo.extra.job_management)": [[11, "openeo.extra.job_management.CsvJobDatabase", false]], "cummax() (in module openeo.processes)": [[2, "openeo.processes.cummax", false]], "cummin() (in module openeo.processes)": [[2, "openeo.processes.cummin", false]], "cumproduct() (in module openeo.processes)": [[2, "openeo.processes.cumproduct", false]], "cumsum() (in module openeo.processes)": [[2, "openeo.processes.cumsum", false]], "datacube (class in openeo.rest.datacube)": [[0, "openeo.rest.datacube.DataCube", false]], "datacube() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.datacube", false]], "datacube_from_file() (in module openeo.rest.conversions)": [[0, "openeo.rest.conversions.datacube_from_file", false]], "datacube_from_flat_graph() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.datacube_from_flat_graph", false]], "datacube_from_json() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.datacube_from_json", false]], "datacube_from_process() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.datacube_from_process", false]], "datacube_list (openeo.udf.udf_data.udfdata property)": [[0, "openeo.udf.udf_data.UdfData.datacube_list", false]], "datacube_plot() (in module openeo.rest.conversions)": [[0, "openeo.rest.conversions.datacube_plot", false]], "datacube_to_file() (in module openeo.rest.conversions)": [[0, "openeo.rest.conversions.datacube_to_file", false]], "date() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.date", false]], "date_between() (in module openeo.processes)": [[2, "openeo.processes.date_between", false]], "date_difference() (in module openeo.processes)": [[2, "openeo.processes.date_difference", false]], "date_shift() (in module openeo.processes)": [[2, "openeo.processes.date_shift", false]], "date_time() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.date_time", false]], "delete() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.delete", false]], "delete() (openeo.rest.udp.restuserdefinedprocess method)": [[0, "openeo.rest.udp.RESTUserDefinedProcess.delete", false]], "delete() (openeo.rest.userfile.userfile method)": [[0, "openeo.rest.userfile.UserFile.delete", false]], "delete_job() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.delete_job", false]], "describe() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.describe", false]], "describe() (openeo.rest.udp.restuserdefinedprocess method)": [[0, "openeo.rest.udp.RESTUserDefinedProcess.describe", false]], "describe_account() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.describe_account", false]], "describe_collection() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.describe_collection", false]], "describe_job() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.describe_job", false]], "describe_process() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.describe_process", false]], "dimension_labels() (in module openeo.processes)": [[2, "openeo.processes.dimension_labels", false]], "dimension_labels() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.dimension_labels", false]], "divide() (in module openeo.processes)": [[2, "openeo.processes.divide", false]], "divide() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.divide", false]], "download() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.download", false]], "download() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.download", false]], "download() (openeo.rest.job.resultasset method)": [[0, "openeo.rest.job.ResultAsset.download", false]], "download() (openeo.rest.userfile.userfile method)": [[0, "openeo.rest.userfile.UserFile.download", false]], "download() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.download", false]], "download_file() (openeo.rest.job.jobresults method)": [[0, "openeo.rest.job.JobResults.download_file", false]], "download_files() (openeo.rest.job.jobresults method)": [[0, "openeo.rest.job.JobResults.download_files", false]], "download_result() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.download_result", false]], "download_results() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.download_results", false]], "drop_dimension() (in module openeo.processes)": [[2, "openeo.processes.drop_dimension", false]], "drop_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.drop_dimension", false]], "e() (in module openeo.processes)": [[2, "openeo.processes.e", false]], "ensure_job_dir_exists() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.ensure_job_dir_exists", false]], "eq() (in module openeo.processes)": [[2, "openeo.processes.eq", false]], "estimate() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.estimate", false]], "estimate_job() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.estimate_job", false]], "execute() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.execute", false]], "execute() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.execute", false]], "execute() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.execute", false]], "execute_batch() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.execute_batch", false]], "execute_batch() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.execute_batch", false]], "execute_batch() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.execute_batch", false]], "execute_local_udf() (in module openeo.udf.run_code)": [[0, "openeo.udf.run_code.execute_local_udf", false]], "execute_local_udf() (openeo.rest.datacube.datacube static method)": [[0, "openeo.rest.datacube.DataCube.execute_local_udf", false]], "exists() (openeo.extra.job_management.jobdatabaseinterface method)": [[11, "openeo.extra.job_management.JobDatabaseInterface.exists", false]], "exp() (in module openeo.processes)": [[2, "openeo.processes.exp", false]], "extract_udf_dependencies() (in module openeo.udf.run_code)": [[0, "openeo.udf.run_code.extract_udf_dependencies", false]], "extrema() (in module openeo.processes)": [[2, "openeo.processes.extrema", false]], "feature_collection_list (openeo.udf.udf_data.udfdata property)": [[0, "openeo.udf.udf_data.UdfData.feature_collection_list", false]], "filter_bands() (in module openeo.processes)": [[2, "openeo.processes.filter_bands", false]], "filter_bands() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.filter_bands", false]], "filter_bands() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.filter_bands", false]], "filter_bands() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.filter_bands", false]], "filter_bbox() (in module openeo.processes)": [[2, "openeo.processes.filter_bbox", false]], "filter_bbox() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.filter_bbox", false]], "filter_bbox() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.filter_bbox", false]], "filter_labels() (in module openeo.processes)": [[2, "openeo.processes.filter_labels", false]], "filter_labels() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.filter_labels", false]], "filter_labels() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.filter_labels", false]], "filter_spatial() (in module openeo.processes)": [[2, "openeo.processes.filter_spatial", false]], "filter_spatial() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.filter_spatial", false]], "filter_temporal() (in module openeo.processes)": [[2, "openeo.processes.filter_temporal", false]], "filter_temporal() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.filter_temporal", false]], "filter_vector() (in module openeo.processes)": [[2, "openeo.processes.filter_vector", false]], "filter_vector() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.filter_vector", false]], "first() (in module openeo.processes)": [[2, "openeo.processes.first", false]], "fit_class_random_forest() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.fit_class_random_forest", false]], "fit_curve() (in module openeo.processes)": [[2, "openeo.processes.fit_curve", false]], "fit_curve() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.fit_curve", false]], "fit_regr_random_forest() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.fit_regr_random_forest", false]], "flat_graph() (openeo.internal.graph_building.pgnode method)": [[0, "openeo.internal.graph_building.PGNode.flat_graph", false]], "flat_graph() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.flat_graph", false]], "flat_graph() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.flat_graph", false]], "flat_graph() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.flat_graph", false]], "flatgraphablemixin (class in openeo.internal.graph_building)": [[0, "openeo.internal.graph_building.FlatGraphableMixin", false]], "flatten_dimensions() (in module openeo.processes)": [[2, "openeo.processes.flatten_dimensions", false]], "flatten_dimensions() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.flatten_dimensions", false]], "floor() (in module openeo.processes)": [[2, "openeo.processes.floor", false]], "from_dict() (openeo.udf.udf_data.udfdata class method)": [[0, "openeo.udf.udf_data.UdfData.from_dict", false]], "from_dict() (openeo.udf.xarraydatacube.xarraydatacube class method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.from_dict", false]], "from_dict() (openeo.util.bboxdict class method)": [[0, "openeo.util.BBoxDict.from_dict", false]], "from_file() (openeo.rest._datacube.udf class method)": [[0, "openeo.rest._datacube.UDF.from_file", false]], "from_file() (openeo.udf.xarraydatacube.xarraydatacube class method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.from_file", false]], "from_flat_graph() (openeo.internal.graph_building.pgnode static method)": [[0, "openeo.internal.graph_building.PGNode.from_flat_graph", false]], "from_metadata() (openeo.rest.userfile.userfile class method)": [[0, "openeo.rest.userfile.UserFile.from_metadata", false]], "from_sequence() (openeo.util.bboxdict class method)": [[0, "openeo.util.BBoxDict.from_sequence", false]], "from_url() (openeo.rest._datacube.udf class method)": [[0, "openeo.rest._datacube.UDF.from_url", false]], "geojson() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.geojson", false]], "get_array() (openeo.udf.xarraydatacube.xarraydatacube method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.get_array", false]], "get_asset() (openeo.rest.job.jobresults method)": [[0, "openeo.rest.job.JobResults.get_asset", false]], "get_assets() (openeo.rest.job.jobresults method)": [[0, "openeo.rest.job.JobResults.get_assets", false]], "get_by_status() (openeo.extra.job_management.jobdatabaseinterface method)": [[11, "openeo.extra.job_management.JobDatabaseInterface.get_by_status", false]], "get_datacube_list() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.get_datacube_list", false]], "get_error_log_path() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.get_error_log_path", false]], "get_feature_collection_list() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.get_feature_collection_list", false]], "get_file() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.get_file", false]], "get_job_dir() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.get_job_dir", false]], "get_job_metadata_path() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.get_job_metadata_path", false]], "get_metadata() (openeo.rest.job.jobresults method)": [[0, "openeo.rest.job.JobResults.get_metadata", false]], "get_path() (openeo.testing.testdataloader method)": [[0, "openeo.testing.TestDataLoader.get_path", false]], "get_result() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.get_result", false]], "get_results() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.get_results", false]], "get_results_metadata_url() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.get_results_metadata_url", false]], "get_run_udf_callback() (openeo.rest._datacube.udf method)": [[0, "openeo.rest._datacube.UDF.get_run_udf_callback", false]], "get_structured_data_list() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.get_structured_data_list", false]], "graph_add_node() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.graph_add_node", false]], "gt() (in module openeo.processes)": [[2, "openeo.processes.gt", false]], "gte() (in module openeo.processes)": [[2, "openeo.processes.gte", false]], "href (openeo.rest.job.resultasset attribute)": [[0, "openeo.rest.job.ResultAsset.href", false]], "if_() (in module openeo.processes)": [[2, "openeo.processes.if_", false]], "imagecollection() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.imagecollection", false]], "inspect() (in module openeo.processes)": [[2, "openeo.processes.inspect", false]], "inspect() (in module openeo.udf.debug)": [[0, "openeo.udf.debug.inspect", false]], "int() (in module openeo.processes)": [[2, "openeo.processes.int", false]], "integer() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.integer", false]], "invalidtimeseriesexception": [[0, "openeo.rest.conversions.InvalidTimeSeriesException", false]], "is_infinite() (in module openeo.processes)": [[2, "openeo.processes.is_infinite", false]], "is_nan() (in module openeo.processes)": [[2, "openeo.processes.is_nan", false]], "is_nodata() (in module openeo.processes)": [[2, "openeo.processes.is_nodata", false]], "is_valid() (in module openeo.processes)": [[2, "openeo.processes.is_valid", false]], "job": [[5, "index-0", false]], "job() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.job", false]], "job_id (openeo.rest.job.batchjob attribute)": [[0, "openeo.rest.job.BatchJob.job_id", false]], "job_logs() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.job_logs", false]], "job_results() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.job_results", false]], "jobdatabaseinterface (class in openeo.extra.job_management)": [[11, "openeo.extra.job_management.JobDatabaseInterface", false]], "jobresults (class in openeo.rest.job)": [[0, "openeo.rest.job.JobResults", false]], "last() (in module openeo.processes)": [[2, "openeo.processes.last", false]], "linear_scale_range() (in module openeo.processes)": [[2, "openeo.processes.linear_scale_range", false]], "linear_scale_range() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.linear_scale_range", false]], "list_collection_ids() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_collection_ids", false]], "list_collections() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_collections", false]], "list_file_formats() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_file_formats", false]], "list_file_types() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_file_types", false]], "list_files() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_files", false]], "list_indices() (in module openeo.extra.spectral_indices)": [[14, "openeo.extra.spectral_indices.list_indices", false]], "list_jobs() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_jobs", false]], "list_processes() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_processes", false]], "list_results() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.list_results", false]], "list_service_types() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_service_types", false]], "list_services() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_services", false]], "list_udf_runtimes() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_udf_runtimes", false]], "list_user_defined_processes() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.list_user_defined_processes", false]], "listing": [[5, "index-3", false]], "ln() (in module openeo.processes)": [[2, "openeo.processes.ln", false]], "ln() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.ln", false]], "load_bytes() (openeo.rest.job.resultasset method)": [[0, "openeo.rest.job.ResultAsset.load_bytes", false]], "load_collection() (in module openeo.processes)": [[2, "openeo.processes.load_collection", false]], "load_collection() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_collection", false]], "load_collection() (openeo.rest.datacube.datacube class method)": [[0, "openeo.rest.datacube.DataCube.load_collection", false]], "load_disk_collection() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_disk_collection", false]], "load_disk_collection() (openeo.rest.datacube.datacube class method)": [[0, "openeo.rest.datacube.DataCube.load_disk_collection", false]], "load_geojson() (in module openeo.processes)": [[2, "openeo.processes.load_geojson", false]], "load_geojson() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_geojson", false]], "load_geojson() (openeo.rest.vectorcube.vectorcube class method)": [[0, "openeo.rest.vectorcube.VectorCube.load_geojson", false]], "load_json() (openeo.rest.job.resultasset method)": [[0, "openeo.rest.job.ResultAsset.load_json", false]], "load_json() (openeo.testing.testdataloader method)": [[0, "openeo.testing.TestDataLoader.load_json", false]], "load_json_resource() (in module openeo.util)": [[0, "openeo.util.load_json_resource", false]], "load_ml_model() (in module openeo.processes)": [[2, "openeo.processes.load_ml_model", false]], "load_ml_model() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_ml_model", false]], "load_ml_model() (openeo.rest.mlmodel.mlmodel static method)": [[0, "openeo.rest.mlmodel.MlModel.load_ml_model", false]], "load_result() (in module openeo.processes)": [[2, "openeo.processes.load_result", false]], "load_result() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_result", false]], "load_stac() (in module openeo.processes)": [[2, "openeo.processes.load_stac", false]], "load_stac() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_stac", false]], "load_stac() (openeo.rest.datacube.datacube class method)": [[0, "openeo.rest.datacube.DataCube.load_stac", false]], "load_stac_from_job() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_stac_from_job", false]], "load_uploaded_files() (in module openeo.processes)": [[2, "openeo.processes.load_uploaded_files", false]], "load_url() (in module openeo.processes)": [[2, "openeo.processes.load_url", false]], "load_url() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.load_url", false]], "load_url() (openeo.rest.vectorcube.vectorcube class method)": [[0, "openeo.rest.vectorcube.VectorCube.load_url", false]], "log() (in module openeo.processes)": [[2, "openeo.processes.log", false]], "log10() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.log10", false]], "log2() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.log2", false]], "logarithm() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.logarithm", false]], "logentry (class in openeo.api.logs)": [[0, "openeo.api.logs.LogEntry", false]], "logical_and() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.logical_and", false]], "logical_or() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.logical_or", false]], "logs": [[5, "index-7", false]], "logs() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.logs", false]], "lt() (in module openeo.processes)": [[2, "openeo.processes.lt", false]], "lte() (in module openeo.processes)": [[2, "openeo.processes.lte", false]], "mask() (in module openeo.processes)": [[2, "openeo.processes.mask", false]], "mask() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.mask", false]], "mask_polygon() (in module openeo.processes)": [[2, "openeo.processes.mask_polygon", false]], "mask_polygon() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.mask_polygon", false]], "max() (in module openeo.processes)": [[2, "openeo.processes.max", false]], "max_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.max_time", false]], "mean() (in module openeo.processes)": [[2, "openeo.processes.mean", false]], "mean_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.mean_time", false]], "median() (in module openeo.processes)": [[2, "openeo.processes.median", false]], "median_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.median_time", false]], "merge() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.merge", false]], "merge_cubes() (in module openeo.processes)": [[2, "openeo.processes.merge_cubes", false]], "merge_cubes() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.merge_cubes", false]], "metadata (openeo.rest.job.resultasset attribute)": [[0, "openeo.rest.job.ResultAsset.metadata", false]], "min() (in module openeo.processes)": [[2, "openeo.processes.min", false]], "min_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.min_time", false]], "mlmodel (class in openeo.rest.mlmodel)": [[0, "openeo.rest.mlmodel.MlModel", false]], "mod() (in module openeo.processes)": [[2, "openeo.processes.mod", false]], "module": [[0, "module-openeo.api.logs", false], [0, "module-openeo.api.process", false], [0, "module-openeo.internal.graph_building", false], [0, "module-openeo.metadata", false], [0, "module-openeo.rest._datacube", false], [0, "module-openeo.rest.connection", false], [0, "module-openeo.rest.conversions", false], [0, "module-openeo.rest.datacube", false], [0, "module-openeo.rest.graph_building", false], [0, "module-openeo.rest.job", false], [0, "module-openeo.rest.mlmodel", false], [0, "module-openeo.rest.multiresult", false], [0, "module-openeo.rest.udp", false], [0, "module-openeo.rest.userfile", false], [0, "module-openeo.rest.vectorcube", false], [0, "module-openeo.testing", false], [0, "module-openeo.testing.results", false], [0, "module-openeo.udf.debug", false], [0, "module-openeo.udf.run_code", false], [0, "module-openeo.udf.structured_data", false], [0, "module-openeo.udf.udf_data", false], [0, "module-openeo.udf.xarraydatacube", false], [0, "module-openeo.util", false], [2, "module-openeo.processes", false], [14, "module-openeo.extra.spectral_indices", false], [25, "module-openeo.udf.udf_signatures", false]], "multibackendjobmanager (class in openeo.extra.job_management)": [[11, "openeo.extra.job_management.MultiBackendJobManager", false]], "multiply() (in module openeo.processes)": [[2, "openeo.processes.multiply", false]], "multiply() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.multiply", false]], "multiresult (class in openeo.rest.multiresult)": [[0, "openeo.rest.multiresult.MultiResult", false]], "name (openeo.rest.job.resultasset attribute)": [[0, "openeo.rest.job.ResultAsset.name", false]], "nan() (in module openeo.processes)": [[2, "openeo.processes.nan", false]], "ndvi() (in module openeo.processes)": [[2, "openeo.processes.ndvi", false]], "ndvi() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.ndvi", false]], "neq() (in module openeo.processes)": [[2, "openeo.processes.neq", false]], "normalize_crs() (in module openeo.util)": [[0, "openeo.util.normalize_crs", false]], "normalize_log_level() (in module openeo.api.logs)": [[0, "openeo.api.logs.normalize_log_level", false]], "normalized_difference() (in module openeo.processes)": [[2, "openeo.processes.normalized_difference", false]], "normalized_difference() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.normalized_difference", false]], "not_() (in module openeo.processes)": [[2, "openeo.processes.not_", false]], "number() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.number", false]], "object": [[5, "index-2", false]], "object() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.object", false]], "on_job_cancel() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.on_job_cancel", false]], "on_job_done() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.on_job_done", false]], "on_job_error() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.on_job_error", false]], "openeo.api.logs": [[0, "module-openeo.api.logs", false]], "openeo.api.process": [[0, "module-openeo.api.process", false]], "openeo.extra.spectral_indices": [[14, "module-openeo.extra.spectral_indices", false]], "openeo.internal.graph_building": [[0, "module-openeo.internal.graph_building", false]], "openeo.metadata": [[0, "module-openeo.metadata", false]], "openeo.processes": [[2, "module-openeo.processes", false]], "openeo.rest._datacube": [[0, "module-openeo.rest._datacube", false]], "openeo.rest.connection": [[0, "module-openeo.rest.connection", false]], "openeo.rest.conversions": [[0, "module-openeo.rest.conversions", false]], "openeo.rest.datacube": [[0, "module-openeo.rest.datacube", false]], "openeo.rest.graph_building": [[0, "module-openeo.rest.graph_building", false]], "openeo.rest.job": [[0, "module-openeo.rest.job", false]], "openeo.rest.mlmodel": [[0, "module-openeo.rest.mlmodel", false]], "openeo.rest.multiresult": [[0, "module-openeo.rest.multiresult", false]], "openeo.rest.udp": [[0, "module-openeo.rest.udp", false]], "openeo.rest.userfile": [[0, "module-openeo.rest.userfile", false]], "openeo.rest.vectorcube": [[0, "module-openeo.rest.vectorcube", false]], "openeo.testing": [[0, "module-openeo.testing", false]], "openeo.testing.results": [[0, "module-openeo.testing.results", false]], "openeo.udf.debug": [[0, "module-openeo.udf.debug", false]], "openeo.udf.run_code": [[0, "module-openeo.udf.run_code", false]], "openeo.udf.structured_data": [[0, "module-openeo.udf.structured_data", false]], "openeo.udf.udf_data": [[0, "module-openeo.udf.udf_data", false]], "openeo.udf.udf_signatures": [[25, "module-openeo.udf.udf_signatures", false]], "openeo.udf.xarraydatacube": [[0, "module-openeo.udf.xarraydatacube", false]], "openeo.util": [[0, "module-openeo.util", false]], "or_() (in module openeo.processes)": [[2, "openeo.processes.or_", false]], "order() (in module openeo.processes)": [[2, "openeo.processes.order", false]], "parameter (class in openeo.api.process)": [[0, "openeo.api.process.Parameter", false]], "parquetjobdatabase (class in openeo.extra.job_management)": [[11, "openeo.extra.job_management.ParquetJobDatabase", false]], "persist() (openeo.extra.job_management.jobdatabaseinterface method)": [[11, "openeo.extra.job_management.JobDatabaseInterface.persist", false]], "pgnode (class in openeo.internal.graph_building)": [[0, "openeo.internal.graph_building.PGNode", false]], "pi() (in module openeo.processes)": [[2, "openeo.processes.pi", false]], "plot() (openeo.udf.xarraydatacube.xarraydatacube method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.plot", false]], "polling loop": [[5, "index-6", false]], "polygonal_histogram_timeseries() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.polygonal_histogram_timeseries", false]], "polygonal_mean_timeseries() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.polygonal_mean_timeseries", false]], "polygonal_median_timeseries() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.polygonal_median_timeseries", false]], "polygonal_standarddeviation_timeseries() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.polygonal_standarddeviation_timeseries", false]], "power() (in module openeo.processes)": [[2, "openeo.processes.power", false]], "power() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.power", false]], "predict_curve() (in module openeo.processes)": [[2, "openeo.processes.predict_curve", false]], "predict_curve() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.predict_curve", false]], "predict_random_forest() (in module openeo.processes)": [[2, "openeo.processes.predict_random_forest", false]], "predict_random_forest() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.predict_random_forest", false]], "preview() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.preview", false]], "print_json() (openeo.internal.graph_building.flatgraphablemixin method)": [[0, "openeo.internal.graph_building.FlatGraphableMixin.print_json", false]], "print_json() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.print_json", false]], "print_json() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.print_json", false]], "print_json() (openeo.rest.multiresult.multiresult method)": [[0, "openeo.rest.multiresult.MultiResult.print_json", false]], "print_json() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.print_json", false]], "process() (in module openeo.processes)": [[0, "openeo.processes.process", false]], "process() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.process", false]], "process() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.process", false]], "process_with_node() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.process_with_node", false]], "processbasedjobcreator (class in openeo.extra.job_management)": [[11, "openeo.extra.job_management.ProcessBasedJobCreator", false]], "processbuilder (class in openeo.processes)": [[2, "openeo.processes.ProcessBuilder", false]], "product() (in module openeo.processes)": [[2, "openeo.processes.product", false]], "quantiles() (in module openeo.processes)": [[2, "openeo.processes.quantiles", false]], "raster_cube() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.raster_cube", false]], "raster_to_vector() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.raster_to_vector", false]], "read() (openeo.extra.job_management.jobdatabaseinterface method)": [[11, "openeo.extra.job_management.JobDatabaseInterface.read", false]], "rearrange() (in module openeo.processes)": [[2, "openeo.processes.rearrange", false]], "reduce_bands() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_bands", false]], "reduce_bands_udf() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_bands_udf", false]], "reduce_dimension() (in module openeo.processes)": [[2, "openeo.processes.reduce_dimension", false]], "reduce_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_dimension", false]], "reduce_spatial() (in module openeo.processes)": [[2, "openeo.processes.reduce_spatial", false]], "reduce_spatial() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_spatial", false]], "reduce_temporal() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_temporal", false]], "reduce_temporal_simple() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_temporal_simple", false]], "reduce_temporal_udf() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_temporal_udf", false]], "reduce_tiles_over_time() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.reduce_tiles_over_time", false]], "remove_service() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.remove_service", false]], "rename() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.rename", false]], "rename() (openeo.metadata.spatialdimension method)": [[0, "openeo.metadata.SpatialDimension.rename", false]], "rename() (openeo.metadata.temporaldimension method)": [[0, "openeo.metadata.TemporalDimension.rename", false]], "rename_dimension() (in module openeo.processes)": [[2, "openeo.processes.rename_dimension", false]], "rename_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.rename_dimension", false]], "rename_labels() (in module openeo.processes)": [[2, "openeo.processes.rename_labels", false]], "rename_labels() (openeo.metadata.banddimension method)": [[0, "openeo.metadata.BandDimension.rename_labels", false]], "rename_labels() (openeo.metadata.temporaldimension method)": [[0, "openeo.metadata.TemporalDimension.rename_labels", false]], "rename_labels() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.rename_labels", false]], "request() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.request", false]], "resample_cube_spatial() (in module openeo.processes)": [[2, "openeo.processes.resample_cube_spatial", false]], "resample_cube_spatial() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.resample_cube_spatial", false]], "resample_cube_temporal() (in module openeo.processes)": [[2, "openeo.processes.resample_cube_temporal", false]], "resample_cube_temporal() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.resample_cube_temporal", false]], "resample_spatial() (in module openeo.processes)": [[2, "openeo.processes.resample_spatial", false]], "resample_spatial() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.resample_spatial", false]], "resolution_merge() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.resolution_merge", false]], "restjob (class in openeo.rest.job)": [[0, "openeo.rest.job.RESTJob", false]], "restuserdefinedprocess (class in openeo.rest.udp)": [[0, "openeo.rest.udp.RESTUserDefinedProcess", false]], "result_node() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.result_node", false]], "result_node() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.result_node", false]], "result_node() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.result_node", false]], "resultasset (class in openeo.rest.job)": [[0, "openeo.rest.job.ResultAsset", false]], "results": [[5, "index-8", false]], "round() (in module openeo.processes)": [[2, "openeo.processes.round", false]], "run_jobs() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.run_jobs", false]], "run_synchronous() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.run_synchronous", false]], "run_udf() (in module openeo.processes)": [[2, "openeo.processes.run_udf", false]], "run_udf() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.run_udf", false]], "run_udf_externally() (in module openeo.processes)": [[2, "openeo.processes.run_udf_externally", false]], "sar_backscatter() (in module openeo.processes)": [[2, "openeo.processes.sar_backscatter", false]], "sar_backscatter() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.sar_backscatter", false]], "save_ml_model() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.save_ml_model", false]], "save_result() (in module openeo.processes)": [[2, "openeo.processes.save_result", false]], "save_result() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.save_result", false]], "save_result() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.save_result", false]], "save_to_file() (openeo.udf.xarraydatacube.xarraydatacube method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.save_to_file", false]], "save_user_defined_process() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.save_user_defined_process", false]], "save_user_defined_process() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.save_user_defined_process", false]], "sd() (in module openeo.processes)": [[2, "openeo.processes.sd", false]], "send_job() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.send_job", false]], "send_job() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.send_job", false]], "service() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.service", false]], "set_datacube_list() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.set_datacube_list", false]], "set_structured_data_list() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.set_structured_data_list", false]], "sgn() (in module openeo.processes)": [[2, "openeo.processes.sgn", false]], "sin() (in module openeo.processes)": [[2, "openeo.processes.sin", false]], "sinh() (in module openeo.processes)": [[2, "openeo.processes.sinh", false]], "sort() (in module openeo.processes)": [[2, "openeo.processes.sort", false]], "spatial_extent() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.spatial_extent", false]], "spatialdimension (class in openeo.metadata)": [[0, "openeo.metadata.SpatialDimension", false]], "sqrt() (in module openeo.processes)": [[2, "openeo.processes.sqrt", false]], "start": [[5, "index-4", false]], "start() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.start", false]], "start_and_wait() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.start_and_wait", false]], "start_job() (openeo.extra.job_management.processbasedjobcreator method)": [[11, "openeo.extra.job_management.ProcessBasedJobCreator.start_job", false]], "start_job() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.start_job", false]], "start_job_thread() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.start_job_thread", false]], "status": [[5, "index-5", false]], "status() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.status", false]], "stop() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.stop", false]], "stop_job() (openeo.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.stop_job", false]], "stop_job_thread() (openeo.extra.job_management.multibackendjobmanager method)": [[11, "openeo.extra.job_management.MultiBackendJobManager.stop_job_thread", false]], "store() (openeo.rest.udp.restuserdefinedprocess method)": [[0, "openeo.rest.udp.RESTUserDefinedProcess.store", false]], "string() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.string", false]], "structured_data_list (openeo.udf.udf_data.udfdata property)": [[0, "openeo.udf.udf_data.UdfData.structured_data_list", false]], "structureddata (class in openeo.udf.structured_data)": [[0, "openeo.udf.structured_data.StructuredData", false]], "subtract() (in module openeo.processes)": [[2, "openeo.processes.subtract", false]], "subtract() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.subtract", false]], "sum() (in module openeo.processes)": [[2, "openeo.processes.sum", false]], "tan() (in module openeo.processes)": [[2, "openeo.processes.tan", false]], "tanh() (in module openeo.processes)": [[2, "openeo.processes.tanh", false]], "temporal_interval() (openeo.api.process.parameter class method)": [[0, "openeo.api.process.Parameter.temporal_interval", false]], "temporaldimension (class in openeo.metadata)": [[0, "openeo.metadata.TemporalDimension", false]], "testdataloader (class in openeo.testing)": [[0, "openeo.testing.TestDataLoader", false]], "text_begins() (in module openeo.processes)": [[2, "openeo.processes.text_begins", false]], "text_concat() (in module openeo.processes)": [[2, "openeo.processes.text_concat", false]], "text_contains() (in module openeo.processes)": [[2, "openeo.processes.text_contains", false]], "text_ends() (in module openeo.processes)": [[2, "openeo.processes.text_ends", false]], "this (in module openeo.rest.datacube)": [[0, "openeo.rest.datacube.THIS", false]], "timeseries_json_to_pandas() (in module openeo.rest.conversions)": [[0, "openeo.rest.conversions.timeseries_json_to_pandas", false]], "to_bbox_dict() (in module openeo.util)": [[0, "openeo.util.to_bbox_dict", false]], "to_dict() (openeo.api.process.parameter method)": [[0, "openeo.api.process.Parameter.to_dict", false]], "to_dict() (openeo.internal.graph_building.pgnode method)": [[0, "openeo.internal.graph_building.PGNode.to_dict", false]], "to_dict() (openeo.rest.userfile.userfile method)": [[0, "openeo.rest.userfile.UserFile.to_dict", false]], "to_dict() (openeo.udf.udf_data.udfdata method)": [[0, "openeo.udf.udf_data.UdfData.to_dict", false]], "to_dict() (openeo.udf.xarraydatacube.xarraydatacube method)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube.to_dict", false]], "to_json() (openeo.internal.graph_building.flatgraphablemixin method)": [[0, "openeo.internal.graph_building.FlatGraphableMixin.to_json", false]], "to_json() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.to_json", false]], "to_json() (openeo.rest.mlmodel.mlmodel method)": [[0, "openeo.rest.mlmodel.MlModel.to_json", false]], "to_json() (openeo.rest.multiresult.multiresult method)": [[0, "openeo.rest.multiresult.MultiResult.to_json", false]], "to_json() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.to_json", false]], "to_process_graph_argument() (openeo.internal.graph_building.pgnode static method)": [[0, "openeo.internal.graph_building.PGNode.to_process_graph_argument", false]], "trim_cube() (in module openeo.processes)": [[2, "openeo.processes.trim_cube", false]], "udf": [[25, "index-1", false]], "udf (class in openeo.rest._datacube)": [[0, "openeo.rest._datacube.UDF", false]], "udfdata (class in openeo.udf.udf_data)": [[0, "openeo.udf.udf_data.UdfData", false]], "unflatten_dimension() (in module openeo.processes)": [[2, "openeo.processes.unflatten_dimension", false]], "unflatten_dimension() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.unflatten_dimension", false]], "update() (openeo.rest.udp.restuserdefinedprocess method)": [[0, "openeo.rest.udp.RESTUserDefinedProcess.update", false]], "update_arguments() (openeo.internal.graph_building.pgnode method)": [[0, "openeo.internal.graph_building.PGNode.update_arguments", false]], "upload() (openeo.rest.userfile.userfile method)": [[0, "openeo.rest.userfile.UserFile.upload", false]], "upload_file() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.upload_file", false]], "user-defined functions": [[25, "index-0", false]], "user_context (openeo.udf.udf_data.udfdata property)": [[0, "openeo.udf.udf_data.UdfData.user_context", false]], "user_defined_process() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.user_defined_process", false]], "user_jobs() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.user_jobs", false]], "userfile (class in openeo.rest.userfile)": [[0, "openeo.rest.userfile.UserFile", false]], "validate() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.validate", false]], "validate_process_graph() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.validate_process_graph", false]], "variance() (in module openeo.processes)": [[2, "openeo.processes.variance", false]], "vector_buffer() (in module openeo.processes)": [[2, "openeo.processes.vector_buffer", false]], "vector_reproject() (in module openeo.processes)": [[2, "openeo.processes.vector_reproject", false]], "vector_to_random_points() (in module openeo.processes)": [[2, "openeo.processes.vector_to_random_points", false]], "vector_to_raster() (openeo.rest.vectorcube.vectorcube method)": [[0, "openeo.rest.vectorcube.VectorCube.vector_to_raster", false]], "vector_to_regular_points() (in module openeo.processes)": [[2, "openeo.processes.vector_to_regular_points", false]], "vectorcube (class in openeo.rest.vectorcube)": [[0, "openeo.rest.vectorcube.VectorCube", false]], "vectorcube_from_paths() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.vectorcube_from_paths", false]], "version_discovery() (openeo.rest.connection.connection class method)": [[0, "openeo.rest.connection.Connection.version_discovery", false]], "version_info() (openeo.rest.connection.connection method)": [[0, "openeo.rest.connection.Connection.version_info", false]], "walk_nodes() (openeo.internal.graph_building.pgnode method)": [[0, "openeo.internal.graph_building.PGNode.walk_nodes", false]], "xarraydatacube (class in openeo.udf.xarraydatacube)": [[0, "openeo.udf.xarraydatacube.XarrayDataCube", false]], "xor() (in module openeo.processes)": [[2, "openeo.processes.xor", false]]}, "objects": {"openeo": [[0, 2, 1, "", "connect"], [0, 0, 0, "-", "metadata"], [2, 0, 0, "-", "processes"], [0, 0, 0, "-", "testing"], [0, 0, 0, "-", "util"]], "openeo.api": [[0, 0, 0, "-", "logs"], [0, 0, 0, "-", "process"]], "openeo.api.logs": [[0, 1, 1, "", "LogEntry"], [0, 2, 1, "", "normalize_log_level"]], "openeo.api.process": [[0, 1, 1, "", "Parameter"]], "openeo.api.process.Parameter": [[0, 3, 1, "", "array"], [0, 3, 1, "", "boolean"], [0, 3, 1, "", "bounding_box"], [0, 3, 1, "", "datacube"], [0, 3, 1, "", "date"], [0, 3, 1, "", "date_time"], [0, 3, 1, "", "geojson"], [0, 3, 1, "", "integer"], [0, 3, 1, "", "number"], [0, 3, 1, "", "object"], [0, 3, 1, "", "raster_cube"], [0, 3, 1, "", "spatial_extent"], [0, 3, 1, "", "string"], [0, 3, 1, "", "temporal_interval"], [0, 3, 1, "", "to_dict"]], "openeo.extra": [[14, 0, 0, "-", "spectral_indices"]], "openeo.extra.job_management": [[11, 1, 1, "", "CsvJobDatabase"], [11, 1, 1, "", "JobDatabaseInterface"], [11, 1, 1, "", "MultiBackendJobManager"], [11, 1, 1, "", "ParquetJobDatabase"], [11, 1, 1, "", "ProcessBasedJobCreator"]], "openeo.extra.job_management.JobDatabaseInterface": [[11, 3, 1, "", "count_by_status"], [11, 3, 1, "", "exists"], [11, 3, 1, "", "get_by_status"], [11, 3, 1, "", "persist"], [11, 3, 1, "", "read"]], "openeo.extra.job_management.MultiBackendJobManager": [[11, 3, 1, "", "add_backend"], [11, 3, 1, "", "ensure_job_dir_exists"], [11, 3, 1, "", "get_error_log_path"], [11, 3, 1, "", "get_job_dir"], [11, 3, 1, "", "get_job_metadata_path"], [11, 3, 1, "", "on_job_cancel"], [11, 3, 1, "", "on_job_done"], [11, 3, 1, "", "on_job_error"], [11, 3, 1, "", "run_jobs"], [11, 3, 1, "", "start_job_thread"], [11, 3, 1, "", "stop_job_thread"]], "openeo.extra.job_management.ProcessBasedJobCreator": [[11, 3, 1, "", "__call__"], [11, 3, 1, "", "start_job"]], "openeo.extra.spectral_indices": [[14, 2, 1, "", "append_and_rescale_indices"], [14, 2, 1, "", "append_index"], [14, 2, 1, "", "append_indices"], [14, 2, 1, "", "compute_and_rescale_indices"], [14, 2, 1, "", "compute_index"], [14, 2, 1, "", "compute_indices"], [14, 2, 1, "", "list_indices"]], "openeo.internal": [[0, 0, 0, "-", "graph_building"]], "openeo.internal.graph_building": [[0, 1, 1, "", "FlatGraphableMixin"], [0, 1, 1, "", "PGNode"]], "openeo.internal.graph_building.FlatGraphableMixin": [[0, 3, 1, "", "print_json"], [0, 3, 1, "", "to_json"]], "openeo.internal.graph_building.PGNode": [[0, 3, 1, "", "flat_graph"], [0, 3, 1, "", "from_flat_graph"], [0, 3, 1, "", "to_dict"], [0, 3, 1, "", "to_process_graph_argument"], [0, 3, 1, "", "update_arguments"], [0, 3, 1, "", "walk_nodes"]], "openeo.metadata": [[0, 1, 1, "", "BandDimension"], [0, 1, 1, "", "CollectionMetadata"], [0, 1, 1, "", "SpatialDimension"], [0, 1, 1, "", "TemporalDimension"]], "openeo.metadata.BandDimension": [[0, 3, 1, "", "append_band"], [0, 3, 1, "", "band_index"], [0, 3, 1, "", "band_name"], [0, 3, 1, "", "filter_bands"], [0, 3, 1, "", "rename"], [0, 3, 1, "", "rename_labels"]], "openeo.metadata.SpatialDimension": [[0, 3, 1, "", "rename"]], "openeo.metadata.TemporalDimension": [[0, 3, 1, "", "rename"], [0, 3, 1, "", "rename_labels"]], "openeo.processes": [[2, 1, 1, "", "ProcessBuilder"], [2, 2, 1, "", "absolute"], [2, 2, 1, "", "add"], [2, 2, 1, "", "add_dimension"], [2, 2, 1, "", "aggregate_spatial"], [2, 2, 1, "", "aggregate_spatial_window"], [2, 2, 1, "", "aggregate_temporal"], [2, 2, 1, "", "aggregate_temporal_period"], [2, 2, 1, "", "all"], [2, 2, 1, "", "and_"], [2, 2, 1, "", "anomaly"], [2, 2, 1, "", "any"], [2, 2, 1, "", "apply"], [2, 2, 1, "", "apply_dimension"], [2, 2, 1, "", "apply_kernel"], [2, 2, 1, "", "apply_neighborhood"], [2, 2, 1, "", "apply_polygon"], [2, 2, 1, "", "arccos"], [2, 2, 1, "", "arcosh"], [2, 2, 1, "", "arcsin"], [2, 2, 1, "", "arctan"], [2, 2, 1, "", "arctan2"], [2, 2, 1, "", "ard_normalized_radar_backscatter"], [2, 2, 1, "", "ard_surface_reflectance"], [2, 2, 1, "", "array_append"], [2, 2, 1, "", "array_apply"], [2, 2, 1, "", "array_concat"], [2, 2, 1, "", "array_contains"], [2, 2, 1, "", "array_create"], [2, 2, 1, "", "array_create_labeled"], [2, 2, 1, "", "array_element"], [2, 2, 1, "", "array_filter"], [2, 2, 1, "", "array_find"], [2, 2, 1, "", "array_find_label"], [2, 2, 1, "", "array_interpolate_linear"], [2, 2, 1, "", "array_labels"], [2, 2, 1, "", "array_modify"], [2, 2, 1, "", "arsinh"], [2, 2, 1, "", "artanh"], [2, 2, 1, "", "atmospheric_correction"], [2, 2, 1, "", "between"], [2, 2, 1, "", "ceil"], [2, 2, 1, "", "climatological_normal"], [2, 2, 1, "", "clip"], [2, 2, 1, "", "cloud_detection"], [2, 2, 1, "", "constant"], [2, 2, 1, "", "cos"], [2, 2, 1, "", "cosh"], [2, 2, 1, "", "count"], [2, 2, 1, "", "create_data_cube"], [2, 2, 1, "", "cummax"], [2, 2, 1, "", "cummin"], [2, 2, 1, "", "cumproduct"], [2, 2, 1, "", "cumsum"], [2, 2, 1, "", "date_between"], [2, 2, 1, "", "date_difference"], [2, 2, 1, "", "date_shift"], [2, 2, 1, "", "dimension_labels"], [2, 2, 1, "", "divide"], [2, 2, 1, "", "drop_dimension"], [2, 2, 1, "", "e"], [2, 2, 1, "", "eq"], [2, 2, 1, "", "exp"], [2, 2, 1, "", "extrema"], [2, 2, 1, "", "filter_bands"], [2, 2, 1, "", "filter_bbox"], [2, 2, 1, "", "filter_labels"], [2, 2, 1, "", "filter_spatial"], [2, 2, 1, "", "filter_temporal"], [2, 2, 1, "", "filter_vector"], [2, 2, 1, "", "first"], [2, 2, 1, "", "fit_curve"], [2, 2, 1, "", "flatten_dimensions"], [2, 2, 1, "", "floor"], [2, 2, 1, "", "gt"], [2, 2, 1, "", "gte"], [2, 2, 1, "", "if_"], [2, 2, 1, "", "inspect"], [2, 2, 1, "", "int"], [2, 2, 1, "", "is_infinite"], [2, 2, 1, "", "is_nan"], [2, 2, 1, "", "is_nodata"], [2, 2, 1, "", "is_valid"], [2, 2, 1, "", "last"], [2, 2, 1, "", "linear_scale_range"], [2, 2, 1, "", "ln"], [2, 2, 1, "", "load_collection"], [2, 2, 1, "", "load_geojson"], [2, 2, 1, "", "load_ml_model"], [2, 2, 1, "", "load_result"], [2, 2, 1, "", "load_stac"], [2, 2, 1, "", "load_uploaded_files"], [2, 2, 1, "", "load_url"], [2, 2, 1, "", "log"], [2, 2, 1, "", "lt"], [2, 2, 1, "", "lte"], [2, 2, 1, "", "mask"], [2, 2, 1, "", "mask_polygon"], [2, 2, 1, "", "max"], [2, 2, 1, "", "mean"], [2, 2, 1, "", "median"], [2, 2, 1, "", "merge_cubes"], [2, 2, 1, "", "min"], [2, 2, 1, "", "mod"], [2, 2, 1, "", "multiply"], [2, 2, 1, "", "nan"], [2, 2, 1, "", "ndvi"], [2, 2, 1, "", "neq"], [2, 2, 1, "", "normalized_difference"], [2, 2, 1, "", "not_"], [2, 2, 1, "", "or_"], [2, 2, 1, "", "order"], [2, 2, 1, "", "pi"], [2, 2, 1, "", "power"], [2, 2, 1, "", "predict_curve"], [2, 2, 1, "", "predict_random_forest"], [0, 2, 1, "", "process"], [2, 2, 1, "", "product"], [2, 2, 1, "", "quantiles"], [2, 2, 1, "", "rearrange"], [2, 2, 1, "", "reduce_dimension"], [2, 2, 1, "", "reduce_spatial"], [2, 2, 1, "", "rename_dimension"], [2, 2, 1, "", "rename_labels"], [2, 2, 1, "", "resample_cube_spatial"], [2, 2, 1, "", "resample_cube_temporal"], [2, 2, 1, "", "resample_spatial"], [2, 2, 1, "", "round"], [2, 2, 1, "", "run_udf"], [2, 2, 1, "", "run_udf_externally"], [2, 2, 1, "", "sar_backscatter"], [2, 2, 1, "", "save_result"], [2, 2, 1, "", "sd"], [2, 2, 1, "", "sgn"], [2, 2, 1, "", "sin"], [2, 2, 1, "", "sinh"], [2, 2, 1, "", "sort"], [2, 2, 1, "", "sqrt"], [2, 2, 1, "", "subtract"], [2, 2, 1, "", "sum"], [2, 2, 1, "", "tan"], [2, 2, 1, "", "tanh"], [2, 2, 1, "", "text_begins"], [2, 2, 1, "", "text_concat"], [2, 2, 1, "", "text_contains"], [2, 2, 1, "", "text_ends"], [2, 2, 1, "", "trim_cube"], [2, 2, 1, "", "unflatten_dimension"], [2, 2, 1, "", "variance"], [2, 2, 1, "", "vector_buffer"], [2, 2, 1, "", "vector_reproject"], [2, 2, 1, "", "vector_to_random_points"], [2, 2, 1, "", "vector_to_regular_points"], [2, 2, 1, "", "xor"]], "openeo.rest": [[0, 0, 0, "-", "_datacube"], [0, 0, 0, "-", "connection"], [0, 0, 0, "-", "conversions"], [0, 0, 0, "-", "datacube"], [0, 0, 0, "-", "graph_building"], [0, 0, 0, "-", "job"], [0, 0, 0, "-", "mlmodel"], [0, 0, 0, "-", "multiresult"], [0, 0, 0, "-", "udp"], [0, 0, 0, "-", "userfile"], [0, 0, 0, "-", "vectorcube"]], "openeo.rest._datacube": [[0, 1, 1, "", "UDF"]], "openeo.rest._datacube.UDF": [[0, 3, 1, "", "from_file"], [0, 3, 1, "", "from_url"], [0, 3, 1, "", "get_run_udf_callback"]], "openeo.rest.connection": [[0, 1, 1, "", "Connection"]], "openeo.rest.connection.Connection": [[0, 3, 1, "", "as_curl"], [0, 3, 1, "", "assert_user_defined_process_support"], [0, 3, 1, "", "authenticate_basic"], [0, 3, 1, "", "authenticate_oidc"], [0, 3, 1, "", "authenticate_oidc_access_token"], [0, 3, 1, "", "authenticate_oidc_authorization_code"], [0, 3, 1, "", "authenticate_oidc_client_credentials"], [0, 3, 1, "", "authenticate_oidc_device"], [0, 3, 1, "", "authenticate_oidc_refresh_token"], [0, 3, 1, "", "authenticate_oidc_resource_owner_password_credentials"], [0, 3, 1, "", "capabilities"], [0, 3, 1, "", "collection_items"], [0, 3, 1, "", "create_job"], [0, 3, 1, "", "datacube_from_flat_graph"], [0, 3, 1, "", "datacube_from_json"], [0, 3, 1, "", "datacube_from_process"], [0, 3, 1, "", "describe_account"], [0, 3, 1, "", "describe_collection"], [0, 3, 1, "", "describe_process"], [0, 3, 1, "", "download"], [0, 3, 1, "", "execute"], [0, 3, 1, "", "get_file"], [0, 3, 1, "", "imagecollection"], [0, 3, 1, "", "job"], [0, 3, 1, "", "job_logs"], [0, 3, 1, "", "job_results"], [0, 3, 1, "", "list_collection_ids"], [0, 3, 1, "", "list_collections"], [0, 3, 1, "", "list_file_formats"], [0, 3, 1, "", "list_file_types"], [0, 3, 1, "", "list_files"], [0, 3, 1, "", "list_jobs"], [0, 3, 1, "", "list_processes"], [0, 3, 1, "", "list_service_types"], [0, 3, 1, "", "list_services"], [0, 3, 1, "", "list_udf_runtimes"], [0, 3, 1, "", "list_user_defined_processes"], [0, 3, 1, "", "load_collection"], [0, 3, 1, "", "load_disk_collection"], [0, 3, 1, "", "load_geojson"], [0, 3, 1, "", "load_ml_model"], [0, 3, 1, "", "load_result"], [0, 3, 1, "", "load_stac"], [0, 3, 1, "", "load_stac_from_job"], [0, 3, 1, "", "load_url"], [0, 3, 1, "", "remove_service"], [0, 3, 1, "", "request"], [0, 3, 1, "", "save_user_defined_process"], [0, 3, 1, "", "service"], [0, 3, 1, "", "upload_file"], [0, 3, 1, "", "user_defined_process"], [0, 3, 1, "", "user_jobs"], [0, 3, 1, "", "validate_process_graph"], [0, 3, 1, "", "vectorcube_from_paths"], [0, 3, 1, "", "version_discovery"], [0, 3, 1, "", "version_info"]], "openeo.rest.conversions": [[0, 4, 1, "", "InvalidTimeSeriesException"], [0, 2, 1, "", "datacube_from_file"], [0, 2, 1, "", "datacube_plot"], [0, 2, 1, "", "datacube_to_file"], [0, 2, 1, "", "timeseries_json_to_pandas"]], "openeo.rest.datacube": [[0, 1, 1, "", "DataCube"], [0, 5, 1, "", "THIS"]], "openeo.rest.datacube.DataCube": [[0, 3, 1, "", "__init__"], [0, 3, 1, "", "add"], [0, 3, 1, "", "add_dimension"], [0, 3, 1, "", "aggregate_spatial"], [0, 3, 1, "", "aggregate_spatial_window"], [0, 3, 1, "", "aggregate_temporal"], [0, 3, 1, "", "aggregate_temporal_period"], [0, 3, 1, "", "apply"], [0, 3, 1, "", "apply_dimension"], [0, 3, 1, "", "apply_kernel"], [0, 3, 1, "", "apply_neighborhood"], [0, 3, 1, "", "apply_polygon"], [0, 3, 1, "", "ard_normalized_radar_backscatter"], [0, 3, 1, "", "ard_surface_reflectance"], [0, 3, 1, "", "atmospheric_correction"], [0, 3, 1, "", "band"], [0, 3, 1, "", "band_filter"], [0, 3, 1, "", "chunk_polygon"], [0, 3, 1, "", "count_time"], [0, 3, 1, "", "create_collection"], [0, 3, 1, "", "create_job"], [0, 3, 1, "", "dimension_labels"], [0, 3, 1, "", "divide"], [0, 3, 1, "", "download"], [0, 3, 1, "", "drop_dimension"], [0, 3, 1, "", "execute"], [0, 3, 1, "", "execute_batch"], [0, 3, 1, "", "execute_local_udf"], [0, 3, 1, "", "filter_bands"], [0, 3, 1, "", "filter_bbox"], [0, 3, 1, "", "filter_labels"], [0, 3, 1, "", "filter_spatial"], [0, 3, 1, "", "filter_temporal"], [0, 3, 1, "", "fit_curve"], [0, 3, 1, "", "flat_graph"], [0, 3, 1, "", "flatten_dimensions"], [0, 3, 1, "", "graph_add_node"], [0, 3, 1, "", "linear_scale_range"], [0, 3, 1, "", "ln"], [0, 3, 1, "", "load_collection"], [0, 3, 1, "", "load_disk_collection"], [0, 3, 1, "", "load_stac"], [0, 3, 1, "", "log10"], [0, 3, 1, "", "log2"], [0, 3, 1, "", "logarithm"], [0, 3, 1, "", "logical_and"], [0, 3, 1, "", "logical_or"], [0, 3, 1, "", "mask"], [0, 3, 1, "", "mask_polygon"], [0, 3, 1, "", "max_time"], [0, 3, 1, "", "mean_time"], [0, 3, 1, "", "median_time"], [0, 3, 1, "", "merge"], [0, 3, 1, "", "merge_cubes"], [0, 3, 1, "", "min_time"], [0, 3, 1, "", "multiply"], [0, 3, 1, "", "ndvi"], [0, 3, 1, "", "normalized_difference"], [0, 3, 1, "", "polygonal_histogram_timeseries"], [0, 3, 1, "", "polygonal_mean_timeseries"], [0, 3, 1, "", "polygonal_median_timeseries"], [0, 3, 1, "", "polygonal_standarddeviation_timeseries"], [0, 3, 1, "", "power"], [0, 3, 1, "", "predict_curve"], [0, 3, 1, "", "predict_random_forest"], [0, 3, 1, "", "preview"], [0, 3, 1, "", "print_json"], [0, 3, 1, "", "process"], [0, 3, 1, "", "process_with_node"], [0, 3, 1, "", "raster_to_vector"], [0, 3, 1, "", "reduce_bands"], [0, 3, 1, "", "reduce_bands_udf"], [0, 3, 1, "", "reduce_dimension"], [0, 3, 1, "", "reduce_spatial"], [0, 3, 1, "", "reduce_temporal"], [0, 3, 1, "", "reduce_temporal_simple"], [0, 3, 1, "", "reduce_temporal_udf"], [0, 3, 1, "", "reduce_tiles_over_time"], [0, 3, 1, "", "rename_dimension"], [0, 3, 1, "", "rename_labels"], [0, 3, 1, "", "resample_cube_spatial"], [0, 3, 1, "", "resample_cube_temporal"], [0, 3, 1, "", "resample_spatial"], [0, 3, 1, "", "resolution_merge"], [0, 3, 1, "", "result_node"], [0, 3, 1, "", "sar_backscatter"], [0, 3, 1, "", "save_result"], [0, 3, 1, "", "save_user_defined_process"], [0, 3, 1, "", "send_job"], [0, 3, 1, "", "subtract"], [0, 3, 1, "", "to_json"], [0, 3, 1, "", "unflatten_dimension"], [0, 3, 1, "", "validate"]], "openeo.rest.graph_building": [[0, 1, 1, "", "CollectionProperty"], [0, 2, 1, "", "collection_property"]], "openeo.rest.job": [[0, 1, 1, "", "BatchJob"], [0, 1, 1, "", "JobResults"], [0, 1, 1, "", "RESTJob"], [0, 1, 1, "", "ResultAsset"]], "openeo.rest.job.BatchJob": [[0, 3, 1, "", "delete"], [0, 3, 1, "", "delete_job"], [0, 3, 1, "", "describe"], [0, 3, 1, "", "describe_job"], [0, 3, 1, "", "download_result"], [0, 3, 1, "", "download_results"], [0, 3, 1, "", "estimate"], [0, 3, 1, "", "estimate_job"], [0, 3, 1, "", "get_result"], [0, 3, 1, "", "get_results"], [0, 3, 1, "", "get_results_metadata_url"], [0, 6, 1, "", "job_id"], [0, 3, 1, "", "list_results"], [0, 3, 1, "", "logs"], [0, 3, 1, "", "run_synchronous"], [0, 3, 1, "", "start"], [0, 3, 1, "", "start_and_wait"], [0, 3, 1, "", "start_job"], [0, 3, 1, "", "status"], [0, 3, 1, "", "stop"], [0, 3, 1, "", "stop_job"]], "openeo.rest.job.JobResults": [[0, 3, 1, "", "download_file"], [0, 3, 1, "", "download_files"], [0, 3, 1, "", "get_asset"], [0, 3, 1, "", "get_assets"], [0, 3, 1, "", "get_metadata"]], "openeo.rest.job.ResultAsset": [[0, 3, 1, "", "download"], [0, 6, 1, "", "href"], [0, 3, 1, "", "load_bytes"], [0, 3, 1, "", "load_json"], [0, 6, 1, "", "metadata"], [0, 6, 1, "", "name"]], "openeo.rest.mlmodel": [[0, 1, 1, "", "MlModel"]], "openeo.rest.mlmodel.MlModel": [[0, 3, 1, "", "create_job"], [0, 3, 1, "", "execute_batch"], [0, 3, 1, "", "flat_graph"], [0, 3, 1, "", "load_ml_model"], [0, 3, 1, "", "print_json"], [0, 3, 1, "", "result_node"], [0, 3, 1, "", "save_ml_model"], [0, 3, 1, "", "to_json"]], "openeo.rest.multiresult": [[0, 1, 1, "", "MultiResult"]], "openeo.rest.multiresult.MultiResult": [[0, 3, 1, "", "__init__"], [0, 3, 1, "", "print_json"], [0, 3, 1, "", "to_json"]], "openeo.rest.udp": [[0, 1, 1, "", "RESTUserDefinedProcess"], [0, 2, 1, "", "build_process_dict"]], "openeo.rest.udp.RESTUserDefinedProcess": [[0, 3, 1, "", "delete"], [0, 3, 1, "", "describe"], [0, 3, 1, "", "store"], [0, 3, 1, "", "update"]], "openeo.rest.userfile": [[0, 1, 1, "", "UserFile"]], "openeo.rest.userfile.UserFile": [[0, 3, 1, "", "delete"], [0, 3, 1, "", "download"], [0, 3, 1, "", "from_metadata"], [0, 3, 1, "", "to_dict"], [0, 3, 1, "", "upload"]], "openeo.rest.vectorcube": [[0, 1, 1, "", "VectorCube"]], "openeo.rest.vectorcube.VectorCube": [[0, 3, 1, "", "apply_dimension"], [0, 3, 1, "", "create_job"], [0, 3, 1, "", "download"], [0, 3, 1, "", "execute"], [0, 3, 1, "", "execute_batch"], [0, 3, 1, "", "filter_bands"], [0, 3, 1, "", "filter_bbox"], [0, 3, 1, "", "filter_labels"], [0, 3, 1, "", "filter_vector"], [0, 3, 1, "", "fit_class_random_forest"], [0, 3, 1, "", "fit_regr_random_forest"], [0, 3, 1, "", "flat_graph"], [0, 3, 1, "", "load_geojson"], [0, 3, 1, "", "load_url"], [0, 3, 1, "", "print_json"], [0, 3, 1, "", "process"], [0, 3, 1, "", "result_node"], [0, 3, 1, "", "run_udf"], [0, 3, 1, "", "save_result"], [0, 3, 1, "", "send_job"], [0, 3, 1, "", "to_json"], [0, 3, 1, "", "vector_to_raster"]], "openeo.testing": [[0, 1, 1, "", "TestDataLoader"], [0, 0, 0, "-", "results"]], "openeo.testing.TestDataLoader": [[0, 3, 1, "", "get_path"], [0, 3, 1, "", "load_json"]], "openeo.testing.results": [[0, 2, 1, "", "assert_job_results_allclose"], [0, 2, 1, "", "assert_xarray_allclose"], [0, 2, 1, "", "assert_xarray_dataarray_allclose"], [0, 2, 1, "", "assert_xarray_dataset_allclose"]], "openeo.udf": [[0, 0, 0, "-", "debug"], [0, 0, 0, "-", "run_code"], [0, 0, 0, "-", "structured_data"], [0, 0, 0, "-", "udf_data"], [25, 0, 0, "-", "udf_signatures"], [0, 0, 0, "-", "xarraydatacube"]], "openeo.udf.debug": [[0, 2, 1, "", "inspect"]], "openeo.udf.run_code": [[0, 2, 1, "", "execute_local_udf"], [0, 2, 1, "", "extract_udf_dependencies"]], "openeo.udf.structured_data": [[0, 1, 1, "", "StructuredData"]], "openeo.udf.udf_data": [[0, 1, 1, "", "UdfData"]], "openeo.udf.udf_data.UdfData": [[0, 7, 1, "", "datacube_list"], [0, 7, 1, "", "feature_collection_list"], [0, 3, 1, "", "from_dict"], [0, 3, 1, "", "get_datacube_list"], [0, 3, 1, "", "get_feature_collection_list"], [0, 3, 1, "", "get_structured_data_list"], [0, 3, 1, "", "set_datacube_list"], [0, 3, 1, "", "set_structured_data_list"], [0, 7, 1, "", "structured_data_list"], [0, 3, 1, "", "to_dict"], [0, 7, 1, "", "user_context"]], "openeo.udf.udf_signatures": [[25, 2, 1, "", "apply_datacube"], [25, 2, 1, "", "apply_metadata"], [25, 2, 1, "", "apply_timeseries"], [25, 2, 1, "", "apply_udf_data"], [25, 2, 1, "", "apply_vectorcube"]], "openeo.udf.xarraydatacube": [[0, 1, 1, "", "XarrayDataCube"]], "openeo.udf.xarraydatacube.XarrayDataCube": [[0, 7, 1, "", "array"], [0, 3, 1, "", "from_dict"], [0, 3, 1, "", "from_file"], [0, 3, 1, "", "get_array"], [0, 3, 1, "", "plot"], [0, 3, 1, "", "save_to_file"], [0, 3, 1, "", "to_dict"]], "openeo.util": [[0, 1, 1, "", "BBoxDict"], [0, 2, 1, "", "load_json_resource"], [0, 2, 1, "", "normalize_crs"], [0, 2, 1, "", "to_bbox_dict"]], "openeo.util.BBoxDict": [[0, 3, 1, "", "from_dict"], [0, 3, 1, "", "from_sequence"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "function", "Python function"], "3": ["py", "method", "Python method"], "4": ["py", "exception", "Python exception"], "5": ["py", "data", "Python data"], "6": ["py", "attribute", "Python attribute"], "7": ["py", "property", "Python property"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:function", "3": "py:method", "4": "py:exception", "5": "py:data", "6": "py:attribute", "7": "py:property"}, "terms": {"": [0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 24, 26], "0": [0, 2, 3, 4, 5, 8, 9, 11, 12, 14, 16, 17, 18, 19, 20, 22, 24, 26], "00": [2, 4, 5, 12, 17, 25], "000": 17, "0001": [4, 25, 26], "001": 2, "002": 2, "004": 20, "005": 2, "00z": 2, "01": [0, 2, 4, 5, 6, 11, 12, 13, 17, 18, 20, 24, 25, 26], "010r7": 2, "02": [0, 2, 4, 5, 11, 12, 20, 24], "03": [5, 9, 11, 17, 20, 25, 26], "038": 12, "04": [4, 17, 20, 25], "04t14": 5, "05": [2, 3, 6, 12, 13, 17, 20, 24, 26], "06": [0, 4, 5, 12, 13, 17, 20, 24, 26], "069": 9, "06z": 5, "07": [0, 3, 5, 9, 17, 20, 23, 24, 26], "08": [0, 2, 17, 20, 23, 24], "08730b1b5458a4ed34edeee60ac79254": 12, "087806252": 9, "087f": 5, "08t08": 5, "09": [18, 20, 26], "096e": 12, "0_decad": 2, "0a1": 19, "0o600": 3, "0x7f6505a40d00": 24, "1": [0, 1, 2, 3, 4, 5, 9, 11, 12, 14, 16, 18, 19, 20, 22, 24, 25, 26], "10": [0, 2, 3, 4, 8, 9, 12, 18, 20, 22, 24, 25, 26], "100": [0, 2, 7, 18], "10000000": 0, "100x100km": 13, "1024": 12, "105": 18, "10m": 0, "10mb": 7, "11": [0, 2, 5, 12, 20], "11111111111111": 26, "112": 25, "113": 7, "11354": 12, "115": 7, "116": 17, "11t13": 3, "11z": 5, "12": [0, 2, 4, 12, 17, 19, 20, 26], "123": [3, 7], "127": 19, "128": 25, "128x128": 25, "13": [0, 3, 20], "133": 7, "134": 7, "136": 7, "1386": 6, "138916": 6, "14": [0, 4, 11, 17, 20], "1414": 4, "1414b": 3, "1417": 4, "144": 7, "1443": 4, "1444": 4, "147": 7, "148e": 12, "15": [2, 4, 12, 17, 19, 20, 25, 26], "153": 7, "155": [4, 7], "155e": 12, "156": 4, "157": 7, "158": 7, "159": 7, "16": [0, 2, 4, 6, 20, 24, 25], "163": 4, "17": [0, 4, 17, 20], "170": 7, "175": 7, "176": 7, "1768": 4, "177": 12, "1772": 4, "178": 7, "1785": 4, "1787": 26, "179": 4, "1793": 26, "17t12": 17, "18": [0, 2, 3, 4, 17, 20], "182": 7, "184": 7, "1852": 26, "1855": 4, "1867": 26, "187": 7, "1873": 26, "1891": 4, "1892": 4, "19": [0, 4, 5, 17, 20], "190": 7, "191": 7, "192": 7, "197": 7, "198": 7, "1981": 2, "1e": 0, "2": [0, 2, 3, 4, 5, 9, 11, 12, 13, 17, 18, 20, 22, 24, 25, 26], "20": [0, 2, 4, 6, 20, 24], "200": 7, "2001": 0, "201": 7, "2010": 2, "2017": 9, "2018": 26, "2019": [0, 9, 12, 24], "202": 7, "2020": [2, 3, 4, 6, 13, 17, 18, 19, 20, 24, 26], "2021": [4, 5, 11, 16, 17, 20, 26], "2022": [3, 5, 12, 17, 20, 25], "2023": [12, 17, 20, 23], "2024": 20, "204": 7, "205": 7, "209": 7, "20m": 0, "20z": 3, "21": [0, 2, 20, 26], "210": 7, "21e": 12, "22": [0, 2, 20, 25], "2206": 9, "221": 7, "225": 7, "228": 7, "229": 7, "23": [0, 1, 2, 5, 17, 20, 24], "233": 7, "235": 0, "237": 7, "23z": 5, "24": [0, 20], "240": [0, 7], "242": 7, "2450": 26, "2453": 26, "2467": 26, "247": 7, "2491": 26, "2498": 26, "24t10": 5, "24t13": 3, "25": [0, 12, 20, 25], "250": 14, "255": 0, "256": 25, "259": 7, "26": [0, 14, 17, 20], "260": 7, "264": 7, "27": [0, 12, 20, 26], "274": 7, "275": 7, "276": 7, "278": 7, "279": 7, "28": [0, 2, 5, 20], "280": 7, "284": 7, "285": 7, "286": 7, "287": 7, "288": 7, "288079": 4, "29": [0, 2, 17, 20], "291": 7, "291835566": 9, "293": 7, "298": 7, "2b": 0, "2d": 0, "3": [0, 2, 5, 7, 9, 12, 15, 17, 18, 22, 24, 25, 26], "30": [0, 4, 12, 16, 20], "300": [0, 7], "300250": 4, "302": 7, "304": 7, "308": 7, "309": 7, "31": [0, 2, 4, 11, 20, 25, 26], "310": [7, 16], "312": 7, "314": 7, "316": 7, "317": 7, "32": [0, 2, 11, 16, 20, 26], "320647": 6, "323": 7, "323e": 12, "324": 7, "326": 7, "32632": [0, 12], "327598": 4, "328": 7, "33": [0, 11, 20], "331": 7, "332": 7, "3339": [2, 17], "335": 7, "3359": 7, "336": 7, "3377": 7, "338": 7, "34": [17, 20], "3456": 7, "346": 7, "3485": 7, "3493": 7, "3496": 7, "35": [0, 5, 18], "350": 7, "3509": 7, "352": 7, "3544": 7, "3555": 7, "3578": 7, "3585": 7, "36": [2, 5], "3609": 7, "361": 7, "3612": 7, "3617": 7, "3645": 7, "365": [2, 7], "366": 7, "3670": 7, "3687": 7, "3698": 7, "3700": 7, "373": 7, "3739": 7, "377": 7, "3846": 7, "386": 7, "387": 7, "3889": 7, "39": 17, "390": 7, "391": 7, "3927399": 9, "3jkd": 22, "4": [0, 4, 5, 6, 9, 17, 18, 20, 22, 25, 26], "40": [3, 12], "4008": 7, "401": 7, "4011": 7, "4012": 7, "403": 7, "404": 7, "40bc": 5, "410": 7, "412": 7, "413": 17, "414": 7, "418": 7, "419": 7, "41de": 5, "42": 0, "421": 7, "424": 7, "425": 7, "431": 7, "4326": [0, 17, 25], "432f3b3ef3a": 5, "433": 7, "436": 7, "441b": 5, "442": 7, "443": 7, "448": 7, "449": 7, "451": 7, "452": 7, "454": 7, "459": 7, "46": [5, 12], "460": 7, "463a": 5, "464": 7, "47": 12, "470": 7, "470f": 5, "471": 7, "48": 6, "484": 7, "485": 7, "491": 7, "493": 7, "499": 7, "4990200": 12, "4a54": 5, "4bbb3c72a9234ee998a6de940a148e346a": 25, "4c2e": 5, "4e720e70": 5, "5": [0, 2, 4, 5, 12, 15, 17, 18, 20, 22, 24, 25, 26], "50": [0, 5, 12], "501": 7, "502": 7, "508": [0, 7, 25], "50z": 3, "51": [0, 4, 5, 9, 17, 20, 22, 24, 25, 26], "511": 7, "512": [0, 7], "512x512": 13, "513": 7, "515": 7, "5161000": 0, "5181000": 0, "52": [0, 2, 12], "522": 7, "522499": 4, "524124": 6, "526": 7, "527": 7, "528": 7, "529591": 4, "52e": 12, "53": 12, "5300040": 12, "549": 7, "54ee": 5, "550": 7, "559ed2d4e53c": 5, "56": 17, "566": 7, "567": 7, "568": 7, "56z": 17, "571": 7, "573": 7, "578": 7, "57da31da": 5, "58": 5, "583": 7, "59": 5, "590": 7, "59003": 9, "592": 7, "598": 7, "5a1": 12, "5d806224": 5, "5x3x6": 5, "6": [0, 4, 5, 12, 18, 20, 22, 25, 26], "60": [0, 11, 25], "600000": 12, "604": 7, "612": 7, "623": 7, "624": 7, "628": 7, "633011": 4, "635": 7, "638": 7, "64": 0, "641": 7, "645": 7, "652": 7, "652000": 0, "654": 7, "657": 7, "665": 12, "672000": 0, "68caccff": 5, "7": [0, 2, 4, 12, 20, 26], "70": 26, "705": 12, "723": [7, 25], "726": 4, "74ce": 5, "75": [0, 17, 26], "758216409030558": 9, "75e": 12, "7946": 0, "7fd4": 5, "8": [0, 2, 16, 18, 19, 20, 25, 26], "80": [12, 17], "8000": [14, 19], "8025": 12, "809760": 12, "84": 17, "843e": 12, "846b": 3, "86": 16, "881": 7, "88bd": 5, "8949": 9, "9": [0, 2, 4, 12, 14, 18, 19, 20], "90757893795b": 5, "91": 7, "92db": 5, "935": 12, "96": 7, "9_decad": 2, "9d7d": 5, "A": [0, 2, 3, 4, 5, 11, 13, 17, 20, 26], "AND": [0, 2], "As": [0, 1, 2, 4, 12, 16, 17, 18, 19, 25, 26], "At": [0, 2, 4, 8, 16], "Be": [0, 2, 16], "Being": 24, "By": [0, 2, 4, 11, 16, 17], "For": [0, 1, 2, 3, 4, 5, 9, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26], "If": [0, 2, 3, 4, 5, 7, 11, 12, 14, 19, 21, 24, 25, 26], "In": [0, 2, 3, 4, 5, 11, 12, 14, 17, 19, 24, 25, 26], "It": [0, 2, 3, 4, 5, 6, 12, 13, 14, 16, 17, 18, 19, 20, 21, 24, 25, 26], "Its": 0, "No": [2, 7, 18, 21], "Not": [0, 2, 6, 15, 25, 26], "OR": 2, "On": [1, 2, 3, 7, 9, 26], "One": [0, 2, 11, 17], "Or": [0, 3, 15, 17, 19, 25], "Such": 17, "That": 3, "The": [0, 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26], "Their": 25, "Then": [2, 19], "There": [0, 2, 3, 9, 16, 19, 25, 26], "These": [0, 2, 4, 9, 19, 24, 25], "To": [0, 1, 2, 3, 4, 9, 13, 14, 17, 18, 19, 21, 22, 24, 25, 26], "With": [0, 3, 5, 12, 25], "_": 11, "_____": 17, "________": 17, "____________": 17, "_________________________": 17, "__add__": 23, "__and__": 23, "__call__": 11, "__eq__": 23, "__file__": 0, "__ge__": 23, "__getitem__": 23, "__gt__": 23, "__init__": 0, "__invert__": 23, "__le__": 23, "__lt__": 23, "__mul__": 23, "__ne__": 23, "__neg__": 23, "__or__": 23, "__pow__": 23, "__radd__": 23, "__rmul__": 23, "__rpow__": 23, "__rsub__": 23, "__rtruediv__": 23, "__sub__": 23, "__truediv__": 23, "__version__": 19, "_build": 19, "_builder": 0, "_datacub": 0, "_pg": 7, "_sourc": 0, "_start_job_default": 11, "_version": 19, "_without_": 25, "a1": 19, "a366985ebd67": 5, "aaaaaa": [0, 2], "aai": 4, "ab": 2, "abaa": 5, "abbrevi": [3, 25, 26], "abcdef": [0, 2], "abcdefgh": [0, 2], "abil": 25, "abl": [2, 3, 4, 25], "abort": 2, "about": [0, 2, 3, 4, 7, 17, 18, 19, 21, 24, 25, 26], "abov": [0, 2, 3, 4, 5, 11, 16, 17, 18, 19, 24, 25, 26], "absenc": 0, "absolut": [0, 2, 23, 24], "abstract": [0, 4, 7, 11, 19, 25], "academ": 3, "accept": [0, 1, 2, 11, 25], "access": [0, 3, 7, 13, 16], "access_token": 0, "accident": 3, "accomod": 25, "accord": [0, 2], "accordingli": [2, 7], "account": [0, 2, 4, 5, 20, 26], "accustom": 24, "achiev": 25, "acquisit": 2, "across": [7, 19, 25], "act": 3, "action": [5, 7, 19], "activ": [3, 19], "actual": [0, 1, 2, 4, 7, 14, 17, 25], "acycl": 0, "ad": [0, 2, 3, 6, 8, 11, 14, 18, 19, 20, 22], "add": [0, 1, 2, 3, 6, 7, 11, 12, 15, 17, 18, 19, 23, 24], "add_backend": 11, "add_dimens": [0, 2, 7, 23], "addit": [0, 2, 3, 7, 8, 9, 11, 14, 17, 18, 19, 24, 25, 26], "addition": [12, 14], "address": [2, 3, 4, 7, 19], "adher": 7, "adjust": [7, 25], "adopt": [0, 25], "advanc": [0, 7, 17, 25], "advantag": 25, "advertis": [0, 5, 7, 17], "aerosol": 2, "afe6": 5, "affect": 9, "after": [0, 2, 3, 4, 5, 6, 7, 11, 19, 25], "afterward": [0, 2, 11], "again": [2, 3, 4, 17, 19, 26], "against": [0, 2, 7], "aggreg": [0, 2, 17, 20, 24, 26], "aggregate_spati": [0, 2, 4, 7, 17, 22, 23, 24, 26], "aggregate_spatial_window": [0, 2, 7, 23], "aggregate_tempor": [0, 2, 7, 23, 24], "aggregate_temporal_period": [0, 2, 7, 23], "ai": 25, "aid": 25, "aim": [0, 25], "ak": 18, "algorithm": [0, 2, 3, 4, 9, 25, 26], "alia": [0, 2, 5, 7], "alias": 7, "align": [0, 2, 7], "all": [0, 2, 3, 4, 6, 7, 11, 13, 17, 18, 19, 22, 23, 24, 25, 26], "allow": [0, 2, 3, 4, 5, 7, 8, 11, 12, 15, 16, 17, 18, 19, 24, 25, 26], "allow_common": 0, "alon": 6, "along": [0, 1, 2, 4, 5, 24, 25], "alpha": 19, "alpha1": 25, "alreadi": [0, 2, 3, 5, 6, 7, 11, 17, 19, 26], "also": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26], "altern": [4, 7], "although": [7, 17], "alwai": [0, 2, 3, 7, 8, 17, 25], "ambigu": 7, "among": [0, 16], "amongst": 11, "amount": [0, 17], "an": [0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 18, 19, 20, 21, 24, 26], "analysi": [4, 7, 10, 17, 20], "and_": [2, 23], "angl": [0, 2, 9], "ani": [0, 2, 3, 4, 5, 9, 11, 12, 17, 19, 23, 25], "anno": [0, 2], "annoi": [6, 24], "annot": 25, "anomali": [2, 23], "anonym": [1, 2, 3, 24], "anoth": [0, 2, 3, 5, 16, 17, 19, 22, 24, 25, 26], "anymor": [0, 7, 25], "anyth": [11, 25], "anywai": 7, "apart": [0, 6, 11, 17], "apertur": 9, "api": [3, 4, 5, 7, 9, 10, 12, 16, 17, 18, 19, 20, 22, 26], "appear": 2, "append": [0, 2, 14, 19, 24, 25], "append_and_rescale_indic": [10, 14], "append_band": 0, "append_index": [10, 14], "append_indic": [10, 14], "appli": [0, 2, 7, 9, 13, 17, 18, 20, 23, 24], "applic": [0, 2, 5, 14, 20, 21, 22, 26], "apply_datacub": [0, 7, 25], "apply_dimens": [0, 2, 7, 20, 23, 24], "apply_kernel": [0, 2, 18, 23, 24], "apply_metadata": 25, "apply_neighborhood": [0, 2, 7, 20, 23, 24], "apply_polygon": [0, 2, 7], "apply_timeseri": 25, "apply_udf_data": 25, "apply_vectorcub": [7, 25], "appreci": 19, "approach": [0, 3, 16, 17, 19, 24, 25], "appropri": [0, 17, 24], "april": [0, 2, 17], "ar": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26], "arbitrari": [0, 2], "arcco": [2, 23, 24], "architectur": 3, "archiv": [17, 19, 25], "arcosh": [2, 23, 24], "arcsin": [2, 23], "arctan": [2, 23], "arctan2": [2, 23, 24], "ard": 9, "ard_normalized_radar_backscatt": [0, 2, 9, 23], "ard_surface_reflect": [0, 2, 7, 9, 23], "area": [0, 2, 13, 17], "arg": [0, 11], "argument": [0, 1, 2, 3, 4, 5, 6, 7, 11, 14, 15, 16, 17, 18, 25, 26], "arithmet": 2, "around": [0, 7, 13], "arrai": [0, 1, 2, 7, 12, 18, 24, 25, 26], "array1": 2, "array2": 2, "array_append": [2, 23], "array_appli": [2, 23], "array_concat": [2, 7, 23], "array_contain": [2, 23], "array_cr": [2, 4, 7, 23], "array_create_label": [2, 23], "array_el": [2, 7, 23], "array_filt": [2, 23], "array_find": [2, 23], "array_find_label": [2, 23], "array_interpolate_linear": [2, 23], "array_label": [2, 23], "array_modifi": [2, 7, 23], "arrayelementnotavail": 2, "arraynotlabel": 2, "arsinh": [2, 23], "artanh": [2, 23, 24], "artefact": 4, "artifact": 19, "artifactori": [13, 19], "as_curl": [0, 7], "asc": 2, "ascend": 2, "ascendingprobabilitiesrequir": 2, "asctim": 11, "ashap": 0, "ask": 3, "aspect": [4, 24, 25, 26], "assembl": [7, 24], "assert": [0, 21, 25], "assert_job_results_allclos": 0, "assert_user_defined_process_support": 0, "assert_xarray_allclos": 0, "assert_xarray_dataarray_allclos": 0, "assert_xarray_dataset_allclos": 0, "assertionerror": 0, "assess": 2, "asset": [0, 2, 7, 9, 13, 22], "assign": [0, 2], "associ": [0, 3, 19], "assum": [0, 2, 3, 13, 18, 19, 22, 24, 25], "assur": 4, "asynchron": [20, 26], "atmoshper": 9, "atmospher": [0, 2, 10], "atmospheric_correct": [0, 2, 7, 9, 23], "atmospheric_correction_method": [0, 2], "atmospheric_correction_opt": [0, 2, 7], "atol": 0, "attach": [14, 19], "attempt": [0, 3, 7], "attribut": [7, 12], "audienc": [6, 21, 22], "august": [0, 2], "auth": [0, 4, 7, 8, 20], "auth_config": 0, "auth_connect": 13, "auth_opt": 0, "auth_typ": 0, "authbas": 0, "authconfig": [0, 7], "authent": [0, 2, 7, 8, 20], "authenticate_bas": [0, 3], "authenticate_oidc": [0, 3, 4, 7, 20, 25, 26], "authenticate_oidc_access_token": [0, 7], "authenticate_oidc_authorization_cod": [0, 3], "authenticate_oidc_client_credenti": [0, 3, 7], "authenticate_oidc_devic": [0, 3], "authenticate_oidc_refresh_token": [0, 3], "authenticate_oidc_resource_owner_password_credenti": [0, 3], "author": [0, 3, 7], "auto": [0, 7, 8, 20, 25], "auto_add_save_result": [0, 7], "auto_authent": [3, 8], "auto_collaps": 0, "auto_decod": [0, 7], "auto_valid": 0, "autobuild": 19, "autodetect": 7, "autogener": [7, 23], "autom": 3, "automat": [0, 2, 3, 4, 7, 8, 10, 11, 15, 17, 18, 19, 24, 25], "autotick": 19, "auxiliari": 14, "avail": [0, 2, 3, 4, 5, 7, 9, 11, 13, 16, 17, 19, 21, 24, 25], "averag": [2, 17], "avg": 24, "avoid": [0, 2, 3, 6, 7, 13, 15, 17, 19, 21, 22, 25], "aw": 12, "awar": [0, 2, 17], "awesom": [7, 14], "ax": [0, 2], "axi": [0, 2, 25], "azimuth": 2, "b": [2, 4, 14], "b02": [4, 9, 11, 12, 17, 22, 25, 26], "b03": [9, 11, 12, 17, 22, 25], "b04": [4, 9, 12, 13, 17, 22, 25, 26], "b06d": 5, "b08": [4, 12, 26], "b09": 9, "b0e8adcf": 5, "b1": [0, 19], "b11": 9, "b2": 0, "b3": 0, "b3c0ea88ff38": 5, "b3dw": 22, "b4": 0, "b76a": 5, "b7bfd3b59669": 5, "b8a": 9, "back": [0, 1, 2, 5, 7, 8, 12, 16, 17, 18, 20, 24, 25, 26], "backend": [0, 3, 4, 7, 10, 12, 13, 17, 18, 20, 25], "background": [0, 3, 10, 20], "backscatt": [0, 2, 10], "backward": 0, "bad": [3, 7], "band": [0, 2, 7, 9, 10, 11, 12, 13, 17, 20, 22, 24, 25, 26], "band_filt": 0, "band_index": 0, "band_math_mod": 0, "band_nam": 0, "banddimens": 0, "bar": [6, 7, 11], "bare": 4, "base": [0, 2, 4, 5, 7, 9, 10, 17, 19, 20, 21, 25, 26], "basegeometri": 0, "basemap": 7, "bash": [3, 19], "basi": 3, "basic": [0, 2, 5, 7, 8, 9, 10, 13, 16, 17, 20, 25, 26], "basicconfig": 11, "batch": [0, 1, 2, 7, 11, 13, 18, 20, 22, 25], "batchjob": [0, 5, 7, 11, 22, 25], "bbox": [0, 5, 7, 22, 24, 26], "bbox_filt": 7, "bbox_param": 0, "bboxdict": 0, "bc13": 5, "bdist_wheel": 19, "be04": 5, "bear": 4, "bearer": 0, "becaus": [0, 1, 2, 3, 4, 6, 9, 11, 13, 19, 24, 25, 26], "becom": 3, "been": [0, 2, 7, 9, 11, 14, 25], "befor": [0, 2, 3, 4, 5, 7, 17, 25, 26], "begin": 2, "behavior": [0, 2, 6, 7], "behaviour": [0, 11], "behind": [4, 24], "being": [0, 2, 12, 24, 25], "below": [2, 3, 11, 19, 23, 24, 25], "benchmark": 0, "best": [0, 4, 7, 17, 20], "beta": [16, 19], "beta0": [0, 2], "better": [0, 2, 3, 4, 6, 7], "between": [0, 2, 3, 7, 9, 11, 17, 18, 23, 25], "beyond": 0, "bff6f9b14b8d": 5, "bilinear": 2, "bill": [0, 7], "bin": 19, "binari": [0, 4, 19], "bit": [0, 4, 5, 17, 20, 22, 25], "black": 6, "blindli": [7, 25], "block": [0, 2, 4, 11, 19, 24, 25, 26], "blue": [0, 4, 14, 18, 26], "blur": [0, 2], "bmud": 4, "bool": [0, 11, 14], "boolean": [0, 2, 15, 26], "bootstrap": 3, "border": [0, 2, 17, 25], "bot": 19, "both": [0, 2, 9, 11, 19, 25], "bottom": [0, 2], "bound": [0, 2, 12, 17, 24, 26], "boundari": [0, 2], "bounding_box": [0, 7, 26], "box": [0, 2, 8, 11, 17, 24, 25, 26], "bp": 0, "branch": 19, "brdf": 9, "break": [7, 16], "breiman": 0, "briefli": 26, "bright": [0, 2], "broad": 21, "broken": 7, "browser": [3, 4, 19], "budget": [0, 7], "buffer": 2, "bug": [2, 6, 7, 19, 26], "build": [1, 2, 4, 6, 7, 11, 16, 20, 22, 24, 25], "build_process_dict": [0, 7, 26], "builder": 0, "built": [0, 3, 19], "builtin": [7, 24, 25], "bump": [7, 19], "bunch": 6, "burn": 19, "button": [5, 19], "byte": 0, "c": [19, 21, 26], "c9c51646b6a4": 5, "cach": [3, 7, 8], "calcul": [0, 2, 4, 14, 26], "calendar": [0, 2], "calibr": [9, 13], "call": [0, 2, 3, 4, 5, 6, 7, 11, 12, 13, 16, 17, 18, 21, 22, 25, 26], "callabl": [0, 1, 2, 7, 11], "callback": [0, 1, 2, 4, 7, 11, 20], "can": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26], "cancel": [0, 5, 7, 11], "cancel_running_job_aft": [7, 11], "candid": 2, "canon": [0, 2, 16], "canopi": [0, 4], "capabl": [0, 4, 7], "captur": 0, "card4l": [0, 2, 9], "cardin": 0, "care": [0, 2, 3], "carefulli": [17, 21], "case": [0, 2, 3, 5, 7, 9, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 24, 25, 26], "case_sensit": 2, "catalog": [0, 2, 12, 17], "categori": [0, 2, 7], "caus": [7, 17], "caveat": [3, 17], "cbartext": 0, "cd": 19, "cdefgh": [0, 2], "cdse": [7, 19], "ceil": [2, 23], "cell": [0, 2, 4, 5, 6], "celsiu": 26, "center": [0, 2], "center_wavelength": 12, "central": [3, 4, 24, 25], "centroid": 2, "ceo": 9, "certain": [0, 6, 9, 13, 17, 26], "certainli": [3, 9], "cf": [12, 19], "chain": [0, 24, 25, 26], "challeng": [3, 25], "chang": [0, 2, 4, 8, 11, 12, 14, 15, 16, 19, 20, 21, 22], "changelog": [19, 20], "channel": [0, 7, 19], "chapter": 4, "charact": 25, "charg": 0, "cheap": 13, "check": [2, 4, 5, 12, 21, 25, 26], "check_error": 0, "checkout": [19, 21], "child": [0, 1, 2, 4, 7, 20, 25], "chmod": 7, "choic": [0, 3, 6, 25], "choos": [0, 2, 17, 25], "chosen": [0, 2, 9, 25], "chunk": [0, 25], "chunk_polygon": [0, 7], "chunk_siz": [0, 7], "chunksiz": 12, "chunktyp": 12, "ci": 7, "circumv": [2, 17], "cite": 25, "cl": 0, "cl13n7s3cr3t": 3, "clariti": 7, "class": [0, 1, 4, 5, 6, 7, 11, 18, 20, 22, 24, 26], "classif": [0, 2, 4, 7, 20], "classifi": 22, "classmethod": 0, "clean": 19, "clear": [2, 7, 17, 25], "clearli": [3, 7], "cli": 3, "client": [0, 1, 2, 4, 5, 6, 7, 8, 10, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26], "client_credenti": [0, 3], "client_id": [0, 3], "client_secret": [0, 3], "client_vers": [6, 21], "clientjob": 0, "climat": 2, "climatologi": 2, "climatological_norm": [2, 23], "climatology_period": 2, "clip": [0, 2, 23], "clone": [7, 12, 19], "close": [0, 2, 4, 6, 21, 26], "closest": [0, 2], "cloud": [0, 2, 3, 5, 7, 8, 12, 17, 18, 20], "cloud_cov": [0, 7, 12, 17], "cloud_detect": [2, 23], "cloud_detection_method": [0, 2], "cloud_detection_opt": [0, 2, 7], "cmap": [0, 12], "cmdoption": 2, "co": [1, 2, 23, 24], "coars": 0, "code": [0, 2, 4, 7, 12, 13, 20, 21, 24, 25], "coeffici": [0, 2, 7], "coher": 25, "col_1": 0, "col_2": 0, "collect": [0, 2, 3, 6, 7, 9, 10, 11, 14, 18, 20, 22, 25, 26], "collection_id": [0, 7], "collection_item": [0, 7], "collection_properti": [0, 7, 17], "collectionmetadata": [0, 7, 25], "collectionproperti": 0, "collis": 19, "color": 0, "colormap": 0, "colour": 0, "column": [0, 4, 11, 22], "com": [0, 3, 9, 11, 12, 17, 18, 19, 21, 25], "combin": [0, 2, 7, 13, 17, 24, 25], "come": [3, 4, 5, 6, 17, 25], "comma": 25, "command": [0, 3, 4, 7, 19, 25], "comment": [0, 8, 25], "commit": 3, "common": [0, 2, 3, 4, 6, 9, 11, 14, 17, 20, 25, 26], "common_nam": [0, 2, 12], "commonli": [2, 24], "commonmark": 0, "commun": [3, 6, 21, 22], "compact": [4, 17], "compactli": [1, 2, 24, 26], "compar": [0, 2, 7, 9, 25], "comparablevers": 7, "comparison": [2, 4, 7], "compat": [0, 25, 26], "compil": [2, 19], "complet": [2, 3, 7, 17, 24], "complex": [0, 3, 4, 14, 21, 24, 25, 26], "compliant": [0, 2, 7, 9], "compon": [2, 4, 7], "compos": 12, "composit": 4, "comput": [0, 2, 7, 9, 12, 13, 14, 17, 20], "computation": 9, "compute_and_rescale_indic": [10, 14], "compute_index": [10, 14], "compute_indic": [10, 14], "computed_band_1": 25, "computed_band_2": 25, "concat": 0, "concaten": [0, 2], "concept": [3, 4, 17, 22], "conceptu": [1, 2], "concis": 0, "concret": [0, 1, 2, 19, 25], "conda": [7, 19], "condit": [0, 2, 7], "config": [7, 8, 19, 20], "configur": [0, 2, 3, 7, 19, 20], "conflict": [0, 2, 6, 7, 21, 22], "conftest": 0, "confus": [7, 19, 22, 25, 26], "confusingli": 22, "congrat": 4, "conn": 0, "connect": [5, 6, 7, 8, 9, 11, 12, 14, 15, 16, 17, 18, 20, 22, 23, 24, 25, 26], "connection_provid": 11, "connection_retry_interv": 0, "connector": 25, "consecut": 2, "consequ": [0, 25], "consid": [0, 2, 3, 6, 19, 21], "consider": 25, "consist": [0, 2, 6, 7, 12, 17, 26], "constant": [0, 2, 7, 23, 24], "constraint": [4, 13, 20], "construct": [0, 2, 4, 7, 16, 17, 20, 24, 25, 26], "constructor": [0, 26], "consult": [3, 11, 16, 26], "contain": [0, 2, 3, 4, 7, 8, 9, 11, 12, 14, 16, 17, 18, 19, 24, 25, 26], "content": [0, 2, 7, 12, 25], "context": [0, 2, 7, 8, 11, 20, 25], "continent": 13, "continu": 0, "contour": 0, "contrast": 2, "contribut": [0, 2, 20, 21], "contributing_area": [0, 2], "control": [0, 2, 3, 5, 6, 7, 8, 24, 25], "conveni": [0, 4, 7, 11, 13, 25], "convent": [0, 6, 12, 14, 17, 19], "convers": [4, 7, 20, 24, 26], "convert": [0, 2, 4, 15, 24, 26], "convolut": [0, 2, 24], "cookbook": [7, 20], "coord": 25, "coord_i": 25, "coord_x": 25, "coordin": [0, 2, 4, 5, 7, 12, 17, 22, 25, 26], "copi": [0, 3, 25], "core": 24, "corner": [0, 2], "correct": [0, 2, 10, 25], "correctli": [7, 12, 24, 25], "correl": 25, "correspond": [0, 1, 2, 4, 5, 11, 18, 19, 22, 23, 24, 26], "corrupt": 5, "cosh": [2, 23, 24], "cosin": 2, "cost": [0, 4, 9, 13, 17, 25], "could": [0, 2, 3, 5, 16, 25, 26], "count": [0, 2, 4, 7, 11, 17, 23], "count_by_statu": 11, "count_tim": [0, 23], "counter": 7, "countmismatch": 2, "coupl": [0, 3, 4, 5, 6, 9, 17, 22, 24, 25, 26], "cours": 13, "cousin": 26, "cover": [0, 2, 4, 7, 11, 12, 13, 17, 18, 22, 24, 25, 26], "coverag": 12, "cpu": 0, "cr": [0, 2, 7, 12, 26], "creat": [0, 1, 2, 4, 7, 11, 14, 16, 18, 20, 24, 25, 26], "create_collect": 0, "create_data_cub": 2, "create_job": [0, 5, 7, 11, 13, 15, 18, 22], "create_job_db": [7, 11], "create_raster_cub": [7, 23], "create_servic": [0, 7], "creation": [5, 7, 10, 13], "creator": 11, "credenti": [0, 7, 20], "criteria": 17, "crop": 22, "cryptic": [0, 5, 7], "csv": [4, 7, 11], "csvjobdatabas": [7, 10, 11], "cube": [0, 1, 2, 5, 7, 11, 13, 14, 15, 16, 18, 20, 22], "cube1": [0, 2, 18], "cube2": [0, 2, 18], "cube_upd": 25, "cubearrai": 25, "cubemetadata": 7, "cubic": 2, "cubicsplin": 2, "culprit": 3, "cumbersom": 25, "cummax": [2, 23], "cummin": [2, 23], "cumproduct": [2, 23], "cumsum": [2, 23], "cumul": 2, "curl": [0, 7], "currenc": 7, "current": [0, 2, 3, 4, 5, 7, 8, 9, 11, 19, 24, 25], "curv": 2, "custom": [0, 3, 7, 8, 9, 11, 24, 25, 26], "cycl": 5, "d": 17, "d5b8b8f2": 5, "d7393fba": 3, "dai": [0, 2, 3, 17], "daili": [2, 3], "dask": [7, 12], "data": [0, 1, 2, 3, 5, 7, 10, 11, 12, 13, 14, 18, 20, 22, 25], "data_dict": 0, "data_list": 0, "data_paramet": 0, "data_typ": 12, "dataarrai": [0, 7, 12, 25], "databas": [3, 7, 11], "datacub": [2, 4, 5, 7, 9, 10, 13, 14, 15, 17, 20, 22, 23, 24, 26], "datacube_from_fil": 0, "datacube_from_flat_graph": [0, 7], "datacube_from_json": [0, 7, 16, 18], "datacube_from_process": [0, 7, 16, 18, 26], "datacube_list": 0, "datacube_plot": 0, "datacube_to_fil": 0, "datacubeempti": 2, "datafram": [0, 4, 7, 11, 21], "dataset": [0, 2, 9, 10, 17, 20, 25], "date": [0, 2, 3, 4, 7, 19, 24, 25, 26], "date1": 2, "date2": 2, "date_between": 2, "date_differ": 2, "date_range_filt": 7, "date_shift": [2, 23], "date_tim": [0, 26], "datetim": [0, 7, 17, 25], "datetime64": 12, "david": 14, "davidfrantz": 9, "db": 24, "dd": [2, 17], "debug": [0, 5, 7, 25], "decad": [0, 2], "decemb": [0, 2], "decid": [0, 9], "decim": 2, "deciph": 4, "decis": 9, "declar": [0, 7], "decod": [0, 7], "decompress": 7, "decor": 7, "decoupl": 3, "decreas": 2, "dedic": [17, 25], "dedupl": 0, "deep": 0, "deeper": 17, "deepli": 0, "def": [0, 1, 2, 7, 11, 24, 25], "default": [0, 2, 5, 7, 8, 9, 11, 16, 17, 18, 20, 24, 25, 26], "default_backend": [3, 8], "default_timeout": [0, 7], "defin": [0, 2, 3, 4, 5, 7, 10, 11, 14, 17, 18, 19, 20, 22], "definit": [0, 7, 11, 24], "degre": [0, 2, 16, 26], "dekad": [0, 2], "delet": [0, 5, 7], "delete_job": 0, "delete_servic": [0, 7], "delimit": 25, "deliv": 2, "delta": 2, "dem": [0, 2], "demand": [7, 21], "demo": [6, 12], "dep": 19, "depend": [0, 1, 2, 3, 5, 7, 9, 11, 13, 17, 19, 20], "deprec": [0, 2, 11, 25, 26], "depth": [0, 3, 4, 24], "dereference_from_node_argu": 7, "deriv": 22, "descend": 2, "describ": [0, 2, 3, 5, 7, 11, 16, 19, 21, 24], "describe_account": [0, 3], "describe_collect": [0, 4, 7, 17], "describe_job": [0, 7], "describe_process": [0, 7, 24], "descript": [0, 2, 5, 7, 8, 12, 13, 16, 19, 24, 26], "design": [6, 17, 25], "desir": [0, 2, 3, 4, 5, 7, 11, 18, 24, 25, 26], "desktop": 6, "destin": 0, "detail": [0, 2, 3, 4, 5, 6, 7, 8, 16, 17, 18, 21, 24, 25, 26], "detect": [0, 2, 3, 7, 14, 24, 25], "determin": [0, 2, 11, 13, 14, 17], "dev": [19, 21], "develop": [0, 2, 6, 7, 20, 22, 25, 26], "deviat": [0, 2, 14], "devic": [0, 4, 7, 20], "df": [4, 7, 11], "dfn": 0, "dict": [0, 7, 11, 12, 14, 25], "dictionari": [0, 4, 7, 11, 14, 17, 18, 24, 25], "did": [0, 4, 7], "didn": 5, "differ": [0, 2, 3, 4, 8, 9, 14, 16, 17, 18, 19, 22, 25, 26], "digit": [0, 2, 4, 20, 25], "dilat": [2, 18], "dim": 25, "dimens": [0, 1, 2, 4, 7, 12, 22, 24, 25], "dimension": [0, 2, 25], "dimension_label": [0, 2, 7, 23], "dimensionalreadyexistsexcept": 7, "dimensionexist": [0, 2], "dimensionlabelcountmismatch": 0, "dimensionmismatch": 2, "dimensionnotavail": [0, 2], "dir": 0, "direct": [0, 5, 25], "directli": [0, 1, 2, 3, 4, 6, 7, 10, 11, 17, 18, 19, 20, 21, 22, 24, 25, 26], "directori": [0, 3, 5, 8, 11], "disabl": [0, 2, 7, 19], "disallow": 7, "disconnect": 4, "discov": [1, 2, 17, 25], "discoveri": [0, 2, 7, 20], "discuss": [0, 1, 2, 3, 4, 5, 8, 16, 19, 22, 24, 26], "disk": [0, 5], "displai": [0, 7], "dist": 19, "distanc": [2, 4], "distinct": [0, 2, 7], "distribut": 25, "disturb": [0, 2], "distutil": 7, "div": 18, "dive": 17, "divid": [0, 2, 16, 18, 23, 24, 25, 26], "divide1": 26, "divide18": 26, "dividend": 2, "divis": 2, "divisor": 2, "djf": 2, "do": [0, 1, 2, 3, 4, 5, 7, 12, 13, 15, 17, 19, 22, 24, 25, 26], "doc": [0, 2, 5, 7, 9, 19], "docker": 19, "document": [0, 2, 3, 5, 6, 7, 17, 20, 21, 22, 24, 25, 26], "doe": [0, 2, 3, 4, 5, 7, 11, 17, 19, 24, 25], "doesn": [0, 2, 3, 5, 7], "domain": 7, "domini": [0, 2], "don": [0, 2, 3, 4, 7, 8, 14, 17, 21, 24, 25, 26], "done": [0, 3, 5, 11, 13, 17, 25], "doubl": [7, 15, 17, 21, 25], "down": [0, 2], "download": [0, 2, 7, 9, 11, 15, 17, 18, 19, 20, 22, 26], "download_fil": [0, 5, 7], "download_result": 0, "draft": [7, 26], "drastic": 17, "drawn": 0, "drive": 17, "driver": [4, 7, 25], "drop": [0, 2, 7, 19], "drop_dimens": [0, 2, 23], "dropbox": 17, "dropna": 4, "dtny": 3, "dtype": 12, "due": [0, 2], "dump": [0, 3, 5, 15, 26], "duplic": 7, "durat": 0, "dure": [2, 3, 5, 7, 11, 19], "dynam": [0, 20, 25], "e": [0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14, 17, 19, 21, 22, 23, 24, 25, 26], "e4df8648": 7, "each": [0, 1, 2, 3, 4, 5, 11, 12, 19, 21, 24, 25], "earli": 4, "earlier": [0, 3, 4, 7, 18], "earth": [0, 2, 3, 4, 12, 14, 20, 22], "eas": 3, "easi": [0, 8, 21, 26], "easier": [0, 1, 2, 3, 5, 7, 22, 24, 25], "easiest": [3, 5, 19, 24], "easili": [0, 3, 4, 5, 7, 19, 21, 24, 25], "east": [0, 4, 6, 7, 9, 12, 17, 20, 24, 25, 26], "ecosystem": [0, 21], "edit": [3, 8, 19], "editor": [1, 2, 5, 25, 26], "effect": [2, 25], "effici": 25, "effort": [0, 4, 7], "egi": 4, "either": [0, 2, 11], "element": [0, 2, 12, 25], "element84": 12, "elev": [0, 2], "elevation_model": [0, 2], "elimin": [4, 7, 22, 25], "ellipsoid": [0, 2], "ellipsoid_incidence_angl": [0, 2], "els": [2, 6, 18, 21, 24], "email": 3, "emb": [0, 17, 25], "embed": 0, "empti": [0, 2, 19], "en": 2, "enabl": [0, 2, 3, 7, 19, 26], "encapsul": [4, 16, 18, 25, 26], "enclos": 2, "encod": [0, 4, 7, 15], "encount": [0, 26], "end": [0, 1, 2, 5, 7, 8, 12, 16, 18, 19, 20, 22, 24, 25, 26], "end_dat": [0, 17, 24], "endpoint": [0, 2, 7, 25], "enforc": [5, 7, 19], "engin": [6, 7], "enhanc": 4, "enough": [0, 4, 14, 17, 19, 21], "ensur": [7, 25], "ensure_job_dir_exist": 11, "enter": [3, 4, 19], "entir": 0, "entiti": [0, 17, 18], "entri": [0, 2, 25], "entrypoint": 25, "enum": 0, "enumer": 2, "env": [7, 19], "environ": [0, 5, 7, 8, 19, 21, 25], "eo": [0, 1, 2, 4, 7, 12, 13, 16, 17, 18, 19, 21, 22, 25], "ep": 7, "epsg": [0, 2, 7, 12, 17], "eq": [2, 7, 23, 24], "equal": [0, 2, 7], "equival": [0, 2, 17, 24], "era": [0, 2], "eros": 2, "error": [0, 2, 5, 7, 11, 17, 21, 24, 25], "esa": 9, "especi": [2, 3], "essenti": [0, 7, 17], "establish": [3, 4], "estim": [0, 7, 25], "estimate_job": 0, "etc": [0, 2, 3, 4, 5, 7, 14, 15, 19, 25, 26], "etcetera": 0, "eu": [4, 9], "euler": 2, "evalu": [0, 1, 2, 7, 16, 20, 25], "even": [3, 4, 5, 6, 7, 11, 19, 25, 26], "event": [0, 7], "eventu": 0, "everi": [2, 9, 12, 25], "everyth": [0, 4, 18, 24], "everywher": 25, "evi": [18, 20], "evi_aggreg": [4, 26], "evi_composit": 4, "evi_cub": [4, 25], "evi_cube_mask": [4, 25], "evi_mask": 4, "evi_timeseri": 26, "exact": 25, "exactli": [0, 2, 11, 18, 25], "exampl": [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 12, 13, 15, 16, 17, 19, 21, 22, 24], "example_aoi": 17, "exceed": 24, "except": [0, 2, 3, 5, 7, 9, 11, 25], "excess": 19, "exchang": 25, "exclud": [0, 2], "exclude_max": 2, "exclus": [0, 2], "execut": [0, 1, 2, 3, 7, 10, 12, 16, 18, 19, 20, 22, 26], "execute_batch": [0, 5, 7, 9, 25], "execute_local_udf": [0, 7, 25], "exist": [0, 2, 3, 4, 5, 7, 8, 11, 14, 19, 25], "exit": 3, "exp": [2, 23], "expand": [0, 2, 17], "expans": 24, "expect": [0, 2, 3, 12, 24, 25, 26], "expected_statu": 0, "expens": [9, 25], "experi": [4, 12, 19, 25], "experiment": [0, 2, 4, 7, 8, 11, 12, 14, 16, 17, 22, 24, 25], "expir": [3, 7], "expiri": 3, "explain": [0, 3, 4, 20, 26], "explic": 3, "explicit": [5, 7, 8, 21, 24, 25], "explicitli": [0, 2, 3, 4, 5, 7, 14, 17, 18, 24, 25], "explor": 4, "expon": 2, "exponenti": 2, "export": [0, 3, 7, 10, 13], "export_path": 15, "expos": 26, "express": [0, 1, 2, 4, 7, 17, 24, 25], "extend": [0, 2], "extens": [0, 2, 7, 8, 22, 25], "extent": [0, 2, 4, 7, 9, 12, 20, 24, 26], "extern": [0, 2], "extra": [7, 11, 14, 19, 21], "extract": [0, 2, 4, 7, 13, 19, 26], "extract_udf_depend": [0, 7, 25], "extrema": [2, 23], "ey": [3, 5, 17, 21, 25], "f": [0, 4, 11, 15, 16, 26], "f9f4e3d3": 5, "fact": [5, 6, 15, 17, 25], "factor": [0, 2, 13, 24, 25], "factori": [0, 7], "fahrenheit": [16, 26], "fahrenheit_param": 26, "fahrenheit_to_celsiu": [16, 26], "fahrenheittocelsius1": 26, "fail": [0, 2, 5, 7, 18, 25], "failur": [3, 7], "fairli": [4, 25], "fall": [0, 3, 7, 24, 25], "fallback": [0, 3, 11], "fals": [0, 2, 7, 14, 25], "fanci": [24, 25, 26], "fancy_load_collect": 26, "fancy_upsample_funct": 25, "fancyeo": 25, "far": [3, 4], "fashion": [24, 25], "faster": 6, "favor": [0, 7], "fe79": 5, "feasibl": 3, "featur": [0, 2, 3, 4, 7, 8, 9, 11, 12, 13, 16, 17, 19, 22, 24, 25, 26], "feature_collect": 22, "feature_collection_list": 0, "feature_flag": 24, "featurecollect": [0, 2, 4, 22], "feb": [0, 2], "februari": [0, 2], "fedcba": [0, 2], "feder": 7, "feedback": 11, "feel": [5, 6, 19, 20, 21], "fetch": [0, 3, 5], "fetch_metadata": [0, 7], "few": 9, "fewer": 2, "field": [0, 2, 4, 7, 11, 25, 26], "figur": [6, 25], "file": [0, 2, 4, 5, 7, 11, 12, 13, 15, 16, 17, 18, 20, 21, 25], "file_format": 0, "file_list": 0, "filenam": [0, 7, 8], "fill": [0, 2, 11, 14, 25], "filter": [0, 2, 4, 7, 9, 11, 20, 25], "filter_band": [0, 2, 7, 17, 23, 24], "filter_bbox": [0, 2, 7, 9, 17, 23, 24], "filter_label": [0, 2, 7, 23], "filter_spati": [0, 2, 7, 13, 23], "filter_tempor": [0, 2, 7, 9, 17, 18, 23, 24, 26], "filter_vector": [0, 2, 7], "final": [4, 9, 11, 17, 19, 22, 26], "find": [0, 2, 4, 11, 19, 20, 21, 25], "fine": [7, 14, 18, 19, 26], "finer": 17, "finetun": 24, "finish": [0, 3, 7, 11, 19, 22, 25], "finit": [0, 7], "firewal": 7, "first": [0, 2, 3, 4, 6, 7, 8, 9, 11, 12, 15, 17, 19, 20, 21, 22, 23, 24], "fit": [0, 2, 17], "fit_": 0, "fit_class_random_forest": [0, 2, 7, 22, 23], "fit_curv": [0, 2, 7, 23], "fit_regr_random_forest": [0, 2, 7, 22, 23], "fix": [2, 19, 25], "fixtur": 0, "flag": [0, 2, 7, 11, 16, 19], "flat": [0, 7, 16, 18], "flat_graph": [0, 15], "flatgraphablemixin": 0, "flatten": [0, 7], "flatten_dimens": [0, 2, 7, 23], "flawlessli": 2, "flesh": 4, "flexibl": [0, 11, 24], "flight": 7, "flip": 25, "float": [0, 2, 11, 25, 26], "float32": 12, "float64": 12, "floor": [2, 23], "flow": [0, 7, 20, 22, 24], "fmt": [0, 25], "fncy": 25, "focal": 0, "focu": [6, 22, 25], "focuss": [8, 26], "folder": [0, 2, 5, 7, 8, 11, 12, 19], "follow": [0, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18, 19, 21, 22, 24, 25, 26], "font": 0, "fontsiz": 0, "foo": [0, 11], "forbidden": 7, "forc": [0, 9], "forest": [0, 2, 20], "forg": [7, 19, 21], "fork": 19, "form": [3, 7, 11, 17], "format": [0, 2, 4, 5, 6, 7, 9, 11, 13, 14, 15, 17, 18, 19, 25, 26], "format_opt": 0, "formatt": 19, "formatunsuit": 2, "former": [3, 22], "formula": [0, 4, 14], "forum": [7, 19, 21], "forward": 3, "found": [0, 2, 25], "four": [0, 2], "fourth": [0, 2], "fraction": [0, 2, 7], "frame": 11, "framework": 19, "free": [5, 17, 21, 24], "freedom": 25, "frequenc": [2, 5], "fresh": 21, "freshli": 19, "friendli": [7, 17], "friendlier": 7, "from": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 19, 20, 21, 22], "from_dict": 0, "from_fil": [0, 25], "from_flat_graph": 0, "from_metadata": 0, "from_netcdf_fil": 7, "from_nod": [4, 18, 26], "from_paramet": [7, 18, 24, 25, 26], "from_sequ": 0, "from_url": 0, "from_user_input": 0, "full": [0, 7, 9, 13, 25], "full_width_half_max": 12, "fulli": [0, 2, 4, 5, 11, 16, 19, 25], "function": [0, 1, 4, 6, 7, 8, 11, 12, 14, 20, 21, 22, 23, 24], "fundament": 4, "further": [2, 3, 4, 5, 13, 18, 26], "fuse": 0, "fusion": 4, "futur": 25, "g": [0, 1, 2, 3, 4, 5, 6, 7, 11, 13, 14, 17, 19, 21, 22, 24, 25, 26], "gamma0": [0, 2], "gap": 25, "gatewai": [4, 7], "gaussian": [0, 2], "gdal": 2, "gdalwarp": 2, "ge": 23, "gener": [2, 4, 5, 7, 8, 10, 12, 17, 20, 25, 26], "geo": [7, 24], "geodatafram": 25, "geojson": [0, 2, 4, 7, 11, 13, 17, 22, 25, 26], "geometri": [0, 2, 4, 5, 7, 10, 17, 22, 24, 25, 26], "geometriesoverlap": 0, "geometry_count": 2, "geometrycollect": 2, "geopanda": [11, 21, 25], "geoparquet": 17, "geopyspark": [4, 7], "geosjon": 25, "geospati": 21, "geotiff": [0, 4, 5, 7, 9, 12, 17, 21], "geotrelli": [12, 25], "get": [0, 2, 3, 5, 6, 7, 11, 15, 17, 19, 20, 22, 24, 25, 26], "get_arrai": [0, 25], "get_asset": [0, 5, 9], "get_by_statu": 11, "get_datacube_list": 0, "get_error_log_path": 11, "get_feature_collection_list": 0, "get_fil": 0, "get_job_dir": 11, "get_job_metadata_path": 11, "get_metadata": [0, 5], "get_path": 0, "get_result": [0, 5, 7, 9], "get_results_metadata_url": [0, 7], "get_run_udf_callback": 0, "get_structured_data_list": 0, "getitem": 12, "getter": 11, "gfedcb": [0, 2], "gi": 22, "git": [12, 19, 21], "github": [0, 3, 7, 9, 12, 16, 17, 19, 21, 25], "give": [0, 4, 5, 6, 9, 17, 20, 21, 24, 25], "given": [0, 2, 3, 5, 7, 11, 14, 16, 17, 18, 24, 25, 26], "gl": 3, "glitch": 0, "glob": 0, "glob_pattern": 0, "global": 19, "glue": 25, "go": [4, 16, 19], "goal": [21, 25], "goe": 4, "golai": 25, "good": [17, 19, 25, 26], "googl": [3, 17], "got": 7, "gradual": [17, 19], "grain": 7, "grant": [3, 7], "granular": 17, "graph": [1, 2, 4, 5, 7, 10, 12, 16, 17, 20, 24, 25, 26], "graph_add_nod": 0, "graph_build": [0, 24], "graphic": [4, 5, 25], "gravit": 6, "grd": 9, "great": 25, "greater": [0, 2, 7], "greatli": [11, 19], "green": 12, "grid": [0, 2], "ground": [0, 2, 4], "group": [0, 2, 13, 18], "grown": 0, "gsd": 12, "gt": [2, 23], "gte": [2, 23], "gtiff": [0, 5, 9, 13, 22, 26], "guarante": [12, 25], "guess": [0, 2, 7, 14], "guid": [6, 14], "guidelin": [6, 25], "gz": 25, "h": [0, 3, 19], "h5netcdf": [7, 21], "ha": [0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 14, 17, 19, 22, 25], "hack": 7, "had": [0, 2, 5, 19], "half": 17, "handi": [3, 4, 17], "handl": [0, 2, 3, 4, 5, 7, 10, 12, 14, 20, 24], "hang": 19, "happen": [4, 21], "hard": [3, 6, 9, 17], "hardcod": [7, 18, 26], "harder": 7, "hash": 25, "have": [0, 2, 3, 4, 5, 7, 9, 11, 12, 13, 14, 15, 16, 17, 19, 21, 22, 24, 25, 26], "haven": 14, "haze": 2, "header": [0, 3, 7], "heavi": 22, "heavier": 5, "heavili": [3, 4, 21], "height": [0, 2, 7], "help": [0, 1, 2, 3, 5, 25], "helper": [0, 1, 4, 5, 7, 8, 11, 14, 20, 21, 24, 26], "henc": [9, 17], "here": [0, 2, 3, 4, 6, 13, 16, 17, 18, 19, 22, 24, 25, 26], "hgfedc": [0, 2], "hhhhhh": [0, 2], "hidden": [7, 24], "hide": [0, 7, 26], "hierarchi": [0, 2, 7], "high": [6, 20], "high_resolution_band": 0, "higher": [0, 2, 25], "highest": 0, "highlight": [4, 6, 25], "hint": [1, 2], "histogram": 0, "histori": 19, "hit": [4, 8, 17], "hoc": [6, 19], "home": [3, 8], "homebrew": 19, "homogen": 0, "honor": 0, "hook": 19, "horizont": [0, 2, 6], "host": [2, 19], "hour": [0, 2, 3, 4], "how": [0, 2, 3, 4, 5, 6, 11, 17, 19, 21, 24, 25, 26], "howev": [1, 2, 3, 4, 5, 8, 16, 19, 22, 24, 25], "href": [0, 5, 16], "html": [0, 2, 7, 19], "http": [0, 2, 4, 5, 7, 9, 11, 12, 13, 15, 16, 17, 18, 19, 20, 24, 25], "hub": [4, 9, 17], "human": [0, 6], "hundr": 2, "hyperbol": 2, "hypercub": 0, "i": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26], "ic": 2, "icor": 9, "id": [0, 1, 2, 3, 4, 5, 7, 11, 12, 14, 16, 17, 18, 22, 24, 25, 26], "idea": [17, 25], "ideal": [3, 25], "ident": 3, "identifi": [0, 2, 3, 4, 5, 11], "if_": [2, 23], "ignor": [0, 2, 7, 24], "ignore_nodata": 2, "illumin": 2, "illustr": [3, 4, 16, 17, 25, 26], "imag": [0, 2, 4, 5, 24, 25], "imagecollect": [0, 7], "imagecollectioncli": 7, "imageri": 2, "imagin": 18, "immedi": 2, "impact": [4, 17, 25], "implement": [0, 1, 2, 4, 7, 10, 11, 12, 13, 18, 21, 22, 24, 25], "impli": [0, 16], "implicit": [2, 7], "implicitli": 2, "import": [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 16, 17, 18, 20, 21, 24, 25, 26], "imposs": [3, 6, 9], "impract": 13, "impress": 25, "improv": [0, 2, 6, 7, 11, 17, 25], "imshow": 12, "inaccuraci": 2, "incid": [0, 2], "includ": [0, 2, 3, 5, 7, 11, 13, 25], "include_stac_metadata": 0, "inclus": [0, 2], "incomplet": 2, "inconsist": 25, "incorrectli": 24, "increas": [0, 2, 17], "increment": [0, 2, 18], "indefinit": 11, "indent": [0, 4, 6, 26], "independ": [0, 2], "index": [0, 2, 4, 7, 14, 20, 25], "index_dict": 14, "indic": [0, 2, 4, 7, 10, 13], "indirectli": 0, "individu": [0, 2, 4, 13, 25], "infer": [17, 25], "infinit": [0, 2], "info": [0, 2, 5, 7, 8, 11], "inform": [0, 2, 3, 4, 5, 7, 9, 11, 14, 16, 17, 19, 21, 24, 26], "infrar": [4, 14], "infrastructur": 12, "inher": 2, "ini": [3, 8], "init_pixel_size_i": 25, "init_pixel_size_x": 25, "initi": [0, 2, 3, 7, 11, 20, 25], "initialize_from_df": [7, 11], "inject": [19, 26], "inlin": [0, 4, 11, 24, 25], "inner": 2, "input": [0, 2, 4, 7, 9, 13, 14, 24, 25, 26], "input_max": [0, 7], "input_metadata": 25, "input_min": [0, 7], "input_rang": 14, "inputmax": [0, 2], "inputmin": [0, 2], "inputs_cub": 25, "insensit": [0, 2], "insert": [2, 24], "insid": [0, 1, 2, 7, 16, 17, 22, 25], "inspect": [0, 2, 4, 5, 7, 23, 24, 25, 26], "inspir": 20, "instal": [3, 7, 10, 20, 25], "instanc": [0, 1, 2, 4, 5, 7, 13, 17, 18, 22, 24, 25, 26], "instant": [2, 17, 25], "instanti": 11, "instead": [0, 1, 2, 3, 4, 7, 14, 15, 17, 18, 19, 22, 24, 25, 26], "institut": [3, 12], "instruct": [5, 25], "instrument": 0, "int": [0, 2, 9, 11, 23, 25], "int32": 12, "integ": [0, 2, 7, 25, 26], "integr": [0, 4, 7, 21], "intend": [0, 1, 2, 3, 15, 24], "intens": 5, "intention": [3, 19], "interact": [0, 1, 2, 4, 5, 7, 8, 15, 20, 21, 25], "intercept": 3, "interest": [3, 4, 5, 17], "interesting_rdd_id": 25, "interfac": [2, 11, 20], "intermedi": [17, 18], "intern": [2, 6, 7, 15, 19, 24], "interoper": [0, 7, 15], "interpol": 2, "interpolate_na": 25, "interpret": 0, "interrupt": [7, 11], "intersect": [0, 2], "interv": [0, 2, 3, 26], "introduc": [7, 19, 21, 25], "intrus": 19, "intuit": [11, 24], "invalid": [0, 2, 7, 15, 19], "invalidtimeseriesexcept": 0, "invalidvalu": 0, "invers": 2, "invert": [0, 2], "investig": 5, "invit": 6, "invoc": 0, "invok": [7, 9, 11, 24, 25, 26], "involv": [3, 7], "inward": 2, "io": 7, "ipyleaflet": 0, "irrelev": 6, "is_infinit": [2, 23], "is_nan": [2, 23, 24], "is_nodata": [2, 23], "is_valid": [0, 2, 7, 23, 24], "isol": 25, "issu": [0, 3, 7, 15, 19, 21, 24], "issuer": 3, "item": [0, 2, 5, 7, 10, 18, 22, 24, 26], "item_asset": 7, "item_schema": 0, "iter": [0, 5, 24], "its": [0, 2, 3, 4, 5, 6, 7, 11, 12, 14, 16, 17, 21, 26], "itself": [0, 2, 3, 7, 11, 19, 24], "j0hn123": 3, "j9a7k2": 5, "januari": 2, "jenkin": 19, "jep": 25, "jja": 2, "job": [1, 2, 3, 7, 9, 10, 13, 15, 18, 20, 22, 25, 26], "job_db": 11, "job_id": [0, 5, 7, 11, 22], "job_list": 0, "job_log": [0, 7], "job_manag": 11, "job_opt": [0, 25], "job_result": [0, 7], "job_start": 11, "jobdatabaseinterfac": [7, 10, 11], "joblogentri": 7, "jobresult": [0, 5, 7], "jobs_df": 11, "john": [0, 3], "johndo": 16, "join": 0, "json": [0, 3, 4, 5, 7, 10, 11, 12, 16, 19, 20, 24, 25, 26], "jsonbin": 15, "juli": 16, "june": [0, 2, 12], "jupyt": [4, 7, 12, 15, 17, 20, 21, 25], "just": [0, 2, 3, 4, 5, 6, 7, 14, 15, 17, 18, 19, 22, 24, 25, 26], "kcachegrind": 25, "keep": [0, 1, 2, 3, 4, 5, 6, 7, 17, 19, 21, 24, 25], "kei": [0, 2, 11, 25], "kept": [0, 3], "kernel": [0, 2, 18, 26], "kerneldimensionsuneven": 2, "keyword": [0, 7, 24], "kind": [0, 1, 2, 3, 18, 19, 21, 24, 25], "klnx": 3, "know": [3, 5, 19, 25], "knowledg": [24, 25], "known": [0, 4, 17], "kwarg": [0, 7, 11, 24], "l19": 12, "l1c": 9, "l26": 12, "l2a": [12, 13], "lab": 20, "label": [0, 2, 25], "label_separ": [0, 2], "labelexist": 2, "labelnotavail": 2, "labelsnotenumer": 2, "lack": 7, "laid": 26, "lambda": [0, 1, 2, 4, 7, 17, 20, 24, 25], "lanczo": 2, "land": [0, 2, 7, 22], "landsat8": [7, 14], "languag": [6, 17, 24], "larg": [0, 2, 7, 13, 20, 25], "larger": [0, 3, 4, 5, 11, 13, 19, 24], "largest": 2, "last": [0, 2, 4, 5, 17, 23, 24, 25], "lat": 17, "later": [0, 2, 4, 17], "latest": [0, 7, 9, 19, 21], "latitud": [0, 17], "latter": [2, 3, 8, 22], "layer": [3, 4], "lazi": 12, "lazili": 7, "lc": 18, "le": 23, "lead": [0, 2, 3, 6, 15, 17], "leaf": 0, "leap": 2, "learn": [0, 2, 7, 20], "least": [0, 2, 7, 18, 19, 24, 25, 26], "leav": [0, 11], "left": [0, 2, 7], "leftov": 7, "legaci": [0, 5, 7, 25, 26], "legend": 0, "length": [2, 20], "less": [0, 2, 7], "let": [1, 2, 3, 4, 11, 12, 18, 19, 22, 26], "level": [2, 5, 7, 11, 17, 19, 20, 25], "levelnam": 11, "leverag": [0, 4, 11, 14, 19, 24], "librari": [0, 3, 4, 5, 6, 7, 8, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], "licens": 17, "life": 5, "lifetim": 3, "like": [0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 14, 15, 16, 17, 18, 21, 22, 24, 25, 26], "likewis": [3, 24, 26], "limit": [0, 2, 3, 4, 11, 17, 24, 25], "line": [0, 2, 3, 7, 19, 20, 21, 25], "linear": [0, 2], "linear_scale_rang": [0, 2, 7, 23], "link": [0, 2, 4, 7, 11, 16, 17, 24], "linspac": 25, "linter": 19, "linux": [3, 19, 25], "list": [0, 2, 4, 6, 7, 11, 14, 16, 17, 18, 19, 20, 24, 25, 26], "list_collect": [0, 12, 17], "list_collection_id": [0, 4, 17], "list_fil": [0, 7], "list_file_format": [0, 17], "list_file_typ": 0, "list_indic": [10, 14], "list_job": [0, 5, 7], "list_output_format": 0, "list_process": [0, 24], "list_processgraph": 7, "list_result": 0, "list_servic": [0, 7], "list_service_typ": 0, "list_udf_runtim": [0, 25], "list_user_defined_process": 0, "liter": 24, "live": [3, 19], "ll": [0, 4, 25, 26], "ln": [0, 2, 7, 23], "load": [0, 2, 3, 7, 8, 9, 10, 11, 12, 18, 20, 21, 22, 24, 25, 26], "load_byt": 0, "load_collect": [0, 2, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, 17, 20, 22, 23, 24, 25, 26], "load_dataset": 21, "load_disk_collect": [0, 7], "load_disk_data": 0, "load_geojson": [0, 2, 7, 23], "load_json": [0, 5], "load_json_resourc": 0, "load_ml_model": [0, 2, 7, 22, 23], "load_my_vector_cub": 24, "load_result": [0, 2, 7, 23], "load_stac": [0, 2, 7, 12, 23], "load_stac_from_job": [0, 7], "load_uploaded_fil": [0, 2, 7, 23], "load_url": [0, 2, 7], "loadcollection1": [4, 15, 26], "loaiza": 14, "local": [0, 2, 3, 4, 7, 10, 11, 15, 16, 18, 19, 20], "local_collect": 12, "local_conn": 12, "local_data_fold": 12, "local_incidence_angl": [0, 2], "localconnect": [7, 12], "localprocess": [7, 12], "locat": [0, 3, 4, 13, 24], "log": [2, 4, 7, 11, 20, 23], "log001": 5, "log002": 5, "log003": 5, "log10": [0, 7, 23], "log2": [0, 7, 23], "log_level": 0, "logarithm": [0, 2, 7, 23], "logentri": [0, 7], "logic": [0, 2, 7, 25], "logical_and": [0, 23], "logical_or": [0, 23], "long": [0, 2, 5, 6, 7, 17, 20, 25], "long_nam": 14, "longer": [2, 3, 4], "longitud": [0, 17], "look": [0, 2, 4, 6, 11, 12, 13, 15, 16, 17, 24], "lookup": 7, "loop": [5, 7, 11, 24], "loosevers": 7, "lot": [4, 17, 24, 25], "lousyeo": 25, "low": [4, 17], "low_resolution_band": 0, "lower": [0, 2, 6, 15], "lowercas": 0, "lps22": 20, "lt": [2, 12, 23], "lte": [2, 23], "luckili": [3, 24], "m": [2, 19], "machin": [0, 2, 3, 7, 20, 25], "magic": 19, "mai": [0, 2, 4, 5, 7, 9, 11, 13, 17, 24, 25], "main": 0, "mainli": [0, 5, 15, 24, 25], "maintain": [8, 19, 25], "mainten": [7, 20, 21, 25], "major": [0, 18, 25], "make": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 19, 21, 22, 25], "makefil": 19, "mam": 2, "manag": [0, 4, 5, 7, 8, 10, 20, 24], "mani": [0, 2, 4, 7, 11, 17], "manipul": [0, 2, 25], "manner": [3, 17, 25], "manual": [3, 5, 7, 8, 10, 19, 25], "map": [0, 2, 7, 10, 11, 18, 19, 20, 24, 25, 26], "march": [0, 2, 17], "mark": [7, 19], "mask": [0, 2, 7, 17, 18, 20, 23, 24], "mask_polygon": [0, 2, 7, 23], "mask_resampl": 4, "mask_valu": [0, 2], "masked_s2": 18, "massag": 4, "master": [17, 19], "match": [0, 2, 11, 17, 25], "math": [7, 20, 24, 25, 26], "mathemat": [4, 24, 26], "matplotlib": [0, 21], "max": [0, 2, 4, 7, 11, 20, 23, 24, 25], "max_cloud_cov": [0, 7, 17], "max_poll_interv": 0, "max_poll_tim": [0, 7], "max_tim": [0, 4, 20, 23], "max_vari": 0, "maxima": 2, "maximum": [0, 2, 4, 7, 11, 17, 20, 24], "md": 19, "mean": [0, 1, 2, 3, 4, 11, 17, 18, 22, 23, 24, 25, 26], "mean_tim": [0, 23], "meant": [0, 2], "meanwhil": 3, "measur": [2, 3], "mechan": 26, "med": 2, "media": 0, "median": [0, 2, 4, 12, 23, 24], "median_tim": [0, 23], "medium": 0, "memori": [0, 7], "mention": [4, 18], "merg": [0, 2, 6, 7, 11, 18, 19], "merge_cub": [0, 2, 7, 23], "messag": [0, 2, 3, 5, 7, 11, 19, 21, 25], "meta": [19, 25, 26], "metadaa": 0, "metadata": [2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 20, 26], "metadata_from_stac": 7, "meter": [0, 2, 4], "method": [0, 1, 2, 4, 5, 6, 7, 9, 11, 13, 15, 16, 17, 18, 20, 22, 23, 25, 26], "metric": 0, "micromet": 2, "microsecond": 7, "microsoft": 3, "midnight": [2, 17], "might": [0, 2, 3, 6, 14, 17, 19, 21, 25, 26], "migrat": 7, "millisecond": 2, "min": [0, 2, 23, 25], "min_tim": [0, 23], "mind": 4, "minim": [6, 7, 26], "minima": 2, "minimum": [0, 2, 7, 13, 17], "minor": [7, 19], "minu": 2, "minuend": 2, "minut": [2, 3, 4, 5, 7], "mirror": [0, 2], "miscellan": [10, 20], "mislead": 0, "mismatch": 7, "miss": [2, 7, 26], "mistak": 25, "mistakenli": [7, 24], "mix": [1, 2, 7, 18, 24], "mixin": 0, "mjjaso": 2, "ml": [0, 2, 22], "mlmodel": [7, 20, 22, 23], "mm": [2, 17], "mobil": 3, "mod": [2, 23], "mode": [0, 2, 7, 8, 19, 21], "model": [0, 2, 7, 13, 22, 25], "modif": 2, "modifi": [0, 2, 12], "modu": 25, "modul": [0, 2, 6, 7, 20, 21, 24, 26], "modulenotfounderror": 21, "modulo": 2, "moment": [8, 19], "monitor": [4, 5, 6], "montero": 14, "month": [0, 2, 7, 12], "monthli": [2, 3], "more": [0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 24, 25], "moreov": [5, 18, 24, 25], "mortem": 7, "most": [2, 3, 4, 5, 9, 11, 12, 13, 14, 17, 18, 19, 24, 25, 26], "mostli": [0, 3, 8], "mother": 6, "move": [0, 7], "msphinx": 19, "much": [1, 2, 3, 4, 13, 17, 20, 25, 26], "multi": [0, 2, 10, 18, 20, 26], "multi_result": [0, 18], "multibackendjobmanag": [7, 10, 11, 21], "multilevel": 0, "multipl": [0, 2, 3, 5, 6, 7, 11, 13, 14, 17, 20, 24, 25], "multipli": [0, 2, 18, 23], "multiplicand": 2, "multiply1": 4, "multiply3": 4, "multipoint": 2, "multipolygon": [0, 2], "multiresult": [7, 18, 20], "must": [0, 2, 8, 18, 25], "my": [0, 3, 15, 25], "my_bbox": 0, "my_process": [11, 25], "my_reduc": [1, 2], "my_udf": 25, "my_udp": 18, "myclient": 0, "n": [0, 2, 5, 7, 12, 14, 19], "nadir": 2, "naiv": 15, "name": [0, 2, 3, 5, 7, 11, 12, 14, 17, 18, 19, 20, 21, 24, 26], "namespac": [0, 7, 10, 11], "nan": [2, 23], "nativ": [2, 17], "natur": [2, 25], "nc": [12, 25], "ndarrai": [0, 12], "ndgi": 7, "ndim": 25, "ndjfma": 2, "ndmi": [7, 14], "ndvi": [0, 2, 4, 5, 7, 12, 14, 20, 23, 24], "ndvi_10m": 20, "ndvi_median": 12, "ndwi": 4, "nearest": [0, 2], "necessari": [0, 3, 5, 7, 14, 18, 19, 21, 22, 25], "necessarili": [0, 13, 25], "need": [0, 2, 3, 4, 5, 9, 11, 17, 19, 21, 25, 26], "neg": 2, "neighbor": [0, 2], "neighborhood": [2, 25], "neighbour": 2, "neighbourhood": 0, "nempti": 0, "neq": [2, 7, 23], "nest": [0, 2, 7], "net": 3, "netcdf": [0, 4, 7, 12, 13, 17, 21, 25], "netcdf4": 21, "network": [0, 3], "networkx": 12, "never": [0, 1, 2, 3, 25], "new": [0, 1, 2, 3, 4, 7, 11, 12, 14, 16, 19, 24, 25], "new_metadata": 25, "newli": [0, 2, 5, 19], "newlin": 0, "next": [0, 2, 3, 4, 7, 13, 17, 25], "nfor": 0, "nice": [3, 4, 12, 17, 19], "nicer": 7, "nir": [0, 2, 4, 12, 14, 18, 26], "nnnnnn": [0, 2], "nodata": [0, 2, 12], "nodataavail": 0, "node": [0, 7, 16, 20, 24, 25, 26], "nois": [0, 2], "noise_remov": [0, 2], "nomin": 0, "non": [0, 2, 4, 7, 14, 19, 20, 24], "none": [0, 4, 7, 11, 14, 19, 25, 26], "nor": 7, "normal": [0, 1, 2, 3, 4, 7, 8, 9, 14, 17, 19, 24], "normalize_cr": 0, "normalize_log_level": 0, "normalized_differ": [0, 2, 23], "north": [0, 4, 6, 7, 9, 12, 17, 20, 24, 25, 26], "not_": [2, 23], "notabl": [7, 18], "notat": [0, 7, 24], "note": [0, 1, 2, 3, 4, 5, 8, 9, 11, 13, 16, 17, 18, 19, 24, 25, 26], "notebook": [4, 5, 6, 7, 12, 17, 21, 25], "noth": 0, "notic": 17, "notori": 6, "novemb": [0, 2], "now": [2, 3, 4, 5, 7, 11, 12, 18, 19, 22, 24, 25, 26], "nowadai": 6, "np": 25, "nrb": 2, "nset": 0, "null": [0, 2, 7, 26], "num": 25, "num_tre": 0, "number": [0, 2, 4, 9, 11, 13, 16, 17, 18, 20, 21, 24, 25, 26], "numer": [0, 2], "numpi": [12, 24, 25], "o": [7, 17], "oauth": 3, "obfuscate_auth": 0, "object": [0, 2, 3, 4, 7, 11, 12, 17, 18, 20, 24, 25, 26], "observ": [0, 3, 4, 14, 17, 20, 22, 25], "obtain": [0, 2, 3, 4, 7, 19], "obvious": 14, "occasion": [3, 24], "occlus": 2, "occur": [0, 2], "octob": [0, 2], "off": [2, 8], "offer": [4, 6, 9, 17, 24], "offici": [2, 4, 7, 19, 20, 24, 26], "offlin": 25, "offset": 0, "often": [0, 2, 3, 13, 19, 24, 25, 26], "ogc": [0, 2], "ogr": 2, "oidc": [0, 4, 7, 8, 20], "oidc_auth_renew": 0, "oidc_auth_user_id_token_as_bear": 7, "oidcauthent": 0, "oidcbearerauth": 3, "oidcdevicecodepolltimeout": 7, "oidcexcept": 7, "oidcprovid": 3, "old": [0, 2, 7, 25], "older": [0, 25], "olivierhagol": 9, "omit": [0, 11, 25], "on_job_cancel": 11, "on_job_don": [7, 11], "on_job_error": 11, "onc": [2, 3, 4, 5, 17, 19, 25], "one": [0, 2, 3, 4, 6, 7, 9, 11, 12, 13, 18, 19, 21, 24, 25], "onelin": 19, "ones": [0, 2, 24], "onli": [0, 1, 2, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 24, 25, 26], "onlin": 24, "onto": 2, "op": 7, "open": [0, 2, 4, 6, 7, 12, 15, 16, 17, 19, 21, 26], "openeo": [1, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 18, 19, 21, 22, 24, 26], "openeo_auth_client_id": [0, 3], "openeo_auth_client_secret": [0, 3], "openeo_auth_method": [0, 3], "openeo_auth_provider_id": [0, 3, 7], "openeo_basemap_attribut": 7, "openeo_basemap_url": 7, "openeo_client_config": 8, "openeo_config_hom": 8, "openeo_processes_dask": 7, "openeo_udf": 7, "openeoapierror": 7, "openeoapiplainerror": 7, "openeopycli": 19, "opengeospati": 2, "openid": [0, 4, 7, 8, 20], "oper": [0, 1, 2, 3, 4, 7, 9, 13, 21, 24, 25, 26], "operand": 2, "operandi": 25, "opinion": [0, 6], "opposit": 0, "optic": [0, 2, 9, 17], "optim": [0, 2, 13, 25], "option": [0, 2, 4, 5, 7, 9, 11, 14, 18, 19, 20, 24, 25], "or_": [2, 23], "orbit": 17, "order": [0, 2, 4, 5, 7, 8, 23, 25], "orfeo": 9, "org": [0, 2, 5, 9, 17, 19], "organ": [3, 21], "organis": [3, 4], "orient": [6, 24], "origin": [0, 2, 5, 7, 14, 19, 22, 24, 25], "orthorectifi": [0, 7], "oschmod": 7, "other": [0, 2, 3, 4, 5, 7, 9, 11, 15, 16, 19, 21, 22, 25, 26], "otherwis": [0, 2, 3, 4, 5, 11], "our": [4, 12, 25, 26], "out": [0, 3, 4, 5, 6, 7, 8, 11, 17, 21, 24, 25, 26], "out_format": [0, 5, 7, 13], "outdat": 7, "outer": 2, "output": [0, 2, 4, 7, 13, 14, 15, 17, 18, 19, 24, 25], "output_cub": 25, "output_fil": 11, "output_max": [0, 7], "output_min": [0, 7], "output_rang": 14, "outputfil": 0, "outputmax": [0, 2], "outputmin": [0, 2], "outsid": [0, 2, 3], "outward": 2, "over": [0, 2, 4, 5, 6, 7, 25], "overal": [2, 9], "overhead": [7, 25], "overlap": [0, 2, 17, 25], "overlap_resolv": [0, 2], "overli": 2, "overrid": [0, 11], "overridden": 11, "overrul": 0, "oversampl": 0, "overview": [5, 19], "overwrit": [0, 6], "own": [5, 6, 11, 25, 26], "owner": 0, "ozon": 2, "p": [0, 2, 25], "p1": 18, "packag": [7, 11, 19, 21, 25], "pad": [0, 2], "page": [0, 3, 4, 7, 19, 20, 21], "pagin": 7, "pair": [0, 2, 17], "panda": [0, 4, 11, 25], "pansharpen": 0, "parallel": 11, "parallel_job": 11, "paramet": [0, 2, 7, 9, 11, 14, 16, 18, 20, 24, 25], "parameter": [0, 7, 10, 24], "parameter_column_map": 11, "parameter_default": 11, "parametr": [2, 9], "parcel": [13, 17], "parent": [0, 2, 24], "parenthesi": 6, "parquet": [7, 11, 17, 21], "parquetjobdatabas": [7, 10, 11], "pars": [0, 7, 12, 25], "parse_d": 7, "parse_date_or_datetim": 7, "parse_datetim": 7, "parser": 12, "part": [0, 2, 3, 4, 7, 19, 24, 25], "parti": 12, "partial": [0, 7, 11], "particular": [0, 3, 17, 19, 25], "pass": [0, 2, 3, 7, 11, 15, 17, 18, 19, 22, 25, 26], "passphras": 0, "password": [0, 3], "past": [3, 19, 25], "path": [0, 2, 3, 7, 8, 11, 15, 16, 18, 19, 25], "pathlib": [0, 15, 25], "pattern": [0, 2, 4, 6, 7], "payload": 17, "pd": [4, 11], "peek": 4, "penalti": 25, "peopl": [3, 6, 22], "pep": [0, 7, 25], "pep8": 6, "per": [0, 2, 11, 13, 25], "percentag": 0, "perform": [0, 2, 4, 9, 10, 12, 17, 24, 25], "period": [0, 2, 3, 4, 5], "perm": 3, "permiss": [3, 4, 7, 19], "permissionerror": 7, "permut": 2, "persist": [0, 7, 11, 18], "person": [3, 6, 19], "pg": [0, 12, 26], "pgnode": [0, 2, 7], "pgnodegraphunflatten": 7, "phenologi": 0, "phone": 3, "physic": [2, 4, 20, 25], "pi": [2, 23], "pick": [3, 6, 11], "piggyback": 19, "pip": [12, 19, 25], "pipelin": [4, 24], "pipx": 19, "pitfal": 17, "pixel": [0, 1, 2, 4, 13, 20, 22, 24], "pkce": [0, 7], "place": [0, 4, 6, 8, 19, 24, 25], "placehold": [0, 1, 2, 19], "plai": [0, 3, 6, 18, 24, 25], "plain": 7, "plan": [0, 7, 19], "platform": [0, 7, 12, 14, 19], "pleas": [0, 2, 12, 25, 26], "plenti": 6, "plot": [0, 7, 12, 21], "plu": 0, "plugin": 19, "plural": 5, "point": [0, 2, 3, 4, 5, 13, 17, 18, 22, 26], "pointer": 25, "polici": 17, "poll": [0, 3, 5, 7, 11], "poll_sleep": 11, "pollut": 21, "polygon": [0, 2, 4, 5, 7, 13, 17, 26], "polygonal_histogram_timeseri": [0, 7], "polygonal_mean_timeseri": [0, 7], "polygonal_median_timeseri": [0, 7], "polygonal_standarddeviation_timeseri": [0, 7], "popular": 6, "portabl": [0, 2], "posit": [0, 2, 7], "possibl": [0, 4, 5, 8, 9, 12, 13, 14, 16, 17, 18, 21, 24, 25, 26], "possibli": [0, 7], "post": [0, 5, 7, 19], "postprocess": 25, "potenti": 0, "power": [0, 2, 23, 24], "pq": 17, "pr": 19, "practic": [0, 7, 11, 20, 25], "pre": [0, 2, 7, 17, 20, 25, 26], "preced": 25, "precis": [1, 2], "predefin": [0, 7, 24], "predic": [0, 2], "predict": [0, 2, 22, 25], "predict_": 0, "predict_curv": [0, 2, 7, 23], "predict_random_forest": [0, 2, 7, 22, 23], "predicted_arrai": 25, "predicted_cub": 25, "predictor": [0, 22], "prefer": [5, 19, 24, 25, 26], "prefix": [0, 25], "prepar": [19, 25, 26], "prepend": 25, "preprocess": [0, 4, 9, 17], "prescrib": 24, "present": [2, 17, 25], "preserv": [0, 2, 7, 25], "press": 3, "pretti": 5, "prevent": [0, 7], "preview": [0, 7, 21], "previou": [2, 4, 12, 17, 25], "previous": [0, 4, 6], "primari": 25, "primit": 26, "principl": [2, 6, 17], "print": [0, 3, 4, 7, 8, 15, 16, 19, 21, 24, 26], "print_json": [0, 7, 15, 26], "print_stat": 25, "prior": 25, "prioriti": [0, 2], "privaci": 3, "privat": [3, 16], "privatejsonfil": 7, "probabl": [0, 2, 4], "probe": 8, "problem": [2, 17, 19, 24, 25], "procedur": [0, 3, 24], "process": [1, 4, 5, 7, 9, 10, 13, 17, 19, 20, 22], "process_graph": [0, 4, 15, 18, 26], "process_id": [0, 4, 11, 15, 16, 18, 24, 26], "process_map": 19, "process_with_nod": [0, 7], "processbasedjobcr": [7, 10], "processbuild": [0, 1, 7, 20, 23, 24], "processbuilderbas": 0, "processes_dict": 0, "processgraphunflatten": 7, "processgraphvisitexcept": [7, 18], "processgraphvisitor": 7, "produc": [0, 11, 18, 25], "product": [0, 2, 9, 17, 23], "product_uri": 12, "profil": [3, 6, 7, 20], "profile_dump": 25, "program": [2, 6, 24], "programmat": [4, 5, 17], "progress": [0, 4, 5, 7], "proj": 0, "project": [0, 2, 6, 7, 12, 13, 14, 17, 19, 21, 26], "prolept": [0, 2], "propag": 0, "proper": [7, 25], "properli": [0, 3, 4, 5, 7, 14, 17, 19, 21, 24, 26], "properti": [0, 2, 4, 5, 7, 12, 13, 16, 20, 22, 25, 26], "propos": [2, 7, 19], "proprietari": [0, 2], "protect": 3, "protocol": [3, 7], "provid": [0, 2, 3, 4, 5, 7, 9, 11, 12, 14, 16, 17, 18, 20, 22, 24, 25, 26], "provider_id": [0, 3], "pry": 3, "pseudocod": 24, "pstat": 25, "public": [10, 12, 13, 17], "publicli": [3, 4, 10, 26], "publish": [10, 18, 19], "pull": 0, "pure": [19, 21], "pureposixpath": 0, "purpos": [0, 2, 12, 17, 25], "push": 19, "put": [0, 2, 3, 4, 5, 6, 25], "px": 25, "py": [0, 7, 12, 19, 25], "py3": [19, 25], "pyarrow": [11, 21], "pypi": [19, 21], "pyprof2calltre": 25, "pyproj": [0, 7], "pystac": 7, "pytest": [0, 19], "python": [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 15, 16, 17, 18, 19, 21, 22, 23, 24, 26], "q": 2, "q1": 2, "q3": 2, "q7znsy": 3, "quadrat": 2, "quantil": [2, 23], "quartil": 2, "queri": [4, 17, 24], "question": 17, "queu": [0, 5], "quick": [4, 6, 25], "quit": 3, "quot": [15, 25], "r": [2, 14, 25, 26], "r8dh": 22, "radar": [0, 2, 9], "radian": 2, "radiometr": [0, 2], "rainbow": 0, "rais": [0, 2, 3, 5, 7, 11, 24, 25], "random": [0, 2, 20], "rang": [0, 2, 4, 14, 17, 25, 26], "rare": [0, 19], "raster": [0, 2, 5, 12, 13, 22, 24, 25], "raster_cub": [0, 26], "raster_to_vector": 0, "rasterspec": 12, "rather": [13, 25], "raw": [0, 4, 7, 9, 10, 13, 16, 17, 18, 22, 25, 26], "raw_json": 18, "rc1": 7, "rdd": 25, "rdd_": 25, "rdylbu_r": 0, "re": [0, 2, 3, 7, 19, 21, 24, 26], "reach": [2, 5, 21], "read": [0, 2, 6, 11, 12, 17, 21], "read_text": 25, "readabl": [0, 6, 7, 25], "reader": 6, "readi": [10, 20], "readili": [18, 25], "real": [4, 18], "realiti": 24, "realm": 4, "rearrang": [2, 23], "reason": [0, 3, 6, 17, 24, 25], "rebuild": 19, "receiv": [0, 2, 3, 5, 24, 25], "recent": [19, 25], "recip": 19, "recogn": 14, "recommend": [0, 2, 3, 4, 5, 7, 13, 17, 19, 20, 21, 25], "reconnect": 4, "rectangular": 2, "red": [0, 2, 4, 12, 14, 18, 26], "redact": 3, "redirect": 7, "reduc": [0, 1, 2, 3, 4, 7, 12, 17, 22, 24, 26], "reduce_band": 0, "reduce_bands_udf": 0, "reduce_dimens": [0, 1, 2, 7, 12, 20, 22, 23, 24], "reduce_spati": [0, 2, 7, 23], "reduce_tempor": [0, 7], "reduce_temporal_simpl": [0, 7], "reduce_temporal_udf": 0, "reduce_tiles_over_tim": 0, "reduct": [0, 2], "ref": 25, "refer": [0, 2, 4, 5, 6, 7, 10, 11, 17, 22, 24, 25, 26], "referenc": [0, 7], "reference_system": 25, "reflect": [0, 2, 4, 7, 25], "reflect_pixel": [0, 2], "refresh": [0, 7, 19, 20], "refresh_token": [0, 3], "refresh_token_stor": 0, "refreshtokenstor": [0, 3, 7], "regard": [2, 6, 7], "regardless": 0, "region": [4, 5, 17], "regist": [3, 11], "registr": 3, "registri": 2, "regress": [0, 2, 20], "regro": 19, "regular": [2, 3, 17, 24, 25, 26], "regularli": 11, "reinstat": 7, "reject": 2, "rel": [0, 3, 13, 16, 17], "relat": [0, 2, 3, 4, 7, 19, 21], "relativeorbitnumb": 17, "releas": [20, 21], "relev": [3, 5, 11], "reli": [4, 12], "reliabl": [5, 25], "reload": 19, "remain": [0, 2, 12, 25], "remaind": 2, "remark": 0, "rememb": [11, 13], "remot": [0, 2, 4, 7, 11, 12, 20], "remotesens": 9, "remov": [0, 2, 3, 19, 25], "remove_servic": [0, 7], "renam": [0, 2, 7, 14, 19], "rename_dimens": [0, 2, 23], "rename_label": [0, 2, 7, 23, 25], "render": [4, 7, 12, 15, 17], "renew": [0, 7], "reoccur": 26, "repeat": [0, 2, 25], "repeatedli": 2, "replac": [0, 2, 7, 11, 19], "replace_invalid": [0, 2], "replic": [0, 2], "repo": 19, "report": [0, 2, 4, 19, 26], "repositori": [2, 12, 19], "repr": [7, 15], "repres": [0, 17, 24, 25, 26], "represent": [0, 2, 4, 7, 12, 15, 16, 18, 26], "reproduc": [0, 2, 17, 25], "reproject": 2, "request": [0, 2, 3, 4, 5, 7, 17, 25], "requir": [0, 2, 3, 4, 7, 9, 12, 13, 17, 19, 24, 25, 26], "res001": 5, "res002": 5, "resampl": [0, 2, 4], "resample_cube_spati": [0, 2, 4, 23], "resample_cube_tempor": [0, 2, 7, 23], "resample_spati": [0, 2, 23], "rescal": [4, 14, 20], "rescaled_cub": 25, "research": [2, 3], "resili": 7, "resolut": [0, 2, 7, 12], "resolution_merg": [0, 7, 23], "resolv": [0, 2, 11], "resolve_from_nod": 7, "resourc": [0, 2, 4, 5, 18, 24], "respect": [0, 2, 3, 26], "respond": 14, "respons": [0, 5, 7, 17], "responsibli": 3, "rest": [2, 3, 4, 7, 11, 13, 20, 24, 25, 26], "restart": [11, 21], "restat": 25, "restcap": 0, "restfil": 7, "restjob": [0, 5, 7], "restor": 19, "restrict": [0, 2, 13], "restuserdefinedprocess": [0, 16], "result": [2, 3, 4, 7, 9, 11, 12, 13, 15, 17, 19, 20, 21, 22, 25, 26], "result_ndvi": 12, "result_nod": [0, 7, 24], "resultasset": [0, 5, 7], "resum": 11, "retain": [0, 17, 25], "retent": 17, "retriev": [0, 2, 7, 11, 17, 24, 25], "return": [0, 1, 2, 3, 5, 7, 11, 12, 14, 24, 25], "return_nodata": 2, "reus": [0, 3, 4, 15, 20], "reusabl": [7, 11, 18, 24, 25, 26], "reveal": 25, "revers": [0, 2, 7], "revert": 2, "review": 19, "rework": 7, "rfc": [0, 2, 17], "rfc3339": [0, 2, 7], "rgb": [0, 9], "rich": [0, 7], "right": [0, 2, 3, 5, 17], "rioxarrai": 21, "risk": [3, 6], "rm": [2, 19], "robust": 7, "role": [0, 19, 25], "root": [0, 2, 11, 19, 21], "root_dir": 11, "roughli": [15, 18, 19], "round": [2, 23], "row": [0, 11, 22], "rst": 19, "rtc": [0, 7], "rtol": 0, "rtype": 0, "rudimentari": 25, "rule": [6, 19, 25], "run": [0, 2, 4, 7, 9, 11, 12, 13, 18, 20, 21, 24, 25], "run_cod": [0, 7, 25], "run_job": [7, 11], "run_synchron": [0, 7], "run_udf": [0, 2, 7, 23, 25], "run_udf_cod": 7, "run_udf_extern": [2, 23], "runtim": [0, 2, 7, 25], "runtimeerror": 24, "rxpk": 24, "s1": [12, 14], "s1grd": 9, "s2": [12, 14], "s2_band": 13, "s2_cube": [12, 25], "s2_datacub": 12, "s2_fapar": 6, "s2_l2a_sampl": 12, "s2_scl": 4, "s2b_32tpr_20190102_": 12, "s2b_msil2a_20190102": 12, "s2wi": 7, "safe": 3, "sai": [3, 24, 26], "same": [0, 2, 3, 4, 5, 7, 12, 17, 19, 21, 22, 24, 25, 26], "sampl": [0, 2, 4, 10, 12, 17, 20, 22, 25], "sample_by_featur": 13, "sample_geotiff": 12, "sample_netcdf": 12, "sandbox": 19, "sar": [0, 2, 10, 17], "sar_backscatt": [0, 2, 7, 9, 23], "satellit": [4, 7, 14], "satur": 2, "saturation_": 2, "save": [0, 2, 3, 7, 11, 15, 16, 19, 22], "save_ml_model": [0, 7, 22, 23], "save_result": [0, 2, 5, 7, 23], "save_to_fil": 0, "save_user_defined_process": [0, 7, 16, 26], "savgol_filt": 25, "savitzki": 25, "scalabl": 10, "scalar": 2, "scale": [0, 2, 10, 24, 25], "scenario": 9, "scene": 4, "schema": [0, 7, 11, 18], "scheme": 3, "scipi": [24, 25], "scl": [4, 12], "scl_band": 4, "scope": [3, 7, 17, 21], "screen": 6, "script": [0, 2, 3, 4, 5, 6, 17, 19, 21, 26], "scroll": 6, "sd": [0, 2, 4, 23], "sdw": 0, "search": [2, 12, 20, 25], "season": [0, 2], "second": [0, 2, 4, 5, 7, 11, 24], "secondari": [0, 7], "secondli": 6, "secret": [0, 3, 7, 8], "section": [3, 4, 8, 9, 17, 19, 25], "secur": [3, 4], "see": [0, 2, 3, 4, 5, 8, 11, 12, 14, 16, 17, 24, 25, 26], "seed": [0, 2], "seem": 6, "segment": [0, 2, 19], "select": [0, 2, 4, 9, 19, 20, 26], "self": [0, 4], "semant": [7, 19], "semi": 19, "sen2cor": 4, "send": [0, 2, 4, 5, 7], "send_job": [0, 7], "sens": [8, 17, 20, 24], "sensit": [2, 3], "sensor": [0, 2, 9], "sent": 18, "sentinel": [0, 4, 9, 12, 13, 17], "sentinel1": 7, "sentinel1_grd": [4, 9, 17], "sentinel2": [7, 13, 14, 22, 25], "sentinel2_cub": [4, 26], "sentinel2_l1c_sentinelhub": 9, "sentinel2_l2a": [4, 13, 14, 17, 25, 26], "sentinel2_l2a_sentinelhub": 26, "sentinel2_toc": 18, "sentinelhub": 9, "separ": [0, 2, 3, 4, 5, 7, 9, 11, 13, 17, 19, 24, 25], "septemb": [0, 2], "seq": 0, "sequenc": [0, 2], "seri": [0, 5, 11, 25], "serial": 0, "server": [0, 2, 19, 20], "server_address": [0, 3], "servic": [0, 2, 3, 7, 17, 26], "service_id": 0, "session": [0, 4, 7, 15, 25], "set": [0, 2, 3, 4, 5, 7, 8, 11, 13, 16, 20, 22, 24, 25], "set_datacube_list": 0, "set_structured_data_list": 0, "settingwithcopywarn": 7, "settl": 6, "setup": [19, 21], "setuptool": 19, "sever": [0, 2, 24, 25], "sgn": [2, 23], "sha1": 25, "shadow": [0, 2, 7], "shape": [0, 2, 4, 7, 12, 19, 25], "sharabl": 16, "share": [3, 7, 10, 17, 20], "shell": [3, 21], "short": [0, 2, 3, 4, 25], "shortcut": [0, 2, 7, 17], "shorthand": [0, 7], "should": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 17, 18, 19, 21, 24, 25, 26], "show": [0, 3, 4, 5, 7, 9, 17, 19], "show_axeslabel": 0, "show_bandnam": 0, "show_dat": 0, "shown": [3, 5, 7, 19, 25], "shrink": 2, "side": [0, 1, 2, 4, 5, 7, 10, 16, 20], "sight": [0, 2], "sigma0": [0, 2, 9], "sign": [0, 2], "signal": 25, "signatur": [2, 7, 20, 24], "signific": 9, "signum": 2, "silent": 6, "similar": [12, 24], "simpl": [0, 1, 2, 3, 4, 5, 8, 12, 14, 17, 18, 19, 20, 24, 25, 26], "simpler": 2, "simplest": [19, 25], "simpli": [0, 2, 9, 17], "simplic": 22, "simplifi": [0, 4, 7, 14, 26], "sin": [2, 23], "sinc": [0, 2, 3, 4, 5, 13, 17], "sine": 2, "singl": [0, 2, 4, 7, 11, 12, 13, 14, 15, 16, 18, 24, 25, 26], "singular": 5, "sinh": [2, 23], "site": 0, "situat": [3, 5, 24, 26], "six": [0, 2], "sixth": 2, "size": [0, 2, 3, 13, 25, 26], "size_param": 26, "skip": [0, 5, 7, 17, 19, 25], "skip_verif": 0, "sleep": [0, 11], "slice": [4, 24, 25], "slide": [0, 25], "slightli": 3, "slow": [0, 19], "slow_response_threshold": 0, "slower": 7, "sluo": 4, "smac": 9, "small": [0, 4, 6, 13, 17, 25], "smaller": [13, 25], "smallest": 2, "smooth": 20, "smooth_savitzky_golai": 25, "smoothed_arrai": 25, "smoothed_evi": 25, "smoother": 25, "smoothing_udf": 25, "snake": 0, "snippet": [0, 1, 2, 3, 4, 11, 12, 17, 25], "snow": 2, "so": [0, 2, 3, 4, 5, 9, 13, 16, 19, 21, 22, 24, 25, 26], "soft": [0, 7], "soft_error_max": 0, "softwar": 6, "solut": [3, 17, 19], "solv": [7, 24], "some": [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 14, 16, 17, 19, 21, 24, 25, 26], "somehow": 25, "someon": 6, "someth": [0, 2, 18, 21, 25], "sometim": [19, 25, 26], "somewhat": 22, "son": 2, "sort": [0, 2, 23], "sourc": [0, 2, 3, 6, 7, 11, 14, 19, 20, 24, 25], "south": [0, 4, 6, 7, 9, 12, 17, 20, 24, 25, 26], "space": [6, 9, 17, 25], "span": 2, "spark": 25, "spars": 13, "spatial": [0, 2, 4, 7, 12, 13, 20, 25, 26], "spatial_ext": [0, 2, 4, 6, 7, 9, 12, 17, 20, 22, 25, 26], "spatialdimens": 0, "spatio": [4, 5, 17], "spatiotempor": [0, 25], "speak": [0, 26], "spec": [7, 12, 19, 26], "special": [0, 7, 9, 12, 24, 26], "specif": [0, 2, 3, 4, 7, 9, 12, 13, 17, 19, 24, 25, 26], "specifi": [0, 2, 3, 4, 5, 7, 9, 11, 14, 17, 18, 22, 24, 25, 26], "spectral": [0, 2, 4, 7, 10, 20, 26], "spectral_indic": [7, 14], "spent": 0, "sphinx": 19, "spline": 2, "split": [0, 2, 6, 25], "sqrt": [2, 23, 24], "squar": [0, 2], "sr": 0, "src": 0, "srr3": 20, "srr5": 20, "srr6": 20, "stabl": 2, "stac": [0, 2, 5, 7, 10, 17, 22], "stac_vers": 4, "stack": [0, 12, 25], "stackstac": 12, "stage": [17, 19], "stai": 2, "standalon": 25, "standard": [0, 2, 6, 7, 14, 15, 16, 17, 19, 24], "star": 6, "start": [0, 1, 2, 3, 7, 11, 12, 18, 19, 20, 21, 22, 24, 25, 26], "start_and_wait": [0, 5, 7, 22], "start_dat": [0, 11, 17, 24], "start_job": [0, 7, 11], "start_job_thread": 11, "startswith": 5, "stat": [5, 7, 11, 25], "state": [2, 11], "statement": [1, 2, 4, 5, 6, 24], "static": [0, 2], "statist": [0, 2, 17, 20, 24, 25], "statu": [0, 4, 5, 7, 11, 16], "status": 11, "stdout": 0, "step": [0, 3, 4, 5, 9, 12, 17, 18, 19, 25], "stick": 0, "still": [0, 2, 3, 5, 6, 7, 11, 12, 13, 17, 21, 25], "stolen": 3, "stop": [0, 3, 4, 5, 7, 11, 25], "stop_job": 0, "stop_job_thread": 11, "storag": [0, 3, 7], "store": [0, 2, 3, 5, 7, 8, 11, 13, 16, 18, 20, 24], "store_refresh_token": [0, 3], "str": [0, 11, 14], "straightforward": [3, 5, 18, 24, 26], "strang": 6, "strategi": 0, "stream": 0, "streamlin": [4, 6, 19], "stretch_color": 7, "strict": [6, 7], "strictli": 2, "string": [0, 2, 4, 7, 15, 16, 18, 19, 22, 25, 26], "strip": 0, "strong": 6, "strongli": 6, "structur": [0, 2, 4, 7, 12, 18, 22, 24, 25], "structured_data": 0, "structured_data_list": 0, "structureddata": 0, "stuck": 25, "style": [0, 4, 7, 8, 15, 19, 20, 24, 26], "sub": [0, 2, 7, 18, 19, 24, 26], "subclass": [0, 7], "subcommand": 3, "subfold": 11, "subject": [0, 4, 8, 11, 12, 14, 15, 16, 22, 25], "submit": [0, 5, 26], "submodul": [7, 19], "subpackag": [7, 14], "subprocess": 0, "subrepo": 19, "subsect": 19, "subsequ": [0, 2, 3, 4], "subset": [0, 2, 19, 24], "subshel": 19, "substitut": 18, "subtract": [0, 2, 16, 18, 23, 24, 26], "subtract1": [4, 26], "subtract32": 26, "subtrahend": 2, "subtyp": [0, 7, 11, 26], "success": [0, 3, 8], "successfulli": [2, 3, 5, 7, 19, 22, 26], "suffici": [9, 25], "suffix": [7, 19], "sugar": [1, 2, 4, 11], "suggest": 11, "suit": [3, 19], "suitabl": [0, 2, 9, 17, 25], "sum": [2, 7, 18, 23, 24, 25], "summand": 2, "summari": [0, 7], "sun": [2, 9], "sunazimuthangl": 9, "sunzenithangl": 9, "superclass": 7, "support": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 14, 16, 17, 18, 20, 21, 24, 25, 26], "sure": [3, 4, 5, 7, 9, 17, 19, 21, 25], "surfac": [0, 2], "surpris": 19, "suspect": 3, "swir": 14, "switch": [3, 5], "sy": [0, 25], "symbol": [0, 24, 26], "synchron": [0, 5, 7, 13, 18, 20, 25, 26], "syntact": [1, 2, 4, 11], "syntax": [0, 7, 25, 26], "synthet": 9, "system": [0, 2, 3, 5, 7, 17, 19, 21], "systemat": 0, "t": [0, 1, 2, 3, 4, 5, 7, 8, 12, 14, 17, 21, 22, 24, 25, 26], "tab": [5, 7], "tabl": [0, 23], "tabular": 13, "tag": [19, 25], "take": [0, 1, 2, 3, 4, 5, 7, 11, 18, 20, 24, 26], "taken": [0, 2], "tan": [2, 23], "tangent": 2, "tanh": [2, 23], "tar": 25, "target": [0, 2, 22, 25], "target_band": [0, 2, 7], "target_dimens": [0, 2, 7], "targetdimensionexist": 2, "task": [4, 24], "tast": 6, "technic": [3, 4, 6, 17, 22], "tediou": 19, "temp": 0, "templat": [7, 11], "tempor": [0, 1, 2, 4, 5, 7, 11, 12, 20, 22, 24, 25, 26], "temporal_ext": [0, 2, 4, 6, 7, 9, 11, 12, 13, 17, 20, 22, 25, 26], "temporal_interv": [0, 7, 26], "temporaldimens": 0, "temporalextentempti": [0, 2], "temporari": [0, 7, 19], "temporarili": 19, "ten": [0, 2], "term": [3, 14, 22], "terminologi": 20, "terrain": [0, 2, 9], "terrascop": [4, 16], "terrascope_s2_fapar_v2": 6, "terrascope_s2_ndvi_v2": 20, "test": [2, 7, 9, 11, 12, 17, 20, 21, 25], "test_10": 13, "test_data": 0, "test_input": 25, "testdata": 13, "testdataload": 0, "text": [0, 2], "text_begin": [2, 23], "text_concat": [2, 23], "text_contain": [2, 23], "text_end": [2, 23], "than": [0, 2, 3, 4, 7, 12, 13, 18, 24, 25], "thei": [0, 2, 5, 7, 15, 18, 24, 25], "them": [0, 2, 3, 5, 8, 9, 11, 14, 17, 18, 19, 24, 25], "therefor": [0, 25], "thi": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26], "thin": 0, "thing": [0, 3, 6, 25], "think": 24, "third": [0, 2, 12], "those": [0, 2, 17, 25], "thousand": 25, "thread": [7, 11], "three": [0, 2], "threshold": 0, "through": [0, 2, 3, 4, 5, 7, 8, 10, 17, 18, 19, 22, 24, 25], "throw": [0, 2, 7], "thrown": [0, 2], "thu": [2, 9], "thumb": 25, "ti": 0, "ticket": 16, "tif": 25, "tiff": [4, 5, 20, 26], "tight": 6, "tightli": [0, 9], "tile": 5, "tiled_viewing_servic": 7, "till": [0, 2], "time": [0, 1, 2, 3, 4, 5, 7, 9, 11, 12, 16, 17, 18, 21, 22, 24, 25, 26], "time_window": 26, "timeout": [0, 3, 7], "timeout_second": 11, "timeseri": [0, 1, 2, 5, 20, 24], "timeseries_json_to_panda": [0, 4], "timestamp": [0, 2, 17], "timezon": 7, "timinglogg": 7, "tip": [10, 20, 21], "titl": [0, 4, 5, 7, 11, 12, 13, 15, 16, 19], "tmp": 19, "tmp_path": 0, "to_bbox_dict": [0, 7], "to_celsiu": 16, "to_datetim": 4, "to_dict": 0, "to_fil": 0, "to_json": [0, 4, 7, 15, 26], "to_netcdf_fil": 7, "to_process_graph_argu": 0, "to_show": 0, "toa": 2, "todai": 7, "todo": 18, "togeth": [11, 24, 26], "toggl": 0, "toi": 18, "token": [0, 2, 7, 20], "tokeninvalid": 7, "toler": 0, "toml": 25, "ton": 19, "too": [0, 3, 4, 5, 7, 17, 19, 24, 26], "tool": [2, 5, 6, 7, 8, 12, 15, 17, 19, 20, 21, 25], "toolbox": 9, "toomanydimens": 2, "top": [0, 1, 2, 3, 4, 25, 26], "topic": 4, "total": 2, "total_count": 2, "touch": 2, "tr": 0, "trace": 0, "track": [0, 1, 2, 4, 6, 11, 19, 21, 24], "tracker": [11, 21], "traction": 6, "tradit": 19, "trail": 25, "train": [0, 2, 7, 13], "training_job": 22, "transfer": [13, 18], "transform": [0, 2, 12, 20, 24, 26], "translat": [1, 2, 17, 24], "transpar": 25, "travi": 7, "tree": [0, 3], "tri": [0, 4, 7, 19], "triangl": 0, "trick": [10, 20], "trigger": [0, 3, 4, 7, 19, 25], "trim": [0, 2], "trim_cub": [2, 23], "trivial": 4, "tropic": [0, 2], "troubl": [21, 24], "troubleshoot": 20, "true": [0, 2, 3, 7, 13, 15, 16, 18, 25, 26], "try": [0, 3, 7, 9, 18, 19, 21, 22, 24, 25], "tune": [5, 7, 14, 18, 19, 26], "tupl": [0, 7, 17], "turn": [25, 26], "tutori": [4, 6], "tweak": [7, 19], "twice": 24, "twine": 19, "two": [0, 2, 3, 4, 7, 17, 24, 26], "type": [0, 1, 2, 3, 4, 5, 7, 11, 14, 17, 18, 22, 25, 26], "typeerror": 0, "typic": [0, 3, 5, 11, 12, 14, 17, 18, 19, 24, 25], "u": [3, 4, 16, 19], "u24": 12, "u3": 12, "u65": 12, "udf": [2, 20, 26], "udf_cod": 25, "udf_data": 0, "udf_dict": 0, "udf_modify_spati": 25, "udfdata": [0, 25], "udp": [7, 10, 11, 20, 24, 25], "udp_url": 16, "ui": 25, "ultim": 26, "unambigu": 2, "unari": 0, "unattend": 3, "unavail": 7, "unbound": [0, 2], "unchang": [0, 2, 25], "uncommit": 19, "uncommon": 8, "uncorrect": 9, "undefin": 2, "under": [0, 3, 4, 5, 19, 22], "underli": [0, 2, 9, 15], "underpin": 25, "underscor": 0, "understand": [1, 2, 3, 6, 17, 25], "uneven": 2, "unflatten": [0, 7], "unflatten_dimens": [0, 2, 7, 23], "unfortun": 17, "unhandl": 14, "unhelp": 7, "uniform": 25, "unintend": 17, "unintuit": 17, "union": [0, 2, 11], "uniqu": [0, 2], "unit": [0, 2, 7, 20, 21, 24, 25], "unitmismatch": 2, "unknown": [0, 11], "unless": [2, 19], "unlock": 21, "unmodifi": 0, "unnecessari": 0, "unnecessarili": [6, 7, 17], "unreleas": [19, 20, 21], "unrol": 16, "until": [2, 4, 5], "unus": [3, 7, 25], "unwant": 4, "unzip": 19, "unzipped_virtualenv_loc": 25, "up": [0, 2, 3, 5, 6, 7, 11, 13, 17, 22, 24, 25], "updat": [0, 2, 7, 20, 25], "update_argu": [0, 7, 24], "upgrad": 21, "upload": [0, 7, 17, 19], "upload_fil": 0, "upper": [0, 2], "upstream": [2, 19], "urban": [7, 14], "uri": 0, "url": [0, 2, 4, 7, 8, 10, 11, 12, 15, 17, 18, 20, 22, 24, 25], "us": [0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 25], "usabl": [3, 4, 7], "usag": [0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 24, 26], "use_pkc": 0, "use_pyproj": 0, "user": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 17, 18, 19, 20, 24], "user_cod": 3, "user_context": 0, "user_defined_process": 0, "user_defined_process_id": [0, 26], "user_id": 3, "user_job": 0, "userfil": [7, 20], "usernam": [0, 3], "usual": [0, 2, 3, 5, 8, 13, 17, 19, 24, 25, 26], "utc": [2, 7, 17], "utcnow": 7, "utf8": 15, "util": [7, 20], "utm": 13, "ux": 20, "v0": [7, 19], "v1": [9, 12], "v2": 5, "v3": 25, "valid": [0, 2, 3, 5, 7, 9, 12, 13, 18, 25], "valid_count": 2, "valid_within": [0, 2], "validate_process_graph": 0, "valu": [0, 1, 2, 4, 5, 7, 8, 11, 15, 17, 18, 20, 22, 24, 26], "valuabl": 4, "valueerror": [0, 7], "vapour": [2, 14], "var": 7, "vari": [0, 25], "variabl": [0, 2, 4, 7, 8, 14, 24], "variable_map": 14, "varianc": [2, 23], "variant": [0, 9], "varieti": 9, "variou": [0, 3, 4, 6, 7, 8, 14, 17, 19, 25], "vectocub": 7, "vector": [0, 2, 4, 7, 13, 20, 22, 24, 25], "vector_buff": [2, 23], "vector_reproject": 2, "vector_to_random_point": [2, 23], "vector_to_rast": [0, 7], "vector_to_regular_point": [2, 23], "vectorcub": [7, 15, 18, 20, 23, 24], "vectorcube_from_path": [0, 7, 17], "veget": [0, 2, 4, 7, 14], "venv": [19, 21], "verbos": [3, 7, 8], "veri": [0, 3, 4, 6, 7, 9, 16, 17, 24, 25, 26], "verif": 0, "verifi": [3, 7, 19, 25], "versatil": 25, "version": [0, 2, 3, 5, 6, 7, 8, 9, 11, 14, 17, 18, 19, 20, 21, 22, 24, 26], "version_discoveri": 0, "version_info": [0, 7], "vertic": 2, "vgt": [13, 19], "vh": 9, "via": 19, "view": [2, 6, 9], "viewazimuthmean": 9, "viewer": 17, "viewport": 6, "viewzenithmean": 9, "violat": 19, "virtual": [1, 2, 19, 21, 25], "visibl": [0, 2], "visit": [3, 4, 19], "visual": [0, 5, 7, 12, 17, 25], "visualis": 21, "vito": [4, 9, 13, 16, 19, 20], "vue": 7, "vv": 9, "w": 26, "w3": 0, "wa": [0, 2, 3, 5, 7, 11, 17, 25], "wai": [0, 2, 3, 4, 5, 6, 17, 18, 19, 22, 24, 25, 26], "wait": [0, 4, 7, 11, 19], "walk": [0, 2], "walk_nod": 0, "want": [0, 1, 2, 3, 4, 5, 12, 13, 14, 17, 18, 19, 21, 24, 25, 26], "warn": [0, 5, 7, 16, 21, 25], "warp": 2, "watch": 19, "water": [2, 7, 9, 14], "water_vapor": 2, "wavelength": 2, "wd23": 22, "we": [1, 2, 3, 4, 9, 11, 12, 13, 16, 17, 18, 19, 22, 24, 25, 26], "web": [0, 3, 4, 5, 7, 9, 19, 26], "webbrowser_open": 0, "week": [0, 2], "weekli": 3, "weight": [0, 2], "welcom": [11, 19, 20], "well": [0, 1, 2, 4, 6, 7, 14, 21, 25, 26], "went": 21, "were": [7, 24, 25], "west": [0, 4, 6, 7, 9, 12, 17, 20, 24, 25, 26], "wg": 17, "wgs84": 0, "what": [0, 1, 2, 3, 5, 19, 24, 25, 26], "wheel": [19, 25], "when": [0, 1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 14, 15, 16, 17, 18, 19, 22, 24, 25, 26], "whenev": 25, "where": [0, 1, 2, 3, 5, 6, 7, 8, 11, 13, 18, 19, 22, 25], "whether": [0, 2, 4], "which": [0, 1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14, 15, 17, 18, 19, 21, 24, 25, 26], "while": [0, 1, 2, 3, 4, 5, 6, 7, 11, 17, 18, 19, 24, 25, 26], "whitespac": 19, "whl": [19, 25], "whole": [0, 2, 3, 7, 19, 22, 25], "whose": [0, 2], "wide": [0, 25], "wider": 6, "widget": [0, 7], "wiki": 2, "wikipedia": 2, "window": [0, 2, 4, 7, 17, 20, 25, 26], "winter": 2, "wise": 0, "wish": 24, "within": [0, 2, 7, 25], "without": [0, 2, 3, 4, 7, 8, 9, 12, 15, 18, 19, 25], "wkt2": [0, 2], "won": [3, 25], "word": [0, 24], "work": [0, 3, 4, 5, 7, 8, 9, 11, 13, 17, 19, 20, 21, 25, 26], "workaround": 7, "worker": [5, 25], "workflow": [0, 3, 6, 19, 21, 22, 26], "workspac": [0, 2], "world": 17, "worri": [4, 24, 26], "wors": 3, "would": [0, 2, 3, 7, 11, 25], "wrap": [0, 2, 7], "wrapper": [0, 7, 26], "write": [0, 4, 7, 11, 13, 15, 16, 21, 24], "write_text": 15, "written": 0, "wrong": 26, "wrongli": 24, "wv": 14, "www": [0, 2], "x": [0, 2, 4, 7, 12, 15, 16, 17, 18, 20, 24, 25, 26], "xarrai": [0, 7, 12, 21, 25], "xarraydatacub": [0, 7, 25], "xarrayio": 7, "xdc_dict": 0, "xdg_config_hom": 8, "xor": [2, 23], "xstep": 25, "xyz": [0, 7], "y": [0, 2, 4, 7, 12, 15, 16, 18, 24, 25, 26], "yaml": 19, "year": [0, 2, 7, 11], "yearli": 2, "yellow": 0, "yet": [0, 2, 3, 4, 5, 7, 9, 11, 24, 25], "you": [0, 1, 2, 3, 4, 5, 6, 9, 11, 12, 13, 14, 15, 17, 18, 19, 21, 22, 24, 25, 26], "your": [0, 1, 2, 3, 4, 6, 7, 9, 12, 13, 14, 15, 17, 18, 19, 20, 21, 24, 25, 26], "yourself": [17, 19, 24], "ystep": 25, "yyy0": 2, "yyy1": 2, "yyyi": [2, 17], "zarr": 12, "zero": [0, 2], "zip": [19, 25], "zonal": [0, 2, 4], "zonal_statist": 7, "zone": 13, "zoom": 0, "\u03bcm": 2, "\u03c0": 2}, "titles": ["API (General)", "<no title>", "API: openeo.processes", "Authentication and Account Management", "Getting Started", "Batch Jobs", "Best practices, coding style and general tips", "Changelog", "Configuration", "Analysis Ready Data generation", "openEO CookBook", "Multi Backend Job Manager", "Client-side (local) processing", "Dataset sampling", "Spectral Indices", "Miscellaneous tips and tricks", "Sharing of user-defined processes", "Finding and loading data", "DataCube construction", "Development and maintenance", "openEO Python Client", "Installation", "Machine Learning", "openEO Process Mapping", "Working with processes", "User-Defined Functions (UDF) explained", "User-Defined Processes (UDP)"], "titleterms": {"": 25, "0": [7, 25], "01": 7, "02": 7, "03": 7, "04": 7, "05": 7, "06": 7, "07": 7, "08": 7, "09": 7, "1": 7, "10": 7, "11": 7, "12": 7, "13": [7, 25], "14": 7, "15": 7, "16": 7, "17": 7, "18": 7, "19": 7, "2": 7, "20": 7, "2020": 7, "2021": 7, "2022": 7, "2023": 7, "2024": 7, "21": 7, "22": 7, "23": 7, "24": 7, "25": 7, "26": 7, "27": 7, "28": 7, "29": 7, "30": 7, "31": 7, "32": 7, "33": 7, "34": 7, "4": 7, "5": 7, "6": 7, "7": 7, "8": 7, "9": 7, "A": [24, 25], "The": 18, "account": 3, "ad": [7, 24, 25], "addit": 21, "advanc": [24, 26], "aggreg": 4, "all": 5, "altern": 19, "an": [4, 17, 25], "analysi": 9, "api": [0, 2, 11, 14, 24, 25], "appli": [4, 25], "applic": [3, 25], "apply_dimens": 25, "apply_neighborhood": 25, "argument": 24, "asset": 5, "asynchron": 4, "atmospher": 9, "auth": 3, "authent": [3, 4], "auto": 3, "automat": [5, 14], "back": [3, 4, 9], "backend": 11, "background": [6, 12], "backscatt": 9, "band": [4, 14], "base": [3, 11, 16, 22], "basic": [3, 11, 19, 21, 24], "batch": [4, 5], "best": [3, 6], "bit": 24, "build": [0, 18, 19, 26], "call": 24, "callabl": 24, "callback": [24, 25], "case": 4, "caveat": 24, "chang": [7, 25], "changelog": 7, "check": 19, "child": 24, "class": 2, "classif": 22, "clear": 3, "client": [3, 12, 20], "close": 17, "cloud": 4, "code": [3, 6, 19, 26], "collect": [4, 12, 17], "commit": 19, "common": 24, "comput": 4, "conda": 21, "config": 3, "configur": 8, "connect": [0, 3, 4], "constraint": 25, "construct": 18, "content": [10, 20], "context": 3, "contribut": 19, "conveni": 24, "convers": 0, "cookbook": 10, "correct": 9, "creat": [5, 19], "creation": 11, "credenti": 3, "cube": [4, 17, 24, 25, 26], "data": [4, 9, 17, 24, 26], "datacub": [0, 16, 18, 25], "dataset": 13, "date": 17, "declar": [25, 26], "default": 3, "defin": [16, 24, 25, 26], "depend": [21, 25], "deprec": 7, "develop": [19, 21], "devic": 3, "dictionari": 26, "directli": [5, 15], "discoveri": [4, 17], "do": 11, "document": 19, "down": 17, "download": [4, 5, 25], "dynam": 3, "easi": 19, "enabl": 21, "end": [3, 4, 9, 17], "environ": 3, "eodc": 9, "evalu": 26, "evi": [4, 26], "exampl": [4, 11, 18, 20, 25, 26], "exclud": 17, "execut": [4, 15, 25], "explain": 25, "explor": 17, "export": 15, "extent": 17, "featur": 21, "file": [3, 8, 19, 26], "filter": 17, "find": 17, "fine": 5, "finish": 5, "first": 25, "fix": 7, "flow": 3, "forest": 22, "format": 8, "from": [15, 17, 18, 24, 25, 26], "function": [2, 25, 26], "gener": [0, 3, 6, 9, 19, 24], "geometri": 11, "geotrelli": 9, "get": 4, "go": 5, "grain": 5, "graph": [0, 15, 18], "guidelin": 3, "handl": [11, 17, 25], "helper": [2, 3], "high": 0, "hoc": 25, "http": 3, "implement": 9, "import": 19, "includ": 17, "indic": [14, 20], "infer": 22, "inform": 25, "initi": [4, 17], "inspir": 6, "instal": [12, 19, 21], "integr": 5, "interact": 3, "interfac": 0, "intern": 0, "interv": 17, "item": 12, "job": [0, 4, 5, 11], "json": [15, 18], "jupyt": [5, 6], "lab": 6, "larg": 17, "learn": 22, "left": 17, "length": 6, "level": 0, "like": 19, "line": 6, "list": 5, "load": [4, 5, 16, 17], "load_collect": 18, "local": [12, 25], "locat": 8, "log": [0, 5, 25], "long": 3, "lps22": 7, "machin": 22, "mainten": 19, "manag": [3, 6, 11, 25], "manual": 14, "map": [4, 14, 23], "mask": 4, "math": 4, "metadata": [0, 25], "method": [3, 24], "miscellan": 15, "mlmodel": 0, "modul": 25, "month": 17, "more": 26, "multi": 11, "multipl": [4, 18], "multiresult": 0, "name": 25, "namespac": 16, "node": 18, "non": 3, "notat": 17, "object": 5, "oidc": 3, "one": 5, "openeo": [0, 2, 3, 4, 10, 17, 20, 23, 25], "openid": 3, "option": [3, 8, 21], "other": 24, "paramet": 26, "parameter": [11, 18, 26], "pass": 24, "perform": 13, "period": 17, "pgnode": 24, "pip": 21, "pixel": 25, "practic": [3, 6], "pre": [19, 24], "predefin": 26, "prerequisit": 19, "print": 5, "pro": 19, "procedur": 19, "process": [0, 2, 11, 12, 15, 16, 18, 23, 24, 25, 26], "processbasedjobcr": 11, "processbuild": 2, "profil": 25, "properti": 17, "public": [0, 16], "publicli": 16, "publish": 16, "pull": 19, "python": [20, 25], "qualiti": 19, "quick": 19, "random": 22, "raw": 15, "re": 18, "readi": 9, "recommend": 6, "reconnect": 5, "reduc": 25, "reduce_dimens": 25, "refer": 9, "refresh": 3, "regress": 22, "releas": [7, 19], "remov": 7, "request": 19, "rescal": 25, "rest": 0, "result": [0, 5, 18, 24], "reus": 26, "round": 17, "run": [3, 5, 19], "sampl": 13, "sar": 9, "scalabl": 13, "scale": 13, "schema": 26, "script": 25, "section": 2, "select": 3, "server": 25, "set": [17, 19], "share": 16, "shorthand": 17, "side": [12, 25], "signatur": 25, "singl": [5, 17], "smooth": 25, "some": 18, "sourc": 21, "spatial": 17, "spectral": 14, "srr3": 7, "srr5": 7, "srr6": 7, "stac": 12, "standard": 25, "start": [4, 5, 17], "statist": 4, "store": 26, "string": [17, 24], "style": 6, "synchron": 4, "tabl": 20, "tempor": 17, "terminologi": 24, "test": [0, 19], "through": [16, 26], "timeseri": [4, 25, 26], "tip": [3, 6, 15], "token": 3, "tool": 3, "train": 22, "transform": 25, "trick": [6, 15], "troubleshoot": [3, 21], "tweak": 24, "udf": [0, 7, 25], "udf_signatur": 25, "udp": [0, 16, 26], "unit": 19, "unreleas": 7, "up": 19, "updat": 19, "url": [3, 16], "us": [3, 4, 16, 24, 26], "usag": [12, 19, 20, 25], "user": [16, 25, 26], "userfil": 0, "util": 0, "ux": 7, "valu": 25, "variabl": 3, "vector": 17, "vectorcub": 0, "verif": [19, 25], "verifi": 21, "version": 25, "view": 25, "wait": 5, "window": 19, "work": 24, "workflow": 25, "year": 17, "your": 5}}) \ No newline at end of file diff --git a/udf.html b/udf.html new file mode 100644 index 000000000..8381c4272 --- /dev/null +++ b/udf.html @@ -0,0 +1,917 @@ + + + + + + + + User-Defined Functions (UDF) explained — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

User-Defined Functions (UDF) explained

+

While openEO supports a wide range of pre-defined processes +and allows to build more complex user-defined processes from them, +you sometimes need operations or algorithms that are +not (yet) available or standardized as openEO process. +User-Defined Functions (UDF) is an openEO feature +(through the run_udf process) +that aims to fill that gap by allowing a user to express (a part of) +an algorithm as a Python/R/… script to be run back-end side.

+

There are a lot of details to cover, +but here is a rudimentary example snippet +to give you a quick impression of how to work with UDFs +using the openEO Python Client library:

+
+
Basic UDF usage example snippet to rescale pixel values
+
import openeo
+
+# Build a UDF object from an inline string with Python source code.
+udf = openeo.UDF("""
+import xarray
+
+def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+    cube.values = 0.0001 * cube.values
+    return cube
+""")
+
+# Or load the UDF code from a separate file.
+# udf = openeo.UDF.from_file("udf-code.py")
+
+# Apply the UDF to a cube.
+rescaled_cube = cube.apply(process=udf)
+
+
+
+

Ideally, it allows you to embed existing Python/R/… implementations +in an openEO workflow (with some necessary “glue code”). +However, it is recommended to try to do as much pre- or postprocessing +with pre-defined processes +before blindly copy-pasting source code snippets as UDFs. +Pre-defined processes are typically well-optimized by the backend, +while UDFs can come with a performance penalty +and higher development/debug/maintenance costs.

+
+

Warning

+

Don not confuse user-defined functions (abbreviated as UDF) with +user-defined processes (sometimes abbreviated as UDP) in openEO, +which is a way to define and use your own process graphs +as reusable building blocks. +See User-Defined Processes (UDP) for more information.

+
+
+

Applicability and Constraints

+

openEO is designed to work transparently on large data sets +and your UDF has to follow a couple of guidelines to make that possible. +First of all, as data cubes play a central role in openEO, +your UDF should accept and return correct data cube structures, +with proper dimensions, dimension labels, etc. +Moreover, the back-end will typically divide your input data cube +in smaller chunks and process these chunks separately (e.g. on isolated workers). +Consequently, it’s important that your UDF algorithm operates correctly +in such a chunked processing context.

+

A very common mistake is to use index-based array indexing, rather than name based. The index based approach +assumes that datacube dimension order is fixed, which is not guaranteed. Next to that, it also reduces the readability +of your code. Label based indexing is a great feature of xarray, and should be used whenever possible.

+

As a rule of thumb, the UDF should preserve the dimensions and shape of the input +data cube. The datacube chunk that is passed on by the backend does not have a fixed +specification, so the UDF needs to be able to accomodate different shapes and sizes of the data.

+

There’s important exceptions to this rule, that depend on the context in which the UDF is used. +For instance, a UDF used as a reducer should effectively remove the reduced dimension from the +output chunk. These details are documented in the next sections.

+
+

UDFs as apply/reduce “callbacks”

+

UDFs are typically used as “callback” processes for “meta” processes +like apply or reduce_dimension (also see Processes with child “callbacks”). +These meta-processes make abstraction of a datacube as a whole +and allow the callback to focus on a small slice of data or a single dimension. +Their nature instructs the backend how the data should be processed +and can be chunked:

+
+
apply

Applies a process on each pixel separately. +The back-end has all freedom to choose chunking +(e.g. chunk spatially and temporally). +Dimensions and their labels are fully preserved. +This function has limited practical use in combination with UDF’s.

+
+
apply_dimension

Applies a process to all pixels along a given dimension +to produce a new series of values for that dimension. +The back-end will not split your data on that dimension. +For example, when working along the time dimension, +your UDF is guaranteed to receive a full timeseries, +but the data could be chunked spatially. +All dimensions and labels are preserved, +except for the dimension along which apply_dimension is applied: +the number of dimension labels is allowed to change.

+
+
reduce_dimension

Applies a process to all pixels along a given dimension +to produce a single value, eliminating that dimension. +Like with apply_dimension, the back-end will +not split your data on that dimension. +The dimension along which apply_dimension is applied must be removed +from the output. +For example, when applying reduce_dimension on a spatiotemporal cube +along the time dimension, +the UDF is guaranteed to receive full timeseries +(but the data could be chunked spatially) +and the output cube should only be a spatial cube, without a temporal dimension

+
+
apply_neighborhood

Applies a process to a neighborhood of pixels +in a sliding-window fashion with (optional) overlap. +Data chunking in this case is explicitly controlled by the user. +Dimensions and number of labels are fully preserved. This is the most versatile +and widely used function to work with UDF’s.

+
+
+
+
+
+

UDF function names and signatures

+

The UDF code you pass to the back-end is basically a Python script +that contains one or more functions. +Exactly one of these functions should have a proper UDF signature, +as defined in the openeo.udf.udf_signatures module, +so that the back-end knows what the entrypoint function is +of your UDF implementation.

+
+

Module openeo.udf.udf_signatures

+

This module defines a number of function signatures that can be implemented by UDF’s. +Both the name of the function and the argument types are/can be used by the backend to validate if the provided UDF +is compatible with the calling context of the process graph in which it is used.

+
+
+openeo.udf.udf_signatures.apply_datacube(cube, context)[source]
+

Map a XarrayDataCube to another XarrayDataCube.

+

Depending on the context in which this function is used, the XarrayDataCube dimensions +have to be retained or can be chained. +For instance, in the context of a reducing operation along a dimension, +that dimension will have to be reduced to a single value. +In the context of a 1 to 1 mapping operation, all dimensions have to be retained.

+
+
Parameters:
+
    +
  • cube (XarrayDataCube) – input data cube

  • +
  • context (dict) – A dictionary containing user context.

  • +
+
+
Return type:
+

XarrayDataCube

+
+
Returns:
+

output data cube

+
+
+
+ +
+
+openeo.udf.udf_signatures.apply_metadata(metadata, context)[source]
+
+

Warning

+

This signature is not yet fully standardized and subject to change.

+
+

Returns the expected cube metadata, after applying this UDF, based on input metadata. +The provided metadata represents the whole raster or vector cube. This function does not need to be called for every data chunk.

+

When this function is not implemented by the UDF, the backend may still be able to infer correct metadata by running the +UDF, but this can result in reduced performance or errors.

+

This function does not need to be provided when using the UDF in combination with processes that by design have a clear +effect on cube metadata, such as reduce_dimension()

+
+
Parameters:
+
    +
  • metadata (CollectionMetadata) – the collection metadata of the input data cube

  • +
  • context (dict) – A dictionary containing user context.

  • +
+
+
Return type:
+

CollectionMetadata

+
+
Returns:
+

output metadata: the expected metadata of the cube, after applying the udf

+
+
+
+

Examples

+

An example for a UDF that is applied on the ‘bands’ dimension, and returns a new set of bands with different labels.

+
>>> def apply_metadata(metadata: CollectionMetadata, context: dict) -> CollectionMetadata:
+...     return metadata.rename_labels(
+...         dimension="bands",
+...         target=["computed_band_1", "computed_band_2"]
+...     )
+
+
+
+
+ +
+
+openeo.udf.udf_signatures.apply_timeseries(series, context)[source]
+

Process a timeseries of values, without changing the time instants.

+

This can for instance be used for smoothing or gap-filling.

+
+
Parameters:
+
    +
  • series (Series) – A Pandas Series object with a date-time index.

  • +
  • context (dict) – A dictionary containing user context.

  • +
+
+
Return type:
+

Series

+
+
Returns:
+

A Pandas Series object with the same datetime index.

+
+
+
+ +
+
+openeo.udf.udf_signatures.apply_udf_data(data)[source]
+

Generic UDF function that directly manipulates a UdfData object

+
+
Parameters:
+

data (UdfData) – UdfData object to manipulate in-place

+
+
+
+ +
+
+openeo.udf.udf_signatures.apply_vectorcube(geometries, cube, context)[source]
+

Map a vector cube to another vector cube.

+
+
Parameters:
+
    +
  • geometries (geopandas.geodataframe.GeoDataFrame) – input geometries as a geopandas.GeoDataFrame. This contains the actual shapely geometries and optional properties.

  • +
  • cube (DataArray) – a data cube with dimensions (geometries, time, bands) where time and bands are optional. +The coordinates for the geometry dimension are integers and match the index of the geometries in the geometries parameter.

  • +
  • context (dict) – A dictionary containing user context.

  • +
+
+
Return type:
+

(geopandas.geodataframe.GeoDataFrame, DataArray)

+
+
Returns:
+

output geometries, output data cube

+
+
+
+ +
+
+
+

A first example: apply with an UDF to rescale pixel values

+

In most of the examples here, we will start from an initial Sentinel2 data cube like this:

+
s2_cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    spatial_extent={"west": 4.00, "south": 51.04, "east": 4.10, "north": 51.1},
+    temporal_extent=["2022-03-01", "2022-03-31"],
+    bands=["B02", "B03", "B04"]
+)
+
+
+

The raw values in this initial s2_cube data cube are digital numbers +(integer values ranging from 0 to several thousands) +and to get physical reflectance values (float values, typically in the range between 0 and 0.5), +we have to rescale them. +This is a simple local transformation, without any interaction between pixels, +which is the modus operandi of the apply processes.

+
+

Note

+

In practice it will be a lot easier and more efficient to do this kind of rescaling +with pre-defined openEO math processes, for example: s2_cube.apply(lambda x: 0.0001 * x). +This is just a very simple illustration to get started with UDFs. In fact, it’s very likely that +you will never want to use a UDF with apply.

+
+
+

UDF script

+

The UDF code is this short script (the part that does the actual value rescaling is highlighted):

+
+
udf-code.py
+
1import xarray
+2
+3def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+4    cube.values = 0.0001 * cube.values
+5    return cube
+
+
+
+

Some details about this UDF script:

+
    +
  • line 1: We import xarray as we use this as exchange format.

  • +
  • line 3: We define a function named apply_datacube, +which receives and returns a DataArray instance. +We follow here the apply_datacube() UDF function signature.

  • +
  • line 4: Because our scaling operation is so simple, we can transform the xarray.DataArray values in-place.

  • +
  • line 5: Consequently, because the values were updated in-place, we can return the same Xarray object.

  • +
+
+
+

Workflow script

+

In this first example, we’ll cite a full, standalone openEO workflow script, +including creating the back-end connection, loading the initial data cube and downloading the result. +The UDF-specific part is highlighted.

+
+

Warning

+

This implementation depends on openeo.UDF improvements +that were introduced in version 0.13.0 of the openeo Python Client Library. +If you are currently stuck with working with an older version, +check openeo.UDF API and usage changes in version 0.13.0 for more information on the difference with the old API.

+
+
+
UDF usage example snippet
+
 1import openeo
+ 2
+ 3# Create connection to openEO back-end
+ 4connection = openeo.connect("...").authenticate_oidc()
+ 5
+ 6# Load initial data cube.
+ 7s2_cube = connection.load_collection(
+ 8    "SENTINEL2_L2A",
+ 9    spatial_extent={"west": 4.00, "south": 51.04, "east": 4.10, "north": 51.1},
+10    temporal_extent=["2022-03-01", "2022-03-31"],
+11    bands=["B02", "B03", "B04"]
+12)
+13
+14# Create a UDF object from inline source code.
+15udf = openeo.UDF("""
+16import xarray
+17
+18def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+19    cube.values = 0.0001 * cube.values
+20    return cube
+21""")
+22
+23# Pass UDF object as child process to `apply`.
+24rescaled = s2_cube.apply(process=udf)
+25
+26rescaled.download("apply-udf-scaling.nc")
+
+
+
+

In line 15, we build an openeo.UDF object +from an inline string with the UDF source code. +This openeo.UDF object encapsulates various aspects +that are necessary to create a run_udf node in the process graph, +and we can pass it directly in line 25 as the process argument +to DataCube.apply().

+
+

Tip

+

Instead of putting your UDF code in an inline string like in the example, +it’s often a good idea to load the UDF code from a separate file, +which is easier to maintain in your preferred editor or IDE. +You can do that directly with the +openeo.UDF.from_file method:

+
udf = openeo.UDF.from_file("udf-code.py")
+
+
+
+

After downloading the result, we can inspect the band values locally. +Note see that they fall mainly in a range from 0 to 1 (in most cases even below 0.2), +instead of the original digital number range (thousands):

+_images/apply-rescaled-histogram.png +
+
+
+

UDF’s that transform cube metadata

+

This is a new/experimental feature so may still be subject to change.

+

In some cases, a UDF can have impact on the metadata of a cube, but this can not always +be easily inferred by process graph evaluation logic without running the actual +(expensive) UDF code. This limits the possibilities to validate process graphs, +or for instance make an estimate of the size of a datacube after applying a UDF.

+

To provide evaluation logic with this information, the user should implement the +apply_metadata() function as part of the UDF. +Please refer to the documentation of that function for more information.

+
+
Example of a UDF that adjusts spatial metadata udf_modify_spatial.py
+
import xarray
+from openeo.udf import XarrayDataCube
+from openeo.udf.debug import inspect
+from openeo.metadata import CollectionMetadata
+import numpy as np
+
+def apply_metadata(input_metadata:CollectionMetadata, context:dict) -> CollectionMetadata:
+
+    xstep = input_metadata.get('x','step')
+    ystep = input_metadata.get('y','step')
+    new_metadata = {
+          "x": {"type": "spatial", "axis": "x", "step": xstep/2.0, "reference_system": 4326},
+          "y": {"type": "spatial", "axis": "y", "step": ystep/2.0, "reference_system": 4326},
+          "t": {"type": "temporal"}
+    }
+    return CollectionMetadata(new_metadata)
+
+def fancy_upsample_function(array: np.array, factor: int = 2) -> np.array:
+    assert array.ndim == 3
+    return array.repeat(factor, axis=-1).repeat(factor, axis=-2)
+
+def apply_datacube(cube: XarrayDataCube, context: dict) -> XarrayDataCube:
+    array: xarray.DataArray = cube.get_array()
+
+    cubearray: xarray.DataArray = cube.get_array().copy() + 60
+
+    # We make prediction and transform numpy array back to datacube
+
+    # Pixel size of the original image
+    init_pixel_size_x = cubearray.coords['x'][-1] - cubearray.coords['x'][-2]
+    init_pixel_size_y = cubearray.coords['y'][-1] - cubearray.coords['y'][-2]
+
+    if cubearray.data.ndim == 4 and cubearray.data.shape[0] == 1:
+        cubearray = cubearray[0]
+    predicted_array = fancy_upsample_function(cubearray.data, 2)
+    inspect(predicted_array, "test message")
+    coord_x = np.linspace(start=cube.get_array().coords['x'].min(), stop=cube.get_array().coords['x'].max() + init_pixel_size_x,
+                          num=predicted_array.shape[-2], endpoint=False)
+    coord_y = np.linspace(start=cube.get_array().coords['y'].min(), stop=cube.get_array().coords['y'].max() + init_pixel_size_y,
+                          num=predicted_array.shape[-1], endpoint=False)
+    predicted_cube = xarray.DataArray(predicted_array, dims=['bands', 'x', 'y'], coords=dict(x=coord_x, y=coord_y))
+
+
+    return XarrayDataCube(predicted_cube)
+
+
+
+

To invoke a UDF like this, the apply_neighborhood method is most suitable:

+
udf_code = Path('udf_modify_spatial.py').read_text()
+cube_updated = cube.apply_neighborhood(
+    lambda data: data.run_udf(udf=udf_code, runtime='Python-Jep', context=dict()),
+    size=[
+        {'dimension': 'x', 'value': 128, 'unit': 'px'},
+        {'dimension': 'y', 'value': 128, 'unit': 'px'}
+    ], overlap=[])
+
+
+
+
+

Example: apply_dimension with a UDF

+

This is useful when running custom code over all band values for a given pixel or all observations per pixel. +See section below ‘Smoothing timeseries with a user defined function’ for a concrete example.

+
+
+

Example: reduce_dimension with a UDF

+

The key element for a UDF invoked in the context of reduce_dimension is that it should actually return +an Xarray DataArray _without_ the dimension that is specified to be reduced.

+

So a reduce over time would receive a DataArray with bands,t,y,x dimensions, and return one with only bands,y,x.

+
+
+

Example: apply_neighborhood with a UDF

+

The apply_neighborhood process is generally used when working with complex AI models that require a +spatiotemporal input stack with a fixed size. It supports the ability to specify overlap, to ensure that the model +has sufficient border information to generate a spatially coherent output across chunks of the raster data cube.

+

In the example below, the UDF will receive chunks of 128x128 pixels: 112 is the chunk size, while 2 times 8 pixels of +overlap on each side of the chunk results in 128.

+

The time and band dimensions are not specified, which means that all values along these dimensions are passed into +the datacube.

+
output_cube = inputs_cube.apply_neighborhood(my_udf, size=[
+        {'dimension': 'x', 'value': 112, 'unit': 'px'},
+        {'dimension': 'y', 'value': 112, 'unit': 'px'}
+    ], overlap=[
+        {'dimension': 'x', 'value': 8, 'unit': 'px'},
+        {'dimension': 'y', 'value': 8, 'unit': 'px'}
+    ])
+
+
+

The apply_neighborhood is the most versatile, but also most complex process. Make sure to keep an eye on the dimensions +and the shape of the DataArray returned by your UDF. For instance, a very common error is to somehow ‘flip’ the spatial dimensions. +Debugging the UDF locally can help, but then you will want to try and reproduce the input that you get also on the backend. +This can typically be achieved by using logging to inspect the DataArrays passed into your UDF backend side.

+
+
+

Example: Smoothing timeseries with a user defined function (UDF)

+

In this example, we start from the evi_cube that was created in the previous example, and want to +apply a temporal smoothing on it. More specifically, we want to use the “Savitzky Golay” smoother +that is available in the SciPy Python library.

+

To ensure that openEO understand your function, it needs to follow some rules, the UDF specification. +This is an example that follows those rules:

+
+
Example UDF code smooth_savitzky_golay.py
+
import xarray
+from scipy.signal import savgol_filter
+
+from openeo.udf import XarrayDataCube
+
+
+def apply_datacube(cube: XarrayDataCube, context: dict) -> XarrayDataCube:
+    """
+    Apply Savitzky-Golay smoothing to a timeseries datacube.
+    This UDF preserves dimensionality, and assumes an input
+    datacube with a temporal dimension 't' as input.
+    """
+    array: xarray.DataArray = cube.get_array()
+    filled = array.interpolate_na(dim='t')
+    smoothed_array = savgol_filter(filled.values, 5, 2, axis=0)
+    return XarrayDataCube(
+        array=xarray.DataArray(smoothed_array, dims=array.dims, coords=array.coords)
+    )
+
+
+
+

The method signature of the UDF is very important, because the back-end will use it to detect +the type of UDF. +This particular example accepts a DataCube object as input and also returns a DataCube object. +The type annotations and method name are actually used to detect how to invoke the UDF, so make sure they remain unchanged.

+

Once the UDF is defined in a separate file, we load it +and apply it along a dimension:

+
smoothing_udf = openeo.UDF.from_file('smooth_savitzky_golay.py')
+smoothed_evi = evi_cube_masked.apply_dimension(smoothing_udf, dimension="t")
+
+
+
+
+

Downloading a datacube and executing an UDF locally

+

Sometimes it is advantageous to run a UDF on the client machine (for example when developing/testing that UDF). +This is possible by using the convenience function openeo.udf.run_code.execute_local_udf(). +The steps to run a UDF (like the code from smooth_savitzky_golay.py above) are as follows:

+ +

For example:

+
from pathlib import Path
+from openeo.udf import execute_local_udf
+
+my_process = connection.load_collection(...
+
+my_process.download('test_input.nc', format='NetCDF')
+
+smoothing_udf = Path('smooth_savitzky_golay.py').read_text()
+execute_local_udf(smoothing_udf, 'test_input.nc', fmt='netcdf')
+
+
+

Note: this algorithm’s primary purpose is to aid client side development of UDFs using small datasets. It is not designed for large jobs.

+
+
+

UDF dependency management

+

UDFs usually have some dependencies on existing libraries, e.g. to implement complex algorithms. +In case of Python UDFs, it can be assumed that common libraries like numpy and Xarray are readily available, +not in the least because they underpin the Python UDF function signatures. +More concretely, it is possible to inspect available libraries for the available UDF runtimes +through Connection.list_udf_runtimes(). +For example, to list the available libraries for runtime “Python” (version “3”):

+
>>> connection.list_udf_runtimes()["Python"]["versions"]["3"]["libraries"]
+{'geopandas': {'version': '0.13.2'},
+ 'numpy': {'version': '1.22.4'},
+ 'xarray': {'version': '0.16.2'},
+ ...
+
+
+

Managing and using additional dependencies or libraries that are not provided out-of-the-box by a backend +is a more challenging problem and the practical details can vary between backends.

+
+

Standard for declaring Python UDF dependencies

+
+

Warning

+

This is based on a fairly recent standard and it might not be supported by your chosen backend yet.

+
+

PEP 723 “Inline script metadata” defines a standard +for Python scripts to declare dependencies inside a top-level comment block. +If the openEO backend of your choice supports this standard, it is the preferred approach +to declare the (import) dependencies of your Python UDF:

+
    +
  • It avoids all the overhead for the UDF developer +to correctly and efficiently make desired dependencies available in the UDF.

  • +
  • It allows the openEO backend to optimize dependencies handling.

  • +
+
+

Warning

+

An openEO backend might only support this automatic UDF dependency handling feature +in batch jobs (because of their isolated nature), +but not for synchronous processing requests.

+
+
+

Declaration of UDF dependencies

+

A basic example of how the UDF dependencies can be declared in top-level comment block of your Python UDF:

+
# /// script
+# dependencies = [
+#   "geojson",
+#   "fancy-eo-library",
+# ]
+# ///
+#
+# This openEO UDF script implements ...
+# based on the fancy-eo-library ... using geosjon data ...
+
+import geojson
+import fancyeo
+
+def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+    ...
+
+
+

Some considerations to make sure you have a valid metadata block:

+
    +
  • Lines start with a single hash # and one space (the space can be omitted if the # is the only character on the line).

  • +
  • The metadata block starts with a line # /// script and ends with # ///.

  • +
  • Between these delimiters you put the metadata fields in TOML format, +each line prefixed with # and a space.

  • +
  • Declare your UDF’s dependencies in a dependencies field as a TOML array. +List each package on a separate line as shown above, or put them all on a single line. +It is also allowed to include comments, as long as the whole construct is valid TOML.

  • +
  • Each dependencies entry must be a valid PEP 508 dependency specifier. +This practically means to use the package names (optionally with version constraints) +as expected by the pip install command.

  • +
+

A more complex example to illustrate some more advanced aspects of the metadata block:

+
# /// script
+# dependencies = [
+#   # A comment about using at least version 2.5.0
+#   'geojson>=2.5.0',  # An inline comment
+#   # Note that TOML allows both single and double quotes for strings.
+#
+#   # Install a package "fancyeo" from a (ZIP) source archive URL.
+#   "fancyeo @ https://github.com/fncy/fancyeo/archive/refs/tags/v3.2.0-alpha1.zip",
+#   # Or from a wheel URL, including a content hash to be verified before installing.
+#   "lousyeo @ https://example.com/lousyeo-6.6.6-py3-none-any.whl#sha1=4bbb3c72a9234ee998a6de940a148e346a",
+#   # Note that the last entry may have a trailing comma.
+# ]
+# ///
+
+
+
+
+

Verification

+

Use extract_udf_dependencies() to verify +that your metadata block can be parsed correctly:

+
>>> from openeo.udf.run_code import extract_udf_dependencies
+>>> extract_udf_dependencies(udf_code)
+['geojson>=2.5.0',
+ 'fancyeo @ https://github.com/fncy/fancyeo/archive/refs/tags/v3.2.0-alpha1.zip',
+ 'lousyeo @ https://example.com/lousyeo-6.6.6-py3-none-any.whl#sha1=4bbb3c72a9234ee998a6de940a148e346a']
+
+
+

If no valid metadata block is found, None will be returned.

+
+

Note

+

This function won’t necessarily raise exceptions for syntax errors in the metadata block. +It might just fail to reliably detect anything and skip it as regular comment lines.

+
+
+
+
+

Ad-hoc dependency handling

+

If dependency handling through standardized UDF declarations is not supported by the backend, +there are still ways to manually handle additional dependencies in your UDF. +The exact details can vary between backends, but we can give some general pointers here:

+
    +
  • Multiple Python dependencies can be packaged fairly easily by zipping a Python virtual environment.

  • +
  • For some dependencies, it can be important that the Python major version of the virtual environment is the same as the one used by the backend.

  • +
  • Python allows you to dynamically append (or prepend) libraries to the search path: sys.path.append("unzipped_virtualenv_location")

  • +
+
+
+
+

Profile a process server-side

+
+

Warning

+

Experimental feature - This feature only works on back-ends running the Geotrellis implementation, and has not yet been +adopted in the openEO API.

+
+

Sometimes users want to ‘profile’ their UDF on the back-end. While it’s recommended to first profile it offline, in the +same manner as you can debug UDF’s, back-ends may support profiling directly. +Note that this will only generate statistics over the python part of the execution, therefore it is only suitable for profiling UDFs.

+
+

Usage

+

Only batch jobs are supported! In order to turn on profiling, set ‘profile’ to ‘true’ in job options:

+
job_options={'profile':'true'}
+... # prepare the process
+process.execute_batch('result.tif',job_options=job_options)
+
+
+

When the process has finished, it will also download a file called ‘profile_dumps.tar.gz’:

+
    +
  • rdd_-1.pstats is the profile data of the python driver,

  • +
  • the rest are the profiling results of the individual rdd id-s (that can be correlated with the execution using the SPARK UI).

  • +
+
+
+

Viewing profiling information

+

The simplest way is to visualize the results with a graphical visualization tool called kcachegrind. +In order to do that, install kcachegrind packages (most linux distributions have it installed by default) and it’s python connector pyprof2calltree. +From command line run:

+
pyprof2calltree rdd_<INTERESTING_RDD_ID>.pstats.
+
+
+

Another way is to use the builtin pstats functionality from within python:

+
import pstats
+p = pstats.Stats('restats')
+p.print_stats()
+
+
+
+
+

Example

+

An example code can be found here .

+
+
+
+

Logging from a UDF

+

From time to time, when things are not working as expected, +you may want to log some additional debug information from your UDF, inspect the data that is being processed, +or log warnings. +This can be done using the inspect() function.

+

For example: to discover the shape of the data cube chunk that you receive in your UDF function:

+
+
Sample UDF code with inspect() logging
+
from openeo.udf import inspect
+import xarray
+
+def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
+    inspect(data=[cube.shape], message="UDF logging shape of my cube")
+    cube.values = 0.0001 * cube.values
+    return cube
+
+
+
+

After the batch job is finished (or failed), you can find this information in the logs of the batch job. +For example (as explained at Batch job logs), +use BatchJob.logs() in a Jupyter notebook session +to retrieve and filter the logs interactively:

+_images/logging_arrayshape.png +

Which reveals in this example a chunking shape of [3, 256, 256].

+
+

Note

+

Not all kinds of data (types) are accepted/supported by the data argument of inspect, +so you might have to experiment a bit to make sure the desired debug information is logged as desired.

+
+
+
+

openeo.UDF API and usage changes in version 0.13.0

+

Prior to version 0.13.0 of the openEO Python Client Library, +loading and working with UDFs was a bit inconsistent and cumbersome.

+
    +
  • The old openeo.UDF() required an explicit runtime argument, which was usually "Python". +In the new openeo.UDF, the runtime argument is optional, +and it will be auto-detected (from the source code or file extension) when not given.

  • +
  • The old openeo.UDF() required an explicit data argument, and figuring out the correct +value (e.g. something like {"from_parameter": "x"}) required good knowledge of the openEO API and processes. +With the new openeo.UDF it is not necessary anymore to provide +the data argument. In fact, while the data argument is only still there for compatibility reasons, +it is unused and it will be removed in a future version. +A deprecation warning will be triggered when data is given a value.

  • +
  • DataCube.apply_dimension() has direct UDF support through +code and runtime arguments, preceding the more generic and standard process argument, while +comparable methods like DataCube.apply() +or DataCube.reduce_dimension() +only support a process argument with no dedicated arguments for UDFs.

    +

    The goal is to improve uniformity across all these methods and use a generic process argument everywhere +(that also supports a openeo.UDF object for UDF use cases). +For now, the code, runtime and version arguments are still present +in DataCube.apply_dimension() +as before, but usage is deprecated.

    +

    Simple example to sum it up:

    +
    udf_code = """
    +...
    +def apply_datacube(cube, ...
    +"""
    +
    +# Legacy `apply_dimension` usage: still works for now,
    +# but it will trigger a deprecation warning.
    +cube.apply_dimension(code=udf_code, runtime="Python", dimension="t")
    +
    +# New, preferred approach with a standard `process` argument.
    +udf = openeo.UDF(udf_code)
    +cube.apply_dimension(process=udf, dimension="t")
    +
    +# Unchanged: usage of other apply/reduce/... methods
    +cube.apply(process=udf)
    +cube.reduce_dimension(reducer=udf, dimension="t")
    +
    +
    +
  • +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/udp.html b/udp.html new file mode 100644 index 000000000..41d9fad86 --- /dev/null +++ b/udp.html @@ -0,0 +1,606 @@ + + + + + + + + User-Defined Processes (UDP) — openEO Python Client 0.35.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

User-Defined Processes (UDP)

+
+

Code reuse with user-defined processes

+

As explained before, processes can be chained together in a process graph +to build a certain algorithm. +Often, you have certain (sub)chains that reoccur in the same process graph +of even in different process graphs or algorithms.

+

The openEO API enables you to store such (sub)chains +on the back-end as a so called user-defined process. +This allows you to build your own library of reusable building blocks.

+
+

Warning

+

Do not confuse user-defined processes (sometimes abbreviated as UDP) with +user-defined functions (UDF) in openEO, which is a mechanism to +inject Python or R scripts as process nodes in a process graph. +See User-Defined Functions (UDF) explained for more information.

+
+

A user-defined process can not only be constructed from +pre-defined processes provided by the back-end, +but also other user-defined processes.

+

Ultimately, the openEO API allows you to publicly expose your user-defined process, +so that other users can invoke it as a service. +This turns your openEO process into a web application +that can be executed using the regular openEO +support for synchronous and asynchronous jobs.

+
+
+

Process Parameters

+

User-defined processes are usually parameterized, +meaning certain inputs are expected when calling the process.

+

For example, if you often have to convert Fahrenheit to Celsius:

+
c = (f - 32) / 1.8
+
+
+

you could define a user-defined process fahrenheit_to_celsius, +consisting of two simple mathematical operations +(pre-defined processes subtract and divide).

+

We can represent this in openEO’s JSON based format as follows +(don’t worry too much about the syntax details of this representation, +the openEO Python client will hide this usually):

+
{
+    "subtract32": {
+        "process_id": "subtract",
+        "arguments": {"x": {"from_parameter": "fahrenheit"}, "y": 32}
+    },
+    "divide18": {
+        "process_id": "divide",
+        "arguments": {"x": {"from_node": "subtract32"}, "y": 1.8},
+        "result": true
+    }
+}
+
+
+

The important point here is the parameter reference {"from_parameter": "fahrenheit"} in the subtraction. +When we call this user-defined process we will have to provide a Fahrenheit value. +For example with 70 degrees Fahrenheit (again in openEO JSON format here):

+
{
+    "process_id": "fahrenheit_to_celsius",
+    "arguments" {"fahrenheit": 70}
+}
+
+
+
+

Declaring Parameters

+

It’s good style to declare what parameters your user-defined process expects and supports. +It allows you to document your parameters, define the data type(s) you expect +(the “schema” in openEO-speak) and define default values.

+

The openEO Python client lets you define parameters as +Parameter instances. +In general you have to specify at least the parameter name, +a description and a schema (to declare the expected parameter type). +The “fahrenheit” parameter from the example above can be defined like this:

+
from openeo.api.process import Parameter
+
+fahrenheit_param = Parameter(
+    name="fahrenheit",
+    description="Degrees Fahrenheit",
+    schema={"type": "number"}
+)
+
+
+

To simplify working with parameter schemas, the Parameter class +provides a couple of helpers to create common types of parameters. +In the example above, the “fahrenheit” parameter (a number) can also be created more compactly +with the Parameter.number() helper:

+
fahrenheit_param = Parameter.number(
+    name="fahrenheit", description="Degrees Fahrenheit"
+)
+
+
+

Some useful parameter helpers (class methods of the Parameter class):

+ +

Consult the documentation of these helper class methods for additional features. +For example, declaring a default value for an integer parameter:

+
size_param = Parameter.integer(
+    name="size", description="Kernel size", default=4
+)
+
+
+
+
+

More advanced parameter schemas

+

While the helper class methods of Parameter (discussed above) +cover the most common parameter usage, +you also might need to declare some parameters with a more special or specific schema. +You can do that through the schema argument +of the basic Parameter() constructor. +This “schema” argument follows the JSON Schema draft-07 specification, +which we will briefly illustrate here.

+

Basic primitives can be declared through a (required) “type” field, for example: +{"type": "string"} for strings, {"type": "integer"} for integers, etc.

+

Likewise, arrays can be defined with a minimal {"type": "array"}. +In addition, the expected type of the array items can also be specified, +e.g. an array of integers:

+
{
+    "type": "array",
+    "items": {"type": "integer"}
+}
+
+
+

Another, more complex type is {"type": "object"} for parameters +that are like Python dictionaries (or mappings). +For example, to define a bounding box parameter +that should contain certain fields with certain type:

+
{
+    "type": "object",
+    "properties": {
+        "west": {"type": "number"},
+        "south": {"type": "number"},
+        "east": {"type": "number"},
+        "north": {"type": "number"},
+        "crs": {"type": "string"}
+    }
+}
+
+
+

Check the documentation and examples of JSON Schema draft-07 +for even more features.

+

On top of these generic types, the openEO API also defines a couple of custom (sub)types +in the openeo-processes project +(see the meta/subtype-schemas.json listing). +For example, the schema of an openEO data cube is:

+
{
+    "type": "object",
+    "subtype": "datacube"
+}
+
+
+
+
+
+

Building and storing user-defined process

+

There are a couple of ways to build and store user-defined processes:

+ +
+

Through “process functions”

+

The openEO Python Client Library defines the +official processes in the openeo.processes module, +which can be used to build a process graph as follows:

+
from openeo.processes import subtract, divide
+from openeo.api.process import Parameter
+
+# Define the input parameter.
+f = Parameter.number("f", description="Degrees Fahrenheit.")
+
+# Do the calculations, using the parameter and other values
+fahrenheit_to_celsius = divide(x=subtract(x=f, y=32), y=1.8)
+
+# Store user-defined process in openEO back-end.
+connection.save_user_defined_process(
+    "fahrenheit_to_celsius",
+    fahrenheit_to_celsius,
+    parameters=[f]
+)
+
+
+

The fahrenheit_to_celsius object encapsulates the subtract and divide calculations in a symbolic way. +We can pass it directly to save_user_defined_process().

+

If you want to inspect its openEO-style process graph representation, +use the to_json() +or print_json() method:

+
>>> fahrenheit_to_celsius.print_json()
+{
+  "process_graph": {
+    "subtract1": {
+      "process_id": "subtract",
+      "arguments": {
+        "x": {
+          "from_parameter": "f"
+        },
+        "y": 32
+      }
+    },
+    "divide1": {
+      "process_id": "divide",
+      "arguments": {
+        "x": {
+          "from_node": "subtract1"
+        },
+        "y": 1.8
+      },
+      "result": true
+    }
+  }
+}
+
+
+
+
+

From a parameterized data cube

+

It’s also possible to work with a DataCube directly +and parameterize it. +Let’s create, as a simple but functional example, a custom load_collection +with hardcoded collection id and band name +and a parameterized spatial extent (with default):

+
spatial_extent = Parameter(
+    name="bbox",
+    schema="object",
+    default={"west": 3.7, "south": 51.03, "east": 3.75, "north": 51.05}
+)
+
+cube = connection.load_collection(
+    "SENTINEL2_L2A_SENTINELHUB",
+    spatial_extent=spatial_extent,
+    bands=["B04"]
+)
+
+
+

Note how we just can pass Parameter objects as arguments +while building a DataCube.

+
+

Note

+

Not all DataCube methods/processes properly support +Parameter arguments. +Please submit a bug report when you encounter missing or wrong parameterization support.

+
+

We can now store this as a user-defined process called “fancy_load_collection” on the back-end:

+
connection.save_user_defined_process(
+    "fancy_load_collection",
+    cube,
+    parameters=[spatial_extent]
+)
+
+
+

If you want to inspect its openEO-style process graph representation, +use the to_json() +or print_json() method:

+
>>> cube.print_json()
+{
+  "loadcollection1": {
+    "process_id": "load_collection",
+    "arguments": {
+      "id": "SENTINEL2_L2A_SENTINELHUB",
+      "bands": [
+        "B04"
+      ],
+      "spatial_extent": {
+        "from_parameter": "bbox"
+      },
+      "temporal_extent": null
+    },
+    "result": true
+  }
+}
+
+
+
+
+

Using a predefined dictionary

+

In some (advanced) situation, you might already have +the process graph in dictionary format +(or JSON format, which is very close and easy to transform). +Another developer already prepared it for you, +or you prefer to fine-tune process graphs in a JSON editor. +It is very straightforward to submit this as a user-defined process.

+

Say we start from the following Python dictionary, +representing the Fahrenheit to Celsius conversion we discussed before:

+
fahrenheit_to_celsius = {
+    "subtract1": {
+        "process_id": "subtract",
+        "arguments": {"x": {"from_parameter": "f"}, "y": 32}
+    },
+    "divide1": {
+        "process_id": "divide",
+        "arguments": {"x": {"from_node": "subtract1"}, "y": 1.8},
+        "result": True
+    }}
+
+
+

We can store this directly, taking into account that we have to define +a parameter named f corresponding with the {"from_parameter": "f"} argument +from the dictionary above:

+
connection.save_user_defined_process(
+    user_defined_process_id="fahrenheit_to_celsius",
+    process_graph=fahrenheit_to_celsius,
+    parameters=[Parameter.number(name="f", description="Degrees Fahrenheit")]
+)
+
+
+
+
+

Store to a file

+

Some use cases might require storing the user-defined process in, +for example, a JSON file instead of storing it directly on a back-end. +Use build_process_dict() to build a dictionary +compatible with the “process graph with metadata” format of the openEO API +and dump it in JSON format to a file:

+
import json
+from openeo.rest.udp import build_process_dict
+from openeo.processes import subtract, divide
+from openeo.api.process import Parameter
+
+fahrenheit = Parameter.number("f", description="Degrees Fahrenheit.")
+fahrenheit_to_celsius = divide(x=subtract(x=fahrenheit, y=32), y=1.8)
+
+spec = build_process_dict(
+    process_id="fahrenheit_to_celsius",
+    process_graph=fahrenheit_to_celsius,
+    parameters=[fahrenheit]
+)
+
+with open("fahrenheit_to_celsius.json", "w") as f:
+    json.dump(spec, f, indent=2)
+
+
+

This results in a JSON file like this:

+
{
+  "id": "fahrenheit_to_celsius",
+  "process_graph": {
+    "subtract1": {
+      "process_id": "subtract",
+       ...
+  "parameters": [
+    {
+      "name": "f",
+      ...
+
+
+
+
+
+

Evaluate user-defined processes

+

Let’s evaluate the user-defined processes we defined.

+

Because there is no pre-defined +wrapper function for our user-defined process, we use the +generic openeo.processes.process() function to build a simple +process graph that calls our fahrenheit_to_celsius process:

+
>>> pg = openeo.processes.process("fahrenheit_to_celsius", f=70)
+>>> pg.print_json(indent=None)
+{"process_graph": {"fahrenheittocelsius1": {"process_id": "fahrenheit_to_celsius", "arguments": {"f": 70}, "result": true}}}
+
+>>> res = connection.execute(pg)
+>>> print(res)
+21.11111111111111
+
+
+

To use our custom fancy_load_collection process, +we only have to specify a temporal extent, +and let the predefined and default values do their work. +We will use datacube_from_process() +to construct a DataCube object +which we can process further and download:

+
cube = connection.datacube_from_process("fancy_load_collection")
+cube = cube.filter_temporal("2020-09-01", "2020-09-10")
+cube.download("fancy.tiff", format="GTiff")
+
+
+

See Construct DataCube from process for more information on datacube_from_process().

+
+
+

UDP Example: EVI timeseries

+

In this UDP example, we’ll build a reusable UDP evi_timeseries +to calculate the EVI timeseries for a given geometry. +It’s a simplified version of the EVI workflow laid out in Example use case: EVI map and timeseries, +focussing on the UDP-specific aspects: defining and using parameters; +building, storing, and finally executing the UDP.

+
import openeo
+from openeo.api.process import Parameter
+
+# Create connection to openEO back-end
+connection = openeo.connect("...").authenticate_oidc()
+
+# Declare the UDP parameters
+temporal_extent = Parameter(
+    name="temporal_extent",
+    description="The date range to calculate the EVI for.",
+    schema={"type": "array", "subtype": "temporal-interval"},
+    default =["2018-06-15", "2018-06-27"]
+)
+geometry = Parameter(
+    name="geometry",
+    description="The geometry (a single (multi)polygon or a feature collection of (multi)polygons) of to calculate the EVI for.",
+    schema={"type": "object", "subtype": "geojson"}
+)
+
+# Load raw SENTINEL2_L2A data
+sentinel2_cube = connection.load_collection(
+    "SENTINEL2_L2A",
+    temporal_extent=temporal_extent,
+    bands=["B02", "B04", "B08"],
+)
+
+# Extract spectral bands and calculate EVI with the "band math" feature
+blue = sentinel2_cube.band("B02") * 0.0001
+red = sentinel2_cube.band("B04") * 0.0001
+nir = sentinel2_cube.band("B08") * 0.0001
+evi = 2.5 * (nir - red) / (nir + 6.0 * red - 7.5 * blue + 1.0)
+
+evi_aggregation = evi.aggregate_spatial(
+    geometries=geometry,
+    reducer="mean",
+)
+
+# Store the parameterized user-defined process at openEO back-end.
+process_id = "evi_timeseries"
+connection.save_user_defined_process(
+    user_defined_process_id=process_id,
+    process_graph=evi_aggregation,
+    parameters=[temporal_interval, geometry],
+)
+
+
+

When this UDP evi_timeseries is successfully stored on the back-end, +we can use it through datacube_from_process() +to get the EVI timeseries of a desired geometry and time window:

+
time_window = ["2020-01-01", "2021-12-31"]
+geometry = {
+    "type": "Polygon",
+    "coordinates": [[[5.1793, 51.2498], [5.1787, 51.2467], [5.1852, 51.2450], [5.1867, 51.2453], [5.1873, 51.2491], [5.1793, 51.2498]]],
+  }
+
+evi_timeseries = connection.datacube_from_process(
+    process_id="evi_timeseries",
+    temporal_extent=time_window,
+    geometry=geometry,
+)
+
+evi_timeseries.download("evi-aggregation.json")
+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file

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%0Aj%N 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 Uu~j}f@Ev68D%#4G&t8A}FVHY)fdBvi literal 0 HcmV?d00001 diff --git a/_static/images/batchjobs-webeditor-listing.png b/_static/images/batchjobs-webeditor-listing.png new file mode 100644 index 0000000000000000000000000000000000000000..4462f6d4206415cb09418e560ddc38f3ea2f45fd GIT binary patch literal 63115 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 literal 0 HcmV?d00001 diff --git a/_static/images/local/local_ndvi.jpg b/_static/images/local/local_ndvi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75c523dccbed143016ff1c8fc33c41b9f6cbd107 GIT binary patch literal 96185 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�fF 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 g+zNx}spK~^Ui>fC7XK{|@;};N{2$+g_P5=M^ literal 0 HcmV?d00001 diff --git a/_static/images/udf/apply-rescaled-histogram.png b/_static/images/udf/apply-rescaled-histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..07d97647d4e0438481f3bcc285af2548a55229fc GIT binary patch literal 5777 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 literal 0 HcmV?d00001 diff --git a/_static/images/udf/logging_arrayshape.png b/_static/images/udf/logging_arrayshape.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b8535efe4e4d9261585565c08b5807aea39375 GIT binary patch literal 65288 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 literal 0 HcmV?d00001 diff --git a/_static/images/vito-logo.png b/_static/images/vito-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..64974f447eff3d59c3d13d7994c5a1acf1a8be8a GIT binary patch literal 8365 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 ZSIR!Tih}+U#5mwb`H7lB;bW6`{|}&iuCxFE literal 0 HcmV?d00001 diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 000000000..c7fe6c6fa --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..d96755fdaf8bb2214971e0db9c1fd3077d7c419d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu=nj kDsEF_5m^0CR;1wuP-*O&G^0G}KYk!hp00i_>zopr08q^qX#fBK literal 0 HcmV?d00001 diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..7107cec93a979b9a5f64843235a16651d563ce2d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu>-2 m3q%Vub%g%s<8sJhVPMczOq}xhg9DJoz~JfX=d#Wzp$Pyb1r*Kz literal 0 HcmV?d00001 diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 000000000..0d49244ed --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #333333 } /* Generic.Output */ +.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070a0 } /* Literal.String */ +.highlight .na { color: #4070a0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60add5 } /* Name.Constant */ +.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287e } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #bb60d5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sa { color: #4070a0 } /* Literal.String.Affix */ +.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070a0 } /* Literal.String.Char */ +.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ +.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ +.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #c65d09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #06287e } /* Name.Function.Magic */ +.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ +.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ +.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 000000000..2c774d17a --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 000000000..8a96c69a1 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '