-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement queryables through proxy to online json files (#44)
* Implement queryables through proxy to online json files * lint * Guard against 404 * Add logic for dynamic calculation of collection queryable intersection * Add tests and appease mypy * Update changelog * Improve queryable test specificity * Use cache and guard against errors * Add test for /queryable 404 and use proper cache key
- Loading branch information
1 parent
61985a8
commit b50fb42
Showing
5 changed files
with
144 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import asyncio | ||
import json | ||
from typing import Any, Dict, List, Optional, Set | ||
|
||
import requests | ||
from fastapi import HTTPException, Request | ||
from stac_fastapi.types.core import AsyncBaseFiltersClient | ||
|
||
from pcstac.cache import queryables_endpoint_cache | ||
|
||
|
||
class MSPCFiltersClient(AsyncBaseFiltersClient): | ||
"""Defines a pattern for implementing the STAC filter extension.""" | ||
|
||
queryable_url_template = ( | ||
"https://planetarycomputer.microsoft.com/stac/{cid}/queryables.json" | ||
) | ||
|
||
async def get_queryable_intersection(self, request: Request) -> dict: | ||
"""Generate json schema with intersecting properties of all collections. | ||
When queryables are requested without specifying a collection (/queryable | ||
from the root), a json schema encapsulating only the properties shared by all | ||
collections should be returned. This function gathers all collection | ||
queryables, calculates the intersection, and caches the results so that the | ||
work can be saved for future queries. | ||
""" | ||
pool = request.app.state.readpool | ||
|
||
async with pool.acquire() as conn: | ||
collections = await conn.fetchval( | ||
""" | ||
SELECT * FROM all_collections(); | ||
""" | ||
) | ||
collection_ids = [collection["id"] for collection in collections] | ||
all_queryables = await asyncio.gather( | ||
*[self.get_queryables(cid) for cid in collection_ids], | ||
return_exceptions=True | ||
) | ||
all_queryables = [ | ||
queryable | ||
for queryable in all_queryables | ||
if not isinstance(queryable, Exception) | ||
] | ||
all_properties: List[dict] = [ | ||
queryable["properties"] for queryable in all_queryables | ||
] | ||
all_property_keys: List[Set[str]] = list( | ||
map(lambda x: set(x.keys()), all_properties) | ||
) | ||
intersecting_props = {} | ||
if len(all_property_keys) > 0: | ||
property_name_intersection: List[str] = list( | ||
set.intersection(*all_property_keys) | ||
) | ||
for name in property_name_intersection: | ||
for idx, properties in enumerate(all_properties): | ||
if idx == 0: | ||
maybe_match = properties[name] | ||
else: | ||
if properties[name] != maybe_match: | ||
break | ||
if maybe_match is not None: | ||
intersecting_props[name] = maybe_match | ||
|
||
intersection_schema = { | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"$id": "https://example.org/queryables", | ||
"type": "object", | ||
"title": "", | ||
"properties": intersecting_props, | ||
} | ||
return intersection_schema | ||
|
||
async def get_queryables( | ||
self, collection_id: Optional[str] = None, **kwargs: Dict[str, Any] | ||
) -> Dict[str, Any]: | ||
"""Get the queryables available for the given collection_id. | ||
If collection_id is None, returns the intersection of all | ||
queryables over all collections. | ||
This base implementation returns a blank queryable schema. This is not allowed | ||
under OGC CQL but it is allowed by the STAC API Filter Extension | ||
https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables | ||
""" | ||
if not collection_id: | ||
try: | ||
queryable_resp = queryables_endpoint_cache["/queryables"] | ||
except KeyError: | ||
request = kwargs["request"] | ||
if isinstance(request, Request): | ||
queryable_resp = await self.get_queryable_intersection(request) | ||
queryables_endpoint_cache["/queryables"] = queryable_resp | ||
else: | ||
r = requests.get(self.queryable_url_template.format(cid=collection_id)) | ||
if r.status_code == 404: | ||
raise HTTPException(status_code=404) | ||
elif r.status_code == 200: | ||
try: | ||
queryable_resp = r.json() | ||
except json.decoder.JSONDecodeError: | ||
raise HTTPException(status_code=404) | ||
return queryable_resp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from typing import Callable | ||
|
||
import pytest | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_queryables(app_client, load_test_data: Callable): | ||
resp = await app_client.get("/queryables") | ||
assert resp.status_code == 200 | ||
properties = resp.json()["properties"] | ||
assert "id" in properties | ||
assert "datetime" in properties | ||
assert "naip:year" in properties | ||
assert "naip:state" in properties | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_collection_queryables(app_client, load_test_data: Callable): | ||
resp = await app_client.get("/collections/naip/queryables") | ||
assert resp.status_code == 200 | ||
properties = resp.json()["properties"] | ||
assert "id" in properties | ||
assert "datetime" in properties | ||
assert "naip:year" in properties | ||
assert "naip:state" in properties | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_collection_queryables_404(app_client, load_test_data: Callable): | ||
resp = await app_client.get("/collections/does-not-exist/queryables") | ||
assert resp.status_code == 404 |