import pickle
import openeo
import xarray as xr
from zarr.storage import ZipStoreopenEO
openEO provides a hosted computation interface on the cloud infrastructure itself. It can also be used to download data.
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.