The Planetary Computer catalogs the datasets we host using the STAC (SpatioTemporal Asset Catalog) specification. We provide a STAC API endpoint for searching our datasets by space, time, and more. This quickstart will show you how to search for data using our STAC API and open-source Python libraries. To use our STAC API from R, see Reading data from the STAC API with R.
To get started you'll need the pystac-client library installed. You can install it via pip:
> python -m pip install pystac-client
To access the data, we'll create a pystac_client.Client
. We'll explain the modifier
part later on, but it's what lets us download the data assets Azure Blob Storage.
import pystac_client
import planetary_computer
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
We can use the STAC API to search for assets meeting some criteria. This might include the date and time the asset covers, is spatial extent, or any other property captured in the STAC item's metadata.
In this example we'll search for imagery from Landsat Collection 2 Level-2 area around Microsoft's main campus in December of 2020.
time_range = "2020-12-01/2020-12-31"
bbox = [-122.2751, 47.5469, -121.9613, 47.7458]
search = catalog.search(collections=["landsat-c2-l2"], bbox=bbox, datetime=time_range)
items = search.get_all_items()
len(items)
/srv/conda/envs/notebook/lib/python3.11/site-packages/pystac_client/item_search.py:841: FutureWarning: get_all_items() is deprecated, use item_collection() instead. warnings.warn(
8
In that example our spatial query used a bounding box with a bbox
. Alternatively, you can pass a GeoJSON object as intersects
area_of_interest = {
"type": "Polygon",
"coordinates": [
[
[-122.2751, 47.5469],
[-121.9613, 47.9613],
[-121.9613, 47.9613],
[-122.2751, 47.9613],
[-122.2751, 47.5469],
]
],
}
time_range = "2020-12-01/2020-12-31"
search = catalog.search(
collections=["landsat-c2-l2"], intersects=area_of_interest, datetime=time_range
)
items
is a pystac.ItemCollection
. We can see that 4 items matched our search criteria.
len(items)
8
Each pystac.Item
in this ItemCollection
includes all the metadata for that scene. STAC Items are GeoJSON features, and so can be loaded by libraries like geopandas.
import geopandas
df = geopandas.GeoDataFrame.from_features(items.to_dict(), crs="epsg:4326")
df
geometry | gsd | created | sci:doi | datetime | platform | proj:epsg | proj:shape | description | instruments | ... | landsat:wrs_row | landsat:scene_id | landsat:wrs_path | landsat:wrs_type | view:sun_azimuth | landsat:correction | view:sun_elevation | landsat:cloud_cover_land | landsat:collection_number | landsat:collection_category | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | POLYGON ((-122.72549 48.50884, -120.29248 48.0... | 30 | 2022-05-06T18:04:17.126358Z | 10.5066/P9OGBGM6 | 2020-12-29T18:55:56.738265Z | landsat-8 | 32610 | [7881, 7781] | Landsat Collection 2 Level-2 | [oli, tirs] | ... | 027 | LC80460272020364LGN00 | 046 | 2 | 162.253231 | L2SP | 17.458298 | 100.00 | 02 | T2 |
1 | POLYGON ((-124.52046 48.44245, -121.93932 48.0... | 30 | 2022-05-06T17:25:29.626986Z | 10.5066/P9C7I13B | 2020-12-28T18:20:32.609164Z | landsat-7 | 32610 | [7361, 8341] | Landsat Collection 2 Level-2 | [etm+] | ... | 027 | LE70470272020363EDC00 | 047 | 2 | 152.689113 | L2SP | 14.678880 | 32.00 | 02 | T1 |
2 | POLYGON ((-122.96802 48.44547, -120.39024 48.0... | 30 | 2022-05-06T18:01:04.319403Z | 10.5066/P9C7I13B | 2020-12-21T18:14:50.812768Z | landsat-7 | 32610 | [7251, 8251] | Landsat Collection 2 Level-2 | [etm+] | ... | 027 | LE70460272020356EDC00 | 046 | 2 | 153.649177 | L2SP | 14.779612 | 24.00 | 02 | T2 |
3 | POLYGON ((-124.27547 48.50831, -121.84167 48.0... | 30 | 2022-05-06T17:46:22.246696Z | 10.5066/P9OGBGM6 | 2020-12-20T19:02:09.878796Z | landsat-8 | 32610 | [7971, 7861] | Landsat Collection 2 Level-2 | [oli, tirs] | ... | 027 | LC80470272020355LGN00 | 047 | 2 | 163.360118 | L2SP | 17.414441 | 100.00 | 02 | T2 |
4 | POLYGON ((-122.72996 48.50858, -120.29690 48.0... | 30 | 2022-05-06T18:04:16.935800Z | 10.5066/P9OGBGM6 | 2020-12-13T18:56:00.096447Z | landsat-8 | 32610 | [7881, 7781] | Landsat Collection 2 Level-2 | [oli, tirs] | ... | 027 | LC80460272020348LGN00 | 046 | 2 | 164.126188 | L2SP | 17.799744 | 98.64 | 02 | T2 |
5 | POLYGON ((-124.51935 48.44597, -121.93965 48.0... | 30 | 2022-05-06T17:25:29.412798Z | 10.5066/P9C7I13B | 2020-12-12T18:21:42.991249Z | landsat-7 | 32610 | [7361, 8341] | Landsat Collection 2 Level-2 | [etm+] | ... | 027 | LE70470272020347EDC00 | 047 | 2 | 154.692691 | L2SP | 15.427422 | 12.00 | 02 | T1 |
6 | POLYGON ((-122.98709 48.44790, -120.40945 48.0... | 30 | 2022-05-06T18:01:04.178839Z | 10.5066/P9C7I13B | 2020-12-05T18:16:03.755599Z | landsat-7 | 32610 | [7281, 8251] | Landsat Collection 2 Level-2 | [etm+] | ... | 027 | LE70460272020340EDC00 | 046 | 2 | 155.308739 | L2SP | 16.313570 | 2.00 | 02 | T1 |
7 | POLYGON ((-124.27385 48.50833, -121.83965 48.0... | 30 | 2022-05-06T17:46:22.097338Z | 10.5066/P9OGBGM6 | 2020-12-04T19:02:11.194486Z | landsat-8 | 32610 | [7971, 7861] | Landsat Collection 2 Level-2 | [oli, tirs] | ... | 027 | LC80470272020339LGN00 | 047 | 2 | 164.914060 | L2SP | 18.807230 | 1.90 | 02 | T1 |
8 rows × 23 columns
Some collections implement the eo
extension, which we can use to sort the items by cloudiness. We'll grab an item with low cloudiness:
selected_item = min(items, key=lambda item: item.properties["eo:cloud_cover"])
print(selected_item)
<Item id=LC08_L2SP_047027_20201204_02_T1>
Each STAC item has one or more Assets, which include links to the actual files.
import rich.table
table = rich.table.Table("Asset Key", "Description")
for asset_key, asset in selected_item.assets.items():
table.add_row(asset_key, asset.title)
table
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Asset Key ┃ Description ┃ ┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ qa │ Surface Temperature Quality Assessment Band │ │ ang │ Angle Coefficients File │ │ red │ Red Band │ │ blue │ Blue Band │ │ drad │ Downwelled Radiance Band │ │ emis │ Emissivity Band │ │ emsd │ Emissivity Standard Deviation Band │ │ trad │ Thermal Radiance Band │ │ urad │ Upwelled Radiance Band │ │ atran │ Atmospheric Transmittance Band │ │ cdist │ Cloud Distance Band │ │ green │ Green Band │ │ nir08 │ Near Infrared Band 0.8 │ │ lwir11 │ Surface Temperature Band │ │ swir16 │ Short-wave Infrared Band 1.6 │ │ swir22 │ Short-wave Infrared Band 2.2 │ │ coastal │ Coastal/Aerosol Band │ │ mtl.txt │ Product Metadata File (txt) │ │ mtl.xml │ Product Metadata File (xml) │ │ mtl.json │ Product Metadata File (json) │ │ qa_pixel │ Pixel Quality Assessment Band │ │ qa_radsat │ Radiometric Saturation and Terrain Occlusion Quality Assessment Band │ │ qa_aerosol │ Aerosol Quality Assessment Band │ │ tilejson │ TileJSON with default rendering │ │ rendered_preview │ Rendered preview │ └──────────────────┴──────────────────────────────────────────────────────────────────────┘
Here, we'll inspect the rendered_preview
asset.
selected_item.assets["rendered_preview"].to_dict()
{'href': 'https://planetarycomputer.microsoft.com/api/data/v1/item/preview.png?collection=landsat-c2-l2&item=LC08_L2SP_047027_20201204_02_T1&assets=red&assets=green&assets=blue&color_formula=gamma+RGB+2.7%2C+saturation+1.5%2C+sigmoidal+RGB+15+0.55&format=png', 'type': 'image/png', 'title': 'Rendered preview', 'rel': 'preview', 'roles': ['overview']}
from IPython.display import Image
Image(url=selected_item.assets["rendered_preview"].href, width=500)
That rendered_preview
asset is generated dynamically from the raw data using the Planetary Computer's data API. We can access the raw data, stored as Cloud Optimzied GeoTIFFs in Azure Blob Storage, using one of the other assets.
The actual data assets are in private Azure Blob Storage containers. If forget to pass modifier=planetary_computer.sign_inplace
or manually sign the item, then you'll get a 404 when trying to access the asset.
That's why we included the modifier=planetary_computer.sign_inplace
when we created the pystac_client.Client
earlier. With that, the results returned by pystac-client are automatically signed, so that a token granting access to the file is included in the URL.
selected_item.assets["blue"].href[:250]
'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/047/027/LC08_L2SP_047027_20201204_20210313_02_T1/LC08_L2SP_047027_20201204_20210313_02_T1_SR_B2.TIF?st=2023-11-06T12%3A35%3A44Z&se=2023-11-14T12%3A35%3A44Z&sp=rl&sv'
Everything after the ?
in that URL is a SAS token grants access to the data. See https://planetarycomputer.microsoft.com/docs/concepts/sas/ for more on using tokens to access data.
import requests
requests.head(selected_item.assets["blue"].href).status_code
200
The 200
status code indicates that we were able to successfully access the data using the "signed" URL with the SAS token included.
# import xarray as xr
import rioxarray
ds = rioxarray.open_rasterio(
selected_item.assets["blue"].href, overview_level=4
).squeeze()
img = ds.plot(cmap="Blues", add_colorbar=False)
img.axes.set_axis_off();
import stackstac
ds = stackstac.stack(items)
ds
/srv/conda/envs/notebook/lib/python3.11/site-packages/stackstac/prepare.py:363: UserWarning: The argument 'infer_datetime_format' is deprecated and will be removed in a future version. A strict version of it is now the default, see https://pandas.pydata.org/pdeps/0004-consistent-to-datetime-parsing.html. You can safely remove this argument. times = pd.to_datetime(
<xarray.DataArray 'stackstac-58124a3f2aeb9e86fe45c8d5489954e7' (time: 8, band: 22, y: 7972, x: 12372)> dask.array<fetch_raster_window, shape=(8, 22, 7972, 12372), dtype=float64, chunksize=(1, 1, 1024, 1024), chunktype=numpy.ndarray> Coordinates: (12/31) * time (time) datetime64[ns] 2020-12-04T19:02:11.19... id (time) <U31 'LC08_L2SP_047027_20201204_02_T1... * band (band) <U13 'qa' 'red' ... 'atmos_opacity' * x (x) float64 3.339e+05 3.339e+05 ... 7.05e+05 * y (y) float64 5.374e+06 5.374e+06 ... 5.135e+06 landsat:wrs_type <U1 '2' ... ... title (band) object 'Surface Temperature Quality A... classification:bitfields (band) object None None ... None common_name (band) object None None None ... None None center_wavelength (band) object None None None ... None None full_width_half_max (band) object None None None ... 2.05 None None epsg int64 32610 Attributes: spec: RasterSpec(epsg=32610, bounds=(333870.0, 5135070.0, 705030.0... crs: epsg:32610 transform: | 30.00, 0.00, 333870.00|\n| 0.00,-30.00, 5374230.00|\n| 0.0... resolution: 30.0
Previously, we searched for items by space and time. Because the Planetary Computer's STAC API supports the query parameter, you can search on additional properties on the STAC item.
For example, collections like sentinel-2-l2a
and landsat-c2-l2
both implement the eo
STAC extension and include an eo:cloud_cover
property. Use query={"eo:cloud_cover": {"lt": 20}}
to return only items that are less than 20% cloudy.
time_range = "2020-12-01/2020-12-31"
bbox = [-122.2751, 47.5469, -121.9613, 47.7458]
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": 20}},
)
items = search.get_all_items()
/srv/conda/envs/notebook/lib/python3.11/site-packages/pystac_client/item_search.py:841: FutureWarning: get_all_items() is deprecated, use item_collection() instead. warnings.warn(
Other common uses of the query
parameter is to filter a collection down to items of a specific type, For example, the GOES-CMI collection includes images from various when the satellite is in various modes, which produces images of either the Full Disk of the earth, the continental United States, or a mesoscale. You can use goes:image-type
to filter down to just the ones you want.
search = catalog.search(
collections=["goes-cmi"],
bbox=[-67.2729, 25.6000, -61.7999, 27.5423],
datetime=["2018-09-11T13:00:00Z", "2018-09-11T15:40:00Z"],
query={"goes:image-type": {"eq": "MESOSCALE"}},
)
STAC items are proper GeoJSON Features, and so can be treated as a kind of data on their own.
import contextily
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=[-124.2751, 45.5469, -110.9613, 47.7458],
datetime="2020-12-26/2020-12-31",
)
items = search.item_collection()
df = geopandas.GeoDataFrame.from_features(items.to_dict(), crs="epsg:4326")
ax = df[["geometry", "datetime", "s2:mgrs_tile", "eo:cloud_cover"]].plot(
facecolor="none", figsize=(12, 6)
)
contextily.add_basemap(
ax, crs=df.crs.to_string(), source=contextily.providers.Esri.NatGeoWorldMap
);
Or we can plot cloudiness of a region over time.
import pandas as pd
search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=[-124.2751, 45.5469, -123.9613, 45.7458],
datetime="2020-01-01/2020-12-31",
)
items = search.get_all_items()
df = geopandas.GeoDataFrame.from_features(items.to_dict())
df["datetime"] = pd.to_datetime(df["datetime"])
ts = df.set_index("datetime").sort_index()["eo:cloud_cover"].rolling(7).mean()
ts.plot(title="eo:cloud-cover (7-scene rolling average)");
/srv/conda/envs/notebook/lib/python3.11/site-packages/pystac_client/item_search.py:841: FutureWarning: get_all_items() is deprecated, use item_collection() instead. warnings.warn(
Our catalog
is a STAC Catalog that we can crawl or search. The Catalog contains STAC Collections for each dataset we have indexed (which is not the yet the entirety of data hosted by the Planetary Computer).
Collections have information about the STAC Items they contain. For instance, here we look at the Bands available for Landsat 8 Collection 2 Level 2 data:
import pandas as pd
landsat = catalog.get_collection("landsat-c2-l2")
pd.DataFrame(landsat.summaries.get_list("eo:bands"))
name | common_name | description | center_wavelength | full_width_half_max | |
---|---|---|---|---|---|
0 | TM_B1 | blue | Visible blue (Thematic Mapper) | 0.49 | 0.07 |
1 | TM_B2 | green | Visible green (Thematic Mapper) | 0.56 | 0.08 |
2 | TM_B3 | red | Visible red (Thematic Mapper) | 0.66 | 0.06 |
3 | TM_B4 | nir08 | Near infrared (Thematic Mapper) | 0.83 | 0.14 |
4 | TM_B5 | swir16 | Short-wave infrared (Thematic Mapper) | 1.65 | 0.20 |
5 | TM_B6 | lwir | Long-wave infrared (Thematic Mapper) | 11.45 | 2.10 |
6 | TM_B7 | swir22 | Short-wave infrared (Thematic Mapper) | 2.22 | 0.27 |
7 | ETM_B1 | blue | Visible blue (Enhanced Thematic Mapper Plus) | 0.48 | 0.07 |
8 | ETM_B2 | green | Visible green (Enhanced Thematic Mapper Plus) | 0.56 | 0.08 |
9 | ETM_B3 | red | Visible red (Enhanced Thematic Mapper Plus) | 0.66 | 0.06 |
10 | ETM_B4 | nir08 | Near infrared (Enhanced Thematic Mapper Plus) | 0.84 | 0.13 |
11 | ETM_B5 | swir16 | Short-wave infrared (Enhanced Thematic Mapper ... | 1.65 | 0.20 |
12 | ETM_B6 | lwir | Long-wave infrared (Enhanced Thematic Mapper P... | 11.34 | 2.05 |
13 | ETM_B7 | swir22 | Short-wave infrared (Enhanced Thematic Mapper ... | 2.20 | 0.28 |
14 | OLI_B1 | coastal | Coastal/Aerosol (Operational Land Imager) | 0.44 | 0.02 |
15 | OLI_B2 | blue | Visible blue (Operational Land Imager) | 0.48 | 0.06 |
16 | OLI_B3 | green | Visible green (Operational Land Imager) | 0.56 | 0.06 |
17 | OLI_B4 | red | Visible red (Operational Land Imager) | 0.65 | 0.04 |
18 | OLI_B5 | nir08 | Near infrared (Operational Land Imager) | 0.87 | 0.03 |
19 | OLI_B6 | swir16 | Short-wave infrared (Operational Land Imager) | 1.61 | 0.09 |
20 | OLI_B7 | swir22 | Short-wave infrared (Operational Land Imager) | 2.20 | 0.19 |
21 | TIRS_B10 | lwir11 | Long-wave infrared (Thermal Infrared Sensor) | 10.90 | 0.59 |
We can see what Assets are available on our item with:
pd.DataFrame.from_dict(landsat.extra_fields["item_assets"], orient="index")[
["title", "description", "gsd"]
]
title | description | gsd | |
---|---|---|---|
qa | Surface Temperature Quality Assessment Band | Collection 2 Level-2 Quality Assessment Band (... | NaN |
ang | Angle Coefficients File | Collection 2 Level-1 Angle Coefficients File | NaN |
red | Red Band | NaN | NaN |
blue | Blue Band | NaN | NaN |
drad | Downwelled Radiance Band | Collection 2 Level-2 Downwelled Radiance Band ... | NaN |
emis | Emissivity Band | Collection 2 Level-2 Emissivity Band (ST_EMIS)... | NaN |
emsd | Emissivity Standard Deviation Band | Collection 2 Level-2 Emissivity Standard Devia... | NaN |
lwir | Surface Temperature Band | Collection 2 Level-2 Thermal Infrared Band (ST... | NaN |
trad | Thermal Radiance Band | Collection 2 Level-2 Thermal Radiance Band (ST... | NaN |
urad | Upwelled Radiance Band | Collection 2 Level-2 Upwelled Radiance Band (S... | NaN |
atran | Atmospheric Transmittance Band | Collection 2 Level-2 Atmospheric Transmittance... | NaN |
cdist | Cloud Distance Band | Collection 2 Level-2 Cloud Distance Band (ST_C... | NaN |
green | Green Band | NaN | NaN |
nir08 | Near Infrared Band 0.8 | NaN | NaN |
lwir11 | Surface Temperature Band | Collection 2 Level-2 Thermal Infrared Band (ST... | 100.0 |
swir16 | Short-wave Infrared Band 1.6 | NaN | NaN |
swir22 | Short-wave Infrared Band 2.2 | Collection 2 Level-2 Short-wave Infrared Band ... | NaN |
coastal | Coastal/Aerosol Band | Collection 2 Level-2 Coastal/Aerosol Band (SR_... | NaN |
mtl.txt | Product Metadata File (txt) | Collection 2 Level-2 Product Metadata File (txt) | NaN |
mtl.xml | Product Metadata File (xml) | Collection 2 Level-2 Product Metadata File (xml) | NaN |
cloud_qa | Cloud Quality Assessment Band | Collection 2 Level-2 Cloud Quality Assessment ... | NaN |
mtl.json | Product Metadata File (json) | Collection 2 Level-2 Product Metadata File (json) | NaN |
qa_pixel | Pixel Quality Assessment Band | Collection 2 Level-1 Pixel Quality Assessment ... | NaN |
qa_radsat | NaN | NaN | NaN |
qa_aerosol | Aerosol Quality Assessment Band | Collection 2 Level-2 Aerosol Quality Assessmen... | NaN |
atmos_opacity | Atmospheric Opacity Band | Collection 2 Level-2 Atmospheric Opacity Band ... | NaN |
Some collections, like Daymet include collection-level assets. You can use the .assets
property to access those assets.
collection = catalog.get_collection("daymet-daily-na")
print(collection)
<CollectionClient id=daymet-daily-na>
Just like assets on items, these assets include links to data in Azure Blob Storage.
asset = collection.assets["zarr-abfs"]
print(asset)
<Asset href=abfs://daymet-zarr/daily/na.zarr>
import xarray as xr
ds = xr.open_zarr(
asset.href,
**asset.extra_fields["xarray:open_kwargs"],
storage_options=asset.extra_fields["xarray:storage_options"],
)
ds
<xarray.Dataset> Dimensions: (time: 14965, y: 8075, x: 7814, nv: 2) Coordinates: lat (y, x) float32 dask.array<chunksize=(284, 584), meta=np.ndarray> lon (y, x) float32 dask.array<chunksize=(284, 584), meta=np.ndarray> * time (time) datetime64[ns] 1980-01-01T12:00:00 ... 20... * x (x) float32 -4.56e+06 -4.559e+06 ... 3.253e+06 * y (y) float32 4.984e+06 4.983e+06 ... -3.09e+06 Dimensions without coordinates: nv Data variables: dayl (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> lambert_conformal_conic int16 ... prcp (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> srad (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> swe (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> time_bnds (time, nv) datetime64[ns] dask.array<chunksize=(365, 2), meta=np.ndarray> tmax (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> tmin (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> vp (time, y, x) float32 dask.array<chunksize=(365, 284, 584), meta=np.ndarray> yearday (time) int16 dask.array<chunksize=(365,), meta=np.ndarray> Attributes: Conventions: CF-1.6 Version_data: Daymet Data Version 4.0 Version_software: Daymet Software Version 4.0 citation: Please see http://daymet.ornl.gov/ for current Daymet ... references: Please see http://daymet.ornl.gov/ for current informa... source: Daymet Software Version 4.0 start_year: 1980
Earlier on, when we created our pystac_client.Client
, we specified modifier=planetary_computer.sign_inplace
. That modifier
will automatically "sign" the STAC metadata, so that the assets can be accessed.
Alternatively, you can manually sign the items.
import pystac
item = pystac.read_file(selected_item.get_self_href())
signed_item = planetary_computer.sign(item) # these assets can be accessed
requests.head(signed_item.assets["blue"].href).status_code
200
Internally, that planetary_computer.sign
method is making a request to the Planetary Computer's SAS API to get a signed HREF for each asset. You could do that manually yourself.
collection = item.get_collection()
storage_account = collection.extra_fields["msft:storage_account"]
container = collection.extra_fields["msft:container"]
response = requests.get(
f"https://planetarycomputer.microsoft.com/api/sas/v1/token/{collection.id}"
)
signed_url = item.assets["blue"].href + "?" + response.json()["token"]
requests.head(signed_url).status_code
200
See https://planetarycomputer.microsoft.com/docs/concepts/sas/ for more on how to manually sign assets.