Skip to content

Commit

Permalink
feat(v2): first v1 test (#1523)
Browse files Browse the repository at this point in the history
* feat: first v1 test

* ci: run CI on v2 pull requests

* feat: add assets mixin

* fix: only try to deploy if we're on v2 branch
  • Loading branch information
gadomski authored Feb 13, 2025
1 parent b9abe9e commit c87ecb9
Show file tree
Hide file tree
Showing 14 changed files with 466 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
branches:
- v2
pull_request:
branches:
- v2

jobs:
test:
Expand Down Expand Up @@ -52,6 +55,7 @@ jobs:
path: site/
deploy-docs:
name: Deploy docs
if: github.ref == 'refs/heads/v2'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
requires-python = ">=3.10"
dependencies = ["typing-extensions>=4.12.2"]
dependencies = ["python-dateutil>=2.9.0.post0", "typing-extensions>=4.12.2"]

[project.optional-dependencies]
validate = ["jsonschema>=4.23.0", "referencing>=0.36.2"]
Expand All @@ -35,6 +35,7 @@ dev = [
"pytest>=8.3.4",
"ruff>=0.9.6",
"types-jsonschema>=4.23.0.20241208",
"types-python-dateutil>=2.9.0.20241206",
]
bench = ["asv>=0.6.4"]
docs = ["mike>=2.1.3", "mkdocs-material>=9.6.3", "mkdocstrings-python>=1.14.6"]
Expand Down
9 changes: 9 additions & 0 deletions src/pystac/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,12 @@ def to_dict(self) -> dict[str, Any]:
d = {"href": self.href}
d.update(super().to_dict())
return d


class AssetsMixin:
"""A mixin for things that have assets (Collections and Items)"""

assets: dict[str, Asset]

def add_asset(self, key: str, asset: Asset) -> None:
raise NotImplementedError
26 changes: 24 additions & 2 deletions src/pystac/extent.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

import copy
import datetime
import warnings
from typing import Any, Sequence

from typing_extensions import Self

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 @@ -54,7 +56,19 @@ def from_dict(cls: type[Self], d: dict[str, Any]) -> Self:
"""Creates a new spatial extent from a dictionary."""
return cls(**d)

def __init__(self, bbox: PermissiveBbox | None = None):
@classmethod
@v2_deprecated("Use the constructor instead")
def from_coordinates(
cls: type[Self],
coordinates: list[Any],
extra_fields: dict[str, Any] | None = None,
) -> Self:
if extra_fields:
return cls(coordinates, **extra_fields)
else:
return cls(coordinates)

def __init__(self, bbox: PermissiveBbox | None = None, **kwargs: Any):
"""Creates a new spatial extent."""
self.bbox: Sequence[Sequence[float | int]]
if bbox is None or len(bbox) == 0:
Expand All @@ -63,10 +77,13 @@ def __init__(self, bbox: PermissiveBbox | None = None):
self.bbox = bbox # type: ignore
else:
self.bbox = [bbox] # type: ignore
self.extra_fields = kwargs

def to_dict(self) -> dict[str, Any]:
"""Converts this spatial extent to a dictionary."""
return {"bbox": self.bbox}
d = copy.deepcopy(self.extra_fields)
d["bbox"] = self.bbox
return d


class TemporalExtent:
Expand All @@ -77,6 +94,11 @@ def from_dict(cls: type[Self], d: dict[str, Any]) -> Self:
"""Creates a new temporal extent from a dictionary."""
return cls(**d)

@classmethod
def from_now(cls: type[Self]) -> Self:
"""Creates a new temporal extent that starts now and has no end time."""
return cls([[datetime.datetime.now(tz=datetime.timezone.utc), None]])

def __init__(
self,
interval: PermissiveInterval | None = None,
Expand Down
4 changes: 2 additions & 2 deletions src/pystac/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import warnings
from typing import Any, Sequence

from .asset import Asset
from .asset import Asset, AssetsMixin
from .constants import ITEM_TYPE
from .errors import StacWarning
from .link import Link
from .stac_object import STACObject


class Item(STACObject):
class Item(STACObject, AssetsMixin):
"""An Item is a GeoJSON Feature augmented with foreign members relevant to a
STAC object.
Expand Down
23 changes: 18 additions & 5 deletions src/pystac/stac_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .link import Link

if TYPE_CHECKING:
from .catalog import Catalog
from .io import Read, Write


Expand Down Expand Up @@ -77,13 +78,16 @@ def from_file(

@classmethod
def from_dict(
cls: type[STACObject],
cls: type[Self],
d: dict[str, Any],
*,
href: str | None = None,
root: Catalog | None = None, # TODO deprecation warning
migrate: bool = False,
preserve_dict: bool = True, # TODO deprecation warning
reader: Read | None = None,
writer: Write | None = None,
) -> STACObject:
) -> Self:
"""Creates a STAC object from a dictionary.
If you already know what type of STAC object your dictionary represents,
Expand All @@ -108,17 +112,24 @@ def from_dict(
if type_value == CATALOG_TYPE:
from .catalog import Catalog

return Catalog(**d, href=href, reader=reader, writer=writer)
stac_object: STACObject = Catalog(
**d, href=href, reader=reader, writer=writer
)
elif type_value == COLLECTION_TYPE:
from .collection import Collection

return Collection(**d, href=href, reader=reader, writer=writer)
stac_object = Collection(**d, href=href, reader=reader, writer=writer)
elif type_value == ITEM_TYPE:
from .item import Item

return Item(**d, href=href, reader=reader, writer=writer)
stac_object = Item(**d, href=href, reader=reader, writer=writer)
else:
raise StacError(f"unknown type field: {type_value}")

if isinstance(stac_object, cls):
return stac_object
else:
raise PystacError(f"Expected {cls} but got a {type(stac_object)}")
else:
raise StacError("missing type field on dictionary")

Expand All @@ -136,6 +147,8 @@ def __init__(
"""Creates a new STAC object."""
from .extensions import Extensions

super().__init__()

self.id: str = id
"""The object's id."""

Expand Down
13 changes: 12 additions & 1 deletion tests/test_extent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from pystac import StacWarning, TemporalExtent
from pystac import SpatialExtent, StacWarning, TemporalExtent


def test_temporal_with_datetimes() -> None:
Expand Down Expand Up @@ -45,3 +45,14 @@ def test_temporal_with_bad_tail() -> None:
)
d = extent.to_dict()
assert d == {"interval": [["2025-02-11T00:00:00Z", None]]}


def test_temporal_from_now() -> None:
extent = TemporalExtent.from_now()
assert isinstance(extent.interval[0][0], str)
assert extent.interval[0][1] is None


def test_spatial_from_coordinates() -> None:
with pytest.warns(FutureWarning):
SpatialExtent.from_coordinates([-180, -90, 180, 90])
7 changes: 7 additions & 0 deletions tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ def test_warn_include_self_link() -> None:
def test_warn_transform_hrefs() -> None:
with pytest.warns(FutureWarning):
Item("an-id").to_dict(transform_hrefs=True)


def test_from_dict_migrate() -> None:
d = Item("an-id").to_dict()
d["stac_version"] = "1.0.0"
item = Item.from_dict(d, migrate=True)
item.stac_version == "1.1.0"
Empty file added tests/v1/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions tests/v1/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import json
from typing import Any

import pytest

from .utils import TestCases


@pytest.fixture
def sample_item_dict() -> dict[str, Any]:
m = TestCases.get_path("data-files/item/sample-item.json")
with open(m) as f:
item_dict: dict[str, Any] = json.load(f)
return item_dict
81 changes: 81 additions & 0 deletions tests/v1/data-files/item/sample-item.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"type": "Feature",
"stac_version": "1.1.0",
"id": "CS3-20160503_132131_05",
"properties": {
"datetime": "2016-05-03T13:22:30.040000Z",
"title": "A CS3 item",
"license": "PDDL-1.0",
"providers": [
{
"name": "CoolSat",
"roles": [
"producer",
"licensor"
],
"url": "https://cool-sat.com/"
}
]
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-122.308150179,
37.488035566
],
[
-122.597502109,
37.538869539
],
[
-122.576687533,
37.613537207
],
[
-122.2880486,
37.562818007
],
[
-122.308150179,
37.488035566
]
]
]
},
"links": [
{
"rel": "collection",
"href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v0.8.1/collection-spec/examples/sentinel2.json"
}
],
"assets": {
"analytic": {
"href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/analytic.tif",
"title": "4-Band Analytic",
"product": "http://cool-sat.com/catalog/products/analytic.json",
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
"roles": [
"data",
"analytic"
]
},
"thumbnail": {
"href": "http://cool-sat.com/catalog/CS3-20160503_132130_04/thumbnail.png",
"title": "Thumbnail",
"type": "image/png",
"roles": [
"thumbnail"
]
}
},
"bbox": [
-122.59750209,
37.48803556,
-122.2880486,
37.613537207
],
"stac_extensions": [],
"collection": "CS3"
}
28 changes: 28 additions & 0 deletions tests/v1/test_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from copy import deepcopy
from typing import Any

from pystac import Item

from . import utils


def test_to_from_dict(sample_item_dict: dict[str, Any]) -> None:
param_dict = deepcopy(sample_item_dict)

utils.assert_to_from_dict(Item, param_dict)
item = Item.from_dict(param_dict)
assert item.id == "CS3-20160503_132131_05"

# test asset creation additional field(s)
assert (
item.assets["analytic"].extra_fields["product"]
== "http://cool-sat.com/catalog/products/analytic.json"
)
assert len(item.assets["thumbnail"].extra_fields) == 0

# test that the parameter is preserved
assert param_dict == sample_item_dict

# assert that the parameter is preserved regardless of preserve_dict
Item.from_dict(param_dict, preserve_dict=False)
assert param_dict == sample_item_dict
Loading

0 comments on commit c87ecb9

Please sign in to comment.