diff --git a/README.md b/README.md index e9bd32dc..075a9a78 100644 --- a/README.md +++ b/README.md @@ -84,19 +84,20 @@ via python-dotenv, or directly in the environment in which Prez is run. The envi instantiate a Pydantic `Settings` object which is used throughout Prez to configure its behaviour. To see how prez interprets/uses these environment variables see the `prez/config.py` file. -| Environment Variable | Description | -|-------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| SPARQL_ENDPOINT | Read-only SPARQL endpoint for SpacePrez | -| SPARQL_USERNAME | A username for Basic Auth against the SPARQL endpoint, if required by the SPARQL endpoint. | -| SPARQL_PASSWORD | A password for Basic Auth against the SPARQL endpoint, if required by the SPARQL endpoint. | -| PROTOCOL | The protocol used to deliver Prez. Usually 'http'. | -| HOST | The host on which to server prez, typically 'localhost'. | -| PORT | The port Prez is made accessible on. Default is 8000, could be 80 or anything else that your system has permission to use | -| SYSTEM_URI | Documentation property. An IRI for the Prez system as a whole. This value appears in the landing page RDF delivered by Prez ('/') | -| LOG_LEVEL | One of CRITICAL, ERROR, WARNING, INFO, DEBUG. Defaults to INFO. | -| LOG_OUTPUT | "file", "stdout", or "both" ("file" and "stdout"). Defaults to stdout. | -| PREZ_TITLE | The title to use for Prez instance | -| PREZ_DESC | A description to use for the Prez instance | +| Environment Variable | Description | +|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| SPARQL_ENDPOINT | Read-only SPARQL endpoint for SpacePrez | +| SPARQL_USERNAME | A username for Basic Auth against the SPARQL endpoint, if required by the SPARQL endpoint. | +| SPARQL_PASSWORD | A password for Basic Auth against the SPARQL endpoint, if required by the SPARQL endpoint. | +| PROTOCOL | The protocol used to deliver Prez. Usually 'http'. | +| HOST | The host on which to server prez, typically 'localhost'. | +| PORT | The port Prez is made accessible on. Default is 8000, could be 80 or anything else that your system has permission to use | +| SYSTEM_URI | Documentation property. An IRI for the Prez system as a whole. This value appears in the landing page RDF delivered by Prez ('/') | +| LOG_LEVEL | One of CRITICAL, ERROR, WARNING, INFO, DEBUG. Defaults to INFO. | +| LOG_OUTPUT | "file", "stdout", or "both" ("file" and "stdout"). Defaults to stdout. | +| PREZ_TITLE | The title to use for Prez instance | +| PREZ_DESC | A description to use for the Prez instance | +| DISABLE_PREFIX_GENERATION | Default value is `false`. Very large datasets may want to disable this setting and provide a predefined set of prefixes for namespaces as described in [Link Generation](README-Dev.md#link-generation). | ### Running in a Container diff --git a/prez/app.py b/prez/app.py index 6736f489..530f669e 100644 --- a/prez/app.py +++ b/prez/app.py @@ -25,6 +25,7 @@ from prez.routers.spaceprez import router as spaceprez_router from prez.routers.sparql import router as sparql_router from prez.routers.vocprez import router as vocprez_router +from prez.routers.curie import router as curie_router from prez.services.app_service import healthcheck_sparql_endpoints, count_objects from prez.services.app_service import populate_api_info, add_prefixes_to_prefix_graph from prez.services.exception_catchers import ( @@ -60,6 +61,7 @@ app.include_router(catprez_router) app.include_router(vocprez_router) app.include_router(spaceprez_router) +app.include_router(curie_router) @app.middleware("http") diff --git a/prez/bnode.py b/prez/bnode.py new file mode 100644 index 00000000..acc195ae --- /dev/null +++ b/prez/bnode.py @@ -0,0 +1,23 @@ +from rdflib import Graph, URIRef, BNode + + +def get_bnode_depth( + graph: Graph, node: URIRef | BNode = None, depth: int = 0, seen: list[BNode] = None +) -> int: + """Get the max blank node depth of the node in the graph. + + This is a recursive function. + + >>> graph = Graph().parse(...) + >>> depth = get_bnode_depth(graph, URIRef("node-name")) + """ + if seen is None: + seen = [] + + if isinstance(node, BNode) or depth == 0: + for o in graph.objects(node, None): + if isinstance(o, BNode) and o not in seen: + seen.append(o) + depth = get_bnode_depth(graph, o, depth + 1, seen) + + return depth diff --git a/prez/config.py b/prez/config.py index 619e0d1b..486f642e 100644 --- a/prez/config.py +++ b/prez/config.py @@ -74,6 +74,7 @@ class Settings(BaseSettings): "Knowledge Graph data which can be subset according to information profiles." ) prez_version: Optional[str] + disable_prefix_generation: bool = False @root_validator() def check_endpoint_enabled(cls, values): diff --git a/prez/models/profiles_item.py b/prez/models/profiles_item.py index 86fd8ea1..4bff683c 100644 --- a/prez/models/profiles_item.py +++ b/prez/models/profiles_item.py @@ -39,5 +39,8 @@ def populate(cls, values): if len(r.bindings) > 0: values["classes"] = frozenset([prof.get("class") for prof in r.bindings]) - values["label"] = profiles_graph_cache.value(URIRef(values["uri"]), URIRef("http://www.w3.org/ns/dx/conneg/altr-ext#hasLabelPredicate")) + values["label"] = profiles_graph_cache.value( + URIRef(values["uri"]), + URIRef("http://www.w3.org/ns/dx/conneg/altr-ext#hasLabelPredicate"), + ) return values diff --git a/prez/models/vocprez_item.py b/prez/models/vocprez_item.py index e0f182bc..2f7f76d6 100644 --- a/prez/models/vocprez_item.py +++ b/prez/models/vocprez_item.py @@ -10,7 +10,7 @@ class VocabItem(BaseModel): uri: Optional[URIRef] = None - classes: Optional[Set[URIRef]] + classes: Optional[frozenset[URIRef]] curie_id: Optional[str] = None general_class: Optional[URIRef] = None scheme_curie: Optional[str] = None @@ -29,7 +29,6 @@ def __hash__(self): @root_validator def populate(cls, values): url_path = values.get("url_path") - uri = values.get("uri") concept_curie = values.get("concept_curie") scheme_curie = values.get("scheme_curie") collection_curie = values.get("collection_curie") @@ -38,7 +37,7 @@ def populate(cls, values): return values if url_path in ["/object", "/v/object"]: values["link_constructor"] = f"/v/object?uri=" - elif len(url_parts) == 5: # concepts + elif len(url_parts) == 5 and "/all" not in url_path: # concepts values["general_class"] = SKOS.Concept if scheme_curie: values["curie_id"] = concept_curie diff --git a/prez/queries/object.py b/prez/queries/object.py new file mode 100644 index 00000000..3ed69a5b --- /dev/null +++ b/prez/queries/object.py @@ -0,0 +1,33 @@ +from textwrap import dedent + +from jinja2 import Template + + +def object_inbound_query(iri: str, predicate: str) -> str: + query = Template( + """ + SELECT (COUNT(?iri) as ?count) + WHERE { + BIND(<{{ iri }}> as ?iri) + + ?other <{{ predicate }}> ?iri . + } + """ + ).render(iri=iri, predicate=predicate) + + return dedent(query) + + +def object_outbound_query(iri: str, predicate: str) -> str: + query = Template( + """ + SELECT (COUNT(?iri) as ?count) + WHERE { + BIND(<{{ iri }}> as ?iri) + + ?iri <{{ predicate }}> ?other . + } + """ + ).render(iri=iri, predicate=predicate) + + return dedent(query) diff --git a/prez/queries/vocprez.py b/prez/queries/vocprez.py new file mode 100644 index 00000000..7378fac7 --- /dev/null +++ b/prez/queries/vocprez.py @@ -0,0 +1,150 @@ +from textwrap import dedent + +from jinja2 import Template + + +def get_concept_scheme_query(iri: str, bnode_depth: int) -> str: + query = Template( + """ + PREFIX prez: + PREFIX skos: + + CONSTRUCT { + ?iri ?p ?o . + + {% if bnode_depth > 0 +%} + ?iri ?p0 ?o0 . + {% endif %} + + {% for i in range(bnode_depth) %} + ?o{{ i }} ?p{{ i + 1 }} ?o{{ i + 1 }} . + {% endfor %} + + ?iri prez:childrenCount ?childrenCount . + } + WHERE { + BIND(<{{ iri }}> as ?iri) + ?iri ?p ?o . + FILTER (?p != skos:hasTopConcept) + + { + SELECT (COUNT(?topConcept) AS ?childrenCount) + WHERE { + BIND(<{{ iri }}> as ?iri) + ?iri skos:hasTopConcept ?topConcept . + } + } + + {% if bnode_depth > 0 %} + ?iri ?p0 ?o0 . + {% endif %} + + {% for i in range(bnode_depth) %} + ?o{{ i }} ?p{{ i + 1 }} ?o{{ i + 1 }} . + FILTER (isBlank(?o0)) + {% endfor %} + } + """ + ).render(iri=iri, bnode_depth=bnode_depth) + + return dedent(query) + + +def get_concept_scheme_top_concepts_query(iri: str, page: int, per_page: int) -> str: + query = Template( + """ + PREFIX prez: + PREFIX rdf: + PREFIX rdfs: + PREFIX skos: + + CONSTRUCT { + ?concept skos:prefLabel ?label . + ?concept prez:childrenCount ?narrowerChildrenCount . + ?iri prez:childrenCount ?childrenCount . + ?iri skos:hasTopConcept ?concept . + } + WHERE { + BIND(<{{ iri }}> as ?iri) + ?iri skos:hasTopConcept ?concept . + ?concept skos:prefLabel ?label . + + { + SELECT (COUNT(?childConcept) AS ?childrenCount) + WHERE { + BIND(<{{ iri }}> as ?iri) + ?iri skos:hasTopConcept ?childConcept . + } + } + + { + SELECT ?concept ?label (COUNT(?narrowerConcept) AS ?narrowerChildrenCount) + WHERE { + BIND(<{{ iri }}> as ?iri) + ?iri skos:hasTopConcept ?concept . + ?concept skos:prefLabel ?label . + + OPTIONAL { + ?narrowerConcept skos:broader ?concept . + } + } + GROUP BY ?concept ?label + ORDER BY str(?label) + LIMIT {{ limit }} + OFFSET {{ offset }} + } + } + """ + ).render(iri=iri, limit=per_page, offset=(page - 1) * per_page) + + return dedent(query) + + +def get_concept_narrowers_query(iri: str, page: int, per_page: int) -> str: + query = Template( + """ + PREFIX prez: + PREFIX rdf: + PREFIX rdfs: + PREFIX skos: + + CONSTRUCT { + ?concept skos:prefLabel ?label . + ?concept prez:childrenCount ?narrowerChildrenCount . + ?iri prez:childrenCount ?childrenCount . + ?iri skos:narrower ?concept . + } + WHERE { + BIND(<{{ iri }}> as ?iri) + ?concept skos:broader ?iri . + ?concept skos:prefLabel ?label . + + { + SELECT (COUNT(?childConcept) AS ?childrenCount) + WHERE { + BIND(<{{ iri }}> as ?iri) + ?childConcept skos:broader ?iri . + } + } + + { + SELECT ?concept ?label (COUNT(?narrowerConcept) AS ?narrowerChildrenCount) + WHERE { + BIND(<{{ iri }}> as ?iri) + ?concept skos:broader ?iri . + ?concept skos:prefLabel ?label . + + OPTIONAL { + ?narrowerConcept skos:broader ?concept . + } + } + GROUP BY ?concept ?label + ORDER BY str(?label) + LIMIT {{ limit }} + OFFSET {{ offset }} + } + } + """ + ).render(iri=iri, limit=per_page, offset=(page - 1) * per_page) + + return dedent(query) diff --git a/prez/renderers/renderer.py b/prez/renderers/renderer.py index 56eb58a2..78ee6d81 100644 --- a/prez/renderers/renderer.py +++ b/prez/renderers/renderer.py @@ -1,6 +1,6 @@ import io import logging -from typing import Optional, Dict +from typing import Optional from connegp import RDF_MEDIATYPES, RDF_SERIALIZER_TYPES_MAP from fastapi.responses import StreamingResponse @@ -28,7 +28,7 @@ async def return_from_queries( mediatype, profile, profile_headers, - predicates_for_link_addition: Dict = {}, + predicates_for_link_addition: dict = None, ): """ Executes SPARQL queries, loads these to RDFLib Graphs, and calls the "return_from_graph" function to return the @@ -45,7 +45,7 @@ async def return_from_graph( mediatype, profile, profile_headers, - predicates_for_link_addition: dict = {}, + predicates_for_link_addition: dict = None, ): profile_headers["Content-Disposition"] = "inline" if str(mediatype) in RDF_MEDIATYPES: @@ -88,7 +88,12 @@ async def return_annotated_rdf( queries_for_uncached, annotations_graph = await get_annotation_properties( graph, **profile_annotation_props ) - anots_from_triplestore = await queries_to_graph([queries_for_uncached]) + + if queries_for_uncached is None: + anots_from_triplestore = Graph() + else: + anots_from_triplestore = await queries_to_graph([queries_for_uncached]) + if len(anots_from_triplestore) > 1: annotations_graph += anots_from_triplestore cache += anots_from_triplestore diff --git a/prez/response.py b/prez/response.py new file mode 100644 index 00000000..23d7a9b1 --- /dev/null +++ b/prez/response.py @@ -0,0 +1,12 @@ +from fastapi.responses import StreamingResponse + + +class StreamingTurtleResponse(StreamingResponse): + media_type = "text/turtle" + + def render(self, content: str) -> bytes: + return content.encode("utf-8") + + +class StreamingTurtleAnnotatedResponse(StreamingTurtleResponse): + media_type = "text/anot+turtle" diff --git a/prez/routers/curie.py b/prez/routers/curie.py new file mode 100644 index 00000000..8cff8c83 --- /dev/null +++ b/prez/routers/curie.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter, HTTPException, status +from fastapi.responses import PlainTextResponse +from rdflib import URIRef +from rdflib.term import _is_valid_uri + +from prez.services.curie_functions import get_uri_for_curie_id, get_curie_id_for_uri + +router = APIRouter(tags=["Identifier Resolution"]) + + +@router.get( + "/identifier/curie/{iri:path}", + summary="Get the IRI's CURIE identifier", + response_class=PlainTextResponse, + responses={ + status.HTTP_400_BAD_REQUEST: {"content": {"application/json": {}}}, + status.HTTP_500_INTERNAL_SERVER_ERROR: {"content": {"application/json": {}}}, + }, +) +def get_curie_route(iri: str): + if not _is_valid_uri(iri): + raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Invalid characters in {iri}") + try: + return get_curie_id_for_uri(URIRef(iri)) + except ValueError as err: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, f"Error processing IRI {iri}" + ) from err + except Exception as err: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + f"Unhandled server error for IRI {iri}", + ) from err + + +@router.get( + "/identifier/iri/{curie}", + summary="Get the CURIE identifier's fully qualified IRI", + response_class=PlainTextResponse, + responses={ + status.HTTP_400_BAD_REQUEST: {"content": {"application/json": {}}}, + status.HTTP_500_INTERNAL_SERVER_ERROR: {"content": {"application/json": {}}}, + }, +) +def get_iri_route(curie: str): + try: + return get_uri_for_curie_id(curie) + except ValueError as err: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, f"Invalid input '{curie}'. {err}" + ) from err + except Exception as err: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + f"Unhandled server error for curie {curie}", + ) from err diff --git a/prez/routers/object.py b/prez/routers/object.py index 7a76966e..8f9a941d 100644 --- a/prez/routers/object.py +++ b/prez/routers/object.py @@ -1,11 +1,71 @@ -from fastapi import APIRouter, Request +from fastapi import APIRouter, Request, HTTPException, status, Query from starlette.responses import PlainTextResponse from prez.models import SpatialItem, VocabItem, CatalogItem +from prez.routers.curie import get_iri_route +from prez.sparql.methods import sparql_query_non_async +from prez.queries.object import object_inbound_query, object_outbound_query router = APIRouter(tags=["Object"]) +@router.get( + "/count", summary="Get object's statement count", response_class=PlainTextResponse +) +def count_route( + curie: str, + inbound: str = Query( + None, + examples={ + "skos:inScheme": { + "summary": "skos:inScheme", + "value": "http://www.w3.org/2004/02/skos/core#inScheme", + }, + "skos:topConceptOf": { + "summary": "skos:topConceptOf", + "value": "http://www.w3.org/2004/02/skos/core#topConceptOf", + }, + "empty": {"summary": "Empty", "value": None}, + }, + ), + outbound: str = Query( + None, + examples={ + "empty": {"summary": "Empty", "value": None}, + "skos:hasTopConcept": { + "summary": "skos:hasTopConcept", + "value": "http://www.w3.org/2004/02/skos/core#hasTopConcept", + }, + }, + ), +): + """Get an Object's statements count based on the inbound or outbound predicate""" + iri = get_iri_route(curie) + + if inbound is None and outbound is None: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + "At least 'inbound' or 'outbound' is supplied a valid IRI.", + ) + + if inbound and outbound: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + "Only provide one value for either 'inbound' or 'outbound', not both.", + ) + + if inbound is not None: + query = object_inbound_query(iri, inbound) + _, rows = sparql_query_non_async(query) + for row in rows: + return row["count"]["value"] + + query = object_outbound_query(iri, outbound) + _, rows = sparql_query_non_async(query) + for row in rows: + return row["count"]["value"] + + @router.get("/object", summary="Object") async def object( request: Request, @@ -29,7 +89,9 @@ async def object( try: item = prez_items[prez](uri=uri, url_path="/object") returned_items[prez] = item - except Exception: # will get exception if URI does not exist with classes in prez flavour's SPARQL endpoint + except ( + Exception + ): # will get exception if URI does not exist with classes in prez flavour's SPARQL endpoint pass if len(returned_items) == 0: return PlainTextResponse( diff --git a/prez/routers/vocprez.py b/prez/routers/vocprez.py index 8d630980..8b50d71c 100644 --- a/prez/routers/vocprez.py +++ b/prez/routers/vocprez.py @@ -2,18 +2,34 @@ from typing import Optional from fastapi import APIRouter, Request -from rdflib import URIRef +from rdflib import URIRef, SKOS, Literal, DCTERMS from starlette.responses import PlainTextResponse from prez.models.profiles_and_mediatypes import ProfilesMediatypesInfo from prez.models.vocprez_item import VocabItem from prez.models.vocprez_listings import VocabMembers -from prez.renderers.renderer import return_from_queries, return_profiles +from prez.reference_data.prez_ns import PREZ +from prez.renderers.renderer import ( + return_from_queries, + return_profiles, + return_from_graph, +) +from prez.services.curie_functions import get_curie_id_for_uri +from prez.sparql.methods import queries_to_graph from prez.sparql.objects_listings import ( generate_listing_construct, generate_listing_count_construct, generate_item_construct, ) +from prez.sparql.resource import get_resource +from prez.bnode import get_bnode_depth +from prez.queries.vocprez import ( + get_concept_scheme_query, + get_concept_scheme_top_concepts_query, + get_concept_narrowers_query, +) +from prez.response import StreamingTurtleAnnotatedResponse +from prez.routers.curie import get_iri_route router = APIRouter(tags=["VocPrez"]) @@ -26,7 +42,6 @@ async def vocprez_home(): @router.get("/v/collection", summary="List Collections") -@router.get("/v/scheme", summary="List ConceptSchemes") @router.get("/v/vocab", summary="List Vocabularies") async def schemes_endpoint( request: Request, @@ -59,12 +74,203 @@ async def schemes_endpoint( ) -@router.get("/v/vocab/{scheme_curie}", summary="Get ConceptScheme") -@router.get("/v/scheme/{scheme_curie}", summary="Get ConceptScheme") +@router.get( + "/v/vocab/{scheme_curie}/all", summary="Get Concept Scheme and all its concepts" +) async def vocprez_scheme(request: Request, scheme_curie: str): + """Get a SKOS Concept Scheme and all of its concepts. + + Note: This may be a very expensive operation depending on the size of the concept scheme. + """ return await item_endpoint(request) +@router.get( + "/v/vocab/{concept_scheme_curie}", + summary="Get a SKOS Concept Scheme", + response_class=StreamingTurtleAnnotatedResponse, + responses={ + 200: { + "content": {"text/turtle": {}}, + }, + }, +) +async def concept_scheme_route(request: Request, concept_scheme_curie: str): + """Get a SKOS Concept Scheme. + + `prez:childrenCount` is an `xsd:integer` count of the number of top concepts for this Concept Scheme. + """ + profiles_mediatypes_info = ProfilesMediatypesInfo( + request=request, classes=frozenset([SKOS.ConceptScheme]) + ) + + iri = get_iri_route(concept_scheme_curie) + resource = await get_resource(iri) + bnode_depth = get_bnode_depth(resource, iri) + concept_scheme_query = get_concept_scheme_query(iri, bnode_depth) + + return await return_from_queries( + [concept_scheme_query], + profiles_mediatypes_info.mediatype, + profiles_mediatypes_info.profile, + profiles_mediatypes_info.profile_headers, + ) + + +@router.get( + "/v/vocab/{concept_scheme_curie}/top-concepts", + summary="Get a SKOS Concept Scheme's top concepts", + response_class=StreamingTurtleAnnotatedResponse, + responses={ + 200: { + "content": {"text/turtle": {}}, + }, + }, +) +async def concept_scheme_top_concepts_route( + request: Request, + concept_scheme_curie: str, + page: int = 1, + per_page: int = 20, +): + """Get a SKOS Concept Scheme's top concepts. + + `prez:childrenCount` is an `xsd:integer` count of the number of top concepts for this Concept Scheme. + """ + profiles_mediatypes_info = ProfilesMediatypesInfo( + request=request, classes=frozenset([SKOS.ConceptScheme]) + ) + + iri = get_iri_route(concept_scheme_curie) + concept_scheme_top_concepts_query = get_concept_scheme_top_concepts_query( + iri, page, per_page + ) + + graph = await queries_to_graph([concept_scheme_top_concepts_query]) + for concept in graph.objects(iri, SKOS.hasTopConcept): + if isinstance(concept, URIRef): + concept_curie = get_curie_id_for_uri(concept) + graph.add( + ( + concept, + PREZ.link, + Literal(f"/v/vocab/{concept_scheme_curie}/{concept_curie}"), + ) + ) + graph.add( + ( + concept, + DCTERMS.identifier, + Literal(concept_curie, datatype=PREZ.identifier), + ) + ) + + return await return_from_graph( + graph, + profiles_mediatypes_info.mediatype, + profiles_mediatypes_info.profile, + profiles_mediatypes_info.profile_headers, + ) + + +@router.get( + "/v/vocab/{concept_scheme_curie}/{concept_curie}/narrowers", + summary="Get a SKOS Concept's narrower concepts", + response_class=StreamingTurtleAnnotatedResponse, + responses={ + 200: { + "content": {"text/turtle": {}}, + }, + }, +) +async def concept_narrowers_route( + request: Request, + concept_scheme_curie: str, + concept_curie: str, + page: int = 1, + per_page: int = 20, +): + """Get a SKOS Concept's narrower concepts. + + `prez:childrenCount` is an `xsd:integer` count of the number of narrower concepts for this concept. + """ + profiles_mediatypes_info = ProfilesMediatypesInfo( + request=request, classes=frozenset([SKOS.Concept]) + ) + + iri = get_iri_route(concept_curie) + concept_narrowers_query = get_concept_narrowers_query(iri, page, per_page) + + graph = await queries_to_graph([concept_narrowers_query]) + for concept in graph.objects(iri, SKOS.narrower): + if isinstance(concept, URIRef): + concept_curie = get_curie_id_for_uri(concept) + graph.add( + ( + concept, + PREZ.link, + Literal(f"/v/vocab/{concept_scheme_curie}/{concept_curie}"), + ) + ) + graph.add( + ( + concept, + DCTERMS.identifier, + Literal(concept_curie, datatype=PREZ.identifier), + ) + ) + + return await return_from_graph( + graph, + profiles_mediatypes_info.mediatype, + profiles_mediatypes_info.profile, + profiles_mediatypes_info.profile_headers, + ) + + +@router.get( + "/v/vocab/{concept_scheme_curie}/{concept_curie}", + summary="Get a SKOS Concept", + response_class=StreamingTurtleAnnotatedResponse, + responses={ + 200: { + "content": {"text/turtle": {}}, + }, + }, +) +async def concept_route( + request: Request, concept_scheme_curie: str, concept_curie: str +): + """Get a SKOS Concept.""" + profiles_mediatypes_info = ProfilesMediatypesInfo( + request=request, classes=frozenset([SKOS.Concept]) + ) + + concept_iri = get_iri_route(concept_curie) + graph = await get_resource(concept_iri) + graph.add( + ( + concept_iri, + PREZ.link, + Literal(f"/v/vocab/{concept_scheme_curie}/{concept_curie}"), + ) + ) + graph.add( + ( + concept_iri, + DCTERMS.identifier, + Literal(concept_curie, datatype=PREZ.identifier), + ) + ) + + return await return_from_graph( + graph, + profiles_mediatypes_info.mediatype, + profiles_mediatypes_info.profile, + profiles_mediatypes_info.profile_headers, + ) + + @router.get("/v/collection/{collection_curie}", summary="Get Collection") async def vocprez_collection(request: Request, collection_curie: str): return await item_endpoint(request) @@ -77,14 +283,6 @@ async def vocprez_collection_concept( return await item_endpoint(request) -@router.get("/v/scheme/{scheme_curie}/{concept_curie}", summary="Get Concept") -@router.get("/v/vocab/{scheme_curie}/{concept_curie}", summary="Get Concept") -async def vocprez_scheme_concept( - request: Request, scheme_curie: str, concept_curie: str -): - return await item_endpoint(request) - - @router.get("/v/object", summary="Get VocPrez Object") async def item_endpoint(request: Request, vp_item: Optional[VocabItem] = None): """Returns a VocPrez skos:Concept, Collection, Vocabulary, or ConceptScheme in the requested profile & mediatype""" @@ -93,7 +291,7 @@ async def item_endpoint(request: Request, vp_item: Optional[VocabItem] = None): vp_item = VocabItem( **request.path_params, **request.query_params, - url_path=str(request.url.path) + url_path=str(request.url.path), ) prof_and_mt_info = ProfilesMediatypesInfo(request=request, classes=vp_item.classes) vp_item.selected_class = prof_and_mt_info.selected_class diff --git a/prez/services/app_service.py b/prez/services/app_service.py index bb93956a..da698239 100644 --- a/prez/services/app_service.py +++ b/prez/services/app_service.py @@ -13,8 +13,9 @@ ) from prez.config import settings from prez.reference_data.prez_ns import PREZ, ALTREXT -from prez.sparql.methods import query_to_graph +from prez.sparql.methods import query_to_graph, sparql_query_non_async from prez.sparql.objects_listings import startup_count_objects +from prez.services.curie_functions import get_curie_id_for_uri log = logging.getLogger(__name__) @@ -98,3 +99,31 @@ async def add_prefixes_to_prefix_graph(): f'"{f.name}"' ) log.info("Prefixes from local files added to prefix graph") + + if settings.disable_prefix_generation: + log.info("DISABLE_PREFIX_GENERATION set to false. Skipping prefix generation.") + else: + query = """ + SELECT DISTINCT ?iri + WHERE { + ?iri ?p ?o . + FILTER(isIRI(?iri)) + } + """ + + success, results = sparql_query_non_async(query) + iris = [iri["iri"]["value"] for iri in results] + skipped_count = 0 + skipped = [] + for iri in iris: + try: + get_curie_id_for_uri(iri) + except ValueError: + skipped_count += 1 + skipped.append(iri) + + log.info( + f"Generated prefixes for {len(iris)} IRIs. Skipped {skipped_count} IRIs." + ) + for skipped_iri in skipped: + log.info(f"Skipped IRI {skipped_iri}") diff --git a/prez/services/model_methods.py b/prez/services/model_methods.py index cb377d72..e830c3c8 100644 --- a/prez/services/model_methods.py +++ b/prez/services/model_methods.py @@ -6,7 +6,9 @@ from prez.sparql.methods import sparql_query_non_async, sparql_ask_non_async -def get_classes(uri: URIRef, parent_predicates: List[URIRef] = None): +def get_classes( + uri: URIRef, parent_predicates: List[URIRef] = None +) -> frozenset[URIRef]: q = f""" SELECT ?class {{<{uri}> a ?class . }} diff --git a/prez/services/triplestore_client.py b/prez/services/triplestore_client.py deleted file mode 100644 index a6ecfe0c..00000000 --- a/prez/services/triplestore_client.py +++ /dev/null @@ -1,6 +0,0 @@ -from httpx import AsyncClient -from prez.config import settings - -sparql_client = AsyncClient( - auth=(settings.sparql_username, settings.sparql_password), -) diff --git a/prez/sparql/methods.py b/prez/sparql/methods.py index bbdca33c..388150f8 100644 --- a/prez/sparql/methods.py +++ b/prez/sparql/methods.py @@ -124,7 +124,7 @@ async def query_to_graph(query: str): return g.parse(data=response.text, format="turtle") -async def queries_to_graph(queries: List[str]): +async def queries_to_graph(queries: List[str]) -> Graph: """ Sends multiple SPARQL queries asynchronously and parses the responses into an RDFLib Graph. Args: queries: List[str]: A list of SPARQL queries to be sent asynchronously. diff --git a/prez/sparql/resource.py b/prez/sparql/resource.py new file mode 100644 index 00000000..955c4e6a --- /dev/null +++ b/prez/sparql/resource.py @@ -0,0 +1,8 @@ +from rdflib import Graph + +from prez.sparql.methods import query_to_graph + + +async def get_resource(iri: str) -> Graph: + query = f"""DESCRIBE <{iri}>""" + return await query_to_graph(query) diff --git a/tests/curies/test_curie_endpoint.py b/tests/curies/test_curie_endpoint.py new file mode 100644 index 00000000..2909374a --- /dev/null +++ b/tests/curies/test_curie_endpoint.py @@ -0,0 +1,44 @@ +import pytest +from fastapi.testclient import TestClient + +from prez.app import app + + +@pytest.fixture +def client() -> TestClient: + testclient = TestClient(app) + + # Make a request for the following IRI to ensure + # the curie is available in the 'test_curie' test. + iri = "http://example.com/namespace/test" + response = testclient.get(f"/identifier/curie/{iri}") + assert response.status_code == 200 + assert response.text == "nmspc:test" + + return testclient + + +@pytest.mark.parametrize( + "iri, expected_status_code", + [ + ["d", 400], + ["http://!", 400], + ["http://example.com/namespace", 200], + ], +) +def test_iri(iri: str, expected_status_code: int, client: TestClient): + response = client.get(f"/identifier/curie/{iri}") + assert response.status_code == expected_status_code + + +@pytest.mark.parametrize( + "curie, expected_status_code", + [ + ["d", 400], + ["ns1", 400], + ["nmspc:test", 200], + ], +) +def test_curie(curie: str, expected_status_code: int, client: TestClient): + response = client.get(f"/identifier/iri/{curie}") + assert response.status_code == expected_status_code diff --git a/tests/data/bnode_depth/bnode_depth-1.ttl b/tests/data/bnode_depth/bnode_depth-1.ttl new file mode 100644 index 00000000..96ac0525 --- /dev/null +++ b/tests/data/bnode_depth/bnode_depth-1.ttl @@ -0,0 +1,14 @@ +PREFIX dcat: +PREFIX dcterms: +PREFIX isoroles: +PREFIX prov: +PREFIX schema: +PREFIX skos: +PREFIX xsd: + + + a dcat:Catalog ; + schema:member [ + schema:name "123" ; + ] ; +. \ No newline at end of file diff --git a/tests/data/bnode_depth/bnode_depth-2.ttl b/tests/data/bnode_depth/bnode_depth-2.ttl new file mode 100644 index 00000000..17f3f133 --- /dev/null +++ b/tests/data/bnode_depth/bnode_depth-2.ttl @@ -0,0 +1,17 @@ +PREFIX dcat: +PREFIX dcterms: +PREFIX isoroles: +PREFIX prov: +PREFIX schema: +PREFIX skos: +PREFIX xsd: + + + a dcat:Catalog ; + schema:member [ + schema:name "123" ; + schema:member [ + schema:name "456" + ] ; + ] ; +. \ No newline at end of file diff --git a/tests/data/bnode_depth/bnode_depth-4.ttl b/tests/data/bnode_depth/bnode_depth-4.ttl new file mode 100644 index 00000000..c9957602 --- /dev/null +++ b/tests/data/bnode_depth/bnode_depth-4.ttl @@ -0,0 +1,39 @@ +PREFIX dcat: +PREFIX dcterms: +PREFIX isoroles: +PREFIX prov: +PREFIX rdfs: +PREFIX schema: +PREFIX skos: +PREFIX xsd: + + a schema:Thing . + + + a dcat:Catalog ; + dcterms:created "2022-07-31"^^xsd:date ; + dcterms:description """The Indigenous Data Network's demonstration catalogue of datasets. This catalogue contains records of datasets in Australia, most of which have some relation to indigenous Australia. +The purpose of this catalogue is not to act as a master catalogue of indigenous data in Australia to demonstrate improved metadata models and rating systems for data and metadata in order to improve indigenous data governance. +The content of this catalogue conforms to the Indigenous Data Network's Catalogue Profile which is a profile of the DCAT, SKOS and PROV data models."""@en ; + dcterms:identifier "democat"^^xsd:token ; + dcterms:modified "2022-08-29"^^xsd:date ; + dcterms:title "IDN Demonstration Catalogue" ; + prov:qualifiedAttribution [ + a prov:QualifiedAttribution ; + dcat:hadRole + isoroles:author , + isoroles:custodian , + isoroles:owner ; + prov:agent [ + a schema:Organization ; + schema:name "some org" ; + schema:member [ + a schema:Person ; + schema:name "some person" ; + schema:memberOf [ + schema:name "another some org" + ] ; + ] ; + ] ; + ] ; +. diff --git a/tests/data/vocprez/expected_responses/collection_listing_anot.ttl b/tests/data/vocprez/expected_responses/collection_listing_anot.ttl index 5f63e699..617a244e 100644 --- a/tests/data/vocprez/expected_responses/collection_listing_anot.ttl +++ b/tests/data/vocprez/expected_responses/collection_listing_anot.ttl @@ -3,10 +3,17 @@ @prefix skos: . @prefix xsd: . + a skos:Collection ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:definition "Borehole purposes applicable to regulatory notification forms."@en ; + skos:prefLabel "PGGD selection"@en ; + ns1:link "/v/collection/brhl-prps:pggd" . + a skos:Collection ; - skos:definition "All Concepts in this vocabulary" ; dcterms:provenance "this vocabulary" ; + skos:definition "All Concepts in this vocabulary" ; skos:prefLabel "Contact Type - All Concepts"@en ; ns1:link "/v/collection/cgi:contacttype" . -skos:Collection ns1:count 1 . +skos:Collection ns1:count 2 . + diff --git a/tests/data/vocprez/expected_responses/concept-coal.ttl b/tests/data/vocprez/expected_responses/concept-coal.ttl new file mode 100644 index 00000000..9bc5d8ce --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept-coal.ttl @@ -0,0 +1,33 @@ +PREFIX bhpur: +PREFIX cs4: +PREFIX dcterms: +PREFIX ns1: +PREFIX rdfs: +PREFIX skos: + +bhpur:coal + a skos:Concept ; + dcterms:identifier "brhl-prps:coal"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs4: ; + skos:definition "Wells and bores drilled to facilitate the mining of coal under permits governed by the Queensland Mineral Resources Act 1989"@en ; + skos:inScheme cs4: ; + skos:prefLabel "Coal"@en ; + skos:topConceptOf cs4: ; + ns1:link "/v/vocab/df:borehole-purpose/brhl-prps:coal" ; +. + +dcterms:identifier + rdfs:label "Identifier"@en ; + dcterms:description "Recommended practice is to identify the resource by means of a string conforming to an identification system. Examples include International Standard Book Number (ISBN), Digital Object Identifier (DOI), and Uniform Resource Name (URN). Persistent identifiers should be provided as HTTP URIs."@en ; +. + +dcterms:provenance + rdfs:label "Provenance"@en ; + dcterms:description "The statement may include a description of any changes successive custodians made to the resource."@en ; +. + +cs4: + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Borehole Purpose"@en ; +. diff --git a/tests/data/vocprez/expected_responses/concept-open-cut-coal-mining.ttl b/tests/data/vocprez/expected_responses/concept-open-cut-coal-mining.ttl new file mode 100644 index 00000000..b529b7b6 --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept-open-cut-coal-mining.ttl @@ -0,0 +1,38 @@ +PREFIX bhpur: +PREFIX cs4: +PREFIX dcterms: +PREFIX ns1: +PREFIX rdfs: +PREFIX skos: + +bhpur:open-cut-coal-mining + a skos:Concept ; + dcterms:identifier "brhl-prps:open-cut-coal-mining"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs4: ; + skos:broader bhpur:coal ; + skos:definition "Wells drilled for the purpose of assessing coal resources for an open cut coal mine."@en ; + skos:inScheme cs4: ; + skos:prefLabel "Open-Cut Coal Mining"@en ; + ns1:link "/v/vocab/df:borehole-purpose/brhl-prps:open-cut-coal-mining" ; +. + +dcterms:identifier + rdfs:label "Identifier"@en ; + dcterms:description "Recommended practice is to identify the resource by means of a string conforming to an identification system. Examples include International Standard Book Number (ISBN), Digital Object Identifier (DOI), and Uniform Resource Name (URN). Persistent identifiers should be provided as HTTP URIs."@en ; +. + +dcterms:provenance + rdfs:label "Provenance"@en ; + dcterms:description "The statement may include a description of any changes successive custodians made to the resource."@en ; +. + +bhpur:coal + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Coal"@en ; +. + +cs4: + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Borehole Purpose"@en ; +. diff --git a/tests/data/vocprez/expected_responses/concept-with-2-narrower-concepts.ttl b/tests/data/vocprez/expected_responses/concept-with-2-narrower-concepts.ttl new file mode 100644 index 00000000..34197ca2 --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept-with-2-narrower-concepts.ttl @@ -0,0 +1,35 @@ +PREFIX dcterms: +PREFIX ns1: +PREFIX rdfs: +PREFIX skos: +PREFIX xsd: + + + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:narrower + , + ; + skos:prefLabel "Coal"@en ; + ns1:childrenCount 2 ; +. + +dcterms:identifier + rdfs:label "Identifier"@en ; + dcterms:description "Recommended practice is to identify the resource by means of a string conforming to an identification system. Examples include International Standard Book Number (ISBN), Digital Object Identifier (DOI), and Uniform Resource Name (URN). Persistent identifiers should be provided as HTTP URIs."@en ; +. + + + dcterms:identifier "brhl-prps:open-cut-coal-mining"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Open-Cut Coal Mining"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:open-cut-coal-mining" ; +. + + + dcterms:identifier "brhl-prps:underground-coal-mining"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Underground Coal Mining"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:underground-coal-mining" ; +. diff --git a/tests/data/vocprez/expected_responses/concept_scheme_no_children.ttl b/tests/data/vocprez/expected_responses/concept_scheme_no_children.ttl new file mode 100644 index 00000000..27d246dd --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept_scheme_no_children.ttl @@ -0,0 +1,57 @@ +PREFIX dcterms: +PREFIX ns1: +PREFIX ns2: +PREFIX owl: +PREFIX prov: +PREFIX rdfs: +PREFIX schema: +PREFIX skos: +PREFIX xsd: + + + a + owl:Ontology , + skos:ConceptScheme ; + dcterms:created "2020-07-17"^^xsd:date ; + dcterms:creator ; + dcterms:modified "2023-03-16"^^xsd:date ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + ns2:status ; + skos:definition "The primary purpose of a borehole based on the legislative State Act and/or the resources industry sector."@en ; + skos:prefLabel "Borehole Purpose no children"@en ; + prov:qualifiedDerivation [ + prov:entity ; + prov:hadRole + ] ; + ns1:childrenCount 0 ; +. + +dcterms:created + rdfs:label "Date Created"@en ; + dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en ; +. + +dcterms:creator + rdfs:label "Creator"@en ; + dcterms:description "Recommended practice is to identify the creator with a URI. If this is not possible or feasible, a literal value that identifies the creator may be provided."@en ; +. + +dcterms:modified + rdfs:label "Date Modified"@en ; + dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en ; +. + +dcterms:provenance + rdfs:label "Provenance"@en ; + dcterms:description "The statement may include a description of any changes successive custodians made to the resource."@en ; +. + +dcterms:publisher + rdfs:label "Publisher"@en ; +. + + + skos:prefLabel "stable"@en ; + schema:color "#2e8c09" ; +. diff --git a/tests/data/vocprez/expected_responses/concept_scheme_top_concepts_with_children.ttl b/tests/data/vocprez/expected_responses/concept_scheme_top_concepts_with_children.ttl new file mode 100644 index 00000000..75b1de09 --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept_scheme_top_concepts_with_children.ttl @@ -0,0 +1,89 @@ +PREFIX dcterms: +PREFIX ns1: +PREFIX rdfs: +PREFIX skos: +PREFIX xsd: + + + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:hasTopConcept + , + , + , + , + , + , + , + ; + skos:prefLabel "Borehole Purpose"@en ; + ns1:childrenCount 8 ; +. + +dcterms:identifier + rdfs:label "Identifier"@en ; + dcterms:description "Recommended practice is to identify the resource by means of a string conforming to an identification system. Examples include International Standard Book Number (ISBN), Digital Object Identifier (DOI), and Uniform Resource Name (URN). Persistent identifiers should be provided as HTTP URIs."@en ; +. + + + dcterms:identifier "brhl-prps:coal"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Coal"@en ; + ns1:childrenCount 2 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:coal" ; +. + + + dcterms:identifier "brhl-prps:geothermal"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Geothermal"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:geothermal" ; +. + + + dcterms:identifier "brhl-prps:greenhouse-gas-storage"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Greenhouse Gas Storage"@en ; + ns1:childrenCount 1 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:greenhouse-gas-storage" ; +. + + + dcterms:identifier "brhl-prps:mineral"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Mineral"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:mineral" ; +. + + + dcterms:identifier "brhl-prps:non-industry"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Non-Industry"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:non-industry" ; +. + + + dcterms:identifier "brhl-prps:oil-shale"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Oil Shale"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:oil-shale" ; +. + + + dcterms:identifier "brhl-prps:petroleum"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Petroleum"@en ; + ns1:childrenCount 3 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:petroleum" ; +. + + + dcterms:identifier "brhl-prps:water"^^ns1:identifier ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:prefLabel "Water"@en ; + ns1:childrenCount 0 ; + ns1:link "/v/vocab/def2:borehole-purpose/brhl-prps:water" ; +. diff --git a/tests/data/vocprez/expected_responses/concept_scheme_with_children.ttl b/tests/data/vocprez/expected_responses/concept_scheme_with_children.ttl new file mode 100644 index 00000000..86aadff0 --- /dev/null +++ b/tests/data/vocprez/expected_responses/concept_scheme_with_children.ttl @@ -0,0 +1,57 @@ +PREFIX dcterms: +PREFIX ns1: +PREFIX ns2: +PREFIX owl: +PREFIX prov: +PREFIX rdfs: +PREFIX schema: +PREFIX skos: +PREFIX xsd: + + + a + owl:Ontology , + skos:ConceptScheme ; + dcterms:created "2020-07-17"^^xsd:date ; + dcterms:creator ; + dcterms:modified "2023-03-16"^^xsd:date ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + ns1:status ; + skos:definition "The primary purpose of a borehole based on the legislative State Act and/or the resources industry sector."@en ; + skos:prefLabel "Borehole Purpose"@en ; + prov:qualifiedDerivation [ + prov:entity ; + prov:hadRole + ] ; + ns2:childrenCount 8 ; +. + +dcterms:created + rdfs:label "Date Created"@en ; + dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en ; +. + +dcterms:creator + rdfs:label "Creator"@en ; + dcterms:description "Recommended practice is to identify the creator with a URI. If this is not possible or feasible, a literal value that identifies the creator may be provided."@en ; +. + +dcterms:modified + rdfs:label "Date Modified"@en ; + dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en ; +. + +dcterms:provenance + rdfs:label "Provenance"@en ; + dcterms:description "The statement may include a description of any changes successive custodians made to the resource."@en ; +. + +dcterms:publisher + rdfs:label "Publisher"@en ; +. + + + skos:prefLabel "stable"@en ; + schema:color "#2e8c09" ; +. diff --git a/tests/data/vocprez/expected_responses/empty.ttl b/tests/data/vocprez/expected_responses/empty.ttl new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/vocprez/expected_responses/vocab_anot.ttl b/tests/data/vocprez/expected_responses/vocab_anot.ttl deleted file mode 100644 index f891ebee..00000000 --- a/tests/data/vocprez/expected_responses/vocab_anot.ttl +++ /dev/null @@ -1,297 +0,0 @@ -@prefix dcterms: . -@prefix ns1: . -@prefix rdfs: . -@prefix skos: . -@prefix xsd: . - -dcterms:created rdfs:label "Date Created"@en ; - dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en . - -dcterms:creator rdfs:label "Creator"@en ; - dcterms:description "Recommended practice is to identify the creator with a URI. If this is not possible or feasible, a literal value that identifies the creator may be provided."@en . - -dcterms:identifier rdfs:label "Identifier"@en ; - dcterms:description "Recommended practice is to identify the resource by means of a string conforming to an identification system. Examples include International Standard Book Number (ISBN), Digital Object Identifier (DOI), and Uniform Resource Name (URN). Persistent identifiers should be provided as HTTP URIs."@en . - -dcterms:modified rdfs:label "Date Modified"@en ; - dcterms:description "Recommended practice is to describe the date, date/time, or period of time as recommended for the property Date, of which this is a subproperty."@en . - -dcterms:provenance rdfs:label "Provenance"@en ; - dcterms:description "The statement may include a description of any changes successive custodians made to the resource."@en . - -dcterms:publisher rdfs:label "Publisher"@en . - -dcterms:source rdfs:label "Source"@en ; - dcterms:description "This property is intended to be used with non-literal values. The described resource may be derived from the related resource in whole or in part. Best practice is to identify the related resource by means of a URI or a string conforming to a formal identification system."@en . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "alteration facies contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:alteration_facies_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "angular unconformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:angular_unconformable_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "buttress unconformity"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:buttress_unconformity" . - - dcterms:provenance "FGDC"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "chronostratigraphic-zone contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:chronostratigraphic_zone_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "conductivity contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:conductivity_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "conformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:conformable_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "deformation zone contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:deformation_zone_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "density contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:density_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "disconformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:disconformable_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "faulted contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:faulted_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "geologic province contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:geologic_province_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "glacial stationary line"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:glacial_stationary_line" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "igneous phase contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:igneous_phase_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "impact structure boundary"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:impact_structure_boundary" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "magnetic polarity contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:magnetic_polarity_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "magnetic susceptiblity contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:magnetic_susceptiblity_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "magnetization contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:magnetization_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "metamorphic facies contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:metamorphic_facies_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "metasomatic facies contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:metasomatic_facies_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "mineralisation assemblage contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:mineralisation_assemblage_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "nonconformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:nonconformable_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader , - ; - skos:inScheme ; - skos:prefLabel "paraconformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:paraconformable_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "radiometric contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:radiometric_contact" . - - dcterms:provenance "base on Nichols, Gary, 1999, Sedimentology and stratigraphy, Blackwell, p. 62-63."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "sedimentary facies contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:sedimentary_facies_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "sedimentary intrusive contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:sedimentary_intrusive_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "seismic contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:seismic_contact" . - - dcterms:provenance "this vocabulary, concept to encompass boundary of caldron, caldera, or crater."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "volcanic subsidence zone boundary"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:volcanic_subsidence_zone_boundary" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "weathering contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:weathering_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:prefLabel "igneous intrusive contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:igneous_intrusive_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - ; - skos:prefLabel "depositional contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:depositional_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - , - ; - skos:prefLabel "magnetic contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:magnetic_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - , - , - ; - skos:prefLabel "metamorphic contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:metamorphic_contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - , - , - , - ; - skos:prefLabel "geophysical contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:geophysical_contact" . - - dcterms:provenance "Neuendorf, K.K.E, Mehl, J.P. & Jackson, J.A. (eds), 2005. Glossary of geology, 5th Edition. American Geological Institute, Alexandria, 779 p."@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - , - , - , - ; - skos:prefLabel "unconformable contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:unconformable_contact" . - - dcterms:provenance "adapted from Jackson, 1997, page 137, NADM C1 2004"@en ; - skos:inScheme ; - skos:narrower , - , - , - , - , - ; - skos:prefLabel "contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:contact" . - - dcterms:provenance "this vocabulary"@en ; - skos:broader ; - skos:inScheme ; - skos:narrower , - , - , - , - , - , - , - , - , - ; - skos:prefLabel "lithogenetic contact"@en ; - ns1:link "/v/vocab/2016.01:contacttype/cntcttyp:lithogenetic_contact" . - - a skos:ConceptScheme ; - dcterms:created "2009-07-14"^^xsd:date ; - dcterms:creator ; - dcterms:identifier "contacttype"^^xsd:token ; - dcterms:modified "2020-06-23"^^xsd:date ; - dcterms:provenance "Original set of terms from the GeosciML standard" ; - dcterms:publisher ; - dcterms:source "http://www.opengis.net/doc/geosciml/4.1"^^xsd:anyURI ; - skos:changeNote "2009 Revised from ContactType200811 with addition of impact_structure_boundary and volcanic_subsidence_zone_boundary, and addition of more metadata annotation"@en, - "2009-12-07 SMR Update metadata properties for version, creator, title, and format. Change skos:HistoryNote to dc:source for information on origin of terms and definitions."@en, - "2011-02-16 SMR replace URN with cgi http URI's. Last changes to fix URN for conceptScheme that was not updated in original updates."@en, - "2012-02-07 SMR update URI to replace numeric final token with English-language string as in original URN scheme."@en, - "2012-02-27 SMR add skos:exactMatch triples to map URIs for concepts in this vocabulary to number-token URIs in 201012 version of same concepts."@en, - "2012-11-24 SMR Update to 201211 version; add collection entity, check all pref labels are lower case, remove owl:NamedIndividual and Owl:Thing rdf:types."@en, - "2016-06-15 OLR - redo Excel spreadsheet to work with XSLT, to make consistent SKOS-RDF with all CGI vocabularies. Generate new SKOS-RDF file."@en, - "2020-06-23 NJC Added properties to ensure vocab matched Geoscience Australia's vocab profile (http://linked.data.gov.au/def/ga-skos-profile). Just annotation properties, no new content. Agents (creator/publisher) now not text but RDF resource. Dates (create/modified) derived from editorial notes & existing date properties."@en ; - skos:definition "This scheme describes the concept space for Contact Type concepts, as defined by the IUGS Commission for Geoscience Information (CGI) Geoscience Terminology Working Group. By extension, it includes all concepts in this conceptScheme, as well as concepts in any previous versions of the scheme. Designed for use in the contactType property in GeoSciML Contact elements."@en ; - skos:editorialNote "This file contains the 2016 SKOS-RDF version of the CGI Contact Type vocabulary. Compilation and review in MS Excel spreadsheet, converted to MS Excel for SKOS generation using GSML_SKOS_fromXLS_2016.01.xslt."@en ; - skos:hasTopConcept ; - skos:prefLabel "Contact Type"@en . diff --git a/tests/data/vocprez/expected_responses/vocab_listing_anot.ttl b/tests/data/vocprez/expected_responses/vocab_listing_anot.ttl index 88ae302a..db0eb520 100644 --- a/tests/data/vocprez/expected_responses/vocab_listing_anot.ttl +++ b/tests/data/vocprez/expected_responses/vocab_listing_anot.ttl @@ -1,37 +1,78 @@ -@prefix dcterms: . -@prefix ns1: . -@prefix ns2: . -@prefix rdfs: . -@prefix schema: . -@prefix skos: . -@prefix xsd: . +PREFIX dcterms: +PREFIX ns1: +PREFIX ns2: +PREFIX prov: +PREFIX rdfs: +PREFIX schema: +PREFIX skos: +PREFIX xsd: -dcterms:publisher rdfs:label "Publisher"@en . + + a skos:ConceptScheme ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + ns1:status ; + skos:prefLabel "Borehole Purpose"@en ; + prov:qualifiedDerivation [ + prov:entity ; + prov:hadRole + ] ; + ns2:link "/v/vocab/def2:borehole-purpose" ; +. + + + a skos:ConceptScheme ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + ns1:status ; + skos:prefLabel "Borehole Purpose no children"@en ; + prov:qualifiedDerivation [ + prov:entity ; + prov:hadRole + ] ; + ns2:link "/v/vocab/def2:borehole-purpose-no-children" ; +. + +dcterms:publisher + rdfs:label "Publisher"@en ; +. - a skos:ConceptScheme ; + + a skos:ConceptScheme ; dcterms:provenance "Original set of terms from the GeosciML standard" ; dcterms:publisher ; skos:prefLabel "Contact Type"@en ; - ns2:link "/v/vocab/2016.01:contacttype" . + ns2:link "/v/vocab/2016.01:contacttype" ; +. - a skos:ConceptScheme ; + + a skos:ConceptScheme ; dcterms:publisher ; skos:prefLabel "Registry Status Vocabulary"@en ; - ns2:link "/v/vocab/def:reg-statuses" . + ns2:link "/v/vocab/def:reg-statuses" ; +. - a skos:ConceptScheme ; + + a skos:ConceptScheme ; dcterms:provenance "Created for the MER catalogue upgrade project, 2022"@en ; dcterms:publisher ; ns1:status ; skos:prefLabel "Vocabulary Derivation Modes"@en ; - ns2:link "/v/vocab/def:vocdermods" . + ns2:link "/v/vocab/def:vocdermods" ; +. - a skos:ConceptScheme ; + + a skos:ConceptScheme ; dcterms:provenance "This vocabulary was built on an extract of the WAROX system's lookup table"@en ; skos:prefLabel "WAROX Alteration Type"@en ; - ns2:link "/v/vocab/def:warox-alteration-types" . + ns2:link "/v/vocab/def:warox-alteration-types" ; +. - skos:prefLabel "stable"@en ; - schema:color "#2e8c09" . + + skos:prefLabel "stable"@en ; + schema:color "#2e8c09" ; +. -skos:ConceptScheme ns2:count 4 . +skos:ConceptScheme + ns2:count 6 ; +. diff --git a/tests/data/vocprez/input/borehole-purpose-no-children.ttl b/tests/data/vocprez/input/borehole-purpose-no-children.ttl new file mode 100644 index 00000000..d2e619fd --- /dev/null +++ b/tests/data/vocprez/input/borehole-purpose-no-children.ttl @@ -0,0 +1,26 @@ +PREFIX agldwgstatus: +PREFIX cs: +PREFIX dcterms: +PREFIX owl: +PREFIX prov: +PREFIX rdfs: +PREFIX reg: +PREFIX sdo: +PREFIX skos: +PREFIX xsd: + +cs: + a + owl:Ontology , + skos:ConceptScheme ; + dcterms:created "2020-07-17"^^xsd:date ; + dcterms:creator ; + dcterms:modified "2023-03-16"^^xsd:date ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + reg:status agldwgstatus:stable ; + skos:definition "The primary purpose of a borehole based on the legislative State Act and/or the resources industry sector."@en ; + prov:qualifiedDerivation [ prov:entity ; + prov:hadRole ] ; + skos:prefLabel "Borehole Purpose no children"@en ; +. diff --git a/tests/data/vocprez/input/borehole-purpose.ttl b/tests/data/vocprez/input/borehole-purpose.ttl new file mode 100644 index 00000000..cdb797bb --- /dev/null +++ b/tests/data/vocprez/input/borehole-purpose.ttl @@ -0,0 +1,238 @@ +PREFIX agldwgstatus: +PREFIX bhpur: +PREFIX cs: +PREFIX dcterms: +PREFIX owl: +PREFIX prov: +PREFIX rdfs: +PREFIX reg: +PREFIX sdo: +PREFIX skos: +PREFIX xsd: + +bhpur:carbon-capture-and-storage + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:greenhouse-gas-storage ; + skos:definition "Wells that deposit carbon dioxide into an underground geological formation after capture from large point sources, such as a cement factory or biomass power plant."@en ; + skos:inScheme cs: ; + skos:prefLabel "Carbon Capture and Storage"@en ; +. + +bhpur:open-cut-coal-mining + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:coal ; + skos:definition "Wells drilled for the purpose of assessing coal resources for an open cut coal mine."@en ; + skos:inScheme cs: ; + skos:prefLabel "Open-Cut Coal Mining"@en ; +. + +bhpur:pggd + a skos:Collection ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + skos:definition "Borehole purposes applicable to regulatory notification forms."@en ; + skos:member + bhpur:coal-seam-gas , + bhpur:conventional-petroleum , + bhpur:geothermal , + bhpur:greenhouse-gas-storage , + bhpur:unconventional-petroleum , + bhpur:water ; + skos:prefLabel "PGGD selection"@en ; +. + +bhpur:shale-gas + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:unconventional-petroleum ; + skos:definition "Wells targetting shale that produces natural gas. A shale that is thermally mature enough and has sufficient gas content to produce economic quantities of natural gas."@en ; + skos:inScheme cs: ; + skos:prefLabel "Shale Gas"@en ; +. + +bhpur:shale-oil + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:unconventional-petroleum ; + skos:definition "Wells targetting shale that produces oil. Oil obtained by artificial maturation of oil shale. The process of artificial maturation uses controlled heating, or pyrolysis, of kerogen to release the shale oil."@en ; + skos:inScheme cs: ; + skos:prefLabel "Shale Oil"@en ; +. + +bhpur:tight-gas + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:unconventional-petroleum ; + skos:definition "Wells targetting gas from relatively impermeable reservoir rock."@en ; + skos:inScheme cs: ; + skos:prefLabel "Tight Gas"@en ; +. + +bhpur:tight-oil + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:unconventional-petroleum ; + skos:definition "Wells targetting oil from relatively impermeable reservoir rock."@en ; + skos:inScheme cs: ; + skos:prefLabel "Tight Oil"@en ; +. + +bhpur:underground-coal-mining + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:coal ; + skos:definition "Wells drilled for the purpose of assessing coal resources for an underground coal mine."@en ; + skos:inScheme cs: ; + skos:prefLabel "Underground Coal Mining"@en ; +. + +bhpur:coal-seam-gas + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:petroleum ; + skos:definition "Wells targetting coal seams where hydrocarbons are kept in place via adsorption to the coal surface and hydrostatic pressure"@en ; + skos:inScheme cs: ; + skos:prefLabel "Coal Seam Gas"@en ; +. + +bhpur:conventional-petroleum + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:petroleum ; + skos:definition "Wells targetting conventional petroleum reservoirs where buoyant forces keep hydrocarbons in place below a sealing caprock."@en ; + skos:inScheme cs: ; + skos:prefLabel "Conventional Petroleum"@en ; +. + +bhpur:mineral + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled to facilitate the mining of minerals, excluding coal and oil shale, under permits governed by the Queensland Mineral Resources Act (1989)"@en ; + skos:inScheme cs: ; + skos:prefLabel "Mineral"@en ; + skos:topConceptOf cs: ; +. + +bhpur:non-industry + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:altLabel "Non-Industry"@en ; + skos:definition "Wells and bores drilled by non-industry agents outside of the State Resources Acts"@en ; + skos:inScheme cs: ; + skos:prefLabel "Non-Industry"@en ; + skos:topConceptOf cs: ; +. + +bhpur:oil-shale + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled to facilitate the mining of oil shale under permits governed by the Queensland Mineral Resources Act 1989"@en ; + skos:inScheme cs: ; + skos:prefLabel "Oil Shale"@en ; + skos:topConceptOf cs: ; +. + +bhpur:geothermal + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled under permits governed by the Queensland Geothermal Energy Act 2010"@en ; + skos:inScheme cs: ; + skos:prefLabel "Geothermal"@en ; + skos:topConceptOf cs: ; +. + +bhpur:water + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled under permits governed by the Queensland Water Act 2000. A well or bore is only considered a water well or bore where drilled under the Water Act, e.g. a well or bore drilled to serve a water observation function under the Petroleum Act is considered a Petroleum Well with an Observation function or sub-purpose. Additional rights, obligations, and responsibilities may be conferred by intersecting legislation on wells and bores drilled by mineral and coal permit holders and petroleum and gas permit holders under the Mineral Resources Act 1989 and the Petroleum and Gas (Production and Safety) Act 2004 respectively."@en ; + skos:inScheme cs: ; + skos:prefLabel "Water"@en ; + skos:topConceptOf cs: ; +. + + + a sdo:Organization ; + sdo:name "Geological Survey of Queensland" ; + sdo:url "http://www.business.qld.gov.au/industries/mining-energy-water/resources/geoscience-information/gsq"^^xsd:anyURI ; +. + +bhpur:coal + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled to facilitate the mining of coal under permits governed by the Queensland Mineral Resources Act 1989"@en ; + skos:inScheme cs: ; + skos:prefLabel "Coal"@en ; + skos:topConceptOf cs: ; +. + +bhpur:greenhouse-gas-storage + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:altLabel "GHG"@en ; + skos:definition "Wells and bores drilled under permits governed by the Queensland Greenhouse Gas Storage Act 2009"@en ; + skos:inScheme cs: ; + skos:prefLabel "Greenhouse Gas Storage"@en ; + skos:topConceptOf cs: ; +. + +bhpur:petroleum + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:definition "Wells and bores drilled under permits governed by the Queensland Petroleum Act 1923 and Petroleum and Gas (Production and Safety) Act 2004. This includes water observation, water disposal, and water supply wells drilled under the relevant Petroleum Acts rather than the Water Act."@en ; + skos:inScheme cs: ; + skos:prefLabel "Petroleum"@en ; + skos:topConceptOf cs: ; +. + +bhpur:unconventional-petroleum + a skos:Concept ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + rdfs:isDefinedBy cs: ; + skos:broader bhpur:petroleum ; + skos:definition "Wells targetting unconventional reservoirs whose properties including porosity, permeability, or trapping mechanism differ from conventional reservoirs"@en ; + skos:inScheme cs: ; + skos:prefLabel "Unconventional Petroleum"@en ; +. + +cs: + a + owl:Ontology , + skos:ConceptScheme ; + dcterms:created "2020-07-17"^^xsd:date ; + dcterms:creator ; + dcterms:modified "2023-03-16"^^xsd:date ; + dcterms:provenance "Compiled by the Geological Survey of Queensland" ; + dcterms:publisher ; + reg:status agldwgstatus:stable ; + skos:definition "The primary purpose of a borehole based on the legislative State Act and/or the resources industry sector."@en ; + skos:hasTopConcept + bhpur:coal , + bhpur:geothermal , + bhpur:greenhouse-gas-storage , + bhpur:mineral , + bhpur:non-industry , + bhpur:oil-shale , + bhpur:petroleum , + bhpur:water ; + prov:qualifiedDerivation [ prov:entity ; + prov:hadRole ] ; + skos:prefLabel "Borehole Purpose"@en ; +. diff --git a/tests/object/test_count.py b/tests/object/test_count.py new file mode 100644 index 00000000..ee45683c --- /dev/null +++ b/tests/object/test_count.py @@ -0,0 +1,73 @@ +import os +import subprocess +from time import sleep + +import pytest +from fastapi.testclient import TestClient + +PREZ_DIR = os.getenv("PREZ_DIR") +LOCAL_SPARQL_STORE = os.getenv("LOCAL_SPARQL_STORE") + + +@pytest.fixture(scope="module") +def test_client(request): + print("Run Local SPARQL Store") + p1 = subprocess.Popen(["python", str(LOCAL_SPARQL_STORE), "-p", "3031"]) + sleep(1) + + def teardown(): + print("\nDoing teardown") + p1.kill() + + request.addfinalizer(teardown) + + # must only import app after config.py has been altered above so config is retained + from prez.app import app + + return TestClient(app) + + +def get_curie(test_client: TestClient, iri: str) -> str: + with test_client as client: + response = client.get(f"/identifier/curie/{iri}") + if response.status_code != 200: + raise ValueError(f"Failed to retrieve curie for {iri}. {response.text}") + return response.text + + +@pytest.mark.parametrize( + "iri, inbound, outbound, count", + [ + [ + "http://linked.data.gov.au/def/borehole-purpose", + "http://www.w3.org/2004/02/skos/core#inScheme", + None, + 0, + ], + [ + "http://linked.data.gov.au/def/borehole-purpose-no-children", + "http://www.w3.org/2004/02/skos/core#inScheme", + None, + 0, + ], + [ + "http://linked.data.gov.au/def/borehole-purpose", + None, + "http://www.w3.org/2004/02/skos/core#hasTopConcept", + 0, + ], + ], +) +def test_count( + test_client: TestClient, + iri: str, + inbound: str | None, + outbound: str | None, + count: int, +): + curie = get_curie(test_client, iri) + + with test_client as client: + params = {"curie": curie, "inbound": inbound, "outbound": outbound} + response = client.get(f"/count", params=params) + assert int(response.text) == count diff --git a/tests/test_bnode.py b/tests/test_bnode.py new file mode 100644 index 00000000..95ea818e --- /dev/null +++ b/tests/test_bnode.py @@ -0,0 +1,27 @@ +import pathlib + +import pytest +from rdflib import Graph, URIRef + +from prez.bnode import get_bnode_depth + + +WORKING_DIR = pathlib.Path().absolute() + + +@pytest.mark.parametrize( + "input_file, iri, expected_depth", + [ + ("bnode_depth-1.ttl", "https://data.idnau.org/pid/democat", 1), + ("bnode_depth-2.ttl", "https://data.idnau.org/pid/democat", 2), + ("bnode_depth-4.ttl", "https://data.idnau.org/pid/democat", 4), + ], +) +def test_bnode_depth(input_file: str, iri: str, expected_depth: int) -> None: + file = WORKING_DIR / "data/bnode_depth" / input_file + + graph = Graph() + graph.parse(file) + + depth = get_bnode_depth(graph, URIRef(iri)) + assert depth == expected_depth diff --git a/tests/vocprez/test_endpoints_vocprez.py b/tests/vocprez/test_endpoints_vocprez.py index 293dee97..cd099809 100644 --- a/tests/vocprez/test_endpoints_vocprez.py +++ b/tests/vocprez/test_endpoints_vocprez.py @@ -4,16 +4,16 @@ from time import sleep import pytest -from rdflib import Graph, URIRef, RDF, SKOS +from fastapi.testclient import TestClient +from rdflib import Graph from rdflib.compare import isomorphic PREZ_DIR = os.getenv("PREZ_DIR") LOCAL_SPARQL_STORE = os.getenv("LOCAL_SPARQL_STORE") -from fastapi.testclient import TestClient @pytest.fixture(scope="module") -def vp_test_client(request): +def test_client(request): print("Run Local SPARQL Store") p1 = subprocess.Popen(["python", str(LOCAL_SPARQL_STORE), "-p", "3031"]) sleep(1) @@ -30,84 +30,181 @@ def teardown(): return TestClient(app) -@pytest.fixture(scope="module") -def a_vocab_link(vp_test_client): - with vp_test_client as client: - r = client.get("/v/vocab") - g = Graph().parse(data=r.text) - vocab_uri = g.value(None, RDF.type, SKOS.ConceptScheme) - vocab_link = g.value(vocab_uri, URIRef(f"https://prez.dev/link", None)) - return vocab_link +def get_curie(test_client: TestClient, iri: str) -> str: + with test_client as client: + response = client.get(f"/identifier/curie/{iri}") + if response.status_code != 200: + raise ValueError(f"Failed to retrieve curie for {iri}. {response.text}") + return response.text -@pytest.fixture(scope="module") -def a_concept_link(vp_test_client, a_vocab_link): - # get the first concept endpoint - r = vp_test_client.get(a_vocab_link) - g = Graph().parse(data=r.text) - concept_uri = next(g.subjects(predicate=SKOS.inScheme, object=None)) - concept_link = g.value(concept_uri, URIRef(f"https://prez.dev/link", None)) - return concept_link - - -def test_vocab_item(vp_test_client, a_vocab_link): - with vp_test_client as client: - r = client.get( - f"{a_vocab_link}?_mediatype=text/anot+turtle" - ) # hardcoded to a smaller vocabulary - sparql store has poor performance w/ CONSTRUCT - response_graph = Graph(bind_namespaces="rdflib").parse(data=r.text) +def test_vocab_listing(test_client: TestClient): + with test_client as client: + response = client.get(f"/v/vocab?_mediatype=text/anot+turtle") + response_graph = Graph().parse(data=response.text) expected_graph = Graph().parse( - Path(__file__).parent / "../data/vocprez/expected_responses/vocab_anot.ttl" + Path(__file__).parent + / "../data/vocprez/expected_responses/vocab_listing_anot.ttl" ) - assert response_graph.isomorphic(expected_graph), print( - f"Graph delta:{(expected_graph - response_graph).serialize()}" + assert isomorphic(expected_graph, response_graph) + + +@pytest.mark.parametrize( + "iri, expected_result_file, description", + [ + [ + "http://linked.data.gov.au/def2/borehole-purpose", + "concept_scheme_with_children.ttl", + "Return concept scheme and a prez:childrenCount of 8", + ], + [ + "http://linked.data.gov.au/def2/borehole-purpose-no-children", + "concept_scheme_no_children.ttl", + "Return concept scheme and a prez:childrenCount of 0", + ], + ], +) +def test_concept_scheme( + test_client: TestClient, iri: str, expected_result_file: str, description: str +): + curie = get_curie(test_client, iri) + + with test_client as client: + response = client.get(f"/v/vocab/{curie}?_mediatype=text/anot+turtle") + response_graph = Graph(bind_namespaces="rdflib").parse(data=response.text) + expected_graph = Graph().parse( + Path(__file__).parent + / f"../data/vocprez/expected_responses/{expected_result_file}" ) - - -def test_vocab_listing(vp_test_client): - with vp_test_client as client: - r = client.get(f"/v/vocab?_mediatype=text/anot+turtle") - response_graph = Graph().parse(data=r.text) + assert isomorphic(expected_graph, response_graph), f"Failed test: {description}" + + +@pytest.mark.parametrize( + "iri, expected_result_file, description", + [ + [ + "http://linked.data.gov.au/def2/borehole-purpose", + "concept_scheme_top_concepts_with_children.ttl", + "Return concept scheme and a prez:childrenCount of 8", + ], + [ + "http://linked.data.gov.au/def2/borehole-purpose-no-children", + "empty.ttl", + "Return concept scheme and a prez:childrenCount of 0", + ], + ], +) +def test_concept_scheme_top_concepts( + test_client: TestClient, iri: str, expected_result_file: str, description: str +): + curie = get_curie(test_client, iri) + + with test_client as client: + response = client.get( + f"/v/vocab/{curie}/top-concepts?_mediatype=text/anot+turtle" + ) + response_graph = Graph(bind_namespaces="rdflib").parse(data=response.text) expected_graph = Graph().parse( Path(__file__).parent - / "../data/vocprez/expected_responses/vocab_listing_anot.ttl" + / f"../data/vocprez/expected_responses/{expected_result_file}" ) - assert response_graph.isomorphic(expected_graph), print( - f"Graph delta:{(expected_graph - response_graph).serialize()}" + assert isomorphic(expected_graph, response_graph), f"Failed test: {description}" + + +@pytest.mark.parametrize( + "concept_scheme_iri, concept_iri, expected_result_file, description", + [ + [ + "http://linked.data.gov.au/def2/borehole-purpose", + "http://linked.data.gov.au/def/borehole-purpose/coal", + "concept-with-2-narrower-concepts.ttl", + "Return concept with 2 narrower concepts.", + ], + [ + "http://linked.data.gov.au/def2/borehole-purpose", + "http://linked.data.gov.au/def2/borehole-purpose/open-cut-coal-mining", + "empty.ttl", + "Return nothing, no children.", + ], + ], +) +def test_concept_narrowers( + test_client: TestClient, + concept_scheme_iri: str, + concept_iri: str, + expected_result_file: str, + description: str, +): + concept_scheme_curie = get_curie(test_client, concept_scheme_iri) + concept_curie = get_curie(test_client, concept_iri) + + with test_client as client: + response = client.get( + f"/v/vocab/{concept_scheme_curie}/{concept_curie}/narrowers?_mediatype=text/anot+turtle" ) - - -def test_concept(vp_test_client, a_concept_link): - with vp_test_client as client: - r = client.get(f"{a_concept_link}?_mediatype=text/anot+turtle") - response_graph = Graph().parse(data=r.text) + response_graph = Graph(bind_namespaces="rdflib").parse(data=response.text) expected_graph = Graph().parse( Path(__file__).parent - / "../data/vocprez/expected_responses/concept_anot.ttl" + / f"../data/vocprez/expected_responses/{expected_result_file}" ) - assert response_graph.isomorphic(expected_graph), print( - f"Graph delta:{(expected_graph - response_graph).serialize()}" + assert isomorphic(expected_graph, response_graph), f"Failed test: {description}" + + +@pytest.mark.parametrize( + "concept_scheme_iri, concept_iri, expected_result_file, description", + [ + [ + "http://linked.data.gov.au/def/borehole-purpose", + "http://linked.data.gov.au/def/borehole-purpose/coal", + "concept-coal.ttl", + "Return the coal concept and its properties.", + ], + [ + "http://linked.data.gov.au/def/borehole-purpose", + "http://linked.data.gov.au/def/borehole-purpose/open-cut-coal-mining", + "concept-open-cut-coal-mining.ttl", + "Return the open-cut-coal-mining concept and its properties.", + ], + ], +) +def test_concept( + test_client: TestClient, + concept_scheme_iri: str, + concept_iri: str, + expected_result_file: str, + description: str, +): + concept_scheme_curie = get_curie(test_client, concept_scheme_iri) + concept_curie = get_curie(test_client, concept_iri) + + with test_client as client: + response = client.get( + f"/v/vocab/{concept_scheme_curie}/{concept_curie}?_mediatype=text/anot+turtle" ) + response_graph = Graph(bind_namespaces="rdflib").parse(data=response.text) + expected_graph = Graph().parse( + Path(__file__).parent + / f"../data/vocprez/expected_responses/{expected_result_file}" + ) + assert isomorphic(expected_graph, response_graph) -def test_collection_listing(vp_test_client): - with vp_test_client as client: - r = client.get(f"/v/collection?_mediatype=text/anot+turtle") - response_graph = Graph().parse(data=r.text, format="turtle") +def test_collection_listing(test_client: TestClient): + with test_client as client: + response = client.get(f"/v/collection?_mediatype=text/anot+turtle") + response_graph = Graph().parse(data=response.text, format="turtle") expected_graph = Graph().parse( Path(__file__).parent / "../data/vocprez/expected_responses/collection_listing_anot.ttl" ) - assert response_graph.isomorphic(expected_graph), print( - f"Graph delta:{(expected_graph - response_graph).serialize()}" - ) + assert isomorphic(expected_graph, response_graph) -def test_collection_listing_item(vp_test_client): - with vp_test_client as client: - r = client.get("/v/collection/cgi:contacttype") - assert r.status_code == 200 - response_graph = Graph().parse(data=r.text, format="turtle") +def test_collection_listing_item(test_client: TestClient): + with test_client as client: + response = client.get("/v/collection/cgi:contacttype") + assert response.status_code == 200 + response_graph = Graph().parse(data=response.text, format="turtle") expected_graph = Graph().parse( Path(__file__).parent / "../data/vocprez/expected_responses/collection_listing_item.ttl"