Skip to content

feat(v2): asset hrefs #1526

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 14, 2025
Merged
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
27 changes: 13 additions & 14 deletions src/pystac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
)
from .extent import Extent, SpatialExtent, TemporalExtent
from .functions import get_stac_version, read_dict, set_stac_version
from .io import DefaultReader, DefaultWriter, read_file, write_file
from .io import read_file, write_file
from .item import Item
from .link import Link
from .render import DefaultRenderer, Renderer
from .media_type import MediaType
from .stac_object import STACObject


Expand All @@ -26,32 +26,31 @@ def __getattr__(name: str) -> Any:
from .stac_io import StacIO

return StacIO
else:
raise AttributeError(name)


__all__ = [
"Asset",
"ItemAsset",
"Catalog",
"Collection",
"Container",
"DEFAULT_STAC_VERSION",
"DefaultReader",
"DefaultRenderer",
"DefaultWriter",
"Extent",
"Item",
"ItemAsset",
"Link",
"Container",
"PystacError",
"PystacWarning",
"Renderer",
"STACObject",
"SpatialExtent",
"StacError",
"StacWarning",
"Extent",
"SpatialExtent",
"TemporalExtent",
"get_stac_version",
"read_dict",
"read_file",
"set_stac_version",
"read_file",
"write_file",
"Item",
"Link",
"MediaType",
"STACObject",
]
10 changes: 4 additions & 6 deletions src/pystac/asset.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import copy
from typing import Any
from typing import Any, Protocol, runtime_checkable

from typing_extensions import Self

Expand Down Expand Up @@ -77,10 +77,8 @@ def to_dict(self) -> dict[str, Any]:
return d


class AssetsMixin:
"""A mixin for things that have assets (Collections and Items)"""
@runtime_checkable
class Assets(Protocol):
"""A protocol for things that have assets (Collections and Items)"""

assets: dict[str, Asset]

def add_asset(self, key: str, asset: Asset) -> None:
raise NotImplementedError
10 changes: 5 additions & 5 deletions src/pystac/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
ITEM_TYPE = "Feature"
"""The type field of a JSON STAC Item."""

CHILD_REL = "child"
CHILD = "child"
"""The child relation type, for links."""
ITEM_REL = "item"
ITEM = "item"
"""The item relation type, for links."""
PARENT_REL = "parent"
PARENT = "parent"
"""The parent relation type, for links."""
ROOT_REL = "root"
ROOT = "root"
"""The root relation type, for links."""
SELF_REL = "self"
SELF = "self"
"""The self relation type, for links."""
56 changes: 22 additions & 34 deletions src/pystac/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator

from . import io
from .decorators import v2_deprecated
from . import deprecate
from .constants import CHILD, ITEM
from .io import Write
from .item import Item
from .link import Link
Expand Down Expand Up @@ -50,9 +50,7 @@ def walk(self) -> Iterator[tuple[Container, list[Container], list[Item]]]:
"""
children: list[Container] = []
items: list[Item] = []
for link in filter(
lambda link: link.is_child() or link.is_item(), self.iter_links()
):
for link in self.iter_links(CHILD, ITEM):
stac_object = link.get_stac_object()
if isinstance(stac_object, Container):
children.append(stac_object)
Expand Down Expand Up @@ -125,43 +123,33 @@ def get_collections(self, recursive: bool = False) -> Iterator[Collection]:
if recursive and isinstance(stac_object, Container):
yield from stac_object.get_collections(recursive=recursive)

def render(
def save(
self,
root: str | Path,
catalog_type: Any = None,
dest_href: str | Path | None = None,
stac_io: Any = None,
*,
writer: Write | None = None,
) -> None:
"""Renders this container and all of its children and items.

See the [pystac.render][] documentation for more.

Args:
root: The directory at the root of the rendered filesystem tree.
"""
# TODO allow renderer customization
from .render import DefaultRenderer

renderer = DefaultRenderer(str(root))
renderer.render(self)

def save(self, writer: Write | None = None) -> None:
"""Saves this container and all of its children.

This will error if any objects don't have an `href` set. Use
[Container.render][pystac.Container.render] to set those `href` values.

