diff --git a/dsms/core/utils.py b/dsms/core/utils.py index 54d1e00..33bdd5b 100644 --- a/dsms/core/utils.py +++ b/dsms/core/utils.py @@ -2,16 +2,17 @@ import re from typing import Any from urllib.parse import urljoin +from uuid import UUID import requests from requests import Response -def _kitem_id2uri(kitem_id: str) -> str: +def _kitem_id2uri(kitem_id: UUID) -> str: "Convert a kitem id in the DSMS to the full resolvable URI" from dsms import Context - return f"{Context.dsms.config.host_url}/{kitem_id}" + return urljoin(str(Context.dsms.config.host_url), str(kitem_id)) def _uri2kitem_idi(uri: str) -> str: diff --git a/dsms/knowledge/kitem.py b/dsms/knowledge/kitem.py index 4319d7c..b6445d3 100644 --- a/dsms/knowledge/kitem.py +++ b/dsms/knowledge/kitem.py @@ -404,7 +404,9 @@ def dsms(cls, value: "DSMS") -> None: @property def subgraph(cls) -> Optional[Graph]: """Getter for Subgraph""" - return _get_subgraph(cls.id, cls.dsms.config.kitem_repo) + return _get_subgraph( + cls.id, cls.dsms.config.kitem_repo, is_kitem_id=True + ) @property def context(cls) -> "Context": diff --git a/dsms/knowledge/sparql_interface/sparql_interface.py b/dsms/knowledge/sparql_interface/sparql_interface.py index 239d49d..f3073bf 100644 --- a/dsms/knowledge/sparql_interface/sparql_interface.py +++ b/dsms/knowledge/sparql_interface/sparql_interface.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING +from dsms.knowledge.sparql_interface.subgraph import Subgraph from dsms.knowledge.sparql_interface.utils import ( _add_rdf, _sparql_query, @@ -9,7 +10,7 @@ ) if TYPE_CHECKING: - from typing import Any, Dict + from typing import Any, Dict, TextIO, Union from dsms.core.dsms import DSMS @@ -21,6 +22,7 @@ class SparqlInterface: def __init__(self, dsms): """Initalize the Sparql interface""" self._dsms: "DSMS" = dsms + self._subgraph = Subgraph(dsms) def query( self, query: str, repository: str = "knowledge" @@ -28,10 +30,25 @@ def query( """Perform Sparql Query""" return _sparql_query(query, repository) - def update(self, filepath: str, repository: str = "knowledge") -> None: + def update( + self, + file_or_pathlike: "Union[str, TextIO]", + repository: str = "knowledge", + ) -> None: """Perform update query from local file""" - _sparql_update(filepath, self._dsms.config.encoding, repository) - - def add_rdf(self, filepath: str, repository: str = "knowledge") -> None: + _sparql_update( + file_or_pathlike, self._dsms.config.encoding, repository + ) + + def insert( + self, + file_or_pathlike: "Union[str, TextIO]", + repository: str = "knowledge", + ) -> None: """Upload RDF to triplestore from local file""" - _add_rdf(filepath, self._dsms.config.encoding, repository) + _add_rdf(file_or_pathlike, self._dsms.config.encoding, repository) + + @property + def subgraph(cls) -> Subgraph: + """Subgraph interface for DSMS""" + return cls._subgraph diff --git a/dsms/knowledge/sparql_interface/subgraph.py b/dsms/knowledge/sparql_interface/subgraph.py new file mode 100644 index 0000000..9be0f23 --- /dev/null +++ b/dsms/knowledge/sparql_interface/subgraph.py @@ -0,0 +1,44 @@ +"""DSMS Subgraph interface""" + +from typing import TYPE_CHECKING + +from dsms.knowledge.sparql_interface.utils import ( + _create_subgraph, + _delete_subgraph, + _get_subgraph, + _update_subgraph, +) + +if TYPE_CHECKING: + from rdflib import Graph + + from dsms import DSMS + + +class Subgraph: + """Subgraph interface for DSMS""" + + def __init__(self, dsms): + """Initalize the Sparql interface""" + self._dsms: "DSMS" = dsms + + def update(self, graph: "Graph", repository: str = "knowledge") -> None: + """Update a subgraph in the DSMS""" + _update_subgraph(graph, self._dsms.config.encoding, repository) + + def create(self, graph: "Graph", repository: str = "knowledge") -> None: + """Create a subgraph in the DSMS""" + _create_subgraph(graph, self._dsms.config.encoding, repository) + + def delete(self, identifier: str, repository: str = "knowledge") -> None: + """Delete a subgraph in the DSMS""" + _delete_subgraph(identifier, repository) + + def get( + self, + identifier: str, + repository: str = "knowledge", + is_kitem_id: bool = False, + ) -> "Graph": + """Get a subgraph from the DSMS""" + return _get_subgraph(identifier, repository, is_kitem_id) diff --git a/dsms/knowledge/sparql_interface/utils.py b/dsms/knowledge/sparql_interface/utils.py index 0e0403e..056a54b 100644 --- a/dsms/knowledge/sparql_interface/utils.py +++ b/dsms/knowledge/sparql_interface/utils.py @@ -2,12 +2,14 @@ import io from typing import TYPE_CHECKING -from rdflib import Graph +from rdflib.plugins.sparql.results.jsonresults import JSONResult from dsms.core.utils import _kitem_id2uri, _perform_request if TYPE_CHECKING: - from typing import Any, Dict + from typing import Any, Dict, Optional, TextIO, Union + + from rdflib import Graph def _sparql_query(query: str, repository: str) -> "Dict[str, Any]": @@ -28,44 +30,64 @@ def _sparql_query(query: str, repository: str) -> "Dict[str, Any]": \n `{query}`""" ) from excep - return response["results"]["bindings"] + return response def _sparql_update( - filepath: str, + file_or_pathlike: "Union[str, TextIO]", encoding: str, repository: str, ) -> None: """Submit plain SPARQL-query to the DSMS instance.""" - - with open(filepath, mode="r+", encoding=encoding) as file: - response = _perform_request( - "api/knowledge/update-query", - "post", - files={"file": file}, - params={"repository": repository}, - ) + response = _perform_request( + "api/knowledge/update-query", + "post", + files=_get_file_or_pathlike(file_or_pathlike, encoding), + params={"repository": repository}, + ) if not response.ok: raise RuntimeError(f"Sparql was not successful: {response.text}") -def _add_rdf(filepath: str, encoding: str, repository: str) -> None: - """Create the subgraph in the remote backend""" +def _get_file_or_pathlike( + file_or_pathlike: "Union[str, TextIO]", encoding: str +) -> "TextIO": + if isinstance(file_or_pathlike, str): + with open(file_or_pathlike, mode="r+", encoding=encoding) as file: + files = {"file", file} + else: + if "read" not in dir(file_or_pathlike): + raise TypeError( + f"{file_or_pathlike} is neither a path" + f"or a file-like object." + ) + files = {"file": file_or_pathlike} + return files - with open(filepath, mode="r+", encoding=encoding) as file: - response = _perform_request( - "api/knowledge/add-rdf", - "post", - files={"file": file}, - params={"repository": repository}, - ) + +def _add_rdf( + file_or_pathlike: "Union[str, TextIO]", + encoding: str, + repository: str, + context: "Optional[str]" = None, +) -> None: + """Create the subgraph in the remote backend""" + params = {"repository": repository} + if context: + params["context"] = context + response = _perform_request( + "api/knowledge/add-rdf", + "post", + files=_get_file_or_pathlike(file_or_pathlike, encoding), + params=params, + ) if not response.ok: raise RuntimeError( f"Not able to create subgraph in backend: {response.text}" ) -def _delete_subgraph(identifier: str, encoding: str, repository: str) -> None: +def _delete_subgraph(identifier: str, repository: str) -> None: """Get subgraph related to a certain dataset id.""" query = f""" DELETE {{ @@ -79,42 +101,49 @@ def _delete_subgraph(identifier: str, encoding: str, repository: str) -> None: GRAPH ?g {{ ?s ?p ?o . }} }} }}""" - _sparql_update(query, encoding, repository) + response = _sparql_query(query, repository) + if not response.get("boolean"): + raise RuntimeError( + f"Deleteing subgraph was not successful: {response}" + ) + +def _create_subgraph(graph: "Graph", encoding: str, respository: str) -> None: + """Create the subgraph in the remote backend""" + upload_file = io.BytesIO(graph.serialize(encoding=encoding)) + _add_rdf( + upload_file, encoding, respository, context=f"<{graph.identifier}>" + ) -def _update_subgraph(graph: Graph, encoding: str, repository: str) -> None: + +def _update_subgraph(graph: "Graph", encoding: str, repository: str) -> None: """Update the subgraph in the remote backend""" - _delete_subgraph(graph.identifier, encoding, repository) - _add_rdf(graph, encoding, repository) + _delete_subgraph(graph.identifier, repository) + _create_subgraph(graph, encoding, repository) -def _get_subgraph(kitem_id: str, repository: str) -> Graph: +def _get_subgraph( + identifier: str, repository: str, is_kitem_id: bool = False +) -> "Graph": """Get subgraph related to a certain dataset id.""" - uri = _kitem_id2uri(kitem_id) + if is_kitem_id: + identifier = _kitem_id2uri(identifier) query = f""" SELECT DISTINCT ?s ?p ?o WHERE {{ BIND( - <{uri}> as ?g + <{identifier}> as ?g ) {{ GRAPH ?g {{ ?s ?p ?o . }} }} }}""" data = _sparql_query(query, repository) - - buffer = io.StringIO() - buffer.writelines( - f"<{row['s']['value']}> <{row['p']['value']}> <{row['o']['value']}> ." - for row in data - ) - buffer.seek(0) - - graph = Graph(identifier=uri) - graph.parse(buffer, format="n3") + graph = JSONResult(data) + graph.identifier = identifier if len(graph) == 0: - raise ValueError(f"Subgraph for id `{kitem_id}` does not exist.") + raise ValueError(f"Subgraph for id `{identifier}` does not exist.") return graph