From 6eb4ccd13cf9bc29f438848ecfe6f2d90e81a529 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 13:53:17 +1000 Subject: [PATCH 01/15] removing flake8, requests, doctest-ignore-unicode --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 97125175b..999f9e760 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ -flake8 html5lib isodate pyparsing -requests -doctest-ignore-unicode From a30fb38b6c81e4586ed059616c8e539ec9dce06b Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 14:19:53 +1000 Subject: [PATCH 02/15] replace requests with urllib --- rdflib/plugins/sparql/evaluate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rdflib/plugins/sparql/evaluate.py b/rdflib/plugins/sparql/evaluate.py index 9a12200b6..496bc8721 100644 --- a/rdflib/plugins/sparql/evaluate.py +++ b/rdflib/plugins/sparql/evaluate.py @@ -17,7 +17,9 @@ import collections import itertools import re -import requests +from urllib.request import urlopen, Request +from urllib.parse import urlencode +import json as j from pyparsing import ParseException from rdflib import Variable, Graph, BNode, URIRef, Literal @@ -313,13 +315,11 @@ def evalServiceQuery(ctx, part): } # GET is easier to cache so prefer that if the query is not to long if len(service_query) < 600: - response = requests.get(service_url, params=query_settings, headers=headers) + response = urlopen(Request(service_url + "?" + urlencode(query_settings), headers=headers)) else: - response = requests.post( - service_url, params=query_settings, headers=headers - ) - if response.status_code == 200: - json = response.json() + response = urlopen(Request(service_url, data=urlencode(query_settings).encode(), headers=headers)) + if response.status == 200: + json = j.loads(response.read()) variables = res["vars_"] = json["head"]["vars"] # or just return the bindings? res = json["results"]["bindings"] @@ -329,7 +329,7 @@ def evalServiceQuery(ctx, part): yield bound else: raise Exception( - "Service: %s responded with code: %s", service_url, response.status_code + "Service: %s responded with code: %s", service_url, response.status ) From 7cb6bb4c05e110e35e930b03e61b3cf75e7b2595 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:13:06 +1000 Subject: [PATCH 03/15] replace requests with urllib for SPARQLStore/SPARQLUpdateStore, update relevant e.g.s --- examples/sparqlstore_example.py | 40 +++++--- rdflib/plugins/stores/sparqlconnector.py | 65 ++++++------ rdflib/plugins/stores/sparqlstore.py | 122 ++++++++++++++++++----- 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 936f65402..1d2ed9589 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 @@ 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 @@ ).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 abec85a8d..4e667297c 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 @@ class SPARQLConnectorException(Exception): class SPARQLConnector(object): - """ this class deals with nitty gritty details of talking to a SPARQL server """ @@ -37,10 +39,13 @@ def __init__( 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 @@ def __init__( 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 @@ def method(self, method): 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 @@ def query(self, query, default_graph=None): 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 @@ def query(self, query, default_graph=None): 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 51c40844a..612c3074c 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 @@ def __init__( 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 @@ def triples(self, spo, context=None): ) 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 @@ def _is_contextual(self, graph): 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 @@ def __init__( SPARQLStore.__init__( self, - queryEndpoint, + query_endpoint, sparql11, context_aware, update_endpoint=update_endpoint, @@ -494,6 +526,21 @@ def __init__( 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 @@ def add(self, spo, context=None, quoted=False): """ 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 @@ def update(self, query, initNs={}, initBindings={}, queryGraph=None, DEBUG=False """ 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 @@ def add_graph(self, graph): 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 @@ def remove_graph(self, graph): 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 38a8b481a..3ca69186f 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 @@ def do_POST(self): ``` """ 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 @@ def do_POST(self): ): 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 @@ def do_POST(self): ): 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 From e3543532a04908db19d5a290a997520de046395e Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:13:43 +1000 Subject: [PATCH 04/15] add nose to run tests, doctest-... moved from requirements.txt --- requirements.dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.dev.txt b/requirements.dev.txt index ba2deca77..b139c016e 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,3 +1,5 @@ sphinx sphinxcontrib-apidoc black==20.8b1 +nose==1.3.7 +doctest-ignore-unicode==0.1.2 From 0e03ec89fd366f2c827719262554a65bc44662a6 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:18:16 +1000 Subject: [PATCH 05/15] removal of comments referring to requests --- rdflib/plugins/stores/sparqlstore.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index 612c3074c..ba3805027 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -73,9 +73,9 @@ class SPARQLStore(SPARQLConnector, Store): matching plugin registered. Built in is support for ``xml``, ``json``, ``csv``, ``tsv`` and ``application/rdf+xml``. - The underlying SPARQLConnector builds in the requests library. + The underlying SPARQLConnector uses the urllib library. Any extra kwargs passed to the SPARQLStore connector are passed to - requests when doing HTTP calls. I.e. you have full control of + urllib when doing HTTP calls. I.e. you have full control of cookies/auth/headers. Form example: @@ -694,8 +694,8 @@ def update(self, query, initNs={}, initBindings={}, queryGraph=None, DEBUG=False - **When:** If context-awareness is enabled and the graph is not the default graph of the store. - **Why:** To ensure consistency with the :class:`~rdflib.plugins.stores.memory.Memory` store. - The graph must except "local" SPARQL requests (requests with no GRAPH keyword) - like if it was the default graph. + The graph must accept "local" SPARQL requests (requests with no GRAPH keyword) + as if it was the default graph. - **What is done:** These "local" queries are rewritten by this store. The content of each block of a SPARQL Update operation is wrapped in a GRAPH block except if the block is empty. From 9461e55d94cf4557a288c01ba7506cec74951193 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:21:08 +1000 Subject: [PATCH 06/15] removed black as it's not a requirement (nor flake8) --- requirements.dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index b139c016e..d9497938a 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,5 +1,4 @@ sphinx sphinxcontrib-apidoc -black==20.8b1 nose==1.3.7 doctest-ignore-unicode==0.1.2 From 80713a6edfc8656ee70c1e0352cce28742fac776 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:29:41 +1000 Subject: [PATCH 07/15] re-adding flake8 for Travis --- requirements.dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.dev.txt b/requirements.dev.txt index d9497938a..e9575faec 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -2,3 +2,4 @@ sphinx sphinxcontrib-apidoc nose==1.3.7 doctest-ignore-unicode==0.1.2 +flake8 From b7025f57295e4313a0ba48f4cae1fec19c329863 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:34:00 +1000 Subject: [PATCH 08/15] apparently flake8 is needed in requirements.txt --- requirements.dev.txt | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index e9575faec..d9497938a 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -2,4 +2,3 @@ sphinx sphinxcontrib-apidoc nose==1.3.7 doctest-ignore-unicode==0.1.2 -flake8 diff --git a/requirements.txt b/requirements.txt index 999f9e760..78eec213e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ html5lib isodate pyparsing +flake8 From 3c63d052b7fcbfca5b417e6189f8cad3ae71d307 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:48:43 +1000 Subject: [PATCH 09/15] more requirements shuffling for Travis --- .travis.yml | 1 + requirements.dev.txt | 1 + requirements.txt | 1 - setup.py | 4 +--- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9811fa4a8..848aebc20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ before_install: install: - pip install --default-timeout 60 -r requirements.txt + - pip install --default-timeout 60 -r requirements.dev.txt - pip install --default-timeout 60 coverage coveralls nose-timer && export HAS_COVERALLS=1 - python setup.py install diff --git a/requirements.dev.txt b/requirements.dev.txt index d9497938a..059a4e9eb 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,4 +1,5 @@ sphinx sphinxcontrib-apidoc nose==1.3.7 +flake8 doctest-ignore-unicode==0.1.2 diff --git a/requirements.txt b/requirements.txt index 78eec213e..999f9e760 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ html5lib isodate pyparsing -flake8 diff --git a/setup.py b/setup.py index 7e4d4e217..969f5fe06 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,10 @@ "networkx", "nose", "doctest-ignore-unicode", - "requests", ] kwargs["test_suite"] = "nose.collector" kwargs["extras_require"] = { "html": ["html5lib"], - "sparql": ["requests"], "tests": kwargs["tests_require"], "docs": ["sphinx < 4", "sphinxcontrib-apidoc"], } @@ -47,7 +45,7 @@ def find_version(filename): author="Daniel 'eikeon' Krech", author_email="eikeon@eikeon.com", maintainer="RDFLib Team", - maintainer_email="rdflib-dev@google.com", + maintainer_email="rdflib-dev@googlegroups.com", url="https://github.com/RDFLib/rdflib", license="BSD-3-Clause", platforms=["any"], From b8a2647a78835cbbe0e5b156fc026467f0bef21b Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:49:09 +1000 Subject: [PATCH 10/15] update Fuseki version --- .travis.fuseki_install_optional.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.fuseki_install_optional.sh b/.travis.fuseki_install_optional.sh index 49e91f2c1..106f78076 100644 --- a/.travis.fuseki_install_optional.sh +++ b/.travis.fuseki_install_optional.sh @@ -2,7 +2,7 @@ set -v -uri="http://archive.apache.org/dist/jena/binaries/apache-jena-fuseki-2.4.0.tar.gz" +uri="http://archive.apache.org/dist/jena/binaries/apache-jena-fuseki-3.9.0.tar.gz" if wget "$uri" && tar -zxf *jena*fuseki*.tar.gz && From 67b805873b2cdf85bc4ab3859200a2abc2289aff Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 21:49:32 +1000 Subject: [PATCH 11/15] update CONTRIBUTORS with maintainers, update LICENSE to 2020 --- CONTRIBUTORS | 3 +++ LICENSE | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 51987b61e..d24f564cc 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,6 +1,7 @@ Aaron Swartz Andrew Eland Andrew Kuchling +Ashley Sommer Arve Knudsen Chimezie Ogbuji Daniel Krech @@ -18,7 +19,9 @@ Kendall Clark Leandro López Lucio Torre Michel Pelletier +Natanael Arndt Nacho Barrientos Arias +Nicholas J. Car Niklas Lindström Phil Dawes Phillip Pearson diff --git a/LICENSE b/LICENSE index 38c0f03a0..467d1efe1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ LICENSE AGREEMENT FOR RDFLIB ------------------------------------------------ -Copyright (c) 2002-2017, RDFLib Team +Copyright (c) 2002-2020, RDFLib Team See CONTRIBUTORS and http://github.com/RDFLib/rdflib All rights reserved. From f473263be52db516816eeb654d8b5df2fca3486e Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Sat, 3 Oct 2020 22:09:38 +1000 Subject: [PATCH 12/15] updated to support Python 3.6+ only (not 3.5) --- .travis.yml | 2 +- README.md | 2 +- docs/developers.rst | 2 +- setup.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 848aebc20..33536b1a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ git: depth: 3 python: - - 3.5 - 3.6 - 3.7 + - 3.8 jobs: include: diff --git a/README.md b/README.md index 5de6e79ca..6e3e6a8b9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Please see the list for all packages/repositories here: ## Versions * `5.x.y` supports Python 2.7 and 3.4+ and is [mostly backwards compatible with 4.2.2](https://rdflib.readthedocs.io/en/stable/upgrade4to5.html). Only bug fixes will be applied. - * `6.x.y` is the next major release which will support Python 3.5+. (Current master branch) + * `6.x.y` is the next major release which will support Python 3.6+. (Current master branch) ## Installation diff --git a/docs/developers.rst b/docs/developers.rst index d05139f78..d31a5e548 100644 --- a/docs/developers.rst +++ b/docs/developers.rst @@ -59,7 +59,7 @@ Compatibility RDFLib 5.x.y tries to be compatible with python versions 2.7, 3.4, 3.5, 3.6, 3.7. -The current master branch (which will be released as 6.0.0) will only support Python 3.5 and newer. +The current master branch (which will be released as 6.0.0) will only support Python 3.6 and newer. Releasing diff --git a/setup.py b/setup.py index 969f5fe06..8dd82c2b8 100644 --- a/setup.py +++ b/setup.py @@ -49,11 +49,10 @@ def find_version(filename): url="https://github.com/RDFLib/rdflib", license="BSD-3-Clause", platforms=["any"], - python_requires=">=3.5", + python_requires=">=3.6", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 4665306a0c16512b638f98e59fc71dc2d331f06a Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Tue, 6 Oct 2020 22:13:22 +1000 Subject: [PATCH 13/15] de-duplicate SPARQLStore injected PREFIXes --- rdflib/plugins/stores/sparqlstore.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index ba3805027..16c1f9d74 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -154,7 +154,7 @@ def _query(self, *args, **kwargs): return super(SPARQLStore, self).query(*args, **kwargs) def _inject_prefixes(self, query, extra_bindings): - bindings = list(self.nsBindings.items()) + list(extra_bindings.items()) + bindings = set(list(self.nsBindings.items()) + list(extra_bindings.items())) if not bindings: return query return "\n".join( @@ -165,9 +165,6 @@ def _inject_prefixes(self, query, extra_bindings): ] ) - def _preprocess_query(self, query): - return self._inject_prefixes(query) - def query(self, query, initNs={}, initBindings={}, queryGraph=None, DEBUG=False): self.debug = DEBUG assert isinstance(query, str) From 1ddb2a3b3bb3a1f9c830b1f1e1b79ed2cf57e8cc Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Tue, 6 Oct 2020 23:40:31 +1000 Subject: [PATCH 14/15] fixed SPARQLStore Graph().query() GET, with tests --- rdflib/plugins/stores/sparqlconnector.py | 14 +++++++------- rdflib/plugins/stores/sparqlstore.py | 5 +++-- test/test_sparqlstore.py | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index 4e667297c..cda79d7e3 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -1,15 +1,13 @@ import logging -import threading from urllib.request import urlopen, Request from urllib.parse import urlencode from urllib.error import HTTPError, URLError import base64 -import os - from io import BytesIO from rdflib.query import Result +from rdflib import BNode log = logging.getLogger(__name__) @@ -72,12 +70,12 @@ def method(self, method): self._method = method def query(self, query, default_graph: str = None, named_graph: str = None): - if not self.query_endpoint: raise SPARQLConnectorException("Query endpoint not set!") params = {"query": query} - if default_graph: + # 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) != BNode: params["default-graph-uri"] = default_graph headers = {"Accept": _response_mime_types[self.returnFormat]} @@ -93,7 +91,10 @@ def query(self, query, default_graph: str = None, named_graph: str = None): if self.method == "GET": args["params"].update(params) qsa = "?" + urlencode(args["params"]) - res = urlopen(Request(self.query_endpoint + qsa, headers=args["headers"])) + try: + res = urlopen(Request(self.query_endpoint + qsa, headers=args["headers"])) + except Exception as e: + 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"}) try: @@ -102,7 +103,6 @@ def query(self, query, default_graph: str = None, named_graph: str = None): return e.code, str(e), None else: raise SPARQLConnectorException("Unknown method %s" % self.method) - return Result.parse( BytesIO(res.read()), content_type=res.headers["Content-Type"].split(";")[0] ) diff --git a/rdflib/plugins/stores/sparqlstore.py b/rdflib/plugins/stores/sparqlstore.py index 16c1f9d74..a5bc3716e 100644 --- a/rdflib/plugins/stores/sparqlstore.py +++ b/rdflib/plugins/stores/sparqlstore.py @@ -165,11 +165,12 @@ def _inject_prefixes(self, query, extra_bindings): ] ) - def query(self, query, initNs={}, initBindings={}, queryGraph=None, DEBUG=False): + def query(self, query, initNs=None, initBindings=None, queryGraph=None, DEBUG=False): self.debug = DEBUG assert isinstance(query, str) - query = self._inject_prefixes(query, initNs) + if initNs is not None: + query = self._inject_prefixes(query, initNs) if initBindings: if not self.sparql11: diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py index 3ca69186f..a3ee4ea8e 100644 --- a/test/test_sparqlstore.py +++ b/test/test_sparqlstore.py @@ -64,6 +64,30 @@ def test_query_with_added_prolog(self): for i in res: assert type(i[0]) == Literal, i[0].n3() + def test_counting_graph_and_store_queries(self): + q = """ + SELECT ?s + WHERE { + ?s ?p ?o . + } + LIMIT 5 + """ + g = Graph("SPARQLStore") + g.open(self.path) + c = 0 + for r in g.query(q): + c += 1 + + assert c == 5, "Graph(\"SPARQLStore\") didn't return 5 records" + + from rdflib.plugins.stores.sparqlstore import SPARQLStore + st = SPARQLStore(query_endpoint=self.path) + c = 0 + for r in st.query(q): + c += 1 + + assert c == 5, "SPARQLStore() didn't return 5 records" + class SPARQLStoreUpdateTestCase(unittest.TestCase): def setUp(self): From c923eb554ae7c33a9545f6499ff5ab994c49ccd2 Mon Sep 17 00:00:00 2001 From: Nicholas Car Date: Wed, 7 Oct 2020 00:21:50 +1000 Subject: [PATCH 15/15] update SPARQLStore test due to Error type change --- test/test_sparqlstore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py index a3ee4ea8e..63b475cc6 100644 --- a/test/test_sparqlstore.py +++ b/test/test_sparqlstore.py @@ -50,7 +50,7 @@ def test_noinitNs(self): SELECT ?label WHERE { ?s a xyzzy:Concept ; xyzzy:prefLabel ?label . } LIMIT 10 """ - self.assertRaises(HTTPError, self.graph.query, query) + self.assertRaises(ValueError, self.graph.query, query) def test_query_with_added_prolog(self): prologue = """\