Args:
writer: The writer that will be used for the save operation. If not
provided, this container's writer will be used.
"""
if catalog_type:
deprecate.argument("catalog_type")
if dest_href:
deprecate.argument("dest_href")
if stac_io:
deprecate.argument("stac_io")
if writer is None:
writer = self.writer
io.write_file(self, writer=writer)

self.save_object(stac_io=stac_io, writer=writer)
for stac_object in self.get_children_and_items():
if isinstance(stac_object, Container):
stac_object.save(writer)
stac_object.save(
writer=writer
) # TODO do we need to pass through any of the deprecated arguments?
else:
io.write_file(stac_object, writer=writer)
stac_object.save_object(writer=writer)

@v2_deprecated("Use render() and then save()")
@deprecate.function("Use render() and then save()")
def normalize_and_save(
self,
root_href: str,
Expand Down
19 changes: 18 additions & 1 deletion src/pystac/decorators.py → src/pystac/deprecate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,24 @@
from typing import Any, Callable


def v2_deprecated(message: str) -> Callable[..., Any]:
def argument(name: str) -> None:
warnings.warn(
f"Argument {name} is deprecated in PySTAC v2.0 and will be removed in a future "
"version.",
FutureWarning,
)


def module(name: str) -> None:
warnings.warn(
f"Module pystac.{name} is deprecated in PySTAC v2.0 "
"and will be removed in a future "
"version.",
FutureWarning,
)


def function(message: str) -> Callable[..., Any]:
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
@wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
Expand Down
4 changes: 2 additions & 2 deletions src/pystac/extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

from typing_extensions import Self

from . import deprecate
from .constants import DEFAULT_BBOX, DEFAULT_INTERVAL
from .decorators import v2_deprecated
from .errors import StacWarning
from .types import PermissiveBbox, PermissiveInterval

Expand Down Expand Up @@ -57,7 +57,7 @@ def from_dict(cls: type[Self], d: dict[str, Any]) -> Self:
return cls(**d)

@classmethod
@v2_deprecated("Use the constructor instead")
@deprecate.function("Use the constructor instead")
def from_coordinates(
cls: type[Self],
coordinates: list[Any],
Expand Down
8 changes: 4 additions & 4 deletions src/pystac/functions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Any

from . import deprecate
from .constants import DEFAULT_STAC_VERSION
from .decorators import v2_deprecated
from .stac_object import STACObject


@v2_deprecated("Use DEFAULT_STAC_VERSION instead.")
@deprecate.function("Use DEFAULT_STAC_VERSION instead.")
def get_stac_version() -> str:
"""**DEPRECATED** Returns the default STAC version.

Expand All @@ -24,7 +24,7 @@ def get_stac_version() -> str:
return DEFAULT_STAC_VERSION


@v2_deprecated(
@deprecate.function(
"This function is a no-op. Use `Container.set_stac_version()` to modify the STAC "
"version of an entire catalog."
)
Expand All @@ -38,7 +38,7 @@ def set_stac_version(version: str) -> None:
"""


@v2_deprecated("Use STACObject.from_dict instead")
@deprecate.function("Use STACObject.from_dict instead")
def read_dict(d: dict[str, Any]) -> STACObject:
"""**DEPRECATED** Reads a STAC object from a dictionary.

Expand Down
72 changes: 35 additions & 37 deletions src/pystac/io.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Input and output.

In PySTAC v2.0, reading and writing STAC objects has been split into separate
protocols, [Read][pystac.io.Read] and [Write][pystac.io.Write] classes. This
should be a transparent operation for most users:
protocols, [Read][pystac.io.Read] and [Write][pystac.io.Write]. This should be
transparent for most users:

```python
catalog = pystac.read_file("catalog.json")
Expand All @@ -25,71 +25,69 @@
from pathlib import Path
from typing import Any, Protocol

from . import deprecate
from .errors import PystacError
from .stac_object import STACObject


def read_file(href: str | Path, reader: Read | None = None) -> STACObject:
def read_file(
href: str | Path,
stac_io: Any = None,
*,
reader: Read | None = None,
) -> STACObject:
"""Reads a file from a href.

Uses the default [Reader][pystac.DefaultReader].

Args:
href: The href to read
reader: The [Read][pystac.Read] to use for reading

Returns:
The STAC object

Examples:
>>> item = pystac.read_file("item.json")
"""
if stac_io:
deprecate.argument("stac_io")
return STACObject.from_file(href, reader=reader)


def write_file(
stac_object: STACObject,
obj: STACObject,
include_self_link: bool | None = None,
dest_href: str | Path | None = None,
stac_io: Any = None,
*,
href: str | Path | None = None,
writer: Write | None = None,
) -> None:
"""Writes a STAC object to a file, using its href.

If the href is not set, this will throw and error.

Args:
stac_object: The STAC object to write
obj: The STAC object to write
dest_href: The href to write the STAC object to
writer: The [Write][pystac.Write] to use for writing
"""
if include_self_link is not None:
deprecate.argument("include_self_link")
if stac_io:
deprecate.argument("stac_io")

if writer is None:
writer = DefaultWriter()
if href is None:
href = stac_object.href
if href is None:
raise PystacError(f"cannot write {stac_object} without an href")
data = stac_object.to_dict()
if isinstance(href, Path):
writer.write_json_to_path(data, href)

if dest_href is None:
dest_href = obj.href
if dest_href is None:
raise PystacError(f"cannot write {obj} without an href")
d = obj.to_dict()
if isinstance(dest_href, Path):
writer.write_json_to_path(d, dest_href)
else:
url = urllib.parse.urlparse(href)
url = urllib.parse.urlparse(dest_href)
if url.scheme:
writer.write_json_to_url(data, href)
writer.write_json_to_url(d, dest_href)
else:
writer.write_json_to_path(data, Path(href))


def make_absolute_href(href: str, base: str | None) -> str:
if urllib.parse.urlparse(href).scheme:
return href # TODO file:// schemes

if base:
if urllib.parse.urlparse(base).scheme:
raise NotImplementedError("url joins not implemented yet, should be easy")
else:
if base.endswith("/"): # TODO windoze
return str((Path(base) / href).resolve(strict=False))
else:
return str((Path(base).parent / href).resolve(strict=False))
else:
raise NotImplementedError
writer.write_json_to_path(d, Path(dest_href))


class Read(Protocol):
Expand Down
Loading