Skip to content

Commit 285e24f

Browse files
bitnerlossyrob
andauthored
Add Support for CQL JSON to Stac FastAPI PGStac Backend (#209)
* wait for 5432 to be available before running apps in docker * allow filter item in search for CQL search * bump required pgstac version to v0.3.0 * update pgstac dependency to 0.3.1 * Update docker to pgstac 0.3.1. Fix #179 (enforce limit range). Tests for CQL * Update docker to pgstac 0.3.1. Fix #179 (enforce limit range). Tests for CQL * test for cql search * make tests wait for database to be running * bump pgstac version to 0.3.3 * update pgstac test_item_search_get_filter_cql to reflect 200 with empty features array rather than 404 Co-authored-by: Rob Emanuele <rdemanuele@gmail.com>
1 parent b0e9d91 commit 285e24f

File tree

6 files changed

+96
-17
lines changed

6 files changed

+96
-17
lines changed

Makefile

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ docker-shell-pgstac:
3636

3737
.PHONY: test-sqlalchemy
3838
test-sqlalchemy: run-joplin-sqlalchemy
39-
$(run_docker) /bin/bash -c 'export && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest'
39+
$(run_docker) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest'
4040

4141
.PHONY: test-pgstac
4242
test-pgstac:
43-
$(run_pgstac) /bin/bash -c 'export && cd /app/stac_fastapi/pgstac/tests/ && pytest'
43+
$(run_pgstac) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/pgstac/tests/ && pytest'
4444

4545
.PHONY: run-database
4646
run-database:
@@ -50,10 +50,6 @@ run-database:
5050
run-joplin-sqlalchemy:
5151
docker-compose run --rm loadjoplin-sqlalchemy
5252

53-
.PHONY: run-joplin-sqlalchemy
54-
run-joplin-sqlalchemy:
55-
docker-compose run --rm loadjoplin-sqlalchemy
56-
5753
.PHONY: test
5854
test: test-sqlalchemy test-pgstac
5955

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ services:
7171

7272
database:
7373
container_name: stac-db
74-
image: ghcr.io/stac-utils/pgstac:v0.3.2
74+
image: ghcr.io/stac-utils/pgstac:v0.3.3
7575
environment:
7676
- POSTGRES_USER=username
7777
- POSTGRES_PASSWORD=password

stac_fastapi/pgstac/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"pytest-asyncio",
2727
"pre-commit",
2828
"requests",
29-
"pypgstac==0.3.2",
29+
"pypgstac==0.3.3",
3030
"httpx",
3131
"shapely",
3232
],

stac_fastapi/pgstac/stac_fastapi/pgstac/types/search.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from types import DynamicClassAttribute
66
from typing import Any, Callable, Dict, List, Optional, Set, Union
77

8-
from pydantic import Field, root_validator, validator
8+
from pydantic import Field, conint, root_validator, validator
99
from stac_pydantic.api import Search
1010
from stac_pydantic.api.extensions.fields import FieldsExtension as FieldsBase
1111
from stac_pydantic.utils import AutoValueEnum
@@ -96,9 +96,11 @@ class PgstacSearch(Search):
9696
fields: FieldsExtension = Field(FieldsExtension())
9797
# Override query extension with supported operators
9898
query: Optional[Dict[str, Dict[Operator, Any]]]
99+
filter: Optional[Dict]
99100
token: Optional[str] = None
100101
datetime: Optional[str] = None
101102
sortby: Any
103+
limit: Optional[conint(ge=0, le=10000)] = 10
102104

103105
@root_validator(pre=True)
104106
def validate_query_fields(cls, values: Dict) -> Dict:

