From 843a4f482f9102dc34125ae42428c3f369162c0d Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 11 Sep 2024 12:41:47 +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 -> 2273426 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 -> 276272 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 -> 88055 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 -> 27086 bytes .doctrees/development.doctree | Bin 0 -> 81261 bytes .doctrees/environment.pickle | Bin 0 -> 1384292 bytes .doctrees/index.doctree | Bin 0 -> 8432 bytes .doctrees/installation.doctree | Bin 0 -> 23170 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 -> 147679 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 | 148 + _modules/openeo/api/logs.html | 229 + _modules/openeo/api/process.html | 532 ++ _modules/openeo/extra/job_management.html | 839 +++ .../spectral_indices/spectral_indices.html | 620 ++ _modules/openeo/internal/graph_building.html | 573 ++ _modules/openeo/metadata.html | 869 +++ _modules/openeo/processes.html | 6173 ++++++++++++++++ _modules/openeo/rest/_datacube.html | 457 ++ _modules/openeo/rest/connection.html | 2256 ++++++ _modules/openeo/rest/conversions.html | 263 + _modules/openeo/rest/datacube.html | 2954 ++++++++ _modules/openeo/rest/graph_building.html | 208 + _modules/openeo/rest/job.html | 751 ++ _modules/openeo/rest/mlmodel.html | 257 + _modules/openeo/rest/udp.html | 265 + _modules/openeo/rest/userfile.html | 242 + _modules/openeo/rest/vectorcube.html | 771 ++ _modules/openeo/testing.html | 170 + _modules/openeo/testing/results.html | 524 ++ _modules/openeo/udf/debug.html | 157 + _modules/openeo/udf/run_code.html | 427 ++ _modules/openeo/udf/structured_data.html | 174 + _modules/openeo/udf/udf_data.html | 283 + _modules/openeo/udf/udf_signatures.html | 223 + _modules/openeo/udf/xarraydatacube.html | 526 ++ _modules/openeo/util.html | 828 +++ _sources/api-processbuilder.rst.txt | 87 + _sources/api-processes.rst.txt | 68 + _sources/api.rst.txt | 168 + _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 | 16 + _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 | 198 + _sources/development.rst.txt | 420 ++ _sources/index.rst.txt | 75 + _sources/installation.rst.txt | 127 + _sources/machine_learning.rst.txt | 118 + _sources/process_mapping.rst.txt | 332 + _sources/processes.rst.txt | 465 ++ _sources/udf.rst.txt | 702 ++ _sources/udp.rst.txt | 527 ++ _static/alabaster.css | 663 ++ _static/basic.css | 925 +++ _static/custom.css | 139 + _static/doctools.js | 156 + _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 | 199 + _static/minus.png | Bin 0 -> 90 bytes _static/plus.png | Bin 0 -> 90 bytes _static/pygments.css | 75 + _static/searchtools.js | 620 ++ _static/sphinx_highlight.js | 154 + api-processbuilder.html | 196 + api-processes.html | 4557 ++++++++++++ api.html | 6508 +++++++++++++++++ auth.html | 665 ++ basics.html | 527 ++ batch_jobs.html | 446 ++ best_practices.html | 213 + changelog.html | 1262 ++++ configuration.html | 239 + cookbook/ard.html | 234 + cookbook/index.html | 225 + cookbook/job_manager.html | 443 ++ 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 | 301 + development.html | 519 ++ genindex.html | 1744 +++++ index.html | 374 + installation.html | 231 + lib/openeo/__init__.py | 26 + lib/openeo/_version.py | 1 + lib/openeo/api/__init__.py | 3 + lib/openeo/api/logs.py | 99 + lib/openeo/api/process.py | 363 + 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 | 667 ++ 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 | 422 ++ 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 | 116 + lib/openeo/internal/warnings.py | 92 + 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 | 321 + lib/openeo/rest/_testing.py | 217 + 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 | 938 +++ lib/openeo/rest/auth/testing.py | 292 + lib/openeo/rest/connection.py | 1964 +++++ lib/openeo/rest/conversions.py | 124 + lib/openeo/rest/datacube.py | 2584 +++++++ lib/openeo/rest/graph_building.py | 78 + lib/openeo/rest/job.py | 546 ++ lib/openeo/rest/mlmodel.py | 118 + lib/openeo/rest/rest_capabilities.py | 54 + lib/openeo/rest/service.py | 58 + lib/openeo/rest/udp.py | 123 + lib/openeo/rest/userfile.py | 100 + lib/openeo/rest/vectorcube.py | 593 ++ 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 | 297 + lib/openeo/udf/structured_data.py | 47 + lib/openeo/udf/udf_data.py | 135 + lib/openeo/udf/udf_signatures.py | 87 + lib/openeo/udf/xarraydatacube.py | 381 + lib/openeo/util.py | 686 ++ machine_learning.html | 244 + objects.inv | Bin 0 -> 5725 bytes process_mapping.html | 609 ++ processes.html | 539 ++ py-modindex.html | 267 + search.html | 144 + searchindex.js | 1 + udf.html | 897 +++ udp.html | 606 ++ 218 files changed, 82089 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/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/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..5de282b69 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 6e0e07ba1375d9503c45f70c1b610793 +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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

