Skip to content

Commit

Permalink
Add documentation. All tests passing.
Browse files Browse the repository at this point in the history
  • Loading branch information
recalcitrantsupplant committed Sep 27, 2024
1 parent d9298c1 commit ef366a2
Show file tree
Hide file tree
Showing 36 changed files with 568 additions and 559 deletions.
89 changes: 89 additions & 0 deletions README-OGC-Features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
Prez provides an OGC Features compliant API

The API is mounted as a sub application at `"/catalogs/{catalogId}/collections/{recordsCollectionId}/features"` by default.
It can be mounted at a different path by setting the configuration setting `ogc_features_mount_path` (or corresponding upper cased environment variable).

Queryables are a part of the OGC Features specifications which provide a listing of which parameters can be queried.
The queryables are a flat set of properties on features.

Because Prez consumes an RDF Knowledge Graph, it is desirable to query more than top level properties.
To achieve this, Prez provides a mechanism to declare paths through the graph as queryables.
To declare these paths, you can use SHACL.

An example is provided below:
```
@prefix cql: <http://www.opengis.net/doc/IS/cql2/1.0/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix dwc: <http://rs.tdwg.org/dwc/terms/> .
@prefix ex: <http://example.com/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sname: <https://fake-scientific-name-id.com/name/afd/> .
@prefix sosa: <http://www.w3.org/ns/sosa/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:BDRScientificNameQueryableShape
a sh:PropertyShape ;
a cql:Queryable ;
sh:path (
[ sh:inversePath sosa:hasFeatureOfInterest ]
sosa:hasMember
sosa:hasResult
dwc:scientificNameID
) ;
sh:name "Scientific Name" ;
dcterms:identifier "scientificname" ;
sh:datatype xsd:string ;
sh:in (
sname:001
sname:002
) ;
.
```
It is recommended that templated SPARQL queries are used to periodically update the `sh:in` values, which correspond to enumerations.
# TODO other SHACL predicates can be reused to specify min/max values, etc. where the range is numeric and enumerations are not appropriate.

When Prez starts, it will query the remote repository (typically a triplestore) for all Queryables.
It queries for them using a CONSTRUCT query, serializes this as JSON-LD, and does a minimal transformation to produce the OGC Features compliant response.
The query is:
```
"""
PREFIX cql: <http://www.opengis.net/doc/IS/cql2/1.0/>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
CONSTRUCT {
?queryable cql:id ?id ;
cql:name ?title ;
cql:datatype ?type ;
cql:enum ?enums .
}
WHERE {?queryable a cql:Queryable ;
dcterms:identifier ?id ;
sh:name ?title ;
sh:datatype ?type ;
sh:in/rdf:rest*/rdf:first ?enums ;
}
"""
```
And the output after transformation is of the form (which is the format required for OGC Features):
```
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "http://localhost:8000/catalogs/dtst:bdr/collections/syn:68a782a8-d7fe-4b3e-8377-c76c9cc245cc/features/queryables",
"type": "object",
"title": "Global Queryables",
"description": "Global queryable properties for all collections in the OGC Features API.",
"properties": {
"scientificname": {
"title": "Scientific Name",
"type": "string",
"enum": [
"https://fake-scientific-name-id.com/name/afd/001",
"https://fake-scientific-name-id.com/name/afd/002",
]
}
}
}
```

Separately, Prez internally translates the declared SHACL Property Path expression into SPARQL and injects this into queries when the queryable, e.g. `scientificname`, in the example above, is requested.
484 changes: 183 additions & 301 deletions poetry.lock

Large diffs are not rendered by default.

80 changes: 8 additions & 72 deletions prez/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from pyoxigraph import Store
from rdflib import Dataset, URIRef, Graph, SKOS, RDF
from sparql_grammar_pydantic import IRI, Var
from starlette.responses import JSONResponse

from prez.cache import (
store,
Expand All @@ -25,9 +24,8 @@
JSONMediaType,
GeoJSONMediaType,
)
from prez.models.ogc_features import Queryables
from prez.models.query_params import QueryParams
from prez.reference_data.prez_ns import ALTREXT, ONT, EP, OGCE, OGCFEAT, PREZ
from prez.reference_data.prez_ns import ALTREXT, ONT, EP, OGCE, OGCFEAT
from prez.repositories import PyoxigraphRepo, RemoteSparqlRepo, OxrdflibRepo, Repo
from prez.services.classes import get_classes_single
from prez.services.connegp_service import NegotiatedPMTs
Expand Down Expand Up @@ -140,13 +138,18 @@ async def load_annotations_data_to_oxigraph(store: Store):
store.load(file_bytes, "application/n-triples")


