diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index fcb9393..17162af 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -101,6 +101,7 @@ jobs: PGDATABASE: postgis APP_HOST: 0.0.0.0 APP_PORT: 8080 + ENABLE_TRANSACTIONS_EXTENSIONS: TRUE test-docs: runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 11a6d8e..bdc2ea6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### Changed +- add `ENABLE_TRANSACTIONS_EXTENSIONS` env variable to enable `transaction` extensions +- disable transaction and bulk_transactions extensions by default **breaking change** - update `stac-fastapi-*` version requirements to `>=5.2,<6.0` - add pgstac health-check in `/_mgmt/health` diff --git a/docker-compose.yml b/docker-compose.yml index 725a52e..962aa2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: - DB_MIN_CONN_SIZE=1 - DB_MAX_CONN_SIZE=1 - USE_API_HYDRATE=${USE_API_HYDRATE:-false} + - ENABLE_TRANSACTIONS_EXTENSIONS=TRUE ports: - "8082:8082" volumes: diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 5e9dd13..d19a71e 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -49,16 +49,6 @@ settings = Settings() -# application extensions -application_extensions_map = { - "transaction": TransactionExtension( - client=TransactionsClient(), - settings=settings, - response_class=JSONResponse, - ), - "bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()), -} - # search extensions search_extensions_map = { "query": QueryExtension(), @@ -93,25 +83,30 @@ "pagination": TokenPaginationExtension(), } -known_extensions = { - *application_extensions_map.keys(), +enabled_extensions = { *search_extensions_map.keys(), *cs_extensions_map.keys(), *itm_col_extensions_map.keys(), "collection_search", } -enabled_extensions = ( - os.environ["ENABLED_EXTENSIONS"].split(",") - if "ENABLED_EXTENSIONS" in os.environ - else known_extensions -) +if ext := os.environ.get("ENABLED_EXTENSIONS"): + enabled_extensions = set(ext.split(",")) -application_extensions = [ - extension - for key, extension in application_extensions_map.items() - if key in enabled_extensions -] +application_extensions = [] + +if os.environ.get("ENABLE_TRANSACTIONS_EXTENSIONS", "").lower() in ["yes", "true", "1"]: + application_extensions.append( + TransactionExtension( + client=TransactionsClient(), + settings=settings, + response_class=JSONResponse, + ), + ) + + application_extensions.append( + BulkTransactionExtension(client=BulkTransactionsClient()), + ) # /search models search_extensions = [ diff --git a/tests/api/test_api.py b/tests/api/test_api.py index e4ce8d0..9185a79 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -68,6 +68,19 @@ ) +async def test_default_app_no_transactions( + app_client_no_transaction, load_test_data, load_test_collection +): + coll = load_test_collection + item = load_test_data("test_item.json") + resp = await app_client_no_transaction.post( + f"/collections/{coll['id']}/items", json=item + ) + + # the default application does not have the transaction extensions enabled! + assert resp.status_code == 405 + + async def test_post_search_content_type(app_client): params = {"limit": 1} resp = await app_client.post("search", json=params) @@ -892,3 +905,40 @@ async def test_no_extension( finally: await close_db_connection(app) + + +async def test_default_app(default_client, default_app, load_test_data): + api_routes = { + f"{list(route.methods)[0]} {route.path}" for route in default_app.routes + } + assert set(STAC_CORE_ROUTES).issubset(api_routes) + assert set(STAC_TRANSACTION_ROUTES).issubset(api_routes) + + # Load collections + col = load_test_data("test_collection.json") + resp = await default_client.post("/collections", json=col) + assert resp.status_code == 201 + + # Load items + item = load_test_data("test_item.json") + resp = await default_client.post(f"/collections/{col['id']}/items", json=item) + assert resp.status_code == 201 + + resp = await default_client.get("/conformance") + assert resp.status_code == 200 + conf = resp.json()["conformsTo"] + assert ( + "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction" in conf + ) + assert "https://api.stacspec.org/v1.0.0/collections/extensions/transaction" in conf + assert "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2" in conf + assert "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" in conf + assert "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core" in conf + assert ( + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf + ) + assert "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter" in conf + assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search" in conf + 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 diff --git a/tests/conftest.py b/tests/conftest.py index 052f260..397ecec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import asyncpg import pytest from fastapi import APIRouter -from fastapi.responses import ORJSONResponse from httpx import ASGITransport, AsyncClient from pypgstac import __version__ as pgstac_version from pypgstac.db import PgstacDB @@ -18,6 +17,7 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import ( ItemCollectionUri, + JSONResponse, create_get_request_model, create_post_request_model, create_request_model, @@ -189,7 +189,7 @@ def api_client(request): search_get_request_model=search_get_request_model, search_post_request_model=search_post_request_model, collections_get_request_model=collection_search_extension.GET, - response_class=ORJSONResponse, + response_class=JSONResponse, router=APIRouter(prefix=prefix), health_check=health_check, ) @@ -290,14 +290,11 @@ async def load_test2_item(app_client, load_test_data, load_test2_collection): return Item.model_validate(resp.json()) -@pytest.fixture( - scope="session", -) -def api_client_no_ext(): - api_settings = Settings( - testing=True, - ) - return StacApi( +@pytest.fixture(scope="function") +async def app_no_ext(database): + """Default stac-fastapi-pgstac application without only the transaction extensions.""" + api_settings = Settings(testing=True) + api_client_no_ext = StacApi( settings=api_settings, extensions=[ TransactionExtension(client=TransactionsClient(), settings=api_settings) @@ -306,9 +303,6 @@ def api_client_no_ext(): health_check=health_check, ) - -@pytest.fixture(scope="function") -async def app_no_ext(api_client_no_ext, database): postgres_settings = PostgresSettings( postgres_user=database.user, postgres_pass=database.password, @@ -319,12 +313,9 @@ async def app_no_ext(api_client_no_ext, database): ) logger.info("Creating app Fixture") time.time() - app = api_client_no_ext.app - await connect_to_db(app, postgres_settings=postgres_settings) - - yield app - - await close_db_connection(app) + await connect_to_db(api_client_no_ext.app, postgres_settings=postgres_settings) + yield api_client_no_ext.app + await close_db_connection(api_client_no_ext.app) logger.info("Closed Pools.") @@ -336,3 +327,70 @@ async def app_client_no_ext(app_no_ext): transport=ASGITransport(app=app_no_ext), base_url="http://test" ) as c: yield c + + +@pytest.fixture(scope="function") +async def app_no_transaction(database): + """Default stac-fastapi-pgstac application without any extensions.""" + api_settings = Settings(testing=True) + api = StacApi( + settings=api_settings, + extensions=[], + client=CoreCrudClient(), + health_check=health_check, + ) + + postgres_settings = PostgresSettings( + postgres_user=database.user, + postgres_pass=database.password, + postgres_host_reader=database.host, + postgres_host_writer=database.host, + postgres_port=database.port, + postgres_dbname=database.dbname, + ) + logger.info("Creating app Fixture") + time.time() + await connect_to_db(api.app, postgres_settings=postgres_settings) + yield api.app + await close_db_connection(api.app) + + logger.info("Closed Pools.") + + +@pytest.fixture(scope="function") +async def app_client_no_transaction(app_no_transaction): + logger.info("creating app_client") + async with AsyncClient( + transport=ASGITransport(app=app_no_transaction), base_url="http://test" + ) as c: + yield c + + +@pytest.fixture(scope="function") +async def default_app(database, monkeypatch): + """Test default stac-fastapi-pgstac application.""" + monkeypatch.setenv("POSTGRES_USER", database.user) + monkeypatch.setenv("POSTGRES_PASS", database.password) + monkeypatch.setenv("POSTGRES_HOST_READER", database.host) + monkeypatch.setenv("POSTGRES_HOST_WRITER", database.host) + monkeypatch.setenv("POSTGRES_PORT", str(database.port)) + monkeypatch.setenv("POSTGRES_DBNAME", database.dbname) + monkeypatch.delenv("ENABLED_EXTENSIONS", raising=False) + + monkeypatch.setenv("ENABLE_TRANSACTIONS_EXTENSIONS", "TRUE") + monkeypatch.setenv("USE_API_HYDRATE", "TRUE") + monkeypatch.setenv("ENABLE_RESPONSE_MODELS", "TRUE") + + from stac_fastapi.pgstac.app import app + + await connect_to_db(app) + yield app + await close_db_connection(app) + + +@pytest.fixture(scope="function") +async def default_client(default_app): + async with AsyncClient( + transport=ASGITransport(app=default_app), base_url="http://test" + ) as c: + yield c