Repository: RDFLib/rdflib

Issue:
Missing Content-Type header for SPARQLConnector update method (?)
In rdflib 4.2.2 the SPARQLWrapper added an applicable content-type header when sending queries and updates to a SPARQL endpoint.
The 5.0.0 the SPARQLConnector does not add a content-type and, since the GraphDB we use as SPARQL endpoint rejects update requests when Content-Type: application/sparql-update is not added to the header, I had to monkey patch the update method to forcebily add the content type to the header.

I also tried to add a headers parameter with the content-type to the SPARQLUpdateStore parameter when constructing a SPARQLUpdateStore instance, but this then the content type is also used for query request, which results in rejected query requests in case the store instance is used for querying.

Should the the content type be added by the SPARQLConnector like the SPARQLWrapper did or am i missing an other intended solution?

Best regards,
Arnoud


Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index f37f0750..2d2ada1d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,4 @@
 # http://travis-ci.org/#!/RDFLib/rdflib
-sudo: false
 language: python
 branches:
   only:
@@ -10,16 +9,14 @@ git:
   depth: 3
 
 python:
-    - 2.7
-    - 3.4
     - 3.5
     - 3.6
+    - 3.7
 
-matrix:
+jobs:
   include:
-    - python: 3.7
+    - python: 3.8
       dist: xenial
-      sudo: true
 
 before_install:
     - pip install -U setuptools pip  # seems travis comes with a too old setuptools for html5lib
