diff --git a/CHANGES.md b/CHANGES.md index 6d598257..eaad2c09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Changed + +- remove pygstac dependency +- refactor tests fixtures to test multiple version of PgSTAC + ## [6.1.1] - 2025-11-20 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index e1ae7dde..1afe8667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ dependencies = [ "buildpg", "brotli_asgi", "cql2>=0.3.6", - "pypgstac>=0.9,<0.10", "hydraters>=0.1.3", "typing_extensions>=4.9.0", "jsonpatch>=1.33.0", @@ -69,11 +68,12 @@ dev = [ "pytest", "pytest-cov", "pytest-asyncio>=0.17,<1.3", - "pre-commit", + "pypgstac>=0.9,<0.10", "requests", "shapely", "httpx", "psycopg[pool,binary]==3.2.*", + "pre-commit", "bump-my-version", ] docs = [ diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index ad5864d7..3d01fb7a 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -11,8 +11,8 @@ from buildpg import render from cql2 import Expr from fastapi import HTTPException, Request +from hydraters import hydrate from pydantic import ValidationError -from pypgstac.hydration import hydrate from stac_fastapi.api.models import JSONResponse from stac_fastapi.types.core import AsyncBaseCoreClient, Relations from stac_fastapi.types.errors import InvalidQueryParameter, NotFoundError diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 7f67e28d..0c9057dc 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -7,8 +7,6 @@ import pytest from fastapi import Request from httpx import ASGITransport, AsyncClient -from pypgstac.db import PgstacDB -from pypgstac.load import Loader from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model @@ -701,7 +699,7 @@ async def search(query: Dict[str, Any]) -> List[Item]: @pytest.mark.asyncio -async def test_wrapped_function(load_test_data, database) -> None: +async def test_wrapped_function(load_test_data, pgstac) -> None: # Ensure wrappers, e.g. Planetary Computer's rate limiting, work. # https://github.com/gadomski/planetary-computer-apis/blob/2719ccf6ead3e06de0784c39a2918d4d1811368b/pccommon/pccommon/redis.py#L205-L238 @@ -740,11 +738,11 @@ async def get_collection( ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) extensions = [ @@ -797,29 +795,23 @@ async def get_collection( @pytest.mark.asyncio @pytest.mark.parametrize("validation", [True, False]) @pytest.mark.parametrize("hydrate", [True, False]) -async def test_no_extension( - hydrate, validation, load_test_data, database, pgstac -) -> None: +async def test_no_extension(hydrate, validation, load_test_data, pgstac) -> None: """test PgSTAC with no extension.""" - connection = f"postgresql://{database.user}:{quote_plus(database.password)}@{database.host}:{database.port}/{database.dbname}" - with PgstacDB(dsn=connection) as db: - loader = Loader(db=db) - loader.load_collections(os.path.join(DATA_DIR, "test_collection.json")) - loader.load_items(os.path.join(DATA_DIR, "test_item.json")) - settings = Settings( testing=True, use_api_hydrate=hydrate, enable_response_models=validation, ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) - extensions = [] + extensions = [ + TransactionExtension(client=TransactionsClient(), settings=settings), + ] post_request_model = create_post_request_model(extensions, base_model=PgstacSearch) api = StacApi( client=CoreCrudClient(pgstac_search_model=post_request_model), @@ -835,6 +827,17 @@ async def test_no_extension( ) try: async with AsyncClient(transport=ASGITransport(app=app)) as client: + response = await client.post( + "http://test/collections", + json=load_test_data("test_collection.json"), + ) + assert response.status_code == 201 + response = await client.post( + "http://test/collections/test-collection/items", + json=load_test_data("test_item.json"), + ) + assert response.status_code == 201 + landing = await client.get("http://test/") assert landing.status_code == 200, landing.text assert "Queryables" not in [ diff --git a/tests/api/test_links_with_root_path.py b/tests/api/test_links_with_root_path.py index 3418ab9f..33bfd746 100644 --- a/tests/api/test_links_with_root_path.py +++ b/tests/api/test_links_with_root_path.py @@ -10,18 +10,18 @@ @pytest.fixture(scope="function") -async def app_with_root_path(database, monkeypatch): +async def app_with_root_path(pgstac, monkeypatch): """ Provides the global stac_fastapi.pgstac.app.app instance, configured with a specific ROOT_PATH environment variable and connected to the test database. """ monkeypatch.setenv("ROOT_PATH", ROOT_PATH) - monkeypatch.setenv("PGUSER", database.user) - monkeypatch.setenv("PGPASSWORD", database.password) - monkeypatch.setenv("PGHOST", database.host) - monkeypatch.setenv("PGPORT", str(database.port)) - monkeypatch.setenv("PGDATABASE", database.dbname) + monkeypatch.setenv("PGUSER", pgstac.user) + monkeypatch.setenv("PGPASSWORD", pgstac.password) + monkeypatch.setenv("PGHOST", pgstac.host) + monkeypatch.setenv("PGPORT", str(pgstac.port)) + monkeypatch.setenv("PGDATABASE", pgstac.dbname) monkeypatch.setenv("ENABLE_TRANSACTIONS_EXTENSIONS", "TRUE") # Reload the app module to pick up the new environment variables diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py index cf1bf141..ccd9e51f 100644 --- a/tests/clients/test_postgres.py +++ b/tests/clients/test_postgres.py @@ -539,13 +539,13 @@ async def test_create_bulk_items_id_mismatch( # assert item.collection == coll.id -async def test_db_setup_works_with_env_vars(api_client, database, monkeypatch): +async def test_db_setup_works_with_env_vars(api_client, pgstac, monkeypatch): """Test that the application starts successfully if the POSTGRES_* environment variables are set""" - monkeypatch.setenv("PGUSER", database.user) - monkeypatch.setenv("PGPASSWORD", database.password) - monkeypatch.setenv("PGHOST", database.host) - monkeypatch.setenv("PGPORT", str(database.port)) - monkeypatch.setenv("PGDATABASE", database.dbname) + monkeypatch.setenv("PGUSER", pgstac.user) + monkeypatch.setenv("PGPASSWORD", pgstac.password) + monkeypatch.setenv("PGHOST", pgstac.host) + monkeypatch.setenv("PGPORT", str(pgstac.port)) + monkeypatch.setenv("PGDATABASE", pgstac.dbname) await connect_to_db(api_client.app) await close_db_connection(api_client.app) @@ -573,16 +573,16 @@ async def custom_get_connection( class TestDbConnect: @pytest.fixture - async def app(self, api_client, database): + async def app(self, api_client, pgstac): """ app fixture override to setup app with a customized db connection getter """ postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.debug("Customizing app setup") diff --git a/tests/conftest.py b/tests/conftest.py index 4a71d05a..f4b42f39 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ from urllib.parse import quote_plus as quote from urllib.parse import urljoin -import asyncpg +import psycopg import pytest from fastapi import APIRouter from httpx import ASGITransport, AsyncClient @@ -63,12 +63,6 @@ def database(postgresql_proc): version=postgresql_proc.version, password="a2Vw:yk=)CdSis[fek]tW=/o", ) as jan: - connection = f"postgresql://{jan.user}:{quote(jan.password)}@{jan.host}:{jan.port}/{jan.dbname}" - with PgstacDB(dsn=connection) as db: - migrator = Migrate(db) - version = migrator.run_migration() - assert version - yield jan @@ -77,24 +71,25 @@ def database(postgresql_proc): "0.8.6", "0.9.8", ], - autouse=True, ) -async def pgstac(database): +def pgstac(request, database): + pgstac_version = request.param + connection = f"postgresql://{database.user}:{quote(database.password)}@{database.host}:{database.port}/{database.dbname}" - yield - conn = await asyncpg.connect(dsn=connection) - await conn.execute( - """ - DROP SCHEMA IF EXISTS pgstac CASCADE; - """ - ) - await conn.close() + # Clear PgSTAC + with psycopg.connect(connection) as conn: + with conn.cursor() as cur: + cur.execute("DROP SCHEMA IF EXISTS pgstac CASCADE;") + with PgstacDB(dsn=connection) as db: migrator = Migrate(db) - version = migrator.run_migration() + version = migrator.run_migration(toversion=pgstac_version) + assert version == request.param logger.info(f"PGStac Migrated to {version}") + yield database + # Run all the tests that use the api_client in both db hydrate and api hydrate mode @pytest.fixture( @@ -198,13 +193,13 @@ def api_client(request): @pytest.fixture(scope="function") -async def app(api_client, database): +async def app(api_client, pgstac): postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.info("Creating app Fixture") app = api_client.app @@ -293,7 +288,7 @@ async def load_test2_item(app_client, load_test_data, load_test2_collection): @pytest.fixture(scope="function") -async def app_no_ext(database): +async def app_no_ext(pgstac): """Default stac-fastapi-pgstac application without only the transaction extensions.""" api_settings = Settings(testing=True) api_client_no_ext = StacApi( @@ -306,11 +301,11 @@ async def app_no_ext(database): ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.info("Creating app Fixture") await connect_to_db( @@ -334,7 +329,7 @@ async def app_client_no_ext(app_no_ext): @pytest.fixture(scope="function") -async def app_no_transaction(database): +async def app_no_transaction(pgstac): """Default stac-fastapi-pgstac application without any extensions.""" api_settings = Settings(testing=True) api = StacApi( @@ -345,11 +340,11 @@ async def app_no_transaction(database): ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.info("Creating app Fixture") await connect_to_db( @@ -373,13 +368,13 @@ async def app_client_no_transaction(app_no_transaction): @pytest.fixture(scope="function") -async def default_app(database, monkeypatch): +async def default_app(pgstac, monkeypatch): """Test default stac-fastapi-pgstac application.""" - monkeypatch.setenv("PGUSER", database.user) - monkeypatch.setenv("PGPASSWORD", database.password) - monkeypatch.setenv("PGHOST", database.host) - monkeypatch.setenv("PGPORT", str(database.port)) - monkeypatch.setenv("PGDATABASE", database.dbname) + monkeypatch.setenv("PGUSER", pgstac.user) + monkeypatch.setenv("PGPASSWORD", pgstac.password) + monkeypatch.setenv("PGHOST", pgstac.host) + monkeypatch.setenv("PGPORT", str(pgstac.port)) + monkeypatch.setenv("PGDATABASE", pgstac.dbname) monkeypatch.delenv("ENABLED_EXTENSIONS", raising=False) monkeypatch.setenv("ENABLE_TRANSACTIONS_EXTENSIONS", "TRUE") @@ -402,7 +397,7 @@ async def default_client(default_app): @pytest.fixture(scope="function") -async def app_advanced_freetext(database): +async def app_advanced_freetext(pgstac): """Default stac-fastapi-pgstac application without only the transaction extensions.""" api_settings = Settings(testing=True) @@ -428,11 +423,11 @@ async def app_advanced_freetext(database): ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.info("Creating app Fixture") await connect_to_db( @@ -456,7 +451,7 @@ async def app_client_advanced_freetext(app_advanced_freetext): @pytest.fixture(scope="function") -async def app_transaction_validation_ext(database): +async def app_transaction_validation_ext(pgstac): """Default stac-fastapi-pgstac application with extension validation in transaction.""" api_settings = Settings(testing=True, validate_extensions=True) api = StacApi( @@ -472,11 +467,11 @@ async def app_transaction_validation_ext(database): ) postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) logger.info("Creating app Fixture") await connect_to_db( diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index 049ff823..504065cb 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -354,7 +354,7 @@ async def test_collection_search_freetext( res = await app_client.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") # free-text resp = await app_client.get( @@ -397,7 +397,7 @@ async def test_collection_search_freetext_advanced( res = await app_client_advanced_freetext.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") # free-text resp = await app_client_advanced_freetext.get( @@ -447,7 +447,7 @@ async def test_all_collections_with_pagination(app_client, load_test_data): res = await app_client.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") data = load_test_data("test_collection.json") collection_id = data["id"] @@ -483,7 +483,7 @@ async def test_all_collections_without_pagination(app_client_no_ext, load_test_d res = await app_client_no_ext.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") data = load_test_data("test_collection.json") collection_id = data["id"] @@ -512,7 +512,7 @@ async def test_get_collections_search_pagination( res = await app_client.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") resp = await app_client.get("/collections") assert resp.json()["numberReturned"] == 2 @@ -647,7 +647,7 @@ async def test_get_collections_search_offset_1( res = await app_client.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") # BUG: pgstac doesn't return a `prev` link when limit is not set # offset=1, should have a `previous` link diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index b7795fba..1e9f8957 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -1696,7 +1696,7 @@ async def test_item_search_freetext(app_client, load_test_data, load_test_collec res = await app_client.get("/_mgmt/health") pgstac_version = res.json()["pgstac"]["pgstac_version"] if tuple(map(int, pgstac_version.split("."))) < (0, 9, 2): - pass + pytest.skip("Need PgSTAC > 0.9.2") test_item = load_test_data("test_item.json") resp = await app_client.post( diff --git a/tests/resources/test_mgmt.py b/tests/resources/test_mgmt.py index e2bd9c05..232b1bba 100644 --- a/tests/resources/test_mgmt.py +++ b/tests/resources/test_mgmt.py @@ -33,7 +33,7 @@ async def test_health(app_client): assert body["pgstac"]["pgstac_version"] -async def test_health_503(database): +async def test_health_503(pgstac): """Test health endpoint error.""" # No lifespan so no `get_connection` is application state @@ -58,11 +58,11 @@ async def test_health_503(database): # No lifespan so no `get_connection` is application state postgres_settings = PostgresSettings( - pguser=database.user, - pgpassword=database.password, - pghost=database.host, - pgport=database.port, - pgdatabase=database.dbname, + pguser=pgstac.user, + pgpassword=pgstac.password, + pghost=pgstac.host, + pgport=pgstac.port, + pgdatabase=pgstac.dbname, ) # Create connection pool but close it just after await connect_to_db(api.app, postgres_settings=postgres_settings) diff --git a/uv.lock b/uv.lock index cdb703e1..59768d9b 100644 --- a/uv.lock +++ b/uv.lock @@ -4043,7 +4043,6 @@ dependencies = [ { name = "jsonpatch" }, { name = "orjson" }, { name = "pydantic" }, - { name = "pypgstac" }, { name = "stac-fastapi-api" }, { name = "stac-fastapi-extensions" }, { name = "stac-fastapi-types" }, @@ -4066,6 +4065,7 @@ dev = [ { name = "mirakuru" }, { name = "pre-commit" }, { name = "psycopg", extra = ["binary", "pool"] }, + { name = "pypgstac" }, { name = "pystac", version = "1.10.1", source = { registry = "https://pypi.org/simple" }, extra = ["validation"], marker = "python_full_version < '3.10'" }, { name = "pystac", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, extra = ["validation"], marker = "python_full_version >= '3.10'" }, { name = "pytest" }, @@ -4097,7 +4097,6 @@ requires-dist = [ { name = "jsonpatch", specifier = ">=1.33.0" }, { name = "orjson" }, { name = "pydantic" }, - { name = "pypgstac", specifier = ">=0.9,<0.10" }, { name = "stac-fastapi-api", specifier = ">=6.1,<7.0" }, { name = "stac-fastapi-extensions", specifier = ">=6.1,<7.0" }, { name = "stac-fastapi-types", specifier = ">=6.1,<7.0" }, @@ -4115,6 +4114,7 @@ dev = [ { name = "mirakuru", specifier = ">=2.0,<3.0" }, { name = "pre-commit" }, { name = "psycopg", extras = ["pool", "binary"], specifier = "==3.2.*" }, + { name = "pypgstac", specifier = ">=0.9,<0.10" }, { name = "pystac", extras = ["validation"] }, { name = "pytest" }, { name = "pytest-asyncio", specifier = ">=0.17,<1.3" },