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