Explore CDSE ODATA API#
Notebook Description#
This Jupyter Notebook demonstrates how to explore and interact with the Copernicus Data Space Ecosystem (CDSE) ODATA API. The notebook provides a step-by-step guide to querying metadata, visualizing product footprints, and downloading geospatial data from various collections.
Key Steps#
Library Imports:
Utilizes libraries such as
requests,geopandas,folium,shapely, andmatplotlibfor querying, processing, and visualizing geospatial data.
Authentication:
Demonstrates how to generate an access token using a username and password for secure interaction with the ODATA API.
Querying the ODATA API:
Searches for products in specific collections (e.g., Sentinel-2, Sentinel-1, Landsat-8) based on spatial (bounding box or AOI), temporal (date range), and cloud cover filters.
Footprint Visualization:
Visualizes the footprints of queried products on an interactive map using
folium.
Data Download:
Downloads selected products as ZIP files and extracts them for further analysis.
Customizable Parameters:
Allows users to specify parameters such as collection name, cloud cover limit, area of interest (AOI), and date range for tailored queries.
Use Case#
This notebook is ideal for users who want to:
Explore metadata and product availability in the Copernicus Data Space Ecosystem.
Visualize product footprints interactively on a map.
Download geospatial data for further analysis.
Prerequisites#
Python environment with the required libraries installed (
requests,geopandas,folium,shapely,matplotlib).A valid CDSE account with a username and password.
Basic understanding of Python and Jupyter Notebooks.
Familiarity with geospatial data concepts.
Import the required libraries#
import os
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import requests
import zipfile
import geopandas as gpd
import folium
from shapely import wkt
from dotenv import load_dotenv
load_dotenv("../.env")
True
Load username and password from env file#
USERNAME = os.getenv('USERNAME')
PASSWORD = os.getenv('PASSWORD')
Helping functions to create geometry from string and convert bounding box format#
def clean_footprint(footprint):
wkt_str = footprint.split(";")[1][:-1]
return wkt.loads(wkt_str)
def convert_bounds(bbox, invert_y=False):
"""
Helper method for changing bounding box representation to leaflet notation
``(lon1, lat1, lon2, lat2) -> ((lat1, lon1), (lat2, lon2))``
"""
x1, y1, x2, y2 = bbox
if invert_y:
y1, y2 = y2, y1
return ((y1, x1), (y2, x2))
Get access token using username and password#
def get_access_token(username: str, password: str) -> str:
data = {
"client_id": "cdse-public",
"username": username,
"password": password,
"grant_type": "password",
}
try:
r = requests.post(
"https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token",
data=data,
)
r.raise_for_status()
except Exception as e:
raise Exception(
f"Access token creation failed. Reponse from the server was: {r.json()}"
)
return r.json()["access_token"]
Query the CDSE ODATA API for differet collections#
# Configuration
start_date = "2024-06-01"
end_date = "2024-09-01"
#data_collection = "LANDSAT-8-ESA"
data_collection = "SENTINEL-2"
# data_collection = "SENTINEL-1"
cloud_cover_limit = 5.00 # Maximum cloud cover percentage
aoi = "POLYGON((19.6 41.8, 22.0 41.8, 26.5 41.2, 28.3 40.3, 28.0 38.8, 26.0 36.0, 24.0 35.0, 22.0 36.0, 20.0 37.0, 19.0 38.5, 19.0 40.0, 19.6 41.8))"
# tile_id = "30TXQ"
product_type = "S2MSI2A"
point_location = "POINT(-1.3009 44.5556)" # Localisation du point au format WKT
#Search in the Copernicus Catalogue
response = requests.get(
f"https://catalogue.dataspace.copernicus.eu/odata/v1/Products?$filter="
f"Collection/Name eq '{data_collection}' and "
f"OData.CSC.Intersects(area=geography'SRID=4326;{aoi}') and "
f"Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq 'cloudCover' and "
f"att/OData.CSC.DoubleAttribute/Value lt {cloud_cover_limit}) and "
# # f"Attributes/OData.CSC.StringAttribute/any(att:att/Name eq 'tileId' and "
# # f"att/OData.CSC.StringAttribute/Value eq '{tile_id}') and "
# f"Attributes/OData.CSC.StringAttribute/any(att:att/Name eq 'productType' and "
# f"att/OData.CSC.StringAttribute/Value eq '{product_type}') and "
# # f"OData.CSC.Within(area=geography'SRID=4326;{point_location}') and "
# f"OData.CSC.Intersects(area=geography'SRID=4326;{point_location}') and "
f"ContentDate/Start gt {start_date}T00:00:00.000Z and "
f"ContentDate/Start lt {end_date}T00:00:00.000Z&$top=100"
)
response.raise_for_status()
metadata = response.json()
products_df = pd.DataFrame.from_dict(metadata["value"])
print(f"Found {len(products_df)} products.")
products_df['geometry'] = products_df['Footprint'].apply(clean_footprint)
gdf = gpd.GeoDataFrame(products_df, geometry='geometry')
gdf.set_crs("EPSG:4326", inplace=True)
Found 100 products.
| @odata.mediaContentType | Id | Name | ContentType | ContentLength | OriginDate | PublicationDate | ModificationDate | Online | EvictionDate | S3Path | Checksum | ContentDate | Footprint | GeoFootprint | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | application/octet-stream | d63cf75d-991d-41fa-9eb4-8f3113f034e7 | S2A_MSIL2A_20240607T094041_N0510_R036_T34TCL_2... | application/octet-stream | 755009760 | 2024-06-07T13:32:15.000000Z | 2024-06-07T13:44:01.038477Z | 2024-11-11T12:15:47.721572Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/07/S2A_MSIL... | [{'Value': '6a9af52984d62382693acde029f58f0b',... | {'Start': '2024-06-07T09:40:41.024000Z', 'End'... | geography'SRID=4326;POLYGON ((19.6816974043764... | {'type': 'Polygon', 'coordinates': [[[19.68169... | POLYGON ((19.6817 41.54318, 18.60282 41.52685,... |
| 1 | application/octet-stream | b2b7f98c-02d9-47bc-a140-be722a140571 | S2A_MSIL2A_20240607T094041_N0510_R036_T34SCJ_2... | application/octet-stream | 164282618 | 2024-06-07T13:31:41.000000Z | 2024-06-07T13:43:31.828287Z | 2024-11-11T12:15:58.747447Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/07/S2A_MSIL... | [{'Value': '863c1af27581008a95dc18031b03d01c',... | {'Start': '2024-06-07T09:40:41.024000Z', 'End'... | geography'SRID=4326;POLYGON ((18.7416089837887... | {'type': 'Polygon', 'coordinates': [[[18.74160... | POLYGON ((18.74161 38.91958, 18.74314 38.92399... |
| 2 | application/octet-stream | 47b03d27-f7d2-442a-9dcc-50909068f818 | S2B_MSIL1C_20240630T085559_N0510_R007_T35SND_2... | application/octet-stream | 882138284 | 2024-06-30T11:29:42.000000Z | 2024-06-30T11:37:28.750697Z | 2024-11-09T10:48:57.344322Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L1C/2024/06/30/S2B_MSIL... | [{'Value': 'aad5230e92fc1b6801a0d93f7ddca00c',... | {'Start': '2024-06-30T08:55:59.024000Z', 'End'... | geography'SRID=4326;POLYGON ((26.9997665456227... | {'type': 'Polygon', 'coordinates': [[[26.99976... | POLYGON ((26.99977 39.75027, 26.99977 38.76086... |
| 3 | application/octet-stream | 6a8ac0dc-b632-44ee-ae75-ea454492ad65 | S2B_MSIL2A_20240630T085559_N0510_R007_T35TNE_2... | application/octet-stream | 1174323525 | 2024-06-30T12:21:04.000000Z | 2024-06-30T12:36:10.616897Z | 2024-11-09T10:48:17.235509Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/30/S2B_MSIL... | [{'Value': 'c7665e40df94d28fb4ec84fca32fc86d',... | {'Start': '2024-06-30T08:55:59.024000Z', 'End'... | geography'SRID=4326;POLYGON ((26.9997634361085... | {'type': 'Polygon', 'coordinates': [[[26.99976... | POLYGON ((26.99976 40.65086, 26.99977 39.66161... |
| 4 | application/octet-stream | fe48c186-fdb5-4ef0-a8a8-2b8763543834 | S2B_MSIL2A_20240630T085559_N0510_R007_T34SGE_2... | application/octet-stream | 78150988 | 2024-06-30T11:42:36.000000Z | 2024-06-30T11:49:44.903938Z | 2024-11-09T10:45:28.873467Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/30/S2B_MSIL... | [{'Value': 'b882d2d3e298d53f41891a066259dba4',... | {'Start': '2024-06-30T08:55:59.024000Z', 'End'... | geography'SRID=4326;POLYGON ((24.2331750694851... | {'type': 'Polygon', 'coordinates': [[[24.23317... | POLYGON ((24.23318 35.11103, 24.39833 35.10722... |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 95 | application/octet-stream | 91f02128-a20e-42ab-8085-f6f5124357e8 | S2A_MSIL2A_20240624T093041_N0510_R136_T34SCJ_2... | application/octet-stream | 1054723531 | 2024-06-24T14:05:02.000000Z | 2024-06-24T14:18:12.391549Z | 2024-11-10T00:44:02.507249Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/24/S2A_MSIL... | [{'Value': 'fc27bdef54452239ed20d7b9a3380b98',... | {'Start': '2024-06-24T09:30:41.024000Z', 'End'... | geography'SRID=4326;POLYGON ((18.6663671694377... | {'type': 'Polygon', 'coordinates': [[[18.66636... | POLYGON ((18.66637 39.72681, 18.6989 38.73821,... |
| 96 | application/octet-stream | f5529255-4b33-40e2-a7bb-17ca694ca79b | S2A_MSIL2A_20240624T093041_N0510_R136_T34TCK_2... | application/octet-stream | 1104061118 | 2024-06-24T14:05:03.000000Z | 2024-06-24T14:18:30.427790Z | 2024-11-10T00:43:01.565370Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/24/S2A_MSIL... | [{'Value': '3fc02d552468d8e4fc52e2e9eca79cf8',... | {'Start': '2024-06-24T09:30:41.024000Z', 'End'... | geography'SRID=4326;POLYGON ((18.6353192129726... | {'type': 'Polygon', 'coordinates': [[[18.63531... | POLYGON ((18.63532 40.62664, 18.66935 39.63822... |
| 97 | application/octet-stream | d4f54da9-df46-46b8-ba47-25f60a80d0a9 | S2A_MSIL2A_20240608T090601_N0510_R050_T34TGK_2... | application/octet-stream | 972343913 | 2024-06-08T13:30:26.000000Z | 2024-06-08T13:42:28.353262Z | 2024-11-11T10:22:07.345864Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/08/S2A_MSIL... | [{'Value': '24341f314e2e35dc0c29a15ead15dd6a',... | {'Start': '2024-06-08T09:06:01.024000Z', 'End'... | geography'SRID=4326;POLYGON ((23.3642082337128... | {'type': 'Polygon', 'coordinates': [[[23.36420... | POLYGON ((23.36421 40.62665, 23.33019 39.63823... |
| 98 | application/octet-stream | 9f86254a-5d99-453f-a4a3-aea18aaf5c36 | S2A_MSIL2A_20240608T090601_N0510_R050_T34SFH_2... | application/octet-stream | 759911864 | 2024-06-08T13:31:04.000000Z | 2024-06-08T13:41:08.985614Z | 2024-11-11T10:22:36.477704Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/08/S2A_MSIL... | [{'Value': '959ce36bfb6864a8d61d2b6c0334224c',... | {'Start': '2024-06-08T09:06:01.024000Z', 'End'... | geography'SRID=4326;POLYGON ((22.4824460421271... | {'type': 'Polygon', 'coordinates': [[[22.48244... | POLYGON ((22.48245 37.84879, 23.3841 37.8353, ... |
| 99 | application/octet-stream | 1347fcde-5c9e-4aea-aad7-e8d008ba983e | S2A_MSIL2A_20240608T090601_N0510_R050_T35SLB_2... | application/octet-stream | 933844693 | 2024-06-08T13:30:56.000000Z | 2024-06-08T13:40:11.253251Z | 2024-11-11T10:24:17.546703Z | True | 9999-12-31T23:59:59.999999Z | /eodata/Sentinel-2/MSI/L2A/2024/06/08/S2A_MSIL... | [{'Value': '115aa839a49809ea0b72d09fb2b0f6e6',... | {'Start': '2024-06-08T09:06:01.024000Z', 'End'... | geography'SRID=4326;POLYGON ((25.9586605029682... | {'type': 'Polygon', 'coordinates': [[[25.95866... | POLYGON ((25.95866 37.9429, 24.72447 37.92558,... |
100 rows × 16 columns
Visualize products footprints using folium#
map1 = folium.Map()
gdf.explore(
"Id",
categorical=True,
tooltip=[
"Id",
"Name",
"OriginDate",
],
popup=True,
style_kwds=dict(fillOpacity=0.1, width=2),
name="ODATA",
m=map1,
)
map1.fit_bounds(bounds=convert_bounds(gdf.unary_union.bounds))
map1
/var/folders/j3/513qxyhx4l30byl48tz1k1jr0000gn/T/ipykernel_25311/4062689537.py:16: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
map1.fit_bounds(bounds=convert_bounds(gdf.unary_union.bounds))
Generate access token to be used to download data#
access_token = get_access_token(USERNAME, PASSWORD)
headers = {"Authorization": f"Bearer {access_token}"}
download_folder = "../data/downloaded-products"
extracted_folder = "../data/extracted-products"
os.makedirs(download_folder, exist_ok=True)
Download data from CDSE ODATA API and extract it#
for _, product in products_df.iterrows():
product_id = product["Id"]
product_title = product["Name"].replace(" ", "_")
shortened_name = "_".join(product_title.split("_")[:6])
download_url = f"https://zipper.dataspace.copernicus.eu/odata/v1/Products({product_id})/$value"
# Start the download session
session = requests.Session()
session.headers.update(headers)
response = session.get(download_url, stream=True)
# Save the product as a ZIP file
zip_file_path = os.path.join(download_folder, f"{shortened_name}.zip")
if response.status_code == 200:
with open(zip_file_path, "wb") as file:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
file.write(chunk)
print(f"Downloaded: {zip_file_path}")
# Extract the ZIP file
try:
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
extraction_path = os.path.join(extracted_folder, shortened_name)
os.makedirs(extraction_path, exist_ok=True)
zip_ref.extractall(extraction_path)
print(f"Extracted to: {extraction_path}")
except zipfile.BadZipFile:
print(f"Error: {zip_file_path} is not a valid ZIP file.")
else:
print(f"Failed to download product {product_title}. HTTP status: {response.status_code}")
break
Downloaded: ../data/downloaded-products/S2A_MSIL2A_20240607T094041_N0510_R036_T34TCL.zip
Extracted to: ../data/extracted-products/S2A_MSIL2A_20240607T094041_N0510_R036_T34TCL