Skip to content

Commit c703ea8

Browse files
committed
feat: add bands
1 parent 1e2f3db commit c703ea8

File tree

7 files changed

+191
-1
lines changed

7 files changed

+191
-1
lines changed

Diff for: pystac/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"STACObjectType",
2222
"Link",
2323
"HIERARCHICAL_LINKS",
24+
"Band",
2425
"Catalog",
2526
"CatalogType",
2627
"Collection",
@@ -75,6 +76,7 @@
7576
SpatialExtent,
7677
TemporalExtent,
7778
)
79+
from pystac.band import Band
7880
from pystac.common_metadata import CommonMetadata
7981
from pystac.summaries import RangeSummary, Summaries
8082
from pystac.asset import Asset

Diff for: pystac/asset.py

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union
88

99
from pystac import common_metadata, utils
10+
from pystac.band import Band
1011
from pystac.html.jinja_env import get_jinja_env
1112

1213
if TYPE_CHECKING:
@@ -71,13 +72,15 @@ def __init__(
7172
description: Optional[str] = None,
7273
media_type: Optional[str] = None,
7374
roles: Optional[List[str]] = None,
75+
bands: Optional[List[Band]] = None,
7476
extra_fields: Optional[Dict[str, Any]] = None,
7577
) -> None:
7678
self.href = utils.make_posix_style(href)
7779
self.title = title
7880
self.description = description
7981
self.media_type = media_type
8082
self.roles = roles
83+
self._bands = bands
8184
self.extra_fields = extra_fields or {}
8285

8386
# The Item which owns this Asset.
@@ -113,6 +116,16 @@ def get_absolute_href(self) -> Optional[str]:
113116
return utils.make_absolute_href(self.href, item_self)
114117
return None
115118

