From 1260543f3bf113741043307435f1519178c9609e Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 26 Apr 2024 22:02:01 +0200 Subject: [PATCH 01/10] update stac-fastapi to next major version --- docker-compose.yml | 2 +- setup.py | 18 +++++++++++------- stac_fastapi/pgstac/config.py | 10 +++++----- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bf79b65c..8b9f4e6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,7 @@ services: database: container_name: stac-db - image: ghcr.io/stac-utils/pgstac:v0.7.10 + image: ghcr.io/stac-utils/pgstac:v0.8.5 environment: - POSTGRES_USER=username - POSTGRES_PASSWORD=password diff --git a/setup.py b/setup.py index 3110150e..0d32ade5 100644 --- a/setup.py +++ b/setup.py @@ -8,22 +8,26 @@ install_requires = [ "attrs", "orjson", - "pydantic[dotenv]>=1.10.8", # https://github.com/pydantic/pydantic/issues/5821 - "stac_pydantic==2.0.*", - "stac-fastapi.types~=2.5.5.post1", - "stac-fastapi.api~=2.5.5.post1", - "stac-fastapi.extensions~=2.5.5.post1", + "pydantic", + "stac_pydantic==3.0.*", + # "stac-fastapi.api~=3.0", + # "stac-fastapi.extensions~=3.0", + # "stac-fastapi.types~=3.0", + # For now we use latest commit in master + "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", + "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", + "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", "asyncpg", "buildpg", "brotli_asgi", "pygeofilter>=0.2", - "pypgstac==0.7.*", + "pypgstac==0.8.*", ] extra_reqs = { "dev": [ "pystac[validation]", - "pypgstac[psycopg]==0.7.*", + "pypgstac[psycopg]==0.8.*", "pytest-postgresql", "pytest", "pytest-cov", diff --git a/stac_fastapi/pgstac/config.py b/stac_fastapi/pgstac/config.py index 5fe95822..d45e6ebc 100644 --- a/stac_fastapi/pgstac/config.py +++ b/stac_fastapi/pgstac/config.py @@ -4,6 +4,7 @@ from urllib.parse import quote from pydantic import BaseModel, Extra +from pydantic_settings import SettingsConfigDict from stac_fastapi.types.config import ApiSettings from stac_fastapi.pgstac.types.base_item_cache import ( @@ -58,7 +59,7 @@ class Settings(ApiSettings): postgres_pass: str postgres_host_reader: str postgres_host_writer: str - postgres_port: str + postgres_port: int postgres_dbname: str db_min_conn_size: int = 10 @@ -89,7 +90,6 @@ def testing_connection_string(self): """Create testing psql connection string.""" return f"postgresql://{self.postgres_user}:{quote(self.postgres_pass)}@{self.postgres_host_writer}:{self.postgres_port}/pgstactestdb" - class Config(ApiSettings.Config): - """Model config.""" - - env_nested_delimiter = "__" + model_config = SettingsConfigDict( + **{**ApiSettings.model_config, **{"env_nested_delimiter": "__"}} + ) From 4bf1a401fa4fc50cd700b4dd01cf6af27fb357a7 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Sat, 27 Apr 2024 00:19:45 +0200 Subject: [PATCH 02/10] update transaction endpoint --- stac_fastapi/pgstac/core.py | 4 +++ stac_fastapi/pgstac/extensions/query.py | 2 +- stac_fastapi/pgstac/transactions.py | 48 +++++++++++++++++++------ tests/clients/test_postgres.py | 6 ++-- tests/conftest.py | 4 +-- tests/data/test_item.json | 4 +-- 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 6301c43e..54a10540 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -226,6 +226,10 @@ async def _get_base_item(collection_id: str) -> Dict[str, Any]: for feature in collection.get("features") or []: base_item = await base_item_cache.get(feature.get("collection")) + + # Exclude None values + base_item = {k: v for k, v in base_item.items() if v is not None} + feature = hydrate(base_item, feature) # Grab ids needed for links that may be removed by the fields extension. diff --git a/stac_fastapi/pgstac/extensions/query.py b/stac_fastapi/pgstac/extensions/query.py index 3d5f7500..f035cda0 100644 --- a/stac_fastapi/pgstac/extensions/query.py +++ b/stac_fastapi/pgstac/extensions/query.py @@ -34,7 +34,7 @@ def operator(self) -> Callable[[Any, Any], bool]: class QueryExtensionPostRequest(BaseModel): """Query Extension POST request model.""" - query: Optional[Dict[str, Dict[Operator, Any]]] + query: Optional[Dict[str, Dict[Operator, Any]]] = None class QueryExtension(QueryExtensionBase): diff --git a/stac_fastapi/pgstac/transactions.py b/stac_fastapi/pgstac/transactions.py index 3687068a..a16f2dd8 100644 --- a/stac_fastapi/pgstac/transactions.py +++ b/stac_fastapi/pgstac/transactions.py @@ -14,6 +14,7 @@ ) from stac_fastapi.types import stac as stac_types from stac_fastapi.types.core import AsyncBaseTransactionsClient +from stac_pydantic import Collection, Item, ItemCollection from starlette.responses import JSONResponse, Response from stac_fastapi.pgstac.config import Settings @@ -69,11 +70,13 @@ def _validate_item( async def create_item( self, collection_id: str, - item: Union[stac_types.Item, stac_types.ItemCollection], + item: Union[Item, ItemCollection], request: Request, **kwargs, ) -> Optional[Union[stac_types.Item, Response]]: """Create item.""" + item = item.model_dump(mode="json") + if item["type"] == "FeatureCollection": valid_items = [] for item in item["features"]: # noqa: B020 @@ -100,6 +103,7 @@ async def create_item( ).get_links(extra_links=item.get("links")) return stac_types.Item(**item) + else: raise HTTPException( status_code=400, @@ -111,10 +115,12 @@ async def update_item( request: Request, collection_id: str, item_id: str, - item: stac_types.Item, + item: Item, **kwargs, ) -> Optional[Union[stac_types.Item, Response]]: """Update item.""" + item = item.model_dump(mode="json") + self._validate_item(request, item, collection_id, item_id) item["collection"] = collection_id @@ -130,31 +136,49 @@ async def update_item( return stac_types.Item(**item) async def create_collection( - self, collection: stac_types.Collection, request: Request, **kwargs + self, + collection: Collection, + request: Request, + **kwargs, ) -> Optional[Union[stac_types.Collection, Response]]: """Create collection.""" + collection = collection.model_dump(mode="json") + 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( collection_id=collection["id"], request=request - ).get_links(extra_links=collection.get("links")) + ).get_links(extra_links=collection["links"]) return stac_types.Collection(**collection) async def update_collection( - self, collection: stac_types.Collection, request: Request, **kwargs + self, + collection: Collection, + request: Request, + **kwargs, ) -> Optional[Union[stac_types.Collection, Response]]: """Update collection.""" + col = collection.model_dump(mode="json") + async with request.app.state.get_connection(request, "w") as conn: - await dbfunc(conn, "update_collection", collection) - collection["links"] = await CollectionLinks( - collection_id=collection["id"], request=request - ).get_links(extra_links=collection.get("links")) - return stac_types.Collection(**collection) + await dbfunc(conn, "update_collection", col) + + col["links"] = await CollectionLinks( + collection_id=col["id"], request=request + ).get_links(extra_links=col.get("links")) + + return stac_types.Collection(**col) async def delete_item( - self, item_id: str, collection_id: str, request: Request, **kwargs + self, + item_id: str, + collection_id: str, + request: Request, + **kwargs, ) -> Optional[Union[stac_types.Item, Response]]: """Delete item.""" q, p = render( @@ -164,6 +188,7 @@ async def delete_item( ) async with request.app.state.get_connection(request, "w") as conn: await conn.fetchval(q, *p) + return JSONResponse({"deleted item": item_id}) async def delete_collection( @@ -172,6 +197,7 @@ async def delete_collection( """Delete collection.""" async with request.app.state.get_connection(request, "w") as conn: await dbfunc(conn, "delete_collection", collection_id) + return JSONResponse({"deleted collection": collection_id}) diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py index 0de8f4bb..94785579 100644 --- a/tests/clients/test_postgres.py +++ b/tests/clients/test_postgres.py @@ -21,9 +21,10 @@ async def test_create_collection(app_client, load_test_data: Callable): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 post_coll = Collection.parse_obj(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) + resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 get_coll = Collection.parse_obj(resp.json()) @@ -66,6 +67,7 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle assert resp.status_code == 200 post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) + resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) @@ -86,7 +88,7 @@ async def test_create_item_no_collection_id( json=item, ) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.get(f"/collections/{coll.id}/items/{item['id']}") diff --git a/tests/conftest.py b/tests/conftest.py index b8786c18..0c5e0b74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -186,7 +186,7 @@ async def load_test_collection(app_client, load_test_data): "/collections", json=data, ) - assert resp.status_code == 200 + assert resp.status_code == 201 return Collection.parse_obj(resp.json()) @@ -198,7 +198,7 @@ async def load_test_item(app_client, load_test_data, load_test_collection): f"/collections/{coll.id}/items", json=data, ) - assert resp.status_code == 200 + assert resp.status_code == 201 return Item.parse_obj(resp.json()) diff --git a/tests/data/test_item.json b/tests/data/test_item.json index a6e85a00..1c68b959 100644 --- a/tests/data/test_item.json +++ b/tests/data/test_item.json @@ -504,7 +504,7 @@ { "href": "preview.html", "rel": "preview", - "type": "application/html" + "type": "text/html" } ] -} \ No newline at end of file +} From 069b3ead9f4e0a67f84488414fb0d49dbc62e1d5 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Tue, 30 Apr 2024 00:58:34 +0800 Subject: [PATCH 03/10] fix 201 errors --- tests/api/test_api.py | 42 ++++++------ tests/resources/test_collection.py | 8 +-- tests/resources/test_item.py | 100 ++++++++++++++--------------- 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 02460ce8..71449a41 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -142,14 +142,14 @@ async def test_app_transaction_extension( coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 async def test_app_query_extension(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"query": {"proj:epsg": {"eq": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) @@ -170,7 +170,7 @@ async def test_app_query_extension_limit_1( coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"limit": 1} resp = await app_client.post("/search", json=params) @@ -191,7 +191,7 @@ async def test_app_query_extension_limit_lt0( coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"limit": -1} resp = await app_client.post("/search", json=params) @@ -204,7 +204,7 @@ async def test_app_query_extension_limit_gt10000( coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"limit": 10001} resp = await app_client.post("/search", json=params) @@ -215,7 +215,7 @@ async def test_app_query_extension_gt(load_test_data, app_client, load_test_coll coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gt": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) @@ -228,7 +228,7 @@ async def test_app_query_extension_gte(load_test_data, app_client, load_test_col coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gte": item["properties"]["proj:epsg"]}}} resp = await app_client.post("/search", json=params) @@ -244,7 +244,7 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" ) resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) - assert resp.status_code == 200 + assert resp.status_code == 201 second_item = load_test_data("test_item.json") second_item["id"] = "another-item" @@ -253,7 +253,7 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti "%Y-%m-%dT%H:%M:%SZ" ) resp = await app_client.post(f"/collections/{coll.id}/items", json=second_item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "collections": [coll.id], @@ -281,7 +281,7 @@ async def test_search_invalid_date(load_test_data, app_client, load_test_collect coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "datetime": "2020-XX-01/2020-10-30", @@ -296,7 +296,7 @@ async def test_bbox_3d(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) - assert resp.status_code == 200 + assert resp.status_code == 201 australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1] params = { @@ -329,7 +329,7 @@ async def test_search_point_intersects(load_test_data, app_client, load_test_col coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 new_coordinates = [] for coordinate in item["geometry"]["coordinates"][0]: @@ -338,7 +338,7 @@ async def test_search_point_intersects(load_test_data, app_client, load_test_col item["geometry"]["coordinates"] = [new_coordinates] item["bbox"] = [value * -1 for value in item["bbox"]] resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 point = [150.04, -33.14] intersects = {"type": "Point", "coordinates": point} @@ -365,7 +365,7 @@ async def test_search_line_string_intersects( coll = load_test_collection item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 line = [[150.04, -33.14], [150.22, -33.89]] intersects = {"type": "LineString", "coordinates": line} @@ -498,7 +498,7 @@ async def test_item_collection_filter_bbox( coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) - assert resp.status_code == 200 + assert resp.status_code == 201 bbox = "100,-50,170,-20" resp = await app_client.get(f"/collections/{coll.id}/items", params={"bbox": bbox}) @@ -520,7 +520,7 @@ async def test_item_collection_filter_datetime( coll = load_test_collection first_item = load_test_data("test_item.json") resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) - assert resp.status_code == 200 + assert resp.status_code == 201 datetime_range = "2020-01-01T00:00:00.00Z/.." resp = await app_client.get( @@ -556,13 +556,13 @@ async def test_deleting_items_with_identical_ids(app_client): response = await app_client.post( "/collections", json=collection.to_dict(include_self_link=False) ) - assert response.status_code == 200 + assert response.status_code == 201 item_as_dict = item.to_dict(include_self_link=False) item_as_dict["collection"] = collection.id response = await app_client.post( f"/collections/{collection.id}/items", json=item_as_dict ) - assert response.status_code == 200 + assert response.status_code == 201 response = await app_client.get(f"/collections/{collection.id}/items") assert response.status_code == 200, response.json() assert len(response.json()["features"]) == 1 @@ -595,7 +595,7 @@ async def test_sorting_and_paging(app_client, load_test_collection, direction: s f"/collections/{collection_id}/items", json=item.to_dict(include_self_link=False, transform_hrefs=False), ) - assert response.status_code == 200 + assert response.status_code == 201 async def search(query: Dict[str, Any]) -> List[Item]: items: List[Item] = [] @@ -685,12 +685,12 @@ async def get_collection( "http://test/collections", json=load_test_data("test_collection.json"), ) - assert response.status_code == 200 + 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 == 200 + assert response.status_code == 201 response = await client.get( "http://test/collections/test-collection/items/test-item" ) diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index 6b8795b0..7b97cb4b 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -12,7 +12,7 @@ async def test_create_collection(app_client, load_test_data: Callable): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 post_coll = Collection.parse_obj(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") @@ -66,7 +66,7 @@ async def test_create_collection_conflict(app_client, load_test_data: Callable): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 Collection.parse_obj(resp.json()) resp = await app_client.post( "/collections", @@ -104,7 +104,7 @@ async def test_returns_valid_collection(app_client, load_test_data): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.get(f"/collections/{in_json['id']}") assert resp.status_code == 200 @@ -127,7 +127,7 @@ async def test_returns_valid_links_in_collections(app_client, load_test_data): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Get collection by ID resp = await app_client.get(f"/collections/{in_json['id']}") diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index 0f268839..6015f455 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -26,7 +26,7 @@ async def test_create_collection(app_client, load_test_data: Callable): "/collections", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 post_coll = Collection.parse_obj(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") @@ -71,7 +71,7 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle f"/collections/{coll.id}/items", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) @@ -117,7 +117,7 @@ async def test_fetches_valid_item( f"/collections/{coll.id}/items", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) @@ -203,7 +203,7 @@ async def test_get_collection_items(app_client, load_test_collection, load_test_ f"/collections/{coll.id}/items", content=item.json(), ) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.get( f"/collections/{coll.id}/items", @@ -224,7 +224,7 @@ async def test_create_item_conflict( f"/collections/{coll.id}/items", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.post( f"/collections/{coll.id}/items", @@ -254,7 +254,7 @@ async def test_create_item_missing_collection( item["collection"] = None resp = await app_client.post(f"/collections/{coll.id}/items", json=item) - assert resp.status_code == 200 + assert resp.status_code == 201 post_item = resp.json() assert post_item["collection"] == coll.id @@ -300,7 +300,7 @@ async def test_pagination(app_client, load_test_data, load_test_collection): item.id = item.id + str(idx) item.properties.datetime = f"2020-01-{idx:02d}T00:00:00" resp = await app_client.post(f"/collections/{coll.id}/items", json=item.dict()) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.get(f"/collections/{coll.id}/items", params={"limit": 3}) assert resp.status_code == 200 @@ -362,7 +362,7 @@ async def test_item_search_by_id_post(app_client, load_test_data, load_test_coll resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"collections": [test_item["collection"]], "ids": ids} resp = await app_client.post("/search", json=params) @@ -395,14 +395,14 @@ async def test_item_search_spatial_query_post( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "collections": [test_item["collection"]], @@ -423,14 +423,14 @@ async def test_item_search_temporal_query_post( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) @@ -454,14 +454,14 @@ async def test_item_search_temporal_window_post( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) item_date_before = item_date - timedelta(seconds=1) @@ -493,7 +493,7 @@ async def test_item_search_sort_post(app_client, load_test_data, load_test_colle resp = await app_client.post( f"/collections/{first_item['collection']}/items", json=first_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_item = load_test_data("test_item.json") second_item["id"] = "another-item" @@ -502,7 +502,7 @@ async def test_item_search_sort_post(app_client, load_test_data, load_test_colle resp = await app_client.post( f"/collections/{second_item['collection']}/items", json=second_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "collections": [first_item["collection"]], @@ -524,7 +524,7 @@ async def test_item_search_by_id_get(app_client, load_test_data, load_test_colle resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"collections": test_item["collection"], "ids": ",".join(ids)} resp = await app_client.get("/search", params=params) @@ -540,14 +540,14 @@ async def test_item_search_bbox_get(app_client, load_test_data, load_test_collec resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "collections": test_item["collection"], @@ -568,14 +568,14 @@ async def test_item_search_get_without_collections( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "bbox": ",".join([str(coord) for coord in test_item["bbox"]]), @@ -595,14 +595,14 @@ async def test_item_search_temporal_window_get( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # Add second item with a different datetime. second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) item_date_before = item_date - timedelta(seconds=1) @@ -625,7 +625,7 @@ async def test_item_search_sort_get(app_client, load_test_data, load_test_collec resp = await app_client.post( f"/collections/{first_item['collection']}/items", json=first_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_item = load_test_data("test_item.json") second_item["id"] = "another-item" @@ -634,7 +634,7 @@ async def test_item_search_sort_get(app_client, load_test_data, load_test_collec resp = await app_client.post( f"/collections/{second_item['collection']}/items", json=second_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"collections": [first_item["collection"]], "sortby": "-datetime"} resp = await app_client.get("/search", params=params) assert resp.status_code == 200 @@ -651,13 +651,13 @@ async def test_item_search_post_without_collection( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = { "bbox": test_item["bbox"], @@ -676,13 +676,13 @@ async def test_item_search_properties_jsonb( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = {"query": {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"] - 1}}} @@ -700,14 +700,14 @@ async def test_item_search_properties_field( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") second_test_item["properties"]["eo:cloud_cover"] = 5 resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"query": {"eo:cloud_cover": {"eq": 0}}} resp = await app_client.post("/search", json=params) @@ -724,13 +724,13 @@ async def test_item_search_get_query_extension( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -764,13 +764,13 @@ async def test_item_search_post_filter_extension_cql( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -814,13 +814,13 @@ async def test_item_search_post_filter_extension_cql2( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -868,13 +868,13 @@ async def test_item_search_post_filter_extension_cql2_with_query_fails( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 second_test_item = load_test_data("test_item2.json") resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=second_test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -919,7 +919,7 @@ async def test_pagination_item_collection( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 ids.append(uid) # Paginate through all 5 items with a limit of 1 (expecting 5 requests) @@ -959,7 +959,7 @@ async def test_pagination_post(app_client, load_test_data, load_test_collection) resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 ids.append(uid) # Paginate through all 5 items with a limit of 1 (expecting 5 requests) @@ -1006,7 +1006,7 @@ async def test_pagination_token_idempotent( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 ids.append(uid) page = await app_client.post( @@ -1042,7 +1042,7 @@ async def test_field_extension_get(app_client, load_test_data, load_test_collect resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 params = {"fields": "+properties.proj:epsg,+properties.gsd,+collection"} resp = await app_client.get("/search", params=params) @@ -1056,7 +1056,7 @@ async def test_field_extension_post(app_client, load_test_data, load_test_collec resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 body = { "fields": { @@ -1088,7 +1088,7 @@ async def test_field_extension_exclude_and_include( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 body = { "fields": { @@ -1110,7 +1110,7 @@ async def test_field_extension_exclude_default_includes( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 body = {"fields": {"exclude": ["geometry"]}} @@ -1269,7 +1269,7 @@ async def test_preserves_extra_link( expected_href = urljoin(str(app_client.base_url), "preview.html") resp = await app_client.post(f"/collections/{coll.id}/items", json=test_item) - assert resp.status_code == 200 + assert resp.status_code == 201 response_item = await app_client.get( f"/collections/{coll.id}/items/{test_item['id']}", @@ -1290,7 +1290,7 @@ async def test_item_search_post_filter_extension_cql_explicitlang( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -1336,7 +1336,7 @@ async def test_item_search_post_filter_extension_cql2_2( resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 # EPSG is a JSONB key params = { @@ -1424,7 +1424,7 @@ async def test_get_filter_cql2text(app_client, load_test_data, load_test_collect resp = await app_client.post( f"/collections/{test_item['collection']}/items", json=test_item ) - assert resp.status_code == 200 + assert resp.status_code == 201 epsg = test_item["properties"]["proj:epsg"] collection = test_item["collection"] From 082a8d7f1590127231af09c8762ae42f657af4c6 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Tue, 30 Apr 2024 01:22:46 +0800 Subject: [PATCH 04/10] 201 in conftest --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0c5e0b74..636f6ad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -210,7 +210,7 @@ async def load_test2_collection(app_client, load_test_data): "/collections", json=data, ) - assert resp.status_code == 200 + assert resp.status_code == 201 return Collection.parse_obj(resp.json()) @@ -222,5 +222,5 @@ async def load_test2_item(app_client, load_test_data, load_test2_collection): f"/collections/{coll.id}/items", json=data, ) - assert resp.status_code == 200 + assert resp.status_code == 201 return Item.parse_obj(resp.json()) From 9edc01a62308adf61060313e7534e40e4a18f297 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 3 May 2024 15:31:35 +0200 Subject: [PATCH 05/10] use latest stac-fastapi commit --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0d32ade5..166c33b6 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ # "stac-fastapi.extensions~=3.0", # "stac-fastapi.types~=3.0", # For now we use latest commit in master - "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", - "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", - "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@e7f82d6996af0f28574329d57f5a5e90431d66bb#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", + "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", + "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", + "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", "asyncpg", "buildpg", "brotli_asgi", From 5731518d5ab9148dcd24843e7c731d5152272954 Mon Sep 17 00:00:00 2001 From: Jonathan Healy Date: Sat, 4 May 2024 02:47:18 +0800 Subject: [PATCH 06/10] Update tests (#111) * update test item tests * test collection tests * lint * test postgres * test api * more * update item * revert * remove hack * Update stac_fastapi/pgstac/core.py Co-authored-by: Vincent Sarago * fix types --------- Co-authored-by: Vincent Sarago --- stac_fastapi/pgstac/core.py | 5 +- stac_fastapi/pgstac/transactions.py | 1 + stac_fastapi/pgstac/utils.py | 4 +- tests/api/test_api.py | 68 +++++++------- tests/clients/test_postgres.py | 89 ++++++++++--------- tests/conftest.py | 9 +- tests/resources/test_collection.py | 49 +++++++---- tests/resources/test_item.py | 132 ++++++++++++++++------------ 8 files changed, 200 insertions(+), 157 deletions(-) diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 7d853b5b..ef76d577 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -277,6 +277,9 @@ async def item_collection( # If collection does not exist, NotFoundError wil be raised await self.get_collection(collection_id, request=request) + if datetime: + datetime = format_datetime_range(datetime) + base_args = { "collections": [collection_id], "bbox": bbox, @@ -393,7 +396,7 @@ async def get_search( # noqa: C901 base_args["filter-lang"] = "cql2-json" if datetime: - base_args["datetime"] = datetime + base_args["datetime"] = format_datetime_range(datetime) if intersects: base_args["intersects"] = orjson.loads(unquote_plus(intersects)) diff --git a/stac_fastapi/pgstac/transactions.py b/stac_fastapi/pgstac/transactions.py index a16f2dd8..ed624f93 100644 --- a/stac_fastapi/pgstac/transactions.py +++ b/stac_fastapi/pgstac/transactions.py @@ -162,6 +162,7 @@ async def update_collection( **kwargs, ) -> Optional[Union[stac_types.Collection, Response]]: """Update collection.""" + col = collection.model_dump(mode="json") async with request.app.state.get_connection(request, "w") as conn: diff --git a/stac_fastapi/pgstac/utils.py b/stac_fastapi/pgstac/utils.py index 21cfec1b..bd0efffc 100644 --- a/stac_fastapi/pgstac/utils.py +++ b/stac_fastapi/pgstac/utils.py @@ -116,7 +116,7 @@ def dict_deep_update(merge_to: Dict[str, Any], merge_from: Dict[str, Any]) -> No merge_to[k] = v -def format_datetime_range(dt_range: DateTimeType) -> Union[str, Any]: +def format_datetime_range(dt_range: Union[DateTimeType, str]) -> str: """ Convert a datetime object or a tuple of datetime objects to a formatted string for datetime ranges. @@ -132,7 +132,7 @@ def format_datetime_range(dt_range: DateTimeType) -> Union[str, Any]: return dt_range.isoformat().replace("+00:00", "Z") # Handle a tuple containing datetime objects or None - if isinstance(dt_range, tuple): + elif isinstance(dt_range, tuple): start, end = dt_range # Convert start datetime to string if not None, otherwise use ".." diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 5107a695..f43db871 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -72,19 +72,19 @@ async def test_get_queryables_content_type(app_client, load_test_collection): assert resp.headers["content-type"] == "application/schema+json" coll = load_test_collection - resp = await app_client.get(f"collections/{coll.id}/queryables") + resp = await app_client.get(f"collections/{coll['id']}/queryables") assert resp.headers["content-type"] == "application/schema+json" async def test_get_features_content_type(app_client, load_test_collection): coll = load_test_collection - resp = await app_client.get(f"collections/{coll.id}/items") + resp = await app_client.get(f"collections/{coll['id']}/items") assert resp.headers["content-type"] == "application/geo+json" async def test_get_features_self_link(app_client, load_test_collection): # https://github.com/stac-utils/stac-fastapi/issues/483 - resp = await app_client.get(f"collections/{load_test_collection.id}/items") + resp = await app_client.get(f"collections/{load_test_collection['id']}/items") assert resp.status_code == 200 resp_json = resp.json() self_link = next((link for link in resp_json["links"] if link["rel"] == "self"), None) @@ -94,7 +94,7 @@ async def test_get_features_self_link(app_client, load_test_collection): async def test_get_feature_content_type(app_client, load_test_collection, load_test_item): resp = await app_client.get( - f"collections/{load_test_collection.id}/items/{load_test_item.id}" + f"collections/{load_test_collection['id']}/items/{load_test_item['id']}" ) assert resp.headers["content-type"] == "application/geo+json" @@ -141,14 +141,14 @@ async def test_app_transaction_extension( ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 async def test_app_query_extension(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"eq": item["properties"]["proj:epsg"]}}} @@ -169,7 +169,7 @@ async def test_app_query_extension_limit_1( ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": 1} @@ -190,7 +190,7 @@ async def test_app_query_extension_limit_lt0( ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": -1} @@ -203,7 +203,7 @@ async def test_app_query_extension_limit_gt10000( ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"limit": 10001} @@ -214,7 +214,7 @@ async def test_app_query_extension_limit_gt10000( async def test_app_query_extension_gt(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gt": item["properties"]["proj:epsg"]}}} @@ -227,7 +227,7 @@ async def test_app_query_extension_gt(load_test_data, app_client, load_test_coll async def test_app_query_extension_gte(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 params = {"query": {"proj:epsg": {"gte": item["properties"]["proj:epsg"]}}} @@ -243,7 +243,7 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti item_date = datetime.strptime( first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ" ) - resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 second_item = load_test_data("test_item.json") @@ -252,11 +252,11 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti second_item["properties"]["datetime"] = another_item_date.strftime( "%Y-%m-%dT%H:%M:%SZ" ) - resp = await app_client.post(f"/collections/{coll.id}/items", json=second_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=second_item) assert resp.status_code == 201 params = { - "collections": [coll.id], + "collections": [coll["id"]], "sortby": [{"field": "datetime", "direction": "desc"}], } @@ -267,7 +267,7 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti assert resp_json["features"][1]["id"] == second_item["id"] params = { - "collections": [coll.id], + "collections": [coll["id"]], "sortby": [{"field": "datetime", "direction": "asc"}], } resp = await app_client.post("/search", json=params) @@ -280,12 +280,12 @@ async def test_app_sort_extension(load_test_data, app_client, load_test_collecti async def test_search_invalid_date(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 params = { "datetime": "2020-XX-01/2020-10-30", - "collections": [coll.id], + "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) @@ -295,13 +295,13 @@ async def test_search_invalid_date(load_test_data, app_client, load_test_collect async def test_bbox_3d(load_test_data, app_client, load_test_collection): coll = load_test_collection first_item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1] params = { "bbox": australia_bbox, - "collections": [coll.id], + "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 @@ -313,7 +313,7 @@ async def test_bbox_3d(load_test_data, app_client, load_test_collection): async def test_app_search_response(load_test_data, app_client, load_test_collection): coll = load_test_collection params = { - "collections": [coll.id], + "collections": [coll["id"]], } resp = await app_client.post("/search", json=params) assert resp.status_code == 200 @@ -328,7 +328,7 @@ async def test_app_search_response(load_test_data, app_client, load_test_collect async def test_search_point_intersects(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 new_coordinates = [] @@ -337,7 +337,7 @@ async def test_search_point_intersects(load_test_data, app_client, load_test_col item["id"] = "test-item-other-hemispheres" item["geometry"]["coordinates"] = [new_coordinates] item["bbox"] = [value * -1 for value in item["bbox"]] - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 point = [150.04, -33.14] @@ -364,7 +364,7 @@ async def test_search_line_string_intersects( ): coll = load_test_collection item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 line = [[150.04, -33.14], [150.22, -33.89]] @@ -384,7 +384,7 @@ async def test_search_line_string_intersects( async def test_landing_forwarded_header(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - await app_client.post(f"/collections/{coll.id}/items", json=item) + await app_client.post(f"/collections/{coll['id']}/items", json=item) response = ( await app_client.get( "/", @@ -403,7 +403,7 @@ async def test_landing_forwarded_header(load_test_data, app_client, load_test_co async def test_search_forwarded_header(load_test_data, app_client, load_test_collection): coll = load_test_collection item = load_test_data("test_item.json") - await app_client.post(f"/collections/{coll.id}/items", json=item) + await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ @@ -424,7 +424,7 @@ async def test_search_x_forwarded_headers( ): coll = load_test_collection item = load_test_data("test_item.json") - await app_client.post(f"/collections/{coll.id}/items", json=item) + await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ @@ -448,7 +448,7 @@ async def test_search_duplicate_forward_headers( ): coll = load_test_collection item = load_test_data("test_item.json") - await app_client.post(f"/collections/{coll.id}/items", json=item) + await app_client.post(f"/collections/{coll['id']}/items", json=item) resp = await app_client.post( "/search", json={ @@ -495,17 +495,17 @@ async def test_item_collection_filter_bbox( ): coll = load_test_collection first_item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 bbox = "100,-50,170,-20" - resp = await app_client.get(f"/collections/{coll.id}/items", params={"bbox": bbox}) + resp = await app_client.get(f"/collections/{coll['id']}/items", params={"bbox": bbox}) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 1 bbox = "1,2,3,4" - resp = await app_client.get(f"/collections/{coll.id}/items", params={"bbox": bbox}) + resp = await app_client.get(f"/collections/{coll['id']}/items", params={"bbox": bbox}) assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 0 @@ -517,12 +517,12 @@ async def test_item_collection_filter_datetime( ): coll = load_test_collection first_item = load_test_data("test_item.json") - resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=first_item) assert resp.status_code == 201 datetime_range = "2020-01-01T00:00:00.00Z/.." resp = await app_client.get( - f"/collections/{coll.id}/items", params={"datetime": datetime_range} + f"/collections/{coll['id']}/items", params={"datetime": datetime_range} ) assert resp.status_code == 200 resp_json = resp.json() @@ -530,7 +530,7 @@ async def test_item_collection_filter_datetime( datetime_range = "2018-01-01T00:00:00.00Z/2019-01-01T00:00:00.00Z" resp = await app_client.get( - f"/collections/{coll.id}/items", params={"datetime": datetime_range} + f"/collections/{coll['id']}/items", params={"datetime": datetime_range} ) assert resp.status_code == 200 resp_json = resp.json() @@ -577,7 +577,7 @@ async def test_deleting_items_with_identical_ids(app_client): @pytest.mark.parametrize("direction", ("asc", "desc")) async def test_sorting_and_paging(app_client, load_test_collection, direction: str): - collection_id = load_test_collection.id + collection_id = load_test_collection["id"] for i in range(10): item = Item( id=f"item-{i}", diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py index 94785579..79d81181 100644 --- a/tests/clients/test_postgres.py +++ b/tests/clients/test_postgres.py @@ -31,17 +31,18 @@ async def test_create_collection(app_client, load_test_data: Callable): assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) -async def test_update_collection(app_client, load_test_collection): +async def test_update_collection(app_client, load_test_collection, load_test_data): in_coll = load_test_collection - in_coll.keywords.append("newkeyword") + in_coll["keywords"].append("newkeyword") - resp = await app_client.put(f"/collections/{in_coll.id}", json=in_coll.dict()) + resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll) assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 get_coll = Collection.parse_obj(resp.json()) + in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) assert "newkeyword" in get_coll.keywords @@ -49,26 +50,26 @@ async def test_update_collection(app_client, load_test_collection): async def test_delete_collection(app_client, load_test_collection): in_coll = load_test_collection - resp = await app_client.delete(f"/collections/{in_coll.id}") + resp = await app_client.delete(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 404 async def test_create_item(app_client, load_test_data: Callable, load_test_collection): coll = load_test_collection in_json = load_test_data("test_item.json") - in_item = Item.parse_obj(in_json) resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) - assert resp.status_code == 200 + assert resp.status_code == 201 + in_item = Item.parse_obj(in_json) post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) - resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) @@ -84,18 +85,18 @@ async def test_create_item_no_collection_id( item["collection"] = None resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item, ) assert resp.status_code == 201 - resp = await app_client.get(f"/collections/{coll.id}/items/{item['id']}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) - assert get_item.collection == coll.id + assert get_item.collection == coll["id"] async def test_create_item_invalid_ids( @@ -107,7 +108,7 @@ async def test_create_item_invalid_ids( item = load_test_data("test_item.json") item["id"] = "invalid/id" resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item, ) assert resp.status_code == 400 @@ -122,7 +123,7 @@ async def test_create_item_invalid_collection_id( item = load_test_data("test_item.json") item["collection"] = "wrong-collection-id" resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item, ) assert resp.status_code == 400 @@ -137,7 +138,7 @@ async def test_create_item_bad_body( item = load_test_data("test_item.json") item["type"] = "not-a-type" resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item, ) assert resp.status_code == 400 @@ -147,16 +148,18 @@ async def test_update_item(app_client, load_test_collection, load_test_item): coll = load_test_collection item = load_test_item - item.properties.description = "Update Test" + item["properties"]["description"] = "Update Test" resp = await app_client.put( - f"/collections/{coll.id}/items/{item.id}", content=item.json() + f"/collections/{coll['id']}/items/{item['id']}", json=item ) assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) + + item = Item(**item) assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) assert get_item.properties.description == "Update Test" @@ -165,10 +168,10 @@ async def test_delete_item(app_client, load_test_collection, load_test_item): coll = load_test_collection item = load_test_item - resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.delete(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 404 @@ -177,15 +180,15 @@ async def test_get_collection_items(app_client, load_test_collection, load_test_ item = load_test_item for _ in range(4): - item.id = str(uuid.uuid4()) + item["id"] = str(uuid.uuid4()) resp = await app_client.post( - f"/collections/{coll.id}/items", - content=item.json(), + f"/collections/{coll['id']}/items", + json=item, ) - assert resp.status_code == 200 + assert resp.status_code == 201 resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", ) assert resp.status_code == 200 fc = resp.json() @@ -209,17 +212,17 @@ async def test_create_item_collection( item_collection = {"type": "FeatureCollection", "features": items, "links": []} resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item_collection, ) assert resp.status_code == 201 resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", ) for item in items: - resp = await app_client.get(f"/collections/{coll.id}/items/{item['id']}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 @@ -240,19 +243,19 @@ async def test_create_item_collection_no_collection_ids( item_collection = {"type": "FeatureCollection", "features": items, "links": []} resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item_collection, ) assert resp.status_code == 201 resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", ) for item in items: - resp = await app_client.get(f"/collections/{coll.id}/items/{item['id']}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - assert resp.json()["collection"] == coll.id + assert resp.json()["collection"] == coll["id"] async def test_create_item_collection_invalid_collection_ids( @@ -272,7 +275,7 @@ async def test_create_item_collection_invalid_collection_ids( item_collection = {"type": "FeatureCollection", "features": items, "links": []} resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item_collection, ) @@ -295,7 +298,7 @@ async def test_create_item_collection_invalid_item_ids( item_collection = {"type": "FeatureCollection", "features": items, "links": []} resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=item_collection, ) @@ -317,14 +320,14 @@ async def test_create_bulk_items( payload = {"items": items} resp = await app_client.post( - f"/collections/{coll.id}/bulk_items", + f"/collections/{coll['id']}/bulk_items", json=payload, ) assert resp.status_code == 200 assert resp.text == '"Successfully added 2 items."' for item_id in items.keys(): - resp = await app_client.get(f"/collections/{coll.id}/items/{item_id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item_id}") assert resp.status_code == 200 @@ -343,20 +346,20 @@ async def test_create_bulk_items_already_exist_insert( payload = {"items": items, "method": "insert"} resp = await app_client.post( - f"/collections/{coll.id}/bulk_items", + f"/collections/{coll['id']}/bulk_items", json=payload, ) assert resp.status_code == 200 assert resp.text == '"Successfully added 2 items."' for item_id in items.keys(): - resp = await app_client.get(f"/collections/{coll.id}/items/{item_id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item_id}") assert resp.status_code == 200 # Try creating the same items again. # This should fail with the default insert behavior. resp = await app_client.post( - f"/collections/{coll.id}/bulk_items", + f"/collections/{coll['id']}/bulk_items", json=payload, ) assert resp.status_code == 409 @@ -377,21 +380,21 @@ async def test_create_bulk_items_already_exist_upsert( payload = {"items": items, "method": "insert"} resp = await app_client.post( - f"/collections/{coll.id}/bulk_items", + f"/collections/{coll['id']}/bulk_items", json=payload, ) assert resp.status_code == 200 assert resp.text == '"Successfully added 2 items."' for item_id in items.keys(): - resp = await app_client.get(f"/collections/{coll.id}/items/{item_id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item_id}") assert resp.status_code == 200 # Try creating the same items again, but using upsert. # This should succeed. payload["method"] = "upsert" resp = await app_client.post( - f"/collections/{coll.id}/bulk_items", + f"/collections/{coll['id']}/bulk_items", json=payload, ) assert resp.status_code == 200 diff --git a/tests/conftest.py b/tests/conftest.py index 636f6ad1..f0bda44c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -187,7 +187,9 @@ async def load_test_collection(app_client, load_test_data): json=data, ) assert resp.status_code == 201 - return Collection.parse_obj(resp.json()) + collection = Collection.parse_obj(resp.json()) + + return collection.model_dump(mode="json") @pytest.fixture @@ -195,12 +197,13 @@ async def load_test_item(app_client, load_test_data, load_test_collection): coll = load_test_collection data = load_test_data("test_item.json") resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=data, ) assert resp.status_code == 201 - return Item.parse_obj(resp.json()) + item = Item.parse_obj(resp.json()) + return item.model_dump(mode="json") @pytest.fixture diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index 7b97cb4b..8d310d7c 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -13,6 +13,7 @@ async def test_create_collection(app_client, load_test_data: Callable): json=in_json, ) assert resp.status_code == 201 + post_coll = Collection.parse_obj(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") @@ -20,31 +21,45 @@ async def test_create_collection(app_client, load_test_data: Callable): get_coll = Collection.parse_obj(resp.json()) assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) - post_self_link = next((link for link in post_coll.links if link.rel == "self"), None) - get_self_link = next((link for link in get_coll.links if link.rel == "self"), None) + post_coll = post_coll.model_dump(mode="json") + get_coll = get_coll.model_dump(mode="json") + post_self_link = next( + (link for link in post_coll["links"] if link["rel"] == "self"), None + ) + get_self_link = next( + (link for link in get_coll["links"] if link["rel"] == "self"), None + ) assert post_self_link is not None and get_self_link is not None - assert post_self_link.href == get_self_link.href + assert post_self_link["href"] == get_self_link["href"] async def test_update_collection(app_client, load_test_data, load_test_collection): in_coll = load_test_collection - in_coll.keywords.append("newkeyword") + in_coll["keywords"].append("newkeyword") - resp = await app_client.put(f"/collections/{in_coll.id}", json=in_coll.dict()) + resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll) assert resp.status_code == 200 put_coll = Collection.parse_obj(resp.json()) - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 get_coll = Collection.parse_obj(resp.json()) + + in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) assert "newkeyword" in get_coll.keywords - put_self_link = next((link for link in put_coll.links if link.rel == "self"), None) - get_self_link = next((link for link in get_coll.links if link.rel == "self"), None) + get_coll = get_coll.model_dump(mode="json") + put_coll = put_coll.model_dump(mode="json") + put_self_link = next( + (link for link in put_coll["links"] if link["rel"] == "self"), None + ) + get_self_link = next( + (link for link in get_coll["links"] if link["rel"] == "self"), None + ) assert put_self_link is not None and get_self_link is not None - assert put_self_link.href == get_self_link.href + assert put_self_link["href"] == get_self_link["href"] async def test_delete_collection( @@ -52,10 +67,10 @@ async def test_delete_collection( ): in_coll = load_test_collection - resp = await app_client.delete(f"/collections/{in_coll.id}") + resp = await app_client.delete(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 404 @@ -84,9 +99,9 @@ async def test_delete_missing_collection( async def test_update_new_collection(app_client, load_test_collection): in_coll = load_test_collection - in_coll.id = "test-updatenew" + in_coll["id"] = "test-updatenew" - resp = await app_client.put(f"/collections/{in_coll.id}", json=in_coll.dict()) + resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll) assert resp.status_code == 404 @@ -172,7 +187,7 @@ async def test_returns_valid_links_in_collections(app_client, load_test_data): async def test_returns_license_link(app_client, load_test_collection): coll = load_test_collection - resp = await app_client.get(f"/collections/{coll.id}") + resp = await app_client.get(f"/collections/{coll['id']}") assert resp.status_code == 200 resp_json = resp.json() link_rel_types = [link["rel"] for link in resp_json["links"]] @@ -183,7 +198,7 @@ async def test_returns_license_link(app_client, load_test_collection): async def test_get_collection_forwarded_header(app_client, load_test_collection): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}", + f"/collections/{coll['id']}", headers={"Forwarded": "proto=https;host=test:1234"}, ) for link in [ @@ -198,7 +213,7 @@ async def test_get_collection_forwarded_header(app_client, load_test_collection) async def test_get_collection_x_forwarded_headers(app_client, load_test_collection): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}", + f"/collections/{coll['id']}", headers={ "X-Forwarded-Port": "1234", "X-Forwarded-Proto": "https", @@ -218,7 +233,7 @@ async def test_get_collection_duplicate_forwarded_headers( ): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}", + f"/collections/{coll['id']}", headers={ "Forwarded": "proto=https;host=test:1234", "X-Forwarded-Port": "4321", diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index 6015f455..f2dedc46 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -37,15 +37,18 @@ async def test_create_collection(app_client, load_test_data: Callable): async def test_update_collection(app_client, load_test_data, load_test_collection): in_coll = load_test_collection - in_coll.keywords.append("newkeyword") + in_coll = load_test_data("test_collection.json") + in_coll["keywords"].append("newkeyword") - resp = await app_client.put(f"/collections/{in_coll.id}", json=in_coll.dict()) + resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll) assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 get_coll = Collection.parse_obj(resp.json()) + + in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) assert "newkeyword" in get_coll.keywords @@ -55,10 +58,10 @@ async def test_delete_collection( ): in_coll = load_test_collection - resp = await app_client.delete(f"/collections/{in_coll.id}") + resp = await app_client.delete(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{in_coll.id}") + resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 404 @@ -66,26 +69,32 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle coll = load_test_collection in_json = load_test_data("test_item.json") - in_item = Item.parse_obj(in_json) resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) assert resp.status_code == 201 + in_item = Item.parse_obj(in_json) post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) - resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) - post_self_link = next((link for link in post_item.links if link.rel == "self"), None) - get_self_link = next((link for link in get_item.links if link.rel == "self"), None) + get_item = get_item.model_dump(mode="json") + post_item = post_item.model_dump(mode="json") + post_self_link = next( + (link for link in post_item["links"] if link["rel"] == "self"), None + ) + get_self_link = next( + (link for link in get_item["links"] if link["rel"] == "self"), None + ) assert post_self_link is not None and get_self_link is not None - assert post_self_link.href == get_self_link.href + assert post_self_link["href"] == get_self_link["href"] async def test_create_item_mismatched_collection_id( @@ -97,10 +106,10 @@ async def test_create_item_mismatched_collection_id( in_json = load_test_data("test_item.json") in_json["collection"] = random.choice(ascii_letters) - assert in_json["collection"] != coll.id + assert in_json["collection"] != coll["id"] resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) assert resp.status_code == 400 @@ -112,17 +121,18 @@ async def test_fetches_valid_item( coll = load_test_collection in_json = load_test_data("test_item.json") - in_item = Item.parse_obj(in_json) + resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) assert resp.status_code == 201 + in_item = Item.parse_obj(in_json) post_item = Item.parse_obj(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) - resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 item_dict = resp.json() @@ -140,25 +150,32 @@ async def test_update_item( coll = load_test_collection item = load_test_item - item.properties.description = "Update Test" + item["properties"]["description"] = "Update Test" resp = await app_client.put( - f"/collections/{coll.id}/items/{item.id}", content=item.json() + f"/collections/{coll['id']}/items/{item['id']}", json=item ) assert resp.status_code == 200 put_item = Item.parse_obj(resp.json()) - resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 get_item = Item.parse_obj(resp.json()) + item = Item(**item) assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) assert get_item.properties.description == "Update Test" - post_self_link = next((link for link in put_item.links if link.rel == "self"), None) - get_self_link = next((link for link in get_item.links if link.rel == "self"), None) + put_item = put_item.model_dump(mode="json") + get_item = get_item.model_dump(mode="json") + post_self_link = next( + (link for link in put_item["links"] if link["rel"] == "self"), None + ) + get_self_link = next( + (link for link in get_item["links"] if link["rel"] == "self"), None + ) assert post_self_link is not None and get_self_link is not None - assert post_self_link.href == get_self_link.href + assert post_self_link["href"] == get_self_link["href"] async def test_update_item_mismatched_collection_id( @@ -169,12 +186,12 @@ async def test_update_item_mismatched_collection_id( in_json = load_test_data("test_item.json") in_json["collection"] = random.choice(ascii_letters) - assert in_json["collection"] != coll.id + assert in_json["collection"] != coll["id"] item_id = in_json["id"] resp = await app_client.put( - f"/collections/{coll.id}/items/{item_id}", + f"/collections/{coll['id']}/items/{item_id}", json=in_json, ) assert resp.status_code == 400 @@ -186,27 +203,29 @@ async def test_delete_item( coll = load_test_collection item = load_test_item - resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.delete(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 404 -async def test_get_collection_items(app_client, load_test_collection, load_test_item): +async def test_get_collection_items( + app_client, load_test_collection, load_test_item, load_test_data +): coll = load_test_collection - item = load_test_item + item = load_test_data("test_item.json") for _ in range(4): - item.id = str(uuid.uuid4()) + item["id"] = str(uuid.uuid4()) resp = await app_client.post( - f"/collections/{coll.id}/items", - content=item.json(), + f"/collections/{coll['id']}/items", + json=item, ) assert resp.status_code == 201 resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", ) assert resp.status_code == 200 fc = resp.json() @@ -219,15 +238,14 @@ async def test_create_item_conflict( ): coll = load_test_collection in_json = load_test_data("test_item.json") - Item.parse_obj(in_json) resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) assert resp.status_code == 201 resp = await app_client.post( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", json=in_json, ) assert resp.status_code == 409 @@ -239,10 +257,10 @@ async def test_delete_missing_item( coll = load_test_collection item = load_test_item - resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.delete(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}") + resp = await app_client.delete(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 404 @@ -253,22 +271,23 @@ async def test_create_item_missing_collection( item = load_test_data("test_item.json") item["collection"] = None - resp = await app_client.post(f"/collections/{coll.id}/items", json=item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 post_item = resp.json() - assert post_item["collection"] == coll.id + assert post_item["collection"] == coll["id"] async def test_update_new_item( - app_client, load_test_data: Callable, load_test_collection, load_test_item + app_client, load_test_data: Callable, load_test_collection ): coll = load_test_collection - item = load_test_item - item.id = "test-updatenewitem" + + new_item = load_test_data("test_item.json") + new_item["id"] = "test-updatenewitem" resp = await app_client.put( - f"/collections/{coll.id}/items/{item.id}", content=item.json() + f"/collections/{coll['id']}/items/{new_item['id']}", json=new_item ) assert resp.status_code == 404 @@ -278,31 +297,30 @@ async def test_update_item_missing_collection( ): coll = load_test_collection item = load_test_item - item.collection = None + item["collection"] = None resp = await app_client.put( - f"/collections/{coll.id}/items/{item.id}", content=item.json() + f"/collections/{coll['id']}/items/{item['id']}", json=item ) assert resp.status_code == 200 put_item = resp.json() - assert put_item["collection"] == coll.id + assert put_item["collection"] == coll["id"] async def test_pagination(app_client, load_test_data, load_test_collection): """Test item collection pagination (paging extension)""" coll = load_test_collection item_count = 21 - test_item = load_test_data("test_item.json") for idx in range(1, item_count): - item = Item.parse_obj(test_item) - item.id = item.id + str(idx) - item.properties.datetime = f"2020-01-{idx:02d}T00:00:00" - resp = await app_client.post(f"/collections/{coll.id}/items", json=item.dict()) + item = load_test_data("test_item.json") + item["id"] = item["id"] + str(idx) + item["properties"]["datetime"] = f"2020-01-{idx:02d}T00:00:00Z" + resp = await app_client.post(f"/collections/{coll['id']}/items", json=item) assert resp.status_code == 201 - resp = await app_client.get(f"/collections/{coll.id}/items", params={"limit": 3}) + resp = await app_client.get(f"/collections/{coll['id']}/items", params={"limit": 3}) assert resp.status_code == 200 first_page = resp.json() assert len(first_page["features"]) == 3 @@ -1268,11 +1286,11 @@ async def test_preserves_extra_link( test_item = load_test_data("test_item.json") expected_href = urljoin(str(app_client.base_url), "preview.html") - resp = await app_client.post(f"/collections/{coll.id}/items", json=test_item) + resp = await app_client.post(f"/collections/{coll['id']}/items", json=test_item) assert resp.status_code == 201 response_item = await app_client.get( - f"/collections/{coll.id}/items/{test_item['id']}", + f"/collections/{coll['id']}/items/{test_item['id']}", params={"limit": 1}, ) assert response_item.status_code == 200 @@ -1468,7 +1486,7 @@ async def test_get_collection_items_forwarded_header( ): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", headers={"Forwarded": "proto=https;host=test:1234"}, ) for link in resp.json()["features"][0]["links"]: @@ -1481,7 +1499,7 @@ async def test_get_collection_items_x_forwarded_headers( ): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", headers={ "X-Forwarded-Port": "1234", "X-Forwarded-Proto": "https", @@ -1497,7 +1515,7 @@ async def test_get_collection_items_duplicate_forwarded_headers( ): coll = load_test_collection resp = await app_client.get( - f"/collections/{coll.id}/items", + f"/collections/{coll['id']}/items", headers={ "Forwarded": "proto=https;host=test:1234", "X-Forwarded-Port": "4321", From b5dfba459fe0ab22a1c7afef72b5e91dddfc0fb1 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 6 May 2024 13:17:05 +0200 Subject: [PATCH 07/10] update and remove deprecated --- setup.py | 6 +++--- stac_fastapi/pgstac/db.py | 2 +- stac_fastapi/pgstac/extensions/filter.py | 13 +++++++------ tests/clients/test_postgres.py | 22 +++++++++++----------- tests/conftest.py | 8 ++++---- tests/resources/test_collection.py | 14 +++++++------- tests/resources/test_item.py | 22 +++++++++++----------- 7 files changed, 44 insertions(+), 43 deletions(-) diff --git a/setup.py b/setup.py index 166c33b6..0e3c46f6 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ # "stac-fastapi.extensions~=3.0", # "stac-fastapi.types~=3.0", # For now we use latest commit in master - "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", - "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", - "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@e4e4120ae6d1a9fe80910073af5889ae08c418f8#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", + "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", + "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", + "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", "asyncpg", "buildpg", "brotli_asgi", diff --git a/stac_fastapi/pgstac/db.py b/stac_fastapi/pgstac/db.py index b684a138..121d44c0 100644 --- a/stac_fastapi/pgstac/db.py +++ b/stac_fastapi/pgstac/db.py @@ -142,6 +142,6 @@ async def create_pool(self, connection_string: str, settings): max_queries=settings.db_max_queries, max_inactive_connection_lifetime=settings.db_max_inactive_conn_lifetime, init=con_init, - server_settings=settings.server_settings.dict(), + server_settings=settings.server_settings.model_dump(), ) return pool diff --git a/stac_fastapi/pgstac/extensions/filter.py b/stac_fastapi/pgstac/extensions/filter.py index 148a952a..15c3d0f1 100644 --- a/stac_fastapi/pgstac/extensions/filter.py +++ b/stac_fastapi/pgstac/extensions/filter.py @@ -1,10 +1,9 @@ """Get Queryables.""" -from typing import Any, Optional +from typing import Any, Dict, Optional from buildpg import render from fastapi import Request -from fastapi.responses import JSONResponse from stac_fastapi.types.core import AsyncBaseFiltersClient from stac_fastapi.types.errors import NotFoundError @@ -13,8 +12,11 @@ class FiltersClient(AsyncBaseFiltersClient): """Defines a pattern for implementing the STAC filter extension.""" async def get_queryables( - self, request: Request, collection_id: Optional[str] = None, **kwargs: Any - ) -> JSONResponse: + self, + request: Request, + collection_id: Optional[str] = None, + **kwargs: Any, + ) -> Dict[str, Any]: """Get the queryables available for the given collection_id. If collection_id is None, returns the intersection of all @@ -37,5 +39,4 @@ async def get_queryables( raise NotFoundError(f"Collection {collection_id} not found") queryables["$id"] = str(request.url) - headers = {"Content-Type": "application/schema+json"} - return JSONResponse(queryables, headers=headers) + return queryables diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py index 79d81181..86e1c9b9 100644 --- a/tests/clients/test_postgres.py +++ b/tests/clients/test_postgres.py @@ -16,18 +16,18 @@ async def test_create_collection(app_client, load_test_data: Callable): in_json = load_test_data("test_collection.json") - in_coll = Collection.parse_obj(in_json) + in_coll = Collection.model_validate(in_json) resp = await app_client.post( "/collections", json=in_json, ) assert resp.status_code == 201 - post_coll = Collection.parse_obj(resp.json()) + post_coll = Collection.model_validate(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) @@ -41,7 +41,7 @@ async def test_update_collection(app_client, load_test_collection, load_test_dat resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) assert "newkeyword" in get_coll.keywords @@ -65,13 +65,13 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle json=in_json, ) assert resp.status_code == 201 - in_item = Item.parse_obj(in_json) - post_item = Item.parse_obj(resp.json()) + in_item = Item.model_validate(in_json) + post_item = Item.model_validate(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 - get_item = Item.parse_obj(resp.json()) + get_item = Item.model_validate(resp.json()) assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) @@ -95,7 +95,7 @@ async def test_create_item_no_collection_id( assert resp.status_code == 200 - get_item = Item.parse_obj(resp.json()) + get_item = Item.model_validate(resp.json()) assert get_item.collection == coll["id"] @@ -157,7 +157,7 @@ async def test_update_item(app_client, load_test_collection, load_test_item): resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - get_item = Item.parse_obj(resp.json()) + get_item = Item.model_validate(resp.json()) item = Item(**item) assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) @@ -411,10 +411,10 @@ async def test_create_bulk_items_already_exist_upsert( # postgres_transactions: TransactionsClient, # load_test_data: Callable, # ): -# coll = Collection.parse_obj(load_test_data("test_collection.json")) +# coll = Collection.model_validate(load_test_data("test_collection.json")) # postgres_transactions.create_collection(coll, request=MockStarletteRequest) -# item = Item.parse_obj(load_test_data("test_item.json")) +# item = Item.model_validate(load_test_data("test_item.json")) # for _ in range(5): # item.id = str(uuid.uuid4()) diff --git a/tests/conftest.py b/tests/conftest.py index f0bda44c..c1b6f6f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -187,7 +187,7 @@ async def load_test_collection(app_client, load_test_data): json=data, ) assert resp.status_code == 201 - collection = Collection.parse_obj(resp.json()) + collection = Collection.model_validate(resp.json()) return collection.model_dump(mode="json") @@ -202,7 +202,7 @@ async def load_test_item(app_client, load_test_data, load_test_collection): ) assert resp.status_code == 201 - item = Item.parse_obj(resp.json()) + item = Item.model_validate(resp.json()) return item.model_dump(mode="json") @@ -214,7 +214,7 @@ async def load_test2_collection(app_client, load_test_data): json=data, ) assert resp.status_code == 201 - return Collection.parse_obj(resp.json()) + return Collection.model_validate(resp.json()) @pytest.fixture @@ -226,4 +226,4 @@ async def load_test2_item(app_client, load_test_data, load_test2_collection): json=data, ) assert resp.status_code == 201 - return Item.parse_obj(resp.json()) + return Item.model_validate(resp.json()) diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index 8d310d7c..798ed33d 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -7,18 +7,18 @@ async def test_create_collection(app_client, load_test_data: Callable): in_json = load_test_data("test_collection.json") - in_coll = Collection.parse_obj(in_json) + in_coll = Collection.model_validate(in_json) resp = await app_client.post( "/collections", json=in_json, ) assert resp.status_code == 201 - post_coll = Collection.parse_obj(resp.json()) + post_coll = Collection.model_validate(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) post_coll = post_coll.model_dump(mode="json") @@ -39,12 +39,12 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio resp = await app_client.put(f"/collections/{in_coll['id']}", json=in_coll) assert resp.status_code == 200 - put_coll = Collection.parse_obj(resp.json()) + put_coll = Collection.model_validate(resp.json()) resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) @@ -76,13 +76,13 @@ async def test_delete_collection( async def test_create_collection_conflict(app_client, load_test_data: Callable): in_json = load_test_data("test_collection.json") - Collection.parse_obj(in_json) + Collection.model_validate(in_json) resp = await app_client.post( "/collections", json=in_json, ) assert resp.status_code == 201 - Collection.parse_obj(resp.json()) + Collection.model_validate(resp.json()) resp = await app_client.post( "/collections", json=in_json, diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index f2dedc46..f006aaa9 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -21,17 +21,17 @@ async def test_create_collection(app_client, load_test_data: Callable): in_json = load_test_data("test_collection.json") - in_coll = Collection.parse_obj(in_json) + in_coll = Collection.model_validate(in_json) resp = await app_client.post( "/collections", json=in_json, ) assert resp.status_code == 201 - post_coll = Collection.parse_obj(resp.json()) + post_coll = Collection.model_validate(resp.json()) assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) @@ -46,7 +46,7 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio resp = await app_client.get(f"/collections/{in_coll['id']}") assert resp.status_code == 200 - get_coll = Collection.parse_obj(resp.json()) + get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) @@ -75,14 +75,14 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle ) assert resp.status_code == 201 - in_item = Item.parse_obj(in_json) - post_item = Item.parse_obj(resp.json()) + in_item = Item.model_validate(in_json) + post_item = Item.model_validate(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 - get_item = Item.parse_obj(resp.json()) + get_item = Item.model_validate(resp.json()) assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) get_item = get_item.model_dump(mode="json") @@ -128,8 +128,8 @@ async def test_fetches_valid_item( ) assert resp.status_code == 201 - in_item = Item.parse_obj(in_json) - post_item = Item.parse_obj(resp.json()) + in_item = Item.model_validate(in_json) + post_item = Item.model_validate(resp.json()) assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") @@ -156,12 +156,12 @@ async def test_update_item( f"/collections/{coll['id']}/items/{item['id']}", json=item ) assert resp.status_code == 200 - put_item = Item.parse_obj(resp.json()) + put_item = Item.model_validate(resp.json()) resp = await app_client.get(f"/collections/{coll['id']}/items/{item['id']}") assert resp.status_code == 200 - get_item = Item.parse_obj(resp.json()) + get_item = Item.model_validate(resp.json()) item = Item(**item) assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) assert get_item.properties.description == "Update Test" From 1ea0bfdc8b307342bd953bb83f88fe80b5d44add Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 6 May 2024 21:47:03 +0200 Subject: [PATCH 08/10] update deprecated methods --- .pre-commit-config.yaml | 2 +- setup.py | 10 +++------- stac_fastapi/pgstac/config.py | 6 ++++-- stac_fastapi/pgstac/core.py | 4 +++- stac_fastapi/pgstac/types/search.py | 9 +++++---- tests/api/test_api.py | 4 ++-- tests/clients/test_postgres.py | 18 ++++++++++++------ tests/conftest.py | 4 ++-- tests/resources/test_collection.py | 10 +++++++--- tests/resources/test_item.py | 22 +++++++++++++++------- 10 files changed, 54 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0c6c0c4..844a44bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,4 +21,4 @@ repos: additional_dependencies: - types-requests - types-attrs - - pydantic~=1.10 + - pydantic~=2.0 diff --git a/setup.py b/setup.py index 0e3c46f6..17eda306 100644 --- a/setup.py +++ b/setup.py @@ -10,13 +10,9 @@ "orjson", "pydantic", "stac_pydantic==3.0.*", - # "stac-fastapi.api~=3.0", - # "stac-fastapi.extensions~=3.0", - # "stac-fastapi.types~=3.0", - # For now we use latest commit in master - "stac-fastapi.api @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.api&subdirectory=stac_fastapi/api", - "stac-fastapi.extensions @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.extensions&subdirectory=stac_fastapi/extensions", - "stac-fastapi.types @ git+https://github.com/stac-utils/stac-fastapi/@0de6ace95acd76a4850b2ad95756926119d9fd69#egg=stac-fastapi.types&subdirectory=stac_fastapi/types", + "stac-fastapi.api~=3.0.0a0", + "stac-fastapi.extensions~=3.0.0a0", + "stac-fastapi.types~=3.0.0a0", "asyncpg", "buildpg", "brotli_asgi", diff --git a/stac_fastapi/pgstac/config.py b/stac_fastapi/pgstac/config.py index d45e6ebc..9c825006 100644 --- a/stac_fastapi/pgstac/config.py +++ b/stac_fastapi/pgstac/config.py @@ -3,7 +3,7 @@ from typing import List, Type from urllib.parse import quote -from pydantic import BaseModel, Extra +from pydantic import BaseModel from pydantic_settings import SettingsConfigDict from stac_fastapi.types.config import ApiSettings @@ -34,12 +34,14 @@ ] -class ServerSettings(BaseModel, extra=Extra.allow): +class ServerSettings(BaseModel): """Server runtime parameters.""" search_path: str = "pgstac,public" application_name: str = "pgstac" + model_config = SettingsConfigDict(extra="allow") + class Settings(ApiSettings): """Postgres-specific API settings. diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index ef76d577..403f70d7 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -162,7 +162,9 @@ async def _search_base( # noqa: C901 search_request.conf = search_request.conf or {} search_request.conf["nohydrate"] = settings.use_api_hydrate - search_request_json = search_request.json(exclude_none=True, by_alias=True) + search_request_json = search_request.model_dump_json( + exclude_none=True, by_alias=True + ) try: async with request.app.state.get_connection(request, "r") as conn: diff --git a/stac_fastapi/pgstac/types/search.py b/stac_fastapi/pgstac/types/search.py index 18b26b05..2ccc5c14 100644 --- a/stac_fastapi/pgstac/types/search.py +++ b/stac_fastapi/pgstac/types/search.py @@ -2,7 +2,7 @@ from typing import Dict, Optional -from pydantic import validator +from pydantic import ValidationInfo, field_validator from stac_fastapi.types.search import BaseSearchPostRequest @@ -14,10 +14,11 @@ class PgstacSearch(BaseSearchPostRequest): conf: Optional[Dict] = None - @validator("filter_lang", pre=False, check_fields=False, always=True) - def validate_query_uses_cql(cls, v, values): + @field_validator("filter_lang", check_fields=False) + @classmethod + def validate_query_uses_cql(cls, v: str, info: ValidationInfo): """Use of Query Extension is not allowed with cql2.""" - if values.get("query", None) is not None and v != "cql-json": + if info.data.get("query", None) is not None and v != "cql-json": raise ValueError( "Query extension is not available when using pgstac with cql2" ) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index f43db871..b7a58928 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -5,7 +5,7 @@ import orjson import pytest from fastapi import Request -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_post_request_model @@ -678,7 +678,7 @@ async def get_collection( app = api.app await connect_to_db(app) try: - async with AsyncClient(app=app) as client: + async with AsyncClient(transport=ASGITransport(app=app)) as client: response = await client.post( "http://test/collections", json=load_test_data("test_collection.json"), diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py index 86e1c9b9..36225219 100644 --- a/tests/clients/test_postgres.py +++ b/tests/clients/test_postgres.py @@ -23,12 +23,16 @@ async def test_create_collection(app_client, load_test_data: Callable): ) assert resp.status_code == 201 post_coll = Collection.model_validate(resp.json()) - assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == post_coll.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 get_coll = Collection.model_validate(resp.json()) - assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert post_coll.model_dump(exclude={"links"}) == get_coll.model_dump( + exclude={"links"} + ) async def test_update_collection(app_client, load_test_collection, load_test_data): @@ -43,7 +47,7 @@ async def test_update_collection(app_client, load_test_collection, load_test_dat get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) - assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == get_coll.model_dump(exclude={"links"}) assert "newkeyword" in get_coll.keywords @@ -67,12 +71,14 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle assert resp.status_code == 201 in_item = Item.model_validate(in_json) post_item = Item.model_validate(resp.json()) - assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) + assert in_item.model_dump(exclude={"links"}) == post_item.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 get_item = Item.model_validate(resp.json()) - assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) + assert in_item.model_dump(exclude={"links"}) == get_item.model_dump(exclude={"links"}) async def test_create_item_no_collection_id( @@ -160,7 +166,7 @@ async def test_update_item(app_client, load_test_collection, load_test_item): get_item = Item.model_validate(resp.json()) item = Item(**item) - assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) + assert item.model_dump(exclude={"links"}) == get_item.model_dump(exclude={"links"}) assert get_item.properties.description == "Update Test" diff --git a/tests/conftest.py b/tests/conftest.py index c1b6f6f5..f3f1cf0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest from fastapi import APIRouter from fastapi.responses import ORJSONResponse -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient from pypgstac.db import PgstacDB from pypgstac.migrate import Migrate from pytest_postgresql.janitor import DatabaseJanitor @@ -166,7 +166,7 @@ async def app_client(app): if app.state.router_prefix != "": base_url = urljoin(base_url, app.state.router_prefix) - async with AsyncClient(app=app, base_url=base_url) as c: + async with AsyncClient(transport=ASGITransport(app=app), base_url=base_url) as c: yield c diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index 798ed33d..ff5c8ade 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -15,11 +15,15 @@ async def test_create_collection(app_client, load_test_data: Callable): assert resp.status_code == 201 post_coll = Collection.model_validate(resp.json()) - assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == post_coll.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 get_coll = Collection.model_validate(resp.json()) - assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert post_coll.model_dump(exclude={"links"}) == get_coll.model_dump( + exclude={"links"} + ) post_coll = post_coll.model_dump(mode="json") get_coll = get_coll.model_dump(mode="json") @@ -47,7 +51,7 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) - assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == get_coll.model_dump(exclude={"links"}) assert "newkeyword" in get_coll.keywords get_coll = get_coll.model_dump(mode="json") diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index f006aaa9..8b52ead0 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -28,11 +28,15 @@ async def test_create_collection(app_client, load_test_data: Callable): ) assert resp.status_code == 201 post_coll = Collection.model_validate(resp.json()) - assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == post_coll.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{post_coll.id}") assert resp.status_code == 200 get_coll = Collection.model_validate(resp.json()) - assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert post_coll.model_dump(exclude={"links"}) == get_coll.model_dump( + exclude={"links"} + ) async def test_update_collection(app_client, load_test_data, load_test_collection): @@ -49,7 +53,7 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio get_coll = Collection.model_validate(resp.json()) in_coll = Collection(**in_coll) - assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"}) + assert in_coll.model_dump(exclude={"links"}) == get_coll.model_dump(exclude={"links"}) assert "newkeyword" in get_coll.keywords @@ -77,13 +81,15 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle in_item = Item.model_validate(in_json) post_item = Item.model_validate(resp.json()) - assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) + assert in_item.model_dump(exclude={"links"}) == post_item.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") assert resp.status_code == 200 get_item = Item.model_validate(resp.json()) - assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) + assert in_item.model_dump(exclude={"links"}) == get_item.model_dump(exclude={"links"}) get_item = get_item.model_dump(mode="json") post_item = post_item.model_dump(mode="json") @@ -130,7 +136,9 @@ async def test_fetches_valid_item( in_item = Item.model_validate(in_json) post_item = Item.model_validate(resp.json()) - assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"}) + assert in_item.model_dump(exclude={"links"}) == post_item.model_dump( + exclude={"links"} + ) resp = await app_client.get(f"/collections/{coll['id']}/items/{post_item.id}") @@ -163,7 +171,7 @@ async def test_update_item( get_item = Item.model_validate(resp.json()) item = Item(**item) - assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"}) + assert item.model_dump(exclude={"links"}) == get_item.model_dump(exclude={"links"}) assert get_item.properties.description == "Update Test" put_item = put_item.model_dump(mode="json") From 2302ab49b8a331b53d006b82074d179885c39445 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 7 May 2024 11:31:55 +0200 Subject: [PATCH 09/10] remove context extension --- stac_fastapi/pgstac/app.py | 2 -- tests/conftest.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 92c43089..3c08c233 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -11,7 +11,6 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model from stac_fastapi.extensions.core import ( - ContextExtension, FieldsExtension, FilterExtension, SortExtension, @@ -39,7 +38,6 @@ "sort": SortExtension(), "fields": FieldsExtension(), "pagination": TokenPaginationExtension(), - "context": ContextExtension(), "filter": FilterExtension(client=FiltersClient()), "bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()), } diff --git a/tests/conftest.py b/tests/conftest.py index f3f1cf0b..dcd5d546 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,6 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model from stac_fastapi.extensions.core import ( - ContextExtension, FieldsExtension, FilterExtension, SortExtension, @@ -125,7 +124,6 @@ def api_client(request, database): SortExtension(), FieldsExtension(), TokenPaginationExtension(), - ContextExtension(), FilterExtension(client=FiltersClient()), BulkTransactionExtension(client=BulkTransactionsClient()), ] From bd3543d0a16652f9d814cd3c0ec7422efd19c769 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 7 May 2024 17:04:08 +0200 Subject: [PATCH 10/10] update changelog --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ce55793a..ee64215b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,8 @@ ### Changed -- update pgstac version to `0.8.x` +- Update stac-fastapi libraries to v3.0.0a0 ([#108](https://github.com/stac-utils/stac-fastapi-pgstac/pull/108)) +- Update pgstac version to `0.8.x` ## [2.5.0] - 2024-04-25