import requests
from typing import List, Optional, cast
from pystac import Collection, MediaType
from pystac_client import Client, CollectionClient
from datetime import datetime
Access the EOPF Zarr STAC API with Python
Introduction
In this section, we will dive into the programmatic access of EOPF Zarr Collections available in the EOPF Sentinel Zarr Sample Service STAC Catalog. We will introduce Python libraries enable us to effectively access and search through STAC catalogs.
What we will learn
- 🔍 How to programmatically browse through available collections availalbe via the EOPF Zarr STAC API
- 📊 Understanding collection metadata in user-friendly terms
- 🎯 Searching for specific data with help of the
pystac
andpystac-client
libraries.
Prerequisites
For this tutorial, we will make use of the pystac and pystac_client Python libraries that facilitate the programmatic access and efficient search of a STAC Catalog.
Import libraries
Helper functions
list_found_elements
As we are expecting to visualise several elements that will be stored in lists, we define a function that will allow us retrieve item id
’s and collections id
’s for further retrieval.
def list_found_elements(search_result):
id = []
= []
coll for item in search_result.items(): #retrieves the result inside the catalogue.
id.append(item.id)
coll.append(item.collection_id)return id , coll
Establish a connection to the EOPF Zarr STAC Catalog
Our first step is to establish a connection to the EOPF Sentinel Zarr Sample Service STAC Catalog. For this, you need the Catalog’s base URL, which you can find on the web interface under the API & URL tab. By clicking on 🔗Source, you will get the address of the STAC metadata file - which is availabl here.
Copy paste the URL: https://stac.core.eopf.eodc.eu/
.
With the Client.open()
function, we can create the access to the starting point of the Catalog by providing the specific url. If the connection was successful, you will see the description of the STAC catalog and additional information.
= "https://stac.core.eopf.eodc.eu/" #root starting point
eopf_stac_api_root_endpoint = Client.open(url=eopf_stac_api_root_endpoint) # calls the selected url
eopf_catalog eopf_catalog
- type "Catalog"
- id "eopf-sample-service-stac-api"
- stac_version "1.1.0"
- description "STAC catalog of the EOPF Sentinel Zarr Samples Service"
links[] 20 items
0
- rel "self"
- href "https://stac.core.eopf.eodc.eu/"
- type "application/json"
1
- rel "root"
- href "https://stac.core.eopf.eodc.eu/"
- type "application/json"
- title "EOPF Sentinel Zarr Samples Service STAC API"
2
- rel "data"
- href "https://stac.core.eopf.eodc.eu/collections"
- type "application/json"
3
- rel "conformance"
- href "https://stac.core.eopf.eodc.eu/conformance"
- type "application/json"
- title "STAC/OGC conformance classes implemented by this server"
4
- rel "search"
- href "https://stac.core.eopf.eodc.eu/search"
- type "application/geo+json"
- title "STAC search"
- method "GET"
5
- rel "search"
- href "https://stac.core.eopf.eodc.eu/search"
- type "application/geo+json"
- title "STAC search"
- method "POST"
6
- rel "http://www.opengis.net/def/rel/ogc/1.0/queryables"
- href "https://stac.core.eopf.eodc.eu/queryables"
- type "application/schema+json"
- title "Queryables"
- method "GET"
7
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-2-l2a"
- type "application/json"
- title "Sentinel-2 Level-2A"
8
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-slstr-l1-rbt"
- type "application/json"
- title "Sentinel-3 SLSTR Level-1 RBT"
9
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-olci-l2-lfr"
- type "application/json"
- title "Sentinel-3 OLCI Level-2 LFR"
10
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-2-l1c"
- type "application/json"
- title "Sentinel-2 Level-1C"
11
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-slstr-l2-lst"
- type "application/json"
- title "Sentinel-3 SLSTR Level-2 LST"
12
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-1-l1-slc"
- type "application/json"
- title "Sentinel-1 Level-1 SLC"
13
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-olci-l1-efr"
- type "application/json"
- title "Sentinel-3 OLCI Level-1 EFR"
14
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-olci-l1-err"
- type "application/json"
- title "Sentinel-3 OLCI Level-1 ERR"
15
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-1-l2-ocn"
- type "application/json"
- title "Sentinel-1 Level-2 OCN"
16
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-1-l1-grd"
- type "application/json"
- title "Sentinel-1 Level-1 GRD"
17
- rel "child"
- href "https://stac.core.eopf.eodc.eu/collections/sentinel-3-olci-l2-lrr"
- type "application/json"
- title "Sentinel-3 OLCI Level-2 LRR"
18
- rel "service-desc"
- href "https://stac.core.eopf.eodc.eu/api"
- type "application/vnd.oai.openapi+json;version=3.0"
- title "OpenAPI service description"
19
- rel "service-doc"
- href "https://stac.core.eopf.eodc.eu/api.html"
- type "text/html"
- title "OpenAPI service documentation"
conformsTo[] 21 items
- 0 "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2"
- 1 "http://www.opengis.net/spec/cql2/1.0/conf/cql2-json"
- 2 "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text"
- 3 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core"
- 4 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson"
- 5 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30"
- 6 "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter"
- 7 "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter"
- 8 "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter"
- 9 "https://api.stacspec.org/v1.0.0/collections"
- 10 "https://api.stacspec.org/v1.0.0/collections/extensions/transaction"
- 11 "https://api.stacspec.org/v1.0.0/core"
- 12 "https://api.stacspec.org/v1.0.0/item-search"
- 13 "https://api.stacspec.org/v1.0.0/item-search#fields"
- 14 "https://api.stacspec.org/v1.0.0/item-search#query"
- 15 "https://api.stacspec.org/v1.0.0/item-search#sort"
- 16 "https://api.stacspec.org/v1.0.0/ogcapi-features"
- 17 "https://api.stacspec.org/v1.0.0/ogcapi-features#fields"
- 18 "https://api.stacspec.org/v1.0.0/ogcapi-features#query"
- 19 "https://api.stacspec.org/v1.0.0/ogcapi-features#sort"
- 20 "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction"
- title "EOPF Sentinel Zarr Samples Service STAC API"
Congratulations. We successfully connected to the EOPF Zarr STAC Catalog and we can now start exploring its content.
Explore available collections
Once a connection established, the next logical step is to get an overview of all the collections the STAC catalog offers. We can do this with the function get_all_collections()
. The result is a list, which we can loop through to print the relevant collection IDs.
Please note: Since the EOPF Zarr STAC Catalog is still in active development, we need to test whether a collection is valid, otherwise you might get an error message. The code below is testing for validity and for one collection, it throws an error.
You see, that so far, we can browse through 10 available collections
try:
for collection in eopf_catalog.get_all_collections():
print(collection.id)
except Exception:
print(
"* [https://github.com/EOPF-Sample-Service/eopf-stac/issues/18 appears to not be resolved]"
)
sentinel-2-l2a
sentinel-3-slstr-l1-rbt
sentinel-3-olci-l2-lfr
sentinel-2-l1c
sentinel-3-slstr-l2-lst
sentinel-1-l1-slc
sentinel-3-olci-l1-efr
sentinel-3-olci-l1-err
sentinel-1-l2-ocn
sentinel-1-l1-grd
* [https://github.com/EOPF-Sample-Service/eopf-stac/issues/18 appears to not be resolved]
In a next step, we can select one collection
and retrieve certain metadata that allow us to get more information about the selected collection, such as keywords, the ID and useful links for resources.
= eopf_catalog.get_collection('sentinel-2-l2a')
S2l2a_coll print('Keywords: ',S2l2a_coll.keywords)
print('Catalog ID: ',S2l2a_coll.id)
print('Available Links: ',S2l2a_coll.links)
Keywords: ['Copernicus', 'Sentinel', 'EU', 'ESA', 'Satellite', 'Global', 'Imagery', 'Reflectance']
Catalog ID: sentinel-2-l2a
Available Links: [<Link rel=items target=https://stac.core.eopf.eodc.eu/collections/sentinel-2-l2a/items>, <Link rel=parent target=https://stac.core.eopf.eodc.eu/>, <Link rel=root target=<Client id=eopf-sample-service-stac-api>>, <Link rel=self target=https://stac.core.eopf.eodc.eu/collections/sentinel-2-l2a>, <Link rel=license target=https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice>, <Link rel=cite-as target=https://doi.org/10.5270/S2_-znk9xsj>, <Link rel=http://www.opengis.net/def/rel/ogc/1.0/queryables target=https://stac.core.eopf.eodc.eu/collections/sentinel-2-l2a/queryables>]
Searching inside the EOPF STAC API
With the .search()
function of the pystac-client
library, we can search inside a STAC catalog we established a connection with. We can filter based on a series of parameters to tailor the search for available data for a specific time period and geographic bounding box.
Filter for temporal extent
Let us search on the datetime
parameter. For this, we specify the datetime
argument for a time period we are interested in, e.g. from 1 May 2020 to 31 May 2023. In addition, we also specify the collection
parameter indicating that we only want to search for the Sentinel-2 L2A collection.
We apply the helper function list_found_elements
which constructs a list from the search result. If we check the length of the final list, we can see that for the specified time period, 196 items were found.
= eopf_catalog.search( #searching the catalog
time_frame ='sentinel-2-l2a',
collections="2020-05-01T00:00:00Z/2023-05-31T23:59:59.999999Z") # the interval we are interested in, separated by '/'
datetime
# we apply the helper function `list_found_elements`
=list_found_elements(time_frame)
time_itemsprint(time_frame)
print("Search Results:")
print('Total Items Found for Sentinel-2 L-2A between May 1, 2020, and May 31, 2023: ',len(time_items[0]))
<pystac_client.item_search.ItemSearch object at 0x73f66d5c3610>
Search Results:
Total Items Found for Sentinel-2 L-2A between May 1, 2020, and May 31, 2023: 196
Filter for spatial extent
Now, let us filter based on a specific area of interest. We can use the bbox
argument, which is composed by providing the top-left and bottom-right corner coordinates. It is similar to drawing the extent in the interactive map of the EOPF browser interface.
For example, we defined a bounding box of the outskirts of Innsbruck, Austria. We then again apply the helper funnction list_found_elements
and see that for the defined area, only 39 items are available.
= eopf_catalog.search( #searching the catalog
bbox_search ='sentinel-2-l2a',
collections=(
bbox11.124756, 47.311058, #top left
11.459839, 47.463624 #bottom-right
)
)
=list_found_elements(bbox_search) #we apply our constructed function that stores internal information
innsbruck_sets
#Results
print("Search Result:")
print('Total Items Found: ',len(innsbruck_sets[0]))
Search Result:
Total Items Found: 39
Combined filtering: Collection + temporal extent + spatial extent
As a usual workflow, we often look for datasets within an AOI and a specific period of time time. The search()
function allows us also to combine the collection
, bbox
and datetime
arguments in one search request.
Let us now search for Items available for the AOI aroudn Innsbruck within the previously defined timeframe for the Sentinel-2 Level-2A collection. As a result, we get 27 Items that are available for our selection.
= eopf_catalog.search(
innsbruck_s2 = 'sentinel-2-l2a', # interest Collection,
collections=(11.124756, 47.311058, # AOI extent
bbox11.459839,47.463624),
='2020-05-01T00:00:00Z/2025-05-31T23:59:59.999999Z' # interest period
datetime
)
=list_found_elements(innsbruck_s2)
combined_ins
print("Search Results:")
print('Total Items Found for Sentinel-2 L-2A over Innsbruck: ',len(combined_ins[0]))
Search Results:
Total Items Found for Sentinel-2 L-2A over Innsbruck: 27
Let us now repeat a combine search for a different collection. Let us define a new AOI for the coastal area of Rostock, Germany and let us search over the Sentinel-3 SLSTR-L2 collection for the same time period as above. As a result, 14 Items are available for the specified search.
= eopf_catalog.search(
rostock_s3 =(11.766357,53.994566, # AOI extent
bbox12.332153,54.265086),
= ['sentinel-3-slstr-l2-lst'], # interest Collection
collections='2020-05-01T00:00:00Z/2025-05-31T23:59:59.999999Z' # interest period
datetime
)
=list_found_elements(rostock_s3)
combined_ros
print("Search Results:")
print('Total Items Found for Sentinel-3 SLSTR-L2 over Rostock Coast: ',len(combined_ros[0]))
Search Results:
Total Items Found for Sentinel-3 SLSTR-L2 over Rostock Coast: 14
Retrieve Asset URLs for accessing the data
So far, we have made a search among the STAC catalog and browsed over the general metadata of the collections. To access the actual EOPF Zarr Items
, we need to get their storage location in the cloud.
The relevant information we can find inside the .items
argument by the .get_assets()
function. Inside, it allows us to specify the .MediaType
we are interested in. In our example, we want to obtain the location of the .zarr
file.
Let us retrieve the url of the 27 available items over Innsbruck. The resulting URL we can then use to directly access an asset in our workflow.
=[] # a list with the ids of the items we are interested in
assets_locfor x in range(len(combined_ins[0])): # We retrieve only the first asset in the Innsbruck list combined_ins
# we set into the Sentinel-2 L-2A collection
assets_loc.append(S2l2a_coll 0][x]) # We only get the Innsbruck filtered items
.get_item(combined_ins[=MediaType.ZARR)) # we obtain the .zarr location
.get_assets(media_type
= assets_loc[0] # we select the first item from our list
first_item
print("Search Results:")
print('URL for accessing',combined_ins[0][0],'item: ',first_item['product']) # assets_loc[0] corresponds only to the first element:
Search Results:
URL for accessing S2B_MSIL2A_20250530T101559_N0511_R065_T32TPT_20250530T130924 item: <Asset href=https://objects.eodc.eu:443/e05ab01a9d56408d82ac32d69a5aae2a:202505-s02msil2a/30/products/cpm_v256/S2B_MSIL2A_20250530T101559_N0511_R065_T32TPT_20250530T130924.zarr>
Retrieve Item metadata
Finally, once you selected an Item
, you can also explore the relevant metadata on Item
level. For example with the keys()
function, you can retrieve the available assets of the selected Item.
print('Available Assets: ', list(first_item.keys()))
Available Assets: ['SR_10m', 'SR_20m', 'SR_60m', 'AOT_10m', 'B01_20m', 'B02_10m', 'B03_10m', 'B04_10m', 'B05_20m', 'B06_20m', 'B07_20m', 'B08_10m', 'B09_60m', 'B11_20m', 'B12_20m', 'B8A_20m', 'SCL_20m', 'TCI_10m', 'WVP_10m', 'product']
đź’Ş Now it is your turn
The following expercises will help you master the STAC API and understand how to find the data you need.
Task 1: Explore Your Own Area of Interest
- Go to http://bboxfinder.com/ and select an area of interest (AOI) (e.g. your hometown, a research site, etc.)
- Copy the bounding box coordinates of your area of interest
- Change the provided code above to search for data over your AOI
Task 2: Temporal Analysis
- Compare data availability across different years for the Sentinel-2 L-2A Collection.
- Search for items in year 2022
- Repeat the search for year 2024
Task 3: Explore the SAR Mission and combine multiple criteria
- Do the same for a different
Collection
for example the Sentinel-1 Level-1 GRD, e.g. you can use the IDsentinel-1-l1-grd
- How many assets are availanble for the year 2024?
Conclusion
This tutorial has provided a clear and practical introduction on how you can programmatically access and search through EOPF Sentinel Zarr Sample Service STAC API.
What’s next?
In the following section, we will explore how to retrieve an Item of our interest, based on several parameters and load the actual data array as xarray
.