Skip to content

Commit

Permalink
Fix invalid bbox format for Geomet
Browse files Browse the repository at this point in the history
OGC Features compliance changes
  • Loading branch information
recalcitrantsupplant committed Sep 16, 2024
1 parent 0ea4117 commit 0b44603
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 108 deletions.
26 changes: 24 additions & 2 deletions prez/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,10 +466,10 @@ async def get_profile_nodeshape(
)


async def get_url_path(
async def get_url(
request: Request,
):
return request.url.path
return request.url


async def get_endpoint_uri(
Expand Down Expand Up @@ -547,3 +547,25 @@ async def get_template_query(
template_query = prez_system_graph.value(s, RDF.value, None)
return str(template_query)
return None


async def check_unknown_params(request: Request):
known_params = {
"_mediatype",
"page",
"limit",
"datetime",
"bbox",
"filter-lang",
"filter_crs",
"q",
"filter",
"order_by",
"order_by_direction",
}
unknown_params = set(request.query_params.keys()) - known_params
if unknown_params:
raise HTTPException(
status_code=400,
detail=f"Unknown query parameters: {', '.join(unknown_params)}",
)
7 changes: 6 additions & 1 deletion prez/models/ogc_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ class Link(BaseModel):
title: Optional[str] = None


class Links(BaseModel):
links: List[Link]


class OGCFeaturesLandingPage(BaseModel):
title: str
description: str
links: List[Link]


def generate_landing_page_links(url_path):
def generate_landing_page_links(url):
url_path = url.path
link_dicts = [
{
"href": f"{settings.system_uri}{url_path}",
Expand Down
34 changes: 17 additions & 17 deletions prez/models/query_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,40 +124,40 @@ class QueryParams:

def __init__(
self,
mediatype: Optional[str] = Query(
"text/turtle", alias="_mediatype", description="Requested mediatype"
mediatype: str = Query(
default="text/turtle", alias="_mediatype", description="Requested mediatype"
),
page: Optional[int] = Query(
1, ge=1, description="Page number, must be greater than 0"
page: int = Query(
default=1, ge=1, description="Page number, must be greater than 0"
),
limit: Optional[int] = Query(
10,
limit: int = Query(
default=10,
ge=1,
le=10000,
description="Number of items per page, must be 1<=limit<=10000",
alias="limit",
style="form",
explode=False,
),
datetime: Optional[tuple] = Depends(validate_datetime),
bbox: List[float] = Depends(reformat_bbox),
filter_lang: Optional[FilterLangEnum] = Query(
"cql2-json",
filter_lang: FilterLangEnum = Query(
default="cql2-json",
description="Language of the filter expression",
alias="filter-lang",
),
filter_crs: Optional[str] = Query(
filter_crs: str = Query(
"http://www.opengis.net/def/crs/OGC/1.3/CRS84",
description="CRS used for the filter expression",
),
q: Optional[str] = Query(None, description="Search query", example="building"),
filter: Optional[str] = Query(
None,
q: str = Query(None, description="Search query", example="building"),
filter: str = Query(
default=None,
description="CQL JSON expression.",
),
order_by: Optional[str] = Query(
None, description="Optional: Field to order by"
),
order_by_direction: Optional[OrderByDirectionEnum] = Query(
None,
order_by: str = Query(default=None, description="Optional: Field to order by"),
order_by_direction: OrderByDirectionEnum = Query(
default=None,
description="Optional: Order direction, must be 'ASC' or 'DESC'",
),
):
Expand Down
41 changes: 32 additions & 9 deletions prez/routers/ogc_features_router.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from typing import Optional, List

from fastapi import Depends, FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from starlette import status
from starlette.requests import Request
from starlette.responses import StreamingResponse, JSONResponse

from prez.dependencies import (
get_data_repo,
cql_get_parser_dependency,
get_url_path,
get_url,
get_ogc_features_mediatype,
get_system_repo,
get_endpoint_nodeshapes,
get_profile_nodeshape,
get_endpoint_uri_type,
get_ogc_features_path_params,
get_template_query,
check_unknown_params,
)
from prez.exceptions.model_exceptions import (
ClassNotFoundException,
Expand All @@ -28,7 +33,6 @@
from prez.repositories import Repo
from prez.routers.api_extras_examples import ogc_features_openapi_extras
from prez.routers.conformance import router as conformance_router
from prez.services.connegp_service import generate_link_headers
from prez.services.exception_catchers import (
catch_400,
catch_404,
Expand All @@ -39,7 +43,7 @@
catch_prefix_not_found_exception,
catch_no_profiles_exception,
)
from prez.services.listings import ogc_features_listing_function
from prez.services.listings import ogc_features_listing_function, generate_link_headers
from prez.services.objects import ogc_features_object_function
from prez.services.query_generation.cql import CQLParser
from prez.services.query_generation.shacl import NodeShape
Expand All @@ -62,15 +66,33 @@
features_subapi.include_router(conformance_router)


@features_subapi.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=jsonable_encoder(
{
"detail": exc.errors(),
"body": exc.body,
"note": {
"This error was caught as a RequestValidationError which OGC Features "
"specification specifies should be raised with a status code of 400. "
"It would otherwise be a 422 Unprocessable Entity."
},
}
),
)


@features_subapi.api_route(
"/",
summary="OGC Features API",
methods=ALLOWED_METHODS,
)
async def ogc_features_api(
url_path: str = Depends(get_url_path),
url: str = Depends(get_url),
):
links = generate_landing_page_links(url_path)
links = generate_landing_page_links(url)
link_headers = generate_link_headers(links)
lp = OGCFeaturesLandingPage(
title="OGC API - Features",
Expand Down Expand Up @@ -114,10 +136,11 @@ async def ogc_features_api(
openapi_extra=ogc_features_openapi_extras.get("feature-collection"),
)
async def listings_with_feature_collection(
validate_unknown_params: bool = Depends(check_unknown_params),
endpoint_uri_type: tuple = Depends(get_endpoint_uri_type),
endpoint_nodeshape: NodeShape = Depends(get_endpoint_nodeshapes),
profile_nodeshape: NodeShape = Depends(get_profile_nodeshape),
url_path: str = Depends(get_url_path),
url: str = Depends(get_url),
mediatype: str = Depends(get_ogc_features_mediatype),
path_params: dict = Depends(get_ogc_features_path_params),
query_params: QueryParams = Depends(),
Expand All @@ -131,7 +154,7 @@ async def listings_with_feature_collection(
endpoint_nodeshape,
profile_nodeshape,
mediatype,
url_path,
url,
data_repo,
system_repo,
cql_parser,
Expand Down Expand Up @@ -166,7 +189,7 @@ async def listings_with_feature_collection(
async def objects(
template_query: Optional[str] = Depends(get_template_query),
mediatype: str = Depends(get_ogc_features_mediatype),
url_path: str = Depends(get_url_path),
url: str = Depends(get_url),
path_params: dict = Depends(get_ogc_features_path_params),
data_repo: Repo = Depends(get_data_repo),
system_repo: Repo = Depends(get_system_repo),
Expand All @@ -175,7 +198,7 @@ async def objects(
content, headers = await ogc_features_object_function(
template_query,
mediatype,
url_path,
url,
data_repo,
system_repo,
**path_params,
Expand Down
31 changes: 0 additions & 31 deletions prez/services/connegp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,34 +342,3 @@ async def _do_query(self, query: str) -> tuple[Graph, list]:
log.debug(tabulate(table_data, headers=headers, tablefmt="grid"))

return response


def generate_ogc_features_links(url_path: str, selected_mediatype: str) -> List[Link]:
components_after_collections = url_path.split("collections")[1:]
components_len = len(components_after_collections)

if components_len == 1: # collections or a specific collection - links are the same
self_link = Link(
rel="self",
href=f"{settings.system_uri}{url_path}?{urlencode({'_mediatype': selected_mediatype})}",
type="application/json",
)

alt_links = [
Link(
rel="alternate",
href=f"{settings.system_uri}{url_path}?{urlencode({'_mediatype': mediatype})}",
type=mediatype,
)
for mediatype in RDF_MEDIATYPES
if mediatype != selected_mediatype
]
return [self_link] + alt_links
return []


def generate_link_headers(links) -> Dict[str, str]:
link_header = ", ".join(
[f'<{link.href}>; rel="{link.rel}"; type="{link.type}"' for link in links]
)
return {"Link": link_header}
4 changes: 2 additions & 2 deletions prez/services/generate_queryables.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from prez.reference_data.prez_ns import PREZ, OGCFEAT


def generate_queryables_json(item_graph, annotations_graph, url_path, endpoint_uri):
def generate_queryables_json(item_graph, annotations_graph, url, endpoint_uri):
queryable_props = {}
for queryable in item_graph.subjects():
queryable_props[str(queryable)] = QueryableProperty(
Expand All @@ -21,7 +21,7 @@ def generate_queryables_json(item_graph, annotations_graph, url_path, endpoint_u
"Local queryable properties for the collection in the OGC Features API."
)
queryable_params = {
"$id": f"{settings.system_uri}{url_path}",
"$id": f"{settings.system_uri}{url.path}",
"title": title,
"description": description,
"properties": queryable_props,
Expand Down
Loading

0 comments on commit 0b44603

Please sign in to comment.