From 01446eba8b7ed35a5ea23d0247948c029d9f245c Mon Sep 17 00:00:00 2001 From: hrodmn Date: Mon, 28 Apr 2025 10:02:22 -0500 Subject: [PATCH 1/7] disable transaction and bulk_transactions extensions by default resolves #231 --- CHANGES.md | 2 ++ stac_fastapi/pgstac/app.py | 4 +++- tests/api/test_api.py | 11 ++++++++++ tests/conftest.py | 43 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5668356..296b4bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Disable transaction and bulk_transactions extensions by default **breaking change** + ## [5.0.2] - 2025-04-07 ### Fixed diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index fcc1288..5ee54b1 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -101,10 +101,12 @@ "collection_search", } +disabled_extensions = {"transaction", "bulk_transactions"} + enabled_extensions = ( os.environ["ENABLED_EXTENSIONS"].split(",") if "ENABLED_EXTENSIONS" in os.environ - else known_extensions + else known_extensions - disabled_extensions ) application_extensions = [ diff --git a/tests/api/test_api.py b/tests/api/test_api.py index e4ce8d0..e4aeb62 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -68,6 +68,17 @@ ) +async def test_default_app_no_transactions( + default_app_client, load_test_data, load_test_collection +): + coll = load_test_collection + item = load_test_data("test_item.json") + resp = await default_app_client.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) diff --git a/tests/conftest.py b/tests/conftest.py index 7944e8d..d7ea947 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,6 +41,7 @@ from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_pydantic import Collection, Item +from stac_fastapi.pgstac.app import api as default_api from stac_fastapi.pgstac.config import PostgresSettings, Settings from stac_fastapi.pgstac.core import CoreCrudClient from stac_fastapi.pgstac.db import close_db_connection, connect_to_db @@ -334,3 +335,45 @@ 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 default_client_app(): + api_settings = Settings( + testing=True, + ) + api = default_api + api.settings = api_settings + + return api + + +@pytest.fixture(scope="function") +async def default_app(default_client_app, database): + 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() + app = default_client_app.app + await connect_to_db(app, postgres_settings=postgres_settings) + + yield app + + await close_db_connection(app) + + logger.info("Closed Pools.") + + +@pytest.fixture(scope="function") +async def default_app_client(default_app): + logger.info("creating app_client") + async with AsyncClient( + transport=ASGITransport(app=default_app), base_url="http://test" + ) as c: + yield c From 4d07c6e81ed9da20b5a130aaa9d7dcd1dfc3e823 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Mon, 28 Apr 2025 10:21:36 -0500 Subject: [PATCH 2/7] enable all extensions in cicd --- .github/workflows/cicd.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index fcb9393..37bb5aa 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 + ENABLED_EXTENSIONS: transaction,bulk_transactions,query,sort,fields,filter,pagination,collection_search test-docs: runs-on: ubuntu-latest From fbfa77446826970dc6a6267d47cbb1085ac0d1f3 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 30 Apr 2025 10:15:46 -0500 Subject: [PATCH 3/7] apply suggestions from review Co-authored-by: Vincent Sarago --- docker-compose.yml | 1 + stac_fastapi/pgstac/app.py | 34 +++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 725a52e..0186bb7 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} + - ENABLED_EXTENSIONS=transaction,bulk_transactions,query,sort,fields,filter,pagination,collection_search ports: - "8082:8082" volumes: diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 5ee54b1..ce61b57 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -49,8 +49,8 @@ settings = Settings() -# application extensions -application_extensions_map = { +# transaction extensions +transaction_extensions_map = { "transaction": TransactionExtension( client=TransactionsClient(), settings=settings, @@ -93,27 +93,31 @@ "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", } -disabled_extensions = {"transaction", "bulk_transactions"} +if ext := os.environ.get("ENABLED_EXTENSIONS"): + enabled_extensions = set(ext.split(",")) -enabled_extensions = ( - os.environ["ENABLED_EXTENSIONS"].split(",") - if "ENABLED_EXTENSIONS" in os.environ - else known_extensions - disabled_extensions -) +application_extensions = [] -application_extensions = [ - extension - for key, extension in application_extensions_map.items() - if key in enabled_extensions -] +if "transaction" in enabled_extensions: + application_extensions.append( + TransactionExtension( + client=TransactionsClient(), + settings=settings, + response_class=ORJSONResponse, + ), + ) + +if "bulk_transactions" in enabled_extensions: + application_extensions.append( + BulkTransactionExtension(client=BulkTransactionsClient()), + ) # /search models search_extensions = [ From 074dc4554379018d7231b6cfb6d35dd58b817298 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 12 May 2025 18:33:12 +0200 Subject: [PATCH 4/7] refactor fixtures --- tests/conftest.py | 57 +++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 047da05..18298bf 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, @@ -41,7 +41,6 @@ from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_pydantic import Collection, Item -from stac_fastapi.pgstac.app import api as default_api from stac_fastapi.pgstac.config import PostgresSettings, Settings from stac_fastapi.pgstac.core import CoreCrudClient, health_check from stac_fastapi.pgstac.db import close_db_connection, connect_to_db @@ -190,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, ) @@ -291,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) @@ -307,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, @@ -320,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.") @@ -340,18 +330,16 @@ async def app_client_no_ext(app_no_ext): @pytest.fixture(scope="function") -async def default_client_app(): - api_settings = Settings( - testing=True, +async def default_app(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, ) - api = default_api - api.settings = api_settings - - return api - -@pytest.fixture(scope="function") -async def default_app(default_client_app, database): postgres_settings = PostgresSettings( postgres_user=database.user, postgres_pass=database.password, @@ -362,12 +350,9 @@ async def default_app(default_client_app, database): ) logger.info("Creating app Fixture") time.time() - app = default_client_app.app - await connect_to_db(app, postgres_settings=postgres_settings) - - yield app - - await close_db_connection(app) + await connect_to_db(api.app, postgres_settings=postgres_settings) + yield api.app + await close_db_connection(api.app) logger.info("Closed Pools.") From a6c501fae468c66b7dd980920ccebc3e3acfab4a Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 12 May 2025 23:22:56 +0200 Subject: [PATCH 5/7] remove unused --- stac_fastapi/pgstac/app.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 2d037d4..8b8861d 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -49,16 +49,6 @@ settings = Settings() -# transaction extensions -transaction_extensions_map = { - "transaction": TransactionExtension( - client=TransactionsClient(), - settings=settings, - response_class=JSONResponse, - ), - "bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()), -} - # search extensions search_extensions_map = { "query": QueryExtension(), @@ -105,6 +95,7 @@ application_extensions = [] +# transaction extensions if "transaction" in enabled_extensions: application_extensions.append( TransactionExtension( From 3749c124486962e6771419d3d51163bc9b9cc495 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Tue, 13 May 2025 14:05:17 +0200 Subject: [PATCH 6/7] use specific ENV ENABLE_TRANSACTIONS_EXTENSIONS for transactions (#239) * use specific ENV ENABLE_TRANSACTIONS_EXTENSIONS for transactions * Update docker-compose.yml Co-authored-by: Henry Rodman --------- Co-authored-by: Henry Rodman --- .github/workflows/cicd.yaml | 2 +- docker-compose.yml | 2 +- stac_fastapi/pgstac/app.py | 4 +--- tests/api/test_api.py | 43 +++++++++++++++++++++++++++++++++++-- tests/conftest.py | 34 +++++++++++++++++++++++++++-- 5 files changed, 76 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 37bb5aa..17162af 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -101,7 +101,7 @@ jobs: PGDATABASE: postgis APP_HOST: 0.0.0.0 APP_PORT: 8080 - ENABLED_EXTENSIONS: transaction,bulk_transactions,query,sort,fields,filter,pagination,collection_search + ENABLE_TRANSACTIONS_EXTENSIONS: TRUE test-docs: runs-on: ubuntu-latest diff --git a/docker-compose.yml b/docker-compose.yml index 0186bb7..962aa2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - DB_MIN_CONN_SIZE=1 - DB_MAX_CONN_SIZE=1 - USE_API_HYDRATE=${USE_API_HYDRATE:-false} - - ENABLED_EXTENSIONS=transaction,bulk_transactions,query,sort,fields,filter,pagination,collection_search + - ENABLE_TRANSACTIONS_EXTENSIONS=TRUE ports: - "8082:8082" volumes: diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 8b8861d..d19a71e 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -95,8 +95,7 @@ application_extensions = [] -# transaction extensions -if "transaction" in enabled_extensions: +if os.environ.get("ENABLE_TRANSACTIONS_EXTENSIONS", "").lower() in ["yes", "true", "1"]: application_extensions.append( TransactionExtension( client=TransactionsClient(), @@ -105,7 +104,6 @@ ), ) -if "bulk_transactions" in enabled_extensions: application_extensions.append( BulkTransactionExtension(client=BulkTransactionsClient()), ) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index e4aeb62..9185a79 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -69,11 +69,13 @@ async def test_default_app_no_transactions( - default_app_client, load_test_data, load_test_collection + app_client_no_transaction, load_test_data, load_test_collection ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await default_app_client.post(f"/collections/{coll['id']}/items", json=item) + 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 @@ -903,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 18298bf..397ecec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -330,7 +330,7 @@ async def app_client_no_ext(app_no_ext): @pytest.fixture(scope="function") -async def default_app(database): +async def app_no_transaction(database): """Default stac-fastapi-pgstac application without any extensions.""" api_settings = Settings(testing=True) api = StacApi( @@ -358,8 +358,38 @@ async def default_app(database): @pytest.fixture(scope="function") -async def default_app_client(default_app): +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: From 8d92252077543bae8c70f4c760d0f2b3b5bd18bd Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 13 May 2025 15:06:27 +0200 Subject: [PATCH 7/7] update changelog --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 17734a3..bdc2ea6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,8 @@ ### Changed -- Disable transaction and bulk_transactions extensions by default **breaking change** +- 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`