6vPcFaee9$^h6N>B(%2!K-$K zYJX{K6KYr}CnmmsRjb=ytL2fhY?0LH_O0G9?`;Jd$%8lu3w5=HtRT~opsEGPFef~$Ap2q4bTd>IqriVwiV$aa+#my$bMlQ zB+r2^Hc!xJj@3#NDDmA;=%Vln-7bRjGg4?n7`U543V22eRl-B$qL3W9O$v1zk(;2) z4Dp+$hWeMG!C!^f;HJS18EJ4!7`U4T3V22u+!-Ds7Y*ddd1w%B%bhglB&z%#52c-T z51$`6-2HcPGuy;n{ChC_4&KE-4-}Ml@h`(){4So<3iN$^L10zHYoaj?$IkZ==~?dN zm&-f3P}{`gVuh$NRw|Y^mWxq&q_7Dt=*VTI9U^b75;xi`RHK#a z&x3D6_)`O1G2&I$T2ve^jBYB$)|57T8yx>IFODlez#a?NPG<{pc#3+`T}9pN3w2QI znx%R}3wHX+%<>O;W~nV2&ta#KMI(Qyts_y##J99BY1Z+{KJZD_Hi~z&4Ts2+ix-#) zDyj$mL^8C#HU^DcI@@<&=pk`gTK&Q2C(Y<&~y!!?VkQqLGs)D%4{)< z@Pt)GpKj4KSkdRoE_io=tH-30#RcU?^J?(`;5D+k&joltgJ%?n;Z;Svn&N|s_>WES zp804rSQ{T5LK+R$w&JUcSDbUnDJS)HAKx2YP}>4d<7#PR6XaSQ9LNyWT7&T4@@N6x zHHOC;8`1IICuC}OLidTi(Pd-6wNbsiX*dM%q-+gOaBUy^a-ghM_Gd%M)2uxZrjW`C zJXKH#whOP+hM(DfG8)aMHXlchUr|Py`d&~Lq=p}_>Bu9>m*Fa;v2T{6q}vtNB00jS zkuNpV{1Smi4uBer0v^@^bR{;vY?DY!0a-B5@M;FC+dd~S4u@DX+Bf?(|T?xW7=A64Qz8Aqo zR&2uwipXvZaujEJ74;58pu}zBRn$B2H~OLt^RMMY?%M%Y$RHBf%^!rWok6buRn&MB z?|#iSWP90rn4e^ROLZ&)S(={UwS~9W5!-IM>FL?tuVYWWjjGTT>>zJ z!q6JEj5W5paM>sruCIoAfJ>txINb3C=4(>42X#d%4MiF@Xb+&FZZ1@vZ!jn9LW(OB z-woP+5adq_ZCc@bQeZA|I^$=`>m+CYBa?7C0~QNieQ+vYujHM;uI*;w&A45&-O_#P>d|qH^BJGrA)0h zD4rphjvRMqXU1{ro<6I66tCwfZhuejO=XKqnaY9U3?+z%4Q8zp#Ffl=P1jO4wvX5* zN9+XluCD}fjL_YeQd0tcro77q#6o%%(1JXc=&n87GMJvFJCJSsh7O|pb?u}3_Ku-@ zKfTBpE8*P?x(PvtjeV|N`IXpMQ(#JLLQ!xiv5qz;`IOk8o@8_#8zf%@JuS70_#JUt z`A^k+D&m!Uf=x-dj)-mu3Y|0|T?*!+!U$N3nrO$9RwQA}DZP9g0%MLF11ouNbl%43 zqS`2=L8EnPuu!~qSqbd2LtsiDDm8k$5r|e1qv3+ELxC0DAP7Ezako&fgNc1hxj77W z-a@fi-4->s)S^dmF_EbGwX-tLvvMh)z~V-S}BzhM+bJVqlL+y-@A zn@zNC!MwByQdqnSb%>coOPHMT;CM3{Eo}z-)L^YS>{MV=9BW?>GNna8t*m}Ua9=`M z#m`ip5M}lBHkDOCJnD|hYW4sLQf$pf`NU6GZTWEtr=Q{E{CeXrthw_InoDo|iNFpA zKoJ?x8-E8~$(_3WGd7Eb=!asSNxc!jQ!?^nfu6WP9T%$S{47TGG-yI(-hTK?hqT4n z0IN-HaXxe%tIj(GU zZ~{;!)yLJBQkXe$Tm?bus%Cp+I0RKL3c9&620%V5nx^~Z_PV IuZ?pogoNt9dQ< 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 wlI{qriuTno<-v^CC~TleO~OgMc|qC1&CCJoP)@ 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 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 zcmeHw36xxCb*5!&>(b^08_Vl+dr^1O)oL$*XUCF@s;i26 zRgyXk#AITSW?r_TSdswQCM*fVN#+b8gvp#C1QJ4+Kp0?#I3aOJ$dKWLoIo-RnZtbd z{`+55ud7NeA=~nSQrD~ZZ}-3ZfA9UDT=&*B7p=L7{!6ySwL-CSaw5#x+?TURal0J24T=hRsF+-4=6UqgbuX<_cl05!I(!g$+-%l1$iW)Qj`Y2BttmUQ*1* z=swwyD~EB6hxmJ4C0r7JZOk`o=F5DTJ64Yt#5?$-m~SOlxua#}Xsu?lp%E@p+g2fy ztSuKSQR|U4$@*G|^)yTM%7t}>^#qC${J5@BY?LGGr{vb% z4)lVsk`H$0a@A&~5gZ6B;bO#FMjfF2+Ue*dZ9uQho@((%7lOiz3O5xlDGVPh+<34# z9~LgfUKB1zpD)1wufqSY#{aK*qSY+iP#DGQw~5yaHy3VvWF4<-2#^dH>tU^6Pye&8 z1ju^3b1e5M&k z^@+?hx|6e@#&Ipm6&H$895hZ;gA?I0{-_2P9${A04>cB8TbN9qdkZtTu&Z}mxTi48 zIIvOS1mnV{`DVEsHD=3zyM+w~&xh!j*MM;puf?K2ejnzvonquX7a3cZ?SU=YBGjWQTO@>Y`m8arbNSfEi8suJ7KOQj>y_B5fuc-!+H@)1vq3FS1a*& zCT`{mq#hI-aZoNU0h98<7*w-nz1UbD!&Jh!39wIO9zbh}6XQW~fmICXjGC69j*zxs zh>TYm26J85ocN-U~%`&JosyD)71uUDE z0-=V!HQOf`Oz6LK0dtLQV!W(vM>Gz4TV?h9 zdv?$1>gewR2Y(xQH<|RY)z(n%B0jDuCu7h-O?ab15UcqY2bt-fRX{Ln4(<|QV@S?u zZ^I@FA=P>|NEEfY`g-rn`CAS6%iA&B>e8Lv|Iz^7Y*T@n(niIOtq%TWkU8Bs2wfQ_ z=uEo1j3;|Y>A=@1W@t0Pgnn506J7I8YV3tu$w#+zM`Bak~hH z3)wv|Z|ZD@kR8jWU=2nxY|=b<*s%!wvKSl5UOU$#n=LE70Ia|FT~iRe*@ECZG@eWv z@jYP!WtQ3E$Z)Azs0@z>!==eW<@P)7G*Y|&hDth@hB5y%0bO`+;eE9CN*dp0&D^MT zZ#mkWk71g^8pXE3R#%&BRuoj{OJwWJ&6#JA2`8WllC^4!NXC~k{4K6bg48$xt)6r} z*5`w8p3sS8rwy$&YfuzzXI@SdJh4#h+Ml3uq82sYMb4Asg6{B1cny`tGrcydNYGq2l0?uFT6e#ca> zgD`r>>lFY8Y^`zmaG$t5Zq{HHoor)j%MyFe5?-}JQBTD{t+=Bnk1 z%x<))Wb;uCzvPQ?t{KOqDl1`8sFHWB4##W*Tm%|xaDsKnGO3!-cN^7eun^XR0^DCv ztic!9_|JhO!Ey?`>4OF3FcM~pj^I&)849diXqHLEZNd!$FD4w2&}Esu;PnRljWikZ zi;{$5<(JH^eTS5VITQ2*r)QpgCqe3&C*SqFgq6Y46B_4wamFgqd6$K)z|u#R^i&q1 zb|TH(zV@Pv-~q)B10~1C^6;_2HH*y~8-o|NaUzN;Q<=M^`!bx>Rt;=6ua%o*fZ7Owl4=SZIT=p-yg99;;_EG#c#iH*Wi zoXPAr<{o!R#L*5GBJ^C8C%T0p8kHBu;Q&{@Y3;UwXeSq^@<9_ZQddwSqvIh|3vY|m z&d__d27s>fXaseHOz&JSirLXEj7v0HAWyxmNQhmCmcn8=z&R118jnt74pqi(&|R1-jPtBC0P|J!MaE_U`ZoH@G(u?0IpT6;j+epT(683cY4}>b zcs$H42RJ{%wpSJ-K5szpIJSUg&X`Vn$}1Nq4B|Lr9H$dPCl@C_bP^#>zQ%*7kpo9B z$5nU|$dL_r31Z=1uuh&rg#POh9uWaVC*dilfuyRCF{c?~-SiH)@;p511pr?VOw1^Y zQ4AJh>F6edK~xXHa)e0$EMNwzd(kJ$h!fE~=2Jgj%)!UP5_iP%RtD26un^f|G8xiu zTH00OF*!I7*2D2xtt{F$>sFUNDY2h1Jj?3xZC;nD8fB?PZwDos2P>6Bn)g{<`m;3Z zkkWl@MMtsXm!aQNqTlXpIQcZ8To2rJ6*=o!Zw2Ez z8eVx{6)yYCyG_1YBmRyzVk(9g;1?^=92vcTC3C*x(KAoJXVhA&58d8K?;foAH+aqI zuu;cs^!^70jI@H3%ra$qLaRUhAXPW;spU0icRU`h69Rw5LnjM?FB|ihukdx7_k*=% zU-SA)-vUBUngOHd{=T%YH0WCM@z>WI3f+f!{VC5Yef|oC=zW1g-TN=Rxp0cSMd|>M z&`wGS%`|7`Fz5xXk+jmYr!`6}z5wfUprKooxas0SyurIWQArt8azXPKXN>1U^H&se z0q%$&JBJpRMLpXGqMipe^>mj@B$JdJBdg^p5Eq@=KqhN-Xl554zKRZ>q~s^pbq!8G z*o-y!jf8Cr`;pv+aVCU=5tcP$cxI32{2K4a2Mw=v~bGep^77A zBV5|f#&n*!_X0rA2%uV_eaT>GX_40L9%+@{ioH>;(`0*B1XkMcm51DjMS+{v059_f zNYyEoQ-Ws2YVnmF3B~F@eg0bRS(LNnu@jfC4r!cJJZT*xjMYKY z>mZes6yKy#nQ#6wP7z&f$Mgig)A{0gf$`R2eQ5Q@XQsh7iekknZsr@i(Tt=ju>;x* zQTlfTcuFR@DA`Q34+@?=35v1Y^5z{l?GdL)sWd8OsIbobI+#xwVkuMBj15RcV#pkH*Oc zJZLP}5O;eC4U}xcXLOPj-D0D4nt#S&T%~o2o63}@l$AoqF1?*5ClV!QFv->mlO(lV zNfUbir+Ro_sqyswkMsLRg->484GB6hP9GsYO>+lAl-_|qlxkGcPv6z)DZJ&kv~PX; zU=|vDo`&g~f;3WkAAau@2|kN=(<1pb5|lnfz1k@7XY`NH;tHj<<_MW3nW-_12Ja$3>Jimq_$+5Jf!&qTJYyD9&sw z99d6&trCu0g1!dCkxS9EUmRI+l^%M$8r%219=2Pt33~s#J-n~bgWebD(Oua1UTb14 zLf~|7Lny1pAOU3*+wN`ramW*)0wQNe>Z1@ypPE7Z4DFcoI@mCX~{0 zzD&o24*ftXfp{WQi;;G!!>htuYK0B-);vDZQevDN)SbLEVC5?n0n| zr{5UKHggm<(rz8Z69{)r+^li>&)C=!97aXTJM}OJaU2}H1hdFV2kmU2FoKYU#MrS= zL=+qamy_2?I;BKtosOlDF%@bQw$oIWDFqD)ZDLv2Y=o3^bLw40fFk@CiPW5mjgS!p zLcuyh9(4|B+F4F@nwknk?s0|#kxbyQ8y;;LPb2&y1!`I2>>6Gy50C3#5FW*Uk7hcU zM9)0;fy~HE=cL=auGNx73mQvBl%8wh+}acBDe}2#MKrYru6P4?*L6vx$S@b0En7xU zofqhCEystW6;Hm+qY4GCO%@JYJw4eEU8L4lJ{Tglb} z{6=N+9nFG$Q8fnwh)R%H)Bs5bpi&+PfU|N()ATf7d7ySmf&k+;DT^h!-YZ4nIG1Fs za<{4nh3i}cFH5WyLD}S&;#Fo)2BBt+OvYYw-mEm)wkRZLR!L4PdS3pdg{mJtuazNb zLP8>1J>Oh~nO@9iC2nOA&Kc!7Lf=FlTCw`_b(M`ksfFTf#46E|9=}gP@F_aNt?U5Y z?ZoR(1$!A*$n4KY!2?jD6fO7ow$UfP&=-}HKJh2I1?v+rrms~>PEnnP02X`6GM1G* z*Lx&?B}>J`Fta~tG(#up6r)e6$j~eiUrI_M>7bO4&t)m-Sc#%3=j(99BGwXA#F~U< z`9LGh@R=BC16`CA!MZEJIkbjf2Fa@wJ&JLtk?NdcT`N0usB3`2B~Vs44mYN(v3MDI zs7rKwS3$5h!uBZ(7J&U=s`C^< zTtiWmNGZuPuY`j390V|uZomH^HdOIf8pr#|8E9J6VN*pi_ZZy!Q?=zO%E+FYdjqkU zs5$``K||%u17Roj=yC#G70)@2-mVnkl;>z|K^=nKa!eYHWJ%k9@D}1Y6Osp3XHJ6o z=)|?scWF@ac}^zRSTyNKh!HzD@&9?VbagQ4th%_i42NZHIZHrnb6Bzh@*VV(CfrgS zRU44SVL2hz@*rKEm8X*-+RbXdiRwZJQBh!dit8)24hk36)ZjZz^3Qf~{wp9A?II9r zGP$bD_8ZLgH{3|Jv=D0=MDcYDWK^tMdr`5p4%Oq^T1mPUKwn8oD!m7bKV5n+KDBOC zdOyCo`p_HP6=;c++%io$55nZEL#zZLa>WsOfaFn=ETdjUei_4`-=-7Q}uff;tC)3V4%Zpx;L~?6#1( zgB42MgD!yLCkmb3droJ|GIX%{8jLBK}H^LMBfo_@EDVFI}l^R_ARgh1Er73 zuXSK387e(Mwh#9urPM^`l`1Qc_E@nn$Ym;OW2L(h`G`Ed8VR=(%)Y|-m^6=oIrN6! z(T_eGR=_N+!y3%K{>VW4+UmBi-#QS56{Pl1S}biS>2WBo*x8Q_kO~tObXY?+tBTHX zu^?k(fk^Qr^A81+C;}46;AkyQTBJsA$!<{jcn0*z>WfVC81yl1uM$XKCcp>kt@BF~OXxDaFYPJHCht#dwV- zEnb7|n&L{`(b1K9M8s+Y)5J&}XKK&y8x;EQeW4J!Q$MF$usiivZO75f5JC`G{JSE> z+Yv23P`A#KDkkjN zdkmHRDp_8kbbVuLRL&WnI9O+H2`AsoKKOtyV8tq6h2B|Yf!G4 zu)6YR-J4tV0~UXF`0`JK0v{3uQsFgXIwr>pA6@ARBMy29X6!N+I=EwQf>P^o20`2$ z2?IuBvs+1>T)KtXz8#o^M(cmo=oEs%;nE43FKgd1t+5d41?yao zuZ#XK9%vI4Rovb;0$3Bf&YM^&(^14it6{$L*Ut+?vcToV^p*PFQiLmc(3VvpMVeE_gr zr30dmxsWgh@VBPH!W`}++$}=wq%38yS6R!89407VhErE~h^TBI`)uh#6BHAKx``5O zaLK~hWrJ8L(qQK%Qx^IkG+B$olYXroh)3Ftk#Sk8_6(0|UEXO9jNkL@SiMDGk9+jx z9%&y~RX7RUHYeM2G`2wZo2)IAC)*B;odfQ9Z_X}r?)`xKHRxx+1MV`K_Hn>nx*E#Y z!P0jAaQJ;bwuT5EwHH&}m&7vbA z_Lv*@3+QV=V*DVQTEw_QW}JvANnN2AH1a3F90Ir<*{HmFwnI*(yU_a<Ge2;TDzt%#VYAS^mr&)XovU zbE_pkaemF(;#%L(!0wbBaeYUh zi&~!&XWKitT*N!pK;QNTO2q?ZMAGbp2)8{h{+xRg;ZIwrg+7OpA#wE(N-J17(KG*r zqhN9VDz3Nz1?#|IS#JiVMyPOt;4oYQhxKw@!2@Y)sJhFAaX6}?W8oPUN$wP&A*U{- z9O3jmLstdhYjvK^;z|6{2^YJAtV$1ftj>{@vz$YNsa!6Xbl`w4dW)y5kBiwy%>f?) zMq5US-rZd4mTeAsFs(ZjnOfxVfpZ?SDx!$9^7EjSd4Udsanj6@$271_W8~luWVN6% zP@;9Q&KRhvj-0N{9;EnDwIG_N$_G;pDRD-wr-J$6);~nr0W#!fBG)$O6I!*9l{4G$ zjDO%bz30<@Y9;H2^rc($dBu8{PrKfy%iED&QU6w#8mum_^}6hdM(mo^ZPY*oz=5|zO6<3Q0$F){soxlHnB5?u9m-VOhe=M51q{^ zSWw&Np^9~evr(}(g>UNxzFDmgc&$?jfvW-2cuWObU8ZSunlh#Jg14#(ttC!twVWM% zI?l<5KZD*->90GX(&4ir1Yl*Ze#b_&J<%5&l|e-5jDnO>;Z@o~y^BgDUvX6zLxKNs zKVte&&5x@(#n5MP_yL)?G#)JCct!dUwW7idRd7L>Cq#C>b^gxI!ps@Exx{d8g(oO3 z3y5OFxsq`YPM?jrTUh>Yz1?xW0!LPVm4e)iNAImP0pUWp&4#iFVrd7Hpd`tTdy51zHjgApa$Ap=8v1{!e@FXngYzeEzh{3>Ws{irBYy9v+jpxVv&JAmyfxf%c7 zg{NU$zfR6SVOX%EQJv+|g&eRLq@|_pm0})Qp0dz_Ux5L6oqW}2UD!ARw0wt?DOk#f zAYgW#tsqm1Z=sHQqGX4t3Z`lX%BYx3?m2^*Tioe00J7~Cu?`9QOs4;W*OSXBdQZP#ED+f^)MhS#t#`1| zvBee#$1IBeM~@CI6XB*6QBkStH?DxQ)_C9b#!IJr3I(GXGTndsxrh05Ckd!C74r=n z^7pDYHuQf1!@p-3roy`P4cZx@xqrvyT;}xSj$ThcWXBfv`qq$+ zS5|tP6g|X4-*$dN-D0jU)`%lxcC}fTY&wze~H=^lz=vO$fc8yJFS9D@vMi%*4(31us zhEPJumPN^3k8$0I=w!%FJea}FB~FhC9xYayCxa0b$t;W_`Z@x)Ci?;<0c2pr59hnC*1TSkb+FLSHfMC6aPBJNd+@@H~EO!krEoX(0%og**nY6bBKiW0C zr+oXJm6sm9GgrT}ansA``uNVxEVi&~n4&V25l`>SYK(Ntw>bXg^V6uBaqs;OcFE_e zh|b8cbY{sN!t*5*6r=9E zed6|=J47t!A*|?rlv0iAyWzUMPjbc!Fh@4B43<3mFc+i)^#3(bPCve~4IteZ31=ND zlN{LW(UUP2a36BO@pM$3!K@QL5E1;la)i{iwgXz22D-H}1P*MsC(s@lbtlo1B3di3 zD$9`uWLGG61KAf+^Do^D2w7oL+=+^vusRXp3P|A4ofw}*D{(BFZvJy^DYa#WYM-vK z%3uemAgVvY>`L2d6IBou%~w3JBiu%k7xC&XFj8EYm|!PM%pOnGXU*q7A`zeMZ#B&MTRNOKcUAc0cC#IkJZ?KbB_Jn6m5NTEf!<^w2| zRm}%SakDiuGNqyQ*~$o^uVA9KM>Jh2=0Gvsf~~vjTnjgFAh4ikZ*|lP3~j! zO|})76n?p;26h=h;^~BzN?W zn@px|sPjM57{Z>o)ZZ9uD&O&@;?k$I9D0mz{x0zC9?{y4?Pg}C`c z#5Z@p+{6uTK5zp`K!UlnwKLZG)3@TK1Co4){kRq@l>4#Op|aST&Gq!sDqXO!9eoYB z8#@Nr4X5mc5atda^v-*|O<|8h@B6Y(?8_Ol(+<1za?9#inUAtFtyLJt}U5^Z~ zT~2U)H{C0J&#vw!jw}fI%hIukqW77HVYXh7iK`|P zjcVubZEwyJX2wrTspB|H8q-u`1M$lz=wIWzHfD9ZB|b75+&e%#3Y0R8gR@o>xU|I? z-9o)(5A|Hn;OtqRr=Sb%r!M_#nvD?aH>3l`TEbJ_5;_X`9?Ipu5&aA(QhpLmS8xwn z68?01*wLK>Y_re5c90@K`^k#HMvVgor~S?D4X3$Z3#YZuESZF7K>CC_BJa%{(gNon z>Dg2fLh-@=8yfEg7TAyXLR}!xURL$MC;`k`m^o$pH{hxW^+OSIzUD_;6Tyh8XcJaY zd4dKN|!RcpR=Q7u>&MbQ&@&E@MJkf zSE&j{V7y=o;;uomRJia^FQ&@=d_;y6LP99<`gmAIB#GBKCqx`awKFne=wOk!7D~BO z-C$AAM_Vsc*MhCK9Ln;*;IO(TtH*c-%k#H$jy;?z(R!W5_&ao+bq>Q29B+c zlOn@mN=7UWQoMdHgR7mu8#sE)HvW|h(mM`6uy6XILpVs+iBY2FO;;12Ps0kmqX)z23*IC{(`*ymHq~nWbe4Tpj7)aOudXt@Z3o zg;Ia+%NL=q0U6E9(6nDh)92}z=hJjCcB?0fjN;*Mcy9&`_ipL^+MtuysbqXJt@=O2|KA`W@!#x<4 zh2mnf&c}JPlv2<`STUcT(4}>zX_lMnz<~qy(CvW(eEMxOF&MX5>AmGEm#X<}tk024 zpMcBHIy>iR1>1_}V`$l2RcSEI6DQO`_^ah1U@_Mg8Bn-QNKScxtPahOV^>Na#XR(( zIZaADf#Q~OXny-{xEm-y3&7$MNmRI#VseF3ilKlwU4(Jf$dsy9%Pi{X?C`42O)VMr z229JA^p*yJq#(KO z$Hpiz5PmbRgPRY8(JmtEsZ=fZCo9t;VBi>vcn5K~0HmOVzFK{m!Wp7vq(5c$^TEM< zNHShm=ciXSP>M93M8_BS$`1~PO$6nz9Q`^S1;C(c1@IRWrvxn zD6~9XhFh$o5Z3cl>6;?(5d(%Bbl|tZL*RHF4=JdE`{sCp%s$HA=0r1c`%-;OT7j6J znwFYP=d3d-Uqm-Wq7Jyovjki!2piqt}%}iu)r!xF0;7$ls z<19Ox0%p#sNn>Ny@NopUE!C`agaa)ohb!0teG)KZW8)c=$cT%G*8os?krm{q<8E>r zc8z)zWkxWJNXWCY>%`o0IdlNT0QE^4BcA!|Pug8MYNB8^vM zvhClC$MSEvCAdhdkwgAgm5b!4o$HR(V(uxf^Y^REFs1( zk5En$?a&C@)No*jDRD}#;(Zj32bU#1Kp>J7^PI1(^@mQKvl60pQaXR2_|jqAVl5{U z53{?CPV02;AQ=Kv=0srTY^~g48~@wet--B`Z+ShX$}{#n(%5W8B&1QCZP-8at7wl>Ff>}mbkS%}}0m-J=xQVK45(Yn*Ej)pbER?zVY8*Ym z_0a?Ozt`2mMj@u+!7whf`Q}n>obPE8e}?%y-6&tojniKgaBl4G*LUiUs;_6Ji7c=c z0`Zl-#TPVytUL>dC`61dT?ptv6vKovr}I3gFj%%Q9%QqG-1ZXa5<;*{sFVDQx-6Hv z#nl##Oy;Pe73$?o7tHXoCxE|?(j3GOShiTrP6ne;S0@-$7L0ZKy#Tmuz^_hZ06{6o zXD;lshEqUyggoc=Sx7rrwwp66d+f+*@^+bQQucU*HiwgYAAqGKjbDU@KvRj>5~B!n zBrT9wBZ4QZ2?EJRy;zD2SU~rjK_?d9y9TSWT}$b#TbNKn z6%iN*rHB(w9ynk?j&2AR?1PEL2}^k!(lfCL>m?pxx!RU9VPz9;vYuK!-RJd`PNH4C4Q`GSaUi&_?)KC#%5Sk^OxA0Cij$wXNIF))yC1R zZOYty^UV$)5x;e2A#HpNjqU$3uY(RUO!-nsREmeE0{V_8-P45iJKPRBp!EQc$n+)4 z7f*+Y%?^tyblj;jh3U6@f#mRsM)~{fH&9xNTi!8w$7FW$rP({~n!f$cskh`ml;hX!KAstrz zm~O#`Rqt>Qt3IJw|J35<*$VZ= zae}(dT#-AAE=Aw;CDTAiG@_l+6}dOepd6CCk|cYphCo|QoGs3()iw-iNLXz7U0yFM zOEDUkrPv28iSaA|r?qGwbiK))?k^~uAa!CSFoFb)^#SU2a*1hz0+kCKtM#dZA zaj01~Vq%mXgW3l>BqDN+!&@A8%LQ;X+RUjP5@CUK9|FnsBc*=c!&D=siU;8epp+H9 zQ_98{NXreq2CgU(%>7 znRj=S%ssEvnuos@gL?|YY&=rL^g%*cwOx*|3_1!+uE>%mD@8p^RXZv`h|dn`tJ{H; zG1TXRBsJm=`#m9lo~T5PojXRQ9F*aXUA0b& z&_Ib$<}2#7n-R-7@3w^Lc!mu~b2B(u3pi-C+|KWPmQ2{YeC3D+sW=`s64d^{~@$lsB8mI8wjkMiGh4 zDtrafKfwP{I-b&uVHwdM2y}?yCF1Mu^0ENd+?*1Cv&Fn9KMdxCL+V*HC!m?%_l->OW=qUZZPdUieQ&b*b{v&njD_4KgU)5<;x8k;pAw;pfGlrG2B z?;hmTthTy8SQGo9)tDI2islv1TL3!4i|8wZ&uhIz3!|0$K;J!yP@EssbWcac|M2C=DlS{N=AaaN5IH8)1f_7>|;01BfMh3&3C=^lAjyRqw zc}G>_s^TzM#ch*^wN@*W--`JjM{Yql@+sgES=erx&b#;uT%LowEH{hJ#BW{d!_Xq; zU(y@UW~0G>YJplI!;pSv_42D;FDvsZ8kcz0%yrmf+fci(jqWyDQ1oARkhE-FNPd7DBoen777QF_BbZH;RAFPL=ip@5`Xen+=p)$-3twY(_VCKJ<^s<>dUh=OHr zX^p5?a5pX>=C|}LBN*@B+Cqw*g2`Qwl(!*$)UghL8|7;g&zM|90KpCaC6H)PX zY6t#`@RlG8yq!#(%cy;L8?ljgYQ!BX_JvM|lC>2iC|j&*=7*uW8`qWCH-X5DkUDj^ z&zy-%xpJ@bP~a^Uct;?XzHU?$eag+Ox;FQFiFgSnr<-vvBP{(T$1*Jx#az4&U7r$rEl?#VvD=7rw)7gv$Pjt54)?|Mm>8cy}nW% zx*qEVzJozD(AK>NP1CooUBGo;o+stp{X<2DbIH?F_S!l>=)+<8pHp|tCKXbHDXc`J z?&D`krWkbc(YZIRfj6kI0`}0RF3{SA>hd>Ae~@jTX5>OM}<(8Oihy?$J?{Ph=!5 z&C0YI>w>>>wVC&SrV{MJ|EJ@daU&qyYP{D_fX+_mf-wO?o3|M6=3ASOU&Q|vE`Ors z8#9IX7T!mEQ)Qcevn!i<*@Lo5G7NKzMF?V-!KPH{r-Fc8^1I`DL^jGzWbNI9_F!%D zHCR}8`Rz*s6@>FGZ&hZK|GEF097i*cRzvbWNvDR4f)ZFPDn`*Sa43pjRLW_wQj`ZW zZG4%Wayvw$i5%e*LIk<=l<0Icv$slzFN9R=gS4!$9fVc>>${DmhI#V+dajpO+C`Je z^LG83R7NaP>4if6$;BnZWk!-a<;G^^{wulL?%6jR*eZP)el*L;tU6)zicsVNd3?AW z5!+Ztkpwb#+p(aPs6&MI1Bi5A^Pg*9^SyvW>9d%N_BGSQ#1k;v6kqdx3*Gq09{NbR zT3O8E=)A&sYaf==5-KR*XyqP}2ZS!9)uGs{*rU0Y6&ta9h$ae9i*Rjyh-!8aRMxhL>UuVR6%K45r^=OyC~1Dx0SCC zF>ddXUh&qFyhk)K$ncYo>(iL6E_y2+XR|$RtEX3cJ-NE7FT?4IEgG8@zRx_TNXLSS z4_&v8;BCDTnF9Rq$`ol0UG8T)R8>K}BL`NMvaMg8Ru_bqHH^WL~6%41^`^ylzvI&~x&P1@LEy^6yWy5OlEE%KF9l+uPm zrNKxZH#O0HPEmA>{zZW&$Y7i?qA7$Wx`x<+?jz)?R}ci1ORamJkekm{V#U&oW)SHN z4XhK@SSfId>d;mzhP^MIBRE;q`ZkZgT>f?Upb%Areb*oxVomSe-tS#oc(92=xm5B{6fSHOYg#Su_?wyANBRMBffVfD z)Gb)SzT#q=;*-}LrBd39W^3!ptJ4tKC6JT00oOCuYE7y{Q0CXXla0U4Mz zdDe$90Q!BsT%kgxVxeY^qI@=U53qHr*bJTphSsdaWN+XeJ#keRYy@I^QjKmDDV9T&6LUClMwlYd zy=3=Z7C4oy1X>*a-uWlZGHs=Cuu_&c9*$E2yFjI=lm(FPHz&|tcR}@yxuN4t^UjBJWW}m@@bDrS$g*iI+30d(U-TArPb?Kye)HI`%+ZTwl*~@8 zL!ywsD?R0fq7__nfRH>eGfu+MY!N;Rr0|6(3bcfJ{ZJW3$Xg{OhRc1qog1Nt_f|OV zAJ0^fzay?`m2zUEd7QNkc_d379t)}w*J2}W zggi#Je^-3hRRG{_o5ILcWR3p2MZV8tnNj6ayOnN21$sr}AUJx~QXet~I$>~l8JQZ0 z)gw@VFEGAUV|)_6lgTeX3xH~A z8|$~ar^If+#sE=fmr{qlN10vjjql3y{-QCqqOSp?F|I??^k|GTh%&o@)D;3WD`}_C z8)2P-2EFNFys(SK!cuJQA+Va&QVIG~2l`Y6lp1h0RREt6C$)188qKOUs*x>3csX{{ zjvBEd5x2VnTBp^{&2f*g{;m{?nWuPJy+7#n?ozvdo<$LgG#@6LgFWhuJtBxTDSyem z;r^{e)c1(lnIM_dePA46+u6f3wI}D%jD*<-u+0K&{bZDm7+9xojCSC)1?_9^W3 zF7OQqdj-5{F#ymNc)riG(3JF4pOm!Iz_d3?;!O~g)V)FB&B>Z9r(i!K9WG3OQZq1V zk27b>Jd~6$ncRCOBUl*w3m(Q=V(=PQ3>pj6$U#oiR`(5=7p(r@=k=dX5hMs{Ml1-| z*pa;cJiG6aeRO;mVS(HoBfGxk$}ThSwvzj2WKwgV-{%fCny(|KcBghkpg3IiIyG^ieyLxlJ_5&KrW|4KxSs<24$JF##YoeL*+AGLT*g(dV>4|X zYxn%V@$i46FHT2C-Cx%&_^5ji9d!>S8z_Sru4XG?IQjL5;9tbK=?I-V;_d|zqDVeR z$ES`%TDqk{B*LO1BSJ;F7@w-hh`R>I#tuesT*`Zb`Se@%2KTeu8G={11hKdoxfWFs zQaYZg0cOP1Eh_9mV$A=K7-&jC3ozHxcZteG z!rVS;qo@umNMRV)?J3jE)@^4JoVcbyp8k?q;aERV<|vpQx&^a_18x+|M_!4baIQ!v z!kbMf7{%IyCg~D0)RU%fTvl2zJc^=-n^e2EAufGp@*iO(2k{Mj!FGPs;{u%OOJK+~ z%Qf6?KZPI)zTcZ0I~+qK_2CfDRQUymP$y?}p{)+Ec^0q0Z0p^6llqC>IYdaP_7_N) z;!iw4@r{%ZDNY%=q={7(-f}|GNuz%?tvXC{wOw&%9vz)1Cm?l$F!e+D2NlWpbgBR) z4td>c^#;<7J6^BH_qb&^VqM}^5kWbJ5C&8f)nkrP0@(@5PaqWLf)|whGzC;uSIr|j zN2HlT_)+AeNV?*SK_5PP=pdu!MBCv|a_^adR~uv7yY^zS+5;Z8SeDi{$I>$9)h26) zt)3q9dP-&5oc>H>GsnK71DaSp`*ZA#6(1#x-Ya;T_@J`GIeWhoA4$6zm27 zUIKvo@g+WH?TS&v=7rM8WOJ6NOhNWs3}u+{>-r)J<$8OS0a7}yEAx(V{Q)@^NXj@} z-*`kYBSgjIks+0%f;Sek7G8!NX%%keTNz^Fv49SADq{m&va-5u=WgLypOR*Q*QE_* zahSJw+vIXVYc&=Zw4Pc${hZg+%3P4fmR!(W+Nok!t)A28Xz^rgj@DDFr~hy!b8Hvr zR?q$%)3H~f`wtPi|D*te;ndr^qLd=#ls7bg`UJY|hd;f>%q4yDiu8E$9ZBy=gM5eveO(0*e!LD(6MQ|WJ?ngu_;WT9u^0^g9oU}b18AT7oNaeKzR?tau zOD{80(7m~bm#wY;v%Zi@ZT%d9`+xdde`GLttOyGj&S#@*jck9{C$i0()%S*M3@(Fg z-5YgqUKQy>8a0?+Mc;tu%AVTC@h;F%LY?%J}w3 zxb7OR?@+0ET|-DvaV5Wgyq&F7bUnPQ`F{+ z^7z^$m+zWntGpX}R`zu;*{q&Ws!i))vQg$Zp#JVH_$-rBTgsi4*!2Q(1>SsxP;a%NNiMRW`JKJe6!A`>WDinmq~M;8N>U zvNdkhi#1dhT%wJN(c!h?NmS;oi)V>t&*sXwfDenoc(Q?hu(=*l?B{G5j!S^^;v`cD z%lhlOTFI7jSXpd>ECBI5zF!((B={N5xcvNHMPvqOd;M3R*O@(Vk;kPaFHBlC`^+YSVirgg_1ny~a2w-ohw>-9dQ$0MPzdo`ixs2!y zm{VB6QSMYc7mFq9zw1hx=J-90!rQpsUCQ3k=L5SAJnl%iu6|+NL z6yPt|z-eGlHAh#=X})kdF$!>5@3rC*;cgk#beI^|GD`x=EX_xGQ1CLH*F;eRV_b=< zIN`C`2<2f>UtL@s#IwFkx`2SI~Y!uuCV3Ds2u+AG_qDi=Jqe zK8%0D(#Iie@Z}@;CoFw-9e?>G{s~JrZ{RQ6@lRO#g!uAN{zApQ(S{0shg`HBU#Nh3 z$Tec{h03#p&13`3LJls(+4ZGsz*|Z$q>mgNiKRLE_+!kZ^hfmZ`}Fa9^znz75YM#q zDf;V^^zkLkizi(ABK`Gw`uGxN#PcqFk^cHTef$x9(DX~6rjI|MkKYT;hAYq9gUpuF-SqJ_ zc=bzPrH`-B$J6w2IW2V?ePrlk13sF`5CMkDTJydzXihO?PBThU)ggTL{4npb>^S9_XQdWu(hnpb$LM5{Z^D?81rI?XFORif3L=9Qc((JD^! z3QqIvPxH)A^Q=+0`*NyhetqFK-a7n4`&M|r*t7-OL*BH5v~z@Br9Yt$-ZcJ;u&nes z`ru9DzX;h%PxD67w(wuPX}8c<-ZcJ;H|=S9#+$}}@ut0w_K-J?|Kd%fYox`d(H)tL zoP1M>*fhGcgmILMf{RU~nrdRx=qOrj8Xb3wO{0Su-Zb`(HODce_7m|QI2L%`(tNe7*&@IC0`H-Id&uc=NR7&kE_eEe)=_V?AVaS7`;xyv z`GX*39hWqQO*x#0fbn4sP^C8PA>9|x-vSo%RO<+YLM^OAynwqaS zcH~DvT4}bvh`|X;zeOaNaz2-=lS8ho#o~UFyDGmWqv}0!g^W0&b1; z6@RnG6@O#3R{Y+6SNw1FFSKtZ& 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 zcmeHw3y@pad7daf7o>luc2hM2Qd8qU22-8hZiUT>!+( zxB!xt2WIT!6WHO^p(nmZQCwG+`9l4*d;02qQ?Rk7w4YGfByeJ|NC6s_=(T`*&Xt~V29r*mTFfrZlO@C z7d*ccY$??9?W$L6bw1vi_@2(Io$+9#=`Q;9b~EpFf;-S6Uo2G$O|RCu*a^0?d8<_M zGvWK2eJ|fC)oan~#f`;H#myHx~SbN-w271#Ii5Pxs1xmESomO{H>zU18erKUH}Htk?@%U$44o#Kw-?&7xMgT?j5 zJ;C$S#d_77ZnkSRuQ|O~Z(f?NH@uoxPdAoY#d&&-rM#bM`dHCJ z&#!Oiidz-<4gBub4}v`>>li5Ca$0rAtvMX(>GKZfnf7WS3`bXL6#^2gm{F@ZSA2-D zDt;RPRD2lQ@Cg2U6#wnTe-l`I@iAF+S~V|DVWOKc(UD*Td*LoL-A2*g+r?8QEN9B` z{Fa-ml>DOOI(ap5AV~MQPdRO$=um3;cH5WUea^Bd42GQC&QR~EfU-0{&O)zq$S~=!7 zT=I`Cji%>&&CB?6<9wYg5iVs}?QlR^GML5c_#?mpN?qhSWh3m(UEe#Z)U?wCZ{5j{;b7fLg$#<5(>zq8u z6>xVJ+?KcKE&*EwX5+=tV0(O=j@fR{-tOWZ!8Q=H1*KMxz-7on{u$l`g z;@Ni2`SxO~)i^diU2NwvA=9ll7p9+>%TjigEYA4<??94}h3Max`NEdqRml-KwYET3QhtNF-G3 z3@CL&ON;XA5%B@$md=*KP@$q4>1=(J%ll47A_wRis?}w$xunsv`;P{G-@z4%PCTGC zS$;WsLnh|{_aYWB>)=O9+fU=vE<$vp>fn;Es3Uot2Xd*_XhRK}j~(VboF*vDgIT$? z5?M^CmYVbH>AVYps$k1i3aNcCIg7z*i-L?cy)QE}y-=z9zS~^N6kFBGBS3D-XK715 z15cbvd6|WbGvBTiU?Zrtpg{OgAS#sGV?$O-mpo_eMKpnlqT=j>T-vn0?;o2=;Q^E- zE$GE(tX@C&)XS&G6x$@5dj2{>%Hll>(FUn4E$_yINTPCYI7~I_?drOD)pUlk)$eGK> z@kKbYS`85I>TqcP=G9Lb#DDYZr-}OZv;BBB0Q%z<5&#-!EF?%`t6{GC7_wQTX5eFB z;F=M|7|>oU_B-lrXj3)nT(EIfp=XuIM3x-tZlV)dOaR~AmIKqIrL2Knxe;%+1fr_f zaz)8gHZOcF)(eqLJr|w@wsm6BbINTDns0hwD}~g!FajDA`0^lzj*KXrX2Mw*Lg&b9 zhUx^0Q6A=0FKarQ6Z9h=T6~KDW?{kK$F|wW5TvsmVtdX;VudZEZL?vm`Hf{!vwHr~ zM9;mM3<>5Rvi0sVrvtPa4Ia|whwd9`jk9b8wTR`O7;3A!?YCNDOZQm*MNX53_t9Wi zm-gcm*2I3fF961i->vLPn_*8PPIfgXm4871(i?V>MX4Crrlj&e$9tAFX@iW=+n7}R z*tz1z&lNktq3~qyZ2;^>7cm>sUV9%mkYX<({loNmxa*TVJ(yt4sUXZvCh0v0J~p z%H8^-8@F3s*4<;b20H+_yVe14UX#Y`qgcZePdwo~;=oe{8&SnND;@C86n`|Fx*Hme zPozG$_r<5rT{wO2=`;JX&%FPsy;Dx~V1M?^{u6kR$z)Q}nHmY0tg~N*z%$G3Tu>KLV+~BABshTLNyM)dSi*RtgHrrZFs6ac|jbUj&g89B+50ZBk4o*t($<+N$5b9_!YRCed)ERHh>>CeqMyVeg89Mwf+^NYdCw7lFC}51Pptr?fju3QCTbWyVl+-$z+Ht_pgl1rBB zoDVcJQvUHZ4QlQChZFl=G`i(Gd(TZF61YaGCUI59FHrujaF&?ly zdtC3g3W&?XXBuEyraqQG!X9>)ui&44=JsA1rfrLE{KAH4lX!> z*$|?_tk&7Ks3e>RLsj-yU14>!Em+Qk_r_4w3=`aEPU3WgnU)FA`rWei_w^n)SZ5#a z!S)kvNB}Tv=S8T|+$Z_i(DN^1skW&mjuEzNi$F)vUHRvuH^gJ`G$v%kip?O?)5_bb()?oc$t02hh_o9!>NIwHX_tKOu}t^ zamK|5rI1;W10lKi0jS&bfZ&W6^{&8lEvHdFUvJl16H_U~#?oKrI`eJ`*h9?)YRDDf zWuugz67JAOz!Cju>d-5A4QMsdaTZ^4;zZ-|_1F;Lu}+NNO6*hI{d9PBfYfYhSJ!Q@ zM*nJJ^xoV_JKlSAseDSiZJ$zNe{Dw*7oBtzJ=7mN)RH0({Jm65O9i-o1i{kvQXy|K z{$1*4>~e^I{C%RE@8`d9D$9I7YJ%9+AG$Sh(%Ql#0$I||9wqJcKwW`o`Bki6%YEf< z5FM1Ee6;&p*1&(o#yx%EL&!RR$t~y;TF^_@8CgH1tkY{vwzzW!95CAbn7ES!w2?#l zx!%~HKGAKC8vNRdiY`u7WvHsU@TSN2gXcNaThFahSS|2t1OEFGf+vBoWMOopA@;~; z?4*t>$Ds}L#XJHa>WgHLq4Yq;=+)}&g(8pXdai}IR1nE*l3W~r=u5cynn7H5cKHO#?tXKj|E!Z5%8=I&n7zTEs5~7ex9v&mpKbCRpcxrcEQkga-xHA?eB;qv!QJd zQl5>P$DGS0A1I>*AJ;8qyaoK%o@TRYx*n?jG#*VCfZI^KXfA;Q2`nV8MxcP(>dZ^$ z(@&m#>8ZV;nPfD$Nuxzvzymx7;{uxt-n@%Ulx4n}i*d*qHt>y?1yCeKp{WKG1q#~; z9ck7VL6x*9iK;;IQG@wou2}w+Q>bI`nL-DBJ})9Y(3r zjP_`RbPrY2-eAQPxK)ulEbM{Jn5lpRvs9o#V17xs4OZm05y%Jq7v2VP7U(a&+EMa_ z*WOw%SQOVL4O#M=alr@Mo$?eI9W*%%7%%2SNc{V`YcZ*;5mf4P=M0fOj3RfHo|OIf|>AL(pfs}ERD|6 z=nSE=g|z8R6e|W5BVTdBnjP)D?VMRvu-(FI9Hy4KwEUMo@wGHa^|6qRKON>Ug0+bR zddzwH<Z8RvKNKR6_;>shZ)~@Ej(%~TV8^~v3YF!T7PO-l*~kB8W}`T z0P--3;Drj@YI1y;yoS-)_uh0MzIpYNvJ$vo`T+9HtDm7plmukxv$?fEX)yfr5xx#% zzpGf7ZqxpFuvaq?kR^h|@zH~ul;6O}o@Pno!Fc#0MU{}A@n$!A4%yWTybmsqKFp;Y zJb_jaEM#*_S;_i%wNw5wCRhFhKK$VRt_{I9(Tr9;yDTfGmfcah- z`0)UT1#>7#FW%1QClOLxO?61_32?p@_rD{3P5< z0)3JEF8K!>oAOlp65?!Xj*f9gER{ga_gYGS0)L3|IK%uo*k|L}Q%E2HOElga$WVy- zhy`@b&1F&_@WjiAMMS&fR~e*+bZf+vq!H7BBysGS58s97jJsy@CdgEkr6W9UDSdtI z?F3Lu*8AfK(G)kaWW9Tq!CmA1U9tVJdi&EvZ~YOSzJ87mvomnN^JZU{M1SoH#Mh+JB`wvhGih^eZH zDiMlZ!^ng%G_+AX7=j~lE;0(-sO}9pjRH15sDv*v@zX*0_8NPa!=Imkkgn&N+4EO;hQ4~Y=gK{S~_RLl2ud`)!5pXgq-g`aH~fXJ= zhR`DYF0=_F>%K3>2;-4;|K>$cF`h6au1~O>_!T7nt^mCqHON?vLikb?>NJ&Fz|K(s zmgmGw@}F@N<(3YgiOndA(wl>HY)9>-R7{0>Eozl!WKSqz!v;kmCLNYPo10k1%f7`{ zM$UA1a$x$(Ib*2`1S3fc`~r=S(W`X{h!U4VqVCIO9!H^?T} z1)7Y0o(esCd6hyA?TX`cE0jlCrh1=w3V)o$ACXsZ{IUycJH2~zb8%Aua*AjvgGqjf zUS;smGN(f&!*r^XQa95%qteP!P#P}7n@DAWiU?FdwM-U^gayrqu}%(nsDzv%A|-Tx zp}K*BX<>;DXem`VG9V5}2{ouI@~6A7(DYFBPl72TJD^z6jiHf&79vp0BOQ)kq*#>L z&}nwO!h9;a8lWGEFBIVt$l6Dm7xX2}RQn^K2Kk=xd~V>TTbTIA2|~PK!u!`b&;Y{w zpC=|1XS@Rn?|-@imGt7-{}=CBJo{z|?;it|awFsberyP;8f+sc2Pc!H%5~qyXxFxA z3bh6u<GqUm;5ac;dQjv8x1Ub`p z4G}f>FAQ3K7QL(nL0-mtHx)tt3MtAcZ~FEG+#1|GnBnsd-oYdJjJiwu2GbaJsIh^OD^+{*RYcV_*$0vS9d%M~V zy=b$)5cW%@T|Lv9eDqKb zb)Lx>ubng@$hj^yQKas{Jj6r@oaa#xREn_xT!F4~3+pN3l@}Hm>40{svSw)O<^z!@ zETd?n)bJR(%syyj)iq0#c7@xpzuB;-<>S$Tu-#(umlNE!d}h6&pWxe8pbLE>?deNC z)^M*ShKq}8i33VUi*k-4=L|uI)|ma5cz+Ly)l1vem7uNee{fAp`p<6%&R|%4axVsB z@jn6<-xMV1k1v)j_lIMrf*B@w&=^+JVdO|yF_DoYrjn&@XU8xIFO&$AzNx4T2k8@N zH}WKHxCo*zY8isJ|4wAyFwmfoT|*)xSKYsz>82jFy40e1--w7jzN6 zO9rFJApo@!d7%X!F5=2eNlY(K1Ec#t6)N8uwiri>U|#|GiMdckkx&z8#0ZX2_Vhw& zp~QNWVIW=@+J-~AQCz~+6Zpjnld&Q~p1v;YvFP|;MbHaFEsU^xZ1u}lTO5h?<=^5c zO8DK80HHOP-$~3Rj`ooZ+`pLFzegvl5rO{z?^z;nH;wjn`CV1UUyX|N8BizTEnNMX z>xCMIc9MH(=)YfF31_8g2s1NrC9g?D)^U)C9bTd^Ujj5HoJnUUGcz-@j}csm@95F; z;F#62hT79TTOP4mYBup!PeZ{MK>7=8-C*eky<#waCJt)DrivecrHT<4h(54GZ$&ok z6%i{;g?vm>h9L1qaR{1mAwrIPN~~`*5b8+o(ctC{XwY+HQ$HI>LJEqiIsaT7W(GY^ zBCb;hJnhdM0xpgdx6Ae#}7?;V7c$y7Uzx%KcdA z+7RUzVv9~_cTn|C_$-=$HRU{nba=;*XvTXdBzB~NQdTg@vZOIpG^H&?QJJuhrhrw9 z6Y-tN8j2!N@fMw5m36r4N$ebCXc03 zNYG%?URtgjI(E!yK|@YSupa9isuDtaQ!=f3w(QqyZ`BX%Sff7;5Tl`+#JDTEgG1PJ8t9#HdXHfbVSM^Q)Rk>MOFw2V<5PnB1c9P zW&we84U!Ma4QUxPlX^d5D$$YwT?lzb6dAC&8CC;CXcp@jt0d6cUS#PJ%ODft1~cGj zt6q1iNWVy_`bL;9*n13RFs4La2!(4`OWbu6Rs;+yV=#CO)QhYeAdNyw+_V%XVPhWJ z8xkE(6{d=%GpDnd<~)OT2z8asI}Q9(xBo476DG=s;(OHT;3Z z@Nwxrar|o|2LT^gfsA#(29&rE4ep= z@&A7tkxJk8c9i}v;}v72HLnFDwD6(M0{kk*jUx}0v!$=khR%{cKAU$uh16N1kuwhY zFpngDl-)wPqsVU7yMY1Gn?oTR^Njy6dP6)qzk^9e93}YaGh%@$=$VNI>%wB=Jmb|b zoIZ6*u3*iip7ioAbHMe}0t>9bXa+M;L#}E@BAd>~EuhpzO=1U`a;2*9!`)vk)fgm* zdO10$-!7vQMM;j{tGZW8)pk`?CXjG&&qaNNM(K)IiK>qM3??kX+c&|K&8~et+Jj*d z`oCiB9(FAfZh}Wd`C|Sy$`3I`RV!l|z)(t!n7UyDwT+Ig|A&bVc5L;s%6~OJXYncn7l(iiP5eW!PKe0;*K|WaU{{M`?ya zirx}JYU;JiC>qHdEOlJ1+)2O|;rnshjtiUt(>iLm-dq+Qa~qP_ zz12gKmZ6P;iPmX;-{!bq#fnTmXk56lTi5#{tP&uM^Y$V3ubj6ji&_n@a1s-UixUI- zZ68D@tMS`T;=L98w)TzYk>B>+q#>O~*c%?P7}q}pry#T`2?dX@lCb;iTvBE4#sai> zcW;*;t*+nKcTaj)+oH8yRC$p%h|yTnT!y<5+65`YkMx>xvppG<_5S;ZS*A#`(Hf_R zp?Nrn}yZS_r9+D)lgw_QbtDX=lv&2-z_K|r`jZjZ0E$)RSbU+ zKQE1FHgQfB5l}wqP;wOc3`dfSED^@FApgmWWTPS8FbnESrOOdehgP8i-gq8miUq%G{*^g)$MK3_kembfqC&N%z zRVyI$2pf?AR~_M?r&yuO3mi*9=7rNXtS%6DGGQ<6w&Dw0bW0E+SZjbk!SxD%o)R!9 zSkUUMl?Ov~@Yl2Pz$cVm7p_X8->s4ouP8idh)$_kh#6>6^WIO-?kqcD< z$oO!nUE-=;r+bB-FccZCa`c8Gs_hDsw-_}n#f`1BA*2{1&B-d6I4b9}9y{6viwV4u zK;ZHuN=obKZC?7b(tc^?@=*VV){sA)7_vXHhnN0vo^sS>Z!m&b6I}+ISY+<~Jy1yl zWLH;Av%3HMP;dzAwnlK+q)^uCFFD;+$zB>)fZ1vx(IHs=GUpr0UZ$Bff&b8VK?BNO zzbPBt8z*{Yf&E9kcLQWEz0jEK^(SC_(MEJ`Ne*C+s3ckpb{2Dq+V&B%dHg17#e!*- zY7$}~yiUUi;RU5CYST(ktM4%eC=5w~;338d#wkdADH&tYb1#>`#-RA&96$|eST^#N zdbn!yS*>f}&-9jAm`Ux7Ai{3sAtHgYEKpv`Ps?|IS_-eC zZgWgxY19@{DaBBFmTq0&Zk9U1-T}Mo1dMJ$)&nrbN8!R?-DIbH&lbou%%C3b!EviUA{xNSY~tVYssXMu#YA4ktf|2aQV4<4mYy6*4QTXCz9<3 zYHRk};j(+Cb|&|m_RT%f8)D!53!Bzw=OGKcXvs?&91x@sal#vIP>>3O7HYufFEO79 zsnWFINPHd7X(iB5qjhMLLW;FqCNnRAML-*rM>Mh@?wf2f3Ce0N?I^;d!W&pcst8#S z#wgZ;Rbxo~FIq=iJ|ss}kp=m)IwC{(a1{fqGoDhCAk42}|Ey9Aj)JbhgitLDwQG4z zrCj+WvY3RdPEO`zo66XxNo0>H1_W6TdIiIxR4f#N?yL>0EFDX^^Q>>h7HWO?tLdN< zbz%qTvuZ#E*9Uz+?{1{=1DA%WhyGDb*cgx|MCE#Qt zVo`?ZxQ*n2o|vgc!uxxmt~JEr#1Op&A+otpDCE3EI6ilMX~3G-vZcrLgza%^(d$h` zd+%FaJv-DAL-}7Xadr;6u}N|k58!cv#e(P~W2OL)&xdsZ$i)ond2MG#(Ed3N(U>>c4K3}t7R{V>Rp1ouG5 zp+=ZnVZG#TrX@KvypMN+F03F+4SFjl zLU^MSxJ;GU_afJgV#MkFxM&eRO}Q~ZhGaKf*_h8DbG(W;_!1nVB2|v5Y2iw9RtS{T z78u*m8vX4*i21IQ z>c3ZSUj0~aB0Uf zTj1P=iXCE)s8#C1A7vhvn#o8&!b>O5NK=UU2uWf75wfxvpkr>@+SPSD>v;QOVu4n; z*D8vie<{&h{IE=Fr>u>`@#P$P*C1;iY~t{W31~D^UXO3qo4>#MN$wKxE`zqgmn1mN zG1f?SFa*ey2G$&8R|T}GY37Kpo>LWxkeS_y5uecg4&7I0w{b~kK`64D}~j?v7u ze}?gJxPg!MLFtH+<-YK#h_p)1v@Vel%NvRo98nkrquU2m4G?AIR4Mc#baQfILSr#3 z6tZF=N|RX+T(TDy36i@ZNM2WvfP4WIBWEeF;AwZju;CD(K|cTvUYc^`&x3rQ)Fj`I z@EeKi`hm$Xjj>fRjqwCB8m4iL2#4#J6sC|L@BypC3z|uVfNYqRJg%coCsJS$qBhEQ z+Bj>n+*7(WuEg^%LHLVBt@@Y7<0D5L;ue}w#13ZG%ns6t9f;ULa6Tj@7O;qn+rkTk zl*0gQWIM~Lr)>3oqN$_P?Yj4Cknnx0Lc&7{5EvwUUjkLE_}@f8kwT@OaYbwIDBALK zYu@t56YDiwehGqys$xJE&&v|Qff?b|!d2dIQ_2(Vt!r5`2w%)E0<6YN?wyTc%bXc zd{X1$15|g}fvt{xR~=o8TM?pBDetQYH*p10U{UN>k4WW#gTS5#Z!znTBhi2tjthe} z(95J$at)&2q^zeFbxaG9uJD+KwnY|?k(ov``i+|h0Z1x04hE91GdvUwYJDt1lwqt? zPm#bJy;Y!1gT@S=sAepxytyuD2tFL-r`+D%nbMZft%y|T2R2FllQTP$=}_{BMv8jNr9 ze;?${al9)|u8cc!=;BoP2-nHUqsRB_M+fzz!$+b=2f|iI^%J|RXgwD%zAAr*vwkJ| zAqk-yywF33)pU;>)r&fWVl-Ulkwbdrhr=axd2)1LxJtX9Xf4qqQx{(iO=!0ddyB=Wt%qdV8z#9RDOFp4*I!kt*0V6Y-UX}M7zV!Yx zs)9_v)H$6SiaA!8BnOcc6_`*IOwQu)ZbWswMk`z5o!R={3#qy`a74S72)sx?otVSz z6`6$$4I?k2l(mPYX&xNbn+YS$O-AhPZILT0Z;fq1fW+)0x5)&lrz`G<<^TCyLRzvy zNP1n2NH=ag<+d9&sl(EQ)BidLQsMOXc4==-=?jS|#hL1WarOTcovbFncLwMf+&-2_PFq0JmQ>T zO^Bwu2z^)U9THfPVx><;6nt}BMLmOisO_t$_w7#~*thak)KHn|u)vkoVn=>D)Rv?B zsl;?H=5$>jQn^BOS>A3wL1eSFSr;J=z?)WVnYM0>zS=svG^f8L!8Zu_=@Q)&28-C9 zxP(PeML^N5*9!>BvqwX#vSFbezQEE7OFUn*y>zp;(ewxv-rHz8=~(5SZyhv^RJQ*E z{=7DoZ9c)PBP;#voe)IB2mu|HuqNFkRC7`wW0V}%xAthM0;-#`GA7i%NFOZ7-RUK- znK4y&@gwsmLe)x6ZM(nuc1?6bgjS7JJ-0T7=qj;UhS3qoIN{OEC`OK|D+V;#EUepW z(j0aqU|~snsZ{)wykP?9eH>^Y0dzDmrBw-_JJHE%1W*U>-E;x;yNJl#2@ybFc?+?v zA7(_#1PfGXq||Y3o74mA{lk5?v0MCI1c3+pp}IB1GUPQ=+-gyTp5x}QOpn#Wv#XzE z$|5u)btb1Ym^vT`(9QfEe2B-s%@~v`UI_vj{e!Wih%wNZo{npSxH1Wb064BT3sRg0 z&5a%=;`(y{_}aMsYrVLB&``<3F;BcGIt!kvRd0E7pc|n?x&*7bj?{ovW%%JyK=d|P zYLoUiR8t^}MGTvA&z^koWsz=cUhCvjr*sU=TQIOexW-Tztks;4CV&{JIl)W<;Syea zjd3{hGAz1p3WE&)VuTd0tr;o4FR`AHU^8#4!4YZY5zoFv`;?b2)_BiQWl8z*;b}*L zr#J?2kcLK5m{;>m7@V8a3yw%^ZZ0J*e{+k$*kw4Na1c{%9?PUo7-UnuqWNO81nm%4 z{PM6?XMPy|A2kU_hFv-DHCh~lHTzUOk9pi+QdCg^yqXYJWa%h;<-`l8GpX=zUC4yS z<^tT#sGe(*8+^1$gnuT=t~fKd=&hEJu-)MdIpR{hR0iRYG(63KNNB3GrE)5i0F)-odCrkNS-X5 zXK*U~I|Ln|*C?dK?#n|#SNc=nXRTL#JFXQyBp2vpagmPdBc;2P?!|h?u2f|(%?6AJ zKyiKkn24UTk;i3Pk{dEU4@+TT9wsXzd?f}O=Q|T)!h4p`-iQdjF9lGmjrNt&eo+`|yDSGyA0Vf&GskI(h_`Ze?Njg5^Zu;1jx0cSrON2hqK?4f~TWer!&3?3yr-aAS*-d{XEcHLI6f<%1m=i*B+ zVoC6TJ}10F8aid#^lZh*T8-$_unq|{>Juj`@uz*_WcQquvOP4BhSzdH?P3)l2^rNA z?Fy|7x7d!A5AI83yzJKK4T+ci6~xPye*pLrg%q($g|I&=lPEc4${E4rph6q9PKscm z7nWW`E;|ys)qQzd%bQ}k%Bs60sv@$D!V2$@s*tU&tQfi}sv%J$#AwUaw-gIo%$-Ny z>8HVQhp~o0&^jp z!B3`b=$=?JX)3TqrB2>OUX%9fY5t3Umhpo$p=WO1(Cz#355v`^vLWSX>6n zDIdqj^E^}38iDQa=*ch)e@-HvIfnWL;rtrMc~D^4d#gD!OFRBN-_gt+=8K451%FQM z7^!#q&H_~}NlLh>Tuq>G+L+ElY@lI8m|1yqHxLn-9ijyfg828d-NoMZvr8y7c zgfdt|8x9Mmh?>@=R~7w9KPX0(q9l@Q!Rb_>ZiS{!h~PLIG+)MXR6%qr4icCGRV^bu zex%dSDYG9r(jVN|wZD1wlX`R?2?{rR{T7ROIl&?}9iQ(dCIz+*z53PXZz3SXn@Sf!kUAf>>m*s$hqIyqXAo&o8;^|zcFX0*wpV~6k zU^FZpTJ&f%vai|}5DUqb-NbD+-KA_Efz~ZJeCT1Ha(rA&TlESh{+&k>Hf|hFI747$ z;FC3a-zsi3S8$yx$6!r2vl#y%XEADNnHfR;>Q5+dcLRzy;1ms6bf;rku!){#LoN2`LWJVt)e{0Mg@I}%S#DP$6xL_Ti z`X^P&Q+`TlKpWI=_c-rC{6|UA)~G4qiQFF>@4U&Dq*68?Rc=URd2bK z&hvK!DcuI6K7U7>>sS|T&$pXRxR0`7U+02t`U!IwI_H8dzFLgm3GOM>S^uo!XFzr+ zH&rWif=w?Y85ZSoKi*L?V6aK~+B)ZoJI)0oVZp0<6FrU46atV`8~C!dS+BP+Rpe4! zL8aa4POw!?A&YS@p+PGA+HO|x?XFfmkBS)iOZ*j6*@!tempZ}Dd33`IjjSJE5AI?J zY^`0*UZG>R^LntIp)?I#ea*tGEgv23R!>lnL!M-Lux6!T1{Q+e1$!T7Vr@@nnQah#r^3`ITxA z8?xfC2I6#rk#=i7eI)Id@b`o9AG4_1P;X{gE4Sh`u>+iHzSRkKD9BsAY?NL)!Jfn~ zi7v{&%@bg|>etLczx`nRhs4Xl-6F66P(^^Kf;Dx5yDIP)wqeEx#0&U-r&sF)_p*#^ zvyd%#kTr3MdmdRcfWsz8IrCU1AW|x5geYzbw!3ZEhs~ql&Jxh=u0p%o$VMm$z{&$j zB!yv-9qy1Pg6!OV^Qhr_?Ls|!rAmal8`2Nz;b!eO1xxa+?Bx=!@e-8Xk%ex?RBtRK zoaJUsDj>UUSPYB}%%p+k2Urs}%kml!zjLt@jPfrGf)sF}O|Qn@3fS_k2r$N0sjttk z3+^Gk0dr~#Kt!oO+&O#q-e7yfYoPuEN_7BY3KRK+^}4bojo=+x<-lV<(^zU1;kBwAE^IL*Fz{zShBF<<5%mo65Tnp|-k>eIFM=O>ViWU3^{N2C^ z^iHr1C7zqD?1$T+Y9D~QSH7#SS-E9Jfp!o4(#+%anu^{4WdR}HzSM4Di0znNtJDJg z6&tt)?5XEVwRzw`$QO2!QNUJ`XeM>XF_w$$Pt0h8uq2>NHRlyT!AR$Vr`~H}j9p0j zb}wbUs#~gL!;0`S>wCf|eBf$jUh%YR!FsQDxpS_a3$`e30`dhWvM?cNqm{*Jfs;e< z<{nH6nAMVUlfbfhDM-FE_LSoVJGGSk;(me*v4ugmOSmziCYSzVJEU6a{dlbKwT zSzMDDT$9nS$;hv_iw|=w0;sr6pi@jM(994P0-Bd2&|KwT0-B$XK=a@Emw@K85oqKu z0nJzVOhCh1FBNEbH-Z8UtCA|vu%@2^4Fks%XebB^Xv#+gK6lwvCirkyzC;NJq5yC~ zJs~sI?G%F9PgFawD^O)MSFc2bD^2f9Jd9e8#CjajqsmJ=WBrRD)B%^n6(+F(cw#nN zBA$TNiv-LwZL7(=z+u1nBn5Rm|2Qzo5wz2s&fQxJ&cL2@jw^ZWw( nEFV_=1zRHk3ESCPs^u%~0*;*e0#B+`fyZ-rl2HbQ;rIUoWX}Mb 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 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 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..a06a2f09ba0e71567b5a364610439c83a1efa6ce GIT binary patch literal 276272 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 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..ed9f6f34fa82cb8741c967325d23862a9cf94a9e GIT binary patch literal 27086 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 literal 0 HcmV?d00001 diff --git a/.doctrees/development.doctree b/.doctrees/development.doctree new file mode 100644 index 0000000000000000000000000000000000000000..af10a158cceb4fbfa7c9b06693c5a085434ef46f GIT binary patch literal 81261 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
@B0AGX;qpSvj?uOE>YXR{^8&WkyrdL&{up`H4a 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

imcgU=E`y<|siY$yA82UmM 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%0AagRPFLw%4zZkF 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

9~B*YUJ!dc?(g97`|^%w2li~g{}|7H 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�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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/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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 k}<+U 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 XN*czhLWZ^q>~F%b=h(oM&Oi2FfN1*z 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 zcmeHw3zQt^RiF#t@Yi5!aiGggSr9#-Gvl~KKU;_z+$06({EQc)1*|32P1U3l}2!uyC%VXKI4+xyI zy9e0s-v3$ERn;{;BgZE4Nk-k(^}p_a-}nCaf9IwTY`kp4W%OTomEWn=TgxT4T5Ywf zp5G6*R@;@{qSp%g@9$5%)IZlB568OhlHcxiD_%dm3~y9w^=7r}wfZmO#~t*3P;dGr z*}>+(?Jjtxd2Lg8bE)K%d%oA5s(SPFmRFtXblVlr_dS26U)%CxKP#Mn6ZC6Wgkxn50DRl5*8RGDwf3U7uiI<2yzaiGcK6J_cE@XZ?WxX6 zP;0lQD$P22NPdVO{e8V^r_}X>es8We7H;vqN>FdNG>~hXYMTiX*Ui))Uft9mvZg-98!(0)cNe;Dr)IS6}N?_PNgPZ2--mOn%8yeEyq=_IoxX@XghN~&-~fm^_zuG3y>IeVM+xvtw?aexg(b~GJYyzm3TM#+yKL3{MJH$%>a7Oks&j%xGI6~(IDVyD z?*w?$*G!QWl?0w$DxCB@X9g<7%&d)kQ+h754kUuJNYq_#;p3u9LR>Ob%^ofRX8<_F zbgqiwi-n>ew2{2#X+C>X3@Nncuy?N_r--Jfwis%nVMae_cRrj^e2^K%ow;z<8YS-% zG%lqrLBC*i{xhR2p+)N9%SSiXbfd7iEW8quvI+)LUIuMhTqN9+No-<^AwB9XR@9Y2 z0*&)mNLL06iv{JYrmfZJN#^xiX3i6J81d+ z@CvB;eOOHNbrDKmzxL&D3%&&_9av9SQ1{_B{3R{3Ua1HD3;Z*2*#3ELC(WnSrI9-R zOHw>MMKr}>1@49y7@{(mzpfVc7%!qZK@3&tD88eXH?%;+O*6z z{l9d4jiaTK);A|X{`B>b(9bm?bRlY+E0YW^OJ$9!lbzli&=Fe!ylg|3vtnt>K(z;x z1%Q|>69Z?TceCC)+dc!ih;1Ob41@J-z2ZrIE9iAwth+*86>76&<&dLLm|bVdn{(U_ z(FBXs0uLe0EpOR_ivgS7^GTZ}9lGl+^qQ{ftl;-Lopv{18t_+Il^V3ob`R5FE!%p~ zXwPXRxCEhe2x)w5kfOJGl)TNLq+vDTT@|L5LPP90{)N zthfV%+s+EI_%oapJ5LCIigN-k0jQ~7*H_9#(oY67#Z`d@E|kV?x&<4TcSnp$|2fM; zrs$0{W@wu|zXyU{fsRdn3!Y^CZC-Wn-pRtE2lW?Zp{R;Ne$E?R+@p$>y^mTGVi zkeR9IM^c}$2yf3ZxRI)jX}u8PJ+gGRYX;`1pt(IVcdNGtV!dSx99VNaKI>_ZtQ7!e zJL79Ih^x@iMl!{G?QM;0=c7-;RPdJF#ZD7Gr20HN2ioNK5iTFFJy=d>;rMZ$bZhY1 z)I9J`JU!wy{d&)z5w1}vIp4C@o-9zGSQV(%A}1}}#UjVT?aeyq#BG+@wP34sJ1fp% zXMEn7a_&7a;p}tvmmX02pY)ReKf0gA#QY3ZBOF^^Y|i){m%Pwhp&gKb@yI#Zcnhsb zxNAz$i5flRN6FPzY4_le6Fz9j9Gm+ndUIxH=k;!sgLb(hc4KySvViS^Hv-E|;hYaZExATg3}U6x^fVLVxhnVp4~FX+0JV0M;8)a-1vPHN5U?1Z7n@w_v( zcDEDTUF_5rxl{HY-I%&awN=+80>K*TZLyIuNw%>}Z#xZ|9#ie>*AJN1oZ_o*tYAEC zOI~S%U9EGg+6k+ha7|0w#5-zrmp!Y3*?Tpo%>o&$F8~T0febVgNWkL$#e4E{wh4E^;b$8$(paSP* z&~7`6#ZrFTsdF zo$9^i!G&2#7GZKgUu~;J*fQF+vDy6#SS$?73@?pQd+O#W#nSLZxoCFmV_z^9}b$UPg<*05iR4Vz1upt-U#@iVW!9kbRR2YEhu{A7eOB8)ry zR>`%a%+c^zORJTeZJXAA!SxPJpVRE_V)U$;#9v`MM`9AMkAR}KO)GqRIP0X^ zfg2H7BweWOQ<^8pypz;mSnZZ@H$1tCO*tW=fFy_$*2rXk02?#h47(mz(@1bMB>!l% zwjGy82{V&IC|jekO}AiI!%0leSVLbs8bC2yXt%3~RjNw*Th7(F@*O=*puy2C=)j&9l2O3)7zy#|C(yc>w3MKD|tRMk^s?)5)4Nf|O zG%`pq9~u#UmY~=h+tk{B(wy$Dd|+nL91-b_`>0b9N*mMrVGVkJY|tp*8L$0zczI`q z{4py?fJ3InX9X~+M7TM1aHp_+gtcg=J__{Ndu`1NyVna0C}P z{PrRdIT?Pf-r}6sEuJgL^vUV))iGJEFTDlmIg-9qj%=7w3QnIQHZOge#A+$Ym3*C? zz-cf{fYKKM#e9s%VX8$)m#rZv4CIL=$0^)7HLnSWDNHuF!@C}0mH2IOky7QbQ|$23 zLksC3xHr4i1^*Q@u`$DoYxLwv=L_VwTEZr$}Ax;y;|s6#M#7l+m_k7aR0N^X$JRSjtx38_Ycs~ zDBSZ0%TBFzkje-a;RiXGG?^YBe?F5N5h2o=VXQ9+a9we$yoHr>s7h)K7 zdk)f%Xc7^`+NyYh1&=sk=aahBZJN}5OO`BETr5ST+6r`{I5qO}Vh^Jw-G9W6jU?%s z5g-{!7kVd=j!NOb`CikK6-pG1v$#TLDGqj2Wz0c{1pZ;DHN$UBi100YbLPY^ZdnpFbLeuetYVE?}#8#hX9npB4?|G$HdMq&Sdigp&Q4pshR#F}5 z%FO<^3igi;G!}Jy3-VDZb!(^;!PsqcJ*4^v63E#ay9wb1Z7dT||u z=QZeO6oR}B?JR;^SK%p^8xfve(9d?BDqJW}uOkLqAf^6okVgC?;k604&KrmcT@|AO ztJ8zAPNSUDR#PbAC9@;+N_QqulVL^i3Fl))1j;c6W%gXQ(9A_H4K>*)7Xr(ED?TcZ z)9G7aD<&}|1mU~|uRD}0$@d;cR!vejt2DLX`9q>-Qb%*`Hr$1+q4SaNMcAaA zjpnQ$4l+*Cxf1!l;h>a~Kt9T#eiqrbaqf2sUSc1z;bn8$Cz5t^)!3E5YhNUc7D~^x z#!%5cTSQ!zxSxDNR;xclEtMD0&nV2cj&`Fn+dDEX6}jFBx9x%6pXa%Pxm`Y4us}hN zZ6-NPB(9Qv6SnCk8c}SMB@$J?*3`ZuHnk{w%fCzS;FU*P+l)HRo_r75jlN5(wkIQ- zv{^7d3mO?4l}J&m=8p+m^&@7hEQ-=STeJJvI_=d*M%=4UtZA>ZDN3$4ouZzy^p`b> z7NZKpcRQadKqz!_2n0zju~Y$SlA;QPpG@M^Znx{M43)b4Rz&6^lEx2>pvW*yTx1nO z?)#oHP}X3cyoy(0i_J&Zycr1u{#7Dd!HcgkB2#X$Mc#i%@2!J%@_R#&GmRDh1=@|y zii?>7lUExd?iJ9)mwBcTm(NX+0Hg|h!oi;Ur$KAbA^SE7x z$0=RsL=DVIwJ^uQ5dVJ5n$sgE+1G#ZN8`pt-? zOHav@O#j|P{TO~T$Rl3GtFXDmBS~C>0FVoYi8B7@3CRRkxY}?9(Wk`?{(TG=qvAQg zaR0d>n3={6ei`jX=LWCI^zccZD7XNhN40&q^ovcFmPU3oZGTpc=n4G$ z4{2PnQ`bdzO7?DT*B{5`IO=wNm7x%Aokth@OSBt(yY3pgUA*G-JsY=-K-%4OK4G|R zd|UHzTSApj3;|M%+ftJ>Zu`?o2!vz?oYF(B0P=cvaXe!vwGXt&ubHW)a0QCNq;iJy z1>K;?X99{SMpb0KJIH8XoeQve?3*sy79`O04VyGg?=dv3_Q2Y3i?(m1HtV36U59=~ zF`sWoyU}U8lu6qIE056lcY<=R;aP((m-E^hs?QiklTt=FO}ZC(!SNkVGQ(l|P4xk~@__`~lLZQI; zYh@&T+c$PbJRO^I69aUshEM{PK1=&AB(D{q(>+_5@~tsUiAv7=YSOVGc$20ky?}P3 z(`I&nPOdjXk^dA3dz9x2ihM9HMLrcrE(?Ox+v;hJ=JCW3n8j#~niyJ3C+Tpr$T{qX z8>VROjmydPZ!&zW{6yoEc;69}x}n3AbQVIL-{evoq5=ns@5oeUkt?ZlWpOUuK`r?@ zl|_YrWF7}PdPt0koOUN4I#O0S1CdVbS;scn(}MVkjE@?V$kglj=dZY_S-bzP*zQN^ z$JRWt6S@k=cVR2dHbjq5J?|qCbMQ!1NkYgz8bxoy-4r(Ql0Px558r39 zxG!4&Li}K5?*~jqO5gtO2v`h;kB7T70$C@z`;7uJcs*6C%YMt?5iU4LKiBQq^}X)E z=&^EbC&L@XvC*=wC{ZoXt(5sXfph)F$FS1IWtfB?-jMhrZYIEMG)?gu(v33?jNf*Q z>I}Qpvve}HYSti+wCW;tL*Fx+z$aFPpLC4;r*sQGM*aazS+j}LUqu$+^SBH`WD1M) z`4~gM2`>_}xl+`f@oL?#^l;w;N<-7h+6k20LV+~qLW^x2`jtn+I@&XfO6LnW_)F)H z`DoW-eW4aOEe}1wm8*`lQ}t;BKVO&xC>_mJ+R3j=WxKlajB&jp0bDA)iE4&{9aUw? z%K&vHdpLSQToKiz_+&Aa@RCJt3S6Ct3cUO1B=54vCy+g@s-L#IvS6k@@~%2p6_do^ ztlXRwN1Ll%cRrY!KIFW%76cuCX5T*2siwsAO6~5#zK3VadU3K~lBl4q>XpoO>)t%d z3!+$34|%v`a#Cp#Q;e)Qe48yc_#ZUV$m)RRsPdW)Ec|EcS12-xr32OO2(9{JsZ_~zeBqX_Eb5xle8K3`-Q6<{khE1A2 zz6}#t13urHWh`T*qy(uW_YsUfn7|Rq;N{(Ci&wJT2v^M7wK1+LK$oMSexxZR9v+xo zH7ixfxGSlpHHX_XcP|3|jjTe1$vyJs^tyGZE@=#&75mrFc$zJkIyg6H2nMkhb@NZS z%aR;ZO0HPtc=>l#Qp5UP-V~%~+PDvMq1`Qn1*0qBrX@4K>G1v$Ws*^6;DXQxERN4S9~S>W8Ww;28PuOys{1H9%t?zD7W3U}TKlcH zP#eA(!9Q+5*vEBvlp!w)Ea-|kghSE|Ad_|JyrLLJ)mISZdg@+Prtj*UNT|pP5LRV` zeXE?awKeaEZA}zaue0}EuTJgyHD=f4Z&t_ej&+<1QE4`es3`Dv8gyqmYV$pr#vBFK!JzX!pSn0HPzg4?Y@Mnz35L-+L4n zPr$sHG}S+v-ZIx4q1ur)#|Sh`^*lg)Wvw-u`R0ky%?8h6!AcvG`^Sy+MmP*PG?VHrh(SD7aip@&SvhnLP=ck5iV2W4zjQqc@E^|nyga_aTzTg23K)_YIl6aCO5_Xf5JT~ z{0`JmZC&= zbauTUs1$|WX&qW~x;8ea;q;i^tDwnyl*1&)cH2G_f=+}?OU8$iqpOF-sbdw3hE3{xtcX#%&3ms+&IuVzI)HA&FO36WZaDXo!rg4*b|c{8SO4W@oIwio?s z-F=S!BiD)1tz8*m+lj~M+HXE50)2~sM4ywc3s7{miWWvJudIg7xFYyj-GW28pT{zE zC^vlK6yKdd7Oh>KbID!d(wySDX;L70*AtiHsiNxB>(!fC?Ys5dS5wjYH-L|7)u>=| z1u|chUMWj%MXK6_C&UGVIN+&jIg9&ACH^@2YF6j9;#>vMOj$aG%kr(VeoH2-8z?tE zM9V4wUV?SS(iEesscpgN93GnN&^eEru4Dcl%mp^f~z^MQEhw# zy^IoB{RG-sk*F&WRft`rYE^AKkJ_JiGDq(EA@8vK*3e z)`?|m@&PoV46#^ArSKWKuPV0zDLc64RPhqhAFboSHvVZ33ANxO$AgIX1sarMjXDzF z(2oo%=ttAxosnMC=3$}7@5*JRSIfzR9=|;Vo@s%~KS8@yp~oHzJt9kG07mfQi`cX; z4VjmX7xeyb41eFo3u=;v7YhO}st7hwjbc%Ro-RynHm6{Y_UdD-qtrSFq4; zNS#&0z0h@0m@s;SRziAg_2n3sMWCGo`_wv(DrBoc?QL5n0ZfMzxj|{6_jYQZnYEq= zy~ohYDBSM`v|AN=@3PQa<3I$fzXX)q#M1%e-;;?P78t074F;oSWpPRxYr~MG$2cW5 zF%*$LOyfI{+R*q;%5R9~wi2r~vft8uCrJ0Tz>_MzhbzNU#*hv?CA(2R2kS0!;th0N z%$Kbh4Zkkt=2J4(TvT+c^@^fW9B)(aAhJ8~7Q(#N#vO}oTvU47$p|!E%KUr1Zo%r} zS0d`-4?$g2-V7Q1IMS@sS?Fi#b6ppA`6-7DoF~YOo5x+}h&9SdB03^HHJpOquFgn* zgSs2U^Jn2IzL zcP$cKX?WC;NCCQ9I7kcxtnn6N<3%@5=e2F7n%V7YYRT*hx*3K0ycO+i?n7flD`s1l z*vBho53Y+4OlAH2J`jzd3FMj%A6es`TM&!yP&xyl*(i;UW~0zPc9+HJFTihUUuR*V zGG9XHsH+5#fz6k>7VNjJdv6jhzqs9bTtovwET9bB-)xN0Qeob{3q#fKF3!hOy$A2U zD50W50ew^UDqme!!JX-pnGYYUxk_DYKYWZx-0CjwP_i%OFgM|1He!L}%ZJsP@o|3A zSvia=f9W4wp~xejgz?LDw!64UOC%pCHU;{Z&d@oxa%Kugmr8WMZi^~t;=(;27wDDA zW-iNy%DNH>@784xgSVMk<ZLW5py$2j-BnR3b zMHqrH#X;~H<{XPKB zR`(*(D_nd&YM7DxDnqw{=W-4fkK3);k$c2o|UYsU#m2ZuRbwWuUuk{Lc(fXrh~A#Wdw z;E@U}lpaTf;RxJRCCE2%es;Ei^rC!iklxw~R5N*S&r+9Mp~3Jop0)9 zxd|ll7u3BVkum+EHT+-3hL56jeuDTj^fQVU{WoYgIzgziZA=iUh!8>jzX7Te%$Zeg zBm}Xbk+B)~=G%-29wqF@_A$L5SK9Adlemgr$#kml?Seyj)Hd`p%9h-ScB5~JD!G=j zC1%a(yRuc$p9Pfo&gA1uwAqF)MIx7{KLpd|;fEh~5*1?@oQ5A1D8{KA4rS(0I1m-~SK_0u7PT(j$5f9Oihc~8sF&jrAO03h>H_KNwyD-tw zVn1i}ZQ#JQa@flH1Yc=<9FAQanLY)WHy*(R{Kij+Co~)J1mZ0t8h`7PbRnu$%#p)t zBtSU1Z2IC%y@>BAVk| zMHB8Gy2X3K(n#-+H+W|P$P%Cp4etT(?AZ0w<`VH8uV;z&=l=*_|fAo zIvPcnc_-Rgy3A^i9`CUYt^xhh)?kk9CLTgSX2tT&!*P!9UmpqUEC9!MHR_5O@t&rC zk-2LzVpi@4z4W(STnflod@h|(Y?R_Yaewv}@Eu8kWm(2H<;vwb988H`qg;#}Ly#nk z%a!H1gL_MdbQ#&U59*q@;@5+?g}G25IR*El<-Ce<9O?Q!`NZ*4 zM~^@8%z^SV2Oce}Z?AjqF*Qd~n^!LCaCD7A9V5O34{-6Z>HCV77x)rL3`n%P^HU&^ zQPiDdw-TE$*k?pu_}=4fDlpMrLK1`1fPCR|w(Q`H^Jgn=Vy;-8m2N-CFZ@rQ1otUW z$dXKD-#PKlQiP^)f8LZnF^d>K9b+1nALFk0kje8;5HFvlPNnzagh~Oj2K|-Tpd-td zUqVNt$d@mo-5TUe?z=dT?IpY+7_)e}JLg_opvu^vHOT+}MjMbW|NlGvJlvHrT>k$# zuROZFwjfILxBe~KtzmywA^)Qr6!EFXL7z`fafcSC7{hTJa0t6h!&gC>sPNOwOVg?# z**+7d#U~t=LjslG>!2QKP{>EChQX8yCvcY_{IiJSpg;h0BM_SY|tq6Y1i;@;&sr`D9p8lb}tX{?j!C>KxXcGM;zVYz9$L#8KUoX z^!s|S*2j6-(ap`nTHkkT6`)F+em2TBOSXp3a5xyKZKmoVgB@xF#DlXpd*YSvbzSe zW1?o0F_K>PVYMOh?Ev9OhRDGv6fHS>DP{|k2uYi=CZLKvTX_3DF}$_J@p{B;KSbS2 z)}Iu!{cvpfDDT#Sd1!#}gXm`zx%^SIvpD}MlNeSXvpx0@;VJ>0#dADpo3R_~6tw*X z0=U?aw4m)T(krQ~EdQQ-npYX!QJWE_89JXsyV3V#q@ZnVTau!-@vX@xJbAl~ zryrDITVl`&QCgC9#^r8;vO0V#qG@rFC|G@B=(8wVD3@BAgALJIcAz27`hpJ8vM)`3 zdr^#m_o7n73d|-(eA&@B5I9x!@1;WzowRz*Cch@*!UM5YE2?@4h+Gk7L?CM6`==nf zk%aFfG2v?}U)&grdU6H;9aS10?3AZlIM_+erl^^Yah|k?9E@?^$}L=T7?>i0Qxx(L zq0+`Ncv7rDC%>N&K@5i+B^wgtL_APMIX7)XO!Ejv*HKQIig*HfkP+qF^Bj8`5MeV% zSIvnq733W9mazo3V6?6-ZalSDc<9r>(I9m>vVIEf}z$XxU40=NK zV9$xD-Rw5Io?BgUYRCXb5v+K=91>*B!l*zL&cE@Elw6Ece4m6jre3L0F)vh)UO`3b zHoP_zkc9hYg+i++aoqChWmN2tg|cv9<*e5v7ZLnSVhR8zTsE*S;OxQ6G|5RyqarsC zew(Mt)NPbfYffvpc@+oEkDVr#qi6u6s>A!?+fKSG6z1T&TEy8+LhAW;v)NuE z?_j)i+{z>>rUJW2FQ?z>?I+zD3fI;=;34qjh}ZP%J;d?bEugoL;}SH0DV;v&0jX3i zIGDOC->@lIhQ!23a4kpS8r6?(2UkfjVgevomR8qc-AF{mEo)^+t zikz*Mn4)$Lp-hpHHY^owVAxoi6Mjz}bTIWyjf+e(J2?($gX9U!`Bb;(2^Hy_5G zfosOe11_!e90JOYr1Q*0vDV@SpORiD%eAG>bD$`sdxi)e*_ds|t#A|p)bAp;fM6-4 zA<@1#4OOsGX&lP0snS^#)S;@hyFh_XoA19BxkJQ1sH7#US2_X#6hVR|)go3%CyuMp z+m?LgTNtcVoHt#x`CF{wi5RPhW_oPml`55H!@Sk-Sgc{XZAtz}Sz4GzV`F=F8uY9Y zvZr=)2AtJ|Z%Ek*YmoAqmT^a(9koV_FDaklx)??A2xAVvl;1O%bi@yA`Ecd|KgGT| zKlyN?2pDu|>Y%zhK}mFjafTnGEKXSN&wrF_(=2ZZs5cVJdutRQ3=)lpd-$m76bvqH zo3On`H$-(;`i;MbX=#~T;&rmO^y?N!Ldiqx=)N`?apXB#ut^{Nml20IY9s5ru*AmK z$(&WGq%;BXB-_ZkN*re;?hK{B=_!&Pcy6@G_pgSO*^v4k-GU9Nm8c)vJA*Sz0gNvyR!9Y`088g|AmvC-;q%!MWc>%Gsi{SIz;Y z$txXAp6uu6RDO=^^rE!;R1tD#8J*Ot@ai>WU|3jw2VTMo?_oDdG?-H6fiBW z{6iWI269+StAjaXX+(rJew;cNPTpOEyA(M^_>-}DL`7=E*-L1UT6*Be(9b9i_@6~P z+u6&53+$>G6=gF}!|>XS z5-i7fX^OeTb3y+eYz2AXl*dBH2_uyYpic%Rz)qDd7(|%J>rysTg^x9tMKF>b7TtjS z&Lg_10)pwqDix4+&RrYY1Jw-ZG$&dOE?=ajDxKt_=q_dn&_oI%>zGh`JXuZRsH%mpl>{GJYZEt%HV!pg{`i?686&z7+PCYG!OA4+oHPd3Ar_iwu)zNI34z$vAAx;Y7@pNdMl_h z{?a0p%D2`CRcSNae+E;5$~@T$wm67bkZ((~kKLb?;)nYxFoX1hJF=N&+Ra`3by{L) zCbmmfo|H2T90?ZM52!;=AjN9nx@E@4ub^GZ^;Y=pNY zBS9Ha)GKY_^pA?9i5m)6;1g82Q8@J;z#5)pCt}k>tQ~$x%L`2jB=W4QBMKyni^07* z4*0aYXSh_`60XqZ*(JF}E!;a|lDE}q>#_*ZiMtAB3vfQ+edLMsP({kkeeg*f&6QPC zv1lrss)Xu%uS-&2RG(`vwJ0~x5}7*(0c-J{FviLfcx6Hg{_L!>Nv=!WB5QztY=Ec? zOW~C?E0zauQ>%RWx7u1m97vWP@GWb_hy*?1M+sy?Pq-lwjUr0I$1?$(-8a@lr>6IlpH_X_KP}sps**d)?n^;^o+8zhvgihfi%*@R@5*cY zDw(WS3%(8{8%ZtrbedX#G&|hWCa(@agpgn#6{l>)@}-6KmVhGJpZMQexw5eJ={V~~ znuCRvmv4`{A}m>OOV(6{z%*AETuJ>1CVyQb4q9XGh>aO#(!}EPTvJd$FQZt2yV1^K z@K+38(qfhTq)S>-)+8s@+ z$aKga`-skQsbh~De@E_gmE%I=iYFkWjDrL>Klk_v=LrF;8Rv;N9(98HB65-GMr)({xb)t&NguNI?;te$Msl6&nf5xEh?j?bhTdGY1B(C>kLSE@Kf> z!cs*!aF;#>{)})lQ+5uSr1BauRWrtasGuSSlPZe`Sw46X2NmE+T>;?8?Lp0H;uyh* zumdRoh7UUp#1`Luhu%Nos*YNSI|nyIVbqT3E?Hf^JJw}7zq3+0X&x*sa83J&yUk8} zca;6MCLf1~r2yQs3bcsp1K&q55%T7aJa}cz==(FlnO8mdUWRY_R-}oZkD;9GL;c{sWtH7$-Nj*C!m`CU>Xjm~rsVd(KX6E;Lb26$y;>O;*Nu3U zCS4;bnTx4<18VdrC&+EeqnzkjPfTRdh@9NmYw}!1)$=RedWYghhl{7cxu6Ss4i0iH zaAy1Hg*tWryxZ-%D`a}n;SXHsOp$ao>=eAfUKbv|qC8rmoPMYn zcVK$|15?xYPwhV_>^AZ3z#%FlhxnHe7isHKzo#uo{qBqt=V>iKh>b7eJV$+^({8RT zki7!mog;5u#|EPGg@~g;2u0uYCpgnAiTh=NMb-6-zy;Hy)S==z%00T>c?M2HfPKk{ zoD=4|2@B%TlGi-Ix9CN^?XiQ%N!9!$VAMuPG&vmmF7h2r4Zj1l6;#TP|9PN@4!%>Cn!q|PM~Ye^5QPR=SZ>#Y8lx+pYj$8^*N|cgBI$L#!d$aQWeKAzE79oV z81w)#toAz7rRnME{W!N(O@H$~`^^?y#;JWiV802;h&Kc5!IP4D;+5qIZLu2?0io+1R=6mD zjqZCDuY0(-=(#QI62tc--|jg;XSTFMZvBY9j2Pj$`XxME) zG+_LmAUZ@K-QM^pj{^wTy2#Ku9D|H}MXOPVU(jf$_`#%B2nofc@^bnTGrq)(Nt#|_ z##BA@5;MNUjK2}gSUZ2OUMMgwd~*aE@eB{Fc(S=V-DiirBa6G4l z$b@j9k;{8WbfuQ1`P(r|a~*sEe-P_3-2k@;it@6pb(_P-I1=D#Bn)fn*T+VS34^>O^p zXVvqB49|#WVSOKX3sG5FP#JV$vNtJCxZlJH_fPFVFm>;K*qcg2$9uBcfih%I2y0PV zQ^iWV-L1m+hf4@4jr@fd4wm*GdJrYqQJMX~eGfkH;-tfm9ylZ)-G4|vdf?yz`RL%Z zdUU^f>)^fW(IMT}e$|)V{fp3+>82aDMU1->^ENJsQet3h@MulUNDxZ9R@6lT&tVIQ z;spgN%c_y4k(rJaa>XE1%0hy-K^&*i`u?SDiU(I;S=*#Fftz;Z)dU(}0I#~x_#$yE zr2sU36W<1e{)WpVLf=sRcz9DRXf$PTtKXPIXI9+jX1uEc7&By%+hLZF!cTUrAUo{y zSsnJ7#)2ECF_I4Z(6Hi3^iaV~wvDdN4|qNwd8qBNCVGqgqROyB9_2WuIMENUw|_SM z3{Ln85%Q$C&eNOJ!GVV`-`bUGvb8<1gBOrl1|T&HmGug$4DFSH;&JxrsM~J1RYt$iM}NvajRemFh_og`E1w!>zs-P;>;r zw`;11#{~7JUqWggoPsUfd%gLMI8B0J*!%kvFY$nz5$M5<&HdxGiR0lI=fJyA=g`v_ zc_>l%XR(7X+q&&`fT`kyz;Zv_un#wAiz$>b?isvLkY9Vj3!<2+gt&5v)nE>p2|h^B>pIMxg1ryiK{>-c<2^vAN-?Y6t+8eDEo zuZta^siNLnc$I*>)ypRCr62Bz{Sxb<(E#R!J4C-`4yyGn(H~-s!>efxJ^+QHOr0jy z)DO2S0|F4Q;``+uEQo7yQ7q^0hLZdh+`(f+Nr;3<8%N zdFrt@6pXE6%eTR2tiL|JA-sm@4Vcqf03tT)sHopRcJ2E*E;l7pl1Eav2hQp~@gG zr)l5|rBJvmyYYnttJ@2=(7@gchFC~spY{oyO{_}06VR21?A{n>>1@hTb{ z>C*=K!}U$Lq{%;{ukWTm+o^*B{o%|IP6nxwYT(fyRvTDT*hfp4#GhU`M$06Ff&t45 zJ$|{fJJ0L7z-zj|>p9PBInV1j-=H;I;Q60#(A+QZywCHTFYtWNH)yUG zc%Bz{j^`URzY9FK^9`EUg$V;^Ct%KPZ7)+ z$Oj41|Cj!Jo&GSm?;w~nxas~u0dD@p;HKLW1-SVWm*k|9y#n0)iNQ_f{{*=C6N8%$ zK{GP(Sv&!5%2Q&TV&@Tqn?Lo!?Kb;hMBiSii{jkGvcwaT66!rcIrbCr9_R_|hAxrU z9Eo4u6?>@P9*guirbgvPJ0tyb9m77@_jU2U>Aj9MxC{S{YV;ow{mLuYB6lhQ9 z`i*65>(d+jclF-@zKj?WI6Mg42P>k1Yh%!2R#1>;Q9acmGkZkLvBpD>_c~1u zMMJNqPvkFS_QD0y5Z zPJ{5?(!4*7fx?|yQ&4avXpc936HSClI|yD0?&@QIYoQM(EEGwBRZebzkTm6k z{p_V)eO11tZpV9Bc#wOkCsw=E2M3qR8D#pvxkq+k_A2-0T;<*CwaU+}c9pLmT&1Wa z##N+K@4d}l_TxF1{pflv`^&3c_L0nGM*{a;{yKZTZ^^md7uIRLxUYD%3!cebFdebu z>T}uaT+X@9-g>R`K^_j_>=A_y2y!W9FHe=eeKzzOL&$uk*aa^>j23F&|@wAm|Y6 zs`?EGqB92P!%X|Y>+9baAA&aq4^@~U6ZrFIvZH0|9~4T~YjojxP2P+}@5~<}Cj;FBHG5w5%+@jLi8n z;E?9myUwq6#lY}#{7oeYVw-`fUpBmpns z+nK%_{B#)EYS>$Nrp(@Dsxe!u-YO$H&CcT0Z}0#4Sjumr(3r=dhl>^mzI>Ms3^?M&CPGM zQ?qAaV8(1=ZGI>rK0a%vJ7-Q$+G-e8?mJgd{;{qusM2#LcFC_MJ0s)mDw&+jBCPYIq~uJoW4F*KRUkPy zn67(Zz%lyxIU6Az+^vPmY2NOxE*nWih1FDB3bM>)NM7UF$#?scp{>W^KQsxiMMOnE zXeY}y4ExO$)b0xDrd_yYl_+WMloq(-+uhkYUXFcB=h3cAd2-sUY@L@+$@^mk3Dw=# zhqUltx>D>tZ#wF%tgJkBz;Zpx4txaO-swb+q-OM zy-CHwCz%hko)H@x>mL&r8~b^5v}kS3J79f0?9F`Hkblt?4Gkyi>I_C@cc4#j2Hfj6 zc8~FlbFah|P0hIhNM)*`n<|M!@>?4~mO8W@t0bY= z*x8->3eEjMnR$I}VKXu`Jh+iW#N)Lm znAo)!FJ251Y{Ni?^P8KmZiA@yl6*!Bo;=~?Iq!LOuD77Oyq0|W>(rE(px~7_q3d09 zMMTM&xX2GRJJdPTD(`}$M~||yvAw9MP$apxp6)ZJv}4xfw%5vuJ)d(8$Y0`go!XwB zy*GG!?9ERgE5^0Ha+S*W^zjh~kZo&gy9R^pUsJr)@t6jBPP6X>X8J1c`K}jQNuRsB zGx>IYguR)JR*J{rj(;ZvrJrC4Qjz^#krgkk2Z!f?{5h|EA8SjVtIP>>+2I3Osk;kSqlESH?3@8J`ZAl{?QF~^$oKiyr#AOl~e7{ ziU8kkPjC<)38bn7T5N1=EZ@D*(%#O;+*H-=P4t%URFp~ija|9=hqy#W7F(fpem zPDvUAh#MdfICcq>F?8Yc=c?i1;VsS0v?Y&VHeK*)pw<=NK8)$-sevp~FG7}#8fF3=6j9pGuMjLxfee8^ss(l&4cqA`3 z7bcmc_2I(@YEKd7EPF9ne`v|?9C%Pz&S~harKW;|^bj;=*IUcR$@Fa3fe}a_k?}-% z5QP5=vX6V+(}N&XuN6lMssQnODD1cBe%rqo<-bknFQ)s&GZi7qK`z6k$AL0E@T=aq z+z6)SsMLvxi4v=~bfX^A?a$yCOfBef%PTA7r6xWz9zb1n`!x{xyy)*TJ$-$f%J7>v zbE~T_oK;W|my*&2xar-;090QD2+8m;qM@;ostgBIJTYMdyW!?W@Lm{pM1r!17zyst zMrd>u=nn$0Vu!XH;5@2)t${nXTTzPg@wid z!$6;j$24UIY_0+Qaa+Ox#GT3U)WxQ}vNG?EY0BbgVE214fFc*%0;Km3s)zNHLb;%z z$JZo>0@^$Z62yb0_v50K{|Ti_mV*ayhnUOnVG>yTdk;~&2ML&?neO9t=&z-wzkfYNRx}W8XcNKt!izMdrMWPbiCP@;ktDSnzIB;j> z*;(7hgF}(DAow0<2FZO@??eC;Ol^VJKED#lo|~7~GWDEBb7?(l_h{1XC<_Zs5s9j( zSOUq~($+>s>lzs)6nWB+iclWA`X^E&jcjb53y4Ze>fntFZrIvRo^8m>%2Eg98Jh)w zodQt$!CPqG=Iec90$XW8aW|D%`$5+)ER-C5w!506;`larZyEa$2>NX~f1cpv>e`oQ zgcpEqZ*Nnl6o|Q-3svpC6F+BGYLoEvXFy3 zlmTMmZ%rI@_k$7dc_C?cR`I&G6#<8!Q1-)n!38F=D2hqIjwSW@1gHmv$iK(;R|A(t zwS$qy9Ncam_2>OJtnQHy9ps*lgX*I0-zG4x7Mx$d59Wn3Ze|CX1ou!0jdjKS!aeQg zXx48A8bRafCM0Oue2?1XE5C;GravZU5_h*3%h**=_c!~(!&X$Fh*{xx+l%jR-5;7>-XLb$ z)&%H$V%W!lN@fz^_GMcCF!!e%sM+1w1d`*}Rur<`sS(Sy2PaYSDY|>Ut;Nr}nKHO# zh$^X#6V^3FbpLo9{`PUWE|x5ONeE|K?QrtZ18V9SoOT6Prm<1_(4j+lFJE?~DWe5o zD%&%;}EiO?R-e-oQXXWiLb1@DXfF* z06wq8w&^e%D{H^$1;FUd#oi(v^WM&&gS`PNi56k6xn5CaWzw&D8wmyNgW2rB%H0dp z1s@{5tLp~P$Rn%E?59obti`6S&@%B$YsrKf&X&8IANxl-kn_hu!h@Eg(Ld|z=&Fs8 zrDc&6W-0Qil24(SxOkCoO?f%oxi`OaxYDDPrlQKq_F=9@J{TP7vp`>8nQre(^%((F zT%8_38c572qZqi>Ze$}QlTH!$*l;KR;Lu6tKa!~?1RUIN$l#Ym;%HWO_EXSRyN8CH z7phluXkL;?xjpc@!GCjTRKAhZz|71U$Qd52egWsFHD1L#b*rnZz?lxb@pA5WEs|+O zRNw8CFn{w|!lJr=Ym#r5>0MSvh<2!}Q1kn5kM_%smuNG1#q=OuL*(2h)JI+JT*Ma^ z>HujNoAvIU>ij6tvN0p){vjkfNy@SixC>Yp@WC&-b95PP4aFZ6*Te$lS%^&Y)d0SN z`CWX*{eK<0)Z~yiHxBq91VtHDZdN|xhyRqS#L;oih3nHA#@NfNbb+|La|tUi1jY(G z!q=N?kW*r_x4h?3v$`TO_yYEDGCzw>t9qTQW5~GgWRwDs2jnE;mSBG-HW?2ua(e&Z z5ODowuQcz`hoXg==`|v>L$lKt2-mc=fw<1`kI{X0v7o-bey%o$NVH5%O?`SX!0*y@ ziM`^GIBaumzPnY5n6I)me&o&9L^HZ`&yCbvT;cE};Ig0Fw(p?kjPhgrJ&;;G$5k*Ft{M!=;pVsVr2;ovqZMV`B!E|MWANqVk-aQ*@oT&`L#YXu z?Z*XDRaDcY3_$VJizEwKkY)=D)a>jmKnkMPe}I*p{ppY^kz|KNrX(gNT1BI~L5YD5 z&jMsKyCmWk?Z0-dv}mu#aEkorHB6#`s$4IywsB$Tc?IlAjfr1;l@L zG-CDFpGaTk;^H!`xRW6(Cr4hWTBKUwb8{azO@c$8*0g7U*)t{B+y@ppJ}s>fs70%& z!`Sf04yv)>>Will6?!u>Gk&wVIcKD$f5Ud(laYCz9nmom7(EHR7^}3%x2HC zb2vg%%VkN6>X(k)*|1n(Lo>Iw{DB^#$z0&R;DDP?o;p?Pzu^w+st;#LNKT&VS9(UT z#-$@9AbS41X_s)hQlrA+= z3$?Vimby)9fUS^y_EUEndGWs~_%7Sgw0l3n7TBo1%$SQ}a@feP6bc3Z^5xah%@ImG zFi1hLdv%;HLbPRp$CB69sF)>a+$t;imXp&BEv-o70#mj+(WWC22_X_UU#lbA(~LAV zn;!Y>1siW5v7Hpq6s)k!4E0xKPrzvV1}zEVUURPtO%C0Eed%njwG zT(IG&{G*Qky!)4{dJac4akgv|7nQvwVU3(UpFiJv7rH+pD$2;$xAJvyu^_BnWw+YK z*4B%<>R7Y2+)CSl0$W!Cj@wkS-$b}@aM_`t>!;e(fxrdr_4CjWo<=Sk%U@|F8(g_R v$mmoXLnHeyaWJm<`(K{$pX>+3?$W<{F}?BN8>b79AQ0?|j(VZ0Rq+1+w{wv- 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 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 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 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 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 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 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 zcmZsC1yEH{+wP%Lx;qr6$LvVg}sA21v@J{I|UmTKPx*wD~DgrfD{Bm0g;syQ}_ISl;NSRK7W1F z#Q}qAwx=)3{~3-UvWEgmES@%npuC63#M|Eaa*t)lyJbEt?MM5XBz7p4Y-(@nQfeV#=kG{6=V?qD27^y zd472roScm7DMh#_wFx}-55&x}b)NfG}3Ch+WLSN4%i6$dUdx7$3iLD@lcZC2R$>EYiyB_)gc zf}$d7Ha5($u`%zexQK`z8FJr{qD*gOK{E8jw@L%S21mvPoBQTm#Ru(1Tf(sn0R*x* z1n>tUs-}jms;atiaDe>ra}|gvD8b;0prN799&gsCFUQBm!kU_Rgr6T=np;|Wrl+wH(8(mF zrQyZJ#W%LMLuSjgZjNG}+Qc!V!>X!S+#YV7V*U&d57VNBOC%>JtKdkfsFoyMTwLV( z{oBEh%%zPS|Dy(>t($mv3174=9(U$MHnv$Cd zfFYFu~d46~MZ{NShQu+Al9B${& zADIYADw9r9*k0nG>IyUi;GEMOS|n) zy#Wv5V5US3C%i95RZWd9;hiMdCo0U$%ri%ebtSvI<_BO?CKVLC(NI;LIv(FRMP^Hr z{xLtFT3b_-b98*X?{8&gb@gvyLHi(4NjQf#GPkz2mKGl$f9W0Jg_M4SHSXixs>gZ( z78cgtd^kFJVHg@|shd3-8rlq4ExO1r#3O7b0~3GpRq1v%H^nl9eOrl@6e!a0Iv#}{ zPW_+dy4I?VU~tLFtK2I@DYC&gmOS3ysEOyqvxSxRB)xl`)Y8J2oRy{2WVh0sKqTTP z2p-$t5sqlv$;6RKnVP=-{{8#L#zsJNH0r?3`r=|bnj(1q(AiHn zQqwL%`xwEC;qqC+q`0`aylw|LXe7J=|1SUB4Ev6K=L;Sec_z8PK9-P>fFU9#Hga%4 ze=j>z(%$}EIAzGRxVTuf%kRr7 z#wJ|%7r&u^uS2h?sp*L(2S_UNx@4?Pw$zX#l?z%ef~G@Ke4hh zvgZPJD+#Y71iS_o5hnE)xP8;Ak#JGgH+R2%wDt94B*M{h8X9o*nDGV4;$rpOJUnEi zq~Ii}Fd+ebn-qL}gyep=l)AdQj^KJf{hifH<8dH^yuAK=zy7SOqZ2VOAYHCgC;g{G ziVBmTpZ}$7ad2>u2zle^=;%oM54^=RW{nxy9!ezq_i?&V!Q^|Eh`;d3T1OP9R|elC zK7A4d+Z+Q63mOzVZC%~c7TZGk^dNHo2h7;m*o7Js1h4=TZ<=rd4Tl8}PIKgG`m{d! zhWb6;^$ZOm`9GW^bl#r`K)UP3g`>oR;d}b}V8B6y1UPj5gXiYv2A#RP*=dW_VvK}Q zw;mC41~!~YZwL}1I{M(*W?w>D8X~ybihr4njg7Z=hgAvRzoW{?$Q-S85X;KSc3gZ{ zB4UgzpU&cadOY>dD=tP9@HqK2qeh8=sK328nfFq<`$FaRh<_p{f;K6i*4W4mp0FrY zLUwH}R!K>THG{eMx+p3Q=%s0_I8sJC!^w`Wu0Oz6gKZ!7{P0I4&RbXMb;s_P(UFlK z%MM+yO4K9Y$fX$eN8)xk5x?|cjJps6wlvie97;+`UcU$4_KprrOw2$bpKHU7?!bXg z+?V$t`ifUIo4UBTVB+9}1Oz~|wY3fB%5`{M_mmx0B46GRBAlaDnyjv=$v#B@KEmng z!Am-;zMeC`pa2SDzup=RC5E@!Ne+k1$eI`khXaSy9|-dy?3=jEyMB@YxCL$Z?n(L2 z!|-pFNsuK-_-9#&%xrn9&2&$x!%%k(B}7#=YP5^LX!lN+XH3edeak_lMb@A~@YLqa zk>7(&kA{%jxu2h3oHr3{(2yF~6_=-j)C{K$uKTKt`mHz+R_o$7&T;&l8&SmAyhCWv1z|m~ooG+Tfw0Q%!5nK{_-t4zXu9Co$ zaN?tTc>AdaGXC_$z3^(c8mS~aCO&G#NuZTerl0rEvah4qMqEQ?>i)GysdP@^Djwa5 zKV?s=#6Ge>hY8;2517XG(*<7N?GbXB8nXQJoKfd|GQ!io^nUN3WJiB=nU;B|kZoQ! zMnU@!R=z3H9cmKHasYGQsm8YYw&gV*$11bs!>43Ip`p&f%YSW~fBu9=;j;)ISYaU2 z#0g_Y88{7jdwH>Ke*^8zGX1T-?Xf@nL&P_gi{2Z3?i2{PO+sSgj(EE5Ty4hg*;usK zD}H1tjiioFc7aW9)cH8XnUxCYF$LcZmx*x3R}cf!qG)u!&7D7vdTk6k^ehbtI2rvA zmPhJT%<_4Da*Uauk^Qj01)k0oFAU?!^uWb8_?EEk=|(uuUze~?ZbgT(1NvMk5M_Gj zNiV;S)D;E_o)q~MEp(1OvRA4(Yr=!Qv4-s)n9Ol^n9L;qTxf~qAPe}8U$|4O6dtQf z?E}Pa-U1+LGRXkX0iYEC3mcdJFd`%u+B_WMl4-smW3&GLE#L8oyu6ZNxEd(xLKf!K zQtVJC?Sq0J80+TPU@2Ct|LO@JQ_MkC$gM8u4O}qG4sJCl`acE-sTmnjhlYj-I4zaq2HFiaV8qH+{BP?-+lZ?T7>(6q+u{}TS3FyU7l@v#IV84_U0bis`J0T$ zgt-}XpVL^_=RoZ1?byKlpLm8aFwb8tCeo0xPG3Vqz;mKM(c@eGx=K*hRu_a2fEcd& zz-f)`9Cpl8+;xFW44LD)HPTyijq!cf!Ho&nimEOclGySxdw?!G@A(m2&1Ewf$JFY- z0hT9-ZI5d+=*JI992}hN=4Jv={*6+rSnz&5XUpg*4v;)?yd!+X$3uTYft>lL7EL|Q z2_l;7@r?e%x;2J!UndUs$ao(w;*Jl=mYoaBXi##qL6dN?$m4ZqD8%nK+6bG!GVrL@ zXoLF8z18z|wdIShrB>|9rU<3~5%bwwhoA+0*kS3QZ%S?ojrQ$wIdd*vx$Y#Dgu|9{ z4k>LW46o(;(w%v9|M;$X&<{|cFmzmc6}|7o&u;I95AYz7b9xTk^&N2Na8|~;fx%l| z*Al?@a)aOcTOt--z3?R5dEx=p_Dg1ZTmsa?j?f|c$98`YB51Vspw{<3?Hk@m*tS(B z(|>7EA8g3-Z^|)S|7yTKHKP)b$64s5no@^!@u8F7KfZ5zvnKoFs24xpmkTw=LKSQ= z_0z{-MJCBSd9s%KIHSrcD<2w4wF(N;1;T9px%h;}<+%&u9e=~}VpO}fCZ+gcKTihU6jnW?bO0V*uAYDjI37B7@P#SR< z8IW=xBhLeWkset)Nh`MD5EJ=!+Fs)M!@} z_=~&^#WrPA7kv}Yndf4`h6Ms?PNBm2%&#A7rLbh9?59Hhd9?uZ>QGQcz@WWR`&1}v zC<_`bDuzX{Qk2T9FVrM&x9~4VRo~^@@xp%UsAW?zceqy!9}TX&I`K`tAq19vR8ws> z=B{SF>hsiZ?+#AqtThYe>Vd~nv9e=)&(Y!pBKyxsu+TYWX(2uBlLxTRd2)N%nfbl2 zknYb>94ZueM%6aBBSichhQiJ*F>vl&+SunDx3X5Wv8(U1Q1~)}CMv3^wzOd%&{_Js z?B7ssyCOq-H4yB6j9o_&hd^BH$TxlsdwQmE^{jsnKj8kL$8ZstKZnJxg{h6H5U&Hl zpAI*^N_8;NisJA^m#U7V$pkkDS(w-h4)|!0lVxUh22hp!z<}z#kG6pj>QlO$V<%le zC->P+=ui8@hj(C8OP`!&?ai~CJRpqlwz|B9YH3sL!D#Za{*wyU*)E|-w@sO*HzRbt z$HNsaHiSe&fPA#=6&>fZwz6BEv`(!P_XFfdlDdh%25hbS5%$8?&IN6UFH9_p_(t5X zy&EEk^HPE1maC9z8h`|-I|(~R^jjmizw?S>H`Id#H8va_X&?bM%st>}HM@LZb@)vl zZ9`QxChD8`iZ{=1fcKZRcU3tBKk3I;Xx(>mY=&Il@@j_QA6E|rE=iMv1a1&Av*XZF zZ!y(O>r~vw3Ew!0Kp^lY%$1TS2UB&l(X)qWJGWD+??vQ-8U(#(N965A=bArCrCdVv zDvlAZ{5_pW@!2#F;LojP5I|9DuGrK?i^*3)GEOMhap&rd2; z65^0-NaHnD`1B}eb|Hm@x@BFtT!b2Dhhz~8LD3r*oj+|(ZU}xU1_${SpKp)JyZf|j z-^o4Ryx^fsLo5b)r8;-U#;!QgZ@REgo$;|jLOfR%kZwpBeYtA~66ET=L;SSwy91{4 z@xDup^Ft8~#|>PFsN*$s`=NSJRpt^xqYHxaVE^j3%y%{i;dRFc1Me`ejK>?ruQ!5e z(RRJ7?=@e+8ge0aBm4#B-3sLW_cLV?8 zr^9fUGSpcyp>+g0R7nPAvwr7N%)O61Ei=LJ;Xpdoh?!V;JIh55VZ9zXM0`~;vk(r} z-Ww*7mZC!WF1^nmeliwLdpG+L-6&Zyfcyje7U`fWP86KBesX{s9Az5UHkDs17qluS zLQ*uGN1XHw&!VXJIx>4Dszcnkc=AuEdE$FYYEGB{Jt#|4N4my;5}RontO%B!4z)sZ?(WNH2#;lYjDkBwX?0 z*Wcn2TSQUDsT)QHrQ)-XDU*g;)*OX>P<@6@LIi<<1xhGLtY2NUj?6u^qbf`+6biz#tqzkvp z%9iEv=$~+A#D4B+n=ux%4qV%#5>!0Hdi#E{fr4NOE7VR5ny0{(2PB}05dXv7Gt$*F zHl%t<2)$k79nHYyA?aF8YKPCc?v{0x^iQ^sJAcH}0^S?l8}fut=r zrU5L`)aW;mn1Na``X6%rEZw;gF=!~rNlBqpodOsGM>iced;pk?&?^2Qe)N-xPDTYV z_G#C7t6)@nVEt=IP_G}Whoy`qh08c+Tg<2<$KkRC&YEM0g=A69LpDE2+3KaM0SpB6 zullDwa2#Mw48bGN;3WKY`yp||kn+c3!oIelRxptj!@$RHF|T5Tqy1)tq)gCEOTO zjD2}F{ixzOScBv9fn?uD9r>88N4NZvP}L^AU}V8~qdc-Vo}FAh{E&p@w{z{-<M3idf$lp}OOF=DMAHydMb{&~zfTap;E+nhoi=gqG;&WX=aD z0zA0Zip5tPo@N_0O#AhCaBQ0F@BS2)U1n}^sx=7Vv4~c z2R6sm-uWxl9XsEH8{AaRY8}L_GO%OiACUvNKtb=u_(N}d!B=)J9Llx~z+mvX0e#zU zvVIH7ku|CpF=EJDEW1iA@o}~x+(KbLY~9|kc7YX-;}dp1G@rt&_pofq>S1h*uPIwz zr=aacG{Pc(yh8)Ue&f2Cl-AFkA#&F*{NwMrE&rAp$hL0bKNAn5rf0hR*4-_bRDq4! zH`+UMbk&*j+o6sqat)D$ks@d3O=(n+*5)@@zgK+y?;2{oWBmuP+TB=!CZ9!S*&y{% z=?qoM_zSb5@sN-#i1Fzc^GPXbn3es2>0!ej*$>^W z!Us>-C2c&JP_W?2OcWl>(WA77YVS?sK_|VOMB&oy&@5z+AU?7K-_|;XxmeU&Q>)n~ z`P=yZzS`8vpd~R06FTs$*o*O1VXr7%l)a3uvm@&*0et!giu8F{pG)_A&Hh6Y3xd?B z9TWD3Oct9%*S_6)x&6nU9zGY!qzKlBUdAx`v6^aX&kA-d@>$f$dsHL5xn@>lawbZI zfLOnsxAuivnaD4wv`Ry|7i{&N***+kUm~=|i)ygoEu_Y%bw;fjnOMgO$(ox!XQN z>zub#7YC>#w0>y9u`#zBgDq#{dO?YHB#zn_&&G_D2%&WAl%PQbkB#1(w`SsN>^e>Ts*?@}ofBmf<(#vbmVt zd^D0^#WJF_wpw1#PHb$WQP(3(+)P>)nGs6HP_M88^jzzQzM$x&cl&V6sq< zr>@TEn&2qbl}=O++p5Wg`Z%I5nz?AQ3|_T+2V^v|4Ex(P?-?e;*g#t-oIG3+{t^Dj z4snfyY}d<8DyCBpyblA5#2;hkEX&xZ4Lu^7-oX@`X13hT`;qfTpuC(kM1B4ff6qtz z&z!`hzT?kNkDJln-%AJBug$R((F9Q;)-Cw~df{Amcbw|_mjNSn87V~N@rY+r87-E$ zIustaO7%6ckSEg}J-L7Fbs@XDdEpxVNwmbqzWF?Du4V3TfeuBGIB1W~ajr;pt4r_h zif`8co!^Rqiiyuvw_nxA++cGYT0mO1#uw_XTWp0bBa7STJ*pw({;!FAcQ*omb1FO= zjN`P77@=CC+`Lst9=%n2Bwr5kYzx^Lk!EOmFv}#AKneYz1xA zt7r*v%pgFyNEhN4(jF!HiOUCc^9 zSH{(8Ar-D25yS~?8+HR6w4ezwhr2Rs){3A;5^WsKzwTUhn8)~{M!|?b| z2iZneAHFGPW+s?Y|Cn&OJlF7rOkHvwE+Lc2 z7kr-6s8%wJo78g&-YtpD?i%Z`|Pw?zmg?<|zBOZ#I+eI>`l#1tJMH2?lAkWSm^*TSLpS1bjyTc+N zQ1kKEZ701ED zqhexuRnhGZ)q$)9f#UJS!(?F_7ta5Pf!_wSLl8S$v7sV5f}K9`o#lL)w@IypHxIy(pD zs5dciJ#wA%iTa8Rh!6AL2AU|%e5=gs*dOLM1}(M5YF4JUp^RLLNc2J_ z0}8HhuB;o}5q35JF>#nIZ)MvC=dCZ zX?47xo4-c~JXTRW&Aono`IYnF22avv&SG_8ui%=xA43s+@;f&S>rI8$d}xpzbx)B| z*M@x}c5aoD39R=UT$=2{K21B+(8DLE&50a|vx|#f@PqIexSVI_`+7#egvzU}RkaZS z3r8$ZU$I?w2LU`U_wT3rHx#%|1QFMOVtF1Z!n6P_2~R}+a$#yakUfC|2(183x9Y{p z;n&@c8&^G_MswTUrJw@O+D@l41*36{6ezj9~|W0NE|u`37^_@;_=r zPM&j^@lY^|zciJQD75caog2#PIPTUPi*>hzXA#CRvlP+}Kk`y<-R5ThPZj_NxyJji zwW*oe-{D)}CBBnOJgu9bo4Xzs0?_SMB753jEMmzWg`zU=?Wm=*zF=CC9duZ!8b=0e zgk5xz!Di`2?b2Fs7XkP6>KWW7>EQZQ35lz2o=Ii7!;V8sh~&(KO9^`?vS$Z=$_*7) zHS~q_{JUMd|GxI5Z)R;I7&(4N#dy#4bA*;6A2+b&7wac=Om{ev{(yA@n6em{g{WC) zS*{!mW@XF46Kkgdy=4Ub%+%5vbtz2chDu6UIC1n4{Ah`_J)w8?qcDb^V%BPfKx_OY zoMoV|sjhDL<1^;Z(b3S!e)@hTbh0r-rZaG&^7Pcic&{V9E1gc;R)ljVk__ji9syd{7Zkeru;u2AwM>r|zVf zbxz;>Czd~_p=SUqV63A#PM^hQ#YgRshXO{=AY3KJY$T@S>bz&e)$U(utDdnCAc&J;XzOtfjpD*MU*4w!p8RUQUjuH zb}9-9)nVV&3ir}0^fQYZJ9tIOvg<|8VDpZFmQvAEYFHWrQ5!toyXc^1erq$UGfMU; zURV?7cVqU(@@%z5NsOrh_7|_!RLu{U6lrMHQz0~5ug#J~$ZPUB4CyxhXek5Z zVKr0hBfHPFt*M1Y9tbt8t*yONQ0V{NAH{X8ATR%tD+me-GPJXcsraxnehTshaUxFy zn3xyjLC<&dU7YY(m}DP+zMrb!uSqG-$|46zh@MSHLv!;m;C$qEc9MZWg(P@D4-XI3 z4(r0a{QTCW5~(Ri&F%YVb!CHCH`DcZiVl}zZoJn9Q4`GPqPpMjukv?P8d9U;zbl==6P}?%$0(iv6y+G5X|zzI#S+! zju+X5f!HyIf6sz}@Gdj=S-G8O81!goZQ+w?pWT6pYu^K&n`bWMCY~(G_ zGWqY9t2v8IijCh%9SX3oHv^Dg!~zkPO0{BcE0oM-$4a;Qyg>Z???w&vP2b? z#$R20pqwXu`$aO*#GEZhnrr{@9(XJR*Yot$Z9*?jCc*o-V85q_OC?V)ubjd{c+d%6 z{Ov8lsD9((G+M*$2%-3ES0d|&IP$1OMGfyL-ZJAo;S%eR;#!>5x2f1F1a-$b3oTBE zk~C|wlArZo+4HpI)vO|8haC?B9q1=5_pRE_>7Q&Dml$s)xYh%6qDb+KVuAKk_41m> z4^<>9SV%tkQ<6ZnxZ68No(81>va!1xW;I<%$-{$xa&l5!S_%zO*U;!09aT7PEs#t7 z4K`@A=Q)*LMvU62YC3m~S87ZF+q()|L58Pt6>w{y5~_Dbqum;$Zy^_uL_4k0F}yy;|w zBXe___4V}=gTVbJ5cFjI^5x6kdt?B829xO2Tw|xj2>nA#n2vLKtm2WK>YXWW#~r5D zbr3Sm1#S1A-{#qQ{bhY%w?ycST?qzYFvM#W0aTjS_&VY9bHRiap)!mbhxp-pt#`2c z&Q_%Gj3wZEpM=<8rWl`3ghLX&@Hhow6aT7BkB8% zp-A}VeiAmW??H(HNDKsb`W0U8tIqsV!IkILjKY0Y?^#@X%DBD7MMkA{l_N8}h~pO^ zqIx3jVPo%rGde|3LS(#VrLDo`;S<@oksm#Qnz#0%LMH7e^q>NVzQJbR@py@A`_G@P zK6DXS2(N&^7Dx=xGBW-Emv}{^6=izByclO4W_Xp1?^K-}4xW0eW+A;oZKM6D5YST( z$$qY84pW^1wNn(phPSjWtuB-RuR!BnY7OLZ2%db_+mUQ}k9 z%#61%OHrhML;qlS#&*a1aVAWQen4Dab~gI69&>df0Rtg$2hQp_Z5>oqa^)#(WPWEz zYNzi%vpH2PE@#*L&C&A;1-EfWlo&sNvfWtRP~qGbdz1s5Xz5Pm@(C&_i}A*TYhSJ% z^~%5P(EtdvaVkdYlg@b$M?KIiB06z;3vs7PZ~C0N5SSW;YBSIoJJ9Uo_$l>NoKJO@~|a&~LW& zABFm&lse$N^z&k+&2u!qsQl0TlEKh73T<&47iK<=-tnYWa?NZ$0wPSze4_^gc$24Xq zUg%kX-ry`SMEFkr%vS2`Do4DL;y4%aV_b9T>`5Ch5ugl63I#;Dc`|o-){arJ0ada* z`TV8_7&T()WO7@C!J86H)z{hnS+eW^d;Rn%m`tB|)o z7L~7Q%PkbR|2Bbje@NvzycpAYLFF9&oIjU2%`BDw1_79b@_gs;?!w=~gwO}5hR)eF zag~CijW@-jUgcwPBM&%O=#}XTk1Aj74_@O*5-E{V#aMW!eKBWD7&y#V%0KrW_KkRD zTyCnZBY+63b+UauS3)_+ZGl(wfV-JViJiuv`!v~F3ow<-4&V(74i&be__fTNB*l;H zoPM;nIThReu_~X-iJw4m^n?KWX=V(lMyzL5UTMz1Uj@K13U*1v9q-Q zldEqCm0o_?6)gTaM__=xW3ga)fE|EaMAO%(DIhXJMq|XsQ<_`BS zPS5`EI1f+zvU+=tpiA|ZTrYC(Faxuqh9?mMQ8b4>VjzsA(IMVl5`J;LY5=?Yf$abV zX(mB}_H?^-?S^W&r+a;MefNy^^kbB$LDxaJ0sG;{=!VitbkOv3YhckmuLd zxk7N&fDNt)FNo6SG_gfMPmSq8LA-L?N(JgzRB#zinSCDpWwvJtEv>fPH>QO2l%oIoIt%}iv0Fca^s8CQyW0J` z&OCNNfffusmfzhZfpVNJVIBtDTh^r}t~M*&YGu(}fVos^<6;MA4TVb*>f)r5$`Oh< zC?B0TQv&WfH?4038$x^)2As`~DPp!VNZQH&<#NN4c)|fIw7Sip^5za`7Tmxc0?*<_ zuLPp0n68@0?YGM_D=VrmwjrQne+Jf42s>uj?$}(~Js;3mcURC@LPh7$o}=H~Rytvn zFXAB%8w5;n&_TAXs6an|<=kmets6HXE^mHl5X@Mv%>L+ehRr?gU2)2vEGk`dUa5w3 zjgZ+*-*kB8w*^#Q$o73cK#eaf-0q5OdsPh|z=(xBfIwXm~R<&E(c?Dka>coX~$=|k7Np@GeqV}-%-1mkJpFo01N&JJI% zFVLEFsm}(@-Z4iMNbTURkTt;syQFckn^fe#bK;8v0S!u^gZ~89eGxpVI3Q%rm~`uj z4~E++jO6a9fz0R|{}RgvEEQNH`@lm=Q)ivbwpd%(gNrn#q<3gt z;tP#s^uM6N>9>`T5a9TD-3HaBp!>=9|4K#sjW_H5hKr6|iU zGCP|hBrNRrLXBd^#>j{~_%{_TEn=4MwN92WOyF6*36@V>m!e$e0L+H&~$Uvas}53l+l_JbHVS7y65TlZ=@$} z?4moap{ewK5#Y*3_6OO|-_3+mM#={0xjtLkh>X~`ZxSD3Dl1nIx)%=_9X(b za5_4=W+w^ew`qWrfs{O+xw*M^=O;dh;pkVc|B0Tjtmv(@d7zIA-H;$7gNcFvZ&r{0 zRNeI3lxP!e^}z$t+BeAUJR0;1$75R6SVdR}gsI zDYPn0&>dIG+U-SQ#nf*b$~}uXb<~h!*&kM_lx{rfTSWgphx*Co6nxFtmszNypZSeB z>ClOY$kKGYF?U);e8g_UXf6b_VCjrhwusnR&9Ws9qh8pw^dXp8YHDf~6%{B52nu4D zkA8(?PDSze_lJdpdqL7IEiH)1$l@Td&dA8fYBR@3!tYu&e) zBK?~u=S^mlJrAua4#c2@AW9(S1ks~ZGNKy}PQ$)l(RLTo=l}O2aca+`|G~TWDwmf8 zk$=oGv8Z9s!cCu6*s&$=TrbEC0qdcbjp%FWfkFmIDnSAE2nzujM=JK(OPLT5d#7jE zvEmjM7LLBY{4?8sjsz=yYN!fdr$Vz{q^?ot16fv==+toOO)m4RSgw938u-{$~mGZ;cc%{ZM0iv;yF!v~}rb}*&T2PkE=%{((0 z(SQW}SBJ4F`mYWnXVXTL!mJ+&2>T1Ia&&V$UJa+f$i$F04=rhD(RpjvpSc@YjKLQK zLwM&xZkR6d<|VClKe?op5++%FeVQZZ9;p8+dmBr^Z4{wCKa3!8dCT2W=mumQ{zYD3 zvX&T9foKa?v4g4+v>(ziB)oraPZJcH&@X@dAQS%xL(`V`NO?wji5Tuk0ZqQCaUKFk z^7rMq$RLOfL)jcEp311IV(pHk2Yamh;@_OCN*EbYO2?7Cip*8k)m=eRqXehzZwF|337$Cb4ovTjP+dR?4rO6LBR0Ij7 z!>+U1L>$Aj%HXJiN1gMlWHRGD7s3ZGPm0aJ_032lUCBQRfs1t()B*y;ZfGzj&+N1iEKru>)PQ7dtvbRcf~_pfK|gRjCmt=)-< zld)t*8hp=a3s;!Fn%cQ?eW{F}n;AD`bCpQz^Osf#-BWpiqYG>o2o2;23^oW>xUR}) z0IBE-TL-IuzNO229V!m@Me}0MQ*Zix{kaEETAYS15~N*=>m^6qzVC^jg}x967Hng? zyD!c3m!1%>QwWU_<0Co?SMzWLw@Hc3IxcIw^XHsB_LPB?j7yP50vj^e=`Y^E^~dz4 zfum*~3sE1QBiOjKu*;r1$0?zHQ9D4cFxutAH+Y-oAV8?1gJmmOpDQ`vg$k9j{FCxE zOw9{Mc;Z60cZa^Jvl4HcLDl*vOdE0ElWp@D#|-8Fd-p_MgJmgX554hX#@c{hjl$mS z$i^IL6eJ8#+1`ZmMVZw0!o^|L_I`4W$KBoPH>v4mJgM%B{{5%bRY5ngRk*Zd8rwwY z4W=tY@-7HBoXbu?9bJorg46$sJtgw@{Bl+T@h{^V2#8(I@AzbC4qyGmXNwB9DO+M+ zQ}u`~sjWd3N`z+PrNz5pym+y5Pu?qQUQ8{#^ygXU~FTj+4T^WuUc$qLIX zO#o;UE=CJ4B18w~?{ARP8u%A#rc~BKqcKUtQ~XNm+(zo_c#>R|VYN(h&wQ4BYAT|d z-ZOASQEORkPa7FT=((lxx4aJBVZUBZh()~Wj0NA?8VrCq0=L@K@i+L7K$noT3ue02 z*A@aykfJH?61}MRv#&~(uhNJ%Ed%1K(Tvq&C-u;(oi#zTW6;WllQR{osbP z)4nQE@%0iDkh>mjg8J|l%SkE{K4(~ad;5AvS6^Qt^Re#{VDjts-#?@EPG1m(hXt0= z{c%%5LIM<+VX|Frd_^rCJ;B#`K~|Xbr6eM#3lPl1j!ZoyR{h*7Nc=M8fMw-LH)Xe@ zk&8*rosJ?)q=ZTbO0&7@k1Gb#nf9aor%LGB@DjAeKCsk6BZDY;M@G!r)sXb`^yo&Q z@}Yw137{)T0_jiRdJjbSUw#4QVQOmX?4Yd5-KzI1z2!_)xsN)!3s@rH+fr7PWGNP8 zaiIsb`Npl~lgoHK4yEf<5ppVe#F!n8zK%IIe(#ZEIXiwKI^nb(vjzjg-kMfL_kvgQ z$kHS}a>w4jlYK8JGnK%Ul}khSrPc0^XQ4yVz#s$~nc%yoqWt`Teq%~@b}TSFQ(Vga z=~)gNWRW&AYGv;Ptc^ecMFUYzqi=2CG5}Ou?F^>}f69DR7kL!Jv zGLVVD3=I?njMo%brktv2Xlj;fs4J`MP7E;RP`fwqax8&xHUUwE@Z2k+D%@d7-9g`~ z$bDo@cPRHz;4$lW?p76w%GcZP&pqBD@4VYGF_PiDUYt8BY}or-YEOSyj9f6jDH04w zsR5r5`3HwMjYEH4-b9twBd|-Oe|=Mg_61sLmT^+nF(Y0SUMzPKXo}H9T*!ISG0;s7 zm~AI9M$H>&2o~(a%5+pb$zDa)NngJ?0Utuu2EiDFe#-y0uk^p2jLtnV)&lVFl?$GX z|DXZI?tuE;t!>*iQF<666=QW-4=b})*`)fJe5cW0F0Pv8XB@x`U$WSvMeYa3{*`PH z`#Gjthl-O9rlPu){Rfl`9J|Hs1F{F#M>YX(5WHLRTiY z-#uVxhMgzY-uK3qBdsKSVB|Y)z4@J=B>(4qBLhJx@(d~Rj|y!K9*9y)%0>pgSHhpl z!X(SS%+;2!DDxO_pRASFaoJx~l{s_c729jkm5QOyEo`{pjEPLuQ-Kf)upRvs62E+@ z_HApYI0u7HY-YnatwKUVq&QNPZ{H$ZUtiZBYT^q#*6!ox6wc<;aN=Sc03`R1-ShMK_t!%x&~IsI zfI$c&CMH(h*f;>@8h!pkL0SZqy+U2IsKlE?pK6s02Y@2s1gL+9H@cnu@0(NfL10bq z{hZtWqJ4zIE1TtCWgRl9w=&Qh$Rv@iznYf`A9(iTEMJR_?Wf_%VxE_j)5W%I12(zqrMOyNJ?wMy^>WVWe;cqV;BfC}urlVOVsCD1#!=Z2}$OKGEC{}McQ~1=4L8i(y z#2hj~LR)`yA*j22V_0@#!6oTdTxL1kN8)FM*V!xx^|mseY8WUGH*9>tLuPJIH5v~_ zC-1X|flmd22q+3nebZ+>9j1cK&@G-j1=$84Zk0EvuT;I0iVy1OfmGlRdj^ij@0E!x zD2SRJEbre4w~o!6tH~4bvV_rO3KIi!ov}v1HN14M(}s zRanZoX_Ymye^}Z^lu#_906A>f+Du=xCg5L9{sr665E1BN#AdBUIjtVerS=eC5`P;k zz;hgHX0zy*V(fWU2W<+vE(oW*E@ptqv6h`Lkvxfn~1s~pDxLN}wP2Kys6oG@4 z!B-$Mr5gTDfl-W7rnods9U~()XwUU*VM!!>jsBZ7Fd6jwK`XcRV$rv?aqp91ZrYze zgx(ROLr!muiIGnT0pmgE&vzL{^;|=!JiG%~jI1=J4R73%mk#K~)33Lm=`cs?u@sE| zQn%oJt~K3CMs|y6(`kHV{a5hsrrm(ar^1p37{WIzWcUE?!Se9%pppru*_TyS$>`~k zfRR;9eEje*_?BESwfg^LD^~t>I;?k+f_Yn@@rRe$3F_ zO$y&;mQd_I=<&^+r_D2mHZX&W?kOtYf=Cib#1E_)YiZUjEliuXzcbM2OnZj$6^wdj z1UjGV{+)jeFEnu_A`g`Z6hx1g`*Y2go*um$>zX##BID?i>Dfq;)e=}>Iz@*M5spAR zS>)_}prU*kzrg<+SR9Xd6=~z+I5Vv@)-Zgt*jjh0QKVY0yXLdamieIs|Z#*fs72 zj3F+as7HH!-&5=-*QeEjDXpMFAFTeLEIk$&ph1Sp7N@-+)qAJpe*; zp6ew9ATQPNdr2^*Wxy~Ie4>ZV`mfMwzEn_gJ05X!ksMZ|_M9D~XkOxiZkhP!u_kn+ zR@UXYGsET1ZIv`{EfJk}$h|01n87~jiCY)%T3hPkKx8P=FP%kkwAvO7cC_xY9C9duFsNpM`I*6fc{OQy;HUYgE^pw<7pBqoV z0tH3hm4nJUr4S7yKv)Q@)nc{7zX^2~(R{J>9^6^Pc7;RUr=I!}Y)QH(R4S%FU|=Qk zzE$`9t*|f|u-GguEk(gBE|}lNASZvO1;VdqE{J}WfCM9%=l4nn;Z`9%%-wnpYIT3o z8YEGRr`<5=f5OUIQ*1B3!8{WIkYI$Ck)w`JiDyGCN807|U6qYrucT;^e(n^mP&!%E zxchd#m;A8x1B`aFxgTi*){X#7?}b`36wPw&Krl1@5^Z{`_`MsnM95EuCsglfU&n17kmaf0^ps~9xzMqJ(Ea)Wgly!7+3x*YISvk$k(&Rb1B!m1)>@D}JpRyXhBe7G$nb zfx$8DOMXY+krRUsrC@bLnOH6`jFq>$%anj2XFuMzcm@;wq$R*l0sSE_$`|dK5U0b7 z5TC`m67DXa-Fk-hBJ&E$f3UqT^P)NVaq32&EdrM&wO?#uF}1$c$fVr#T?=u=`FHW8 zZ-b3Ll%u#w?1=(zN~H}s=k-Q0YIWFOFMiC4c>^ggB@LZ*%K>jz_+#~z$k&b$6;8tI9IG_qsr1_BlDhvPX$tbZer$0Zl6nNzXW-3?*@0({Rb4Bu6 z8qpy?r8fNW7OY5uUubRwU~8E=7OkIikVTL||C+hrXwaZk89P@uD|r-Zn6+>K6RJeA zp-3+xvO!t&(Hw#C`^ay{2A0WtV+y{92C%;6Rj~!aI3mW;3G8`s$(0!@1HeiKhYSt0 z8zPZBpWj_s#ZUmu3XNQO?E4Dxe!XPavR5$A|7!VvT6@c=D!aCAbRklbf^;h&CDJ0@ zAdS+!=#*}d?h*k(ke2Q)>24|M?(VK{uKV8a-ru{QZ;bu^*kkwuQCRD`*1YCC&htD% zghSwyChoV7W$=F|!aHho-n(_nSUU+NcVK`^+1j5a#X#DPXaDrH!xYF_U279nbQKxe zKe<_Fy&(R-XinOR1@wt$dhWszm2r_a2NSgGs}9`boD0z4ga2(EGRINC1J z-8lA?BcZhH6NG0#f|Q_|t@*mNA?B)s$T@d#fI{Luvch5UTM&_3$WaSEeKlnWS0V_`<| z+ir_q0umODt|6>S<@Tw+dw~?EPQivC@gmBfq|-C+MoQ7q$O9Bj zRHtjWLcSIFdeW_*w0{TiqB_Id zs4NA-kJ$wW9x%b(Gn#XJaYb~PHV(D~KOo>V@OC|EAgZ;o-16<|@@^l!k^*0ItS!WMsrA1E4lud!kyfD56DqwU$ zuwok_2&*-#U{vito8&}|_yy^cNEHvmPatz?9sK>{OVJ-A0A|vu4v?(k`N}+&ZN+xq zPiV>gQ_9eoFo)?^!$ExI-gKB>{w9lh{k6*3Y#mGo<@?*W4FG!Ore&$-Xb_V5P*&6V z9B{$2hNHlA6=($%8klI^;o9s?;2}(rGZB-decOGsp=a@$FUQ8hIm9HTP{8Fg$h?%g za7yyi&>UgY6 z_~qif)`k!32c=fm7wV)pexffI4U}u$wUwUeDv5Qw3&#`=(w@JUecqYEn1;pnmAAeZ z%(MOeyx_a#5*;Ekl=|P1)9q#RBRc5sp%HVUftH$xxcFwu@_!pL7`S!Ua2zTW@a09Y zKl&k_yT^iQrZ3<=T_F5?>Lo>F#fa-3hcL2jQ-QPeF8zW}%Bt#JgCCQ@^w``$Q(9Nc zBk|%%_vMKUFvW=p7ecge^+o6zP#Pw_>lS$MDNrq0*_kMil#zJ~nfvXE0}L$0zI@>a z9w)~$79~RT6GdsOMt>3UBhf)?0ZtFGm63_KTe4X}JA}{tMRZy|9XgPR6r6+BXiiiG z61m|ETa~Pmr^`7rrp~Iw=DBW3Kb=(lQ$CE$mI&$HX{3YAGp&WfkC2qq%7zdSJusj^ z^}l3ScXW9vp`d_eXK#P56Th(M>vc^cDHfuakJUpj+DWqvM@KefQ zula%l0C2(*)yage&^Nu}!94Og7Q8uoKY(0xs3{Q}oU2c^N60pA&Ud3zQX+wh3c5d2 z1^PFlMn=>xU-7Ij07{#pdY-@xM-DaeZGptdGp^N>?9~TH=N?9*p7F9A(}BjO6y{b9 z8S*eXx&ieQKB-Qr3U&PeK%Q=3-}o~(UCZefbxe~NWIH4?k>;3^t&80^&WiyWNwo>z zG!$ykHwsFcLSbw&KsE+_UKZen!*_S&w`_*>4+Byf9*77_+ZYRo0-p^75*Ng=WW&Ix zO|8lC?UU<>l!Vo|uuMlkN1YgI_HF^|0YNb9a`{)VfB4A&<;;_iMT8EkCQ%dt7nHf~ zycp-A4aq7i2L~7!LGYYO5imy+rv@O$PyhYU4C5;kPbN z>sdfI^T)rQsOpX*IXUle8=Y5DfxyD&Sao<)o zBSBz2;9ZWyJ{P*o{I<;S{KAE0l{(FL(pQhO4VSgFyOmx`sqO@kzU?+2*C>|Ko(SjC zDiwz?nwgsVhW;?s|22^~gU5esSF?Xz6)@XBs=}kd8?5pT%D|hYq;Gf}94JX9NcsX> zO;k|u>*>JV1A$2b)HuOCfzbHt?!B8b+vRa$(o*8ByI1q%pRHur_Vh>!FL6&FQ0M0=W-h+>&0b2 zC&Q=oM?)y^uQQ*Z=e;{>uXQ`82hA_onx52gM~78YC@8I9l?7iexMMegcsns;6swpXt}aKa(g*3>az&tQ{23r{_$({n!Deqc;KaLw@{ zFWwyc@cZ5Q2~eqzzV~Q1oWNyMHCfU=(WA%z@(glSH70LSG}E|nA8_&5@kn}V*_qH{ z71(I>cr`Jb;jTunl{Rj1GuV|^xJv1d<;e_8jP!RX3h#ND`HfKN<9|_>h`4@zEU3xG z)WDEUF688Ya6Py6cyUK4)j*^FO}VjeZ7iq$R}3Zt_vAdhz-RJ>W}HB$=&HNlaCx+fbG22cXCqaunK~nkO~60SHCjdWb*65e zT?g99iYJ=nZ!8-Vljrz8qC=*p*P>P(g{~U=aCtUKyPn;ujGt98$g*QVD)3Jgv@9Qz z<6bm^+kbU7Cf~}825Ml?9rmfO=cb~f0v16iV93|e>W2h1G7302_)JAZvjRZ5jgwOs zct%mk$y0@*$no$vSBgX46Qc~zh{GE=+>Bf_=;LJ_&+W~W8ako$xpQNgPW-C;OdGPS zLXH_#s|u~IqWnGDLwas6?DhzPpX>elPxb@>**s6^)@7nJ33HVsFcnTfRqYzBedMk2D*%$oQNw zE@Ih3r?1O|A0ffUYvX%X8^To?l=WFQX=hgu>Ka@9T^1+e7hS84O{*c7tj}F(gGU?r z)zx^1&5!({VPUqw#wS?xCE$BgQd1*n)_fKN9z9nc{G(%Ia1g*A21Xjdd#DBU+@imI z(_fJn?Ae0iq1gWv!*puEf7Q~5v7mL&Fg$la*l&)El+>FCm&VG8j)E}=@x~{oZ!r(b z7OIganMH0c;e6t~vsq^pL6$;r&f!5~fzm2t!5~wq%(oz2wM@IUrl&Dt9_Ogu^jeX{ zT7XH+V`xVmw4zBPbhny{@?y__hKK=|i@qLCwlk$X->S{-Yz^N|2 z2WMMyY;rPaXh`nt{5&1(Q=q@f<$6N3vYO=>JXx$AmMM{iTWn`%SL(E{0TBdau#?kM zC}M=DfA}-wXl4%AJWdbE+p;%A3ShAztcM+21)=YnxM*G}7gihmtY^8Od>Xf&UcsQZ zX^$TlCeV6|^92F2XH(}(C-9n{FX^-MxK&o)i!TU}kVY4pFPM&1TtIHUONej!wQkH21CBwXPoj8E3!3=MoNvCWsw7 z$~us434Eu7UQd^_VzB z(`}!K2Txd$T=aKah8G%713$C1wKbTZ5in!|#D}^W*4X-wB%5QAW zb3%RYo|Qy{S{-KrqdEr3;yW8OliVr}*eWnY5!{YyT1igjPBfNR-%`dncZs$X5+ntz?itUv>O( z9*_2ew8rnbJ(RTg-24!|Xq&r)*qs>j5ie)Vb+8lAMVEGWQB_q{ftQs2bh#01ydy1* zdUNxOt%NqxNCmuaumAx`7jR4qNK|cqRumNM9(hs-lj#}9FyqF2t8ZJMxzdPSjLrL(t~izQTl zjnIu>lyvO0p$F54Df8g;F@8_+S>XBE-vh;0d`*3 z*Vo8sXrjQ)th^%Y4ICJ6sg;90X`5>k5NIHRPEMS#8B-us22X%8Gh~`d2IJ;2c(}UR z=j`t>ByHU9EUuoA=<@C1_SZ+axarB2fP3e8CGJa)diDrMEjSe?da*|P!|7$as2vy4 zPPg(KZsz0Izhz;p4fA18tBIfuBC?K4M%z{AckE~AR{BQjE8Me1cj|YjfAfA;=p{aU zDw?*pw7Fg$o+#$gDR>W+v(Im5d^o(y?SwDk*6#k2y6V=g|7>}AIXEh+8i0?<{y;YX8d4PQ1dwVs%UCiB_#A3spiJTrKcVby!|7dGBP#~>x}F{#npBiT<@=pz0GYk=fa05M;&%f$9g zFiDV^B|FAgsVPae>)0mYMleCpcFVqa{pDMTw`;XN6&Jg~siWD4N@A}&azlA;7E=ja zBn*7%fIg$OZ=Z)08}>bF3S7?GSmDJMticI|-OafY_UBRP=toY~=Mp;ggU=VOseLNF zi@P6JIZE1zOme&e!j^T@^kx))?>K0$yJi*lVncKs3dO&z8i7qrqeOdcom%bv#XG9` zpYYMYnkV%1%005pQ)cdQW2pZK=;mbiRxc7c!)+d1dz*~pT@qym4P75tw2DCUP16uiwhPR|>G z0}m#W$t(u%h8;R@m|SI;C1S*(uslA-xP*TCVU$!qlDkeGDp@xk1fZg-E~^b~jZj}R ziRi!Fgsm7H^u6-YQOO(WSO|bC=wX{xnK-(KHf3ALE?1pA(G`#R$}OD}Tkw6Dya=+Z zLY@U2v(m77g=7MT+uUX`W-Q*vNoO4y?U?noPa(COs#PMGNzfNNT8CB=J4Fa5&xH3^ zm?DxB#;2Wmu%)%79n4b@e*yQV41*q92GZRJHrdXFuFw}H(@CH4Ilg@OVZxwd`d8&3 zy5XqrrA%e>tG8)y8E7fL1w;F{M{e1$go9~}N2pLGdG^u7LMyORYx0~5RJ*?h)FbUC z)Mhcs@WVgT+^pw23QXO9rhIw?KBVNKk^&zQQ$|A6>(eeD zUA>PxB4ueFWq%EJKH-f-8U(0c+`P-ffn88E^V{lry#Z>Afh(^h2xKq=1Dus2onz$C zXl?u_o9To3zN zLxbiI?zP=bGzwI{EB@kY(Vom!fD{J$pgF8Y-DmB**zG4PN{u9z_Y6lnOm*a^(X3x+ z#Hg7h_KjW8-P-;pU}3mxC!aHlAKluMPn_qTErU!MsE>!jmhiU7kq~O&hk zUxfx2V6pT#O%wAvhM*>l+lpKnuiu>29CE~$7@jQqAcA6{!1Ebtb$M|`3430Flr}`c zZFqxl`%D)nHqp+n&{7@0o_r@kSr~aFK>h5Saz{11lnSx#cJX?hx3H!A_}Osj_&Jsp zu5-rkZmE6p3z1J4tr23&wvgKDH4!z|KzR)7dhL!RZkn4`B__#9*VNOBa)+6R?LJ8l z7CmHiN`HbF^*N|cv9s6`(%Y@d;TD=EE4g=Tr*C%H2<@Qp(5^T1E4G%;p$3$PS;Ve& zkLpSKDv`oP8pHAk{D09JOoHlXw$#F2 zm{9Vc(tUlqizV%Tvw^f>K6jY==uEqVy`0Pw!#wL|T84f;WR;u3=o)!m{dOX-KQ)Hw zcRBJS^;bL@`Jzan54WO97v}O!bVI^wY~K@3>|b(s@F5clW10vf6F#f>kWkMr!ZU%8 zrI7^=xe2-FMofELewSx)KU((v z+ijOE%%bAW+a0+dw1}77ln@~VMuw4U}`|<=>`%Vr9Ozd!A?b3se2{MhlNL z1$O!ajNp=OPIlq;Q&C-DifYs!i}mLKJrpm{)(C0=K`zj#lt6jW`Cvv zg>A8oJ9z}GJl^a_J1t`-s8nj0UDuFu((ZgLp62cyCDYYO^~smUQJ7itne1Cx+jSw) zw0sz5=g;K_zyeW7<`UH-|}$T9?P&<-HYr1r;Z zXD#s0L$%(e33p^34t-aiP8Uk*{Lt~JonzpE{RI;Ll3!J$x~CYGSPJL(biP-r|81a` z&nxMRmXU0n^y0o%!Nt_00=Z4n0X<#)na=zDk`NrZ%(7{r-QyRj>SPHnQfBil+w*q! zr&EC+kSWE}IiM=4s;>Y+2e=&rN^DySpF8mLIhYf2aA3c?zkg0j8nI!#T`L(P>I)o( zfxR3B71dK}YQIY*1aF4qNR1jomzubNTg;(B-omluSsN%DyVzLaD@ZB8Dc zV^};*)`^JZw7B!I_}0f?qNN+zFwZG<>TRAs_sL-GI26=txxoY8ie$g!`M;+-Es8m5o5v@GQos7>_E>N6 zW5!rpWa4px-L|UE0=yz9QM2WeLxJV$Pkzstv^QK}asJsRG*(u|9ZD650yYR>Mh>eF zAx91xjWd74iCi4EJ&tW%*c0?UcS1hzhMjQuNFADvZx=rHx zmOG!}oA1f28nYmJpiWGyW>|_91}!G|T5@x$2a$>mPx219h36Cx?oX<};AfVAO)Bx2 zw1OE_iqxxvFr%%Lc2jezB#uBi&eOfy zxX)!XiBL3O9Sll#fL{$-9q+zg5L_Vbxm@CM5r(40LIMZN2oQ{3y!@com83dW!B5Y?u)MPq3R5gJ-(L{Jn3upa zcx%r0)$DgHeO`@2W@>B{a67QDu)KlWes6qtSJz86HXO*;A3s8X1#wS2`M=K5pxFfM z{oNi}O{$KM$)n@C%t}A=rZ8E5i&G@2eB2up*m8WxV2Yl3i4vuw7MRc91dxR3=yF7Y zmnE?dLnN^SkE0=}!?T8ttEDyraH#uE8+=}}y|hzvTWmKEqG9}9E@_eO`blvz{J~*M z{d%`~PAw3^U{+C4&9k3*xo5k2?BME{iX^0~OD-iPRcbay^Hac!%%Ceg`dxD$S{8!D zcC>_uAEgM5;rM7GGb$>o?a`9ZPhMB-^z?LObo9HqAqiO4UvM9q}T)B73X~KAZht4VUNbaicbME7jeUB6;J<2>HTPI#v zKmC#2bv)3|BI-kP8kdHo^h%t78u@{f zl#~>p<`#9Stz^^#n+&+}wKrUlz;WNqv!3<|M*@BnN=*JD*>d}h0UyzqiVaKx0=h&(;)FF2R`jC@3gg_8Z|s8aBw3 zd(7Cf(>4SFe1?7%i{}FI>IzK+Xs?78#;t=q2KxDuk;}`KHNOAl7RpISk~o~+^-vw7 zKX@8t5QbrkIjZs60JT{v=RN|eE>?Mm8kE=QX2DH=`2LmqC}{nJv}|%YK&cg=V5cYAuIEf>pAe| z4+QiMDr^)Tw{7aR_4U@FAq+x7LXg2ge0BweLSG#uCa|UMi##@n%4E_zMqfmv-8*+O z;uwQEd`Z(;FWEi)E-IVfZ`SjaGtF)}2Qfr>B>JMv9F+zJv9Aq^%joq>8>W#=G(DCL zCww%uF_xIINiIG$+LKFCJ-7H-M7_O1GNo~r|HhbqPN$JWd*->c9{~I2n-6>6&-ykQ z8mA{C4fa2+;rL-1Q_;v6Wcr(_#WG8fiBQW_6jJm2a3)^0nvKQ&Roq(+_At8h^d9ro z)$|V&V6PS1NW6rU4T~6N8 z`C8z&*K0I%K8(_hGyVA6t=ydxHxVrV{m*J!9?wGzh&evLyIlE{`!FxIO~^S9&ClSG zY0uyph~D(6Zzr)B33+oz|8wV?lL8#1Os(eg*^UdngIS`LRR}1<)MR<6D`p!Bylm@2 zn|0l>F(2$S21Y$*5{(L3w~CRvqG|QeF>Zjd2$yxU+^xsL2BUng#E!|5uGl?}#;)d_ zmW@Dkg1N7$$frra$##kBp^vp2B3YMCBn%PfE+~{81OqRjJ1;ZX$+G6>x8h3aKN>oe zZ$`i$YB}HC#s1dwy-YB+A$}7*(WN?_T9i=0;!BYIojOJgC5d6 zj6mVJQIqDu5?OhP@0n^3{h?@ZlPgx`7Fur-*PTrg(@e&7z!u_+3Z`o|RSiLe_9boK zXTwx?jMMUeU<$l7t7DZ#0MQYSG0i&A#n75AZO9*iE3aQ}`CG1WMmwRun@6y6&(+F9 z<;z1Qzy~I?F;bjIQ4kTO>B)QN(obGfQmfzBOTnn75YF;_zErOR@~d6o3(ECOuC1j* zVQ1Yzijc{I9>b&n=CHCrsF%xzl*zc>$ZVBFTPNr?*nCm>zOec3b%>9N^7lN8Hwy1K zrUcEZqA0(wC(}~;Z}l@CFdLQ6TwV2V8%?#9v~34P3R=Dj^oQ57A`L1*5a=m5D5XPA zlSc#H40r;M#C#t^19T3vePD(Ys$3X#xvQ<~L2|M*_g=Pl%D)9uSFo$sHQ}b8NX0-F zZcrA~BNbNAc(oD&6iP#8<2YsdI4-dDjYsQdHG= zlFDLGZ6zo&r1W=tXX;h6ftCXvr)pRFUM0C3saS5fEIj15y$Lqntgfgdx%_UWLKpkk z%rmy4tab?ir8=y&7p7Voa+P>>qU*1@D?H@$EiVIijj}F7tMvBYb4uEn{(a}rh;Gzo ztg31D%}8_B3BYtP-O#8>vWF7VOw~Ne9eGb9j0|nJJYe1g-TN3Xjj-MvY{l93?y3$< z#f34ST9!f6-Kws=x^)-Yv}V6>h8pzqm+s-q4h4J*%T6hM-Yz0_04jptWi|0-XdHOxkK6@bLinn_2aKYWxMyIa*;1v8!uunm|sFrFA)X_dJt@ z3I}+S4RZ%F*i%5(NLM(a$mzxWYBYCz*<{$1(+qEsRXLF<^;pY{xnQAD3>C9$t(@ z*+HVv#xCOUCU)eU!ao5XG*SFPEA%~^YuiA7e><=|0r2W;VxswuO<*VBjx7Ic)6yG1 z4``I2GKJApKnb0glofyiPEk(l1vDQ)@2_96Vq;B9M0ma_{YTXuUc5_*9m4E+kT6l~(NLc}5rIYplSGhZ1+HJ`QhNuZF28fWB)>aUe1eaS>M9MLl35rJ$z4Ny*WbW6XcBEOn8nxq2sSujFk_N#y8A5KIgoAA%D^5k3dCBU3kRz)bUSd(vV z+&~2c59x{`j{;vqGIxnUlFjnn^(jbCA?KJh=>PuaYE$451w-PdU}}8)DF{xnsT-vn z4Na9luCTAuI&$G<=g1oxkwRv{h!JR~$|nefYiG{JFi@+isd)v2kU)Rg1tMPJ{_^Ec zt|nPG`6MO5>J+mhgPg1^N|2@iAQWKuGG85|5Jt!(GV}5BDrjrJ@zVXL2*bn?H{=F7 z%9wX={!UB)uOIZ5ty=NM#>U{Vuy(+~Dzltsg=DU}!L0g#ni;5oB_$KNcSCKRXAbLK{GM!fI*$prPhpDu@yT{tMIxn+E)s zwkOmw|G)g8mLsZm!ts_6Cd3;`uq`b9@K&(e^3UYuV^*o8S>wz;TyKl#i@$Va@)?yL zOHQK(((J{bxVk91N2*hlNh~9CjDLvEvM7K5pw>kh-E2H=S>Q4e>%|Ly8AEIJN^|ki z;Lz)>cJ`IdSkrw!ZhUW>9zn?6+=-EbS{K?BnRzw24chvC`tO4;-hI1-S(8i$r(fga zk%W8vY?8@nvo!rWJq zB|sE%#&^(cOjY`|B!2F3B&4-2HI^&HNs_p=&H5ANcOi{kYE~?aP zx$&YDQXmHTobf>z0^yLn5!$`s5x+0Zc@0K4nbamU0vA2qmYp4Qz~q5Bz@fFn;-z3+ zse`#Xzc>cf3~*wBhZnY(0p|r)Y<;;lf~i29oSa~-P0;L2S4H#3a?1Vi9SdjW-wr6) z;lXq0?|FNX4nnp-5>s$!sILO8sFKnPu*<_ix}(U06lndycmAud1!K(!fn|wf=j

!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 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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/_images/logging_arrayshape.png b/_images/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/_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..4fa507200 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,148 @@ + + + + + + + Overview: module code — openEO Python Client 0.32.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..2c7af01d5 --- /dev/null +++ b/_modules/openeo/api/logs.html @@ -0,0 +1,229 @@ + + + + + + + openeo.api.logs — openEO Python Client 0.32.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..47c97d5fa --- /dev/null +++ b/_modules/openeo/api/process.html @@ -0,0 +1,532 @@ + + + + + + + openeo.api.process — openEO Python Client 0.32.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[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)
+ + +
+[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..aab6da9a9 --- /dev/null +++ b/_modules/openeo/extra/job_management.html @@ -0,0 +1,839 @@ + + + + + + + openeo.extra.job_management — openEO Python Client 0.32.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.extra.job_management

+import abc
+import contextlib
+import datetime
+import json
+import logging
+import time
+import warnings
+from pathlib import Path
+from typing import Callable, Dict, NamedTuple, Optional, Union
+
+import pandas as pd
+import requests
+import shapely.errors
+import shapely.wkt
+from requests.adapters import HTTPAdapter, Retry
+
+from openeo import BatchJob, Connection
+from openeo.rest import OpenEoApiError
+from openeo.util import deep_get, 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
+
+
+
+[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. + + :param df: job data to store. + """ + ...
+
+ + + +
+[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. + + .. versionadded:: 0.14.0 + """ + + def __init__( + self, + poll_sleep: int = 60, + root_dir: Optional[Union[str, Path]] = ".", + *, + cancel_running_job_after: Optional[int] = None, + ): + """Create a MultiBackendJobManager. + + :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 [seconds]: + Optional temporal limit (in seconds) after which running jobs should be canceled + by the job manager. + + .. versionchanged:: 0.32.0 + Added `cancel_running_job_after` parameter. + """ + 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 + ) + +
+[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 = [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)) + + def _normalize_df(self, df: pd.DataFrame) -> pd.DataFrame: + """Ensure we have the required columns and the expected type for the geometry column. + + :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`. + ("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 run_jobs( + self, + df: pd.DataFrame, + start_job: Callable[[], BatchJob], + job_db: Union[str, Path, JobDatabaseInterface, None] = None, + **kwargs, + ): + """Runs jobs, specified in a dataframe, and tracks parameters. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. + + :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>`. + + .. 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. + """ + # TODO: Defining start_jobs as a Protocol might make its usage more clear, and avoid complicated doctrings, + # but Protocols are only supported in Python 3.8 and higher. + + # 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_path = Path(job_db) + if job_db_path.suffix.lower() == ".csv": + job_db = CsvJobDatabase(path=job_db_path) + elif job_db_path.suffix.lower() == ".parquet": + job_db = ParquetJobDatabase(path=job_db_path) + else: + raise ValueError(f"Unsupported job database file type {job_db_path!r}") + + 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}") + df = job_db.read() + status_histogram = df.groupby("status").size().to_dict() + _log.info(f"Status histogram: {status_histogram}") + + df = self._normalize_df(df) + + while ( + df[ + # TODO: risk on infinite loop if a backend reports a (non-standard) terminal status that is not covered here + (df.status != "finished") + & (df.status != "skipped") + & (df.status != "start_failed") + & (df.status != "error") + & (df.status != "canceled") + ].size + > 0 + ): + + with ignore_connection_errors(context="get statuses"): + self._track_statuses(df) + status_histogram = df.groupby("status").size().to_dict() + _log.info(f"Status histogram: {status_histogram}") + job_db.persist(df) + + if len(df[df.status == "not_started"]) > 0: + # Check number of jobs running at each backend + running = df[(df.status == "created") | (df.status == "queued") | (df.status == "running")] + per_backend = running.groupby("backend_name").size().to_dict() + _log.info(f"Running per backend: {per_backend}") + 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 + to_launch = df[df.status == "not_started"].iloc[0:to_add] + for i in to_launch.index: + self._launch_job(start_job, df, i, backend_name) + job_db.persist(df) + + time.sleep(self.poll_sleep)
+ + + def _launch_job(self, start_job, df, i, backend_name): + """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. + """ + + 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) + + 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" + 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() + df.loc[i, "status"] = status + if status == "created": + # start job if not yet done by callback + try: + job.start() + df.loc[i, "status"] = job.status() + except OpenEoApiError as e: + _log.error(e) + df.loc[i, "status"] = "start_failed" + else: + df.loc[i, "status"] = "skipped" + +
+[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 open(metadata_path, "w") 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, df: pd.DataFrame): + """ + Tracks status (and stats) of running jobs (in place). + Optionally cancels jobs when running too long. + """ + active = df.loc[(df.status == "created") | (df.status == "queued") | (df.status == "running")] + for i in active.index: + job_id = df.loc[i, "id"] + backend_name = df.loc[i, "backend_name"] + previous_status = df.loc[i, "status"] + + try: + con = self._get_connection(backend_name) + the_job = con.job(job_id) + job_metadata = the_job.describe() + 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": + self.on_job_done(the_job, df.loc[i]) + + if previous_status != "error" and new_status == "error": + self.on_job_error(the_job, df.loc[i]) + + if previous_status in {"created", "queued"} and new_status == "running": + df.loc[i, "running_start_time"] = rfc3339.utcnow() + + if new_status == "canceled": + self.on_job_cancel(the_job, df.loc[i]) + + if self._cancel_running_job_after and new_status == "running": + self._cancel_prolonged_job(the_job, df.loc[i]) + + df.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(): + df.loc[i, key] = _format_usage_stat(job_metadata, key) + + except OpenEoApiError as e: + print(f"error for job {job_id!r} on backend {backend_name}") + print(e)
+ + + +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) + + +
+[docs] +class CsvJobDatabase(JobDatabaseInterface): + """ + 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]): + self.path = Path(path) + + 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.path.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(self.path, index=False)
+ + + +
+[docs] +class ParquetJobDatabase(JobDatabaseInterface): + """ + 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]): + self.path = Path(path) + + 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.path.parent.mkdir(parents=True, exist_ok=True) + df.to_parquet(self.path, index=False)
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ 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..99efbb125 --- /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.32.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..284bc51c4 --- /dev/null +++ b/_modules/openeo/internal/graph_building.html @@ -0,0 +1,573 @@ + + + + + + + openeo.internal.graph_building — openEO Python Client 0.32.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 json
+import sys
+from contextlib import nullcontext
+from pathlib import Path
+from typing import Any, Dict, 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)
+
+ + + +def as_flat_graph(x: Union[dict, FlatGraphableMixin, Path, 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): + 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) + 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): + 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 = {} + + def flatten(self, node: PGNode) -> Dict[str, dict]: + """Consume given nested process graph and return flat dict representation""" + self.accept_node(node) + assert len(self._argument_stack) == 0 + self._flattened[self._last_node_id]["result"] = True + return self._flattened + + 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] +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/_modules/openeo/metadata.html b/_modules/openeo/metadata.html new file mode 100644 index 000000000..e283d40e5 --- /dev/null +++ b/_modules/openeo/metadata.html @@ -0,0 +1,869 @@ + + + + + + + openeo.metadata — openEO Python Client 0.32.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..9d02e33d8 --- /dev/null +++ b/_modules/openeo/processes.html @@ -0,0 +1,6173 @@ + + + + + + + openeo.processes — openEO Python Client 0.32.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..582fcc1c2 --- /dev/null +++ b/_modules/openeo/rest/_datacube.html @@ -0,0 +1,457 @@ + + + + + + + openeo.rest._datacube — openEO Python Client 0.32.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: Connection):
+        self._pg = pgnode
+        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) +
+ +
+ +
+
+ +
+
+ + + + + + + \ 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..c00aba8cd --- /dev/null +++ b/_modules/openeo/rest/connection.html @@ -0,0 +1,2256 @@ + + + + + + + openeo.rest.connection — openEO Python Client 0.32.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,
+    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,
+    metadata_from_stac,
+)
+from openeo.rest import (
+    DEFAULT_DOWNLOAD_CHUNK_SIZE,
+    CapabilitiesException,
+    OpenEoApiError,
+    OpenEoApiPlainError,
+    OpenEoClientException,
+    OpenEoRestError,
+)
+from openeo.rest._datacube import 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. + """ + + _MINIMUM_API_VERSION = ComparableVersion("1.0.0") + + def __init__( + self, + url: str, + *, + auth: Optional[AuthBase] = None, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auth_config: Optional[AuthConfig] = None, + refresh_token_store: Optional[RefreshTokenStore] = None, + slow_response_threshold: Optional[float] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + auto_validate: bool = True, + ): + """ + Constructor of Connection, authenticates user. + + :param url: String Backend root url + """ + 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) -> None: + """ + 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 + """ + 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
+ + +
+[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, Any]) -> List[dict]: + """ + Validate a process graph without executing it. + + :param process_graph: (flat) dict representing process graph + :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`. + """ + # TODO #425 move this implementation to `DataCube` and just forward here (like with `load_collection`) + # TODO #425 detect actual metadata from URL + 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() + } + cube = self.datacube_from_process(process_id="load_stac", **arguments) + try: + cube.metadata = metadata_from_stac(url) + except Exception: + _log.warning(f"Failed to extract cube metadata from STAC URL {url}", exc_info=True) + return cube
+ + +
+[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: + """ + Wrapper for :py:meth:`load_stac` that loads the result of a previous job using the STAC collection of its results. + + :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 + """ + 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, Any], **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`) + 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], + 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, str, Path], + *, + timeout: Optional[int] = None, + validate: Optional[bool] = None, + auto_decode: bool = True, + ) -> Union[dict, requests.Response]: + """ + Execute a process graph synchronously and return the result. 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, str, Path], + *, + 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: (flat) dict representing a process graph, or process graph as raw JSON string, + or as local file path or URL + :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 + """ + # 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 = {} +
+ +
+ +
+
+ +
+
+ + + + + + + \ 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..86bd5d9ef --- /dev/null +++ b/_modules/openeo/rest/conversions.html @@ -0,0 +1,263 @@ + + + + + + + openeo.rest.conversions — openEO Python Client 0.32.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..ea6de04ad --- /dev/null +++ b/_modules/openeo/rest/datacube.html @@ -0,0 +1,2954 @@ + + + + + + + openeo.rest.datacube — openEO Python Client 0.32.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,
+)
+from openeo.processes import ProcessBuilder
+from openeo.rest import BandMathException, OpenEoClientException, OperatorException
+from openeo.rest._datacube import (
+    THIS,
+    UDF,
+    _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: Connection, 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: 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 connection to use to connect with the backend. + :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 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)
+ + + @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`. + """ + 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, + polygons: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + process: Union[str, PGNode, typing.Callable, UDF], + mask_value: Optional[float] = None, + context: Optional[dict] = None, + ) -> 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. + + .. warning:: experimental process: not generally supported, API subject to change. + + :param polygons: 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. + """ + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = ["Polygon", "MultiPolygon", "Feature", "FeatureCollection"] + polygons = self._get_geometry_argument(polygons, 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, + polygons=polygons, + 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: + 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 _ensure_save_result( + self, + format: Optional[str] = None, + options: Optional[dict] = None, + ) -> DataCube: + """ + Make sure there is a (final) `save_result` node in the process graph. + If there is already one: check if it is consistent with the given format/options (if any) + and add a new one otherwise. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :return: + """ + # TODO #401 Unify with VectorCube._ensure_save_result and move to generic data cube parent class (not only for raster cubes, but also vector cubes) + result_node = self.result_node() + if result_node.process_id == "save_result": + # There is already a `save_result` node: + # check if it is consistent with given format/options (if any) + args = result_node.arguments + if format is not None and format.lower() != args["format"].lower(): + raise ValueError( + f"Existing `save_result` node with different format {args['format']!r} != {format!r}" + ) + if options is not None and options != args["options"]: + raise ValueError( + f"Existing `save_result` node with different options {args['options']!r} != {options!r}" + ) + cube = self + else: + # No `save_result` node yet: automatically add it. + cube = self.save_result( + format=format or self._DEFAULT_RASTER_FORMAT, options=options + ) + return cube + +
+[docs] + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + ) -> 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). + :return: None if the result is stored to disk, or a bytes object returned by the backend. + """ + if format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + format = guess_format(outputfile) + cube = self._ensure_save_result(format=format, options=options) + 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, + *, + 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, + # TODO: avoid `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). + """ + if "format" in format_options and not out_format: + out_format = format_options["format"] # align with 'download' call arg name + if out_format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + out_format = guess_format(outputfile) + + job = self.create_job(out_format=out_format, job_options=job_options, validate=validate, **format_options) + 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, + # 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). + + :return: Created job. + """ + # TODO: add option to also automatically start the job? + # TODO: avoid using all kwargs as format_options + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + cube = self._ensure_save_result(format=out_format, options=format_options or None) + 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..5750c4e43 --- /dev/null +++ b/_modules/openeo/rest/graph_building.html @@ -0,0 +1,208 @@ + + + + + + + openeo.rest.graph_building — openEO Python Client 0.32.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..848842578 --- /dev/null +++ b/_modules/openeo/rest/job.html @@ -0,0 +1,751 @@ + + + + + + + openeo.rest.job — openEO Python Client 0.32.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, 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, 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, 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, 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, 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)) + with target.open("wb") as f: + response = self._get_response(stream=True) + 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..b86fb3397 --- /dev/null +++ b/_modules/openeo/rest/mlmodel.html @@ -0,0 +1,257 @@ + + + + + + + openeo.rest.mlmodel — openEO Python Client 0.32.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: Connection): + 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], + 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(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/udp.html b/_modules/openeo/rest/udp.html new file mode 100644 index 000000000..4c7919e91 --- /dev/null +++ b/_modules/openeo/rest/udp.html @@ -0,0 +1,265 @@ + + + + + + + openeo.rest.udp — openEO Python Client 0.32.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for openeo.rest.udp

+from __future__ import annotations
+
+import typing
+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], + 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..a19561cf5 --- /dev/null +++ b/_modules/openeo/rest/userfile.html @@ -0,0 +1,242 @@ + + + + + + + openeo.rest.userfile — openEO Python Client 0.32.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..732e886f5 --- /dev/null +++ b/_modules/openeo/rest/vectorcube.html @@ -0,0 +1,771 @@ + + + + + + + openeo.rest.vectorcube — openEO Python Client 0.32.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,
+    _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) + """ + + def __init__(self, graph: PGNode, connection: Connection, 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 {}, + }, + )
+ + + def _ensure_save_result( + self, + format: Optional[str] = None, + options: Optional[dict] = None, + ) -> VectorCube: + """ + Make sure there is a (final) `save_result` node in the process graph. + If there is already one: check if it is consistent with the given format/options (if any) + and add a new one otherwise. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :return: + """ + # TODO #401 Unify with DataCube._ensure_save_result and move to generic data cube parent class + result_node = self.result_node() + if result_node.process_id == "save_result": + # There is already a `save_result` node: + # check if it is consistent with given format/options (if any) + args = result_node.arguments + if format is not None and format.lower() != args["format"].lower(): + raise ValueError(f"Existing `save_result` node with different format {args['format']!r} != {format!r}") + if options is not None and options != args["options"]: + raise ValueError( + f"Existing `save_result` node with different options {args['options']!r} != {options!r}" + ) + cube = self + else: + # No `save_result` node yet: automatically add it. + cube = self.save_result(format=format or "GeoJSON", options=options) + return cube + +
+[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, + ) -> 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). + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + """ + # TODO #401 make outputfile optional (See DataCube.download) + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + if format is None and outputfile: + format = guess_format(outputfile) + cube = self._ensure_save_result(format=format, options=options) + 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, + *, + print=print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + # 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). + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + """ + if out_format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + out_format = guess_format(outputfile) + + job = self.create_job(out_format, job_options=job_options, validate=validate, **format_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, + 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, + **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). + + :return: Created job. + """ + # TODO: avoid using all kwargs as format_options + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + cube = self._ensure_save_result(format=out_format, options=format_options or None) + 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..8fb4160a1 --- /dev/null +++ b/_modules/openeo/testing.html @@ -0,0 +1,170 @@ + + + + + + + openeo.testing — openEO Python Client 0.32.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..45727e9f3 --- /dev/null +++ b/_modules/openeo/testing/results.html @@ -0,0 +1,524 @@ + + + + + + + openeo.testing.results — openEO Python Client 0.32.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..4ef478867 --- /dev/null +++ b/_modules/openeo/udf/debug.html @@ -0,0 +1,157 @@ + + + + + + + openeo.udf.debug — openEO Python Client 0.32.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..19bed7b98 --- /dev/null +++ b/_modules/openeo/udf/run_code.html @@ -0,0 +1,427 @@ + + + + + + + openeo.udf.run_code — openEO Python Client 0.32.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 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..2968028ba --- /dev/null +++ b/_modules/openeo/udf/structured_data.html @@ -0,0 +1,174 @@ + + + + + + + openeo.udf.structured_data — openEO Python Client 0.32.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..3ce4feeee --- /dev/null +++ b/_modules/openeo/udf/udf_data.html @@ -0,0 +1,283 @@ + + + + + + + openeo.udf.udf_data — openEO Python Client 0.32.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..5ce36268c --- /dev/null +++ b/_modules/openeo/udf/udf_signatures.html @@ -0,0 +1,223 @@ + + + + + + + openeo.udf.udf_signatures — openEO Python Client 0.32.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)
+
+from pandas import Series
+
+from openeo.metadata import CollectionMetadata
+from openeo.udf.udf_data import UdfData
+from openeo.udf.xarraydatacube import XarrayDataCube
+
+
+
+[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
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ 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..254ca5df3 --- /dev/null +++ b/_modules/openeo/udf/xarraydatacube.html @@ -0,0 +1,526 @@ + + + + + + + openeo.udf.xarraydatacube — openEO Python Client 0.32.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") 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..e1c4799fc --- /dev/null +++ b/_modules/openeo/util.html @@ -0,0 +1,828 @@ + + + + + + + openeo.util — openEO Python Client 0.32.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]) -> str:
+    """
+    Guess the output format from a given filename and return the corrected format.
+    Any names not in the dict get passed through.
+    """
+    extension = str(filename).rsplit(".", 1)[-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..012719b69 --- /dev/null +++ b/_sources/api.rst.txt @@ -0,0 +1,168 @@ +============= +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.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..171cc3fb7 --- /dev/null +++ b/_sources/cookbook/job_manager.rst.txt @@ -0,0 +1,16 @@ +==================================== +Multi Backend Job Manager +==================================== + +.. 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 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..79163228e --- /dev/null +++ b/_sources/datacube_construction.rst.txt @@ -0,0 +1,198 @@ + +======================= +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 diff --git a/_sources/development.rst.txt b/_sources/development.rst.txt new file mode 100644 index 000000000..5aede41d3 --- /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..58ffd3ced --- /dev/null +++ b/_sources/installation.rst.txt @@ -0,0 +1,127 @@ +************* +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 +============= + +At least *Python 3.7* is required (since version 0.23.0). +Also, 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..62989eb74 --- /dev/null +++ b/_sources/udf.rst.txt @@ -0,0 +1,702 @@ +.. 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. + See :ref:`udf_example_apply` + +`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. + + + +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. + +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=[]) + + +Illustration of data chunking in ``apply`` with a UDF +======================================================== + +TODO + +Example: ``apply_dimension`` with a UDF +======================================== + +TODO + +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..e5179b7a9 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- 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: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +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..4d67807d1 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"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..5ad669a10 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.32.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..990c92177 --- /dev/null +++ b/api-processbuilder.html @@ -0,0 +1,196 @@ + + + + + + + + <no title> — openEO Python Client 0.32.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..b1774de45 --- /dev/null +++ b/api-processes.html @@ -0,0 +1,4557 @@ + + + + + + + + API: openeo.processes — openEO Python Client 0.32.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..739684739 --- /dev/null +++ b/api.html @@ -0,0 +1,6508 @@ + + + + + + + + API (General) — openEO Python Client 0.32.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, 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, 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(polygons, process, mask_value=None, context=None)[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.

+
+

Warning

+

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

+
+
+
Parameters:
+
    +
  • polygons (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

+
+
+
+

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, **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).

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job.

+
+
+
+ +
+
+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)[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).

  • +
+
+
Return type:
+

Optional[bytes]

+
+
Returns:
+

None if the result is stored to disk, or a bytes object returned by the backend.

+
+
+
+ +
+
+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, *, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, job_options=None, validate=None, **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).

  • +
+
+
Return type:
+

BatchJob

+
+
+
+ +
+
+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 (Connection) – The connection to use to connect with the backend.

  • +
  • 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.

+
+
+ +
+
+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, **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, **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).

  • +
+
+
Return type:
+

BatchJob

+
+
Returns:
+

Created job.

+
+
+
+ +
+
+download(outputfile=None, format=None, options=None, *, validate=None)[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).

  • +
+
+
Return type:
+

Optional[bytes]

+
+
+
+

Changed in version 0.21.0: When not specified explicitly, output format is guessed from output file extension.

+
+
+ +
+
+execute(*, validate=None)[source]
+

Executes the process graph.

+
+
Return type:
+

dict

+
+
+
+ +
+
+execute_batch(outputfile=None, out_format=None, *, print=<built-in function print>, max_poll_interval=60, connection_retry_interval=30, job_options=None, validate=None, **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).

  • +
+
+
Return type:
+

BatchJob

+
+
+
+

Changed in version 0.21.0: When not specified explicitly, output format is guessed from output file extension.

+
+
+ +
+
+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, **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, 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.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[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 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, *, auth=None, session=None, default_timeout=None, auth_config=None, refresh_token_store=None, slow_response_threshold=None, oidc_auth_renewer=None, auto_validate=True)[source]
+

Connection to an openEO backend.

+
+
+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:
+

None

+
+
+
+

Added in version 0.31.0.

+
+
+ +
+
+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, str, Path]) – (flat) dict representing a process graph, or process graph as raw JSON string, +or as local file path or URL

  • +
  • 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

+
+
+
+ +
+
+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]) – (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, str, Path]) – (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]
+

Wrapper for load_stac() that loads the result of a previous job using the STAC collection of its results.

+
+
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, Any]) – (flat) dict representing process graph

+
+
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]) – 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

+
>>> 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"},
+:rtype: :sphinx_autodoc_typehints_type:`\:py\:class\:\`dict\``
+
+
+

… ], +… }