stac_fastapi/pgstac/tests/api/test_api.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,50 @@ async def test_app_query_extension(load_test_data, app_client, load_test_collect
6565
assert len(resp_json["features"]) == 1
6666

6767

68+
@pytest.mark.asyncio
69+
async def test_app_query_extension_limit_1(
70+
load_test_data, app_client, load_test_collection
71+
):
72+
coll = load_test_collection
73+
item = load_test_data("test_item.json")
74+
resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
75+
assert resp.status_code == 200
76+
77+
params = {"limit": 1}
78+
resp = await app_client.post("/search", json=params)
79+
assert resp.status_code == 200
80+
resp_json = resp.json()
81+
assert len(resp_json["features"]) == 1
82+
83+
84+
@pytest.mark.asyncio
85+
async def test_app_query_extension_limit_lt0(
86+
load_test_data, app_client, load_test_collection
87+
):
88+
coll = load_test_collection
89+
item = load_test_data("test_item.json")
90+
resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
91+
assert resp.status_code == 200
92+
93+
params = {"limit": -1}
94+
resp = await app_client.post("/search", json=params)
95+
assert resp.status_code == 400
96+
97+
98+
@pytest.mark.asyncio
99+
async def test_app_query_extension_limit_gt10000(
100+
load_test_data, app_client, load_test_collection
101+
):
102+
coll = load_test_collection
103+
item = load_test_data("test_item.json")
104+
resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
105+
assert resp.status_code == 200
106+
107+
params = {"limit": 10001}
108+
resp = await app_client.post("/search", json=params)
109+
assert resp.status_code == 400
110+
111+
68112
@pytest.mark.asyncio
69113
async def test_app_sort_extension(load_test_data, app_client, load_test_collection):
70114
coll = load_test_collection

stac_fastapi/pgstac/tests/resources/test_item.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,51 @@ async def test_item_search_get_query_extension(
682682
)
683683

684684

685+
@pytest.mark.asyncio
686+
async def test_item_search_get_filter_extension_cql(
687+
app_client, load_test_data, load_test_collection
688+
):
689+
"""Test GET search with JSONB query (cql json filter extension)"""
690+
test_item = load_test_data("test_item.json")
691+
resp = await app_client.post(
692+
f"/collections/{test_item['collection']}/items", json=test_item
693+
)
694+
assert resp.status_code == 200
695+
696+
# EPSG is a JSONB key
697+
params = {
698+
"collections": [test_item["collection"]],
699+
"filter": {
700+
"gt": [
701+
{"property": "proj:epsg"},
702+
test_item["properties"]["proj:epsg"] + 1,
703+
]
704+
},
705+
}
706+
resp = await app_client.post("/search", json=params)
707+
resp_json = resp.json()
708+
709+
assert resp.status_code == 200
710+
assert len(resp_json.get("features")) == 0
711+
712+
params = {
713+
"collections": [test_item["collection"]],
714+
"filter": {
715+
"eq": [
716+
{"property": "proj:epsg"},
717+
test_item["properties"]["proj:epsg"],
718+
]
719+
},
720+
}
721+
resp = await app_client.post("/search", json=params)
722+
resp_json = resp.json()
723+
assert len(resp.json()["features"]) == 1
724+
assert (
725+
resp_json["features"][0]["properties"]["proj:epsg"]
726+
== test_item["properties"]["proj:epsg"]
727+
)
728+
729+
685730
@pytest.mark.asyncio
686731
async def test_get_missing_item_collection(app_client):
687732
"""Test reading a collection which does not exist"""
@@ -929,14 +974,6 @@ async def test_get_missing_item(app_client, load_test_data):
929974
assert resp.status_code == 404
930975

931976

932-
@pytest.mark.skip
933-
@pytest.mark.asyncio
934-
async def test_search_invalid_query_field(app_client):
935-
body = {"query": {"gsd": {"lt": 100}, "invalid-field": {"eq": 50}}}
936-
resp = await app_client.post("/search", json=body)
937-
assert resp.status_code == 400
938-
939-
940977
@pytest.mark.asyncio
941978
async def test_relative_link_construction():
942979
req = Request(

0 commit comments

Comments
 (0)