Skip to content

Commit c87ecb9

Browse files
authored
feat(v2): first v1 test (#1523)
* 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
1 parent b9abe9e commit c87ecb9

File tree

14 files changed

+466
-11
lines changed

14 files changed

+466
-11
lines changed

.github/workflows/ci.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
branches:
66
- v2
7+
pull_request:
8+
branches:
9+
- v2
710

811
jobs:
912
test:
@@ -52,6 +55,7 @@ jobs:
5255
path: site/
5356
deploy-docs:
5457
name: Deploy docs
58+
if: github.ref == 'refs/heads/v2'
5559
environment:
5660
name: github-pages
5761
url: ${{ steps.deployment.outputs.page_url }}

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323
"Programming Language :: Python :: 3.13",
2424
]
2525
requires-python = ">=3.10"
26-
dependencies = ["typing-extensions>=4.12.2"]
26+
dependencies = ["python-dateutil>=2.9.0.post0", "typing-extensions>=4.12.2"]
2727

2828
[project.optional-dependencies]
2929
validate = ["jsonschema>=4.23.0", "referencing>=0.36.2"]
@@ -35,6 +35,7 @@ dev = [
3535
"pytest>=8.3.4",
3636
"ruff>=0.9.6",
3737
"types-jsonschema>=4.23.0.20241208",
38+
"types-python-dateutil>=2.9.0.20241206",
3839
]
3940
bench = ["asv>=0.6.4"]
4041
docs = ["mike>=2.1.3", "mkdocs-material>=9.6.3", "mkdocstrings-python>=1.14.6"]

src/pystac/asset.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,12 @@ def to_dict(self) -> dict[str, Any]:
7575
d = {"href": self.href}
7676
d.update(super().to_dict())
7777
return d
78+
79+
80+
class AssetsMixin:
81+
"""A mixin for things that have assets (Collections and Items)"""
82+
83+
assets: dict[str, Asset]
84+
85+
def add_asset(self, key: str, asset: Asset) -> None:
86+
raise NotImplementedError

src/pystac/extent.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3+
import copy
34
import datetime
45
import warnings
56
from typing import Any, Sequence
67

78
from typing_extensions import Self
89

910
from .constants import DEFAULT_BBOX, DEFAULT_INTERVAL
11+
from .decorators import v2_deprecated
1012
from .errors import StacWarning
1113
from .types import PermissiveBbox, PermissiveInterval
1214

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

57-
def __init__(self, bbox: PermissiveBbox | None = None):
59+
@classmethod
60+
@v2_deprecated("Use the constructor instead")
61+
def from_coordinates(
62+
cls: type[Self],
63+
coordinates: list[Any],
64+
extra_fields: dict[str, Any] | None = None,
65+
) -> Self:
66+
if extra_fields:
67+
return cls(coordinates, **extra_fields)
68+
else:
69+
return cls(coordinates)
70+
71+
def __init__(self, bbox: PermissiveBbox | None = None, **kwargs: Any):
5872
"""Creates a new spatial extent."""
5973
self.bbox: Sequence[Sequence[float | int]]
6074
if bbox is None or len(bbox) == 0:
@@ -63,10 +77,13 @@ def __init__(self, bbox: PermissiveBbox | None = None):
6377
self.bbox = bbox # type: ignore
6478
else:
6579
self.bbox = [bbox] # type: ignore
80+
self.extra_fields = kwargs
6681

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

7188

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

