Skip to content

Commit 90e158d

Browse files
authored
[pgstac] Delete items using collection id (#520)
* fix, pgstac: delete items with collection id * chore: update changelog
1 parent 759ef17 commit 90e158d

File tree

4 files changed

+71
-5
lines changed

4 files changed

+71
-5
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* `self` link rel for `/collections/{c_id}/items` ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
2323
* Media type of the item collection endpoint ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
2424
* Manually exclude non-truthy optional values from sqlalchemy serialization of Collections ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
25+
* Deleting items that had repeated ids in other collections ([#520](https://github.com/stac-utils/stac-fastapi/pull/520))
2526

2627
## [2.4.3] - 2022-11-25
2728

stac_fastapi/pgstac/stac_fastapi/pgstac/db.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Database connection handling."""
22

33
import json
4-
from typing import Dict, Union
4+
from contextlib import contextmanager
5+
from typing import Dict, Generator, Union
56

67
import attr
78
import orjson
@@ -61,7 +62,7 @@ async def dbfunc(pool: pool, func: str, arg: Union[str, Dict]):
6162
arg -- the argument to the PostgreSQL function as either a string
6263
or a dict that will be converted into jsonb
6364
"""
64-
try:
65+
with translate_pgstac_errors():
6566
if isinstance(arg, str):
6667
async with pool.acquire() as conn:
6768
q, p = render(
@@ -80,6 +81,13 @@ async def dbfunc(pool: pool, func: str, arg: Union[str, Dict]):
8081
item=json.dumps(arg),
8182
)
8283
return await conn.fetchval(q, *p)
84+
85+
86+
@contextmanager
87+
def translate_pgstac_errors() -> Generator[None, None, None]:
88+
"""Context manager that translates pgstac errors into FastAPI errors."""
89+
try:
90+
yield
8391
except exceptions.UniqueViolationError as e:
8492
raise ConflictError from e
8593
except exceptions.NoDataFoundError as e:

stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
from typing import Optional, Union
55

66
import attr
7+
from buildpg import render
78
from fastapi import HTTPException
89
from starlette.responses import JSONResponse, Response
910

1011
from stac_fastapi.extensions.third_party.bulk_transactions import (
1112
AsyncBaseBulkTransactionsClient,
1213
Items,
1314
)
14-
from stac_fastapi.pgstac.db import dbfunc
15+
from stac_fastapi.pgstac.db import dbfunc, translate_pgstac_errors
1516
from stac_fastapi.pgstac.models.links import CollectionLinks, ItemLinks
1617
from stac_fastapi.types import stac as stac_types
1718
from stac_fastapi.types.core import AsyncBaseTransactionsClient
@@ -98,12 +99,19 @@ async def update_collection(
9899
return stac_types.Collection(**collection)
99100

100101
async def delete_item(
101-
self, item_id: str, **kwargs
102+
self, item_id: str, collection_id: str, **kwargs
102103
) -> Optional[Union[stac_types.Item, Response]]:
103104
"""Delete item."""
104105
request = kwargs["request"]
105106
pool = request.app.state.writepool
106-
await dbfunc(pool, "delete_item", item_id)
107+
async with pool.acquire() as conn:
108+
q, p = render(
109+
"SELECT * FROM delete_item(:item::text, :collection::text);",
110+
item=item_id,
111+
collection=collection_id,
112+
)
113+
with translate_pgstac_errors():
114+
await conn.fetchval(q, *p)
107115
return JSONResponse({"deleted item": item_id})
108116

109117
async def delete_collection(

stac_fastapi/pgstac/tests/api/test_api.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import orjson
55
import pytest
6+
from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent
67

78
STAC_CORE_ROUTES = [
89
"GET /",
@@ -24,6 +25,24 @@
2425
"PUT /collections/{collection_id}/items/{item_id}",
2526
]
2627

28+
GLOBAL_BBOX = [-180.0, -90.0, 180.0, 90.0]
29+
GLOBAL_GEOMETRY = {
30+
"type": "Polygon",
31+
"coordinates": (
32+
(
33+
(180.0, -90.0),
34+
(180.0, 90.0),
35+
(-180.0, 90.0),
36+
(-180.0, -90.0),
37+
(180.0, -90.0),
38+
),
39+
),
40+
}
41+
DEFAULT_EXTENT = Extent(
42+
SpatialExtent(GLOBAL_BBOX),
43+
TemporalExtent([[datetime.now(), None]]),
44+
)
45+
2746

2847
async def test_post_search_content_type(app_client):
2948
params = {"limit": 1}
@@ -513,3 +532,33 @@ async def test_bad_collection_queryables(
513532
):
514533
resp = await app_client.get("/collections/bad-collection/queryables")
515534
assert resp.status_code == 404
535+
536+
537+
async def test_deleting_items_with_identical_ids(app_client):
538+
collection_a = Collection("collection-a", "The first collection", DEFAULT_EXTENT)
539+
collection_b = Collection("collection-b", "The second collection", DEFAULT_EXTENT)
540+
item = Item("the-item", GLOBAL_GEOMETRY, GLOBAL_BBOX, datetime.now(), {})
541+
542+
for collection in (collection_a, collection_b):
543+
response = await app_client.post(
544+
"/collections", json=collection.to_dict(include_self_link=False)
545+
)
546+
assert response.status_code == 200
547+
item_as_dict = item.to_dict(include_self_link=False)
548+
item_as_dict["collection"] = collection.id
549+
response = await app_client.post(
550+
f"/collections/{collection.id}/items", json=item_as_dict
551+
)
552+
assert response.status_code == 200
553+
response = await app_client.get(f"/collections/{collection.id}/items")
554+
assert response.status_code == 200, response.json()
555+
assert len(response.json()["features"]) == 1
556+
557+
for collection in (collection_a, collection_b):
558+
response = await app_client.delete(
559+
f"/collections/{collection.id}/items/{item.id}"
560+
)
561+
assert response.status_code == 200, response.json()
562+
response = await app_client.get(f"/collections/{collection.id}/items")
563+
assert response.status_code == 200, response.json()
564+
assert not response.json()["features"]

0 commit comments

Comments
 (0)