AWS#

Let’s read satellite data stored on AWS.

This tutorial is a bit tricky as EOReader uses rasterio and cloudpathlib libraries and they are currently handling S3 buckets differently. That’s why we’ll use the helper context manager temp_s3 from sertit.s3, that will do the right connections for us.

Note: These Sentinel-2 products are not stored in the .SAFE format. Landsat format is however the same.

Cloud data processed by Element84: Sentinel-2 L2A as COGs#

See this registry (arn:aws:s3:::sentinel-cogs). This registry is open access, so you don’t have to sign-in.

# Imports
import os
import tempenv
import logging

from sertit import logs, s3
from eoreader.reader import Reader, Constellation
from eoreader.bands import BLUE
/home/docs/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/dask/dataframe/__init__.py:42: FutureWarning: 
Dask dataframe query planning is disabled because dask-expr is not installed.

You can install it with `pip install dask[dataframe]` or `conda install dask`.
This will raise in a future version.

  warnings.warn(msg, FutureWarning)
with tempenv.TemporaryEnvironment({
    "AWS_S3_ENDPOINT": "s3.us-west-2.amazonaws.com",
}):
    with s3.temp_s3(no_sign_request=True):
        logs.init_logger(logging.getLogger("eoreader"), logging.DEBUG)
        path = r"s3://sentinel-cogs/sentinel-s2-l2a-cogs/39/K/ZU/2023/10/S2A_39KZU_20231031_0_L2A"
        prod = Reader().open(path)
        prod.plot()
        blue = prod.load(BLUE)[BLUE]
2025-05-05 11:13:47,443 - [DEBUG] - Loading bands ['BLUE']
2025-05-05 11:13:47,508 - [DEBUG] - Read BLUE
2025-05-05 11:13:48,290 - [DEBUG] - Manage nodata for band BLUE
2025-05-05 11:13:48,291 - [DEBUG] - Converting BLUE to reflectance
../_images/dd1c547dfaddac7e3025ddd7051630c9618ce8acfe0b6ed6886e91e4f0034e5b.png
blue[:, ::10, ::10].plot(cmap="Blues_r")
<matplotlib.collections.QuadMesh at 0x7abe64319d60>
../_images/2e84d05aa80b9e3740dcb0f69f4451263f556b1f46abcfb3819b673fcd5462c4.png

Cloud data processed by Sinergise: Sentinel-2 L1C#

See this registry (arn:aws:s3:::sentinel-s2-l1c). This registry needs authentication.

NB: L2A would have been the same (arn:aws:s3:::sentinel-s2-l2a)

Note: Sinergise data are stored as requester pays in AWS. Don’t forget to state this when requesting data!

with tempenv.TemporaryEnvironment({
    "AWS_S3_ENDPOINT": "s3.eu-central-1.amazonaws.com",
    "AWS_SECRET_ACCESS_KEY": os.getenv("AWS_S3_AWS_SECRET_ACCESS_KEY"),
    "AWS_ACCESS_KEY_ID": os.getenv("AWS_S3_AWS_ACCESS_KEY_ID"),
}):
    with s3.temp_s3(requester_pays=True):
        path = r"s3://sentinel-s2-l1c/tiles/10/S/DG/2022/7/8/0"
        prod = Reader().open(path, constellation=Constellation.S2_SIN)
        prod.plot()
---------------------------------------------------------------------------
NoCredentialsError                        Traceback (most recent call last)
Cell In[4], line 8
      6 with s3.temp_s3(requester_pays=True):
      7     path = r"s3://sentinel-s2-l1c/tiles/10/S/DG/2022/7/8/0"
----> 8     prod = Reader().open(path, constellation=Constellation.S2_SIN)
      9     prod.plot()

File ~/checkouts/readthedocs.org/user_builds/eoreader/checkouts/latest/eoreader/reader.py:609, in Reader.open(self, product_path, archive_path, output_path, method, remove_tmp, custom, constellation, **kwargs)
    606     prod = self._open_stac_item(product_path, output_path, remove_tmp, **kwargs)
    607 else:
    608     # If not an Item, it should be a path to somewhere
--> 609     prod = self._open_path(
    610         product_path,
    611         archive_path,
    612         output_path,
    613         method,
    614         remove_tmp,
    615         custom,
    616         constellation,
    617         **kwargs,
    618     )
    620 if not prod:
    621     LOGGER.warning(
    622         f"There is no existing products in EOReader corresponding to {product_path}."
    623     )

File ~/checkouts/readthedocs.org/user_builds/eoreader/checkouts/latest/eoreader/reader.py:730, in Reader._open_path(self, product_path, archive_path, output_path, method, remove_tmp, custom, constellation, **kwargs)
    712 """
    713 Open a path or a cloud URI as an EOReader's product
    714 
   (...)
    725     Product: EOReader's product
    726 """
    728 product_path = AnyPath(product_path)
--> 730 if not product_path.exists():
    731     FileNotFoundError(f"Non existing product: {product_path}")
    733 if custom:

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/cloudpathlib/cloudpath.py:431, in CloudPath.exists(self)
    430 def exists(self) -> bool:
--> 431     return self.client._exists(self)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/cloudpathlib/s3/s3client.py:181, in S3Client._exists(self, cloud_path)
    178     except ClientError:
    179         return False
--> 181 return self._s3_file_query(cloud_path) is not None

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/cloudpathlib/s3/s3client.py:188, in S3Client._s3_file_query(self, cloud_path)
    185 # check if this is an object that we can access directly
    186 try:
    187     # head_object accepts all download extra args (note: Object.load does not accept extra args so we do not use it for this check)
