diff --git a/src/linkeddata_api/domain/label.py b/src/linkeddata_api/domain/label.py index 62a1af6..c94e565 100644 --- a/src/linkeddata_api/domain/label.py +++ b/src/linkeddata_api/domain/label.py @@ -12,16 +12,30 @@ def get( """ Returns a label or None if no label found. """ - query = f""" + template = Template( + """ PREFIX skos: + PREFIX dcterms: + PREFIX rdfs: + PREFIX schema: + PREFIX sdo: + SELECT DISTINCT ?label - WHERE {{ - VALUES (?labelProperty) {{ + WHERE { + VALUES (?labelProperty) { (skos:prefLabel) - }} - <{uri}> ?labelProperty ?label . - }} + (rdfs:label) + (dcterms:title) + (schema:name) + (sdo:name) + (dcterms:identifier) + } + <{{ uri }}> ?labelProperty ?label . + } + LIMIT 1 """ + ) + query = template.render(uri=uri) result = data.sparql.post(query, sparql_endpoint).json() @@ -41,28 +55,37 @@ def _get_from_list_query(uris: list[str]) -> str: template = Template( """ PREFIX skos: - SELECT DISTINCT ?uri (SAMPLE(?_label) AS ?label) + PREFIX dcterms: + PREFIX rdfs: + PREFIX schema: + PREFIX sdo: + + SELECT DISTINCT ?uri ?label WHERE { - VALUES (?uri) { - {% for uri in uris %} - (<{{ uri }}>) - {% endfor %} - } - + {% for uri in uris %} { - ?uri skos:prefLabel ?_label . - } - UNION { - # Also try and fetch label from TERN's controlled vocabularies. - SERVICE { - ?uri skos:prefLabel ?_label . + SELECT DISTINCT ?uri ?label + WHERE { + BIND(<{{ uri }}> as ?uri) + VALUES (?labelProperty) { + (skos:prefLabel) + (rdfs:label) + (dcterms:title) + (schema:name) + (sdo:name) + (dcterms:identifier) + } + ?uri ?labelProperty ?label . } + LIMIT 1 } + {% if not loop.last %}UNION{% endif %} + {% endfor %} } - GROUP BY ?uri """ ) - return template.render(uris=uris) + query = template.render(uris=uris) + return query def get_from_list( diff --git a/src/linkeddata_api/domain/viewer/resource/json/__init__.py b/src/linkeddata_api/domain/viewer/resource/json/__init__.py index 9d7c537..a7c66bb 100644 --- a/src/linkeddata_api/domain/viewer/resource/json/__init__.py +++ b/src/linkeddata_api/domain/viewer/resource/json/__init__.py @@ -244,16 +244,21 @@ def _get_types_and_properties( for row in result["results"]["bindings"]: if row["p"]["value"] == str(RDF.type): - type_label = uri_label_index.get(row["o"]["value"]) or domain.curie.get( - row["o"]["value"] - ) - types.append( - domain.schema.URI( - label=type_label, - value=row["o"]["value"], - internal=uri_internal_index.get(row["o"]["value"], False), + if row["o"]["type"] != "bnode": + type_label = uri_label_index.get(row["o"]["value"]) or domain.curie.get( + row["o"]["value"] ) - ) + types.append( + domain.schema.URI( + label=type_label, + value=row["o"]["value"], + internal=uri_internal_index.get(row["o"]["value"], False), + ) + ) + else: + # TODO: handle types that have bnode values + # E.g. /viewers/general?uri=http://linked.data.gov.au/dataset/ausplots/site-ntabrt0001&sparql_endpoint=https://graphdb.tern.org.au/repositories/knowledge_graph_core + continue else: predicate_label = domain.curie.get(row["p"]["value"]) predicate = domain.schema.URI( @@ -317,7 +322,8 @@ def _get_types_and_properties( for p in properties: if p.predicate.value == predicate.value: found = True - p.objects.append(item) + if item not in p.objects: + p.objects.append(item) if not found: properties.append( diff --git a/src/linkeddata_api/views/api_v1/__init__.py b/src/linkeddata_api/views/api_v1/__init__.py index d4f20e3..e9f597e 100644 --- a/src/linkeddata_api/views/api_v1/__init__.py +++ b/src/linkeddata_api/views/api_v1/__init__.py @@ -2,7 +2,6 @@ # import all sub modules with views registered with blueprint from . import ontology_viewer -from . import vocab_viewer from . import version_info from . import rdf_tools from . import viewer diff --git a/src/linkeddata_api/views/api_v1/openapi.yaml b/src/linkeddata_api/views/api_v1/openapi.yaml index 6e6cb09..5ccfddd 100644 --- a/src/linkeddata_api/views/api_v1/openapi.yaml +++ b/src/linkeddata_api/views/api_v1/openapi.yaml @@ -56,10 +56,14 @@ components: type: string created: title: The date when the resource was created - type: string + oneOf: + - type: string + nullable: true modified: title: The date when the resource was modified - type: string + oneOf: + - type: string + nullable: true Resource: title: Resource type: object @@ -174,6 +178,8 @@ paths: application/json: schema: $ref: "#/components/schemas/EntrypointItemList" + "404": + description: The supplied `viewer_id` value was not found "502": description: Gateway error /viewer/resource: @@ -246,24 +252,14 @@ paths: application/json: schema: $ref: "#/components/schemas/Resource" + "400": + description: Client error "404": description: Resource of URI not found. - content: - text/html: - schema: - type: string "500": description: Internal server error - content: - text/plain: - schema: - type: string "502": description: Error communicating with the database. - content: - text/html: - schema: - type: string /ontology_viewer/classes/flat: get: @@ -302,95 +298,6 @@ paths: type: string "502": description: Gateway error - - /vocab_viewer/nrm/vocabs: - get: - tags: - - NRM vocabularies - summary: Get a list of NRM protocol vocabularies - description: Get a list of concept schemes and collections for the NRM protocol. - responses: - "200": - description: A list of concept schemes and collections. - content: - application/json: - schema: - type: array - items: - title: Vocabulary item - type: object - properties: - id: - title: IRI of item - type: string - label: - title: Label of item - type: string - description: - title: The description of the item - type: string - created: - title: The date when the resource was created - type: string - modified: - title: The date when the resource was modified - type: string - "502": - description: Gateway error - - /vocab_viewer/nrm/resource: - get: - tags: - - NRM vocabularies - summary: Get a resource's description - description: A JSON payload used to render frontend user-interfaces. - parameters: - - in: query - name: uri - schema: - type: string - required: true - description: URI of the resource. - examples: - nrm: - summary: NRM index - value: https://linked.data.gov.au/def/nrm - - in: query - name: sparql_endpoint - schema: - type: string - required: true - description: SPARQL endpoint - examples: - nrm: - summary: NRM SPARQL endpoint - value: https://graphdb.tern.org.au/repositories/dawe_vocabs_core - responses: - "200": - description: A resource's description - content: - application/json: - schema: - type: object - properties: - uri: - type: string - label: - type: string - types: - type: array - items: - $ref: "#/components/schemas/URI" - properties: - type: array - items: - $ref: "#/components/schemas/PredicateObjects" - "404": - description: Resource with URI not found - content: - text/plain: - schema: - type: string /rdf_tools/convert: post: diff --git a/src/linkeddata_api/views/api_v1/viewer/entrypoint.py b/src/linkeddata_api/views/api_v1/viewer/entrypoint.py index 4569474..438ca03 100644 --- a/src/linkeddata_api/views/api_v1/viewer/entrypoint.py +++ b/src/linkeddata_api/views/api_v1/viewer/entrypoint.py @@ -1,5 +1,4 @@ -from flask import Response -from werkzeug.exceptions import HTTPException +from flask import abort from flask_tern import openapi from linkeddata_api.views.api_v1.blueprint import bp @@ -31,10 +30,10 @@ def get_entrypoint(viewer_id: str): sparql_endpoint = obj["sparql_endpoint"] items = obj["func"](sparql_endpoint) except ViewerIDNotFoundError as err: - raise HTTPException(str(err), Response(str(err), 404)) from err + abort(404, str(err)) except (RequestError, SPARQLResultJSONError) as err: - raise HTTPException(err.description, Response(err.description, 502)) from err + abort(502, err.description) except Exception as err: - raise HTTPException(str(err), Response(str(err), 500)) from err + abort(500, err) return jsonify(items, headers={"cache-control": "max-age=600, s-maxage=3600"}) diff --git a/src/linkeddata_api/views/api_v1/viewer/resource.py b/src/linkeddata_api/views/api_v1/viewer/resource.py index 0b30f72..1653963 100644 --- a/src/linkeddata_api/views/api_v1/viewer/resource.py +++ b/src/linkeddata_api/views/api_v1/viewer/resource.py @@ -1,7 +1,6 @@ import logging -from flask import request, Response -from werkzeug.exceptions import HTTPException +from flask import request, Response, abort from flask_tern import openapi from linkeddata_api.views.api_v1.blueprint import bp @@ -29,9 +28,11 @@ def get_resource(): True if include_incoming_relationships == "true" else False ) - if uri is None or sparql_endpoint is None: - err_msg = "Required query parameters 'uri' or 'sparql_endpoint' was not provided." - raise HTTPException(err_msg, Response(err_msg, 404)) + if not uri or not sparql_endpoint: + err_msg = ( + "Required query parameters 'uri' or 'sparql_endpoint' was not provided." + ) + abort(400, err_msg) logger.info( """ @@ -57,17 +58,11 @@ def get_resource(): uri, sparql_endpoint, format_, include_incoming_relationships ) except SPARQLNotFoundError as err: - raise HTTPException(err.description, Response(err.description, 404)) from err + abort(404, err.description) except (RequestError, SPARQLResultJSONError) as err: - raise HTTPException( - description=err.description, - response=Response(err.description, status=502), - ) from err + abort(502, err.description) except Exception as err: - raise HTTPException( - description=str(err), - response=Response(str(err), mimetype="text/plain", status=500), - ) from err + abort(500, err) return Response( result, diff --git a/src/linkeddata_api/views/api_v1/vocab_viewer/__init__.py b/src/linkeddata_api/views/api_v1/vocab_viewer/__init__.py deleted file mode 100644 index 3bc6c74..0000000 --- a/src/linkeddata_api/views/api_v1/vocab_viewer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import nrm diff --git a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/__init__.py b/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/__init__.py deleted file mode 100644 index 140e5e0..0000000 --- a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import vocabs -from . import resource diff --git a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/resource.py b/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/resource.py deleted file mode 100644 index 79c263c..0000000 --- a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/resource.py +++ /dev/null @@ -1,27 +0,0 @@ -from flask import request -from flask_tern import openapi -from flask_tern.logging import create_audit_event, log_audit -from werkzeug.exceptions import HTTPException -from werkzeug.wrappers import Response - -from linkeddata_api.domain.pydantic_jsonify import jsonify -from linkeddata_api.views.api_v1.blueprint import bp -from linkeddata_api.vocab_viewer import nrm - - -@bp.get("/vocab_viewer/nrm/resource") -@openapi.validate(validate_response=False) -def get_nrm_resource(): - uri = request.args.get("uri") - sparql_endpoint = request.args.get("sparql_endpoint") - - try: - result = nrm.resource.get(uri, sparql_endpoint) - except nrm.exceptions.SPARQLNotFoundError as err: - raise HTTPException(err.description, Response(err.description, 404)) from err - except (nrm.exceptions.RequestError, nrm.exceptions.SPARQLResultJSONError) as err: - raise HTTPException(err.description, Response(err.description, 502)) from err - except Exception as err: - raise HTTPException(str(err), Response(str(err), 500)) from err - - return jsonify(result, headers={"cache-control": "max-age=600, s-maxage=3600"}) diff --git a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/vocabs.py b/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/vocabs.py deleted file mode 100644 index f93435f..0000000 --- a/src/linkeddata_api/views/api_v1/vocab_viewer/nrm/vocabs.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask_tern import openapi -from flask_tern.logging import create_audit_event, log_audit -from werkzeug.exceptions import HTTPException -from werkzeug.wrappers import Response - -from linkeddata_api.domain.pydantic_jsonify import jsonify -from linkeddata_api.views.api_v1.blueprint import bp -from linkeddata_api.vocab_viewer import nrm - - -@bp.route("/vocab_viewer/nrm/vocabs") -@openapi.validate(validate_response=False) -def get_nrm_vocabs(): - # TODO: add log audit. - - try: - items = nrm.vocabs.get() - except (nrm.exceptions.RequestError, nrm.exceptions.SPARQLResultJSONError) as err: - raise HTTPException(err.description, Response(err.description, 502)) from err - except Exception as err: - raise HTTPException(str(err), Response(str(err), 500)) from err - - return jsonify(items, headers={"cache-control": "max-age=600, s-maxage=3600"}) diff --git a/src/linkeddata_api/vocab_viewer/__init__.py b/src/linkeddata_api/vocab_viewer/__init__.py deleted file mode 100644 index 3bc6c74..0000000 --- a/src/linkeddata_api/vocab_viewer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import nrm diff --git a/src/linkeddata_api/vocab_viewer/nrm/__init__.py b/src/linkeddata_api/vocab_viewer/nrm/__init__.py deleted file mode 100644 index 4885a17..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from . import vocabs -from . import exceptions -from . import resource -from . import sparql -from . import label -from . import curie -from . import internal_resource -from . import namespaces -from . import schema diff --git a/src/linkeddata_api/vocab_viewer/nrm/curie.py b/src/linkeddata_api/vocab_viewer/nrm/curie.py deleted file mode 100644 index 614d90f..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/curie.py +++ /dev/null @@ -1,68 +0,0 @@ -import requests - -# URIs that don't have curies in external service. -not_found = {} - -# Predefined prefixes. New prefixes get added at runtime. -prefixes = { - "http://purl.org/dc/terms/": "dcterms", - "http://www.w3.org/2004/02/skos/core#": "skos", - "http://www.w3.org/2000/01/rdf-schema#": "rdfs", - "https://schema.org/": "schema", - "https://w3id.org/tern/ontologies/tern/": "tern", - "http://www.w3.org/2002/07/owl#": "owl", - "http://www.w3.org/2001/XMLSchema#": "xsd", -} - -# Don't find curies for these. -skips = [ - "https://linked.data.gov.au/def/nrm", - "https://linked.data.gov.au/def/test/dawe-cv", -] - - -def uri_in_skips(uri: str) -> bool: - for skip in skips: - if uri.startswith(skip): - return True - return False - - -def get(uri: str): - """Get curie - - 1. Check if it exists in prefixes. - 2. Check if it exists in cache. - 3. Make an expensive request to an external service. Cache the result. - - If all steps fail to find a curie, return the uri as-is. - """ - - for key, val in prefixes.items(): - if uri.startswith(key): - localname = uri.split("#")[-1].split("/")[-1] - curie = f"{val}:{localname}" - return curie - - if uri in not_found: - return not_found.get(uri) - if uri_in_skips(uri): - return uri - - localname = uri.split("#")[-1].split("/")[-1] - r_index = uri.rfind(localname) - base_uri = uri[:r_index] - - response = requests.post( - "https://prefix.zazuko.com/api/v1/shrink", params={"q": base_uri} - ) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError: - not_found[uri] = uri - return uri - - prefix = response.json()["value"][:-1] - prefixes[base_uri] = prefix - return f"{prefix}:{localname}" diff --git a/src/linkeddata_api/vocab_viewer/nrm/exceptions.py b/src/linkeddata_api/vocab_viewer/nrm/exceptions.py deleted file mode 100644 index 56d6c70..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/exceptions.py +++ /dev/null @@ -1,22 +0,0 @@ -class RequestError(Exception): - """Request Exception""" - - def __init__(self, description: str) -> None: - super().__init__(description) - self.description = description - - -class SPARQLResultJSONError(Exception): - """SPARQL Result JSON Error""" - - def __init__(self, description: str) -> None: - super().__init__(description) - self.description = description - - -class SPARQLNotFoundError(Exception): - """SPARQL Not Found Error""" - - def __init__(self, description: str) -> None: - super().__init__(description) - self.description = description diff --git a/src/linkeddata_api/vocab_viewer/nrm/internal_resource.py b/src/linkeddata_api/vocab_viewer/nrm/internal_resource.py deleted file mode 100644 index fc90a95..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/internal_resource.py +++ /dev/null @@ -1,47 +0,0 @@ -from jinja2 import Template - -from linkeddata_api.vocab_viewer import nrm - - -def _get_from_list_query(uris: list[str]) -> str: - template = Template( - """ - PREFIX skos: - SELECT distinct ?uri ?internal - WHERE { - VALUES (?uri) { - {% for uri in uris %} - (<{{ uri }}>) - {% endfor %} - } - - bind(exists{ ?uri ?p ?o } as ?internal) - } - """ - ) - return template.render(uris=uris) - - -def get_from_list( - uris: list[str], - sparql_endpoint: str, -) -> dict[str, str]: - query = _get_from_list_query(uris) - - result = nrm.sparql.post(query, sparql_endpoint) - - return_results = {} - - try: - rows = result["results"]["bindings"] - for row in rows: - uri = str(row["uri"]["value"]) - internal = str(row["internal"]["value"]) - return_results[uri] = True if internal == "true" else False - - except KeyError as err: - raise nrm.exceptions.SPARQLResultJSONError( - f"Unexpected SPARQL result set.\n{result}\n{err}" - ) from err - - return return_results diff --git a/src/linkeddata_api/vocab_viewer/nrm/label.py b/src/linkeddata_api/vocab_viewer/nrm/label.py deleted file mode 100644 index 62ebe13..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/label.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import Union - -from jinja2 import Template - -from linkeddata_api.vocab_viewer import nrm - - -def get( - uri: str, - sparql_endpoint: str, -) -> Union[str, None]: - """ - Returns a label or None if no label found. - """ - query = f""" - PREFIX skos: - SELECT DISTINCT ?label - WHERE {{ - VALUES (?labelProperty) {{ - (skos:prefLabel) - }} - <{uri}> ?labelProperty ?label . - }} - """ - - result = nrm.sparql.post(query, sparql_endpoint) - - try: - rows = result["results"]["bindings"] - for row in rows: - return row["label"]["value"] - except KeyError as err: - raise nrm.exceptions.SPARQLResultJSONError( - f"Unexpected SPARQL result set.\n{result}\n{err}" - ) from err - - -def _get_from_list_query(uris: list[str]) -> str: - # TODO: Currently, we try and fetch from TERN's controlled vocabularies. - # We may want to also fetch with a SERVICE query from other repositories in the future. - template = Template( - """ - PREFIX skos: - SELECT DISTINCT ?uri (SAMPLE(?_label) AS ?label) - WHERE { - VALUES (?uri) { - {% for uri in uris %} - (<{{ uri }}>) - {% endfor %} - } - - { - ?uri skos:prefLabel ?_label . - } - UNION { - # Also try and fetch label from TERN's controlled vocabularies. - SERVICE { - ?uri skos:prefLabel ?_label . - } - } - } - GROUP BY ?uri - """ - ) - return template.render(uris=uris) - - -def get_from_list( - uris: list[str], - sparql_endpoint: str, -) -> dict[str, str]: - """Returns a dict of uri keys and label values. - - In addition to the SPARQL endpoint provided, it also fetches labels - from TERN's controlled vocabularies via a federated SPARQL query. - """ - query = _get_from_list_query(uris) - - result = nrm.sparql.post(query, sparql_endpoint) - - labels = {} - - try: - rows = result["results"]["bindings"] - for row in rows: - uri = str(row["uri"]["value"]) - label = str(row["label"]["value"]) - labels[uri] = label - - except KeyError as err: - if result["results"]["bindings"] == [{}]: - return {} - - raise nrm.exceptions.SPARQLResultJSONError( - f"Unexpected SPARQL result set.\n{result}\n{err}" - ) from err - - return labels diff --git a/src/linkeddata_api/vocab_viewer/nrm/namespaces.py b/src/linkeddata_api/vocab_viewer/nrm/namespaces.py deleted file mode 100644 index 84e5eca..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/namespaces.py +++ /dev/null @@ -1,3 +0,0 @@ -from rdflib import Namespace - -TERN = Namespace("https://w3id.org/tern/ontologies/tern/") diff --git a/src/linkeddata_api/vocab_viewer/nrm/resource/__init__.py b/src/linkeddata_api/vocab_viewer/nrm/resource/__init__.py deleted file mode 100644 index e45da64..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/resource/__init__.py +++ /dev/null @@ -1,324 +0,0 @@ -from rdflib import RDF - -from linkeddata_api.vocab_viewer import nrm -from linkeddata_api.vocab_viewer.nrm.resource.exists_uri import exists_uri -from linkeddata_api.vocab_viewer.nrm.resource.profiles import method_profile -from linkeddata_api.vocab_viewer.nrm.resource.sort_property_objects import ( - sort_property_objects, -) - - -def _get_uris_from_rdf_list(uri: str, rows: list, sparql_endpoint: str) -> list[str]: - new_uris = [] - for row in rows: - if row["o"]["type"] == "bnode" and row["listItem"]["value"] == "true": - # TODO: error handling - move empty result exception to nrm.sparql.post/nrm.sparql.get - query = f""" - PREFIX skos: - PREFIX rdf: - SELECT DISTINCT ?p ?o - where {{ - BIND(<{row["p"]["value"]}> AS ?p) - <{uri}> ?p ?list . - ?list rdf:rest* ?rest . - ?rest rdf:first ?o . - }} - """ - result = nrm.sparql.post( - query, - sparql_endpoint, - ) - - for result_row in result["results"]["bindings"]: - new_uris.append(result_row) - - return new_uris - - -def _get_uri_values_and_list_items( - result: dict, uri: str, sparql_endpoint: str -) -> tuple[list[str], list[str]]: - uri_values = filter( - lambda x: x["o"]["type"] == "uri", result["results"]["bindings"] - ) - - uri_values = [value["o"]["value"] for value in uri_values] - uri_values.append(uri) - - # Replace value of blank node list head with items. - list_items = _get_uris_from_rdf_list( - uri, result["results"]["bindings"], sparql_endpoint - ) - - for row in list_items: - uri_values.append(row["o"]["value"]) - - return uri_values, list_items - - -def _add_rows_for_rdf_list_items(result: dict, uri: str, sparql_endpoint: str) -> dict: - """Add rdf:List items as new rows to the SPARQL result object - - :param result: The SPARQL result dict object - :param uri: URI of the resource - :param sparql_endpoint: SPARQL endpoint to fetch the list items from - :return: An updated SPARQL result dict object - """ - _, list_items = _get_uri_values_and_list_items(result, uri, sparql_endpoint) - - # Add additional rows to the `result` representing the RDF List items. - for i, list_item in enumerate(list_items): - list_item.update( - { - "listItem": { - "datatype": "http://www.w3.org/2001/XMLSchema#boolean", - "type": "literal", - "value": "true", - }, - "listItemNumber": { - "datatype": "http://www.w3.org/2001/XMLSchema#integer", - "type": "literal", - "value": str(i), - }, - } - ) - result["results"]["bindings"].append(list_item) - - return result - - -def _get_uri_label_index( - result: dict, uri: str, sparql_endpoint: str -) -> dict[str, str]: - uri_values, _ = _get_uri_values_and_list_items(result, uri, sparql_endpoint) - uri_label_index = nrm.label.get_from_list(uri_values, sparql_endpoint) - return uri_label_index - - -def _get_uri_internal_index( - result: dict, uri: str, sparql_endpoint: str -) -> dict[str, str]: - uri_values, _ = _get_uri_values_and_list_items(result, uri, sparql_endpoint) - uri_internal_index = nrm.internal_resource.get_from_list( - uri_values, sparql_endpoint - ) - return uri_internal_index - - -def get(uri: str, sparql_endpoint: str) -> nrm.schema.Resource: - query = f""" - SELECT ?p ?o ?listItem ?listItemNumber - WHERE {{ - <{uri}> ?p ?o . - BIND(EXISTS{{?o rdf:rest ?rest}} as ?listItem) - - # This gets set later with the listItemNumber value. - BIND(0 AS ?listItemNumber) - }} - """ - - result = nrm.sparql.post(query, sparql_endpoint) - - try: - - result = _add_rows_for_rdf_list_items(result, uri, sparql_endpoint) - label = nrm.label.get(uri, sparql_endpoint) or uri - types, properties = _get_types_and_properties(result, uri, sparql_endpoint) - - profile = "" - if exists_uri("https://w3id.org/tern/ontologies/tern/MethodCollection", types): - profile = "https://w3id.org/tern/ontologies/tern/MethodCollection" - properties = method_profile(properties) - elif exists_uri("https://w3id.org/tern/ontologies/tern/Method", types): - profile = "https://w3id.org/tern/ontologies/tern/Method" - properties = method_profile(properties) - - incoming_properties = _get_incoming_properties(uri, sparql_endpoint) - - return nrm.schema.Resource( - uri=uri, - profile=profile, - label=label, - types=types, - properties=properties, - # incoming_properties=incoming_properties, - incoming_properties=[] # TODO: - ) - except nrm.exceptions.SPARQLNotFoundError as err: - raise err - except Exception as err: - raise nrm.exceptions.SPARQLResultJSONError( - f"Unexpected SPARQL result.\n{result}\n{err}" - ) from err - - -def _get_incoming_properties(uri: str, sparql_endpoint: str): - query = f""" - SELECT ?p ?o ?listItem ?listItemNumber - WHERE {{ - ?o ?p <{uri}> . - - # This is not required for `incoming_properties` - # but we need to set the values for compatibility with `properties`. - BIND(EXISTS{{?o rdf:rest ?rest}} as ?listItem) - BIND(0 AS ?listItemNumber) - }} - """ - - result = nrm.sparql.post( - query, - sparql_endpoint, - ) - - uri_label_index = _get_uri_label_index(result, uri, sparql_endpoint) - uri_internal_index = _get_uri_internal_index(result, uri, sparql_endpoint) - - incoming_properties = [] - - for row in result["results"]["bindings"]: - subject_label = uri_label_index.get(row["o"]["value"]) or nrm.curie.get( - row["o"]["value"] - ) - item = nrm.schema.URI( - label=subject_label, - value=row["o"]["value"], - internal=uri_internal_index.get(row["o"]["value"], False), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=row["listItemNumber"]["value"] - if row["listItem"]["value"] == "true" - else None, - ) - predicate_label = nrm.curie.get(row["p"]["value"]) - predicate = nrm.schema.URI( - label=predicate_label, - value=row["p"]["value"], - internal=uri_internal_index.get(row["p"]["value"], False), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=int(row["listItemNumber"]["value"]) - if row["listItem"]["value"] == "true" - else None, - ) - - found = False - for p in incoming_properties: - if p.predicate.value == predicate.value: - found = True - p.subjects.append(item) - - if not found: - incoming_properties.append( - nrm.schema.SubjectPredicates(predicate=predicate, subjects=[item]) - ) - - return incoming_properties - - -def _get_types_and_properties( - result: dict, uri: str, sparql_endpoint: str -) -> tuple[list[nrm.schema.URI], list[nrm.schema.PredicateObjects]]: - - types: list[nrm.schema.URI] = [] - properties: list[nrm.schema.PredicateObjects] = [] - - # An index of URIs with label values. - uri_label_index = _get_uri_label_index(result, uri, sparql_endpoint) - - # An index of all the URIs linked to and from this resource that are available internally. - uri_internal_index = _get_uri_internal_index(result, uri, sparql_endpoint) - - if not uri_internal_index.get(uri): - raise nrm.exceptions.SPARQLNotFoundError(f"Resource with URI {uri} not found.") - - for row in result["results"]["bindings"]: - if row["p"]["value"] == str(RDF.type): - type_label = uri_label_index.get(row["o"]["value"]) or nrm.curie.get( - row["o"]["value"] - ) - types.append( - nrm.schema.URI( - label=type_label, - value=row["o"]["value"], - internal=uri_internal_index.get(row["o"]["value"], False), - ) - ) - else: - predicate_label = nrm.curie.get(row["p"]["value"]) - predicate = nrm.schema.URI( - label=predicate_label, - value=row["p"]["value"], - internal=uri_internal_index.get(row["p"]["value"], False), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=int(row["listItemNumber"]["value"]) - if row["listItem"]["value"] == "true" - else None, - ) - if row["o"]["type"] == "uri": - object_label = uri_label_index.get(row["o"]["value"]) or nrm.curie.get( - row["o"]["value"] - ) - item = nrm.schema.URI( - label=object_label, - value=row["o"]["value"], - internal=uri_internal_index.get(row["o"]["value"], False), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=row["listItemNumber"]["value"] - if row["listItem"]["value"] == "true" - else None, - ) - elif row["o"]["type"] == "literal": - datatype = row["o"].get("datatype", "") - if datatype: - datatype = nrm.schema.URI( - label=datatype, - value=datatype, - internal=uri_internal_index.get(datatype, False), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=row["listItemNumber"]["value"] - if row["listItem"]["value"] == "true" - else None, - ) - else: - datatype = None - - item = nrm.schema.Literal( - value=row["o"]["value"], - datatype=datatype, - language=row["o"].get("xml:lang", ""), - list_item=True if row["listItem"]["value"] == "true" else False, - list_item_number=row["listItemNumber"]["value"] - if row["listItem"]["value"] == "true" - else None, - ) - elif row["o"]["type"] == "bnode": - # TODO: Handle blank nodes. - pass - else: - raise ValueError( - f"Expected type to be uri or literal but got {row['o']['type']}" - ) - - found = False - for p in properties: - if p.predicate.value == predicate.value: - found = True - p.objects.append(item) - - if not found: - properties.append( - nrm.schema.PredicateObjects(predicate=predicate, objects=[item]) - ) - - # Duplicates may occur due to processing RDF lists. - # Remove duplicates, if any. - for property_ in properties: - if property_.predicate.list_item: - for obj in property_.objects: - if not obj.list_item: - property_.objects.remove(obj) - - # Sort all property objects by label. - properties.sort(key=lambda x: x.predicate.label) - for property_ in properties: - property_.objects.sort(key=sort_property_objects) - - return types, properties diff --git a/src/linkeddata_api/vocab_viewer/nrm/resource/exists_uri.py b/src/linkeddata_api/vocab_viewer/nrm/resource/exists_uri.py deleted file mode 100644 index eae0d57..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/resource/exists_uri.py +++ /dev/null @@ -1,8 +0,0 @@ -from linkeddata_api.vocab_viewer import nrm - - -def exists_uri(target_uri: str, uris: list[nrm.schema.URI]) -> bool: - for uri in uris: - if uri.value == target_uri: - return True - return False diff --git a/src/linkeddata_api/vocab_viewer/nrm/resource/profiles.py b/src/linkeddata_api/vocab_viewer/nrm/resource/profiles.py deleted file mode 100644 index b5e72d2..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/resource/profiles.py +++ /dev/null @@ -1,51 +0,0 @@ -from rdflib import RDFS, SKOS, SDO, DCTERMS - -from linkeddata_api.vocab_viewer import nrm -from linkeddata_api.vocab_viewer.nrm.namespaces import TERN - - -def _add_and_remove_property( - predicate_uri: str, - old_list: list[nrm.schema.PredicateObjects], - new_list: list[nrm.schema.PredicateObjects], -) -> None: - """Add and remove the PredicateObjects object if matched by predicate_uri in - the referenced lists, 'old_list' and 'new_list' - - Returns a copy of the PredicateObjects object. - """ - predicate_object = None - for property_ in old_list: - if property_.predicate.value == predicate_uri: - new_list.append(property_) - predicate_object = property_ - old_list.remove(property_) - return predicate_object - - -def method_profile( - properties: list[nrm.schema.PredicateObjects], -) -> list[nrm.schema.PredicateObjects]: - new_properties = [] - - _add_and_remove_property(str(RDFS.isDefinedBy), properties, new_properties) - - # Omit skos:prefLabel - _add_and_remove_property(str(SKOS.prefLabel), properties, new_properties) - new_properties.pop() - - _add_and_remove_property(str(TERN), properties, new_properties) - _add_and_remove_property(str(SDO.url), properties, new_properties) - _add_and_remove_property(str(SKOS.memberList), properties, new_properties) - _add_and_remove_property(str(TERN.scope), properties, new_properties) - _add_and_remove_property(str(SKOS.definition), properties, new_properties) - _add_and_remove_property(str(TERN.purpose), properties, new_properties) - # TODO: Change to different property due to issue with RVA - _add_and_remove_property(str(DCTERMS.description), properties, new_properties) - _add_and_remove_property(str(TERN.equipment), properties, new_properties) - _add_and_remove_property(str(TERN.instructions), properties, new_properties) - _add_and_remove_property(str(SKOS.note), properties, new_properties) - _add_and_remove_property(str(DCTERMS.source), properties, new_properties) - _add_and_remove_property(str(TERN.appendix), properties, new_properties) - - return new_properties + properties diff --git a/src/linkeddata_api/vocab_viewer/nrm/resource/sort_property_objects.py b/src/linkeddata_api/vocab_viewer/nrm/resource/sort_property_objects.py deleted file mode 100644 index c010858..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/resource/sort_property_objects.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Union - -from linkeddata_api.vocab_viewer import nrm - - -def sort_property_objects(item: list[Union[nrm.schema.URI, nrm.schema.Literal]]): - if item.list_item: - return item.list_item_number - else: - if item.type == "uri": - return item.label - else: - return item.value diff --git a/src/linkeddata_api/vocab_viewer/nrm/schema.py b/src/linkeddata_api/vocab_viewer/nrm/schema.py deleted file mode 100644 index 19fd668..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/schema.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Union - -from pydantic import BaseModel - - -class Item(BaseModel): - id: str - label: str - description: str = None - created: str = None - modified: str = None - - -class RDFListItemMixin(BaseModel): - """An item in an RDF List""" - - list_item: bool = False - list_item_number: int | None = None - - -class URI(RDFListItemMixin): - type: str = "uri" - label: str - value: str - internal: bool - - def __hash__(self): - return hash(self.value) - - -class Literal(RDFListItemMixin): - type: str = "literal" - value: str - datatype: URI = None - language: str = "" - - def __hash__(self): - datatype = self.datatype.value if self.datatype else "" - return hash(self.value + datatype + self.language) - - -class SubjectPredicates(BaseModel): - predicate: URI - subjects: list[URI] - - -class PredicateObjects(BaseModel): - predicate: URI - objects: list[Union[URI, Literal]] - - -class Resource(BaseModel): - uri: str - profile: str = "" - label: str - types: list[URI] - properties: list[PredicateObjects] - incoming_properties: list[SubjectPredicates] diff --git a/src/linkeddata_api/vocab_viewer/nrm/sparql.py b/src/linkeddata_api/vocab_viewer/nrm/sparql.py deleted file mode 100644 index b390987..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/sparql.py +++ /dev/null @@ -1,39 +0,0 @@ -import requests - -from linkeddata_api.vocab_viewer import nrm - - -def post(query: str, sparql_endpoint: str) -> dict: - headers = { - "accept": "application/sparql-results+json", - "content-type": "application/sparql-query", - } - - response = requests.post(url=sparql_endpoint, headers=headers, data=query) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as err: - raise nrm.exceptions.RequestError(err.response.text) from err - - # TODO: raise empty response error here. - - return response.json() - - -def get(query: str, sparql_endpoint: str) -> dict: - headers = { - "accept": "application/sparql-results+json", - } - params = {"query": query} - - response = requests.get(url=sparql_endpoint, headers=headers, params=params) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as err: - raise nrm.exceptions.RequestError(err.response.text) from err - - # TODO: raise empty response error here. - - return response.json() diff --git a/src/linkeddata_api/vocab_viewer/nrm/vocabs.py b/src/linkeddata_api/vocab_viewer/nrm/vocabs.py deleted file mode 100644 index e4b13b3..0000000 --- a/src/linkeddata_api/vocab_viewer/nrm/vocabs.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Optional - -from linkeddata_api.vocab_viewer import nrm -from . import schema - - -def get_optional_value(row: dict, key: str) -> Optional[str]: - return row.get(key)["value"] if row.get(key) else None - - -def get( - sparql_endpoint: str = "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", -) -> schema.Item: - """Get - - Raises RequestError and SPARQLResultJSONError - """ - - query = """ - PREFIX skos: - PREFIX dcterms: - PREFIX reg: - SELECT - ?uri - (SAMPLE(?_label) as ?label) - (SAMPLE(?_description) as ?description) - (SAMPLE(?_created) as ?created) - (SAMPLE(?_modified) as ?modified) - FROM - FROM - WHERE { - dcterms:hasPart ?uri . - VALUES (?vocabularyType) { - (skos:ConceptScheme) - (skos:Collection) - } - ?uri a ?vocabularyType ; - skos:prefLabel ?_label . - - OPTIONAL { ?uri dcterms:description ?_description } - OPTIONAL { ?uri dcterms:created ?_created } - OPTIONAL { ?uri dcterms:modified ?_modified } - } - GROUP by ?uri - ORDER by ?label - """ - - result = nrm.sparql.post(query, sparql_endpoint) - - vocabs = [] - - try: - for row in result["results"]["bindings"]: - vocabs.append( - schema.Item( - id=str(row["uri"]["value"]), - label=str(row["label"]["value"]), - description=get_optional_value(row, "description"), - created=get_optional_value(row, "created"), - modified=get_optional_value(row, "modified"), - ) - ) - except KeyError as err: - raise nrm.exceptions.SPARQLResultJSONError( - f"Unexpected SPARQL result set.\n{result}\n{err}" - ) from err - - return vocabs diff --git a/tests/api_v1/dawe_viewer/test_get_vocabs.py b/tests/api_v1/dawe_viewer/test_get_vocabs.py deleted file mode 100644 index 78fd031..0000000 --- a/tests/api_v1/dawe_viewer/test_get_vocabs.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -from pytest_mock import MockerFixture - -from flask.testing import FlaskClient - -from requests import Response - - -@pytest.fixture -def url() -> str: - return "/api/v1.0/vocab_viewer/nrm/vocabs" - - -def test_get_vocabs(client: FlaskClient, url: str): - response = client.get(url) - data = response.json - assert len(data) >= 5 - - -def test_get_vocabs_request_error(client: FlaskClient, url: str, mocker: MockerFixture): - mocked_response = Response() - mocked_response.status_code = 400 - - mocker.patch("requests.post", return_value=mocked_response) - - response = client.get(url) - assert response.status_code == 502 - - -def test_get_vocabs_bad_sparql_response( - client: FlaskClient, url: str, mocker: MockerFixture -): - mocked_response = Response() - mocked_response.status_code = 200 - mocked_response._content = b"{}" - - mocker.patch("requests.post", return_value=mocked_response) - - response = client.get(url) - - assert response.status_code == 502 - assert "Unexpected SPARQL result set." in response.text diff --git a/tests/api_v1/viewer/test_entrypoint.py b/tests/api_v1/viewer/test_entrypoint.py new file mode 100644 index 0000000..86bda90 --- /dev/null +++ b/tests/api_v1/viewer/test_entrypoint.py @@ -0,0 +1,38 @@ +import pytest +import requests +from pytest_mock import MockerFixture +from flask.testing import FlaskClient +from werkzeug.test import TestResponse + + +@pytest.fixture +def url() -> str: + return "/api/v1.0/viewer/entrypoint" + + +@pytest.mark.parametrize( + "status_code, viewer_id, content_type, content", + [ + (404, "not-exist", "application/json", "Key 'not-exist' not found"), + (200, "nrm", "application/json", None), + ], +) +def test( + client: FlaskClient, + url: str, + mocker: MockerFixture, + status_code: int, + viewer_id: str, + content_type: str, + content: str, +): + mocked_response = requests.Response() + mocked_response.status_code = status_code + if status_code != 200: + mocked_response._content = content.encode("utf-8") + + mocker.patch("requests.get", return_value=mocked_response) + + response: TestResponse = client.get(url + f"/{viewer_id}") + assert response.status_code == status_code, response.text + assert content_type in response.headers.get("content-type") diff --git a/tests/api_v1/resource/test_resource_describe.py b/tests/api_v1/viewer/test_resource.py similarity index 81% rename from tests/api_v1/resource/test_resource_describe.py rename to tests/api_v1/viewer/test_resource.py index 98e7905..08deab1 100644 --- a/tests/api_v1/resource/test_resource_describe.py +++ b/tests/api_v1/viewer/test_resource.py @@ -3,7 +3,6 @@ from flask.testing import FlaskClient from werkzeug.test import TestResponse import requests -from rdflib import Graph @pytest.fixture @@ -47,11 +46,10 @@ def url() -> str: @pytest.mark.parametrize( - "mocked_status_code, response_status_code, accept_format, expected_format, uri, repository_id, include_incoming_relationships, content, expected_triples_count", + "test_type, response_status_code, accept_format, expected_format, uri, repository_id, include_incoming_relationships, content", [ - # Expected usage ( - 200, + "Expected usage", 200, "text/turtle", "text/turtle", @@ -59,35 +57,29 @@ def url() -> str: "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", "false", value, - 22, ), - # URI resource does not exist ( - 200, + "URI resource does not exist", 404, "text/turtle", - "text/html", + "application/json", "https://linked.data.gov.au/def/nrm/not-exist", "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", "false", "", - None, ), - # RDF4J repository does not exist ( - 415, + "RDF4J repository does not exist", 502, "text/turtle", - "text/html", + "application/json", "https://linked.data.gov.au/def/nrm", "https://graphdb.tern.org.au/repositories/dawe_vocabs_core-not-exist", "false", "", - None, ), - # Include incoming relationships ( - 200, + "Include incoming relationships", 200, "text/turtle", "text/turtle", @@ -95,11 +87,9 @@ def url() -> str: "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", "true", value, - 3179, ), - # No accepted format, default to text/turtle ( - 200, + "No accepted format, default to text/turtle", 200, "", "text/turtle", @@ -107,15 +97,34 @@ def url() -> str: "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", "false", value, - 22, + ), + ( + "uri query parameter not supplied", + 400, + "", + "", + "", + "https://graphdb.tern.org.au/repositories/dawe_vocabs_core", + "false", + "", + ), + ( + "sparql_endpoint query parameter not supplied", + 400, + "", + "", + "https://linked.data.gov.au/def/nrm", + "", + "false", + "", ), ], ) -def test_describe( +def test( client: FlaskClient, url: str, mocker: MockerFixture, - mocked_status_code: int, + test_type: str, response_status_code: int, accept_format: str, expected_format: str, @@ -123,10 +132,8 @@ def test_describe( repository_id: str, include_incoming_relationships, content: str, - expected_triples_count: int, ): mocked_response = requests.Response() - mocked_response.status_code = mocked_status_code mocked_response._content = content.encode("utf-8") mocker.patch("requests.get", return_value=mocked_response) @@ -140,10 +147,5 @@ def test_describe( "format": accept_format, }, ) - assert response.status_code == response_status_code - assert expected_format in response.headers.get("content-type") - - if response.status_code == 200: - graph = Graph() - graph.parse(data=response.text, format=expected_format) - assert len(graph) == expected_triples_count + assert response.status_code == response_status_code, test_type + assert expected_format in response.headers.get("content-type"), test_type