97+
@classmethod
98+
def from_now(cls: type[Self]) -> Self:
99+
"""Creates a new temporal extent that starts now and has no end time."""
100+
return cls([[datetime.datetime.now(tz=datetime.timezone.utc), None]])
101+
80102
def __init__(
81103
self,
82104
interval: PermissiveInterval | None = None,

src/pystac/item.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import warnings
66
from typing import Any, Sequence
77

8-
from .asset import Asset
8+
from .asset import Asset, AssetsMixin
99
from .constants import ITEM_TYPE
1010
from .errors import StacWarning
1111
from .link import Link
1212
from .stac_object import STACObject
1313

1414

15-
class Item(STACObject):
15+
class Item(STACObject, AssetsMixin):
1616
"""An Item is a GeoJSON Feature augmented with foreign members relevant to a
1717
STAC object.
1818

src/pystac/stac_object.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .link import Link
2222

2323
if TYPE_CHECKING:
24+
from .catalog import Catalog
2425
from .io import Read, Write
2526

2627

@@ -77,13 +78,16 @@ def from_file(
7778

7879
@classmethod
7980
def from_dict(
80-
cls: type[STACObject],
81+
cls: type[Self],
8182
d: dict[str, Any],
8283
*,
8384
href: str | None = None,
85+
root: Catalog | None = None, # TODO deprecation warning
86+
migrate: bool = False,
87+
preserve_dict: bool = True, # TODO deprecation warning
8488
reader: Read | None = None,
8589
writer: Write | None = None,
86-
) -> STACObject:
90+
) -> Self:
8791
"""Creates a STAC object from a dictionary.
8892
8993
If you already know what type of STAC object your dictionary represents,
@@ -108,17 +112,24 @@ def from_dict(
108112
if type_value == CATALOG_TYPE:
109113
from .catalog import Catalog
110114

111-
return Catalog(**d, href=href, reader=reader, writer=writer)
115+
stac_object: STACObject = Catalog(
116+
**d, href=href, reader=reader, writer=writer
117+
)
112118
elif type_value == COLLECTION_TYPE:
113119
from .collection import Collection
114120

115-
return Collection(**d, href=href, reader=reader, writer=writer)
121+
stac_object = Collection(**d, href=href, reader=reader, writer=writer)
116122
elif type_value == ITEM_TYPE:
117123
from .item import Item
118124

119-
return Item(**d, href=href, reader=reader, writer=writer)
125+
stac_object = Item(**d, href=href, reader=reader, writer=writer)
120126
else:
121127
raise StacError(f"unknown type field: {type_value}")
128+
129+
if isinstance(stac_object, cls):
130+
return stac_object
131+
else:
132+
raise PystacError(f"Expected {cls} but got a {type(stac_object)}")
122133
else:
123134
raise StacError("missing type field on dictionary")
124135

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

150+
super().__init__()
151+
139152
self.id: str = id
140153
"""The object's id."""
141154

tests/test_extent.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from pystac import StacWarning, TemporalExtent
6+
from pystac import SpatialExtent, StacWarning, TemporalExtent
77

88

99
def test_temporal_with_datetimes() -> None:
@@ -45,3 +45,14 @@ def test_temporal_with_bad_tail() -> None:
4545
)
4646
d = extent.to_dict()
4747
assert d == {"interval": [["2025-02-11T00:00:00Z", None]]}
48+
49+
50+
def test_temporal_from_now() -> None:
51+
extent = TemporalExtent.from_now()
52+
assert isinstance(extent.interval[0][0], str)
53+
assert extent.interval[0][1] is None
54+
55+
56+
def test_spatial_from_coordinates() -> None:
57+
with pytest.warns(FutureWarning):
58+
SpatialExtent.from_coordinates([-180, -90, 180, 90])

tests/test_item.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,10 @@ def test_warn_include_self_link() -> None:
3737
def test_warn_transform_hrefs() -> None:
3838
with pytest.warns(FutureWarning):
3939
Item("an-id").to_dict(transform_hrefs=True)
40+
41+
42+
def test_from_dict_migrate() -> None:
43+
d = Item("an-id").to_dict()
44+
d["stac_version"] = "1.0.0"
45+
item = Item.from_dict(d, migrate=True)
46+
item.stac_version == "1.1.0"

tests/v1/__init__.py

Whitespace-only changes.

tests/v1/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import json
2+
from typing import Any
3+
4+
import pytest
5+
6+
from .utils import TestCases
7+
8+
9+
@pytest.fixture
10+
def sample_item_dict() -> dict[str, Any]:
11+
m = TestCases.get_path("data-files/item/sample-item.json")
12+
with open(m) as f:
13+
item_dict: dict[str, Any] = json.load(f)
14+
return item_dict

0 commit comments

Comments
 (0)