+
+ +
+ +
+
+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.

+
+
+ +
+ +
+
+

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..2d8180e4c --- /dev/null +++ b/auth.html @@ -0,0 +1,665 @@ + + + + + + + + Authentication and Account Management — openEO Python Client 0.32.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..cd6b5dbab --- /dev/null +++ b/basics.html @@ -0,0 +1,527 @@ + + + + + + + + Getting Started — openEO Python Client 0.32.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..6ca6f314a --- /dev/null +++ b/batch_jobs.html @@ -0,0 +1,446 @@ + + + + + + + + Batch Jobs — openEO Python Client 0.32.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..5e0df324f --- /dev/null +++ b/best_practices.html @@ -0,0 +1,213 @@ + + + + + + + + Best practices, coding style and general tips — openEO Python Client 0.32.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..c617eab14 --- /dev/null +++ b/changelog.html @@ -0,0 +1,1262 @@ + + + + + + + + Changelog — openEO Python Client 0.32.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

+
    +
  • 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)

  • +
+
+
+

Changed

+
+
+

Removed

+
+
+

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..e8ed1daed --- /dev/null +++ b/configuration.html @@ -0,0 +1,239 @@ + + + + + + + + Configuration — openEO Python Client 0.32.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..7a297879a --- /dev/null +++ b/cookbook/ard.html @@ -0,0 +1,234 @@ + + + + + + + + Analysis Ready Data generation — openEO Python Client 0.32.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..bbeb67ec5 --- /dev/null +++ b/cookbook/index.html @@ -0,0 +1,225 @@ + + + + + + + + openEO CookBook — openEO Python Client 0.32.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

