openEO

Python
CDSE
Sentinel-2
Time-series
openEO
Usage examples and benchmarking of openEO to get Sentinel-2 time-series data from CDSE
Published

June 26, 2026

openEO provides a hosted computation interface on the cloud infrastructure itself. It can also be used to download data.

import pickle

import openeo
import xarray as xr
from zarr.storage import ZipStore
with open("../params.pkl", "rb") as fp:
    p = pickle.load(fp)
c = openeo.connect("openeo.dataspace.copernicus.eu").authenticate_oidc()
Authenticated using refresh token.
datacube = c.load_collection(
    "SENTINEL2_L2A",
    temporal_extent=[p["start"], p["end"]],
    bands=["B08", "B11", "SCL"],
    max_cloud_cover=80,
)
datacube = datacube.filter_bbox(p["box_3035"], crs=3035)
datacube = datacube.resample_spatial(resolution=10, projection=3035)
datacube.download("openeo.zarr")
---------------------------------------------------------------------------
OpenEoApiError                            Traceback (most recent call last)
Cell In[6], line 1
----> 1 datacube.download("openeo.zarr")

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/datacube.py:2482, in DataCube.download(self, outputfile, format, options, validate, auto_add_save_result, additional, job_options, on_response_headers)
   2480 else:
   2481     res = self
-> 2482 return self._connection.download(
   2483     res.flat_graph(),
   2484     outputfile=outputfile,
   2485     validate=validate,
   2486     additional=additional,
   2487     job_options=job_options,
   2488     on_response_headers=on_response_headers,
   2489 )

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/connection.py:1766, in Connection.download(self, graph, outputfile, timeout, validate, chunk_size, additional, job_options, on_response_headers)
   1762 pg_with_metadata = self._build_request_with_process_graph(
   1763     process_graph=graph, additional=additional, job_options=job_options
   1764 )
   1765 self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate)
-> 1766 response = self.post(
   1767     path="/result",
   1768     json=pg_with_metadata,
   1769     expected_status=200,
   1770     stream=True,
   1771     timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE,
   1772 )
   1773 if on_response_headers := (on_response_headers or self._on_response_headers_sync):
   1774     on_response_headers(response.headers)

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/_connection.py:232, in RestApiConnection.post(self, path, json, **kwargs)
    224 def post(self, path: str, json: Optional[dict] = None, **kwargs) -> Response:
    225     """
    226     Do POST request to REST API.
    227 
   (...)    230     :return: response: Response
    231     """
--> 232     return self.request("post", path=path, json=json, allow_redirects=False, **kwargs)

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/connection.py:779, in Connection.request(self, method, path, headers, auth, check_error, expected_status, **kwargs)
    772     return super(Connection, self).request(
    773         method=method, path=path, headers=headers, auth=auth,
    774         check_error=check_error, expected_status=expected_status, **kwargs,
    775     )
    777 try:
    778     # Initial request attempt
--> 779     return _request()
    780 except OpenEoApiError as api_exc:
    781     if (
    782         api_exc.http_status_code in {HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN}
    783         and api_exc.code == "TokenInvalid"
    784     ):
    785         # Retry if we can refresh the access token

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/connection.py:772, in Connection.request.<locals>._request()
    771 def _request():
--> 772     return super(Connection, self).request(
    773         method=method, path=path, headers=headers, auth=auth,
    774         check_error=check_error, expected_status=expected_status, **kwargs,
    775     )

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/_connection.py:141, in RestApiConnection.request(self, method, path, params, headers, auth, check_error, expected_status, **kwargs)
    139 expected_status = ensure_list(expected_status) if expected_status else []
    140 if check_error and status >= 400 and status not in expected_status:
--> 141     self._raise_api_error(resp)
    142 if expected_status and status not in expected_status:
    143     raise OpenEoRestError(
    144         "Got status code {s!r} for `{m} {p}` (expected {e!r}) with body {body}".format(
    145             m=method.upper(), p=path, s=status, e=expected_status, body=resp.text
    146         )
    147     )

File ~/Documents/Projects/2026/s2-time-series-access/openeo-nb/.venv/lib/python3.13/site-packages/openeo/rest/_connection.py:163, in RestApiConnection._raise_api_error(self, response)
    161     error_message = info.get("message")
    162     if error_code and isinstance(error_code, str) and error_message and isinstance(error_message, str):
--> 163         raise OpenEoApiError(
    164             http_status_code=status_code,
    165             code=error_code,
    166             message=error_message,
    167             id=info.get("id"),
    168             url=info.get("url"),
    169         )
    171 # Failed to parse it as a compliant openEO API error: show body as-is in the exception.
    172 text = response.text

OpenEoApiError: [500] Internal: Server error: java.nio.file.FileSystemException: /tmp/openeo-pydrvr-85bhln9e.save_result.zarr/B03: Not a directory (ref: r-26062009474249c7b4a6a2ac4acca580)

Apparently there is an issue with a synchronous download of zarr data from the cdse openeo backend. While reporting that issue I got the hint that it might work as an asynchronous batch job. So we will try that next.

%%time
job = datacube.create_job(out_format="zarr")
job.start_and_wait()
results = job.get_results()
result_files = results.download_files("data/")
0:00:00 Job 'j-26062010033146988a309be9a0f9f78c': send 'start'
0:00:02 Job 'j-26062010033146988a309be9a0f9f78c': created (progress 0%)
0:00:07 Job 'j-26062010033146988a309be9a0f9f78c': queued (progress 0%)
0:00:14 Job 'j-26062010033146988a309be9a0f9f78c': queued (progress 0%)
0:00:22 Job 'j-26062010033146988a309be9a0f9f78c': queued (progress 0%)
0:00:32 Job 'j-26062010033146988a309be9a0f9f78c': queued (progress 0%)
0:00:45 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:01:00 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:01:19 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:01:44 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:02:18 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:02:55 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:03:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:04:41 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:05:41 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:06:41 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:07:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:08:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:09:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:10:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:11:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:12:42 Job 'j-26062010033146988a309be9a0f9f78c': running (progress N/A)
0:13:43 Job 'j-26062010033146988a309be9a0f9f78c': finished (progress 100%)
CPU times: user 437 ms, sys: 325 ms, total: 762 ms
Wall time: 13min 53s
[PosixPath('data/out.zarr.zip'), PosixPath('data/job-results.json')]

This works but takes longer than odc-stac. We now have to unzip the zarr so we can read it.

# Open the zip store directly
store = ZipStore(result_files[0], mode="r")
ds = xr.open_zarr(store)
ds.B11.isel(time=22).plot();

Alternatively we can also try and get a netCDF file synchronously.

%%time
datacube.download("openeo.nc")
CPU times: user 592 ms, sys: 583 ms, total: 1.17 s
Wall time: 8min 58s
dataset = xr.open_dataset("openeo.nc")
dataset.B11.isel(t=22).plot();

Synchronously downloading a netcdf file works and is faster than the async batch job for the zarr data. But somehow the two approaches return different acquisitions. The netcdf shows the 26th of March as the 23nd acquisition and the zarr shows the 31st of March.

But keep in mind here, that getting the data with openeo does consume limited credits and is similarly fast as odc-stac.