Repository: opendatacube/odc-stac (language: python)

Issue:
Task: tweak `is_raster_data` logic to be less strict
As reported on Slack and in #5 some qa bands from [Landsat Collection](https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr) are not returned by `odc.stac.load`. This is because `is_raster_data(..)` function returns False for `qa_*` bands from that collection.

```json
"cloud_qa": {
  "title": "Cloud Quality Analysis Band",
  "description": "Collection 2 Level-2 Cloud Quality Opacity Band Surface Reflectance",
  "type": "image/vnd.stac.geotiff; cloud-optimized=true",
  "roles": [
    "metadata",
    "cloud",
    "cloud-shadow",
    "snow-ice",
    "water-mask"
  ]
}
```

Example of band that is ignored by code linked below

https://github.com/opendatacube/odc-stac/blob/5d94d48c3d6418681d05667d004b704b5a55a7ba/odc/stac/_eo3.py#L136-L163


### Proposed fix

1. Only use  `asset.roles` as PASS filter only and do not reject, `"data" in asset.roles ==> ACCEPT`
2. Otherwise check for `"image/" in asset.media_type` and maybe detect and reject thumbnails?

I'm thinking this kind of logic

- If `roles` contain "data" assume raster band
- If media type has `"image/"` then assume Raster Band unless it is a thumbnail (is there a standard `role` for that?)
- If there is no data role and no media type, but asset has proj extension data then assume raster band
- Finally check if `href` looks like it's linking to tif file?

It's probably worthwhile adding config option that allows hard override for what assets to consider as raster bands.

cc: @gadomski 





Discussion / hints:
> It's probably worthwhile adding config option that allows hard override for what assets to consider as raster bands.

Agree with this for sure, since there's no hard and fast rule, makes sense to provide the user a way to give us this information. Other than that, I'd say your heuristic makes sense to me -- media type in particular seems like a useful check.

> If roles contain "data" assume raster band

