Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions rdflib/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ def plugins(
"rdflib.plugins.parsers.notation3",
"TurtleParser",
)
register(
"application/x-turtle",
Parser,
"rdflib.plugins.parsers.notation3",
"TurtleParser",
)
register(
"turtle",
Parser,
Expand Down
31 changes: 31 additions & 0 deletions rdflib/plugins/stores/sparqlconnector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from urllib.request import Request, urlopen

from rdflib.plugin import plugins
from rdflib.plugins.sparql import prepareQuery
from rdflib.query import Result, ResultParser
from rdflib.term import BNode
from rdflib.util import FORMAT_MIMETYPE_MAP, RESPONSE_TABLE_FORMAT_MIMETYPE_MAP
Expand Down Expand Up @@ -92,6 +93,11 @@ def query(

headers = {"Accept": self.response_mime_types()}

# change Accept header to an RDF mime type in case of a construct query
qtype = self.__get_query_type__(query)
if qtype in ("ConstructQuery", "DescribeQuery"):
headers.update({"Accept": self.response_mime_types_rdf()})

args = copy.deepcopy(self.kwargs)

# merge params/headers dicts
Expand Down Expand Up @@ -205,5 +211,30 @@ def response_mime_types(self) -> str:
supported_formats.add(plugin.name)
return ", ".join(supported_formats)

def response_mime_types_rdf(self) -> str:
"""Construct a HTTP-Header Accept field to reflect the supported mime types for SPARQL construct/describe queries that return a graph.

If the return_format parameter is set, the mime types are restricted to these accordingly.
"""
rdf_mimetype_map = [
mime for mlist in FORMAT_MIMETYPE_MAP.values() for mime in mlist
]

# use the matched returnType if it matches one of the rdf mime types
if self.returnFormat in FORMAT_MIMETYPE_MAP:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as i remember we have different parsers available for Construct-queries and Select-queries. So i would rather see use of a variable self.returnFormatConstruct than reuse self.returnFormat. Of course, you would have to add it to __init__ too.

return FORMAT_MIMETYPE_MAP[self.returnFormat][0]
else:
return ", ".join(rdf_mimetype_map)

def __get_query_type__(self, query: str) -> str | None:
try:
q = prepareQuery(query)
algebra = getattr(q, "algebra", None)
name = getattr(algebra, "name", None)
return name # e.g. 'SelectQuery', 'ConstructQuery', 'DescribeQuery', 'AskQuery'
except Exception:
log.debug(f"cannot parse query: {query}")
return None


__all__ = ["SPARQLConnector", "SPARQLConnectorException"]
49 changes: 49 additions & 0 deletions test/test_store/test_store_sparqlstore_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,52 @@ def test_query_construct_format(
logging.debug("request = %s", request)
logging.debug("request.headers = %s", request.headers.as_string())
assert request.path_query["query"][0] == query


def test_query_construct_accept_header(
function_httpmock: ServedBaseHTTPServerMock,
) -> None:
"""
Test that no SPARQL result media types are used for construct queries
"""
graph = Graph(
"SPARQLStore",
identifier="http://example.com",
bind_namespaces="none",
)
url = f"{function_httpmock.url}/query"
graph.open(url)

function_httpmock.responses[MethodName.GET].extend(
[
MockHTTPResponse(
200,
"OK",
b"<> a <#test> .",
{"Content-Type": ["text/turtle"]},
)
]
* 2 # two identical responses
)

# case 1: construct query

query_construct = "CONSTRUCT WHERE { ?s ?p ?o }"
graph.query(query_construct)

request_construct = function_httpmock.requests[MethodName.GET].pop()
accept_header_construct = request_construct.headers.get("Accept", "").lower()
# 'Accept' header must not include types for XML or JSON sparql results
assert "application/sparql-results" not in accept_header_construct
# 'Accept' header should be at least the default RDF/XML
assert "application/rdf+xml" in accept_header_construct

# case 2: select query

query_select = "SELECT * WHERE { ?s ?p ?o }"
graph.query(query_select)

request_select = function_httpmock.requests[MethodName.GET].pop()
accept_header_select = request_select.headers.get("Accept", "").lower()
# 'Accept' header should include types for XML or JSON sparql results
assert "application/sparql-results" in accept_header_select