Explorar o código

Create custom bound query

Arkadiusz Ryś %!s(int64=2) %!d(string=hai) anos
pai
achega
6f154d904e
Modificáronse 8 ficheiros con 442 adicións e 241 borrados
  1. 3 1
      pyproject.toml
  2. 3 2
      requirements.txt
  3. 190 0
      spendpoint/endpoint.py
  4. 8 92
      spendpoint/main.py
  5. 35 0
      spendpoint/service.py
  6. 145 146
      spendpoint/wrapper.py
  7. 28 0
      spendpoint/yasgui.html
  8. 30 0
      tests/test_query_endpoint.py

+ 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",

+ 3 - 2
requirements.txt

@@ -1,12 +1,13 @@
 # SpEndPoint
+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
 # Test
-pytest ~= 7.2.1
+pytest        ~= 7.2.1
+sparqlwrapper ~= 2.0.0
 # Doc
 sphinx ~= 6.1.3
 # Dev

+ 190 - 0
spendpoint/endpoint.py

@@ -0,0 +1,190 @@
+# Copied and modified from https://pypi.org/project/rdflib-endpoint/
+
+import logging
+import re
+from typing import Any, Callable, Dict, List, Optional, Union
+from urllib import parse
+
+import pkg_resources
+import rdflib
+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
+
+
+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",)
+
+
+    def __init__(
+        self,
+        *args: Any,
+        title: str,
+        description: str,
+        version: str,
+        example_query: 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
+        self.example_query = example_query
+
+        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:
+            if not query:
+                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"},
+                )
+
+            # Format and return results depending on Accept mime type in request header
+            output_mime_type = request.headers["accept"]
+            if not output_mime_type:
+                output_mime_type = "application/xml"
+            # Handle mime type for construct queries
+            if query_operation == "Construct Query" and (self.is_json_mime_type(output_mime_type) or self.is_csv_mime_type(output_mime_type)):
+                output_mime_type = "text/turtle"
+                # TODO: support JSON-LD for construct query?
+                # g.serialize(format='json-ld', indent=4)
+            if query_operation == "Construct Query" and output_mime_type == "application/xml":
+                output_mime_type = "application/rdf+xml"
+
+            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")
+
+        @self.post("/", name="SPARQL endpoint", description="", responses=api_responses)
+        async def sparql_endpoint_post(request: Request, query: Optional[str] = Query(None)) -> Response:
+            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", self.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 = []
+        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

+ 8 - 92
spendpoint/main.py

@@ -1,98 +1,14 @@
-import rdflib
-from rdflib import RDF, RDFS, ConjunctiveGraph, Literal, URIRef
-from rdflib.plugins.sparql.evalutils import _eval
-from rdflib_endpoint import SparqlEndpoint
+from spendpoint.endpoint import SparqlEndpoint
+from spendpoint import __version__
 
+from spendpoint.service import outlier_service
 
-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,
+    version = __version__,
+    functions = {
+        "https://ontology.rys.app/dt/function/outlier": outlier_service,
     },
-    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,
+    title = "SPARQL endpoint for storage and services",
+    description = "/n".join(("SPARQL endpoint.",))
 )

+ 35 - 0
spendpoint/service.py

@@ -0,0 +1,35 @@
+import logging
+
+import rdflib
+from rdflib import Literal
+from rdflib.plugins.sparql.evalutils import _eval
+from dataclasses import dataclass
+
+logging.basicConfig(encoding="utf-8", level=logging.DEBUG)
+
+
+@dataclass(init=True, repr=True, order=False, frozen=True)
+class Outlier:
+    iri: str
+    value: str
+
+def outlier_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

+ 145 - 146
spendpoint/wrapper.py

@@ -1,146 +1,145 @@
-"""
-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)
+# Copied from https://pypi.org/project/rdflib-endpoint/
+
+"""
+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 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>

+ 30 - 0
tests/test_query_endpoint.py

@@ -0,0 +1,30 @@
+import logging
+import arklog
+import pytest
+# arklog.set_config_logging()
+from SPARQLWrapper import SPARQLWrapper, JSON
+
+logging.basicConfig(encoding="utf-8", level=logging.DEBUG)
+
+# TODO Convert to test
+def query_0():
+    """"""
+    sparql = SPARQLWrapper("http://localhost:8000/")
+    sparql.setReturnFormat(JSON)
+    sparql.setQuery(
+        """
+        PREFIX dtf: <https://ontology.rys.app/dt/function/>
+        SELECT ?outlier ?outlier_value WHERE {
+            BIND(dtf:outlier("data.csv", "2") AS ?outlier)
+        }
+        """
+    )
+    ret = sparql.query().convert()
+    if not ret:
+        logging.info("No outliers!")
+    for r in ret["results"]["bindings"]:
+        logging.info(r)
+
+
+if __name__ == "__main__":
+    query_0()