--> 188     self.client.head_object(
    189         Bucket=cloud_path.bucket,
    190         Key=cloud_path.key.rstrip("/"),
    191         **self.boto3_dl_extra_args,
    192     )
    193     return "file"
    195 # else, confirm it is a dir by filtering to the first item under the prefix plus a "/"

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/client.py:569, in ClientCreator._create_api_method.<locals>._api_call(self, *args, **kwargs)
    565     raise TypeError(
    566         f"{py_operation_name}() only accepts keyword arguments."
    567     )
    568 # The "self" in this scope is referring to the BaseClient.
--> 569 return self._make_api_call(operation_name, kwargs)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/client.py:1005, in BaseClient._make_api_call(self, operation_name, api_params)
   1001     maybe_compress_request(
   1002         self.meta.config, request_dict, operation_model
   1003     )
   1004     apply_request_checksum(request_dict)
-> 1005     http, parsed_response = self._make_request(
   1006         operation_model, request_dict, request_context
   1007     )
   1009 self.meta.events.emit(
   1010     f'after-call.{service_id}.{operation_name}',
   1011     http_response=http,
   (...)
   1014     context=request_context,
   1015 )
   1017 if http.status_code >= 300:

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/client.py:1029, in BaseClient._make_request(self, operation_model, request_dict, request_context)
   1027 def _make_request(self, operation_model, request_dict, request_context):
   1028     try:
-> 1029         return self._endpoint.make_request(operation_model, request_dict)
   1030     except Exception as e:
   1031         self.meta.events.emit(
   1032             f'after-call-error.{self._service_model.service_id.hyphenize()}.{operation_model.name}',
   1033             exception=e,
   1034             context=request_context,
   1035         )

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/endpoint.py:119, in Endpoint.make_request(self, operation_model, request_dict)
    113 def make_request(self, operation_model, request_dict):
    114     logger.debug(
    115         "Making request for %s with params: %s",
    116         operation_model,
    117         request_dict,
    118     )
--> 119     return self._send_request(request_dict, operation_model)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/endpoint.py:196, in Endpoint._send_request(self, request_dict, operation_model)
    194 context = request_dict['context']
    195 self._update_retries_context(context, attempts)
--> 196 request = self.create_request(request_dict, operation_model)
    197 success_response, exception = self._get_response(
    198     request, operation_model, context
    199 )
    200 while self._needs_retry(
    201     attempts,
    202     operation_model,
   (...)
    205     exception,
    206 ):

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/endpoint.py:132, in Endpoint.create_request(self, params, operation_model)
    130     service_id = operation_model.service_model.service_id.hyphenize()
    131     event_name = f'request-created.{service_id}.{operation_model.name}'
--> 132     self._event_emitter.emit(
    133         event_name,
    134         request=request,
    135         operation_name=operation_model.name,
    136     )
    137 prepared_request = self.prepare_request(request)
    138 return prepared_request

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/hooks.py:412, in EventAliaser.emit(self, event_name, **kwargs)
    410 def emit(self, event_name, **kwargs):
    411     aliased_event_name = self._alias_event_name(event_name)
--> 412     return self._emitter.emit(aliased_event_name, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/hooks.py:256, in HierarchicalEmitter.emit(self, event_name, **kwargs)
    245 def emit(self, event_name, **kwargs):
    246     """
    247     Emit an event by name with arguments passed as keyword args.
    248 
   (...)
    254              handlers.
    255     """
--> 256     return self._emit(event_name, kwargs)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/hooks.py:239, in HierarchicalEmitter._emit(self, event_name, kwargs, stop_on_response)
    237 for handler in handlers_to_call:
    238     logger.debug('Event %s: calling handler %s', event_name, handler)
--> 239     response = handler(**kwargs)
    240     responses.append((handler, response))
    241     if stop_on_response and response is not None:

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/signers.py:106, in RequestSigner.handler(self, operation_name, request, **kwargs)
    101 def handler(self, operation_name=None, request=None, **kwargs):
    102     # This is typically hooked up to the "request-created" event
    103     # from a client's event emitter.  When a new request is created
    104     # this method is invoked to sign the request.
    105     # Don't call this method directly.
--> 106     return self.sign(operation_name, request)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/signers.py:198, in RequestSigner.sign(self, operation_name, request, region_name, signing_type, expires_in, signing_name)
    195     else:
    196         raise e
--> 198 auth.add_auth(request)

File ~/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/botocore/auth.py:424, in SigV4Auth.add_auth(self, request)
    422 def add_auth(self, request):
    423     if self.credentials is None:
--> 424         raise NoCredentialsError()
    425     datetime_now = datetime.datetime.utcnow()
    426     request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)

NoCredentialsError: Unable to locate credentials

Cloud data processed by USGS: Landsat-8#

See this registry (arn:aws:s3:::usgs-landsat). This registry needs authentication.

Note: USGS data are stored as requester pays in AWS. Don’t forget to state this when requesting data!

with tempenv.TemporaryEnvironment({
    "AWS_S3_ENDPOINT": "s3.us-west-2.amazonaws.com",
    "AWS_SECRET_ACCESS_KEY": os.getenv("AWS_S3_AWS_SECRET_ACCESS_KEY"),
    "AWS_ACCESS_KEY_ID": os.getenv("AWS_S3_AWS_ACCESS_KEY_ID"),
}):
    with s3.temp_s3(requester_pays=True):
        path = r"s3://usgs-landsat/collection02/level-1/standard/oli-tirs/2021/016/042/LC08_L1TP_016042_20211027_20211104_02_T1"
        prod = Reader().open(path)
        prod.plot()