Documentation >

Reading Data from the STAC API

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.

[1]:
import pystac_client
import planetary_computer

catalog = pystac_client.Client.open(
    "https://planetarycomputer.microsoft.com/api/stac/v1",
    modifier=planetary_computer.sign_inplace,
)

Searching

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.

[2]:
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(
[2]:
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 <https://pystac.readthedocs.io/en/stable/api/item_collection.html#pystac-item-collection>`__. We can see that 4 items matched our search criteria.

[3]:
len(items)
[3]:
8

Each `pystac.Item <https://pystac.readthedocs.io/en/stable/api/pystac.html#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.

[4]:
import geopandas

df = geopandas.GeoDataFrame.from_features(items.to_dict(), crs="epsg:4326")
df
[4]:
row_id 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:

[5]:
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.

[6]:
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
[6]:
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 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.

[7]:
selected_item.assets["rendered_preview"].to_dict()
[7]:
{'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']}
[8]:
from IPython.display import Image

Image(url=selected_item.assets["rendered_preview"].href, width=500)
[8]:

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.

[9]:
selected_item.assets["blue"].href[:250]
[9]:
'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.

[10]:
import requests

requests.head(selected_item.assets["blue"].href).status_code
[10]:
200

The 200 status code indicates that we were able to successfully access the data using the “signed” URL with the SAS token included.

We can load up that single COG using libraries like rioxarray or rasterio

[11]:
# 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();

If you wish to work with multiple STAC items as a datacube, you can use libraries like stackstac or odc-stac.

[12]:
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(
[12]:
<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

Searching on additional properties

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 <https://github.com/stac-extensions/eo>`__ and include an eo:cloud_cover property. Use query={"eo:cloud_cover": {"lt": 20}} to return only items that are less than 20% cloudy.

[13]:
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.

[14]:
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"}},
)

Analyzing STAC Metadata

STAC items are proper GeoJSON Features, and so can be treated as a kind of data on their own.

[15]:
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.

[16]:
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(

Working with STAC Catalogs and Collections

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:

[17]:
import pandas as pd

landsat = catalog.get_collection("landsat-c2-l2")

pd.DataFrame(landsat.summaries.get_list("eo:bands"))
[17]:
row_id 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:

[18]:
pd.DataFrame.from_dict(landsat.extra_fields["item_assets"], orient="index")[
    ["title", "description", "gsd"]
]
[18]:
row_id 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.

[19]:
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.

[20]:
asset = collection.assets["zarr-abfs"]
print(asset)
<Asset href=abfs://daymet-zarr/daily/na.zarr>
[21]:
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
[21]:
<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

Manually signing assets

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.

[22]:
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
[22]:
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.

[23]:
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
[23]:
200

See https://planetarycomputer.microsoft.com/docs/concepts/sas/ for more on how to manually sign assets.