| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- from __future__ import annotations
- import base64
- import copy
- import logging
- from io import BytesIO
- from typing import TYPE_CHECKING, Optional, Tuple
- from urllib.error import HTTPError
- from urllib.parse import urlencode
- from urllib.request import Request, urlopen
- from rdflib.plugin import plugins
- from rdflib.query import Result, ResultParser
- from rdflib.term import BNode
- from rdflib.util import FORMAT_MIMETYPE_MAP, RESPONSE_TABLE_FORMAT_MIMETYPE_MAP
- log = logging.getLogger(__name__)
- if TYPE_CHECKING:
- import typing_extensions as te
- class SPARQLConnectorException(Exception): # noqa: N818
- pass
- class SPARQLConnector:
- """
- this class deals with nitty gritty details of talking to a SPARQL server
- """
- def __init__(
- self,
- query_endpoint: Optional[str] = None,
- update_endpoint: Optional[str] = None,
- returnFormat: Optional[str] = "xml", # noqa: N803
- method: te.Literal["GET", "POST", "POST_FORM"] = "GET",
- auth: Optional[Tuple[str, str]] = None,
- **kwargs,
- ):
- """
- auth, if present, must be a tuple of (username, password) used for Basic Authentication
- Any additional keyword arguments will be passed to to the request, and can be used to setup timeouts etc.
- """
- self._method: str
- self.returnFormat = returnFormat
- self.query_endpoint = query_endpoint
- self.update_endpoint = update_endpoint
- self.kwargs = kwargs
- self.method = method
- if auth is not None:
- if type(auth) is not tuple:
- raise SPARQLConnectorException("auth must be a tuple")
- if len(auth) != 2:
- raise SPARQLConnectorException("auth must be a tuple (user, password)")
- base64string = base64.b64encode(bytes("%s:%s" % auth, "ascii"))
- self.kwargs.setdefault("headers", {})
- self.kwargs["headers"].update(
- {"Authorization": "Basic %s" % base64string.decode("utf-8")}
- )
- @property
- def method(self) -> str:
- return self._method
- @method.setter
- def method(self, method: str) -> None:
- if method not in ("GET", "POST", "POST_FORM"):
- raise SPARQLConnectorException(
- 'Method must be "GET", "POST", or "POST_FORM"'
- )
- self._method = method
- def query(
- self,
- query: str,
- default_graph: Optional[str] = None,
- named_graph: Optional[str] = None,
- ) -> Result:
- if not self.query_endpoint:
- raise SPARQLConnectorException("Query endpoint not set!")
- params = {}
- # this test ensures we don't have a useless (BNode) default graph URI, which calls to Graph().query() will add
- if default_graph is not None and type(default_graph) is not BNode:
- params["default-graph-uri"] = default_graph
- headers = {"Accept": self.response_mime_types()}
- args = copy.deepcopy(self.kwargs)
- # merge params/headers dicts
- args.setdefault("params", {})
- args.setdefault("headers", {})
- args["headers"].update(headers)
- if self.method == "GET":
- params["query"] = query
- args["params"].update(params)
- qsa = "?" + urlencode(args["params"])
- try:
- res = urlopen(
- Request(self.query_endpoint + qsa, headers=args["headers"])
- )
- except Exception as e: # noqa: F841
- raise ValueError(
- "You did something wrong formulating either the URI or your SPARQL query"
- )
- elif self.method == "POST":
- args["headers"].update({"Content-Type": "application/sparql-query"})
- args["params"].update(params)
- qsa = "?" + urlencode(args["params"])
- try:
- res = urlopen(
- Request(
- self.query_endpoint + qsa,
- data=query.encode(),
- headers=args["headers"],
- )
- )
- except HTTPError as e:
- # type error: Incompatible return value type (got "Tuple[int, str, None]", expected "Result")
- return e.code, str(e), None # type: ignore[return-value]
- elif self.method == "POST_FORM":
- params["query"] = query
- args["params"].update(params)
- try:
- res = urlopen(
- Request(
- self.query_endpoint,
- data=urlencode(args["params"]).encode(),
- headers=args["headers"],
- )
- )
- except HTTPError as e:
- # type error: Incompatible return value type (got "Tuple[int, str, None]", expected "Result")
- return e.code, str(e), None # type: ignore[return-value]
- else:
- raise SPARQLConnectorException("Unknown method %s" % self.method)
- return Result.parse(
- BytesIO(res.read()), content_type=res.headers["Content-Type"].split(";")[0]
- )
- def update(
- self,
- query: str,
- default_graph: Optional[str] = None,
- named_graph: Optional[str] = None,
- ) -> None:
- if not self.update_endpoint:
- raise SPARQLConnectorException("Query endpoint not set!")
- params = {}
- if default_graph is not None:
- params["using-graph-uri"] = default_graph
- if named_graph is not None:
- params["using-named-graph-uri"] = named_graph
- headers = {
- "Accept": self.response_mime_types(),
- "Content-Type": "application/sparql-update; charset=UTF-8",
- }
- args = copy.deepcopy(self.kwargs) # other QSAs
- args.setdefault("params", {})
- args["params"].update(params)
- args.setdefault("headers", {})
- args["headers"].update(headers)
- qsa = "?" + urlencode(args["params"])
- res = urlopen( # noqa: F841
- Request(
- self.update_endpoint + qsa, data=query.encode(), headers=args["headers"]
- )
- )
- def response_mime_types(self) -> str:
- """Construct a HTTP-Header Accept field to reflect the supported mime types.
- If the return_format parameter is set, the mime types are restricted to these accordingly.
- """
- sparql_format_mimetype_map = {
- k: FORMAT_MIMETYPE_MAP.get(k, [])
- + RESPONSE_TABLE_FORMAT_MIMETYPE_MAP.get(k, [])
- for k in list(FORMAT_MIMETYPE_MAP.keys())
- + list(RESPONSE_TABLE_FORMAT_MIMETYPE_MAP.keys())
- }
- supported_formats = set()
- for plugin in plugins(name=self.returnFormat, kind=ResultParser):
- if "/" not in plugin.name:
- supported_formats.update(
- sparql_format_mimetype_map.get(plugin.name, [])
- )
- else:
- supported_formats.add(plugin.name)
- return ", ".join(supported_formats)
- __all__ = ["SPARQLConnector", "SPARQLConnectorException"]
|