Skip to content

Feature/validate extensions #280

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 7 commits into from
Aug 8, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .[dev,server]
python -m pip install .[dev,server,validation]
python -m pip install "pypgstac==${{ matrix.pypgstac }}"

- name: Run test suite
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@

### Added

- add `validate_extensions` setting that enables validation of `stac_extensions` from submitted STAC objects
using the `stac_pydantic.extensions.validate_extensions` utility. Applicable only when `TransactionExtension`
is active.
- add `validation` extra requirement to install dependencies of `stac_pydantic` required for extension validation
- add `write_connection_pool` option in `stac_fastapi.pgstac.db.connect_to_db` function
- add `write_postgres_settings` option in `stac_fastapi.pgstac.db.connect_to_db` function to set specific settings for the `writer` DB connection pool
- add specific error message when trying to create `Item` with null geometry (not supported by PgSTAC)
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
],
"server": ["uvicorn[standard]==0.35.0"],
"awslambda": ["mangum"],
"validation": [
"stac_pydantic[validation]",
],
}


Expand Down
7 changes: 7 additions & 0 deletions stac_fastapi/pgstac/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ class Settings(ApiSettings):
invalid_id_chars: List[str] = DEFAULT_INVALID_ID_CHARS
base_item_cache: Type[BaseItemCache] = DefaultBaseItemCache

validate_extensions: bool = False
"""
Validate `stac_extensions` schemas against submitted data when creating or updated STAC objects.

Implies that the `Transactions` extension is enabled.
"""

cors_origins: str = "*"
cors_methods: str = "GET,POST,OPTIONS"
cors_credentials: bool = False
Expand Down
35 changes: 34 additions & 1 deletion stac_fastapi/pgstac/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
import re
from typing import List, Optional, Union
from typing import Any, Dict, List, Optional, Union

import attr
import jsonpatch
Expand All @@ -23,6 +23,7 @@
from stac_fastapi.types import stac as stac_types
from stac_fastapi.types.errors import NotFoundError
from stac_pydantic import Collection, Item, ItemCollection
from stac_pydantic.extensions import validate_extensions
from starlette.responses import JSONResponse, Response

from stac_fastapi.pgstac.config import Settings
Expand All @@ -44,8 +45,38 @@ def _validate_id(self, id: str, settings: Settings):
detail=f"ID ({id}) cannot contain the following characters: {' '.join(invalid_chars)}",
)

def _validate_extensions(
self,
stac_object: Union[
stac_types.Item, stac_types.Collection, stac_types.Catalog, Dict[str, Any]
],
settings: Settings,
) -> None:
"""Validate extensions of the STAC object data."""
if not settings.validate_extensions:
return

if isinstance(stac_object, dict):
if not stac_object.get("stac_extensions"):
return
else:
if not stac_object.stac_extensions:
return

try:
validate_extensions(
stac_object,
reraise_exception=True,
)
except Exception as err:
raise HTTPException(
status_code=422,
detail=f"STAC Extensions failed validation: {err!s}",
) from err

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

def _validate_item(
self,
Expand All @@ -59,6 +90,7 @@ def _validate_item(
body_item_id = item.get("id")

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

if item.get("geometry", None) is None:
raise HTTPException(
Expand Down Expand Up @@ -180,6 +212,7 @@ async def update_collection(
"""Update collection."""

col = collection.model_dump(mode="json")
self._validate_collection(request, col)

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "update_collection", col)
Expand Down
36 changes: 36 additions & 0 deletions tests/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,3 +948,39 @@ async def test_default_app(default_client, default_app, load_test_data):
assert "https://api.stacspec.org/v1.0.0/collections" in conf
assert "https://api.stacspec.org/v1.0.0/ogcapi-features#query" in conf
assert "https://api.stacspec.org/v1.0.0/ogcapi-features#sort" in conf


async def test_app_transactions_validate_extension(
app_client_validate_ext, load_test_data
):
coll = load_test_data("test_collection.json")
# Add attribution extension
# https://github.com/stac-extensions/attribution
coll["stac_extensions"] = [
"https://stac-extensions.github.io/attribution/v0.1.0/schema.json",
]

resp = await app_client_validate_ext.post("/collections", json=coll)
assert resp.status_code == 422
assert "STAC Extensions failed validation:" in resp.json()["detail"]

# add attribution
coll["attribution"] = "something"
resp = await app_client_validate_ext.post("/collections", json=coll)
assert resp.status_code == 201

item = load_test_data("test_item.json")
item["stac_extensions"].append(
"https://stac-extensions.github.io/attribution/v0.1.0/schema.json",
)
resp = await app_client_validate_ext.post(
f"/collections/{coll['id']}/items", json=item
)
assert resp.status_code == 422
assert "STAC Extensions failed validation:" in resp.json()["detail"]

item["properties"]["attribution"] = "something"
resp = await app_client_validate_ext.post(
f"/collections/{coll['id']}/items", json=item
)
assert resp.status_code == 201
45 changes: 45 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,48 @@ async def app_client_advanced_freetext(app_advanced_freetext):
transport=ASGITransport(app=app_advanced_freetext), base_url="http://test"
) as c:
yield c


@pytest.fixture(scope="function")
async def app_transaction_validation_ext(database):
"""Default stac-fastapi-pgstac application with extension validation in transaction."""
api_settings = Settings(testing=True, validate_extensions=True)
api = StacApi(
settings=api_settings,
extensions=[
TransactionExtension(
client=TransactionsClient(),
settings=api_settings,
)
],
client=CoreCrudClient(),
health_check=health_check,
)

postgres_settings = PostgresSettings(
pguser=database.user,
pgpassword=database.password,
pghost=database.host,
pgport=database.port,
pgdatabase=database.dbname,
)
logger.info("Creating app Fixture")
await connect_to_db(
api.app,
postgres_settings=postgres_settings,
add_write_connection_pool=True,
)
yield api.app
await close_db_connection(api.app)

logger.info("Closed Pools.")


@pytest.fixture(scope="function")
async def app_client_validate_ext(app_transaction_validation_ext):
logger.info("creating app_client")
async with AsyncClient(
transport=ASGITransport(app=app_transaction_validation_ext),
base_url="http://test",
) as c:
yield c
Loading