Repository: RDFLib/rdflib

Issue:
requests not included in setup.py
although requests is defined in  requirements.txt it is not in setup.py and hence not picked up by pip install 

Discussion:

Patch:
diff --git a/.travis.fuseki_install_optional.sh b/.travis.fuseki_install_optional.sh
index 49e91f2c..106f7807 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 &&
diff --git a/.travis.yml b/.travis.yml
index 9811fa4a..33536b1a 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:
@@ -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/CONTRIBUTORS b/CONTRIBUTORS
index 51987b61..d24f564c 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 38c0f03a..467d1efe 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.
 
diff --git a/README.md b/README.md
index 5de6e79c..6e3e6a8b 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 d05139f7..d31a5e54 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/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/sparql/evaluate.py b/rdflib/plugins/sparql/evaluate.py
index 9a12200b..496bc872 100644
--- a/rdflib/plugins/sparql/evaluate.py
+++ b/rdflib/plugins/sparql/evaluate.py
@@ -17,7 +17,9 @@ also return a dict of list of dicts
 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
             )
 
 
diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py
index abec85a8..cda79d7e 100644
--- a/rdflib/plugins/stores/sparqlconnector.py
+++ b/rdflib/plugins/stores/sparqlconnector.py
@@ -1,12 +1,13 @@
 import logging
-import threading
-import requests
-
-import os
+from urllib.request import urlopen, Request
+from urllib.parse import urlencode
+from urllib.error import HTTPError, URLError
+import base64
 
 from io import BytesIO
 
 from rdflib.query import Result
+from rdflib import BNode
 
 log = logging.getLogger(__name__)
 
@@ -26,7 +27,6 @@ _response_mime_types = {
 
 
 class SPARQLConnector(object):
-
     """
     this class deals with nitty gritty details of talking to a SPARQL server
     """
@@ -37,10 +37,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 +51,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,19 +69,18 @@ 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!")
 
         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]}
 
         args = dict(self.kwargs)
-        args.update(url=self.query_endpoint)
 
         # merge params/headers dicts
         args.setdefault("params", {})
@@ -94,47 +90,46 @@ class SPARQLConnector(object):
 
         if self.method == "GET":
             args["params"].update(params)
+            qsa = "?" + urlencode(args["params"])
+            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"})
-            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..a5bc3716 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:
@@ -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):
+    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
         """
-        sets the endpoint URL for this SPARQLStore
-        if create==True an exception is thrown.
-        """
-        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")
@@ -152,7 +154,7 @@ class SPARQLStore(SPARQLConnector, Store):
         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(
@@ -163,14 +165,12 @@ class SPARQLStore(SPARQLConnector, Store):
             ]
         )
 
-    def _preprocess_query(self, query):
-        return self._inject_prefixes(query)
-
-    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:
@@ -280,6 +280,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 +384,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 +490,7 @@ class SPARQLUpdateStore(SPARQLStore):
 
     def __init__(
         self,
-        queryEndpoint=None,
+        query_endpoint=None,
         update_endpoint=None,
         sparql11=True,
         context_aware=True,
@@ -481,7 +511,7 @@ class SPARQLUpdateStore(SPARQLStore):
 
         SPARQLStore.__init__(
             self,
-            queryEndpoint,
+            query_endpoint,
             sparql11,
             context_aware,
             update_endpoint=update_endpoint,
@@ -494,6 +524,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 +601,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
@@ -647,8 +692,8 @@ class SPARQLUpdateStore(SPARQLStore):
 
             - **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.
@@ -664,7 +709,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 +799,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 +806,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/requirements.dev.txt b/requirements.dev.txt
index ba2deca7..059a4e9e 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -1,3 +1,5 @@
 sphinx
 sphinxcontrib-apidoc
-black==20.8b1
+nose==1.3.7
+flake8
+doctest-ignore-unicode==0.1.2
diff --git a/requirements.txt b/requirements.txt
index 97125175..999f9e76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,3 @@
-flake8
 html5lib
 isodate
 pyparsing
-requests
-doctest-ignore-unicode
diff --git a/setup.py b/setup.py
index 7e4d4e21..8dd82c2b 100644
--- a/setup.py
+++ b/setup.py
@@ -11,12 +11,10 @@ kwargs["tests_require"] = [
     "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,15 +45,14 @@ setup(
     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"],
-    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",


Test patch:
diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py
index 38a8b481..63b475cc 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
@@ -51,7 +50,7 @@ class SPARQLStoreDBPediaTestCase(unittest.TestCase):
         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 = """\
@@ -65,6 +64,30 @@ class SPARQLStoreDBPediaTestCase(unittest.TestCase):
         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):
@@ -119,7 +142,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 +150,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 +160,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