Sfoglia il codice sorgente

Merge branch 'release-0.1.0'

Arkadiusz Ryś 2 anni fa
parent
commit
3a7cd0dfd4

+ 5 - 0
HISTORY.rst

@@ -2,6 +2,11 @@
 History
 =======
 
+0.1.0 (2023-03-06)
+------------------
+* Create outlier service bridge.
+* Fix Fuseki access errors.
+
 0.0.1 (2023-02-28)
 ------------------
 * Release example.

+ 4 - 0
README.rst

@@ -1,6 +1,10 @@
+##########
 SpEndPoint
 ##########
 
+Creates a SPARQL endpoint supporting custom services.
+Default access at `http://127.0.0.1:8000/gui`.
+
 Installation
 ------------
 

+ 3 - 1
pyproject.toml

@@ -20,16 +20,18 @@ dynamic = ["version", "description"]
 license = {file = "LICENSE"}
 keywords = ["spendpoint"]
 dependencies = [
+    "arklog~=0.5.0",
+    "rdflib~=6.2.0",
     "fastapi~=0.92",
     "starlette~=0.25.0",
     "python-magic~=0.4.27",
-    "rdflib-endpoint~=0.2.7",
     "uvicorn[standard]~=0.20.0",
 ]
 
 [project.optional-dependencies]
 test = [
     "pytest~=7.2.1",
+    "sparqlwrapper~=2.0.0",
 ]
 doc = [
     "sphinx~=6.1.3",

+ 4 - 2
requirements.txt

@@ -1,12 +1,14 @@
 # SpEndPoint
+arklog            ~= 0.5.1
 rdflib            ~= 6.2.0
 fastapi           ~= 0.92
+requests          ~= 2.28.2
 starlette         ~= 0.25.0
 python-magic      ~= 0.4.27
-rdflib-endpoint   ~= 0.2.7
 uvicorn[standard] ~= 0.20.0
 # Test
-pytest ~= 7.2.1
+pytest        ~= 7.2.2
+sparqlwrapper ~= 2.0.0
 # Doc
 sphinx ~= 6.1.3
 # Dev

+ 1 - 1
spendpoint/__init__.py

@@ -1,3 +1,3 @@
 """SPARQL endpoint for ontologies."""
-__version__ = "0.0.1"
+__version__ = "0.1.0"
 __version_info__ = tuple((int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")))

+ 3 - 98
spendpoint/__main__.py

@@ -1,99 +1,4 @@
-import rdflib
-from rdflib import RDF, RDFS, ConjunctiveGraph, Literal, URIRef
-from rdflib.plugins.sparql.evalutils import _eval
+import uvicorn
+from spendpoint.main import app
 
-from rdflib_endpoint import SparqlEndpoint
-
-
-def custom_concat(query_results, ctx, part, eval_part):
-    """
-    Concat 2 string and return the length as additional Length variable
-    \f
-    :param query_results:   An array with the query results objects
-    :param ctx:             <class 'rdflib.plugins.sparql.sparql.QueryContext'>
-    :param part:            Part of the query processed (e.g. Extend or BGP) <class 'rdflib.plugins.sparql.parserutils.CompValue'>
-    :param eval_part:       Part currently evaluated
-    :return:                the same query_results provided in input param, with additional results
-    """
-    argument1 = str(_eval(part.expr.expr[0], eval_part.forget(ctx, _except=part.expr._vars)))
-    argument2 = str(_eval(part.expr.expr[1], eval_part.forget(ctx, _except=part.expr._vars)))
-    evaluation = []
-    scores = []
-    concat_string = argument1 + argument2
-    reverse_string = argument2 + argument1
-    # Append the concatenated string to the results
-    evaluation.append(concat_string)
-    evaluation.append(reverse_string)
-    # Append the scores for each row of results
-    scores.append(len(concat_string))
-    scores.append(len(reverse_string))
-    # Append our results to the query_results
-    for i, result in enumerate(evaluation):
-        query_results.append(
-            eval_part.merge({part.var: Literal(result), rdflib.term.Variable(part.var + "Length"): Literal(scores[i])})
-        )
-    return query_results, ctx, part, eval_part
-
-
-def most_similar(query_results, ctx, part, eval_part):
-    """
-    Get most similar entities for a given entity
-
-    PREFIX openpredict: <https://w3id.org/um/openpredict/>
-    SELECT ?drugOrDisease ?mostSimilar ?mostSimilarScore WHERE {
-        BIND("OMIM:246300" AS ?drugOrDisease)
-        BIND(openpredict:most_similar(?drugOrDisease) AS ?mostSimilar)
-    """
-    # argumentEntity = str(_eval(part.expr.expr[0], eval_part.forget(ctx, _except=part.expr._vars)))
-    # try:
-    #     argumentLimit = str(_eval(part.expr.expr[1], eval_part.forget(ctx, _except=part.expr._vars)))
-    # except:
-    #     argumentLimit = None
-
-    # Using stub data
-    similarity_results = [{"mostSimilar": "DRUGBANK:DB00001", "score": 0.42}]
-
-    evaluation = []
-    scores = []
-    for most_similar in similarity_results:
-        evaluation.append(most_similar["mostSimilar"])
-        scores.append(most_similar["score"])
-
-    # Append our results to the query_results
-    for i, result in enumerate(evaluation):
-        query_results.append(
-            eval_part.merge({part.var: Literal(result), rdflib.term.Variable(part.var + "Score"): Literal(scores[i])})
-        )
-    return query_results, ctx, part, eval_part
-
-
-example_query = """
-PREFIX myfunctions: <https://w3id.org/um/sparql-functions/>
-SELECT ?concat ?concatLength WHERE {
-    BIND("First" AS ?first)
-    BIND(myfunctions:custom_concat(?first, "last") AS ?concat)
-}
-"""
-
-g = ConjunctiveGraph(
-    identifier=URIRef("https://w3id.org/um/sparql-functions/graph/default"),
-)
-
-# Example to add a nquad to the exposed graph
-g.add((URIRef("http://subject"), RDF.type, URIRef("http://object"), URIRef("http://graph")))
-g.add((URIRef("http://subject"), RDFS.label, Literal("foo"), URIRef("http://graph")))
-
-# Start the SPARQL endpoint based on the RDFLib Graph
-app = SparqlEndpoint(
-    graph=g,
-    functions={
-        "https://w3id.org/um/openpredict/most_similar": most_similar,
-        "https://w3id.org/um/sparql-functions/custom_concat": custom_concat,
-    },
-    title="SPARQL endpoint for RDFLib graph",
-    description="A SPARQL endpoint to serve machine learning models, or any other logic implemented in Python. \n[Source code](https://github.com/vemonet/rdflib-endpoint)",
-    version="0.1.0",
-    public_url="https://service.openpredict.137.120.31.102.nip.io/sparql",
-    cors_enabled=True,
-    example_query=example_query,
-)
+uvicorn.run(app, host="0.0.0.0", port=8000)

+ 43 - 0
spendpoint/bridge.py

@@ -0,0 +1,43 @@
+import logging
+import arklog
+import requests
+from rdflib import Graph
+from typing import Union
+
+arklog.set_config_logging()
+
+
+def fetch_outliers(
+        file_name: str = "rotation.csv",
+        column: Union[str, int] = 2,
+        iri: str = "<http://ua.be/drivetrain/description/artifacts/artifacts#drivetrain-sensor-data-v1>"
+    ) -> Graph:
+    """"""
+    outlier_service_url = "http://127.0.0.1:9090/api/csv/outlier"
+    try:
+        column = column if isinstance(column, int) else int(column)
+    except ValueError as e:
+        logging.error(f"Column '{column}' is not parseable to an integer.")
+        raise
+    parameters = {"iri": iri, "column" : column, "file" : file_name}
+    try:
+        outliers_result = requests.post(outlier_service_url, json=parameters, timeout=5)
+        outliers_result.raise_for_status()
+    except requests.exceptions.InvalidSchema as e:
+        logging.error(f"Invalid schema for '{outlier_service_url}'.")
+        raise
+    except requests.exceptions.ConnectTimeout as e:
+        logging.error(f"Request for '{outlier_service_url}' timed out.")
+        raise
+    except requests.exceptions.HTTPError as e:
+        logging.error(f"Service at '{outlier_service_url}' returned an error.")
+        raise
+    except requests.exceptions.ConnectionError as e:
+        logging.error(f"Service at '{outlier_service_url}' is unreachable.")
+        raise
+    # logging.debug(outliers_result.content)
+    # for stmt in outliers_result.content.decode().split("\n"):
+    #     logging.debug(stmt)
+    outlier_graph = Graph()
+    outlier_graph.parse(data=outliers_result.content.decode(encoding="UTF-8"), format="n3")
+    return outlier_graph

+ 196 - 0
spendpoint/endpoint.py

@@ -0,0 +1,196 @@
+# Copied and modified from https://pypi.org/project/rdflib-endpoint/
+
+import logging
+import re
+import arklog
+import pkg_resources
+import rdflib
+from typing import Any, Callable, Dict, List, Optional, Union
+from urllib import parse
+from fastapi import FastAPI, Query, Request, Response
+from fastapi.responses import JSONResponse
+from rdflib import ConjunctiveGraph, Dataset, Graph, Literal, URIRef
+from rdflib.plugins.sparql import prepareQuery
+from rdflib.plugins.sparql.evaluate import evalPart
+from rdflib.plugins.sparql.evalutils import _eval
+from rdflib.plugins.sparql.parserutils import CompValue
+from rdflib.plugins.sparql.sparql import QueryContext, SPARQLError
+
+arklog.set_config_logging()
+
+
+class SparqlEndpoint(FastAPI):
+    """SPARQL endpoint for services and storage of heterogeneous data."""
+
+    @staticmethod
+    def is_json_mime_type(mime: str) -> bool:
+        """"""
+        return mime.split(",")[0] in ("application/sparql-results+json","application/json","text/javascript","application/javascript")
+
+    @staticmethod
+    def is_csv_mime_type(mime: str) -> bool:
+        """"""
+        return mime.split(",")[0] in ("text/csv", "application/sparql-results+csv")
+
+    @staticmethod
+    def is_xml_mime_type(mime: str) -> bool:
+        """"""
+        return mime.split(",")[0] in ("application/xml", "application/sparql-results+xml")
+
+    @staticmethod
+    def is_turtle_mime_type(mime: str) -> bool:
+        """"""
+        return mime.split(",")[0] in ("text/turtle",)
+
+    async def requested_result_type(self, request: Request, operation: str) -> str:
+        logging.debug("Getting mime type.")
+        output_mime_type = request.headers["accept"]
+        # TODO Ugly hack, fix later (Fuseki sends options)
+        output_mime_type = output_mime_type.split(",")[0]
+        if isinstance(output_mime_type, list):
+            return output_mime_type[0]
+
+        # TODO Use match or dict for this
+        if not output_mime_type:
+            logging.warning("No mime type provided. Setting mimetype to 'application/xml'.")
+            return "application/xml"
+        if operation == "Construct Query" and (self.is_json_mime_type(output_mime_type) or self.is_csv_mime_type(output_mime_type)):
+            return "text/turtle"
+        if operation == "Construct Query" and output_mime_type == "application/xml":
+            return "application/rdf+xml"
+        return output_mime_type
+
+    def __init__(self, *args: Any, title: str, description: str, version: str, functions: Dict[str, Callable[..., Any]], graph: Union[Graph, ConjunctiveGraph, Dataset] = ConjunctiveGraph(), **kwargs: Any):
+        """"""
+        self.graph = graph
+        self.functions = functions
+        self.title = title
+        self.description = description
+        self.version = version
+        super().__init__(*args, title=title, description=description, version=version, **kwargs)
+        rdflib.plugins.sparql.CUSTOM_EVALS["evalCustomFunctions"] = self.eval_custom_functions
+        api_responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = {
+            200: {
+                "description": "SPARQL query results",
+                "content": {
+                    "application/sparql-results+json": {
+                        "results": {"bindings": []},
+                        "head": {"vars": []},
+                    },
+                    "application/json": {
+                        "results": {"bindings": []},
+                        "head": {"vars": []},
+                    },
+                    "text/csv": {"example": "s,p,o"},
+                    "application/sparql-results+csv": {"example": "s,p,o"},
+                    "text/turtle": {"example": "service description"},
+                    "application/sparql-results+xml": {"example": "<root></root>"},
+                    "application/xml": {"example": "<root></root>"},
+                    # "application/rdf+xml": {
+                    #     "example": '<?xml version="1.0" encoding="UTF-8"?> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"></rdf:RDF>'
+                    # },
+                },
+            },
+            400: {
+                "description": "Bad Request",
+            },
+            403: {
+                "description": "Forbidden",
+            },
+            422: {
+                "description": "Unprocessable Entity",
+            },
+        }
+
+        @self.get("/", name="SPARQL endpoint", description="", responses=api_responses)
+        async def sparql_endpoint_get(request: Request, query: Optional[str] = Query(None)) -> Response:
+            logging.debug("Received GET request.")
+            if not query:
+                logging.warning("No query provided in GET request!")
+                return JSONResponse({"error": "No query provided."})
+
+            graph_ns = {}
+            for prefix, ns_uri in self.graph.namespaces():
+                graph_ns[prefix] = ns_uri
+
+            try:
+                parsed_query = prepareQuery(query, initNs=graph_ns)
+                query_operation = re.sub(r"(\w)([A-Z])", r"\1 \2", parsed_query.algebra.name)
+            except Exception as e:
+                logging.error("Error parsing the SPARQL query: " + str(e))
+                return JSONResponse(
+                    status_code=400,
+                    content={"message": "Error parsing the SPARQL query"},
+                )
+
+            try:
+                query_results = self.graph.query(query, initNs=graph_ns)
+            except Exception as e:
+                logging.error("Error executing the SPARQL query on the RDFLib Graph: " + str(e))
+                return JSONResponse(
+                    status_code=400,
+                    content={"message": "Error executing the SPARQL query on the RDFLib Graph"},
+                )
+
+            logging.debug(f"{type(query_results)=}")
+            output_mime_type = await self.requested_result_type(request, query_operation)
+            logging.debug(f"Returning {output_mime_type}.")
+            try:
+                if self.is_csv_mime_type(output_mime_type):
+                    return Response(query_results.serialize(format="csv"), media_type=output_mime_type)
+                elif self.is_json_mime_type(output_mime_type):
+                    return Response(query_results.serialize(format="json"), media_type=output_mime_type)
+                elif self.is_xml_mime_type(output_mime_type):
+                    return Response(query_results.serialize(format="xml"), media_type=output_mime_type)
+                elif self.is_turtle_mime_type(output_mime_type):
+                    return Response(query_results.serialize(format="turtle"), media_type=output_mime_type)
+                return Response(query_results.serialize(format="xml"), media_type="application/sparql-results+xml")
+            except Exception as e:
+                logging.exception(e)
+                return JSONResponse(status_code=400, content={"message": "Error executing the SPARQL query on the RDFLib Graph"})
+
+        @self.post("/", name="SPARQL endpoint", description="", responses=api_responses)
+        async def sparql_endpoint_post(request: Request, query: Optional[str] = Query(None)) -> Response:
+            logging.debug("Received POST request.")
+            if not query:
+                # Handle federated query services which provide the query in the body
+                query_body = await request.body()
+                body = query_body.decode("utf-8")
+                parsed_query = parse.parse_qsl(body)
+                for params in parsed_query:
+                    if params[0] == "query":
+                        query = parse.unquote(params[1])
+            return await sparql_endpoint_get(request, query)
+
+        @self.get("/gui", include_in_schema=False)
+        async def serve_yasgui() -> Response:
+            """Serve YASGUI interface"""
+            with open(pkg_resources.resource_filename("spendpoint", "yasgui.html")) as f:
+                html_str = f.read()
+            html_str = html_str.replace("$EXAMPLE_QUERY", "")
+            return Response(content=html_str, media_type="text/html")
+
+    def eval_custom_functions(self, ctx: QueryContext, part: CompValue) -> List[Any]:
+        if part.name != "Extend":
+            raise NotImplementedError()
+
+        query_results = []
+        logging.debug("Custom evaluation.")
+        for eval_part in evalPart(ctx, part.p):
+            # Checks if the function is a URI (custom function)
+            if hasattr(part.expr, "iri"):
+                # Iterate through the custom functions passed in the constructor
+                for function_uri, custom_function in self.functions.items():
+                    # Check if URI correspond to a registered custom function
+                    if part.expr.iri == URIRef(function_uri):
+                        # Execute each function
+                        query_results, ctx, part, eval_part = custom_function(query_results, ctx, part, eval_part)
+            else:
+                # For built-in SPARQL functions (that are not URIs)
+                evaluation: List[Any] = [_eval(part.expr, eval_part.forget(ctx, _except=part._vars))]
+                if isinstance(evaluation[0], SPARQLError):
+                    raise evaluation[0]
+                # Append results for built-in SPARQL functions
+                for result in evaluation:
+                    query_results.append(eval_part.merge({part.var: Literal(result)}))
+        return query_results

+ 16 - 0
spendpoint/main.py

@@ -0,0 +1,16 @@
+import arklog
+from spendpoint.endpoint import SparqlEndpoint
+from spendpoint import __version__
+from spendpoint.service import outlier_service, example_service
+
+arklog.set_config_logging()
+
+app = SparqlEndpoint(
+    version = __version__,
+    functions = {
+        "https://ontology.rys.app/dt/function/outlier": outlier_service,
+        "https://ontology.rys.app/dt/function/example": example_service,
+    },
+    title = "SPARQL endpoint for storage and services",
+    description = "/n".join(("SPARQL endpoint.",))
+)

+ 75 - 0
spendpoint/service.py

@@ -0,0 +1,75 @@
+import logging
+import arklog
+import rdflib
+from rdflib import Literal
+from rdflib.plugins.sparql.evalutils import _eval
+from dataclasses import dataclass
+
+from spendpoint.bridge import fetch_outliers
+arklog.set_config_logging()
+
+
+@dataclass(init=True, repr=True, order=False, frozen=True)
+class Outlier:
+    iri: str
+    value: str
+
+def outlier_service(query_results, ctx, part, eval_part):
+    """
+
+    Example query:
+    PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+    PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+    PREFIX dtf:  <https://ontology.rys.app/dt/function/>
+    PREFIX owl:  <http://www.w3.org/2002/07/owl#>
+
+    SELECT ?outlier ?outlier_relation ?outlier_value WHERE {
+      SERVICE <http://127.0.0.1:8000/> {
+        SELECT ?outlier ?outlier_relation ?outlier_value WHERE {
+          BIND(dtf:outlier("rotation.csv", "2", "<http://ua.be/drivetrain/description/artifacts/artifacts#drivetrain-sensor-data-v1>") AS ?outlier)
+        }
+      }
+    }
+
+    :param query_results:
+    :param ctx:
+    :param part:
+    :param eval_part:
+    :return:
+    """
+    logging.debug(f"Outlier service.")
+    file_name = str(_eval(part.expr.expr[0], eval_part.forget(ctx, _except=part.expr._vars)))
+    column = str(_eval(part.expr.expr[1], eval_part.forget(ctx, _except=part.expr._vars)))
+    iri = str(_eval(part.expr.expr[2], eval_part.forget(ctx, _except=part.expr._vars)))
+    logging.info(f"Looking for outlier in '{file_name}' at column '{column}' for '{iri}'.")
+    outlier_graph = fetch_outliers(file_name, column, iri)
+    for stmt in outlier_graph:
+        query_results.append(eval_part.merge({
+            part.var: stmt[0],
+            rdflib.term.Variable(part.var + "_relation") : stmt[1],
+            rdflib.term.Variable(part.var + "_value") : stmt[2],
+        }))
+    return query_results, ctx, part, eval_part
+
+
+def example_service(query_results, ctx, part, eval_part):
+    """"""
+    logging.debug(f"{query_results=}")
+    logging.debug(f"{ctx=}")
+    logging.debug(f"{part=}")
+    logging.debug(f"{eval_part=}")
+
+    file_name = str(_eval(part.expr.expr[0], eval_part.forget(ctx, _except=part.expr._vars)))
+    column = str(_eval(part.expr.expr[1], eval_part.forget(ctx, _except=part.expr._vars)))
+    logging.info(f"Looking for outlier in '{file_name}' at column '{column}'.")
+
+    outliers = [
+        Outlier(iri="example_0",value="2.0"),
+        Outlier(iri="example_1",value="2.5"),
+        Outlier(iri="example_2",value="3.0"),
+    ]
+
+    for outlier in outliers:
+        query_results.append(eval_part.merge({part.var: Literal(outlier.iri), rdflib.term.Variable(part.var + "_value"): Literal(outlier.value)}))
+    return query_results, ctx, part, eval_part

+ 0 - 146
spendpoint/wrapper.py

@@ -1,146 +0,0 @@
-"""
-This example shows how a custom evaluation function can be added to
-handle certain SPARQL Algebra elements.
-
-A custom function is added that adds ``rdfs:subClassOf`` "inference" when
-asking for ``rdf:type`` triples.
-
-Here the custom eval function is added manually, normally you would use
-setuptools and entry_points to do it:
-i.e. in your setup.py::
-
-    entry_points = {
-        'rdf.plugins.sparqleval': [
-            'myfunc =     mypackage:MyFunction',
-            ],
-    }
-"""
-
-# EvalBGP https://rdflib.readthedocs.io/en/stable/_modules/rdflib/plugins/sparql/evaluate.html
-# Custom fct for rdf:type with auto infer super-classes: https://github.com/RDFLib/rdflib/blob/master/examples/custom_eval.py
-# BGP = Basic Graph Pattern
-# Docs rdflib custom fct: https://rdflib.readthedocs.io/en/stable/intro_to_sparql.html
-# StackOverflow: https://stackoverflow.com/questions/43976691/custom-sparql-functions-in-rdflib/66988421#66988421
-
-# Another project: https://github.com/bas-stringer/scry/blob/master/query_handler.py
-# https://www.w3.org/TR/sparql11-service-description/#example-turtle
-# Federated query: https://www.w3.org/TR/2013/REC-sparql11-federated-query-20130321/#defn_service
-# XML method: https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.plugins.sparql.results.html#module-rdflib.plugins.sparql.results.xmlresults
-
-import rdflib
-from rdflib import Literal, URIRef
-from rdflib.plugins.sparql import parser
-from rdflib.plugins.sparql.algebra import pprintAlgebra, translateQuery
-from rdflib.plugins.sparql.evaluate import evalBGP
-
-# inferredSubClass = rdflib.RDFS.subClassOf * "*"  # any number of rdfs.subClassOf
-biolink = URIRef("https://w3id.org/biolink/vocab/")
-
-
-class Result:
-    pass
-
-
-def add_to_graph(ctx, drug, disease, score):
-    bnode = rdflib.BNode()
-    ctx.graph.add((bnode, rdflib.RDF.type, rdflib.RDF.Statement))
-    ctx.graph.add((bnode, rdflib.RDF.subject, drug))
-    ctx.graph.add((bnode, rdflib.RDF.predicate, biolink + "treats"))
-    ctx.graph.add((bnode, rdflib.RDF.object, disease))
-    ctx.graph.add((bnode, biolink + "category", biolink + "ChemicalToDiseaseOrPhenotypicFeatureAssociation"))
-    ctx.graph.add((bnode, biolink + "has_confidence_level", score))
-
-
-def get_triples(disease):
-    drug = URIRef("http://bio2rdf.org/drugbank:DB00001")
-    score = Literal("1.0")
-
-    r = Result()
-    r.drug = drug
-    r.disease = disease
-    r.score = score
-
-    results = []
-    results.append(r)
-    return results
-
-
-# def parseRelationalExpr(expr):
-
-
-def custom_eval(ctx, part):
-    """ """
-    # print (part.name)
-
-    if part.name == "Project":
-        ctx.myvars = []
-
-    # search extend for variable binding
-    if part.name == "Extend" and hasattr(part, "expr") and not isinstance(part.expr, list):
-        ctx.myvars.append(part.expr)
-
-    # search for filter
-    if part.name == "Filter" and hasattr(part, "expr"):
-        if hasattr(part.expr, "expr"):
-            if part.expr.expr["op"] == "=":
-                part.expr.expr["expr"]
-                d = part.expr.expr["other"]
-                ctx.myvars.append(d)
-        else:
-            if part.expr["op"] == "=":
-                part.expr["expr"]
-                d = part.expr["other"]
-                ctx.myvars.append(d)
-
-    # search the BGP for the variable of interest
-    if part.name == "BGP":
-        triples = []
-        for t in part.triples:
-            if t[1] == rdflib.RDF.object:
-                disease = t[2]
-                # check first if the disease term is specified in the bgp triple
-                if isinstance(disease, rdflib.term.URIRef):
-                    ctx.myvars.append(disease)
-
-                # fetch instances
-                for d in ctx.myvars:
-                    results = get_triples(d)
-                    for r in results:
-                        add_to_graph(ctx, r.drug, r.disease, r.score)
-
-            triples.append(t)
-        return evalBGP(ctx, triples)
-    raise NotImplementedError()
-
-
-if __name__ == "__main__":
-    # add function directly, normally we would use setuptools and entry_points
-    rdflib.plugins.sparql.CUSTOM_EVALS["exampleEval"] = custom_eval
-
-    g = rdflib.Graph()
-
-    q = """PREFIX openpredict: <https://w3id.org/um/openpredict/>
-        PREFIX biolink: <https://w3id.org/biolink/vocab/>
-        PREFIX omim: <http://bio2rdf.org/omim:>
-        SELECT ?disease ?drug ?score
-        {
-            ?association a rdf:Statement ;
-                rdf:subject ?drug ;
-                rdf:predicate ?predicate ;
-                #rdf:object omim:246300 ;
-                rdf:object ?disease ;
-                biolink:category biolink:ChemicalToDiseaseOrPhenotypicFeatureAssociation ;
-                biolink:has_confidence_level ?score .
-            #?disease dcat:identifier "OMIM:246300" .
-            BIND(omim:1 AS ?disease)
-            #FILTER(?disease = omim:2 || ?disease = omim:3)
-            #VALUES ?disease { omim:5 omim:6 omim:7 }
-        }"""
-
-    pq = parser.parseQuery(q)
-    tq = translateQuery(pq)
-    pprintAlgebra(tq)
-
-    # Find all FOAF Agents
-    for x in g.query(q):
-        print(x)

+ 28 - 0
spendpoint/yasgui.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <title>RDFLib endpoint</title>
+  <link href="https://unpkg.com/@triply/yasgui@4/build/yasgui.min.css" rel="stylesheet" type="text/css"/>
+  <script src="https://unpkg.com/@triply/yasgui@4/build/yasgui.min.js"></script>
+</head>
+
+<body>
+<div id="yasgui"></div>
+<script>
+  Yasqe.defaults.value = `$EXAMPLE_QUERY`
+  const url = window.location.href.endsWith('/') ? window.location.href.slice(0, -1) : window.location.href;
+  const yasgui = new Yasgui(document.getElementById("yasgui"), {
+    requestConfig: {endpoint: url + "/"},
+    endpointCatalogueOptions: {
+      getData: function () {
+        return [
+          {endpoint: url + "/"},
+        ];
+      },
+      keys: [],
+    },
+  });
+</script>
+</body>

+ 0 - 0
tests/__init__.py


+ 2 - 0
tests/conftest.py

@@ -0,0 +1,2 @@
+import pytest
+from tests.fixtures.state import test_data

+ 0 - 0
tests/fixtures/__init__.py


+ 6 - 0
tests/fixtures/state.py

@@ -0,0 +1,6 @@
+import pytest
+
+
+@pytest.fixture(params=["http://localhost:8000"])
+def test_data(request):
+    return request.params

+ 100 - 0
tests/test_query_endpoint.py

@@ -0,0 +1,100 @@
+import logging
+from urllib.error import URLError
+import arklog
+from SPARQLWrapper import SPARQLWrapper, JSON
+
+arklog.set_config_logging()
+
+prefixes = "\n".join((
+    "PREFIX dtf:  <https://ontology.rys.app/dt/function/>",
+    "PREFIX owl:  <http://www.w3.org/2002/07/owl#>",
+    "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>",
+    "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>",
+    "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
+))
+
+
+# TODO Convert to test
+def query_0():
+    """"""
+    sparql = SPARQLWrapper("http://localhost:8000")
+    sparql.setReturnFormat(JSON)
+    sparql.setQuery(
+        prefixes +
+        """
+        SELECT ?outlier ?outlier_value WHERE {
+            BIND(dtf:outlier("data.csv", "2") AS ?outlier)
+        }
+        """
+    )
+    try:
+        ret = sparql.query().convert()
+    except URLError as e:
+        logging.error(e)
+        return
+    if not ret:
+        logging.info("No outliers!")
+    for r in ret["results"]["bindings"]:
+        logging.info(r)
+
+
+# TODO Convert to test
+def query_1():
+    """"""
+    sparql = SPARQLWrapper("http://localhost:8000")
+    sparql.setReturnFormat(JSON)
+    sparql.setQuery(
+        prefixes +
+        """
+        SELECT ?outlier WHERE {
+            SERVICE <http://localhost:8000/> {
+                SELECT ?outlier ?outlier_value WHERE {
+                    BIND(dtf:outlier("data2.csv", "1") AS ?outlier)
+                }
+            }
+        }
+        """
+    )
+    try:
+        ret = sparql.query().convert()
+    except URLError as e:
+        logging.error(e)
+        return
+    if not ret:
+        logging.info("No outliers!")
+    for r in ret["results"]["bindings"]:
+        logging.info(r)
+
+
+# TODO Convert to test
+def query_2():
+    """"""
+    sparql = SPARQLWrapper("http://localhost:8000")
+    sparql.setReturnFormat(JSON)
+    sparql.setQuery(
+        prefixes +
+        """
+        SELECT ?outlier ?outlier_relation ?outlier_value WHERE {
+            SERVICE <http://127.0.0.1:8000/> {
+                SELECT ?outlier ?outlier_relation ?outlier_value WHERE {
+                    BIND(dtf:outlier("rotation.csv", "2", "<http://ua.be/drivetrain/description/artifacts/artifacts#drivetrain-sensor-data-v1>") AS ?outlier)
+                }
+            }
+        }
+        """
+    )
+    try:
+        ret = sparql.query().convert()
+    except URLError as e:
+        logging.error(e)
+        return
+    if not ret:
+        logging.info("No outliers!")
+    for r in ret["results"]["bindings"]:
+        logging.info(r)
+
+
+if __name__ == "__main__":
+    query_0()
+    query_1()
+    query_2()