Skip to content

Commit 051152e

Browse files
jsignellgadomski
authored andcommitted
feat: support multiple extension uris
1 parent 0f90e7a commit 051152e

File tree

13 files changed

+166
-37
lines changed

13 files changed

+166
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
- Include a copy of the `fields.json` file (for summaries) with each distribution of PySTAC ([#1045](https://github.com/stac-utils/pystac/pull/1045))
1414
- Removed documentation references to `to_dict` methods returning JSON ([#1074](https://github.com/stac-utils/pystac/pull/1074))
15+
- Expand support for previous extension schema URIs ([#1091](https://github.com/stac-utils/pystac/pull/1091))
1516

1617
### Deprecated
1718

pystac/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@
8787
import pystac.extensions.datacube
8888
import pystac.extensions.eo
8989
import pystac.extensions.file
90+
import pystac.extensions.grid
9091
import pystac.extensions.item_assets
9192
import pystac.extensions.label
9293
import pystac.extensions.pointcloud
9394
import pystac.extensions.projection
95+
import pystac.extensions.raster
9496
import pystac.extensions.sar
9597
import pystac.extensions.sat
9698
import pystac.extensions.scientific
@@ -105,10 +107,12 @@
105107
pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS,
106108
pystac.extensions.eo.EO_EXTENSION_HOOKS,
107109
pystac.extensions.file.FILE_EXTENSION_HOOKS,
110+
pystac.extensions.grid.GRID_EXTENSION_HOOKS,
108111
pystac.extensions.item_assets.ITEM_ASSETS_EXTENSION_HOOKS,
109112
pystac.extensions.label.LABEL_EXTENSION_HOOKS,
110113
pystac.extensions.pointcloud.POINTCLOUD_EXTENSION_HOOKS,
111114
pystac.extensions.projection.PROJECTION_EXTENSION_HOOKS,
115+
pystac.extensions.raster.RASTER_EXTENSION_HOOKS,
112116
pystac.extensions.sar.SAR_EXTENSION_HOOKS,
113117
pystac.extensions.sat.SAT_EXTENSION_HOOKS,
114118
pystac.extensions.scientific.SCIENTIFIC_EXTENSION_HOOKS,

pystac/extensions/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ def get_schema_uri(cls) -> str:
112112
"""Gets the schema URI associated with this extension."""
113113
raise NotImplementedError
114114

115+
@classmethod
116+
def get_schema_uris(cls) -> List[str]:
117+
"""Gets a list of schema URIs associated with this extension."""
118+
return [cls.get_schema_uri()]
119+
115120
@classmethod
116121
def add_to(cls, obj: S) -> None:
117122
"""Add the schema URI for this extension to the
@@ -135,9 +140,8 @@ def remove_from(cls, obj: S) -> None:
135140
def has_extension(cls, obj: S) -> bool:
136141
"""Check if the given object implements this extension by checking
137142
:attr:`pystac.STACObject.stac_extensions` for this extension's schema URI."""
138-
return (
139-
obj.stac_extensions is not None
140-
and cls.get_schema_uri() in obj.stac_extensions
143+
return obj.stac_extensions is not None and any(
144+
uri in obj.stac_extensions for uri in cls.get_schema_uris()
141145
)
142146

143147
@classmethod

pystac/extensions/grid.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
from __future__ import annotations
44

55
import re
6-
from typing import Any, Dict, Optional, Pattern, Set, Union
6+
from typing import Any, Dict, List, Optional, Pattern, Set, Union
77

88
import pystac
99
from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension
1010
from pystac.extensions.hooks import ExtensionHooks
1111

1212
SCHEMA_URI: str = "https://stac-extensions.github.io/grid/v1.1.0/schema.json"
13+
SCHEMA_URIS: List[str] = [
14+
"https://stac-extensions.github.io/grid/v1.0.0/schema.json",
15+
SCHEMA_URI,
16+
]
1317
PREFIX: str = "grid:"
1418

1519
# Field names
@@ -80,6 +84,10 @@ def code(self, v: str) -> None:
8084
def get_schema_uri(cls) -> str:
8185
return SCHEMA_URI
8286

87+
@classmethod
88+
def get_schema_uris(cls) -> List[str]:
89+
return SCHEMA_URIS
90+
8391
@classmethod
8492
def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> GridExtension:
8593
"""Extends the given STAC Object with properties from the :stac-ext:`Grid
@@ -102,8 +110,8 @@ def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> GridExtension:
102110

103111
class GridExtensionHooks(ExtensionHooks):
104112
schema_uri: str = SCHEMA_URI
105-
prev_extension_ids: Set[str] = set()
113+
prev_extension_ids: Set[str] = {*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI]}
106114
stac_object_types = {pystac.STACObjectType.ITEM}
107115

108116

109-
Grid_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks()
117+
GRID_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks()

pystac/extensions/label.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from pystac.utils import StringEnum, get_required, map_opt
1212

1313
SCHEMA_URI = "https://stac-extensions.github.io/label/v1.0.1/schema.json"
14-
14+
SCHEMA_URIS = [
15+
"https://stac-extensions.github.io/label/v1.0.0/schema.json",
16+
SCHEMA_URI,
17+
]
1518
PREFIX = "label:"
1619

1720
PROPERTIES_PROP = PREFIX + "properties"
@@ -691,6 +694,10 @@ def add_geojson_labels(
691694
def get_schema_uri(cls) -> str:
692695
return SCHEMA_URI
693696

697+
@classmethod
698+
def get_schema_uris(cls) -> List[str]:
699+
return SCHEMA_URIS
700+
694701
@classmethod
695702
def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> LabelExtension:
696703
"""Extends the given STAC Object with properties from the :stac-ext:`Label
@@ -791,7 +798,7 @@ class LabelExtensionHooks(ExtensionHooks):
791798
schema_uri: str = SCHEMA_URI
792799
prev_extension_ids = {
793800
"label",
794-
"https://stac-extensions.github.io/label/v1.0.0/schema.json",
801+
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
795802
}
796803
stac_object_types = {pystac.STACObjectType.ITEM}
797804

pystac/extensions/projection.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ def transform(self, v: Optional[List[float]]) -> None:
263263
def get_schema_uri(cls) -> str:
264264
return SCHEMA_URI
265265

266+
@classmethod
267+
def get_schema_uris(cls) -> List[str]:
268+
return SCHEMA_URIS
269+
266270
@classmethod
267271
def ext(cls, obj: T, add_if_missing: bool = False) -> ProjectionExtension[T]:
268272
"""Extends the given STAC Object with properties from the :stac-ext:`Projection
@@ -294,15 +298,6 @@ def summaries(
294298
cls.validate_has_extension(obj, add_if_missing)
295299
return SummariesProjectionExtension(obj)
296300

297-
@classmethod
298-
def has_extension(cls, obj: Union[pystac.Item, pystac.Collection]) -> bool:
299-
if isinstance(obj, pystac.Item) or isinstance(obj, pystac.Collection):
300-
return obj.stac_extensions is not None and any(
301-
uri in obj.stac_extensions for uri in SCHEMA_URIS
302-
)
303-
else:
304-
return False
305-
306301

307302
class ItemProjectionExtension(ProjectionExtension[pystac.Item]):
308303
"""A concrete implementation of :class:`ProjectionExtension` on an
@@ -376,7 +371,11 @@ def epsg(self, v: Optional[List[int]]) -> None:
376371

377372
class ProjectionExtensionHooks(ExtensionHooks):
378373
schema_uri: str = SCHEMA_URI
379-
prev_extension_ids = {"proj", "projection"}
374+
prev_extension_ids = {
375+
"proj",
376+
"projection",
377+
*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
378+
}
380379
stac_object_types = {pystac.STACObjectType.ITEM}
381380

382381

pystac/extensions/raster.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, Dict, Iterable, List, Optional, Union
5+
from typing import Any, Dict, Iterable, List, Optional, Set, Union
66

77
import pystac
88
from pystac.extensions.base import (
99
ExtensionManagementMixin,
1010
PropertiesExtension,
1111
SummariesExtension,
1212
)
13+
from pystac.extensions.hooks import ExtensionHooks
1314
from pystac.utils import StringEnum, get_opt, get_required, map_opt
1415

1516
SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.1.0/schema.json"
16-
17+
SCHEMA_URIS = [
18+
"https://stac-extensions.github.io/raster/v1.0.0/schema.json",
19+
SCHEMA_URI,
20+
]
1721
BANDS_PROP = "raster:bands"
1822

1923

@@ -706,6 +710,10 @@ def _get_bands(self) -> Optional[List[RasterBand]]:
706710
def get_schema_uri(cls) -> str:
707711
return SCHEMA_URI
708712

713+
@classmethod
714+
def get_schema_uris(cls) -> List[str]:
715+
return SCHEMA_URIS
716+
709717
@classmethod
710718
def ext(cls, obj: pystac.Asset, add_if_missing: bool = False) -> RasterExtension:
711719
"""Extends the given STAC Object with properties from the :stac-ext:`Raster
@@ -752,3 +760,12 @@ def bands(self) -> Optional[List[RasterBand]]:
752760
@bands.setter
753761
def bands(self, v: Optional[List[RasterBand]]) -> None:
754762
self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v))
763+
764+
765+
class RasterExtensionHooks(ExtensionHooks):
766+
schema_uri: str = SCHEMA_URI
767+
prev_extension_ids: Set[str] = {*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI]}
768+
stac_object_types = {pystac.STACObjectType.ITEM, pystac.STACObjectType.COLLECTION}
769+
770+
771+
RASTER_EXTENSION_HOOKS: ExtensionHooks = RasterExtensionHooks()

pystac/serialization/migrate.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,20 +188,19 @@ def migrate_to_latest(
188188
# Force stac_extensions property, as it makes
189189
# downstream migration less complex
190190
result["stac_extensions"] = []
191-
pystac.EXTENSION_HOOKS.migrate(result, version, info)
192-
193-
for ext in result["stac_extensions"][:]:
194-
if ext in removed_extension_migrations:
195-
object_types, migration_fn = removed_extension_migrations[ext]
196-
if object_types is None or info.object_type in object_types:
197-
if migration_fn:
198-
migration_fn(result, version, info)
199-
result["stac_extensions"].remove(ext)
200-
201191
result["stac_version"] = STACVersion.DEFAULT_STAC_VERSION
202192
else:
203193
# Ensure stac_extensions property for consistency
204194
if "stac_extensions" not in result:
205195
result["stac_extensions"] = []
206196

197+
pystac.EXTENSION_HOOKS.migrate(result, version, info)
198+
for ext in result["stac_extensions"][:]:
199+
if ext in removed_extension_migrations:
200+
object_types, migration_fn = removed_extension_migrations[ext]
201+
if object_types is None or info.object_type in object_types:
202+
if migration_fn:
203+
migration_fn(result, version, info)
204+
result["stac_extensions"].remove(ext)
205+
207206
return result

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# TODO move all test case code to this file
22

3+
from pathlib import Path
34
from datetime import datetime
45

56
import pytest
@@ -9,6 +10,9 @@
910
from .utils import ARBITRARY_BBOX, ARBITRARY_EXTENT, ARBITRARY_GEOM, TestCases
1011

1112

13+
here = Path(__file__).resolve().parent
14+
15+
1216
@pytest.fixture
1317
def catalog() -> Catalog:
1418
return Catalog("test-catalog", "A test catalog")
@@ -38,3 +42,7 @@ def test_case_8_collection() -> Collection:
3842
def projection_landsat8_item() -> Item:
3943
path = TestCases.get_path("data-files/projection/example-landsat8.json")
4044
return Item.from_file(path)
45+
46+
47+
def get_data_file(rel_path: str) -> str:
48+
return str(here / "data-files" / rel_path)

tests/extensions/test_grid.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
from datetime import datetime
77
from typing import Any, Dict
88

9+
import pytest
910
import pystac
1011
from pystac import ExtensionTypeError
1112
from pystac.extensions import grid
1213
from pystac.extensions.grid import GridExtension
1314
from tests.utils import TestCases
15+
from tests.conftest import get_data_file
1416

1517
code = "MGRS-4CFJ"
1618

@@ -137,3 +139,27 @@ def test_should_raise_exception_when_passing_invalid_extension_object(
137139
GridExtension.ext,
138140
object(),
139141
)
142+
143+
144+
@pytest.fixture
145+
def ext_item() -> pystac.Item:
146+
ext_item_uri = get_data_file("grid/example-sentinel2.json")
147+
return pystac.Item.from_file(ext_item_uri)
148+
149+
150+
def test_older_extension_version(ext_item: pystac.Item) -> None:
151+
old = "https://stac-extensions.github.io/grid/v1.0.0/schema.json"
152+
new = "https://stac-extensions.github.io/grid/v1.1.0/schema.json"
153+
154+
stac_extensions = set(ext_item.stac_extensions)
155+
stac_extensions.remove(new)
156+
stac_extensions.add(old)
157+
item_as_dict = ext_item.to_dict(include_self_link=False, transform_hrefs=False)
158+
item_as_dict["stac_extensions"] = list(stac_extensions)
159+
item = pystac.Item.from_dict(item_as_dict)
160+
assert GridExtension.has_extension(item)
161+
assert old in item.stac_extensions
162+
163+
migrated_item = pystac.Item.from_dict(item_as_dict, migrate=True)
164+
assert GridExtension.has_extension(migrated_item)
165+
assert new in migrated_item.stac_extensions

tests/extensions/test_label.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
)
2121
from pystac.utils import get_opt
2222
from tests.utils import TestCases, assert_to_from_dict
23+
from tests.conftest import get_data_file
24+
import pytest
2325

2426

2527
class LabelTypeTest(unittest.TestCase):
@@ -576,3 +578,27 @@ def test_should_raise_exception_when_passing_invalid_extension_object(
576578
LabelExtension.ext,
577579
object(),
578580
)
581+
582+
583+
@pytest.fixture
584+
def ext_item() -> pystac.Item:
585+
ext_item_uri = get_data_file("label/label-example-1.json")
586+
return pystac.Item.from_file(ext_item_uri)
587+
588+
589+
def test_older_extension_version(ext_item: pystac.Item) -> None:
590+
old = "https://stac-extensions.github.io/label/v1.0.0/schema.json"
591+
new = "https://stac-extensions.github.io/label/v1.0.1/schema.json"
592+
593+
stac_extensions = set(ext_item.stac_extensions)
594+
stac_extensions.remove(new)
595+
stac_extensions.add(old)
596+
item_as_dict = ext_item.to_dict(include_self_link=False, transform_hrefs=False)
597+
item_as_dict["stac_extensions"] = list(stac_extensions)
598+
item = pystac.Item.from_dict(item_as_dict)
599+
assert LabelExtension.has_extension(item)
600+
assert old in item.stac_extensions
601+
602+
migrated_item = pystac.Item.from_dict(item_as_dict, migrate=True)
603+
assert LabelExtension.has_extension(migrated_item)
604+
assert new in migrated_item.stac_extensions

tests/extensions/test_projection.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -539,16 +539,20 @@ def test_summaries_adds_uri(self) -> None:
539539

540540

541541
def test_older_extension_version(projection_landsat8_item: Item) -> None:
542+
old = "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
543+
new = "https://stac-extensions.github.io/projection/v1.1.0/schema.json"
544+
542545
stac_extensions = set(projection_landsat8_item.stac_extensions)
543-
stac_extensions.remove(
544-
"https://stac-extensions.github.io/projection/v1.1.0/schema.json"
545-
)
546-
stac_extensions.add(
547-
"https://stac-extensions.github.io/projection/v1.0.0/schema.json"
548-
)
546+
stac_extensions.remove(new)
547+
stac_extensions.add(old)
549548
item_as_dict = projection_landsat8_item.to_dict(
550549
include_self_link=False, transform_hrefs=False
551550
)
552-
item_as_dict["stac_extensions"] = stac_extensions
551+
item_as_dict["stac_extensions"] = list(stac_extensions)
553552
item = Item.from_dict(item_as_dict)
554553
assert ProjectionExtension.has_extension(item)
554+
assert old in item.stac_extensions
555+
556+
migrated_item = pystac.Item.from_dict(item_as_dict, migrate=True)
557+
assert ProjectionExtension.has_extension(migrated_item)
558+
assert new in migrated_item.stac_extensions

0 commit comments

Comments
 (0)