Skip to content

Commit

Permalink
feat(v2): asset hrefs (#1526)
Browse files Browse the repository at this point in the history
* feat(v2): asset hrefs

* fix: typing
  • Loading branch information
gadomski authored Feb 14, 2025
1 parent a039414 commit 3ce03ed
Show file tree
Hide file tree
Showing 26 changed files with 1,774 additions and 351 deletions.
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

0 comments on commit 3ce03ed

Please sign in to comment.