1717 Iterable ,
1818 Iterator ,
1919 List ,
20+ Mapping ,
2021 Optional ,
2122 Sequence ,
2223 Set ,
2324 Tuple ,
2425 Union ,
2526)
2627
28+ import numpy as np
2729import pystac .asset
2830import pystac .collection
2931import pystac .errors
3032import pystac .item
3133import shapely .geometry
34+ import xarray as xr
3235from affine import Affine
3336from odc .geo import (
3437 CRS ,
4952from odc .loader .types import (
5053 AuxBandMetadata ,
5154 AuxDataSource ,
55+ AuxLoadParams ,
5256 BandKey ,
5357 BandQuery ,
58+ GlobalLoadContext ,
5459 MDParser ,
5560 RasterBandMetadata ,
5661 RasterGroupMetadata ,
6570from pystac .extensions .raster import RasterBand , RasterExtension
6671from toolz import dicttoolz
6772
68- from .model import MDParseConfig , ParsedItem , RasterCollectionMetadata
73+ from .model import (
74+ MDParseConfig ,
75+ ParsedItem ,
76+ PropertyLoadRequest ,
77+ RasterCollectionMetadata ,
78+ )
6979
7080ConversionConfig = Dict [str , Any ]
7181
@@ -485,6 +495,7 @@ def _config(self, collection_id: str | None) -> MDParseConfig:
485495 return MDParseConfig .from_dict (self ._cfg , collection_id )
486496
487497 def extract (self , md : Any ) -> RasterGroupMetadata :
498+ # pylint: disable=too-many-locals
488499 assert isinstance (md , pystac .item .Item )
489500 item = md
490501 c = self ._config (item .collection_id )
@@ -501,7 +512,7 @@ def _keep(kv: tuple[str, pystac.asset.Asset]) -> bool:
501512
502513 data_bands = dicttoolz .itemfilter (_keep , item .assets )
503514
504- bands : dict [BandKey , RasterBandMetadata ] = {}
515+ bands : dict [BandKey , RasterBandMetadata | AuxBandMetadata ] = {}
505516 aliases = alias_map_from_eo (item )
506517
507518 # 1. If band in user config -- use that
@@ -513,12 +524,30 @@ def _keep(kv: tuple[str, pystac.asset.Asset]) -> bool:
513524 for alias , bkey in c .aliases .items ():
514525 aliases .setdefault (alias , []).insert (0 , bkey )
515526
527+ for idx , prop in enumerate (c .with_props ):
528+ bk : BandKey = ("_stac_metadata" , idx + 1 )
529+ bands [bk ] = AuxBandMetadata (
530+ prop .dtype ,
531+ nodata = prop .nodata ,
532+ units = prop .units ,
533+ )
534+ aliases [prop .output_name ] = [bk ]
535+
516536 return RasterGroupMetadata (bands , aliases , c .extra_dims , c .extra_coords )
517537
518538 def driver_data (self , md : Any , band_key : BandKey ) -> Any :
539+ # None for raster bands
540+ # (PropertyLoadRequest, Value|None)
519541 assert isinstance (md , pystac .item .Item )
520- assert band_key
542+ c = self ._config (md .collection_id )
543+
544+ asset_name , band_idx = band_key
521545 driver_data = None
546+
547+ if asset_name == "_stac_metadata" :
548+ prop_cfg = c .with_props [band_idx - 1 ]
549+ driver_data = (prop_cfg , md .properties .get (prop_cfg .key , None ))
550+
522551 return driver_data
523552
524553 def _extract_bands (
@@ -537,6 +566,82 @@ def _extract_bands(
537566 return {(name , idx + 1 ): bm for idx , bm in enumerate (bands )}
538567
539568
569+ class StacAuxReader :
570+ """
571+ Implements AuxReader protocol for STAC items.
572+
573+ Handles reading auxiliary data from STAC items, particularly metadata properties
574+ that are exposed as auxiliary bands.
575+ """
576+
577+ # pylint: disable=too-few-public-methods
578+
579+ def read (
580+ self ,
581+ srcs : Sequence [Sequence [AuxDataSource ]],
582+ cfg : AuxLoadParams ,
583+ used_names : set [str ],
584+ available_coords : Mapping [str , xr .DataArray ],
585+ ctx : GlobalLoadContext ,
586+ * ,
587+ dask_layer_name : str | None = None ,
588+ ) -> xr .DataArray :
589+ """
590+ Read auxiliary data from STAC items.
591+
592+ :param srcs: Auxiliary data sources grouped by time
593+ :param cfg: Loading configuration
594+ :param used_names: Names claimed by raster bands and their coordinates
595+ :param available_coords: Available coordinates, must include time
596+ :param ctx: Load context
597+ :param dask_layer_name: Suggested dask layer name when reading with dask
598+ :return: Auxiliary data loaded into a xarray.DataArray
599+ """
600+ assert (used_names , ctx , dask_layer_name ) is not None
601+ # driver_data: (PropertyLoadRequest, None|float|str|int)
602+
603+ def extract_cfg () -> PropertyLoadRequest :
604+ for row in srcs :
605+ for src in row :
606+ if isinstance (src .driver_data , tuple ) and len (src .driver_data ) == 2 :
607+ prop_cfg , * _ = src .driver_data
608+ return prop_cfg
609+ return PropertyLoadRequest (
610+ key = "" ,
611+ name = "" ,
612+ dtype = cfg .dtype or "float32" ,
613+ nodata = cfg .fill_value ,
614+ )
615+
616+ prop_cfg = extract_cfg ()
617+ _fill = prop_cfg .fill_value
618+ assert _fill is not None
619+
620+ def _value (row : Sequence [AuxDataSource ]) -> Any :
621+ if len (row ) == 0 :
622+ return _fill
623+
624+ dd : Iterator [tuple [PropertyLoadRequest , Any ]] = (
625+ src .driver_data for src in row
626+ )
627+ return prop_cfg .fuser ([v for _ , v in dd if v is not None ])
628+
629+ values = [_value (row ) for row in srcs ]
630+ data = np .array (values , dtype = cfg .dtype )
631+ attrs : dict [str , Any ] = {"units" : prop_cfg .units }
632+ if cfg .fill_value is not None :
633+ attrs ["nodata" ] = cfg .fill_value
634+
635+ # Use time coordinate
636+ time = available_coords ["time" ]
637+ return xr .DataArray (
638+ data ,
639+ coords = {"time" : time },
640+ dims = ["time" ],
641+ attrs = attrs ,
642+ )
643+
644+
540645class _CMDAssembler :
541646 """
542647 Incrementally build up collection metadata from item stream.
@@ -708,32 +813,36 @@ def _get_grid(grid_name: str, asset: pystac.asset.Asset) -> GeoBox:
708813 _grids [grid_name ] = grid
709814 return grid
710815
711- band_names = []
712- for bk , meta in template .meta .bands .items ():
713- asset_name , band_idx = bk
714- asset = _assets .get (asset_name )
715- if asset is None :
716- continue
717- band_names .append (asset_name )
718-
719- grid_name = band2grid .get (asset_name , "default" )
720- geobox : Optional [GeoBox ] = _get_grid (grid_name , asset ) if has_proj else None
721-
722- uri = asset .get_absolute_href ()
723- if uri is None :
724- raise ValueError (
725- f"Can not determine absolute path for band: { asset_name } "
726- ) # pragma: no cover (https://github.com/stac-utils/pystac/issues/754)
727-
816+ def _get_driver_data (bk : BandKey ) -> tuple [Any , str | None ]:
728817 driver_data : Any = None
729818 subdataset : str | None = None
730819 driver_data = md_plugin .driver_data (item , bk )
731820 if isinstance (driver_data , dict ):
732821 subdataset = driver_data .get ("subdataset" , None )
822+ return driver_data , subdataset
823+
824+ for bk , meta in template .meta .bands .items ():
825+ uri : str | None = None
826+ asset_name , band_idx = bk
827+ asset = _assets .get (asset_name )
828+
829+ if asset is not None :
830+ uri = asset .get_absolute_href ()
831+ if uri is None :
832+ raise ValueError (
833+ f"Can not determine absolute path for asset: { asset_name } "
834+ ) # pragma: no cover (https://github.com/stac-utils/pystac/issues/754)
733835
734836 if isinstance (meta , RasterBandMetadata ):
837+ if asset is None or uri is None :
838+ continue
839+
840+ driver_data , subdataset = _get_driver_data (bk )
841+ grid_name = band2grid .get (asset_name , "default" )
842+ geobox : Optional [GeoBox ] = _get_grid (grid_name , asset ) if has_proj else None
843+
735844 # Assumption: if extra dims are defined then asset bands are loaded into 3d+ array
736- # RastetSource .band == 0 indicates "all the bands"
845+ # RasterSource .band == 0 indicates "all the bands"
737846 if meta .extra_dims :
738847 band_idx = 0
739848
@@ -746,16 +855,23 @@ def _get_grid(grid_name: str, asset: pystac.asset.Asset) -> GeoBox:
746855 driver_data = driver_data ,
747856 )
748857 elif isinstance (meta , AuxBandMetadata ):
858+ if uri is None :
859+ uri = f"virtual://{ asset_name } /{ band_idx } "
860+
861+ driver_data , subdataset = _get_driver_data (bk )
749862 bands [bk ] = AuxDataSource (
750863 uri = uri ,
751864 subdataset = subdataset ,
752865 meta = meta ,
753866 driver_data = driver_data ,
754867 )
755868
756- # the assets that aren't bands are accessories
757- acc_names = set (_assets .keys ()).difference (set (band_names ))
758- accessories = {name : {"path" : _assets [name ].href } for name in acc_names }
869+ data_asset_names = set (template .asset_names ())
870+ accessories = {
871+ name : asset .to_dict ()
872+ for name , asset in _assets .items ()
873+ if name not in data_asset_names
874+ }
759875
760876 _cmd = item .common_metadata
761877 return ParsedItem (
@@ -1062,7 +1178,7 @@ def _resolve_driver(
10621178 stac_cfg = {} if stac_cfg is None else stac_cfg
10631179 if driver is None :
10641180 md_parser = StacMDParser (stac_cfg )
1065- return RioDriver (md_parser = md_parser ), md_parser
1181+ return RioDriver (md_parser = md_parser , aux_reader = StacAuxReader () ), md_parser
10661182 rdr = reader_driver (driver )
10671183 md_parser = rdr .md_parser
10681184 if md_parser is None :
0 commit comments