119+
@property
120+
def bands(self) -> Optional[List[Band]]:
121+
if self._bands is None and self.owner is not None:
122+
return self.owner.bands
123+
return self._bands
124+
125+
@bands.setter
126+
def bands(self, bands: Optional[List[Band]]) -> None:
127+
self._bands = bands
128+
116129
def to_dict(self) -> Dict[str, Any]:
117130
"""Returns this Asset as a dictionary.
118131
@@ -138,6 +151,9 @@ def to_dict(self) -> Dict[str, Any]:
138151
if self.roles is not None:
139152
d["roles"] = self.roles
140153

154+
if self.bands is not None:
155+
d["bands"] = [band.to_dict() for band in self.bands]
156+
141157
return d
142158

143159
def clone(self) -> Asset:
@@ -201,6 +217,11 @@ def from_dict(cls: Type[A], d: Dict[str, Any]) -> A:
201217
title = d.pop("title", None)
202218
description = d.pop("description", None)
203219
roles = d.pop("roles", None)
220+
bands = d.pop("bands", None)
221+
if bands is None:
222+
deserialized_bands = None
223+
else:
224+
deserialized_bands = [Band.from_dict(band) for band in bands]
204225
properties = None
205226
if any(d):
206227
properties = d
@@ -211,6 +232,7 @@ def from_dict(cls: Type[A], d: Dict[str, Any]) -> A:
211232
title=title,
212233
description=description,
213234
roles=roles,
235+
bands=deserialized_bands,
214236
extra_fields=properties,
215237
)
216238

Diff for: pystac/band.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from typing import Any, Dict, Optional
5+
6+
7+
@dataclass
8+
class Band:
9+
"""A name and some properties that apply to a band (aka subasset)."""
10+
11+
name: str
12+
"""The name of the band (e.g., "B01", "B8", "band2", "red").
13+
14+
This should be unique across all bands defined in the list of bands. This is
15+
typically the name the data provider uses for the band.
16+
"""
17+
18+
description: Optional[str] = None
19+
"""Description to fully explain the band.
20+
21+
CommonMark 0.29 syntax MAY be used for rich text representation.
22+
"""
23+
24+
properties: Dict[str, Any] = field(default_factory=dict)
25+
"""Other properties on the band."""
26+
27+
@classmethod
28+
def from_dict(cls, d: Dict[str, Any]) -> Band:
29+
"""Creates a new band object from a dictionary."""
30+
try:
31+
name = d.pop("name")
32+
except KeyError:
33+
raise ValueError("missing required field on band: name")
34+
description = d.pop("description", None)
35+
return Band(name=name, description=description, properties=d)
36+
37+
def to_dict(self) -> Dict[str, Any]:
38+
"""Creates a dictionary from this band object."""
39+
d = {
40+
"name": self.name,
41+
}
42+
if self.description is not None:
43+
d["description"] = self.description
44+
d.update(self.properties)
45+
return d

Diff for: pystac/collection.py

+26
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pystac
2323
from pystac import CatalogType, STACError, STACObjectType
2424
from pystac.asset import Asset
25+
from pystac.band import Band
2526
from pystac.catalog import Catalog
2627
from pystac.errors import DeprecatedWarning, ExtensionNotImplemented, STACTypeError
2728
from pystac.layout import HrefLayoutStrategy
@@ -517,6 +518,8 @@ class Collection(Catalog):
517518
"""Default file name that will be given to this STAC object
518519
in a canonical format."""
519520

521+
_bands: Optional[List[Band]]
522+
520523
def __init__(
521524
self,
522525
id: str,
@@ -532,6 +535,7 @@ def __init__(
532535
providers: Optional[List[Provider]] = None,
533536
summaries: Optional[Summaries] = None,
534537
assets: Optional[Dict[str, Asset]] = None,
538+
bands: Optional[List[Band]] = None,
535539
):
536540
super().__init__(
537541
id,
@@ -555,6 +559,8 @@ def __init__(
555559
for k, asset in assets.items():
556560
self.add_asset(k, asset)
557561

562+
self._bands = bands
563+
558564
def __repr__(self) -> str:
559565
return "<Collection id={}>".format(self.id)
560566

@@ -588,6 +594,9 @@ def to_dict(
588594
if any(self.assets):
589595
d["assets"] = {k: v.to_dict() for k, v in self.assets.items()}
590596

597+
if self.bands is not None:
598+
d["bands"] = [band.to_dict() for band in self.bands]
599+
591600
return d
592601

593602
def clone(self) -> Collection:
@@ -664,6 +673,12 @@ def from_dict(
664673
assets = {k: Asset.from_dict(v) for k, v in assets.items()}
665674
links = d.pop("links")
666675

676+
bands = d.pop("bands", None)
677+
if bands is not None:
678+
deserialized_bands = [Band.from_dict(band) for band in bands]
679+
else:
680+
deserialized_bands = None
681+
667682
d.pop("stac_version")
668683

669684
collection = cls(
@@ -680,6 +695,7 @@ def from_dict(
680695
href=href,
681696
catalog_type=catalog_type,
682697
assets=assets,
698+
bands=deserialized_bands,
683699
)
684700

685701
for link in links:
@@ -830,3 +846,13 @@ def full_copy(
830846
@classmethod
831847
def matches_object_type(cls, d: Dict[str, Any]) -> bool:
832848
return identify_stac_object_type(d) == STACObjectType.COLLECTION
849+
850+
@property
851+
def bands(self) -> Optional[List[Band]]:
852+
"""Returns the bands set on this collection."""
853+
return self._bands
854+
855+
@bands.setter
856+
def bands(self, bands: Optional[List[Band]]) -> None:
857+
"""Sets the bands on this collection."""
858+
self._bands = bands

Diff for: pystac/item.py

+26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pystac
88
from pystac import RelType, STACError, STACObjectType
99
from pystac.asset import Asset
10+
from pystac.band import Band
1011
from pystac.catalog import Catalog
1112
from pystac.collection import Collection
1213
from pystac.errors import DeprecatedWarning, ExtensionNotImplemented
@@ -106,6 +107,8 @@ class Item(STACObject):
106107
stac_extensions: List[str]
107108
"""List of extensions the Item implements."""
108109

110+
_bands: Optional[List[Band]]
111+
109112
STAC_OBJECT_TYPE = STACObjectType.ITEM
110113

111114
def __init__(
@@ -122,6 +125,7 @@ def __init__(
122125
collection: Optional[Union[str, Collection]] = None,
123126
extra_fields: Optional[Dict[str, Any]] = None,
124127
assets: Optional[Dict[str, Asset]] = None,
128+
bands: Optional[List[Band]] = None,
125129
):
126130
super().__init__(stac_extensions or [])
127131

@@ -167,6 +171,8 @@ def __init__(
167171
for k, asset in assets.items():
168172
self.add_asset(k, asset)
169173

174+
self._bands = bands
175+
170176
def __repr__(self) -> str:
171177
return "<Item id={}>".format(self.id)
172178

@@ -406,6 +412,16 @@ def get_derived_from(self) -> List[Item]:
406412
"Link failed to resolve. Use get_links instead."
407413
) from e
408414

415+
@property
416+
def bands(self) -> Optional[List[Band]]:
417+
"""Returns the bands set on this item."""
418+
return self._bands
419+
420+
@bands.setter
421+
def bands(self, bands: Optional[List[Band]]) -> None:
422+
"""Sets the bands on this item."""
423+
self._bands = bands
424+
409425
def to_dict(
410426
self, include_self_link: bool = True, transform_hrefs: bool = True
411427
) -> Dict[str, Any]:
@@ -442,6 +458,9 @@ def to_dict(
442458
for key in self.extra_fields:
443459
d[key] = self.extra_fields[key]
444460

461+
if self.bands is not None:
462+
d["properties"]["bands"] = [band.to_dict() for band in self.bands]
463+
445464
return d
446465

447466
def clone(self) -> Item:
@@ -516,13 +535,20 @@ def from_dict(
516535
if k not in [*pass_through_fields, *parse_fields, *exclude_fields]
517536
}
518537

538+
bands = properties.pop("bands", None)
539+
if bands is not None:
540+
deserialized_bands = [Band.from_dict(d) for d in bands]
541+
else:
542+
deserialized_bands = None
543+
519544
item = cls(
520545
**{k: d.get(k) for k in pass_through_fields}, # type: ignore
521546
datetime=datetime,
522547
properties=properties,
523548
extra_fields=extra_fields,
524549
href=href,
525550
assets={k: Asset.from_dict(v) for k, v in assets.items()},
551+
bands=deserialized_bands,
526552
)
527553

528554
for link in links:

Diff for: tests/test_collection.py

+30
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import pystac
1515
from pystac import (
1616
Asset,
17+
Band,
1718
Catalog,
1819
CatalogType,
1920
Collection,
@@ -670,3 +671,32 @@ def test_permissive_temporal_extent_deserialization(collection: Collection) -> N
670671
]["interval"][0]
671672
with pytest.warns(UserWarning):
672673
Collection.from_dict(collection_dict)
674+
675+
676+
def test_set_bands_on_collection(collection: Collection) -> None:
677+
collection.add_asset("data", Asset(href="example.tif"))
678+
collection.bands = [Band(name="analytic")]
679+
assert collection.assets["data"].bands
680+
assert collection.assets["data"].bands[0].name == "analytic"
681+
682+
683+
def test_bands_roundtrip_on_asset(collection: Collection) -> None:
684+
collection.add_asset("data", Asset(href="example.tif"))
685+
collection_dict = collection.to_dict(include_self_link=False, transform_hrefs=False)
686+
collection_dict["assets"]["data"]["bands"] = [{"name": "data"}]
687+
collection = Collection.from_dict(collection_dict)
688+
assert collection.assets["data"].bands
689+
assert collection.assets["data"].bands[0].name == "data"
690+
collection_dict = collection.to_dict(include_self_link=False, transform_hrefs=False)
691+
assert collection_dict["assets"]["data"]["bands"][0]["name"] == "data"
692+
693+
694+
def test_bands_roundtrip_on_collection(collection: Collection) -> None:
695+
collection.add_asset("data", Asset(href="example.tif"))
696+
collection_dict = collection.to_dict(include_self_link=False, transform_hrefs=False)
697+
collection_dict["bands"] = [{"name": "data"}]
698+
collection = Collection.from_dict(collection_dict)
699+
assert collection.assets["data"].bands
700+
assert collection.assets["data"].bands[0].name == "data"
701+
collection_dict = collection.to_dict(include_self_link=False, transform_hrefs=False)
702+
assert collection_dict["bands"][0]["name"] == "data"

Diff for: tests/test_item.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import pystac
1515
import pystac.serialization.common_properties
16-
from pystac import Asset, Catalog, Collection, Item, Link
16+
from pystac import Asset, Band, Catalog, Collection, Item, Link
1717
from pystac.utils import (
1818
datetime_to_str,
1919
get_opt,
@@ -636,3 +636,42 @@ def test_pathlib() -> None:
636636
# This works, but breaks mypy until we fix
637637
# https://github.com/stac-utils/pystac/issues/1216
638638
Item.from_file(Path(TestCases.get_path("data-files/item/sample-item.json")))
639+
640+
641+
def test_bands_do_not_exist(sample_item: Item) -> None:
642+
sample_item.assets["analytic"].bands is None
643+
644+
645+
def test_set_bands(sample_item: Item) -> None:
646+
sample_item.assets["analytic"].bands = [Band(name="analytic")]
647+
assert sample_item.assets["analytic"].bands[0].name == "analytic"
648+
649+
650+
def test_set_bands_on_item(sample_item: Item) -> None:
651+
sample_item.bands = [Band(name="analytic")]
652+
assert sample_item.assets["analytic"].bands
653+
assert sample_item.assets["analytic"].bands[0].name == "analytic"
654+
655+
656+
def test_bands_roundtrip_on_asset(sample_item: Item) -> None:
657+
sample_item_dict = sample_item.to_dict(
658+
include_self_link=False, transform_hrefs=False
659+
)
660+
sample_item_dict["assets"]["analytic"]["bands"] = [{"name": "analytic"}]
661+
item = Item.from_dict(sample_item_dict)
662+
assert item.assets["analytic"].bands
663+
assert item.assets["analytic"].bands[0].name == "analytic"
664+
item_dict = item.to_dict(include_self_link=False, transform_hrefs=False)
665+
assert item_dict["assets"]["analytic"]["bands"][0]["name"] == "analytic"
666+
667+
668+
def test_bands_roundtrip_on_item(sample_item: Item) -> None:
669+
sample_item_dict = sample_item.to_dict(
670+
include_self_link=False, transform_hrefs=False
671+
)
672+
sample_item_dict["properties"]["bands"] = [{"name": "analytic"}]
673+
item = Item.from_dict(sample_item_dict)
674+
assert item.assets["analytic"].bands
675+
assert item.assets["analytic"].bands[0].name == "analytic"
676+
item_dict = item.to_dict(include_self_link=False, transform_hrefs=False)
677+
assert item_dict["properties"]["bands"][0]["name"] == "analytic"

0 commit comments

Comments
 (0)