From 8c57a44cd1a6e14ed0c39614b613cc1d06f9243f Mon Sep 17 00:00:00 2001 From: Carlo van Driesten Date: Thu, 10 Sep 2026 18:42:13 +0200 Subject: [PATCH] fix: verify base rendering across RDF term positions Signed-off-by: jdsika --- CHANGELOG.md | 5 + docs/api.md | 19 +- src/diffable_rdf/canonicalize.py | 120 ++++--- tests/serialization/test_base_iri_fidelity.py | 311 +++++++++++++++++- 4 files changed, 384 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c49148..be3cc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ about it. - `wl_blank_node_labels` and `wl_relabel_quads` now reject embedded `pyoxigraph.Triple` terms with `ValueError`. They operate on supported top-level quad terms only; direction-tagged literals remain supported. +- Base rendering is accepted only after RDFLib and pyoxigraph preserve direct + and literal-datatype IRI terms. A rendering that does not verify is emitted + once more without its base IRI, retaining valid prefixes. Turtle, TriG, and + N3 prefix bindings equal to the base remain available for compact terms; + RDF/XML keeps its XML namespace selection on the retry. - Degraded JSON-LD rejects a relative subject or object identifier exactly matching `@[A-Za-z]+`. JSON-LD reserves these strings, so returning them in an `@id` value can change or discard a graph term. diff --git a/docs/api.md b/docs/api.md index c9009c4..4f5e4b6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -202,21 +202,22 @@ serializer. **`graph.base` is used on this path**, unlike in `deterministic_turtle`: a base IRI is handed to pyoxigraph, which emits a `@base` directive and RFC -3986-correct relative references. If pyoxigraph rejects the base or a prefix -IRI, the call logs a warning and re-serializes without them. - -One exception, with a warning: a base containing a **fragment** is not used for -the Turtle family. RFC 3986 §5.2.2 discards a base's fragment when resolving, so -`http://ex.org/d#a` under base `http://ex.org/d#` is correctly written `<#a>` — -but rdflib's parser resolves a fragment reference by concatenation and reads it -back as `http://ex.org/d##a`, a different IRI in every position. Absolute IRIs -are written instead, so the output means the same thing to both readers. +3986-correct relative references. For Turtle, TriG, and N3, valid prefix +bindings whose namespace equals the base remain available for compact terms. +Those formats are accepted only after RDFLib and pyoxigraph preserve direct and +literal-datatype IRI terms. +If that verification fails, the call logs a warning and makes one further +rendering without the base IRI while retaining valid prefixes; the second +rendering must also verify. If pyoxigraph rejects a base or prefix IRI, the call +logs a warning and serializes without those rejected values. **RDF/XML.** Literal carriage returns are written as ` ` character references, so XML newline normalization cannot turn CR or CRLF into LF. On the fallback path the `rdf:Description` elements and the property elements within them are sorted, because rdflib's RDF/XML serializer orders both by its own graph traversal and RDF/XML gives neither order any meaning. +RDF/XML base rendering is also verified; when it fails, one no-base rendering +keeps the same XML namespace selection and must verify before it is returned. **On the fallback path, every name above works**, and two names for one format produce identical bytes. Two of them get there differently: TriG renders as diff --git a/src/diffable_rdf/canonicalize.py b/src/diffable_rdf/canonicalize.py index ef9dd59..448b97b 100644 --- a/src/diffable_rdf/canonicalize.py +++ b/src/diffable_rdf/canonicalize.py @@ -30,10 +30,9 @@ ``xsd:decimal`` (``1.23``). rdflib parses these back with the correct datatype, so this is lossless. -4. **Base IRI / prefix collision**: When a graph has ``@base`` and a - prefix whose namespace equals the base IRI (e.g. rdflib's auto-bound - ``base:`` prefix), pyoxigraph emits CURIEs like ``base:label`` that - rdflib rejects. We skip such prefixes during serialization. +4. **Base IRI interoperability**: Base-relative output is accepted only after + both readers preserve every IRI position. If the Turtle-family rendering + fails that check, it is rendered once more without a base IRI. 5. **Trailing escaped dot in PN_LOCAL**: pyoxigraph emits CURIEs like ``prefix:local\\.`` for IRIs whose local part ends with ``.``. This @@ -736,17 +735,33 @@ def _assert_round_trips(source: rdflib.Graph, serialized: str, output_format: st # reference rdflib mis-resolves would otherwise slip past: pyoxigraph # resolves it correctly, so comparing only canonical forms compares two # correct readings and sees nothing wrong. - source_iris = {str(term) for triple in source for term in triple if isinstance(term, rdflib.URIRef)} - reparsed_iris = { - str(term) for triple in reparsed for term in triple if isinstance(term, rdflib.URIRef) - } - invented = reparsed_iris - source_iris - if invented: + def iri_sets(graph: rdflib.Graph) -> tuple[set[str], set[str]]: + """Return direct IRI terms and literal datatype IRIs separately.""" + direct: set[str] = set() + datatypes: set[str] = set() + for triple in graph: + for term in triple: + if isinstance(term, rdflib.URIRef): + direct.add(str(term)) + elif isinstance(term, rdflib.Literal) and term.datatype is not None: + datatypes.add(str(term.datatype)) + # RDF 1.1 treats a plain string and an xsd:string literal as the same + # literal. Keep that allowance local to datatype positions: an + # xsd:string URIRef used directly in a triple remains significant. + datatypes.discard(str(rdflib.XSD.string)) + return direct, datatypes + + source_direct, source_datatypes = iri_sets(source) + reparsed_direct, reparsed_datatypes = iri_sets(reparsed) + missing = (source_direct - reparsed_direct) | (source_datatypes - reparsed_datatypes) + invented = (reparsed_direct - source_direct) | (reparsed_datatypes - source_datatypes) + if missing or invented: + direction = "loses" if missing else "reads back" + changed = missing if missing else invented raise ValueError( - f"canonical {output_format} serialization does not round-trip: rdflib reads back " - f"{len(invented)} IRI(s) the input graph does not contain, such as " - f"{sorted(invented)[0]!r}. This is a bug in diffable-rdf: please report it with " - "the input graph." + f"canonical {output_format} serialization does not round-trip: rdflib {direction} " + f"{len(changed)} IRI(s), such as {sorted(changed)[0]!r}. This is a bug in " + "diffable-rdf: please report it with the input graph." ) # It normalizes numeric lexical forms, so use pyoxigraph's parsed terms for @@ -866,22 +881,6 @@ def canonicalize_rdf_graph( # 5. Collect prefixes for formats that support them. base_iri = str(graph.base) if graph.base else None - if base_iri is not None and "#" in base_iri and ox_format in _TURTLE_FAMILY_FORMATS: - # A base with a fragment cannot be used for relativization that rdflib - # can read back. pyoxigraph correctly writes <#a> for - # http://ex.org/d#a under base http://ex.org/d#, per RFC 3986 section - # 5.2.2, which discards the base's fragment. rdflib's notation3 parser - # instead concatenates, yielding http://ex.org/d##a -- a different IRI - # in every position. deterministic_turtle drops the base outright for - # this reason; dropping it just for a fragment base keeps ordinary - # bases working while emitting nothing rdflib will misread. - logger.warning( - "graph.base %r contains a fragment; emitting absolute IRIs instead of " - "relativizing, because rdflib's parser resolves a fragment-relative " - "reference by concatenation rather than per RFC 3986.", - base_iri, - ) - base_iri = None prefixes: dict[str, str] | None = None if ox_format in _PREFIX_FORMATS: prefixes = {} @@ -889,10 +888,14 @@ def canonicalize_rdf_graph( if not prefix: # skip empty prefix (base) continue ns_str = str(namespace) - # Skip prefixes whose namespace matches the base IRI to avoid - # pyoxigraph emitting CURIEs like `base:label` that conflict - # with the @base directive. - if base_iri and ns_str == base_iri: + # Equal-base bindings are emitted only in Turtle-family formats, + # where CURIE terms preserve their namespace without a relative + # XML namespace declaration. + if ( + base_iri + and ns_str == base_iri + and ox_format not in _TURTLE_FAMILY_FORMATS + ): continue # Skip a namespace pyoxigraph cannot declare as a prefix. The # recovery below drops *all* prefixes, so letting one unusable @@ -907,6 +910,7 @@ def canonicalize_rdf_graph( # them. prefixes = _filter_prefixes_to_used(prefixes, _iri_terms(sorted_triples)) used_prefixes = prefixes + used_base_iri = base_iri try: result_bytes = ox.serialize( sorted_triples, @@ -927,21 +931,43 @@ def canonicalize_rdf_graph( format=ox_format, ) used_prefixes = None + used_base_iri = None # pyoxigraph's serialize() stub is a single flat `-> bytes | None` with no # overload distinguishing output=None (returns bytes) from output= # (returns None). Neither call above passes output=, so this is always bytes. - result = result_bytes.decode("utf-8") # type: ignore[union-attr] - if ox_format == ox.RdfFormat.RDF_XML: - result = _finalize_rdf_xml(result) - if ox_format == ox.RdfFormat.JSON_LD: - # pyoxigraph emits compact single-line JSON; re-render it indented so - # the output is diffable line by line, which is the point of this - # library. Safe to route through deterministic_json: pyoxigraph writes - # *expanded* JSON-LD, so there is no @context or @list array whose - # order carries meaning, and the triples were already sorted above. - result = deterministic_json(json.loads(result)) + "\n" - if ox_format in _TURTLE_FAMILY_FORMATS and used_prefixes: - result = _expand_trailing_dot_curies(result, used_prefixes) + def render(result_bytes: bytes, render_prefixes: dict[str, str] | None) -> str: + result = result_bytes.decode("utf-8") + if ox_format == ox.RdfFormat.RDF_XML: + result = _finalize_rdf_xml(result) + if ox_format == ox.RdfFormat.JSON_LD: + # pyoxigraph emits compact single-line JSON; re-render it indented so + # the output is diffable line by line, which is the point of this + # library. Safe to route through deterministic_json: pyoxigraph writes + # *expanded* JSON-LD, so there is no @context or @list array whose + # order carries meaning, and the triples were already sorted above. + result = deterministic_json(json.loads(result)) + "\n" + if ox_format in _TURTLE_FAMILY_FORMATS and render_prefixes: + result = _expand_trailing_dot_curies(result, render_prefixes) + return result + + assert result_bytes is not None + result = render(result_bytes, used_prefixes) if ox_format in _VERIFIED_FORMATS: - _assert_round_trips(graph, result, output_format) + try: + _assert_round_trips(graph, result, output_format) + except ValueError: + if used_base_iri is None: + raise + logger.warning( + "base IRI %r failed round-trip verification; serializing without it", + used_base_iri, + ) + retry_bytes = ox.serialize( + sorted_triples, + format=ox_format, + prefixes=used_prefixes, + ) + assert retry_bytes is not None + result = render(retry_bytes, used_prefixes) + _assert_round_trips(graph, result, output_format) return _with_single_trailing_newline(result) diff --git a/tests/serialization/test_base_iri_fidelity.py b/tests/serialization/test_base_iri_fidelity.py index 8d896d4..2848553 100644 --- a/tests/serialization/test_base_iri_fidelity.py +++ b/tests/serialization/test_base_iri_fidelity.py @@ -1,9 +1,4 @@ -"""Base-IRI contracts for RDF terms read by standard format parsers. - -RFC 3986 section 5.2.2 resolves `<#a>` against `http://ex.org/d#` as -`http://ex.org/d#a`. Format parsers must preserve those graph terms without -introducing an additional fragment separator. -""" +"""Base-IRI contracts for every IRI position read by standard parsers.""" from __future__ import annotations @@ -11,13 +6,23 @@ import pytest from rdflib import Graph, Literal, Namespace, URIRef from rdflib.compare import isomorphic +from rdflib.namespace import XSD import diffable_rdf.canonicalize as canonicalize_module from diffable_rdf import canonicalize_rdf_graph, deterministic_turtle EX = Namespace("http://example.org/") -VERIFIED = ["turtle", "ttl", "trig", "n3", "xml", "rdf/xml"] +TURTLE_FAMILY = ["turtle", "ttl", "trig", "n3"] +XML_ALIASES = ["xml", "rdf/xml"] PARSER = {"ttl": "turtle", "rdf/xml": "xml"} +OX_FORMAT = { + "turtle": ox.RdfFormat.TURTLE, + "ttl": ox.RdfFormat.TURTLE, + "trig": ox.RdfFormat.TRIG, + "n3": ox.RdfFormat.N3, + "xml": ox.RdfFormat.RDF_XML, + "rdf/xml": ox.RdfFormat.RDF_XML, +} def _hash_base_graph() -> Graph: @@ -26,7 +31,7 @@ def _hash_base_graph() -> Graph: return graph -@pytest.mark.parametrize("output_format", VERIFIED) +@pytest.mark.parametrize("output_format", TURTLE_FAMILY + ["xml", "rdf/xml"]) def test_a_fragment_base_preserves_the_terms(output_format: str) -> None: """A fragment base preserves each IRI term in every verified format.""" graph = _hash_base_graph() @@ -70,22 +75,25 @@ def test_canonicalize_rdf_graph_handles_a_relative_base() -> None: assert "http://example.org/s" in canonicalize_rdf_graph(graph, output_format="turtle") -@pytest.mark.parametrize("output_format", ["turtle", "trig", "n3"]) +@pytest.mark.parametrize("output_format", TURTLE_FAMILY) def test_an_ordinary_base_still_relativizes(output_format: str) -> None: - """Only a *fragment* base is dropped; a path base keeps working.""" + """A base that verifies keeps its relative output.""" graph = Graph(base="http://ex.org/d/") graph.add((URIRef("http://ex.org/d/a"), URIRef("http://ex.org/d/p"), Literal("v"))) result = canonicalize_rdf_graph(graph, output_format=output_format) reparsed = Graph().parse(data=result, format=output_format) - assert "@base" in result - assert "" in result, "an ordinary base should still produce relative references" + assert result == ( + '@base .\n' + '@prefix xsd: .\n' + '

"v" .\n' + ) assert isomorphic(reparsed, graph) -def test_dropping_a_fragment_base_is_reported() -> None: - """The serializer reports that it omits a fragment base.""" +def test_removing_an_unverified_base_is_reported() -> None: + """The serializer reports the verification-driven base retry.""" import logging graph = _hash_base_graph() @@ -103,7 +111,227 @@ def emit(self, record: logging.LogRecord) -> None: finally: logger.removeHandler(handler) - assert any("fragment" in message for message in records), records + assert any("failed round-trip verification" in message for message in records), records + + +def _all_iris(graph: Graph) -> set[str]: + """Collect direct and datatype IRI terms from a graph.""" + return { + str(iri) + for triple in graph + for term in triple + for iri in ( + (term,) + if isinstance(term, URIRef) + else (term.datatype,) + if isinstance(term, Literal) and term.datatype is not None + else () + ) + } + + +def _assert_exact_parser_fidelity(graph: Graph, result: str, output_format: str) -> Graph: + """Assert RDFLib and pyoxigraph preserve the same graph terms.""" + reparsed = Graph().parse(data=result, format=PARSER.get(output_format, output_format)) + + assert isomorphic(reparsed, graph) + assert _all_iris(reparsed) == _all_iris(graph) + assert set(ox.parse(result, format=OX_FORMAT[output_format])) == set( + ox.parse(graph.serialize(format="nt"), format=ox.RdfFormat.N_TRIPLES) + ) + return reparsed + + +@pytest.mark.parametrize("output_format", TURTLE_FAMILY) +@pytest.mark.parametrize( + "base", + ["http://ex/d/", "http://ex/d", "http://ex/d?x=", "urn:example:", "http://ex/d#"], +) +def test_an_equal_base_namespace_preserves_every_iri_position(base: str, output_format: str) -> None: + """An equal-base prefix remains available for terms in every IRI position.""" + graph = Graph(base=base, bind_namespaces="none") + graph.bind("base", URIRef(base)) + graph.add((URIRef(base + "subject"), URIRef(base + "predicate"), URIRef(base + "object"))) + graph.add( + ( + URIRef(base + "subject"), + URIRef(base + "predicate"), + Literal("v", datatype=URIRef(base + "datatype")), + ) + ) + before_triples = set(graph) + before_base = graph.base + before_bindings = tuple(graph.namespaces()) + + result = canonicalize_rdf_graph(graph, output_format) + reparsed = _assert_exact_parser_fidelity(graph, result, output_format) + + assert canonicalize_rdf_graph(graph, output_format) == result + assert "@prefix base:" in result + assert "base:subject base:predicate" in result + assert str(reparsed.namespace_manager.store.namespace("base")) == base + assert tuple(graph.namespaces()) == before_bindings + assert graph.base == before_base + assert set(graph) == before_triples + + +@pytest.mark.parametrize("output_format", TURTLE_FAMILY) +def test_an_unprefixed_query_base_retries_without_base_and_preserves_datatypes( + output_format: str, caplog: pytest.LogCaptureFixture +) -> None: + """A query base falls back to absolute terms when relative terms do not verify.""" + base = "http://ex/d?x=" + graph = Graph(base=base, bind_namespaces="none") + graph.add((URIRef(base + "subject"), URIRef(base + "predicate"), URIRef(base + "object"))) + graph.add( + ( + URIRef(base + "subject"), + URIRef(base + "predicate"), + Literal("v", datatype=URIRef(base + "datatype")), + ) + ) + before_triples = set(graph) + before_base = graph.base + before_bindings = tuple(graph.namespaces()) + + with caplog.at_level("WARNING", logger="diffable_rdf.canonicalize"): + result = canonicalize_rdf_graph(graph, output_format) + _assert_exact_parser_fidelity(graph, result, output_format) + + assert "@base" not in result + assert canonicalize_rdf_graph(graph, output_format) == result + assert tuple(graph.namespaces()) == before_bindings + assert graph.base == before_base + assert set(graph) == before_triples + assert [record.message for record in caplog.records if "failed round-trip verification" in record.message] + + +@pytest.mark.parametrize("output_format", TURTLE_FAMILY) +def test_a_datatype_only_query_base_retries_without_base(output_format: str) -> None: + """A datatype IRI alone can require the no-base rendering.""" + base = "http://ex/d?x=" + graph = Graph(base=base, bind_namespaces="none") + graph.add( + ( + URIRef("http://other/s"), + URIRef("http://other/p"), + Literal("v", datatype=URIRef(base + "type")), + ) + ) + + result = canonicalize_rdf_graph(graph, output_format) + _assert_exact_parser_fidelity(graph, result, output_format) + + assert "@base" not in result + assert canonicalize_rdf_graph(graph, output_format) == result + + +@pytest.mark.parametrize("output_format", XML_ALIASES) +@pytest.mark.parametrize( + "datatype", + [ + pytest.param(XSD.integer, id="xsd-integer"), + pytest.param(URIRef("http://custom.example/type"), id="custom"), + ], +) +def test_rdf_xml_retries_without_base_for_typed_literal_namespaces( + output_format: str, + datatype: URIRef, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """RDF/XML retries without a base while retaining its namespace selection.""" + base = "http://ex.org/d/" + graph = Graph(base=base, bind_namespaces="none") + graph.add((URIRef(base + "s"), URIRef("http://ex.org/p"), Literal("1", datatype=datatype, normalize=False))) + original = canonicalize_module.ox.serialize + calls: list[dict[str, object]] = [] + + def record(*args: object, **kwargs: object) -> bytes: + calls.append(kwargs) + return original(*args, **kwargs) + + monkeypatch.setattr(canonicalize_module.ox, "serialize", record) + with caplog.at_level("WARNING", logger="diffable_rdf.canonicalize"): + result = canonicalize_rdf_graph(graph, output_format) + _assert_exact_parser_fidelity(graph, result, output_format) + + assert "xml:base=" not in result + assert len(calls) == 2 + assert calls[0]["prefixes"] == calls[1]["prefixes"] + assert calls[0]["base_iri"] == base + assert "base_iri" not in calls[1] + assert [record.message for record in caplog.records if "failed round-trip verification" in record.message] + + +@pytest.mark.parametrize( + ("output_format", "base_marker"), + [ + pytest.param("turtle", "@base ", id="turtle"), + pytest.param("xml", 'xml:base="http://ex/d/"', id="xml"), + pytest.param("rdf/xml", 'xml:base="http://ex/d/"', id="rdf-xml"), + ], +) +def test_a_verified_base_does_not_serialize_twice( + output_format: str, base_marker: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """A successful first rendering does not take the no-base retry.""" + graph = Graph(base="http://ex/d/", bind_namespaces="none") + graph.add((URIRef("http://ex/d/a"), URIRef("http://ex/d/p"), Literal("v"))) + original = canonicalize_module.ox.serialize + calls: list[dict[str, object]] = [] + + def record(*args: object, **kwargs: object) -> bytes: + calls.append(kwargs) + return original(*args, **kwargs) + + monkeypatch.setattr(canonicalize_module.ox, "serialize", record) + result = canonicalize_rdf_graph(graph, output_format) + + assert base_marker in result + assert len(calls) == 1 + + +@pytest.mark.parametrize("output_format", ["turtle", *XML_ALIASES]) +def test_an_unrelated_backend_error_is_not_retried( + output_format: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """Only rejected prefix and base values receive serializer recovery.""" + graph = Graph(base="http://ex/d/") + graph.add((URIRef("http://ex/d/a"), URIRef("http://ex/d/p"), Literal("v"))) + calls = 0 + + def fail(*args: object, **kwargs: object) -> bytes: + nonlocal calls + calls += 1 + raise ValueError("backend failed") + + monkeypatch.setattr(canonicalize_module.ox, "serialize", fail) + with pytest.raises(ValueError, match="backend failed"): + canonicalize_rdf_graph(graph, output_format) + assert calls == 1 + + +@pytest.mark.parametrize("output_format", ["turtle", *XML_ALIASES]) +def test_a_failed_no_base_retry_propagates_its_validation_error( + output_format: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """The bounded retry still requires a successful final verification.""" + graph = Graph(base="http://ex/d?x=", bind_namespaces="none") + graph.add((URIRef("http://ex/d?x=s"), URIRef("http://ex/d?x=p"), Literal("v"))) + original = canonicalize_module._assert_round_trips + calls = 0 + + def fail(*args: object, **kwargs: object) -> None: + nonlocal calls + calls += 1 + raise ValueError("validation failed") + + monkeypatch.setattr(canonicalize_module, "_assert_round_trips", fail) + with pytest.raises(ValueError, match="validation failed"): + canonicalize_rdf_graph(graph, output_format) + assert calls == 2 + monkeypatch.setattr(canonicalize_module, "_assert_round_trips", original) def test_the_guard_catches_an_iri_the_input_never_contained() -> None: @@ -119,6 +347,59 @@ def test_the_guard_catches_an_iri_the_input_never_contained() -> None: ) +@pytest.mark.parametrize( + ("source", "serialized"), + [ + ( + Literal("v", datatype=URIRef("http://example.org/source-datatype")), + '"v"^^', + ), + ( + Literal("v"), + '"v"^^', + ), + ], + ids=["missing-datatype", "invented-datatype"], +) +def test_the_guard_catches_missing_and_invented_literal_datatypes(source: Literal, serialized: str) -> None: + """Datatype IRIs are checked in addition to direct URIRef terms.""" + graph = Graph() + graph.add((EX.s, EX.p, source)) + + with pytest.raises(ValueError, match="IRI"): + canonicalize_module._assert_round_trips( + graph, + f" {serialized} .\n", + "nt", + ) + + +def test_the_guard_accepts_plain_string_and_xsd_string_datatype_equivalence() -> None: + """RDF 1.1 string equivalence applies only to literal datatype positions.""" + graph = Graph() + graph.add((EX.s, EX.p, Literal("v"))) + + canonicalize_module._assert_round_trips( + graph, + ' "v"^^ .\n', + "nt", + ) + + +def test_the_guard_keeps_a_direct_xsd_string_iri_significant() -> None: + """A direct xsd:string URIRef is not treated as a literal datatype.""" + graph = Graph() + graph.add((XSD.string, EX.p, Literal("v"))) + + with pytest.raises(ValueError, match="IRI"): + canonicalize_module._assert_round_trips( + graph, + ' ' + '"v"^^ .\n', + "nt", + ) + + def test_the_guard_tolerates_rdflibs_literal_normalization() -> None: """Round-trip validation accepts RDFLib integer lexical normalization.""" from rdflib.namespace import XSD