There have been ongoing discussions of how to support vector data w/ STAC (e.g. https://github.com/radiantearth/stac-spec/issues/1150), so `"data" in roles` might not be a hard-and-fast way of asserting that a given asset is a raster, anyways. One more argument for allowing the user to tell us which assets to use.

Fix patch:
diff --git a/odc/stac/_eo3.py b/odc/stac/_eo3.py
index ddfb93c..c15ae08 100644
--- a/odc/stac/_eo3.py
+++ b/odc/stac/_eo3.py
@@ -7,7 +7,7 @@
 import datetime
 import uuid
 from collections import namedtuple
-from functools import partial, singledispatch
+from functools import singledispatch
 from typing import (
     Any,
     Dict,
@@ -62,6 +62,11 @@
     "view:sun_elevation": "eo:sun_elevation",
 }
 
+# Assets with these roles are ignored unless manually requested
+ROLES_THUMBNAIL = {"thumbnail", "overview"}
+
+# Used to detect image assets when media_type is missing
+RASTER_FILE_EXTENSIONS = {"tif", "tiff", "jpeg", "jpg", "jp2", "img"}
 
 (_eo3,) = (
     metadata_from_doc(d) for d in default_metadata_type_docs() if d.get("name") == "eo3"
@@ -137,10 +142,9 @@ def is_raster_data(asset: pystac.asset.Asset, check_proj: bool = False) -> bool:
     """
     Heuristic for determining if Asset points to raster data.
 
-    - Has "data" role --> True
-    - Has roles other than "data" --> False
-    - Has no role but
-      - media_type has ``image/``
+    - If media type looks like image and roles don't look like thumbnail/overview
+    - If media type is undefined and roles contains "data"
+    - If media type is undefined and href ends on image extension
 
     :param asset:
        STAC Asset to check
@@ -150,6 +154,9 @@ def is_raster_data(asset: pystac.asset.Asset, check_proj: bool = False) -> bool:
        extension enabled and if yes only consider bands with
        projection data as "raster data" bands.
     """
+    # pylint: disable=too-many-return-statements
+    #   some of these are for readability
+
     if check_proj:
         if (
             asset.owner is not None
@@ -158,9 +165,29 @@ def is_raster_data(asset: pystac.asset.Asset, check_proj: bool = False) -> bool:
         ):
             return False
 
-    if asset.roles is not None and len(asset.roles) > 0:
-        return "data" in asset.roles
-    return asset.media_type is not None and "image/" in asset.media_type
+    roles: Set[str] = set(asset.roles or [])
+
+    media_type = asset.media_type
+    if media_type is None:
+        # Type undefined
+        #   Look if it has data role
+        if "data" in roles:
+            return True
+        if "metadata" in roles:
+            return False
+    elif "image/" in media_type:
+        # Image:
+        #    False -- when thumbnail
+        #    True  -- otherwise
+        if any(r in roles for r in ROLES_THUMBNAIL):
+            return False
+        return True
+    else:
+        # Some type that is not `image/*`
+        return False
+
+    ext = asset.href.split(".")[-1].lower()
+    return ext in RASTER_FILE_EXTENSIONS
 
 
 def _mk_1x1_geobox(geom: Geometry) -> GeoBox:
@@ -458,6 +485,7 @@ def infer_dc_product_from_item(
        "*": # Applies to all collections if not defined on a collection
          warnings: ignore
     """
+    # pylint: disable=too-many-locals
     if cfg is None:
         cfg = {}
 
@@ -467,10 +495,15 @@ def infer_dc_product_from_item(
     _cfg.update(cfg.get(collection_id, {}))
 
     quiet = _cfg.get("warnings", "all") == "ignore"
+    band_cfg = _cfg.get("assets", {})
 
-    data_bands: Dict[str, pystac.asset.Asset] = dicttoolz.valfilter(
-        partial(is_raster_data, check_proj=True), item.assets
-    )
+    def _keep(kv):
+        name, asset = kv
+        if name in band_cfg:
+            return True
+        return is_raster_data(asset, check_proj=True)
+
+    data_bands: Dict[str, pystac.asset.Asset] = dicttoolz.itemfilter(_keep, item.assets)
 
     aliases = alias_map_from_eo(item, quiet=quiet)
     aliases.update(_cfg.get("aliases", {}))
@@ -478,7 +511,6 @@ def infer_dc_product_from_item(
     # 1. If band in user config -- use that
     # 2. Use data from raster extension (with fallback to "*" config)
     # 3. Use config for "*" from user config as fallback
-    band_cfg = _cfg.get("assets", {})
     band_defaults = _band_metadata(band_cfg.get("*", {}))
     for name, asset in data_bands.items():
         if name not in band_cfg:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index f124c41..638c2d5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,8 +13,8 @@
 from odc.stac import stac2ds
 
 TEST_DATA_FOLDER: Path = Path(__file__).parent.joinpath("data")
-LANDSAT_STAC: str = "ga_ls8c_ard_3-1-0_088080_2020-05-25_final.stac-item.json"
-LANDSAT_ODC: str = "ga_ls8c_ard_3-1-0_088080_2020-05-25_final.odc-metadata.yaml"
+GA_LANDSAT_STAC: str = "ga_ls8c_ard_3-1-0_088080_2020-05-25_final.stac-item.json"
+GA_LANDSAT_ODC: str = "ga_ls8c_ard_3-1-0_088080_2020-05-25_final.odc-metadata.yaml"
 SENTINEL_STAC_COLLECTION: str = "sentinel-2-l2a.collection.json"
 SENTINEL_STAC: str = "S2A_28QCH_20200714_0_L2A.json"
 SENTINEL_STAC_MS: str = "S2B_MSIL2A_20190629T212529_R043_T06VVN_20201006T080531.json"
@@ -22,7 +22,8 @@
     "S2B_MSIL2A_20190629T212529_R043_T06VVN_20201006T080531_raster_ext.json"
 )
 SENTINEL_ODC: str = "S2A_28QCH_20200714_0_L2A.odc-metadata.json"
-USGS_LANDSAT_STAC: str = "LC08_L2SR_081119_20200101_20200823_02_T2.json"
+USGS_LANDSAT_STAC_v1b: str = "LC08_L2SR_081119_20200101_20200823_02_T2.json"
+USGS_LANDSAT_STAC_v1: str = "LC08_L2SP_028030_20200114_20200824_02_T1_SR.json"
 LIDAR_STAC: str = "lidar_dem.json"
 
 # pylint: disable=redefined-outer-name
@@ -34,53 +35,59 @@ def test_data_dir():
 
 
 @pytest.fixture
-def usgs_landsat_stac():
-    with TEST_DATA_FOLDER.joinpath(USGS_LANDSAT_STAC).open("r") as f:
-        return json.load(f)
+def usgs_landsat_stac_v1():
+    return pystac.item.Item.from_file(
+        str(TEST_DATA_FOLDER.joinpath(USGS_LANDSAT_STAC_v1))
+    )
 
 
 @pytest.fixture
-def landsat_stac():
-    with TEST_DATA_FOLDER.joinpath(LANDSAT_STAC).open("r") as f:
-        metadata = json.load(f)
-    return metadata
+def usgs_landsat_stac_v1b():
+    return pystac.item.Item.from_file(
+        str(TEST_DATA_FOLDER.joinpath(USGS_LANDSAT_STAC_v1b))
+    )
+
+
+@pytest.fixture
+def ga_landsat_stac():
+    return pystac.item.Item.from_file(str(TEST_DATA_FOLDER.joinpath(GA_LANDSAT_STAC)))
 
 
 @pytest.fixture
 def lidar_stac():
-    with TEST_DATA_FOLDER.joinpath(LIDAR_STAC).open("r") as f:
-        metadata = json.load(f)
-    return metadata
+    return pystac.item.Item.from_file(str(TEST_DATA_FOLDER.joinpath(LIDAR_STAC)))
 
 
 @pytest.fixture
-def landsat_odc():
+def ga_landsat_odc():
     metadata = yield from documents.load_documents(
-        TEST_DATA_FOLDER.joinpath(LANDSAT_ODC)
+        TEST_DATA_FOLDER.joinpath(GA_LANDSAT_ODC)
     )
     return metadata
 
 
 @pytest.fixture
 def sentinel_stac():
-    with TEST_DATA_FOLDER.joinpath(SENTINEL_STAC).open("r") as f:
-        metadata = json.load(f)
-    return metadata
+    return pystac.item.Item.from_file(str(TEST_DATA_FOLDER.joinpath(SENTINEL_STAC)))
 
 
 @pytest.fixture
-def sentinel_stac_ms():
+def sentinel_stac_ms_json():
     with TEST_DATA_FOLDER.joinpath(SENTINEL_STAC_MS).open("r") as f:
-        metadata = json.load(f)
-    return metadata
+        return json.load(f)
 
 
 @pytest.fixture
-def sentinel_stac_ms_no_ext():
-    with TEST_DATA_FOLDER.joinpath(SENTINEL_STAC_MS).open("r") as f:
-        metadata = json.load(f)
+def sentinel_stac_ms(sentinel_stac_ms_json):
+    metadata = dict(sentinel_stac_ms_json)
+    return pystac.item.Item.from_dict(metadata)
+
+
+@pytest.fixture
+def sentinel_stac_ms_no_ext(sentinel_stac_ms_json):
+    metadata = dict(sentinel_stac_ms_json)
     metadata["stac_extensions"] = []
-    return metadata
+    return pystac.item.Item.from_dict(metadata)
 
 
 @pytest.fixture
@@ -99,9 +106,7 @@ def sentinel_stac_collection():
 
 @pytest.fixture
 def sentinel_odc():
-    with TEST_DATA_FOLDER.joinpath(SENTINEL_ODC).open("r") as f:
-        metadata = json.load(f)
-    return metadata
+    return pystac.item.Item.from_file(str(TEST_DATA_FOLDER.joinpath(SENTINEL_ODC)))
 
 
 @pytest.fixture
diff --git a/tests/data/LC08_L2SP_028030_20200114_20200824_02_T1_SR.json b/tests/data/LC08_L2SP_028030_20200114_20200824_02_T1_SR.json
new file mode 100644
index 0000000..cc4cf7d
--- /dev/null
+++ b/tests/data/LC08_L2SP_028030_20200114_20200824_02_T1_SR.json
@@ -0,0 +1,422 @@
+{
+  "type": "Feature",
+  "stac_version": "1.0.0",
+  "id": "LC08_L2SP_028030_20200114_20200824_02_T1_SR",
+  "properties": {
+    "datetime": "2020-01-14T17:05:46.732651Z",
+    "eo:cloud_cover": 8.18,
+    "view:sun_azimuth": 158.45194309,
+    "view:sun_elevation": 22.57988462,
+    "platform": "LANDSAT_8",
+    "instruments": [
+      "OLI",
+      "TIRS"
+    ],
+    "view:off_nadir": 0,
+    "landsat:cloud_cover_land": 8.18,
+    "landsat:wrs_type": "2",
+    "landsat:wrs_path": "028",
+    "landsat:wrs_row": "030",
+    "landsat:scene_id": "LC80280302020014LGN00",
+    "landsat:collection_category": "T1",
+    "landsat:collection_number": "02",
+    "landsat:correction": "L2SP",
+    "proj:epsg": 32615,
+    "proj:shape": [8041, 7931],
+    "proj:transform": [30, 0, 173085, 0, -30, 4904715],
+    "created": "2021-07-23T22:39:42.962Z",
+    "updated": "2021-07-23T22:39:42.962Z"
+  },
+  "geometry": {
+    "type": "Polygon",
+    "coordinates": [
+      [
+        [-96.40208024671348, 44.23811017843692],
+        [-96.95575299361194, 42.52538972591066],
+        [-94.76036976322224, 42.11901502264999],
+        [-94.14420841422258, 43.828489225801604],
+        [-96.40208024671348, 44.23811017843692]
+      ]
+    ]
+  },
+  "links": [
+    {
+      "rel": "self",
+      "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr/items/LC08_L2SP_028030_20200114_20200824_02_T1_SR"
+    },
+    {
+      "rel": "parent",
+      "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr"
+    },
+    {
+      "rel": "collection",
+      "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr"
+    },
+    {
+      "rel": "root",
+      "href": "https://landsatlook.usgs.gov/stac-server",
+      "type": "application/json"
+    }
+  ],
+  "assets": {
+    "thumbnail": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_thumb_small.jpeg",
+      "type": "image/jpeg",
+      "title": "Thumbnail image",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_thumb_small.jpeg"
+        }
+      },
+      "file:checksum": "1340521440f80ed531c81f635ad77df7be1f2bf7fccd66cc05bc23f71633e1e92642ceb45c2631bd7a0e6ba9c06ab7a72ebc6d00eee60e9b9a8e7000a56675c60336",
+      "roles": [
+        "thumbnail"
+      ]
+    },
+    "reduced_resolution_browse": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_thumb_large.jpeg",
+      "type": "image/jpeg",
+      "title": "Reduced resolution browse image",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_thumb_large.jpeg"
+        }
+      },
+      "file:checksum": "1340462e9ed936c1bc93c72aa339a8a09a289f619a8fba5a06534314c6bce9a6918ac4ae4e01cdb9ca124ca028d8810faaa833253494dcb4e7157566cb7cd0c988ef",
+      "roles": [
+        "overview"
+      ]
+    },
+    "index": {
+      "href": "https://landsatlook.usgs.gov/stac-browser/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1",
+      "type": "text/html",
+      "title": "HTML index page",
+      "roles": [
+        "metadata"
+      ]
+    },
+    "coastal": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B1.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Coastal/Aerosol Band (B1)",
+      "description": "Collection 2 Level-2 Coastal/Aerosol Band (B1) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B1",
+          "common_name": "coastal",
+          "gsd": 30,
+          "center_wavelength": 0.44
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B1.TIF"
+        }
+      },
+      "file:checksum": "13403132ee7509163551299602296a042ff7d86c4f60171627da3907b96d6a86ca8ad69b5567b2a22ff8b7003699c7d226d52a42945c635f00c107d037382b265c0f",
+      "roles": [
+        "data"
+      ]
+    },
+    "blue": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B2.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Blue Band (B2)",
+      "description": "Collection 2 Level-2 Blue Band (B2) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B2",
+          "common_name": "blue",
+          "gsd": 30,
+          "center_wavelength": 0.48
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B2.TIF"
+        }
+      },
+      "file:checksum": "134001ba2e7f24de7a214ebaf2d83b69530ef3c60586a2eeb4cbfd82f4ef4780ba0a6f41beb5076a4658d335beb148cefb7bcbe7f108cfd155c5fc880289f129e35d",
+      "roles": [
+        "data"
+      ]
+    },
+    "green": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B3.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Green Band (B3)",
+      "description": "Collection 2 Level-2 Green Band (B3) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B3",
+          "common_name": "green",
+          "gsd": 30,
+          "center_wavelength": 0.56
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B3.TIF"
+        }
+      },
+      "file:checksum": "134024d17cf8e377a29b1939762f0be65a28c9e7c6d18706bdc8b976d7f0bb07fd2aca16755637a1e443ed198a3a275711dc3bf3991fb0ceb12c6c08a02b20a7b69f",
+      "roles": [
+        "data"
+      ]
+    },
+    "red": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B4.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Red Band (B4)",
+      "description": "Collection 2 Level-2 Red Band (B4) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B4",
+          "common_name": "red",
+          "gsd": 30,
+          "center_wavelength": 0.65
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B4.TIF"
+        }
+      },
+      "file:checksum": "1340d08a3fd5aca81f359d6c6c4a476e4fff4786901a1ec737a5e61aea2b27cf68c21ed957a6dcbbf15e794251bd4294fdbc845fd9f26ddb788cc15dd71412554400",
+      "roles": [
+        "data"
+      ]
+    },
+    "nir08": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B5.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Near Infrared Band 0.8 (B5)",
+      "description": "Collection 2 Level-2 Near Infrared Band 0.8 (B5) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B5",
+          "common_name": "nir08",
+          "gsd": 30,
+          "center_wavelength": 0.86
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B5.TIF"
+        }
+      },
+      "file:checksum": "13401f14a6a3ebea6fa66cb753e037a98b605a8602bce5b0701f1164b9dc2133d78259e5cb276fb990523f228cef1bd3d2d43575752b60e3cab2116bfcd23235d505",
+      "roles": [
+        "data",
+        "reflectance"
+      ]
+    },
+    "swir16": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B6.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Short-wave Infrared Band 1.6 (B6)",
+      "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (B6) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B6",
+          "common_name": "swir16",
+          "gsd": 30,
+          "center_wavelength": 1.6
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B6.TIF"
+        }
+      },
+      "file:checksum": "1340c3190f73a59f6bc6970f56829b8b2cc9eb4eefd346a9497c66172f05b1fccbc397057a56e2ac7fdb87510883dfe4ac0819d7088a95eb2e05b6945895e83bb6c3",
+      "roles": [
+        "data",
+        "reflectance"
+      ]
+    },
+    "swir22": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B7.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Short-wave Infrared Band 2.2 (B7)",
+      "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (B7) Surface Reflectance",
+      "eo:bands": [
+        {
+          "name": "B7",
+          "common_name": "swir22",
+          "gsd": 30,
+          "center_wavelength": 2.2
+        }
+      ],
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_B7.TIF"
+        }
+      },
+      "file:checksum": "1340ef7da2f70b5c4d8689734165d50463c4747cc197a12d77f0494e89a141cdffe6de1c2c8a0b80620fdfde619d9b03fc3ce398e0678639ecdf781e9d7d3d0bc645",
+      "roles": [
+        "data",
+        "reflectance"
+      ]
+    },
+    "qa_aerosol": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_QA_AEROSOL.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Aerosol Quality Analysis Band",
+      "description": "Collection 2 Level-2 Aerosol Quality Analysis Band (ANG) Surface Reflectance",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_SR_QA_AEROSOL.TIF"
+        }
+      },
+      "file:checksum": "1340b410aed22d62007a4373f2fe07baef482bc26b1248d4eee8e9cf6db4c5c510668cc1baa182b9eff65c4ab1a47dd060483a1a430e0ba7e114d37b156970b8ac40",
+      "roles": [
+        "metadata",
+        "data-mask",
+        "water-mask"
+      ]
+    },
+    "ANG.txt": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_ANG.txt",
+      "type": "text/plain",
+      "title": "Angle Coefficients File",
+      "description": "Collection 2 Level-2 Angle Coefficients File (ANG)",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_ANG.txt"
+        }
+      },
+      "file:checksum": "1340afed7d1efb3ae011c0e5e5c0d9d37f1590d1eb53740d135c26021dc7809924f4e7dd666dae484cf4c7e54383fb1c725a48024e673db3d6f65a680c1c08886e83",
+      "roles": [
+        "metadata"
+      ]
+    },
+    "MTL.txt": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.txt",
+      "type": "text/plain",
+      "title": "Product Metadata File",
+      "description": "Collection 2 Level-2 Product Metadata File (MTL)",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.txt"
+        }
+      },
+      "file:checksum": "1340e5b255dfbf38e23e7915a41e149cec5449ab7e647291d0d3d01ba6e53a95ebda87eeffdfa414f21dc8b5cd01b55a77d47dc3bad7c3fe776b44685d0d595f7105",
+      "roles": [
+        "metadata"
+      ]
+    },
+    "MTL.xml": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.xml",
+      "type": "application/xml",
+      "title": "Product Metadata File (xml)",
+      "description": "Collection 2 Level-1 Product Metadata File (xml)",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.xml"
+        }
+      },
+      "file:checksum": "1340b4707a9135f8ab62d2c03ee5a353af6dc2c6f343ce883e9bdce0f56b1c5f4b0a3f962e4efa2d161e3a4d29f6a8a32bc3f08fd8d274bd44c7c28a63254f626da4",
+      "roles": [
+        "metadata"
+      ]
+    },
+    "MTL.json": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.json",
+      "type": "application/json",
+      "title": "Product Metadata File (json)",
+      "description": "Collection 2 Level-2 Product Metadata File (json)",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_MTL.json"
+        }
+      },
+      "file:checksum": "1340c49465354b708e866687f6b037f789daef9b6cd495ccf258f629a27e6686bfc56a6c62a127e01bd4ee46c9ab70e6c10c8c68369d19195b7e6cb0032e607c4bf0",
+      "roles": [
+        "metadata"
+      ]
+    },
+    "qa_pixel": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_QA_PIXEL.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Pixel Quality Assessment Band",
+      "description": "Collection 2 Level-2 Pixel Quality Assessment Band Surface Reflectance",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_QA_PIXEL.TIF"
+        }
+      },
+      "file:checksum": "1340fb374a0cb56fcb61999ac61b42b8ffab4c2eeec2fd6c6dfe0e4370ba1216caf685b37a104cff3bc97e6364abf94617913558addb6b58718e143fd006d7925791",
+      "roles": [
+        "cloud",
+        "cloud-shadow",
+        "snow-ice",
+        "water-mask"
+      ]
+    },
+    "qa_radsat": {
+      "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_QA_RADSAT.TIF",
+      "type": "image/vnd.stac.geotiff; cloud-optimized=true",
+      "title": "Radiometric Saturation Quality Assessment Band",
+      "description": "Collection 2 Level-2 Radiometric Saturation Quality Assessment Band Surface Reflectance",
+      "alternate": {
+        "s3": {
+          "storage:platform": "AWS",
+          "storage:requester_pays": true,
+          "href": "s3://usgs-landsat/collection02/level-2/standard/oli-tirs/2020/028/030/LC08_L2SP_028030_20200114_20200824_02_T1/LC08_L2SP_028030_20200114_20200824_02_T1_QA_RADSAT.TIF"
+        }
+      },
+      "file:checksum": "1340f350703208f41a2e85edf42dbfb3d4ba7e22c354f2c3c74416bdebe4496497acf5efdbcb3e639fa8838b14550badc081b5c1e3cf5633f427d4a8029338643324",
+      "roles": [
+        "saturation"
+      ]
+    }
+  },
+  "bbox": [
+    -96.95575299361194,
+    42.11901502264999,
+    -94.14420841422258,
+    44.23811017843692
+  ],
+  "stac_extensions": [
+    "https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json",
+    "https://stac-extensions.github.io/view/v1.0.0/schema.json",
+    "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
+    "https://stac-extensions.github.io/file/v1.0.0/schema.json",
+    "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
+    "https://stac-extensions.github.io/alternate-assets/v1.1.0/schema.json",
+    "https://stac-extensions.github.io/storage/v1.0.0/schema.json"
+  ],
+  "collection": "landsat-c2l2-sr",
+  "description": "Landsat Collection 2 Level-2 Surface Reflectance Product"
+}
diff --git a/tests/test_dc_load.py b/tests/test_dc_load.py
index 4d2de9e..a09361b 100644
--- a/tests/test_dc_load.py
+++ b/tests/test_dc_load.py
@@ -11,8 +11,8 @@
 from odc.stac._load import most_common_crs
 
 
-def test_dc_load_smoketest(sentinel_stac_ms):
-    item = pystac.item.Item.from_dict(sentinel_stac_ms)
+def test_dc_load_smoketest(sentinel_stac_ms: pystac.item.Item):
+    item = sentinel_stac_ms
     with pytest.warns(UserWarning, match="`rededge`"):
         (ds,) = stac2ds([item], {})
 
diff --git a/tests/test_index_tools.py b/tests/test_index_tools.py
index 604143b..dba3db2 100644
--- a/tests/test_index_tools.py
+++ b/tests/test_index_tools.py
@@ -5,13 +5,14 @@
 import pytest
 import yaml
 from datacube.utils import geometry as geom
+
 from odc.index._grouper import group_by_nothing, key2num, mid_longitude, solar_offset
 from odc.index._index import (
     month_range,
     parse_doc_stream,
+    product_from_yaml,
     season_range,
     time_range,
-    product_from_yaml,
 )
 
 
diff --git a/tests/test_stac_eo3.py b/tests/test_stac_eo3.py
index 5c97043..f9ff884 100644
--- a/tests/test_stac_eo3.py
+++ b/tests/test_stac_eo3.py
@@ -1,6 +1,9 @@
 import uuid
 
 import pystac
+import pystac.asset
+import pystac.collection
+import pystac.item
 import pytest
 from common import mk_stac_item
 from datacube.testutils.io import native_geobox
@@ -77,8 +80,8 @@ def test_mk_product():
         assert m.units == "1"
 
 
-def test_is_raster_data(sentinel_stac_ms):
-    item = pystac.Item.from_dict(sentinel_stac_ms)
+def test_is_raster_data(sentinel_stac_ms: pystac.item.Item):
+    item = sentinel_stac_ms
     assert "B01" in item.assets
     assert "B02" in item.assets
 
@@ -89,8 +92,8 @@ def test_is_raster_data(sentinel_stac_ms):
     assert is_raster_data(item.assets["B02"])
 
 
-def test_eo3_grids(sentinel_stac_ms):
-    item0 = pystac.Item.from_dict(sentinel_stac_ms)
+def test_eo3_grids(sentinel_stac_ms: pystac.item.Item):
+    item0 = sentinel_stac_ms
 
     item = item0.clone()
     assert item.collection_id == "sentinel-2-l2a"
@@ -119,8 +122,8 @@ def test_eo3_grids(sentinel_stac_ms):
 
 
 def test_infer_product_collection(
-    sentinel_stac_collection: pystac.Collection,
-    sentinel_stac_ms_with_raster_ext: pystac.Item,
+    sentinel_stac_collection: pystac.collection.Collection,
+    sentinel_stac_ms_with_raster_ext: pystac.item.Item,
 ):
 
     with pytest.warns(UserWarning):
@@ -159,8 +162,8 @@ def test_infer_product_collection(
         infer_dc_product([])
 
 
-def test_infer_product_item(sentinel_stac_ms):
-    item = pystac.Item.from_dict(sentinel_stac_ms)
+def test_infer_product_item(sentinel_stac_ms: pystac.item.Item):
+    item = sentinel_stac_ms
 
     assert item.collection_id in STAC_CFG
 
@@ -181,8 +184,8 @@ def test_infer_product_item(sentinel_stac_ms):
 
     assert set(product._stac_cfg["band2grid"]) == set(product.measurements)
 
-    _stac = dicttoolz.dissoc(sentinel_stac_ms, "collection")
-    item_no_collection = pystac.Item.from_dict(_stac)
+    _stac = dicttoolz.dissoc(sentinel_stac_ms.to_dict(), "collection")
+    item_no_collection = pystac.item.Item.from_dict(_stac)
     assert item_no_collection.collection_id is None
 
     with pytest.warns(UserWarning, match="Common name `rededge` is repeated, skipping"):
@@ -190,7 +193,7 @@ def test_infer_product_item(sentinel_stac_ms):
     print(product)
 
 
-def test_infer_product_raster_ext(sentinel_stac_ms_with_raster_ext: pystac.Item):
+def test_infer_product_raster_ext(sentinel_stac_ms_with_raster_ext: pystac.item.Item):
     item = sentinel_stac_ms_with_raster_ext.clone()
     with pytest.warns(UserWarning, match="Common name `rededge` is repeated, skipping"):
         product = infer_dc_product(item)
@@ -205,8 +208,8 @@ def test_infer_product_raster_ext(sentinel_stac_ms_with_raster_ext: pystac.Item)
     assert set(product._stac_cfg["band2grid"]) == set(product.measurements)
 
 
-def test_item_to_ds(sentinel_stac_ms):
-    item0 = pystac.Item.from_dict(sentinel_stac_ms)
+def test_item_to_ds(sentinel_stac_ms: pystac.item.Item):
+    item0 = sentinel_stac_ms
     item = item0.clone()
 
     assert item.collection_id in STAC_CFG
@@ -254,8 +257,8 @@ def test_item_to_ds(sentinel_stac_ms):
             infer_dc_product(item, STAC_CFG)
 
 
-def test_item_to_ds_no_proj(sentinel_stac_ms):
-    item0 = pystac.Item.from_dict(sentinel_stac_ms)
+def test_item_to_ds_no_proj(sentinel_stac_ms: pystac.item.Item):
+    item0 = sentinel_stac_ms
     item = item0.clone()
     item.stac_extensions.remove(
         "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
@@ -268,13 +271,13 @@ def test_item_to_ds_no_proj(sentinel_stac_ms):
     geom = Geometry(item.geometry, "EPSG:4326")
     ds = item_to_ds(item, product)
     assert ds.crs == "EPSG:4326"
+    assert ds.extent is not None
     assert ds.extent.contains(geom)
     assert native_geobox(ds).shape == (1, 1)
 
 
-def test_asset_geobox(sentinel_stac):
-    item0 = pystac.Item.from_dict(sentinel_stac)
-
+def test_asset_geobox(sentinel_stac: pystac.item.Item):
+    item0 = sentinel_stac
     item = item0.clone()
     asset = item.assets["B01"]
     geobox = asset_geobox(asset)
@@ -309,12 +312,11 @@ def test_asset_geobox(sentinel_stac):
         asset_geobox(asset)
 
 
-def test_has_proj_ext(sentinel_stac_ms_no_ext):
-    item = pystac.Item.from_dict(sentinel_stac_ms_no_ext)
-    assert has_proj_ext(item) is False
+def test_has_proj_ext(sentinel_stac_ms_no_ext: pystac.item.Item):
+    assert has_proj_ext(sentinel_stac_ms_no_ext) is False
 
 
-def test_band_metadata(sentinel_stac_ms_with_raster_ext: pystac.Item):
+def test_band_metadata(sentinel_stac_ms_with_raster_ext: pystac.item.Item):
     item = sentinel_stac_ms_with_raster_ext.clone()
     asset = item.assets["SCL"]
     bm = band_metadata(asset, BandMetadata("uint16", 0, "1"))
@@ -352,3 +354,43 @@ def test_item_uuid():
     assert id1.version == 5
     assert id2.version == 5
     assert id1 != id2
+
+
+def test_is_raster_data_more():
+    def _a(href="http://example.com/", **kw):
+        return pystac.asset.Asset(href, **kw)
+
+    assert is_raster_data(_a(media_type="image/jpeg")) is True
+    assert is_raster_data(_a(media_type="image/jpeg", roles=["data"])) is True
+    assert is_raster_data(_a(media_type="image/jpeg", roles=["overview"])) is False
+    assert is_raster_data(_a(media_type="image/jpeg", roles=["thumbnail"])) is False
+
+    # no media type defined
+    assert is_raster_data(_a(roles=["data"])) is True
+    assert is_raster_data(_a(roles=["metadata"])) is False
+    assert is_raster_data(_a(roles=["custom-22"])) is False
+
+    # based on extension
+    assert is_raster_data(_a(href="/foo.tif")) is True
+    assert is_raster_data(_a(href="/foo.tiff")) is True
+    assert is_raster_data(_a(href="/foo.TIF")) is True
+    assert is_raster_data(_a(href="/foo.TIFF")) is True
+    assert is_raster_data(_a(href="/foo.jpeg")) is True
+    assert is_raster_data(_a(href="/foo.jpg")) is True
+
+
+def test_issue_n6(usgs_landsat_stac_v1):
+    expected_bands = {
+        "blue",
+        "coastal",
+        "green",
+        "nir08",
+        "red",
+        "swir16",
+        "swir22",
+        "qa_aerosol",
+        "qa_pixel",
+        "qa_radsat",
+    }
+    p = infer_dc_product(usgs_landsat_stac_v1)
+    assert set(p.measurements) == expected_bands
diff --git a/tests/test_utm.py b/tests/test_utm.py
index 4de5c7b..6c3cd71 100644
--- a/tests/test_utm.py
+++ b/tests/test_utm.py
@@ -1,8 +1,8 @@
 """Tests for UTM utilities in odc.index._utm."""
 
 import pytest
-
 from datacube.utils.geometry import unary_union
+
 from odc.index import bin_dataset_stream, bin_dataset_stream2
 from odc.index._utm import mk_utm_gs, utm_region_code, utm_tile_dss, utm_zone_to_epsg