Skip to content

Commit 74a7909

Browse files
mmcfarlandgadomski
andauthored
Update request body types for Transaction Extension spec and client (#574)
* Update request body for Transaction Extension spec A POST against an items endpoint should accept an Item or ItemCollection https://github.com/radiantearth/stac-api-spec/blob/master/ogcapi-features/extensions/transaction/README.md#methods Add literal values to the stac_types `type` attribute in order to allow fastapi a discriminator value for parsing the request body to the correct TypedDict. * Changelog * feat: add a simple smoke test It doesn't exercise much, but at least it demonstrates what we're trying to do. --------- Co-authored-by: Pete Gadomski <[email protected]>
1 parent ed98341 commit 74a7909

File tree

5 files changed

+146
-12
lines changed

5 files changed

+146
-12
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
* Add support for POSTing ItemCollections to the /items endpoint of the Transaction Extension ([#547](https://github.com/stac-utils/stac-fastapi/pull/574)
8+
59
## [2.4.6] - 2023-05-09
610

711
### Changed

stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
class PostItem(CollectionUri):
1919
"""Create Item."""
2020

21-
item: stac_types.Item = attr.ib(default=Body(None))
21+
item: Union[stac_types.Item, stac_types.ItemCollection] = attr.ib(
22+
default=Body(None)
23+
)
2224

2325

2426
@attr.s
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import json
2+
from typing import Iterator, Union
3+
4+
import pytest
5+
from starlette.testclient import TestClient
6+
7+
from stac_fastapi.api.app import StacApi
8+
from stac_fastapi.extensions.core import TransactionExtension
9+
from stac_fastapi.types.config import ApiSettings
10+
from stac_fastapi.types.core import BaseCoreClient, BaseTransactionsClient
11+
from stac_fastapi.types.stac import Item, ItemCollection
12+
13+
14+
class DummyCoreClient(BaseCoreClient):
15+
def all_collections(self, *args, **kwargs):
16+
raise NotImplementedError
17+
18+
def get_collection(self, *args, **kwargs):
19+
raise NotImplementedError
20+
21+
def get_item(self, *args, **kwargs):
22+
raise NotImplementedError
23+
24+
def get_search(self, *args, **kwargs):
25+
raise NotImplementedError
26+
27+
def post_search(self, *args, **kwargs):
28+
raise NotImplementedError
29+
30+
def item_collection(self, *args, **kwargs):
31+
raise NotImplementedError
32+
33+
34+
class DummyTransactionsClient(BaseTransactionsClient):
35+
"""Defines a pattern for implementing the STAC transaction extension."""
36+
37+
def create_item(self, item: Union[Item, ItemCollection], *args, **kwargs):
38+
return {"created": True, "type": item["type"]}
39+
40+
def update_item(self, *args, **kwargs):
41+
raise NotImplementedError
42+
43+
def delete_item(self, *args, **kwargs):
44+
raise NotImplementedError
45+
46+
def create_collection(self, *args, **kwargs):
47+
raise NotImplementedError
48+
49+
def update_collection(self, *args, **kwargs):
50+
raise NotImplementedError
51+
52+
def delete_collection(self, *args, **kwargs):
53+
raise NotImplementedError
54+
55+
56+
def test_create_item(client: TestClient, item: Item) -> None:
57+
response = client.post("/collections/a-collection/items", content=json.dumps(item))
58+
assert response.is_success, response.text
59+
assert response.json()["type"] == "Feature"
60+
61+
62+
def test_create_item_collection(
63+
client: TestClient, item_collection: ItemCollection
64+
) -> None:
65+
response = client.post(
66+
"/collections/a-collection/items", content=json.dumps(item_collection)
67+
)
68+
assert response.is_success, response.text
69+
assert response.json()["type"] == "FeatureCollection"
70+
71+
72+
@pytest.fixture
73+
def client(
74+
core_client: DummyCoreClient, transactions_client: DummyTransactionsClient
75+
) -> Iterator[TestClient]:
76+
settings = ApiSettings()
77+
api = StacApi(
78+
settings=settings,
79+
client=core_client,
80+
extensions=[
81+
TransactionExtension(client=transactions_client, settings=settings),
82+
],
83+
)
84+
with TestClient(api.app) as client:
85+
yield client
86+
87+
88+
@pytest.fixture
89+
def core_client() -> DummyCoreClient:
90+
return DummyCoreClient()
91+
92+
93+
@pytest.fixture
94+
def transactions_client() -> DummyTransactionsClient:
95+
return DummyTransactionsClient()
96+
97+
98+
@pytest.fixture
99+
def item_collection(item: Item) -> ItemCollection:
100+
return {
101+
"type": "FeatureCollection",
102+
"features": [item],
103+
"links": [],
104+
"context": None,
105+
}
106+
107+
108+
@pytest.fixture
109+
def item() -> Item:
110+
return {
111+
"type": "Feature",
112+
"stac_version": "1.0.0",
113+
"stac_extensions": [],
114+
"id": "test_item",
115+
"geometry": {"type": "Point", "coordinates": [-105, 40]},
116+
"bbox": [-105, 40, -105, 40],
117+
"properties": {},
118+
"links": [],
119+
"assets": {},
120+
"collection": "test_collection",
121+
}

stac_fastapi/types/stac_fastapi/types/core.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,21 @@ class BaseTransactionsClient(abc.ABC):
2828

2929
@abc.abstractmethod
3030
def create_item(
31-
self, collection_id: str, item: stac_types.Item, **kwargs
32-
) -> Optional[Union[stac_types.Item, Response]]:
31+
self,
32+
collection_id: str,
33+
item: Union[stac_types.Item, stac_types.ItemCollection],
34+
**kwargs,
35+
) -> Optional[Union[stac_types.Item, Response, None]]:
3336
"""Create a new item.
3437
3538
Called with `POST /collections/{collection_id}/items`.
3639
3740
Args:
38-
item: the item
41+
item: the item or item collection
3942
collection_id: the id of the collection from the resource path
4043
4144
Returns:
42-
The item that was created.
45+
The item that was created or None if item collection.
4346
4447
"""
4548
...
@@ -138,17 +141,21 @@ class AsyncBaseTransactionsClient(abc.ABC):
138141

139142
@abc.abstractmethod
140143
async def create_item(
141-
self, collection_id: str, item: stac_types.Item, **kwargs
142-
) -> Optional[Union[stac_types.Item, Response]]:
144+
self,
145+
collection_id: str,
146+
item: Union[stac_types.Item, stac_types.ItemCollection],
147+
**kwargs,
148+
) -> Optional[Union[stac_types.Item, Response, None]]:
143149
"""Create a new item.
144150
145151
Called with `POST /collections/{collection_id}/items`.
146152
147153
Args:
148-
item: the item
154+
item: the item or item collection
155+
collection_id: the id of the collection from the resource path
149156
150157
Returns:
151-
The item that was created.
158+
The item that was created or None if item collection.
152159
153160
"""
154161
...

stac_fastapi/types/stac_fastapi/types/stac.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""STAC types."""
22
import sys
3-
from typing import Any, Dict, List, Optional, Union
3+
from typing import Any, Dict, List, Literal, Optional, Union
44

55
# Avoids a Pydantic error:
66
# TypeError: You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.9.2.
@@ -58,7 +58,7 @@ class Collection(Catalog, total=False):
5858
class Item(TypedDict, total=False):
5959
"""STAC Item."""
6060

61-
type: str
61+
type: Literal["Feature"]
6262
stac_version: str
6363
stac_extensions: Optional[List[str]]
6464
id: str
@@ -73,7 +73,7 @@ class Item(TypedDict, total=False):
7373
class ItemCollection(TypedDict, total=False):
7474
"""STAC Item Collection."""
7575

76-
type: str
76+
type: Literal["FeatureCollection"]
7777
features: List[Item]
7878
links: List[Dict[str, Any]]
7979
context: Optional[Dict[str, int]]

0 commit comments

Comments
 (0)