async def cql_post_parser_dependency(request: Request) -> CQLParser:
async def cql_post_parser_dependency(
request: Request,
queryable_props: list = Depends(get_queryable_props),
) -> CQLParser:
try:
body = await request.json()
context = json.load(
(Path(__file__).parent / "reference_data/cql/default_context.json").open()
)
cql_parser = CQLParser(cql=body, context=context)
cql_parser = CQLParser(
cql=body, context=context, queryable_props=queryable_props
)
cql_parser.generate_jsonld()
cql_parser.parse()
return cql_parser
Expand Down Expand Up @@ -579,70 +582,3 @@ async def check_unknown_params(request: Request):
status_code=400,
detail=f"Unknown query parameters: {', '.join(unknown_params)}",
)


# TODO cache this
async def generate_queryables_from_shacl_definition(
url: str = Depends(get_url),
endpoint_uri: URIRef = Depends(get_endpoint_uri),
system_repo: Repo = Depends(get_system_repo),
):
query = """
PREFIX cql: <http://www.opengis.net/doc/IS/cql2/1.0/>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
CONSTRUCT {
?queryable cql:id ?id ;
cql:name ?title ;
cql:datatype ?type ;
cql:enum ?enums .
}
WHERE {?queryable a <http://www.opengis.net/doc/IS/cql2/1.0/Queryable> ;
dcterms:identifier ?id ;
sh:name ?title ;
sh:datatype ?type ;
sh:in/rdf:rest*/rdf:first ?enums ;
}
"""
g, _ = await system_repo.send_queries([query], [])
if len(g) == 0:
return JSONResponse(
content={"detail": "No queryables found for this endpoint"},
status_code=404,
)
jsonld_string = g.serialize(format="json-ld")
jsonld = json.loads(jsonld_string)
queryable_props = {}
for item in jsonld:
id_value = item["http://www.opengis.net/doc/IS/cql2/1.0/id"][0]["@value"]
queryable_props[id_value] = {
"title": item["http://www.opengis.net/doc/IS/cql2/1.0/name"][0]["@value"],
"type": item["http://www.opengis.net/doc/IS/cql2/1.0/datatype"][0][
"@id"
].split("#")[
-1
], # hack
"enum": [
enum_item["@id"]
for enum_item in item["http://www.opengis.net/doc/IS/cql2/1.0/enum"]
],
}
if endpoint_uri == OGCFEAT["queryables-global"]:
title = "Global Queryables"
description = (
"Global queryable properties for all collections in the OGC Features API."
)
else:
title = "Local Queryables"
description = (
"Local queryable properties for the collection in the OGC Features API."
)
queryable_params = {
"$id": f"{settings.system_uri}{url.path}",
"title": title,
"description": description,
"properties": queryable_props,
}
return Queryables(**queryable_params).model_dump(exclude_none=True)
4 changes: 1 addition & 3 deletions prez/renderers/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
from prez.renderers.csv_renderer import render_csv_dropdown
from prez.renderers.json_renderer import render_json_dropdown, NotFoundError
from prez.repositories import Repo
from prez.services.annotations import (
get_annotation_properties,
)
from prez.services.annotations import get_annotation_properties
from prez.services.connegp_service import RDF_MEDIATYPES, RDF_SERIALIZER_TYPES_MAP
from prez.services.curie_functions import get_curie_id_for_uri

Expand Down
12 changes: 2 additions & 10 deletions prez/routers/ogc_features_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
get_system_repo,
get_endpoint_nodeshapes,
get_profile_nodeshape,
get_endpoint_uri_type,
get_ogc_features_path_params,
get_template_query,
check_unknown_params,
generate_queryables_from_shacl_definition,
get_queryable_props,
get_endpoint_uri_type,
)
from prez.exceptions.model_exceptions import (
ClassNotFoundException,
Expand Down Expand Up @@ -126,12 +124,6 @@ async def ogc_features_api(
name=OGCFEAT["queryables-local"],
openapi_extra=ogc_features_openapi_extras.get("feature-collection"),
)
async def queryables(
queryables: dict = Depends(generate_queryables_from_shacl_definition),
):
return queryables


@features_subapi.api_route(
"/collections",
methods=ALLOWED_METHODS,
Expand All @@ -145,7 +137,7 @@ async def queryables(
)
async def listings_with_feature_collection(
validate_unknown_params: bool = Depends(check_unknown_params),
endpoint_uri_type: tuple = Depends(get_endpoint_uri_type),
endpoint_uri_type: str = Depends(get_endpoint_uri_type),
endpoint_nodeshape: NodeShape = Depends(get_endpoint_nodeshapes),
profile_nodeshape: NodeShape = Depends(get_profile_nodeshape),
url: str = Depends(get_url),
Expand Down
4 changes: 0 additions & 4 deletions prez/services/generate_queryables.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from fastapi import Depends

from prez.config import settings
from prez.dependencies import get_system_repo
from prez.models.ogc_features import QueryableProperty, Queryables
from prez.reference_data.prez_ns import PREZ, OGCFEAT
from prez.repositories import Repo


def generate_queryables_json(item_graph, annotations_graph, url, endpoint_uri):
Expand Down
Loading

0 comments on commit ef366a2

Please sign in to comment.