Skip to content

Commit

Permalink
Merge pull request #51 from ternaustralia/edmond/entrypoint-update
Browse files Browse the repository at this point in the history
Add v2 API. Add JSON resource rendering with pagination for properties and their values with JSON profile support
  • Loading branch information
edmondchuc authored Jan 25, 2023
2 parents 44dd291 + 008fbdd commit bc4aacc
Show file tree
Hide file tree
Showing 29 changed files with 1,405 additions and 33 deletions.
3 changes: 2 additions & 1 deletion src/linkeddata_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,11 @@ def create_app(config=None) -> Flask:
app.register_blueprint(oidc_login, url_prefix="/api/oidc")

# register api blueprints
from linkeddata_api.views import api_v1, home
from linkeddata_api.views import api_v1, api_v2, home

app.register_blueprint(home.bp, url_prefix="/api")
app.register_blueprint(api_v1.bp, url_prefix="/api/v1.0")
app.register_blueprint(api_v2.bp, url_prefix="/api/v2.0")

# setup build_only route so that we can use url_for("root", _external=True) - "root" route required by oidc session login
# app.add_url_rule("/", "root", build_only=True)
Expand Down
2 changes: 0 additions & 2 deletions src/linkeddata_api/data/sparql.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import requests

from . import exceptions
from linkeddata_api.log_time import log_time


@log_time
def post(
query: str, sparql_endpoint: str, accept: str = "application/sparql-results+json"
) -> requests.Response:
Expand Down
3 changes: 0 additions & 3 deletions src/linkeddata_api/domain/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
from jinja2 import Template

from linkeddata_api import data
from linkeddata_api.log_time import log_time