diff --git a/README.md b/README.md
index 3160930d..e4c8a2eb 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ RDFLib
 [![PyPI](https://img.shields.io/pypi/v/rdflib.svg)](https://pypi.python.org/pypi/rdflib)
 [![PyPI](https://img.shields.io/pypi/pyversions/rdflib.svg)](https://pypi.python.org/pypi/rdflib)
 
-RDFLib is a pure Python package work working with [RDF](http://www.w3.org/RDF/). RDFLib contains most things you need to work with RDF, including:
+RDFLib is a pure Python package for working with [RDF](http://www.w3.org/RDF/). RDFLib contains most things you need to work with RDF, including:
 
 * parsers and serializers for RDF/XML, N3, NTriples, N-Quads, Turtle, TriX, Trig and JSON-LD (via a plugin).
 * a Graph interface which can be backed by any one of a number of Store implementations
diff --git a/docs/sphinx-requirements.txt b/docs/sphinx-requirements.txt
index 55809050..45583540 100644
--- a/docs/sphinx-requirements.txt
+++ b/docs/sphinx-requirements.txt
@@ -1,3 +1,3 @@
-sphinx==3.0.2
+sphinx==3.0.3
 sphinxcontrib-apidoc
 git+https://github.com/gniezen/n3pygments.git
diff --git a/rdflib/extras/external_graph_libs.py b/rdflib/extras/external_graph_libs.py
index 8617b370..25c745c0 100644
--- a/rdflib/extras/external_graph_libs.py
+++ b/rdflib/extras/external_graph_libs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2.7
+#!/usr/bin/env
 # encoding: utf-8
 from __future__ import absolute_import
 from __future__ import division
diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py
index ee981419..abb69a55 100644
--- a/rdflib/plugins/stores/sparqlconnector.py
+++ b/rdflib/plugins/stores/sparqlconnector.py
@@ -87,6 +87,7 @@ class SPARQLConnector(object):
         if self.method == 'GET':
             args['params'].update(params)
         elif self.method == 'POST':
+            args['headers'].update({'Content-Type': 'application/sparql-query'})
             args['data'] = params
         else:
             raise SPARQLConnectorException("Unknown method %s" % self.method)
@@ -106,7 +107,10 @@ class SPARQLConnector(object):
         if default_graph:
             params["using-graph-uri"] = default_graph
 
-        headers = {'Accept': _response_mime_types[self.returnFormat]}
+        headers = {
+            'Accept': _response_mime_types[self.returnFormat],
+            'Content-Type': 'application/sparql-update',
+        }
 
         args = dict(self.kwargs)
 
diff --git a/setup.py b/setup.py
index c115f068..8bba18d2 100644
--- a/setup.py
+++ b/setup.py
@@ -43,15 +43,14 @@ setup(
     url="https://github.com/RDFLib/rdflib",
     license="BSD-3-Clause",
     platforms=["any"],
+    python_requires='>=3.5',
     classifiers=[
             "Programming Language :: Python",
-            "Programming Language :: Python :: 2",
             "Programming Language :: Python :: 3",
-            "Programming Language :: Python :: 2.7",
-            "Programming Language :: Python :: 3.4",
             "Programming Language :: Python :: 3.5",
             "Programming Language :: Python :: 3.6",
             "Programming Language :: Python :: 3.7",
+            "Programming Language :: Python :: 3.8",
             "License :: OSI Approved :: BSD License",
             "Topic :: Software Development :: Libraries :: Python Modules",
             "Operating System :: OS Independent",
diff --git a/tox.ini b/tox.ini
index ee712287..b5b588fb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py27,py34,py35,py36
+    py35,py36,py37,py38
 
 [testenv]
 setenv =
@@ -20,7 +20,7 @@ deps =
 
 [testenv:cover]
 basepython =
-    python2.7
+    python3.7
 commands =
     {envpython} run_tests.py --where=./ \
                  --with-coverage --cover-html --cover-html-dir=./coverage \


Test patch:
diff --git a/test/test_sparqlstore.py b/test/test_sparqlstore.py
index 26a69460..a0a93b57 100644
--- a/test/test_sparqlstore.py
+++ b/test/test_sparqlstore.py
@@ -4,7 +4,10 @@ import os
 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
@@ -67,5 +70,78 @@ class SPARQLStoreDBPediaTestCase(unittest.TestCase):
             assert type(i[0]) == Literal, i[0].n3()
 
 
+class SPARQLStoreUpdateTestCase(unittest.TestCase):
+    def setUp(self):
+        port = self.setup_mocked_endpoint()
+        self.graph = Graph(store="SPARQLUpdateStore", identifier=URIRef("urn:ex"))
+        self.graph.open(("http://localhost:{port}/query".format(port=port),
+            "http://localhost:{port}/update".format(port=port)), create=False)
+        ns = list(self.graph.namespaces())
+        assert len(ns) > 0, ns
+
+    def setup_mocked_endpoint(self):
+        # Configure mock server.
+        s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
+        s.bind(('localhost', 0))
+        address, port = s.getsockname()
+        s.close()
+        mock_server = HTTPServer(('localhost', port), SPARQL11ProtocolStoreMock)
+
+        # Start running mock server in a separate thread.
+        # Daemon threads automatically shut down when the main process exits.
+        mock_server_thread = Thread(target=mock_server.serve_forever)
+        mock_server_thread.setDaemon(True)
+        mock_server_thread.start()
+        print("Started mocked sparql endpoint on http://localhost:{port}/".format(port=port))
+        return port
+
+    def tearDown(self):
+        self.graph.close()
+
+    def test_Query(self):
+        query = "insert data {<urn:s> <urn:p> <urn:o>}"
+        res = self.graph.update(query)
+        print(res)
+
+
+class SPARQL11ProtocolStoreMock(BaseHTTPRequestHandler):
+    def do_POST(self):
+        """
+        If the body should be analysed as well, just use:
+        ```
+        body = self.rfile.read(int(self.headers['Content-Length'])).decode()
+        print(body)
+        ```
+        """
+        contenttype = self.headers.get("Content-Type")
+        if self.path == "/query":
+            if self.headers.get("Content-Type") == "application/sparql-query":
+                pass
+            elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded":
+                pass
+            else:
+                self.send_response(requests.codes.not_acceptable)
+                self.end_headers()
+        elif self.path == "/update":
+            if self.headers.get("Content-Type") == "application/sparql-update":
+                pass
+            elif self.headers.get("Content-Type") == "application/x-www-form-urlencoded":
+                pass
+            else:
+                self.send_response(requests.codes.not_acceptable)
+                self.end_headers()
+        else:
+            self.send_response(requests.codes.not_found)
+            self.end_headers()
+        self.send_response(requests.codes.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.end_headers()
+        return
+
 if __name__ == '__main__':
     unittest.main()