openEO CookBook

+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/job_manager.html b/cookbook/job_manager.html new file mode 100644 index 000000000..fd2501687 --- /dev/null +++ b/cookbook/job_manager.html @@ -0,0 +1,443 @@ + + + + + + + + Multi Backend Job Manager — openEO Python Client 0.32.0a1 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Multi Backend Job Manager

+
+

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.

+
+

Added in version 0.14.0.

+
+
+
+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, start_job, job_db=None, **kwargs)[source]
+

Runs jobs, specified in a dataframe, and tracks parameters.

+
+
Parameters:
+
    +
  • df (DataFrame) – DataFrame that specifies the jobs, and tracks the jobs’ statuses.

  • +
  • 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.

    +
    +

  • +
+
+
+
+

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.

+
+
+ +
+ +
+
+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 exists()[source]
+

Does the job database already exist, to read job data from?

+
+
Return type:
+

bool

+
+
+
+ +
+
+abstract persist(df)[source]
+

Store job data to the 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.

+
+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/cookbook/localprocessing.html b/cookbook/localprocessing.html new file mode 100644 index 000000000..b309f1f5c --- /dev/null +++ b/cookbook/localprocessing.html @@ -0,0 +1,307 @@ + + + + + + + + Client-side (local) processing — openEO Python Client 0.32.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..7a926f678 --- /dev/null +++ b/cookbook/sampling.html @@ -0,0 +1,193 @@ + + + + + + + + Dataset sampling — openEO Python Client 0.32.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..f9b7cb880 --- /dev/null +++ b/cookbook/spectral_indices.html @@ -0,0 +1,450 @@ + + + + + + + + Spectral Indices — openEO Python Client 0.32.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..83c5d561e --- /dev/null +++ b/cookbook/tricks.html @@ -0,0 +1,208 @@ + + + + + + + + Miscellaneous tips and tricks — openEO Python Client 0.32.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..3e97fac78 --- /dev/null +++ b/cookbook/udp_sharing.html @@ -0,0 +1,255 @@ + + + + + + + + Sharing of user-defined processes — openEO Python Client 0.32.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..0ed77324a --- /dev/null +++ b/data_access.html @@ -0,0 +1,412 @@ + + + + + + + + Finding and loading data — openEO Python Client 0.32.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..cbd4774f0 --- /dev/null +++ b/datacube_construction.html @@ -0,0 +1,301 @@ + + + + + + + + DataCube construction — openEO Python Client 0.32.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