@log_time
def get(
uri: str,
sparql_endpoint: str,
Expand Down Expand Up @@ -152,7 +150,6 @@ def _get_from_list_query(uris: list[str]) -> str:
return query


@log_time
def get_from_list(
uris: list[str],
sparql_endpoint: str,
Expand Down
8 changes: 4 additions & 4 deletions src/linkeddata_api/domain/viewer/resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from . import json


def _handle_json_response(uri: str, sparql_endpoint: str) -> str:
def handle_json_response(uri: str, sparql_endpoint: str) -> str:
try:
result = json.get(uri, sparql_endpoint)
except (RequestError, SPARQLNotFoundError, SPARQLResultJSONError) as err:
Expand All @@ -18,7 +18,7 @@ def _handle_json_response(uri: str, sparql_endpoint: str) -> str:
return result.json()


def _handle_rdf_response(
def handle_rdf_response(
uri: str, sparql_endpoint: str, format_: str, include_incoming_relationships: bool
) -> str:
try:
Expand Down Expand Up @@ -56,9 +56,9 @@ def get(
"""

if format_ == "application/json":
result = _handle_json_response(uri, sparql_endpoint)
result = handle_json_response(uri, sparql_endpoint)
else:
result = _handle_rdf_response(
result = handle_rdf_response(
uri, sparql_endpoint, format_, include_incoming_relationships
)
return result
25 changes: 8 additions & 17 deletions src/linkeddata_api/domain/viewer/resource/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@
from linkeddata_api.domain.viewer.resource.json.sort_property_objects import (
sort_property_objects,
)
from linkeddata_api.log_time import log_time

logger = logging.getLogger(__name__)


@log_time
def _get_uris_from_rdf_list(uri: str, rows: list, sparql_endpoint: str) -> list[str]:
new_uris = []
for row in rows:
Expand Down Expand Up @@ -44,7 +42,6 @@ def _get_uris_from_rdf_list(uri: str, rows: list, sparql_endpoint: str) -> list[
return new_uris


@log_time
def _get_uri_values_and_list_items(
result: dict, sparql_endpoint: str, uri: Optional[str] = None
) -> tuple[list[str], list[str]]:
Expand All @@ -70,7 +67,6 @@ def _get_uri_values_and_list_items(
return uri_values, list_items


@log_time
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
Expand Down Expand Up @@ -102,17 +98,15 @@ def _add_rows_for_rdf_list_items(result: dict, uri: str, sparql_endpoint: str) -
return result


@log_time
def _get_uri_label_index(
def get_uri_label_index(
result: dict, sparql_endpoint: str, uri: Optional[str] = None
) -> dict[str, str]:
uri_values, _ = _get_uri_values_and_list_items(result, sparql_endpoint, uri)
uri_label_index = domain.label.get_from_list(uri_values, sparql_endpoint)
return uri_label_index


@log_time
def _get_uri_internal_index(
def get_uri_internal_index(
result: dict, sparql_endpoint: str, uri: Optional[str] = None
) -> dict[str, str]:
uri_values, _ = _get_uri_values_and_list_items(result, sparql_endpoint, uri)
Expand All @@ -122,7 +116,6 @@ def _get_uri_internal_index(
return uri_internal_index


@log_time
def get(uri: str, sparql_endpoint: str) -> domain.schema.Resource:
query = f"""
SELECT ?p ?o ?listItem ?listItemNumber
Expand All @@ -139,7 +132,7 @@ def get(uri: str, sparql_endpoint: str) -> domain.schema.Resource:

result = _add_rows_for_rdf_list_items(result, uri, sparql_endpoint)
label = domain.label.get(uri, sparql_endpoint) or uri
types, properties = _get_types_and_properties(result, sparql_endpoint, uri)
types, properties = get_types_and_properties(result, sparql_endpoint, uri)

profile_uri = ""
ProfileClass = None
Expand Down Expand Up @@ -168,7 +161,6 @@ def get(uri: str, sparql_endpoint: str) -> domain.schema.Resource:
)


@log_time
def _get_incoming_properties(uri: str, sparql_endpoint: str):
query = f"""
SELECT ?p ?o ?listItem ?listItemNumber
Expand All @@ -187,8 +179,8 @@ def _get_incoming_properties(uri: str, sparql_endpoint: str):
sparql_endpoint,
).json()

uri_label_index = _get_uri_label_index(result, sparql_endpoint, uri)
uri_internal_index = _get_uri_internal_index(result, sparql_endpoint, uri)
uri_label_index = get_uri_label_index(result, sparql_endpoint, uri)
uri_internal_index = get_uri_internal_index(result, sparql_endpoint, uri)

incoming_properties = []

Expand Down Expand Up @@ -231,8 +223,7 @@ def _get_incoming_properties(uri: str, sparql_endpoint: str):
return incoming_properties


@log_time
def _get_types_and_properties(
def get_types_and_properties(
result: dict, sparql_endpoint: str, uri: Optional[str] = None
) -> tuple[list[domain.schema.URI], list[domain.schema.PredicateObjects]]:

Expand All @@ -242,10 +233,10 @@ def _get_types_and_properties(
] = defaultdict(set)

# An index of URIs with label values.
uri_label_index = _get_uri_label_index(result, sparql_endpoint, uri)
uri_label_index = get_uri_label_index(result, sparql_endpoint, uri)

# An index of all the URIs linked to and from this resource that are available internally.
uri_internal_index = _get_uri_internal_index(result, sparql_endpoint, uri)
uri_internal_index = get_uri_internal_index(result, sparql_endpoint, uri)

if not uri_internal_index.get(uri) and uri is not None:
raise data.exceptions.SPARQLNotFoundError(f"Resource with URI {uri} not found.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ def _uri(self) -> str:

@staticmethod
def _process_sparql_values(results: dict) -> list[PredicateObjects]:
from linkeddata_api.domain.viewer.resource.json import _get_types_and_properties
from linkeddata_api.domain.viewer.resource.json import get_types_and_properties

_, properties = _get_types_and_properties(
_, properties = get_types_and_properties(
results,
"https://graphdb.tern.org.au/repositories/dawe_vocabs_core",
)
Expand Down
7 changes: 7 additions & 0 deletions src/linkeddata_api/views/api_v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .blueprint import bp

# import all sub modules with views registered with blueprint
from . import ontology_viewer
from . import version_info
from . import rdf_tools
from . import viewer
3 changes: 3 additions & 0 deletions src/linkeddata_api/views/api_v2/blueprint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from flask_tern.openapi import OpenApiBlueprint

bp = OpenApiBlueprint("api_v2", __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import classes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import flat
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flask import request
from flask_tern import openapi
from flask_tern.logging import create_audit_event, log_audit

from linkeddata_api.domain.pydantic_jsonify import jsonify
from linkeddata_api.views.api_v2.blueprint import bp
from . import crud


@bp.route("/ontology_viewer/classes/flat")
@openapi.validate(validate_response=False)
def classes_flat_get():
# TODO: add log audit.

ontology_id = request.args.get("ontology_id")
classes = crud.get(ontology_id)

return jsonify(classes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import List

import requests
from jinja2 import Template
from werkzeug.exceptions import HTTPException
from werkzeug.wrappers import Response
from flask_tern.cache import cache

from . import schema


ontology_id_mapping = {
"tern-ontology": {
"sparql_endpoint": "https://graphdb.tern.org.au/repositories/knowledge_graph_core",
"named_graph": "https://w3id.org/tern/ontologies/tern/",
}
}


query_template = Template(
"""
# Get a list of classes ordered by label.
# Only one string or langString of type "en" is retrieved as label.
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT distinct ?id (SAMPLE(?_label) as ?label)
FROM <http://www.ontotext.com/explicit>
FROM <{{ named_graph }}>
WHERE {
{
?_class a sh:NodeShape .
?_class sh:targetClass ?id .
FILTER(!isBlank(?id))
{
?id rdfs:label ?_label .
}
UNION {
?id skos:prefLabel ?_label .
}
}
}
GROUP BY ?id
ORDER BY ?label
"""
)


@cache.memoize()
def get(ontology_id: str) -> List[schema.ClassItem]:
try:
mapping = ontology_id_mapping[ontology_id]
except KeyError as err:
description = f"Unknown ontology ID '{ontology_id}'. Valid ontology IDs: {list(ontology_id_mapping.keys())}"
raise HTTPException(
description=description, response=Response(description, status=404)
) from err

query = query_template.render(named_graph=mapping["named_graph"])
headers = {
"accept": "application/sparql-results+json",
"content-type": "application/sparql-query",
}

r = requests.post(
url=mapping["sparql_endpoint"], headers=headers, data=query, timeout=60
)

try:
r.raise_for_status()
except requests.exceptions.HTTPError as err:
raise HTTPException(
description=err.response.text,
response=Response(err.response.text, status=502),
) from err

resultset = r.json()
classes = []
for row in resultset["results"]["bindings"]:
classes.append(
schema.ClassItem(id=row["id"]["value"], label=row["label"]["value"])
)

return classes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class ClassItem(BaseModel):
id: str
label: str
Loading

0 comments on commit bc4aacc

Please sign in to comment.