Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 4 additions & 50 deletions core/pystac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"set_stac_version",
]

import warnings
from typing import Any

from pystac.errors import (
Expand Down Expand Up @@ -91,56 +90,11 @@
from pystac.utils import HREF

import pystac.extensions.hooks
import pystac.extensions.classification
import pystac.extensions.datacube
import pystac.extensions.eo
import pystac.extensions.file
import pystac.extensions.grid
import pystac.extensions.item_assets

with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
import pystac.extensions.label
import pystac.extensions.mgrs
import pystac.extensions.mlm
import pystac.extensions.pointcloud
import pystac.extensions.projection
import pystac.extensions.raster
import pystac.extensions.sar
import pystac.extensions.sat
import pystac.extensions.scientific
import pystac.extensions.storage
import pystac.extensions.table
import pystac.extensions.timestamps
import pystac.extensions.version
import pystac.extensions.view
import pystac.extensions.xarray_assets

EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks(
[
pystac.extensions.classification.CLASSIFICATION_EXTENSION_HOOKS,
pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS,
pystac.extensions.eo.EO_EXTENSION_HOOKS,
pystac.extensions.file.FILE_EXTENSION_HOOKS,
pystac.extensions.grid.GRID_EXTENSION_HOOKS,
pystac.extensions.item_assets.ITEM_ASSETS_EXTENSION_HOOKS,
pystac.extensions.label.LABEL_EXTENSION_HOOKS,
pystac.extensions.mgrs.MGRS_EXTENSION_HOOKS,
pystac.extensions.mlm.MLM_EXTENSION_HOOKS,
pystac.extensions.pointcloud.POINTCLOUD_EXTENSION_HOOKS,
pystac.extensions.projection.PROJECTION_EXTENSION_HOOKS,
pystac.extensions.raster.RASTER_EXTENSION_HOOKS,
pystac.extensions.sar.SAR_EXTENSION_HOOKS,
pystac.extensions.sat.SAT_EXTENSION_HOOKS,
pystac.extensions.scientific.SCIENTIFIC_EXTENSION_HOOKS,
pystac.extensions.storage.STORAGE_EXTENSION_HOOKS,
pystac.extensions.table.TABLE_EXTENSION_HOOKS,
pystac.extensions.timestamps.TIMESTAMPS_EXTENSION_HOOKS,
pystac.extensions.version.VERSION_EXTENSION_HOOKS,
pystac.extensions.view.VIEW_EXTENSION_HOOKS,
pystac.extensions.xarray_assets.XARRAY_ASSETS_EXTENSION_HOOKS,
]
)
#: Extension migration/link hooks, discovered lazily from installed extension
#: packages via the ``pystac.extensions`` entry point group. ``pystac-core``
#: intentionally has no compile-time knowledge of any extension.
EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks()


def read_file(href: HREF, stac_io: StacIO | None = None) -> STACObject:
Expand Down
7 changes: 6 additions & 1 deletion core/pystac/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,12 @@ def ext(self) -> AssetExt:

asset.ext.proj.code = "EPSG:4326"
"""
from pystac.extensions.ext import AssetExt
try:
from pystac.extensions.ext import AssetExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return AssetExt(stac_object=self)

Expand Down
7 changes: 6 additions & 1 deletion core/pystac/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,11 @@ def ext(self) -> CatalogExt:

print(collection.ext.version)
"""
from pystac.extensions.ext import CatalogExt
try:
from pystac.extensions.ext import CatalogExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return CatalogExt(stac_object=self)
24 changes: 10 additions & 14 deletions core/pystac/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pystac import CatalogType, STACObjectType
from pystac.asset import Asset, Assets
from pystac.catalog import Catalog
from pystac.errors import DeprecatedWarning, ExtensionNotImplemented, STACTypeError
from pystac.errors import DeprecatedWarning, STACTypeError
from pystac.item_assets import ItemAssetDefinition, _ItemAssets
from pystac.layout import HrefLayoutStrategy
from pystac.link import Link
Expand Down Expand Up @@ -638,8 +638,6 @@ def from_dict(
) -> C:
import warnings

from pystac.extensions.version import CollectionVersionExtension

if migrate:
info = identify_stac_object(d)
d = migrate_to_latest(d, info)
Expand Down Expand Up @@ -710,16 +708,9 @@ def from_dict(
if root:
collection.set_root(root)

try:
version = CollectionVersionExtension.ext(collection)
if version.deprecated:
warnings.warn(
f"The collection '{collection.id}' is deprecated.",
DeprecatedWarning,
)
# Collection asset deprecation checks pending version extension support
except ExtensionNotImplemented:
pass
message = pystac.EXTENSION_HOOKS.get_deprecation_message(collection)
if message is not None:
warnings.warn(message, DeprecatedWarning)

return collection

Expand Down Expand Up @@ -894,6 +885,11 @@ def ext(self) -> CollectionExt:

print(collection.ext.xarray)
"""
from pystac.extensions.ext import CollectionExt
try:
from pystac.extensions.ext import CollectionExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return CollectionExt(stac_object=self)
18 changes: 17 additions & 1 deletion core/pystac/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
from typing import Any
from typing import Any, NoReturn


def _raise_for_missing_ext(error: ModuleNotFoundError) -> NoReturn:
"""Re-raise a friendlier error when the ``.ext`` accessor is unavailable.

The `.ext` accessor lives in `pystac.extensions.ext`, which is distributed
with the `pystac` package rather than `pystac-core`. If that specific module
is missing, point the user at `pystac`; otherwise propagate the original
error (a genuinely broken extension install).
"""
if error.name == "pystac.extensions.ext":
raise ImportError(
"The `.ext` accessor is provided by the `pystac` package, not "
"`pystac-core`. Install `pystac` to use `.ext`."
) from error
raise error


class TemplateError(Exception):
Expand Down
39 changes: 38 additions & 1 deletion core/pystac/extensions/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ def _get_stac_object_types(self) -> set[str]:
def get_object_links(self, obj: STACObject) -> list[str | pystac.RelType] | None:
return None

def get_deprecation_message(self, obj: STACObject) -> str | None:
"""Return a warning message if `obj` is deprecated per this extension.

Emitted as a `pystac.DeprecatedWarning` when loading an object from a
dict. The base implementation returns None (not deprecated).
"""
return None

def has_extension(self, obj: dict[str, Any]) -> bool:
schema_startswith = VERSION_REGEX.split(self.schema_uri)[0] + "/"
return any(
Expand Down Expand Up @@ -75,8 +83,27 @@ def migrate(
class RegisteredExtensionHooks:
hooks: dict[str, ExtensionHooks]

def __init__(self, hooks: Iterable[ExtensionHooks]):
def __init__(self, hooks: Iterable[ExtensionHooks] = ()):
self.hooks = {e.schema_uri: e for e in hooks}
self._discovered = False

def _discover(self) -> None:
"""Register hooks advertised via the ``pystac.extensions`` entry point group.

Installed extensions self-advertise their `ExtensionHooks`, so
`pystac-core` needs no compile-time knowledge of them. Runs once.
"""
if self._discovered:
return
self._discovered = True
import warnings
from importlib.metadata import entry_points

with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
for entry_point in entry_points(group="pystac.extensions"):
hooks = entry_point.load()
self.hooks.setdefault(hooks.schema_uri, hooks)

def add_extension_hooks(self, hooks: ExtensionHooks) -> None:
e_id = hooks.schema_uri
Expand All @@ -92,6 +119,7 @@ def remove_extension_hooks(self, extension_id: str) -> None:
del self.hooks[extension_id]

def get_extended_object_links(self, obj: STACObject) -> list[str | pystac.RelType]:
self._discover()
result: list[str | pystac.RelType] | None = None
for ext in obj.stac_extensions:
if ext in self.hooks:
Expand All @@ -106,6 +134,15 @@ def get_extended_object_links(self, obj: STACObject) -> list[str | pystac.RelTyp
def migrate(
self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
) -> None:
self._discover()
for hooks in self.hooks.values():
if info.object_type in hooks._get_stac_object_types():
hooks.migrate(obj, version, info)

def get_deprecation_message(self, obj: STACObject) -> str | None:
self._discover()
for hooks in self.hooks.values():
message = hooks.get_deprecation_message(obj)
if message is not None:
return message
return None
24 changes: 10 additions & 14 deletions core/pystac/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pystac.asset import Asset, Assets
from pystac.catalog import Catalog
from pystac.collection import Collection
from pystac.errors import DeprecatedWarning, ExtensionNotImplemented
from pystac.errors import DeprecatedWarning
from pystac.link import Link
from pystac.serialization import (
identify_stac_object,
Expand Down Expand Up @@ -426,8 +426,6 @@ def from_dict(
) -> T:
import warnings

from pystac.extensions.version import ItemVersionExtension

if preserve_dict:
d = deepcopy(d)

Expand Down Expand Up @@ -482,16 +480,9 @@ def from_dict(
if root:
item.set_root(root)

try:
version = ItemVersionExtension.ext(item)
if version.deprecated:
warnings.warn(
f"The item '{item.id}' is deprecated.",
DeprecatedWarning,
)
# Item asset deprecation checks pending version extension support
except ExtensionNotImplemented:
pass
message = pystac.EXTENSION_HOOKS.get_deprecation_message(item)
if message is not None:
warnings.warn(message, DeprecatedWarning)

return item

Expand Down Expand Up @@ -534,6 +525,11 @@ def ext(self) -> ItemExt:

item.ext.proj.code = "EPSG:4326"
"""
from pystac.extensions.ext import ItemExt
try:
from pystac.extensions.ext import ItemExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return ItemExt(stac_object=self)
7 changes: 6 additions & 1 deletion core/pystac/item_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ def ext(self) -> ItemAssetExt:

collection.item_assets["data"].ext.proj.epsg = 4326
"""
from pystac.extensions.ext import ItemAssetExt
try:
from pystac.extensions.ext import ItemAssetExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return ItemAssetExt(stac_object=self)

Expand Down
7 changes: 6 additions & 1 deletion core/pystac/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,11 @@ def ext(self) -> LinkExt:

link.ext.file.size = 8675309
"""
from pystac.extensions.ext import LinkExt
try:
from pystac.extensions.ext import LinkExt
except ModuleNotFoundError as e:
from pystac.errors import _raise_for_missing_ext

_raise_for_missing_ext(e)

return LinkExt(stac_object=self)
8 changes: 7 additions & 1 deletion extensions/classification/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.10"
dependencies = ["pystac-core"]
dependencies = [
"pystac-core",
"pystac-ext-raster",
]

[project.entry-points."pystac.extensions"]
classification = "pystac.extensions.classification:CLASSIFICATION_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Expand Down
3 changes: 3 additions & 0 deletions extensions/datacube/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = ["pystac-core"]

[project.entry-points."pystac.extensions"]
datacube = "pystac.extensions.datacube:DATACUBE_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Repository = "https://github.com/stac-utils/pystac"
Expand Down
9 changes: 8 additions & 1 deletion extensions/eo/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.10"
dependencies = ["pystac-core"]
dependencies = [
"pystac-core",
"pystac-ext-projection",
"pystac-ext-view",
]

[project.entry-points."pystac.extensions"]
eo = "pystac.extensions.eo:EO_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Expand Down
3 changes: 3 additions & 0 deletions extensions/file/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = ["pystac-core"]

[project.entry-points."pystac.extensions"]
file = "pystac.extensions.file:FILE_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Repository = "https://github.com/stac-utils/pystac"
Expand Down
3 changes: 3 additions & 0 deletions extensions/grid/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = ["pystac-core"]

[project.entry-points."pystac.extensions"]
grid = "pystac.extensions.grid:GRID_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Repository = "https://github.com/stac-utils/pystac"
Expand Down
3 changes: 3 additions & 0 deletions extensions/item_assets/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = ["pystac-core"]

[project.entry-points."pystac.extensions"]
item_assets = "pystac.extensions.item_assets:ITEM_ASSETS_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Repository = "https://github.com/stac-utils/pystac"
Expand Down
3 changes: 3 additions & 0 deletions extensions/label/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
requires-python = ">=3.10"
dependencies = ["pystac-core"]

[project.entry-points."pystac.extensions"]
label = "pystac.extensions.label:LABEL_EXTENSION_HOOKS"

[project.urls]
Documentation = "https://pystac.readthedocs.io"
Repository = "https://github.com/stac-utils/pystac"
Expand Down
Loading
Loading