+
+
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/development.html b/development.html new file mode 100644 index 000000000..26713b05b --- /dev/null +++ b/development.html @@ -0,0 +1,519 @@ + + + + + + + + Development and maintenance — openEO Python Client 0.32.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..e0f7368c5 --- /dev/null +++ b/genindex.html @@ -0,0 +1,1744 @@ + + + + + + + Index — openEO Python Client 0.32.0a1 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | F + | G + | H + | I + | J + | L + | M + | N + | O + | P + | Q + | R + | S + | T + | U + | V + | X + +
+

_

+ + +
+ +

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + +
+ +

I

+ + + +
+ +

J

+ + + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

X

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..f88cae2b8 --- /dev/null +++ b/index.html @@ -0,0 +1,374 @@ + + + + + + + + openEO Python Client — openEO Python Client 0.32.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..b74a41b0d --- /dev/null +++ b/installation.html @@ -0,0 +1,231 @@ + + + + + + + + Installation — openEO Python Client 0.32.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

+

At least Python 3.7 is required (since version 0.23.0). +Also, 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..bf0c7caac --- /dev/null +++ b/lib/openeo/__init__.py @@ -0,0 +1,26 @@ +""" + + +""" + +__title__ = 'openeo' +__author__ = 'Jeroen Dries' + + +class BaseOpenEoException(Exception): + pass + + +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 + + +def client_version() -> str: + try: + import importlib.metadata + return importlib.metadata.version("openeo") + except Exception: + return __version__ diff --git a/lib/openeo/_version.py b/lib/openeo/_version.py new file mode 100644 index 000000000..08e610769 --- /dev/null +++ b/lib/openeo/_version.py @@ -0,0 +1 @@ +__version__ = "0.32.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..21b0ca020 --- /dev/null +++ b/lib/openeo/api/process.py @@ -0,0 +1,363 @@ +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[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) + + @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..c49af6165 --- /dev/null +++ b/lib/openeo/extra/job_management.py @@ -0,0 +1,667 @@ +import abc +import contextlib +import datetime +import json +import logging +import time +import warnings +from pathlib import Path +from typing import Callable, Dict, NamedTuple, Optional, Union + +import pandas as pd +import requests +import shapely.errors +import shapely.wkt +from requests.adapters import HTTPAdapter, Retry + +from openeo import BatchJob, Connection +from openeo.rest import OpenEoApiError +from openeo.util import deep_get, 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 + + +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. + + :param df: job data to store. + """ + ... + + +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. + + .. versionadded:: 0.14.0 + """ + + def __init__( + self, + poll_sleep: int = 60, + root_dir: Optional[Union[str, Path]] = ".", + *, + cancel_running_job_after: Optional[int] = None, + ): + """Create a MultiBackendJobManager. + + :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 [seconds]: + Optional temporal limit (in seconds) after which running jobs should be canceled + by the job manager. + + .. versionchanged:: 0.32.0 + Added `cancel_running_job_after` parameter. + """ + 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 + ) + + 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 = [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)) + + def _normalize_df(self, df: pd.DataFrame) -> pd.DataFrame: + """Ensure we have the required columns and the expected type for the geometry column. + + :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`. + ("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 run_jobs( + self, + df: pd.DataFrame, + start_job: Callable[[], BatchJob], + job_db: Union[str, Path, JobDatabaseInterface, None] = None, + **kwargs, + ): + """Runs jobs, specified in a dataframe, and tracks parameters. + + :param df: + DataFrame that specifies the jobs, and tracks the jobs' statuses. + + :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 `. + + .. 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. + """ + # TODO: Defining start_jobs as a Protocol might make its usage more clear, and avoid complicated doctrings, + # but Protocols are only supported in Python 3.8 and higher. + + # 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_path = Path(job_db) + if job_db_path.suffix.lower() == ".csv": + job_db = CsvJobDatabase(path=job_db_path) + elif job_db_path.suffix.lower() == ".parquet": + job_db = ParquetJobDatabase(path=job_db_path) + else: + raise ValueError(f"Unsupported job database file type {job_db_path!r}") + + 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}") + df = job_db.read() + status_histogram = df.groupby("status").size().to_dict() + _log.info(f"Status histogram: {status_histogram}") + + df = self._normalize_df(df) + + while ( + df[ + # TODO: risk on infinite loop if a backend reports a (non-standard) terminal status that is not covered here + (df.status != "finished") + & (df.status != "skipped") + & (df.status != "start_failed") + & (df.status != "error") + & (df.status != "canceled") + ].size + > 0 + ): + + with ignore_connection_errors(context="get statuses"): + self._track_statuses(df) + status_histogram = df.groupby("status").size().to_dict() + _log.info(f"Status histogram: {status_histogram}") + job_db.persist(df) + + if len(df[df.status == "not_started"]) > 0: + # Check number of jobs running at each backend + running = df[(df.status == "created") | (df.status == "queued") | (df.status == "running")] + per_backend = running.groupby("backend_name").size().to_dict() + _log.info(f"Running per backend: {per_backend}") + 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 + to_launch = df[df.status == "not_started"].iloc[0:to_add] + for i in to_launch.index: + self._launch_job(start_job, df, i, backend_name) + job_db.persist(df) + + time.sleep(self.poll_sleep) + + def _launch_job(self, start_job, df, i, backend_name): + """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. + """ + + 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) + + 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" + 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() + df.loc[i, "status"] = status + if status == "created": + # start job if not yet done by callback + try: + job.start() + df.loc[i, "status"] = job.status() + except OpenEoApiError as e: + _log.error(e) + df.loc[i, "status"] = "start_failed" + else: + df.loc[i, "status"] = "skipped" + + 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 open(metadata_path, "w") 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, df: pd.DataFrame): + """ + Tracks status (and stats) of running jobs (in place). + Optionally cancels jobs when running too long. + """ + active = df.loc[(df.status == "created") | (df.status == "queued") | (df.status == "running")] + for i in active.index: + job_id = df.loc[i, "id"] + backend_name = df.loc[i, "backend_name"] + previous_status = df.loc[i, "status"] + + try: + con = self._get_connection(backend_name) + the_job = con.job(job_id) + job_metadata = the_job.describe() + 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": + self.on_job_done(the_job, df.loc[i]) + + if previous_status != "error" and new_status == "error": + self.on_job_error(the_job, df.loc[i]) + + if previous_status in {"created", "queued"} and new_status == "running": + df.loc[i, "running_start_time"] = rfc3339.utcnow() + + if new_status == "canceled": + self.on_job_cancel(the_job, df.loc[i]) + + if self._cancel_running_job_after and new_status == "running": + self._cancel_prolonged_job(the_job, df.loc[i]) + + df.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(): + df.loc[i, key] = _format_usage_stat(job_metadata, key) + + except OpenEoApiError as e: + print(f"error for job {job_id!r} on backend {backend_name}") + print(e) + + +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 CsvJobDatabase(JobDatabaseInterface): + """ + 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]): + self.path = Path(path) + + 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.path.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(self.path, index=False) + + +class ParquetJobDatabase(JobDatabaseInterface): + """ + 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]): + self.path = Path(path) + + 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.path.parent.mkdir(parents=True, exist_ok=True) + df.to_parquet(self.path, index=False) 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..d92a496b7 --- /dev/null +++ b/lib/openeo/internal/graph_building.py @@ -0,0 +1,422 @@ +""" +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 json +import sys +from contextlib import nullcontext +from pathlib import Path +from typing import Any, Dict, 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 as_flat_graph(x: Union[dict, FlatGraphableMixin, Path, 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): + 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) + 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): + 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 = {} + + def flatten(self, node: PGNode) -> Dict[str, dict]: + """Consume given nested process graph and return flat dict representation""" + self.accept_node(node) + assert len(self._argument_stack) == 0 + self._flattened[self._last_node_id]["result"] = True + return self._flattened + + 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] 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..afb97dfdb --- /dev/null +++ b/lib/openeo/internal/processes/parse.py @@ -0,0 +1,116 @@ +""" +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 typing +from pathlib import Path +from typing import Iterator, List, Union + +import requests + + +class Schema: + """Schema description of an openEO process parameter or return value.""" + + def __init__(self, schema: Union[dict, list]): + self.schema = schema + + @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" + ) + + +class Parameter: + """openEO process parameter""" + + # TODO unify with openeo.api.process.Parameter? + + NO_DEFAULT = object() + + def __init__(self, name: str, description: str, schema: Schema, default=NO_DEFAULT, optional: bool = False): + self.name = name + self.description = description + self.schema = schema + self.default = default + self.optional = optional + + @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", cls.NO_DEFAULT), + optional=data.get("optional", False), + ) + + def has_default(self): + return self.default is not self.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): + """An openEO process""" + + id: str + parameters: List[Parameter] + returns: Returns + description: str = "" + summary: str = "" + # 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"]], + returns=Returns.from_dict(data["returns"]), + description=data["description"], + summary=data["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) diff --git a/lib/openeo/internal/warnings.py b/lib/openeo/internal/warnings.py new file mode 100644 index 000000000..2c5335bc4 --- /dev/null +++ b/lib/openeo/internal/warnings.py @@ -0,0 +1,92 @@ +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 = "n/a", *, 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: name of the alias (unused) + :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) + + 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..f0afeca47 --- /dev/null +++ b/lib/openeo/rest/_datacube.py @@ -0,0 +1,321 @@ +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: Connection): + self._pg = pgnode + 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) diff --git a/lib/openeo/rest/_testing.py b/lib/openeo/rest/_testing.py new file mode 100644 index 000000000..6ad33ada6 --- /dev/null +++ b/lib/openeo/rest/_testing.py @@ -0,0 +1,217 @@ +import json +import re +from typing import Optional, Union + +from openeo import Connection, DataCube +from openeo.rest.vectorcube import VectorCube + + +class DummyBackend: + """ + Dummy backend that handles sync/batch execution requests + and allows inspection of posted process graphs + """ + + __slots__ = ( + "connection", + "sync_requests", + "batch_jobs", + "validation_requests", + "next_result", + "next_validation_errors", + ) + + # Default result (can serve both as JSON or binary data) + DEFAULT_RESULT = b'{"what?": "Result data"}' + + def __init__(self, requests_mock, connection: Connection): + self.connection = connection + self.sync_requests = [] + self.batch_jobs = {} + self.validation_requests = [] + self.next_result = self.DEFAULT_RESULT + self.next_validation_errors = [] + 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) + + 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)""" + pg = request.json()["process"]["process_graph"] + job_id = f"job-{len(self.batch_jobs):03d}" + self.batch_jobs[job_id] = {"job_id": job_id, "pg": pg, "status": "created"} + 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 ValueError(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" + # TODO: support custom status sequence (instead of directly going to status "finished")? + self.batch_jobs[job_id]["status"] = "finished" + 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) + 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 one and only batch process graph""" + assert len(self.batch_jobs) == 1 + return self.batch_jobs[max(self.batch_jobs.keys())]["pg"] + + 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()] + assert len(pgs) == 1 + 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 RuntimeError( + 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 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..ac03d6e32 --- /dev/null +++ b/lib/openeo/rest/auth/oidc.py @@ -0,0 +1,938 @@ +""" +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 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()) + ) + ) + resp = self._requests.post(url=token_endpoint, data=post_data) + 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") + 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..df6f9c3cc --- /dev/null +++ b/lib/openeo/rest/connection.py @@ -0,0 +1,1964 @@ +""" +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, + 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, + metadata_from_stac, +) +from openeo.rest import ( + DEFAULT_DOWNLOAD_CHUNK_SIZE, + CapabilitiesException, + OpenEoApiError, + OpenEoApiPlainError, + OpenEoClientException, + OpenEoRestError, +) +from openeo.rest._datacube import 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. + """ + + _MINIMUM_API_VERSION = ComparableVersion("1.0.0") + + def __init__( + self, + url: str, + *, + auth: Optional[AuthBase] = None, + session: Optional[requests.Session] = None, + default_timeout: Optional[int] = None, + auth_config: Optional[AuthConfig] = None, + refresh_token_store: Optional[RefreshTokenStore] = None, + slow_response_threshold: Optional[float] = None, + oidc_auth_renewer: Optional[OidcAuthenticator] = None, + auto_validate: bool = True, + ): + """ + Constructor of Connection, authenticates user. + + :param url: String Backend root url + """ + 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) -> None: + """ + 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 + """ + 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 + + 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, Any]) -> List[dict]: + """ + Validate a process graph without executing it. + + :param process_graph: (flat) dict representing process graph + :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`. + """ + # TODO #425 move this implementation to `DataCube` and just forward here (like with `load_collection`) + # TODO #425 detect actual metadata from URL + 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() + } + cube = self.datacube_from_process(process_id="load_stac", **arguments) + try: + cube.metadata = metadata_from_stac(url) + except Exception: + _log.warning(f"Failed to extract cube metadata from STAC URL {url}", exc_info=True) + return cube + + 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: + """ + Wrapper for :py:meth:`load_stac` that loads the result of a previous job using the STAC collection of its results. + + :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 + """ + 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, Any], **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`) + 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], + 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, str, Path], + *, + timeout: Optional[int] = None, + validate: Optional[bool] = None, + auto_decode: bool = True, + ) -> Union[dict, requests.Response]: + """ + Execute a process graph synchronously and return the result. 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, str, Path], + *, + 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: (flat) dict representing a process graph, or process graph as raw JSON string, + or as local file path or URL + :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 + """ + # 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 = {} 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..6603ffbb7 --- /dev/null +++ b/lib/openeo/rest/datacube.py @@ -0,0 +1,2584 @@ +""" +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, +) +from openeo.processes import ProcessBuilder +from openeo.rest import BandMathException, OpenEoClientException, OperatorException +from openeo.rest._datacube import ( + THIS, + UDF, + _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: Connection, 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: 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 connection to use to connect with the backend. + :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 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 _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`. + """ + 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, + polygons: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube], + process: Union[str, PGNode, typing.Callable, UDF], + mask_value: Optional[float] = None, + context: Optional[dict] = None, + ) -> 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. + + .. warning:: experimental process: not generally supported, API subject to change. + + :param polygons: 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. + """ + process = build_child_callback(process, parent_parameters=["data"], connection=self.connection) + valid_geojson_types = ["Polygon", "MultiPolygon", "Feature", "FeatureCollection"] + polygons = self._get_geometry_argument(polygons, 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, + polygons=polygons, + 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: + 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 _ensure_save_result( + self, + format: Optional[str] = None, + options: Optional[dict] = None, + ) -> DataCube: + """ + Make sure there is a (final) `save_result` node in the process graph. + If there is already one: check if it is consistent with the given format/options (if any) + and add a new one otherwise. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :return: + """ + # TODO #401 Unify with VectorCube._ensure_save_result and move to generic data cube parent class (not only for raster cubes, but also vector cubes) + result_node = self.result_node() + if result_node.process_id == "save_result": + # There is already a `save_result` node: + # check if it is consistent with given format/options (if any) + args = result_node.arguments + if format is not None and format.lower() != args["format"].lower(): + raise ValueError( + f"Existing `save_result` node with different format {args['format']!r} != {format!r}" + ) + if options is not None and options != args["options"]: + raise ValueError( + f"Existing `save_result` node with different options {args['options']!r} != {options!r}" + ) + cube = self + else: + # No `save_result` node yet: automatically add it. + cube = self.save_result( + format=format or self._DEFAULT_RASTER_FORMAT, options=options + ) + return cube + + def download( + self, + outputfile: Optional[Union[str, pathlib.Path]] = None, + format: Optional[str] = None, + options: Optional[dict] = None, + *, + validate: Optional[bool] = None, + ) -> 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). + :return: None if the result is stored to disk, or a bytes object returned by the backend. + """ + if format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + format = guess_format(outputfile) + cube = self._ensure_save_result(format=format, options=options) + 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, + *, + 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, + # TODO: avoid `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). + """ + if "format" in format_options and not out_format: + out_format = format_options["format"] # align with 'download' call arg name + if out_format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + out_format = guess_format(outputfile) + + job = self.create_job(out_format=out_format, job_options=job_options, validate=validate, **format_options) + 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, + # 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). + + :return: Created job. + """ + # TODO: add option to also automatically start the job? + # TODO: avoid using all kwargs as format_options + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + cube = self._ensure_save_result(format=out_format, options=format_options or None) + 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..0dd3ff271 --- /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, 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, 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, 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, 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, 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)) + with target.open("wb") as f: + response = self._get_response(stream=True) + 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..0ddb92598 --- /dev/null +++ b/lib/openeo/rest/mlmodel.py @@ -0,0 +1,118 @@ +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: Connection): + 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], + 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(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/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..aea78f093 --- /dev/null +++ b/lib/openeo/rest/udp.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import typing +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], + 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..206130bfc --- /dev/null +++ b/lib/openeo/rest/vectorcube.py @@ -0,0 +1,593 @@ +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, + _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) + """ + + def __init__(self, graph: PGNode, connection: Connection, 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 _ensure_save_result( + self, + format: Optional[str] = None, + options: Optional[dict] = None, + ) -> VectorCube: + """ + Make sure there is a (final) `save_result` node in the process graph. + If there is already one: check if it is consistent with the given format/options (if any) + and add a new one otherwise. + + :param format: (optional) desired `save_result` file format + :param options: (optional) desired `save_result` file format parameters + :return: + """ + # TODO #401 Unify with DataCube._ensure_save_result and move to generic data cube parent class + result_node = self.result_node() + if result_node.process_id == "save_result": + # There is already a `save_result` node: + # check if it is consistent with given format/options (if any) + args = result_node.arguments + if format is not None and format.lower() != args["format"].lower(): + raise ValueError(f"Existing `save_result` node with different format {args['format']!r} != {format!r}") + if options is not None and options != args["options"]: + raise ValueError( + f"Existing `save_result` node with different options {args['options']!r} != {options!r}" + ) + cube = self + else: + # No `save_result` node yet: automatically add it. + cube = self.save_result(format=format or "GeoJSON", options=options) + return cube + + 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, + ) -> 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). + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + + """ + # TODO #401 make outputfile optional (See DataCube.download) + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + if format is None and outputfile: + format = guess_format(outputfile) + cube = self._ensure_save_result(format=format, options=options) + 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, + *, + print=print, + max_poll_interval: float = 60, + connection_retry_interval: float = 30, + job_options: Optional[dict] = None, + validate: Optional[bool] = None, + # 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). + + .. versionchanged:: 0.21.0 + When not specified explicitly, output format is guessed from output file extension. + """ + if out_format is None and outputfile: + # TODO #401/#449 don't guess/override format if there is already a save_result with format? + out_format = guess_format(outputfile) + + job = self.create_job(out_format, job_options=job_options, validate=validate, **format_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, + 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, + **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). + + :return: Created job. + """ + # TODO: avoid using all kwargs as format_options + # TODO: centralize `create_job` for `DataCube`, `VectorCube`, `MlModel`, ... + cube = self._ensure_save_result(format=out_format, options=format_options or None) + 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..6d195082b --- /dev/null +++ b/lib/openeo/udf/run_code.py @@ -0,0 +1,297 @@ +""" + +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 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..019b960c6 --- /dev/null +++ b/lib/openeo/udf/udf_signatures.py @@ -0,0 +1,87 @@ +""" +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) + +from pandas import Series + +from openeo.metadata import CollectionMetadata +from openeo.udf.udf_data import UdfData +from openeo.udf.xarraydatacube import XarrayDataCube + + +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 diff --git a/lib/openeo/udf/xarraydatacube.py b/lib/openeo/udf/xarraydatacube.py new file mode 100644 index 000000000..f01590222 --- /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") 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..762bf5f7c --- /dev/null +++ b/lib/openeo/util.py @@ -0,0 +1,686 @@ +""" +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]) -> str: + """ + Guess the output format from a given filename and return the corrected format. + Any names not in the dict get passed through. + """ + extension = str(filename).rsplit(".", 1)[-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..2a4e26e6a --- /dev/null +++ b/machine_learning.html @@ -0,0 +1,244 @@ + + + + + + + + Machine Learning — openEO Python Client 0.32.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..415c63d6c6142238d4a606d61b6f20527e20fa54 GIT binary patch literal 5725 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 PT%PNNi+%cUT5_v{L`@`S literal 0 HcmV?d00001 diff --git a/process_mapping.html b/process_mapping.html new file mode 100644 index 000000000..3257de2d5 --- /dev/null +++ b/process_mapping.html @@ -0,0 +1,609 @@ + + + + + + + + openEO Process Mapping — openEO Python Client 0.32.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..db3ab7f9c --- /dev/null +++ b/processes.html @@ -0,0 +1,539 @@ + + + + + + + + Working with processes — openEO Python Client 0.32.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..80f55d444 --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,267 @@ + + + + + + + Python Module Index — openEO Python Client 0.32.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.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..8f7806df0 --- /dev/null +++ b/search.html @@ -0,0 +1,144 @@ + + + + + + + Search — openEO Python Client 0.32.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..f2215330c --- /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": [[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, "id2"], [7, "id4"], [7, "id7"], [7, "id10"], [7, "id14"], [7, "id17"], [7, "id24"], [7, "id27"], [7, "id32"], [7, "id38"], [7, "id42"], [7, "id45"], [7, "id48"], [7, "id50"], [7, "id52"], [7, "id57"], [7, "id62"], [7, "id65"], [7, "id72"], [7, "id76"], [7, "id79"], [7, "id81"], [7, "id86"], [7, "id88"], [7, "id92"], [7, "id97"], [7, "id101"], [7, "id104"], [7, "id110"], [7, "id113"], [7, "id117"], [7, "id121"], [7, "id124"], [7, "id127"], [7, "id132"], [7, "id137"], [7, "id140"]], "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 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 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, "id8"], [7, "id11"], [7, "id15"], [7, "id21"], [7, "id25"], [7, "id28"], [7, "id33"], [7, "id39"], [7, "id43"], [7, "id53"], [7, "id58"], [7, "id63"], [7, "id66"], [7, "id68"], [7, "id73"], [7, "id77"], [7, "id82"], [7, "id89"], [7, "id93"], [7, "id98"], [7, "id102"], [7, "id105"], [7, "id108"], [7, "id114"], [7, "id118"], [7, "id125"], [7, "id128"], [7, "id133"]], "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, "id134"]], "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, "id5"], [7, "id12"], [7, "id16"], [7, "id19"], [7, "id22"], [7, "id30"], [7, "id34"], [7, "id36"], [7, "id40"], [7, "id46"], [7, "id49"], [7, "id51"], [7, "id55"], [7, "id60"], [7, "id64"], [7, "id70"], [7, "id78"], [7, "id84"], [7, "id90"], [7, "id106"], [7, "id115"], [7, "id122"], [7, "id130"], [7, "id138"], [7, "id141"]], "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"]], "Illustration of data chunking in apply with a UDF": [[25, "illustration-of-data-chunking-in-apply-with-a-udf"]], "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"]], "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"]], "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, "id18"], [7, "id29"], [7, "id54"], [7, "id69"], [7, "id74"], [7, "id80"], [7, "id83"], [7, "id94"], [7, "id99"], [7, "id111"], [7, "id119"], [7, "id129"], [7, "id135"]], "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, "id75"]], "[0.12.0] - 2022-09-09": [[7, "id71"]], "[0.12.1] - 2022-09-15": [[7, "id67"]], "[0.13.0] - 2022-10-10 - \u201cUDF UX\u201d release": [[7, "udf-ux-release"]], "[0.14.0] - 2023-02-01": [[7, "id61"]], "[0.14.1] - 2023-02-06": [[7, "id59"]], "[0.15.0] - 2023-03-03": [[7, "id56"]], "[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, "id47"]], "[0.19.0] - 2023-06-16": [[7, "id44"]], "[0.20.0] - 2023-06-30": [[7, "id41"]], "[0.21.0] - 2023-07-19": [[7, "id37"]], "[0.21.1] - 2023-07-19": [[7, "id35"]], "[0.22.0] - 2023-08-09": [[7, "id31"]], "[0.23.0] - 2023-10-02": [[7, "id26"]], "[0.24.0] - 2023-10-27": [[7, "id23"]], "[0.25.0] - 2023-11-02": [[7, "id20"]], "[0.26.0] - 2023-11-27 - \u201cSRR6\u201d release": [[7, "srr6-release"]], "[0.27.0] - 2024-01-12": [[7, "id13"]], "[0.28.0] - 2024-03-18": [[7, "id9"]], "[0.29.0] - 2024-05-03": [[7, "id6"]], "[0.30.0] - 2024-06-18": [[7, "id3"]], "[0.31.0] - 2024-07-26": [[7, "id1"]], "[0.4.10] - 2021-02-26": [[7, "id116"]], "[0.4.4] - 2020-08-20": [[7, "id139"]], "[0.4.5] - 2020-10-01": [[7, "id136"]], "[0.4.6] - 2020-10-15": [[7, "id131"]], "[0.4.7] - 2020-10-22": [[7, "id126"]], "[0.4.8] - 2020-11-17": [[7, "id123"]], "[0.4.9] - 2021-01-29": [[7, "id120"]], "[0.5.0] - 2021-03-17": [[7, "id112"]], "[0.6.0] - 2021-03-26": [[7, "id109"]], "[0.6.1] - 2021-03-29": [[7, "id107"]], "[0.7.0] - 2021-04-21": [[7, "id103"]], "[0.8.0] - 2021-06-25": [[7, "id100"]], "[0.8.1] - 2021-08-24": [[7, "id96"]], "[0.8.2] - 2021-08-24": [[7, "id95"]], "[0.9.0] - 2021-10-11": [[7, "id91"]], "[0.9.1] - 2021-11-16": [[7, "id87"]], "[0.9.2] - 2022-01-14": [[7, "id85"]], "[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.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"]]}, "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": 63, "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": {"__init__() (openeo.rest.datacube.datacube method)": [[0, "openeo.rest.datacube.DataCube.__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]], "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_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_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_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.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]], "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.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.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]], "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]], "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.rest.job.batchjob method)": [[0, "openeo.rest.job.BatchJob.start_job", 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]], "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.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]], "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, "", "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"]], "openeo.extra.job_management.JobDatabaseInterface": [[11, 3, 1, "", "exists"], [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"]], "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"]], "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, "-", "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, "", "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.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"]], "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, 19, 20, 21, 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, 12, 20, 24], "03": [5, 9, 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, 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, 21, 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, 21, 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, 16, 26], "320647": 6, "323": 7, "323e": 12, "324": 7, "326": 7, "32632": [0, 12], "327598": 4, "328": 7, "331": 7, "332": 7, "3339": [2, 17], "335": 7, "3359": 7, "336": 7, "3377": 7, "338": 7, "34": 17, "3456": 7, "346": 7, "3485": 7, "3493": 7, "3496": 7, "35": 5, "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, "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], "512": [0, 7], "512x512": 13, "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, "57da31da": 5, "58": 5, "59": 5, "590": 7, "59003": 9, "598": 7, "5a1": 12, "5d806224": 5, "5x3x6": 5, "6": [0, 4, 5, 12, 18, 20, 22, 25, 26], "60": [0, 11, 25], "600000": 12, "612": 7, "633011": 4, "64": 0, "652000": 0, "665": 12, "672000": 0, "68caccff": 5, "7": [0, 2, 4, 12, 20, 21, 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, "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, 21], "Be": [0, 2, 16], "Being": 24, "By": [0, 2, 4, 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, 12, 14, 19, 21, 24, 25, 26], "In": [0, 2, 3, 4, 5, 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, 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, 19, 21, 22, 24, 25, 26], "With": [0, 3, 5, 12, 25], "_____": 17, "________": 17, "____________": 17, "_________________________": 17, "__add__": 23, "__and__": 23, "__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, "_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, 19, 21, 24, 25, 26], "abov": [0, 2, 3, 4, 5, 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, 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": [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, 19, 20, 22], "add": [0, 1, 2, 3, 6, 7, 12, 15, 17, 18, 19, 23, 24], "add_backend": [10, 11], "add_dimens": [0, 2, 7, 23], "addit": [0, 2, 3, 7, 8, 9, 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": [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], "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, 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, 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], "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, 25], "anyth": [11, 25], "anywai": 7, "apart": [0, 6, 17], "apertur": 9, "api": [3, 4, 5, 7, 9, 10, 11, 12, 16, 17, 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, "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, "authconfig": 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_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, 15, 17, 19, 24, 25], "autotick": 19, "auxiliari": 14, "avail": [0, 2, 3, 4, 5, 7, 9, 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, 12, 17, 22, 25, 26], "b03": [9, 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, 20, 25], "background": [0, 3, 10, 20], "backscatt": [0, 2, 10], "bad": [3, 7], "band": [0, 2, 7, 9, 10, 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, 13, 16, 17, 20, 25, 26], "basicconfig": 11, "batch": [0, 1, 2, 7, 13, 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, 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, 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, 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, 16, 18, 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, 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, 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, 20], "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, 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, 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, 22], "com": [0, 3, 9, 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, 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, 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, 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, 8, 24, 25], "conveni": [4, 7, 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, 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, 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, 17, 23], "count_tim": [0, 23], "countmismatch": 2, "coupl": [0, 3, 4, 5, 6, 9, 17, 22, 24, 25, 26], "cours": 13, "cousin": 26, "cover": [0, 2, 4, 7, 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, 20, 24, 25, 26], "create_collect": 0, "create_data_cub": 2, "create_job": [0, 5, 7, 11, 13, 15, 22], "create_raster_cub": [7, 23], "create_servic": [0, 7], "creation": [5, 7, 13], "credenti": [0, 7, 20], "criteria": 17, "crop": 22, "cryptic": [0, 5, 7], "csv": [4, 7, 11], "csvjobdatabas": [10, 11], "cube": [0, 1, 2, 5, 7, 11, 13, 14, 15, 16, 18, 20, 22], "cube1": 2, "cube2": 2, "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, 19, 24, 25], "curv": 2, "custom": [0, 3, 7, 8, 9, 11, 24, 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], "data_dict": 0, "data_list": 0, "data_paramet": 0, "data_typ": 12, "dataarrai": [0, 7, 12, 25], "databas": [3, 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, 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, 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, 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, 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, 11], "dfn": 0, "dict": [0, 7, 12, 14, 25], "dictionari": [0, 4, 7, 14, 17, 18, 24, 25], "did": [0, 4, 7], "didn": 5, "differ": [0, 2, 3, 4, 8, 9, 14, 16, 17, 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, 17, 18, 19, 20, 21, 22, 24, 25, 26], "directori": [0, 3, 5, 8, 11], "disabl": [0, 2, 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, 11, 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, 13, 17, 25], "doubl": [7, 15, 17, 21, 25], "down": [0, 2], "download": [0, 2, 7, 9, 11, 15, 17, 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, 19], "dynam": [0, 20, 25], "e": [0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 17, 19, 21, 22, 23, 24, 25, 26], "e4df8648": 7, "each": [0, 1, 2, 3, 4, 5, 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": [10, 11], "enter": [3, 4, 19], "entir": 0, "entiti": [0, 17], "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, 19, 25, 26], "event": 0, "eventu": 0, "everi": [2, 9, 12, 25], "everyth": [0, 4, 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, 18, 25], "exampl": [0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 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, 10, 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, 8, 21, 24, 25], "explicitli": [0, 2, 3, 4, 5, 7, 14, 17, 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], "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, 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, "feel": [5, 6, 19, 20, 21], "fetch": [3, 5], "fetch_metadata": 0, "few": 9, "fewer": 2, "field": [0, 2, 4, 7, 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, 14, 25], "filter": [0, 2, 4, 7, 9, 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, 17, 19, 22, 26], "find": [0, 2, 4, 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, 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, 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, 24], "flight": 7, "flip": 25, "float": [0, 2, 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, 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], "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, 16, 19, 25], "function": [0, 1, 4, 6, 7, 8, 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, 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], "geojson": [0, 2, 4, 7, 13, 17, 22, 25, 26], "geometri": [0, 2, 4, 5, 7, 17, 22, 24, 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_datacube_list": 0, "get_error_log_path": [10, 11], "get_feature_collection_list": 0, "get_fil": 0, "get_job_dir": [10, 11], "get_job_metadata_path": [10, 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, 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, 18, 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": 19, "green": 12, "grid": [0, 2], "ground": [0, 2, 4], "group": [0, 2, 13], "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, 11, 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, 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, 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, 12, 14, 16, 17, 18, 22, 24, 25, 26], "idea": [17, 25], "ideal": [3, 25], "ident": 3, "identifi": [0, 2, 3, 4, 5], "if_": [2, 23], "ignor": [2, 7, 24], "ignore_nodata": 2, "illumin": 2, "illustr": [3, 4, 16, 17, 20, 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, "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, 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], "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, 20, 25], "inject": [19, 26], "inlin": [0, 4, 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], "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, "intern": [2, 6, 7, 15, 19, 24], "interoper": [0, 7, 15], "interpol": 2, "interpolate_na": 25, "interpret": 0, "interrupt": 11, "intersect": [0, 2], "interv": [0, 2, 3, 26], "introduc": [7, 19, 21, 25], "intrus": 19, "intuit": 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, 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, 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], "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, 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, 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], "leap": 2, "learn": [0, 2, 7, 20], "least": [0, 2, 7, 18, 19, 21, 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, 12, 18, 19, 22, 26], "level": [2, 5, 7, 11, 17, 19, 20, 25], "levelnam": 11, "leverag": [0, 4, 14, 19, 24], "librari": [0, 3, 4, 5, 6, 7, 8, 16, 17, 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, 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, 16, 17, 24], "linspac": 25, "linter": 19, "linux": [3, 19, 25], "list": [0, 2, 4, 6, 7, 14, 16, 17, 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, 12, 13, 15, 16, 17, 24], "lookup": 7, "loop": [5, 7, 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, 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, 17], "manipul": [0, 2, 25], "manner": [3, 17, 25], "manual": [3, 5, 7, 8, 10, 19, 25], "map": [0, 2, 7, 10, 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, 17], "math": [7, 20, 24, 25, 26], "mathemat": [4, 24, 26], "matplotlib": [0, 21], "max": [0, 2, 4, 7, 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, 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, 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, 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, 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, 24, 25], "mortem": 7, "most": [2, 3, 4, 5, 9, 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, 20, 26], "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], "must": [0, 2, 8, 18, 25], "my": [0, 3, 15, 25], "my_bbox": 0, "my_process": 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], "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, 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, "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], "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], "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, 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, 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, "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], "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, "oidcbearerauth": 3, "oidcdevicecodepolltimeout": 7, "oidcprovid": 3, "old": [2, 7, 25], "older": [0, 25], "olivierhagol": 9, "omit": [0, 25], "on_job_cancel": [10, 11], "on_job_don": [10, 11], "on_job_error": [10, 11], "onc": [2, 3, 4, 5, 17, 19, 25], "one": [0, 2, 3, 4, 6, 7, 9, 12, 13, 18, 19, 21, 24, 25], "onelin": 19, "ones": [0, 2, 24], "onli": [0, 1, 2, 4, 5, 6, 7, 8, 9, 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, 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, "overridden": 11, "overrul": 0, "oversampl": 0, "overview": [5, 19], "overwrit": [0, 6], "own": [5, 6, 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, 24], "parametr": [2, 9], "parcel": [13, 17], "parent": [0, 2, 24], "parenthesi": 6, "parquet": [7, 11, 17, 21], "parquetjobdatabas": [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], "particular": [0, 3, 17, 19, 25], "pass": [0, 2, 3, 7, 11, 15, 17, 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, 13], "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, 10, 11], "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], "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, 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, 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, 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": [0, 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, 15, 16, 18, 24, 26], "process_map": 19, "process_with_nod": [0, 7], "processbuild": [0, 1, 7, 20, 23, 24], "processbuilderbas": 0, "processes_dict": 0, "processgraphunflatten": 7, "processgraphvisitexcept": [7, 18], "processgraphvisitor": 7, "produc": [0, 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, 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, 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, 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, 10, 11, 12, 17, 21], "read_text": 25, "readabl": [0, 6, 7, 25], "reader": 6, "readi": [10, 20], "readili": 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, 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": [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], "reli": [4, 12], "reliabl": [5, 25], "reload": 19, "remain": [0, 2, 12, 25], "remaind": 2, "remark": 0, "rememb": [11, 13], "remot": [0, 2, 4, 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": 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, 21, 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], "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, 18, 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, 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, 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, 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, 20, 21, 24, 25], "run_cod": [0, 7, 25], "run_job": [10, 11], "run_synchron": [0, 7], "run_udf": [0, 2, 7, 23, 25], "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, 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, 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], "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, 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, 13, 16, 20, 22, 24, 25], "set_datacube_list": 0, "set_structured_data_list": 0, "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, 21], "sine": 2, "singl": [0, 2, 4, 7, 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, "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, 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": 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, "sphinx_autodoc_typehints_typ": 0, "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, 17, 24], "start_job": [0, 7, 11], "startswith": 5, "stat": [5, 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, "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, "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, 26], "success": [0, 3, 8], "successfulli": [2, 3, 5, 7, 19, 22, 26], "suffici": [9, 25], "suffix": [7, 19], "sugar": [1, 2, 4], "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, 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, 20, 25, 26], "syntact": [1, 2, 4], "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, 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, "tempor": [0, 1, 2, 4, 5, 7, 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, 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, "three": [0, 2], "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, 12, 16, 17, 18, 21, 22, 24, 25, 26], "time_window": 26, "timeout": [0, 3, 7], "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, 25], "togeth": [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, 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, 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, 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, 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, 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, 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, 19], "walk": 2, "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, 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": [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, 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], "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, 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, 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], "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, "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, 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, 16, 22], "basic": [3, 19, 21, 24], "batch": [4, 5], "best": [3, 6], "bit": 24, "build": [0, 19, 26], "call": 24, "callabl": 24, "callback": [24, 25], "case": 4, "caveat": 24, "chang": [7, 25], "changelog": 7, "check": 19, "child": 24, "chunk": 25, "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], "credenti": 3, "cube": [4, 17, 24, 25, 26], "data": [4, 9, 17, 24, 25, 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], "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, 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], "geotrelli": 9, "get": 4, "go": 5, "grain": 5, "graph": [0, 15], "guidelin": 3, "handl": [17, 25], "helper": [2, 3], "high": 0, "hoc": 25, "http": 3, "illustr": 25, "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, "name": 25, "namespac": 16, "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": [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, 12, 15, 16, 18, 23, 24, 25, 26], "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, 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..b006f802f --- /dev/null +++ b/udf.html @@ -0,0 +1,897 @@ + + + + + + + + User-Defined Functions (UDF) explained — openEO Python Client 0.32.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. +See A first example: apply with an UDF to rescale pixel values

+
+
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.

+
+
+
+
+
+

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

+
+
+
+ +
+
+
+

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.

+
+
+

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=[])
+
+
+
+
+

Illustration of data chunking in apply with a UDF

+

TODO

+
+
+

Example: apply_dimension with a UDF

+

TODO

+
+
+

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..41965075b --- /dev/null +++ b/udp.html @@ -0,0 +1,606 @@ + + + + + + + + User-Defined Processes (UDP) — openEO Python Client 0.32.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 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/_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..367b8ed81 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +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..b08d58c9b --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"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] = 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, + }; +} + +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] = item; + + let listItem = document.createElement("li"); + 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 = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).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]. +// 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.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]. + 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], + ]); + } + } + } + + // 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], + ]; + 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]], + ]); + }; + 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], + ]); + } + 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( + '