Skip to content

Commit 94eb97c

Browse files
authored
Implement Storage Extension (#745)
* Implement Storage Extension * Fix docstring in EOExtension * Add StorageExtension to docs * Add CHANGELOG entry for #745 * Fix lint errors
1 parent ca49adb commit 94eb97c

File tree

8 files changed

+800
-1
lines changed

8 files changed

+800
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Experimental support for Python 3.11 ([#731](https://github.com/stac-utils/pystac/pull/731))
88
- Accept PathLike objects in `StacIO` I/O methods, `pystac.read_file` and `pystac.write_file` ([#728](https://github.com/stac-utils/pystac/pull/728))
9+
- Support for Storage Extension ([#745](https://github.com/stac-utils/pystac/pull/745))
910
- Optional `StacIO` instance as argument to `Catalog.save`/`Catalog.normalize_and_save` ([#751](https://github.com/stac-utils/pystac/pull/751))
1011

1112
### Removed

docs/api/extensions/storage.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pystac.extensions.storage
2+
============================
3+
4+
.. automodule:: pystac.extensions.storage
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

pystac/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
import pystac.extensions.sar
9090
import pystac.extensions.sat
9191
import pystac.extensions.scientific
92+
import pystac.extensions.storage
9293
import pystac.extensions.table
9394
import pystac.extensions.timestamps
9495
import pystac.extensions.version
@@ -106,6 +107,7 @@
106107
pystac.extensions.sar.SAR_EXTENSION_HOOKS,
107108
pystac.extensions.sat.SAT_EXTENSION_HOOKS,
108109
pystac.extensions.scientific.SCIENTIFIC_EXTENSION_HOOKS,
110+
pystac.extensions.storage.STORAGE_EXTENSION_HOOKS,
109111
pystac.extensions.table.TABLE_EXTENSION_HOOKS,
110112
pystac.extensions.timestamps.TIMESTAMPS_EXTENSION_HOOKS,
111113
pystac.extensions.version.VERSION_EXTENSION_HOOKS,

pystac/extensions/eo.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ class EOExtension(
299299
def apply(
300300
self, bands: Optional[List[Band]] = None, cloud_cover: Optional[float] = None
301301
) -> None:
302-
"""Applies label extension properties to the extended :class:`~pystac.Item` or
302+
"""Applies Electro-Optical Extension properties to the extended :class:`~pystac.Item` or
303303
:class:`~pystac.Asset`.
304304
305305
Args:

pystac/extensions/storage.py

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
"""Implements the Storage Extension.
2+
3+
https://github.com/stac-extensions/storage
4+
"""
5+
6+
from typing import (
7+
Any,
8+
Dict,
9+
Generic,
10+
Iterable,
11+
List,
12+
Optional,
13+
Set,
14+
TypeVar,
15+
Union,
16+
cast,
17+
)
18+
19+
import pystac
20+
from pystac.extensions.base import (
21+
ExtensionManagementMixin,
22+
PropertiesExtension,
23+
SummariesExtension,
24+
)
25+
from pystac.extensions.hooks import ExtensionHooks
26+
from pystac.utils import StringEnum
27+
28+
T = TypeVar("T", pystac.Item, pystac.Asset)
29+
30+
SCHEMA_URI: str = "https://stac-extensions.github.io/storage/v1.0.0/schema.json"
31+
PREFIX: str = "storage:"
32+
33+
# Field names
34+
PLATFORM_PROP: str = PREFIX + "platform"
35+
REGION_PROP: str = PREFIX + "region"
36+
REQUESTER_PAYS_PROP: str = PREFIX + "requester_pays"
37+
TIER_PROP: str = PREFIX + "tier"
38+
39+
40+
class CloudPlatform(StringEnum):
41+
ALIBABA = "ALIBABA"
42+
AWS = "AWS"
43+
AZURE = "AZURE"
44+
GCP = "GCP"
45+
IBM = "IBM"
46+
ORACLE = "ORACLE"
47+
OTHER = "OTHER"
48+
49+
50+
class StorageExtension(
51+
Generic[T],
52+
PropertiesExtension,
53+
ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]],
54+
):
55+
"""An abstract class that can be used to extend the properties of an
56+
:class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the
57+
:stac-ext:`Storage Extension <storage>`. This class is generic over the type of
58+
STAC Object to be extended (e.g. :class:`~pystac.Item`,
59+
:class:`~pystac.Asset`).
60+
61+
To create a concrete instance of :class:`StorageExtension`, use the
62+
:meth:`StorageExtension.ext` method. For example:
63+
64+
.. code-block:: python
65+
66+
>>> item: pystac.Item = ...
67+
>>> storage_ext = StorageExtension.ext(item)
68+
"""
69+
70+
def apply(
71+
self,
72+
platform: Optional[CloudPlatform] = None,
73+
region: Optional[str] = None,
74+
requester_pays: Optional[bool] = None,
75+
tier: Optional[str] = None,
76+
) -> None:
77+
"""Applies Storage Extension properties to the extended :class:`~pystac.Item` or
78+
:class:`~pystac.Asset`.
79+
80+
Args:
81+
platform (str, CloudProvider) : The cloud provider where data is stored.
82+
region (str) : The region where the data is stored. Relevant to speed of
83+
access and inter-region egress costs (as defined by PaaS provider).
84+
requester_pays (bool) : Is the data requester pays or is it data
85+
manager/cloud provider pays.
86+
tier (str) : The title for the tier type (as defined by PaaS provider).
87+
"""
88+
self.platform = platform
89+
self.region = region
90+
self.requester_pays = requester_pays
91+
self.tier = tier
92+
93+
@property
94+
def platform(self) -> Optional[CloudPlatform]:
95+
"""Get or sets the cloud provider where data is stored.
96+
97+
Returns:
98+
str or None
99+
"""
100+
return self._get_property(PLATFORM_PROP, CloudPlatform)
101+
102+
@platform.setter
103+
def platform(self, v: Optional[CloudPlatform]) -> None:
104+
self._set_property(PLATFORM_PROP, v)
105+
106+
@property
107+
def region(self) -> Optional[str]:
108+
"""Gets or sets the region where the data is stored. Relevant to speed of
109+
access and inter-region egress costs (as defined by PaaS provider)."""
110+
return self._get_property(REGION_PROP, str)
111+
112+
@region.setter
113+
def region(self, v: Optional[str]) -> None:
114+
self._set_property(REGION_PROP, v)
115+
116+
@property
117+
def requester_pays(self) -> Optional[bool]:
118+
# This value "defaults to false", according to the extension spec.
119+
return self._get_property(REQUESTER_PAYS_PROP, bool)
120+
121+
@requester_pays.setter
122+
def requester_pays(self, v: Optional[bool]) -> None:
123+
self._set_property(REQUESTER_PAYS_PROP, v)
124+
125+
@property
126+
def tier(self) -> Optional[str]:
127+
return self._get_property(TIER_PROP, str)
128+
129+
@tier.setter
130+
def tier(self, v: Optional[str]) -> None:
131+
self._set_property(TIER_PROP, v)
132+
133+
@classmethod
134+
def get_schema_uri(cls) -> str:
135+
return SCHEMA_URI
136+
137+
@classmethod
138+
def ext(cls, obj: T, add_if_missing: bool = False) -> "StorageExtension[T]":
139+
"""Extends the given STAC Object with properties from the :stac-ext:`Storage
140+
Extension <storage>`.
141+
142+
This extension can be applied to instances of :class:`~pystac.Item` or
143+
:class:`~pystac.Asset`.
144+
145+
Raises:
146+
147+
pystac.ExtensionTypeError : If an invalid object type is passed.
148+
"""
149+
if isinstance(obj, pystac.Item):
150+
cls.validate_has_extension(obj, add_if_missing)
151+
return cast(StorageExtension[T], ItemStorageExtension(obj))
152+
elif isinstance(obj, pystac.Asset):
153+
cls.validate_owner_has_extension(obj, add_if_missing)
154+
return cast(StorageExtension[T], AssetStorageExtension(obj))
155+
else:
156+
raise pystac.ExtensionTypeError(
157+
f"StorageExtension does not apply to type '{type(obj).__name__}'"
158+
)
159+
160+
@classmethod
161+
def summaries(
162+
cls, obj: pystac.Collection, add_if_missing: bool = False
163+
) -> "SummariesStorageExtension":
164+
"""Returns the extended summaries object for the given collection."""
165+
cls.validate_has_extension(obj, add_if_missing)
166+
return SummariesStorageExtension(obj)
167+
168+
169+
class ItemStorageExtension(StorageExtension[pystac.Item]):
170+
"""A concrete implementation of :class:`StorageExtension` on an :class:`~pystac.Item`
171+
that extends the properties of the Item to include properties defined in the
172+
:stac-ext:`Storage Extension <storage>`.
173+
174+
This class should generally not be instantiated directly. Instead, call
175+
:meth:`StorageExtension.ext` on an :class:`~pystac.Item` to extend it.
176+
"""
177+
178+
item: pystac.Item
179+
"""The :class:`~pystac.Item` being extended."""
180+
181+
properties: Dict[str, Any]
182+
"""The :class:`~pystac.Item` properties, including extension properties."""
183+
184+
def __init__(self, item: pystac.Item):
185+
self.item = item
186+
self.properties = item.properties
187+
188+
def __repr__(self) -> str:
189+
return "<ItemStorageExtension Item id={}>".format(self.item.id)
190+
191+
192+
class AssetStorageExtension(StorageExtension[pystac.Asset]):
193+
"""A concrete implementation of :class:`StorageExtension` on an :class:`~pystac.Asset`
194+
that extends the Asset fields to include properties defined in the
195+
:stac-ext:`Storage Extension <storage>`.
196+
197+
This class should generally not be instantiated directly. Instead, call
198+
:meth:`StorageExtension.ext` on an :class:`~pystac.Asset` to extend it.
199+
"""
200+
201+
asset_href: str
202+
"""The ``href`` value of the :class:`~pystac.Asset` being extended."""
203+
204+
properties: Dict[str, Any]
205+
"""The :class:`~pystac.Asset` fields, including extension properties."""
206+
207+
additional_read_properties: Optional[Iterable[Dict[str, Any]]] = None
208+
"""If present, this will be a list containing 1 dictionary representing the
209+
properties of the owning :class:`~pystac.Item`."""
210+
211+
def __init__(self, asset: pystac.Asset):
212+
self.asset_href = asset.href
213+
self.properties = asset.extra_fields
214+
if asset.owner and isinstance(asset.owner, pystac.Item):
215+
self.additional_read_properties = [asset.owner.properties]
216+
217+
def __repr__(self) -> str:
218+
return "<AssetStorageExtension Asset href={}>".format(self.asset_href)
219+
220+
221+
class SummariesStorageExtension(SummariesExtension):
222+
"""A concrete implementation of :class:`~SummariesExtension` that extends
223+
the ``summaries`` field of a :class:`~pystac.Collection` to include properties
224+
defined in the :stac-ext:`Storage Extension <storage>`.
225+
"""
226+
227+
@property
228+
def platform(self) -> Optional[List[CloudPlatform]]:
229+
"""Get or sets the summary of :attr:`StorageExtension.platform` values
230+
for this Collection.
231+
"""
232+
return self.summaries.get_list(PLATFORM_PROP)
233+
234+
@platform.setter
235+
def platform(self, v: Optional[List[CloudPlatform]]) -> None:
236+
self._set_summary(PLATFORM_PROP, v)
237+
238+
@property
239+
def region(self) -> Optional[List[str]]:
240+
"""Get or sets the summary of :attr:`StorageExtension.region` values
241+
for this Collection.
242+
"""
243+
return self.summaries.get_list(REGION_PROP)
244+
245+
@region.setter
246+
def region(self, v: Optional[List[str]]) -> None:
247+
self._set_summary(REGION_PROP, v)
248+
249+
@property
250+
def requester_pays(self) -> Optional[List[bool]]:
251+
"""Get or sets the summary of :attr:`StorageExtension.requester_pays` values
252+
for this Collection.
253+
"""
254+
return self.summaries.get_list(REQUESTER_PAYS_PROP)
255+
256+
@requester_pays.setter
257+
def requester_pays(self, v: Optional[List[bool]]) -> None:
258+
self._set_summary(REQUESTER_PAYS_PROP, v)
259+
260+
@property
261+
def tier(self) -> Optional[List[str]]:
262+
"""Get or sets the summary of :attr:`StorageExtension.tier` values
263+
for this Collection.
264+
"""
265+
return self.summaries.get_list(TIER_PROP)
266+
267+
@tier.setter
268+
def tier(self, v: Optional[List[str]]) -> None:
269+
self._set_summary(TIER_PROP, v)
270+
271+
272+
class StorageExtensionHooks(ExtensionHooks):
273+
schema_uri: str = SCHEMA_URI
274+
prev_extension_ids: Set[str] = set()
275+
stac_object_types = {
276+
pystac.STACObjectType.COLLECTION,
277+
pystac.STACObjectType.ITEM,
278+
}
279+
280+
281+
STORAGE_EXTENSION_HOOKS: ExtensionHooks = StorageExtensionHooks()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"type": "Collection",
3+
"id": "collection-naip",
4+
"stac_version": "1.0.0",
5+
"description": "Example collection for Storage Extension",
6+
"links": [
7+
{
8+
"rel": "root",
9+
"href": "./collection-naip.json",
10+
"type": "application/json"
11+
},
12+
{
13+
"rel": "item",
14+
"href": "./item-naip.json",
15+
"type": "application/json"
16+
}
17+
],
18+
"stac_extensions": [
19+
"https://stac-extensions.github.io/storage/v1.0.0/schema.json"
20+
],
21+
"summaries": {
22+
"storage:platform": ["AZURE", "GCP", "AWS"],
23+
"storage:region": ["westus2", "us-central1", "us-west-2", "eastus"],
24+
"storage:requester_pays": [true, false],
25+
"storage:tier": ["archive", "COLDLINE", "Standard", "hot"]
26+
},
27+
"extent": {
28+
"spatial": {
29+
"bbox": [
30+
[
31+
-97.75,
32+
30.25,
33+
-97.6875,
34+
30.312499999999996
35+
]
36+
]
37+
},
38+
"temporal": {
39+
"interval": [
40+
[
41+
"2016-09-28T00:00:00Z",
42+
"2016-09-28T00:00:00Z"
43+
]
44+
]
45+
}
46+
},
47+
"license": "proprietary"
48+
}

0 commit comments

Comments
 (0)