Skip to content

Support creating ItemCollections in Transaction Extension #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ As a part of this release, this repository was extracted from the main
### Added

* Ability to customize the database connection ([#22](https://github.com/stac-utils/stac-fastapi-pgstac/pull/22))
* Ability to add ItemCollections through the Transaction API, with more validation ([#35](https://github.com/stac-utils/stac-fastapi-pgstac/pull/35))

### Changed

Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ services:
ports:
- "8082:8082"
volumes:
- ./stac_fastapi:/app/stac_fastapi
- ./scripts:/app/scripts
- .:/app
depends_on:
- database
command: bash -c "./scripts/wait-for-it.sh database:5432 && python -m stac_fastapi.pgstac.app"

database:
container_name: stac-db
image: ghcr.io/stac-utils/pgstac:v0.7.1
image: ghcr.io/stac-utils/pgstac:v0.7.6
environment:
- POSTGRES_USER=username
- POSTGRES_PASSWORD=password
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"orjson",
"pydantic[dotenv]",
"stac_pydantic==2.0.*",
"stac-fastapi.types",
"stac-fastapi.api",
"stac-fastapi.extensions",
"stac-fastapi.types~=2.4.7",
"stac-fastapi.api~=2.4.7",
"stac-fastapi.extensions~=2.4.7",
"asyncpg",
"buildpg",
"brotli_asgi",
Expand Down
25 changes: 24 additions & 1 deletion stac_fastapi/pgstac/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Postgres API configuration."""

from typing import Type
from typing import List, Type
from urllib.parse import quote

from stac_fastapi.types.config import ApiSettings
Expand All @@ -10,6 +10,27 @@
DefaultBaseItemCache,
)

DEFAULT_INVALID_ID_CHARS = [
":",
"/",
"?",
"#",
"[",
"]",
"@",
"!",
"$",
"&",
"'",
"(",
")",
"*",
"+",
",",
";",
"=",
]


class Settings(ApiSettings):
"""Postgres-specific API settings.
Expand All @@ -22,6 +43,7 @@ class Settings(ApiSettings):
postgres_port: database port.
postgres_dbname: database name.
use_api_hydrate: perform hydration of stac items within stac-fastapi.
invalid_id_chars: list of characters that are not allowed in item or collection ids.
"""

postgres_user: str
Expand All @@ -38,6 +60,7 @@ class Settings(ApiSettings):

use_api_hydrate: bool = False
base_item_cache: Type[BaseItemCache] = DefaultBaseItemCache
invalid_id_chars: List[str] = DEFAULT_INVALID_ID_CHARS

testing: bool = False

Expand Down
103 changes: 78 additions & 25 deletions stac_fastapi/pgstac/transactions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""transactions extension client."""

import logging
import re
from typing import Optional, Union

import attr
Expand All @@ -14,6 +15,7 @@
from stac_fastapi.types.core import AsyncBaseTransactionsClient
from starlette.responses import JSONResponse, Response

from stac_fastapi.pgstac.config import Settings
from stac_fastapi.pgstac.db import dbfunc
from stac_fastapi.pgstac.models.links import CollectionLinks, ItemLinks

Expand All @@ -25,25 +27,83 @@
class TransactionsClient(AsyncBaseTransactionsClient):
"""Transactions extension specific CRUD operations."""

async def create_item(
self, collection_id: str, item: stac_types.Item, request: Request, **kwargs
) -> Optional[Union[stac_types.Item, Response]]:
"""Create item."""
def _validate_id(self, id: str, settings: Settings) -> bool:
invalid_chars = settings.invalid_id_chars
id_regex = "[" + "".join(re.escape(char) for char in invalid_chars) + "]"

if bool(re.search(id_regex, id)):
raise HTTPException(
status_code=400,
detail=f"ID ({id}) cannot contain the following characters: {' '.join(invalid_chars)}",
)

def _validate_collection(self, request: Request, collection: stac_types.Collection):
self._validate_id(collection["id"], request.app.state.settings)

def _validate_item(
self,
request: Request,
item: stac_types.Item,
collection_id: str,
expected_item_id: Optional[str] = None,
) -> None:
"""Validate item."""
body_collection_id = item.get("collection")
body_item_id = item.get("id")

self._validate_id(body_item_id, request.app.state.settings)

if body_collection_id is not None and collection_id != body_collection_id:
raise HTTPException(
status_code=400,
detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
)
item["collection"] = collection_id
async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "create_item", item)
item["links"] = await ItemLinks(
collection_id=collection_id,
item_id=item["id"],
request=request,
).get_links(extra_links=item.get("links"))
return stac_types.Item(**item)

if expected_item_id is not None and expected_item_id != body_item_id:
raise HTTPException(
status_code=400,
detail=f"Item ID from path parameter ({expected_item_id}) does not match Item ID from Item ({body_item_id})",
)

async def create_item(
self,
collection_id: str,
item: Union[stac_types.Item, stac_types.ItemCollection],
request: Request,
**kwargs,
) -> Optional[Union[stac_types.Item, Response]]:
"""Create item."""
if item["type"] == "FeatureCollection":
valid_items = []
for item in item["features"]:
self._validate_item(request, item, collection_id)
item["collection"] = collection_id
valid_items.append(item)

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "create_items", valid_items)

return Response(status_code=201)

elif item["type"] == "Feature":
self._validate_item(request, item, collection_id)
item["collection"] = collection_id

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "create_item", item)

item["links"] = await ItemLinks(
collection_id=collection_id,
item_id=item["id"],
request=request,
).get_links(extra_links=item.get("links"))

return stac_types.Item(**item)
else:
raise HTTPException(
status_code=400,
detail=f"Item body type must be 'Feature' or 'FeatureCollection', not {item['type']}",
)

async def update_item(
self,
Expand All @@ -54,32 +114,25 @@ async def update_item(
**kwargs,
) -> Optional[Union[stac_types.Item, Response]]:
"""Update item."""
body_collection_id = item.get("collection")
if body_collection_id is not None and collection_id != body_collection_id:
raise HTTPException(
status_code=400,
detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
)
self._validate_item(request, item, collection_id, item_id)
item["collection"] = collection_id
body_item_id = item["id"]
if body_item_id != item_id:
raise HTTPException(
status_code=400,
detail=f"Item ID from path parameter ({item_id}) does not match Item ID from Item ({body_item_id})",
)

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "update_item", item)

item["links"] = await ItemLinks(
collection_id=collection_id,
item_id=item["id"],
request=request,
).get_links(extra_links=item.get("links"))

return stac_types.Item(**item)

async def create_collection(
self, collection: stac_types.Collection, request: Request, **kwargs
) -> Optional[Union[stac_types.Collection, Response]]:
"""Create collection."""
self._validate_collection(request, collection)
async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "create_collection", collection)
collection["links"] = await CollectionLinks(
Expand Down
Loading