diff --git a/rdflib/plugin.py b/rdflib/plugin.py index f7b98c99f..f3c01f1dc 100644 --- a/rdflib/plugin.py +++ b/rdflib/plugin.py @@ -378,6 +378,12 @@ def plugins( "rdflib.plugins.parsers.notation3", "TurtleParser", ) +register( + "application/x-turtle", + Parser, + "rdflib.plugins.parsers.notation3", + "TurtleParser", +) register( "turtle", Parser, diff --git a/rdflib/plugins/stores/sparqlconnector.py b/rdflib/plugins/stores/sparqlconnector.py index 3dc2158f7..569e6e7d7 100644 --- a/rdflib/plugins/stores/sparqlconnector.py +++ b/rdflib/plugins/stores/sparqlconnector.py @@ -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 @@ -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 @@ -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: + 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"] diff --git a/test/test_store/test_store_sparqlstore_query.py b/test/test_store/test_store_sparqlstore_query.py index d17823087..1f16831df 100644 --- a/test/test_store/test_store_sparqlstore_query.py +++ b/test/test_store/test_store_sparqlstore_query.py @@ -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