diff options
| author | Nicholas Car <nicholas.car@surroundaustralia.com> | 2020-10-03 21:13:06 +1000 |
|---|---|---|
| committer | Nicholas Car <nicholas.car@surroundaustralia.com> | 2020-10-03 21:13:06 +1000 |
| commit | 7cb6bb4c05e110e35e930b03e61b3cf75e7b2595 (patch) | |
| tree | 037e78cf5a1eb6c33e60a3283911e24208ceb49b | |
| parent | a30fb38b6c81e4586ed059616c8e539ec9dce06b (diff) | |
| download | rdflib-7cb6bb4c05e110e35e930b03e61b3cf75e7b2595.tar.gz | |
replace requests with urllib for SPARQLStore/SPARQLUpdateStore, update relevant e.g.s
| -rw-r--r-- | examples/sparqlstore_example.py | 40 | ||||
| -rw-r--r-- | rdflib/plugins/stores/sparqlconnector.py | 65 | ||||
| -rw-r--r-- | rdflib/plugins/stores/sparqlstore.py | 122 | ||||
| -rw-r--r-- | test/test_sparqlstore.py | 19 |
4 files changed, 163 insertions, 83 deletions
diff --git a/examples/sparqlstore_example.py b/examples/sparqlstore_example.py index 936f6540..1d2ed958 100644 --- a/examples/sparqlstore_example.py +++ b/examples/sparqlstore_example.py @@ -1,8 +1,7 @@ """ -A simple example showing how to use the SPARQLStore +Simple examples showing how to use the SPARQLStore """ -import locale from rdflib import Graph, URIRef, Namespace from rdflib.plugins.stores.sparqlstore import SPARQLStore @@ -10,7 +9,7 @@ if __name__ == "__main__": dbo = Namespace("http://dbpedia.org/ontology/") - # using a Graph with the Store type string set to "SPARQLStore" + # EXAMPLE 1: using a Graph with the Store type string set to "SPARQLStore" graph = Graph("SPARQLStore", identifier="http://dbpedia.org") graph.open("http://dbpedia.org/sparql") @@ -22,13 +21,28 @@ if __name__ == "__main__": ).replace(",", ".") ) - # using a SPARQLStore object directly - s = SPARQLStore(endpoint="http://dbpedia.org/sparql") - s.open(None) - pop = graph.value( - URIRef("http://dbpedia.org/resource/Brisbane"), dbo.populationTotal - ) - print( - "According to DBPedia, Brisbane has a population of " - "{0:,}".format(int(pop), ",d") - ) + # EXAMPLE 2: using a SPARQLStore object directly + st = SPARQLStore(query_endpoint="http://dbpedia.org/sparql") + + for p in st.objects(URIRef("http://dbpedia.org/resource/Brisbane"), dbo.populationTotal): + print( + "According to DBPedia, Brisbane has a population of " + "{0:,}".format(int(pop), ",d") + ) + + # EXAMPLE 3: doing RDFlib triple navigation using SPARQLStore as a Graph() + graph = Graph("SPARQLStore", identifier="http://dbpedia.org") + graph.open("http://dbpedia.org/sparql") + # we are asking DBPedia for 3 skos:Concept instances + count = 0 + from rdflib.namespace import RDF, SKOS + for s in graph.subjects(predicate=RDF.type, object=SKOS.Concept): + count += 1 + print(s) + if count >= 3: + break + + # EXAMPLE 4: using a SPARQL endpoint that requires Basic HTTP authentication + # NOTE: this example won't run since the endpoint isn't live (or real) + s = SPARQLStore(query_endpoint="http://fake-sparql-endpoint.com/repository/x", auth=("my_username", "my_password")) + # do normal Graph things diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index abec85a8..4e667297 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -1,6 +1,9 @@ import logging import threading -import requests +from urllib.request import urlopen, Request +from urllib.parse import urlencode +from urllib.error import HTTPError, URLError +import base64 import os @@ -26,7 +29,6 @@ _response_mime_types = { class SPARQLConnector(object): - """ this class deals with nitty gritty details of talking to a SPARQL server """ @@ -37,10 +39,13 @@ class SPARQLConnector(object): update_endpoint=None, returnFormat="xml", method="GET", + auth=None, **kwargs ): """ - Any additional keyword arguments will be passed to requests, and can be used to setup timesouts, basic auth, etc. + 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 timesouts etc. """ self.returnFormat = returnFormat @@ -48,18 +53,12 @@ class SPARQLConnector(object): self.update_endpoint = update_endpoint self.kwargs = kwargs self.method = method - - # it is recommended to have one session object per thread/process. This assures that is the case. - # https://github.com/kennethreitz/requests/issues/1871 - - self._session = threading.local() - - @property - def session(self): - k = "session_%d" % os.getpid() - self._session.__dict__.setdefault(k, requests.Session()) - log.debug("Session %s %s", os.getpid(), id(self._session.__dict__[k])) - return self._session.__dict__[k] + if auth is not None: + assert type(auth) == tuple, "auth must be a tuple" + assert len(auth) == 2, "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): @@ -72,7 +71,7 @@ class SPARQLConnector(object): self._method = method - def query(self, query, default_graph=None): + def query(self, query, default_graph: str = None, named_graph: str = None): if not self.query_endpoint: raise SPARQLConnectorException("Query endpoint not set!") @@ -84,7 +83,6 @@ class SPARQLConnector(object): headers = {"Accept": _response_mime_types[self.returnFormat]} args = dict(self.kwargs) - args.update(url=self.query_endpoint) # merge params/headers dicts args.setdefault("params", {}) @@ -94,47 +92,44 @@ class SPARQLConnector(object): if self.method == "GET": args["params"].update(params) + qsa = "?" + urlencode(args["params"]) + res = urlopen(Request(self.query_endpoint + qsa, headers=args["headers"])) elif self.method == "POST": args["headers"].update({"Content-Type": "application/sparql-query"}) - args["data"] = params + try: + res = urlopen(Request(self.query_endpoint, data=query.encode(), headers=args["headers"])) + except HTTPError as e: + return e.code, str(e), None else: raise SPARQLConnectorException("Unknown method %s" % self.method) - res = self.session.request(self.method, **args) - - res.raise_for_status() - return Result.parse( - BytesIO(res.content), content_type=res.headers["Content-type"] + BytesIO(res.read()), content_type=res.headers["Content-Type"].split(";")[0] ) - def update(self, update, default_graph=None): + def update(self, query, default_graph: str = None, named_graph: str = None): if not self.update_endpoint: raise SPARQLConnectorException("Query endpoint not set!") params = {} - if default_graph: + if default_graph is not None: params["using-graph-uri"] = default_graph + if named_graph is not None: + params["using-named-graph-uri"] = default_graph + headers = { "Accept": _response_mime_types[self.returnFormat], "Content-Type": "application/sparql-update", } - args = dict(self.kwargs) - - args.update(url=self.update_endpoint, data=update.encode("utf-8")) + args = dict(self.kwargs) # other QSAs - # merge params/headers dicts args.setdefault("params", {}) args["params"].update(params) args.setdefault("headers", {}) args["headers"].update(headers) - res = self.session.post(**args) - - res.raise_for_status() - - def close(self): - self.session.close() + qsa = "?" + urlencode(args["params"]) + res = urlopen(Request(self.update_endpoint + qsa, data=query.encode(), headers=args["headers"])) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index 51c40844..612c3074 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -93,17 +93,18 @@ class SPARQLStore(SPARQLConnector, Store): def __init__( self, - endpoint=None, + query_endpoint=None, sparql11=True, context_aware=True, node_to_sparql=_node_to_sparql, returnFormat="xml", + auth=None, **sparqlconnector_kwargs ): """ """ super(SPARQLStore, self).__init__( - endpoint, returnFormat=returnFormat, **sparqlconnector_kwargs + query_endpoint=query_endpoint, returnFormat=returnFormat, auth=auth, **sparqlconnector_kwargs ) self.node_to_sparql = node_to_sparql @@ -113,19 +114,20 @@ class SPARQLStore(SPARQLConnector, Store): self.graph_aware = context_aware self._queries = 0 - # Database Management Methods - def create(self, configuration): - raise TypeError("The SPARQL store is read only") - - def open(self, configuration, create=False): - """ - sets the endpoint URL for this SPARQLStore - if create==True an exception is thrown. + def open(self, configuration: str, create=False): + """This method is included so that calls to this Store via Graph, e.g. Graph("SPARQLStore"), + can set the required parameters """ - if create: - raise Exception("Cannot create a SPARQL Endpoint") + if type(configuration) == str: + self.query_endpoint = configuration + else: + raise Exception( + "configuration must be a string (a single query endpoint URI)" + ) - self.query_endpoint = configuration + # Database Management Methods + def create(self, configuration): + raise TypeError("The SPARQL Store is read only. Try SPARQLUpdateStore for read/write.") def destroy(self, configuration): raise TypeError("The SPARQL store is read only") @@ -280,6 +282,9 @@ class SPARQLStore(SPARQLConnector, Store): ) if vars: + if type(result) == tuple: + if result[0] == 401: + raise ValueError("It looks like you need to authenticate with this SPARQL Store. HTTP unauthorized") for row in result: yield ( row.get(s, s), @@ -381,8 +386,35 @@ class SPARQLStore(SPARQLConnector, Store): else: return graph.identifier != DATASET_DEFAULT_GRAPH_ID - def close(self, commit_pending_transaction=None): - SPARQLConnector.close(self) + def subjects(self, predicate=None, object=None): + """A generator of subjects with the given predicate and object""" + for t, c in self.triples((None, predicate, object)): + yield t[0] + + def predicates(self, subject=None, object=None): + """A generator of predicates with the given subject and object""" + for t, c in self.triples((subject, None, object)): + yield t[1] + + def objects(self, subject=None, predicate=None): + """A generator of objects with the given subject and predicate""" + for t, c in self.triples((subject, predicate, None)): + yield t[2] + + def subject_predicates(self, object=None): + """A generator of (subject, predicate) tuples for the given object""" + for t, c in self.triples((None, None, object)): + yield t[0], t[1] + + def subject_objects(self, predicate=None): + """A generator of (subject, object) tuples for the given predicate""" + for t, c in self.triples((None, predicate, None)): + yield t[0], t[2] + + def predicate_objects(self, subject=None): + """A generator of (predicate, object) tuples for the given subject""" + for t, c in self.triples((subject, None, None)): + yield t[1], t[2] class SPARQLUpdateStore(SPARQLStore): @@ -460,7 +492,7 @@ class SPARQLUpdateStore(SPARQLStore): def __init__( self, - queryEndpoint=None, + query_endpoint=None, update_endpoint=None, sparql11=True, context_aware=True, @@ -481,7 +513,7 @@ class SPARQLUpdateStore(SPARQLStore): SPARQLStore.__init__( self, - queryEndpoint, + query_endpoint, sparql11, context_aware, update_endpoint=update_endpoint, @@ -494,6 +526,21 @@ class SPARQLUpdateStore(SPARQLStore): self._edits = None self._updates = 0 + def open(self, configuration: str or tuple, create=False): + """This method is included so that calls to this Store via Graph, e.g. Graph("SPARQLStore"), + can set the required parameters + """ + if type(configuration) == str: + self.query_endpoint = configuration + elif type(configuration) == tuple: + self.query_endpoint = configuration[0] + self.update_endpoint = configuration[1] + else: + raise Exception( + "configuration must be either a string (a single query endpoint URI) " + "or a tuple (a query/update endpoint URI pair)" + ) + def query(self, *args, **kwargs): if not self.autocommit and not self.dirty_reads: self.commit() @@ -556,7 +603,7 @@ class SPARQLUpdateStore(SPARQLStore): """ Add a triple to the store of triples. """ if not self.update_endpoint: - raise Exception("UpdateEndpoint is not set - call 'open'") + raise Exception("UpdateEndpoint is not set") assert not quoted (subject, predicate, obj) = spo @@ -664,7 +711,7 @@ class SPARQLUpdateStore(SPARQLStore): """ if not self.update_endpoint: - raise Exception("UpdateEndpoint is not set - call 'open'") + raise Exception("Update endpoint is not set!") self.debug = DEBUG assert isinstance(query, str) @@ -754,13 +801,6 @@ class SPARQLUpdateStore(SPARQLStore): elif graph.identifier != DATASET_DEFAULT_GRAPH_ID: self.update("CREATE GRAPH %s" % self.node_to_sparql(graph.identifier)) - def close(self, commit_pending_transaction=False): - - if commit_pending_transaction: - self.commit() - - super(SPARQLStore, self).close() - def remove_graph(self, graph): if not self.graph_aware: Store.remove_graph(self, graph) @@ -768,3 +808,33 @@ class SPARQLUpdateStore(SPARQLStore): self.update("DROP DEFAULT") else: self.update("DROP GRAPH %s" % self.node_to_sparql(graph.identifier)) + + def subjects(self, predicate=None, object=None): + """A generator of subjects with the given predicate and object""" + for t, c in self.triples((None, predicate, object)): + yield t[0] + + def predicates(self, subject=None, object=None): + """A generator of predicates with the given subject and object""" + for t, c in self.triples((subject, None, object)): + yield t[1] + + def objects(self, subject=None, predicate=None): + """A generator of objects with the given subject and predicate""" + for t, c in self.triples((subject, predicate, None)): + yield t[2] + + def subject_predicates(self, object=None): + """A generator of (subject, predicate) tuples for the given object""" + for t, c in self.triples((None, None, object)): + yield t[0], t[1] + + def subject_objects(self, predicate=None): + """A generator of (subject, object) tuples for the given predicate""" + for t, c in self.triples((None, predicate, None)): + yield t[0], t[2] + + def predicate_objects(self, subject=None): + """A generator of (predicate, object) tuples for the given subject""" + for t, c in self.triples((subject, None, None)): + yield t[1], t[2] diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py index 38a8b481..3ca69186 100644 --- a/test/test_sparqlstore.py +++ b/test/test_sparqlstore.py @@ -1,12 +1,11 @@ from rdflib import Graph, URIRef, Literal from urllib.request import urlopen +from urllib.error import HTTPError import unittest from nose import SkipTest -from requests import HTTPError from http.server import BaseHTTPRequestHandler, HTTPServer import socket from threading import Thread -import requests try: assert len(urlopen("http://dbpedia.org/sparql").read()) > 0 @@ -119,7 +118,7 @@ class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler): ``` """ contenttype = self.headers.get("Content-Type") - if self.path == "/query": + if self.path == "/query" or self.path == "/query?": if self.headers.get("Content-Type") == "application/sparql-query": pass elif ( @@ -127,9 +126,9 @@ class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler): ): pass else: - self.send_response(requests.codes.not_acceptable) + self.send_response(406, "Not Acceptable") self.end_headers() - elif self.path == "/update": + elif self.path == "/update" or self.path == "/update?": if self.headers.get("Content-Type") == "application/sparql-update": pass elif ( @@ -137,18 +136,20 @@ class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler): ): pass else: - self.send_response(requests.codes.not_acceptable) + self.send_response(406, "Not Acceptable") self.end_headers() else: - self.send_response(requests.codes.not_found) + print("self.path") + print(self.path) + self.send_response(404, "Not Found") self.end_headers() - self.send_response(requests.codes.ok) + self.send_response(200, "OK") self.end_headers() return def do_GET(self): # Process an HTTP GET request and return a response with an HTTP 200 status. - self.send_response(requests.codes.ok) + self.send_response(200, "OK") self.end_headers() return |
