mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(export): normalize turtle resource iris
This commit is contained in:
@@ -32,6 +32,7 @@ License: MIT
|
||||
from pathlib import Path
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any, Dict, List, Optional, Set, Union
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
from ..utils.exceptions import ProcessingError, ValidationError
|
||||
from ..utils.helpers import ensure_directory, hash_data
|
||||
@@ -395,6 +396,29 @@ class RDFSerializer:
|
||||
|
||||
# OWL-Time namespace URI
|
||||
_OWL_TIME_NS = "http://www.w3.org/2006/time#"
|
||||
_SEMANTICA_NS = "https://semantica.dev/ns#"
|
||||
|
||||
def _as_turtle_iri(
|
||||
self, value: Any, namespaces: Optional[Dict[str, str]] = None
|
||||
) -> str:
|
||||
"""Return an absolute, safely encoded IRI for a Turtle resource."""
|
||||
value = str(value)
|
||||
try:
|
||||
parsed = urlsplit(value)
|
||||
except ValueError:
|
||||
parsed = urlsplit("")
|
||||
if parsed.scheme:
|
||||
prefix, separator, local_name = value.partition(":")
|
||||
namespace = (namespaces or self.namespace_manager.namespaces).get(prefix)
|
||||
if namespace and separator:
|
||||
return quote(namespace + local_name, safe=":/?#[]@!$&'()*+,;=%")
|
||||
remainder = value[len(prefix) + 1 :]
|
||||
if len(prefix) >= 2 and (
|
||||
remainder.startswith(("//", "/"))
|
||||
or any(token in remainder for token in (":", "/", "@"))
|
||||
):
|
||||
return quote(value, safe=":/?#[]@!$&'()*+,;=%")
|
||||
return self._SEMANTICA_NS + quote(value, safe="")
|
||||
|
||||
# Design decision — TemporalBound.OPEN in RDF:
|
||||
# OWL-Time has no standard predicate for "no known end date." We use
|
||||
@@ -467,7 +491,10 @@ class RDFSerializer:
|
||||
text = entity.get("text") or entity.get("label", "")
|
||||
confidence = normalize_confidence(entity.get("confidence", 1.0))
|
||||
|
||||
lines.append(f"<{entity_id}> a <{entity_type}> ;")
|
||||
lines.append(
|
||||
f"<{self._as_turtle_iri(entity_id, merged_namespaces)}> a "
|
||||
f"<{self._as_turtle_iri(entity_type, merged_namespaces)}> ;"
|
||||
)
|
||||
if confidence is None:
|
||||
self.logger.warning(
|
||||
f"Entity {entity_id} has a confidence that is not a number "
|
||||
@@ -488,7 +515,11 @@ class RDFSerializer:
|
||||
target_id = rel.get("target_id") or rel.get("target")
|
||||
rel_type = rel.get("type", DEFAULT_RELATION_TYPE)
|
||||
|
||||
lines.append(f"<{source_id}> <{rel_type}> <{target_id}> .")
|
||||
lines.append(
|
||||
f"<{self._as_turtle_iri(source_id)}> "
|
||||
f"<{self._as_turtle_iri(rel_type)}> "
|
||||
f"<{self._as_turtle_iri(target_id)}> ."
|
||||
)
|
||||
|
||||
if include_temporal:
|
||||
owl_lines = self._owl_time_triples_for_rel(rel, idx, time_axis)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Regression tests for valid Turtle IRI generation (issue #1099)."""
|
||||
|
||||
from rdflib import RDF, Graph, URIRef
|
||||
|
||||
from semantica.export import RDFExporter
|
||||
from semantica.kg.graph_builder import GraphBuilder
|
||||
|
||||
|
||||
def test_turtle_normalizes_graph_builder_default_identifiers():
|
||||
"""Default GraphBuilder labels with spaces become stable absolute IRIs."""
|
||||
source = {
|
||||
"entities": [
|
||||
{"id": "Acme Corp", "name": "Acme Corp", "type": "ORG"},
|
||||
{"id": "Jane Doe", "name": "Jane Doe", "type": "PERSON"},
|
||||
],
|
||||
"relationships": [
|
||||
{"source": "Jane Doe", "target": "Acme Corp", "type": "works_for"},
|
||||
],
|
||||
}
|
||||
graph_data = GraphBuilder(resolve_conflicts=False).build(sources=[source])
|
||||
|
||||
turtle = RDFExporter().export_to_rdf(graph_data, format="turtle")
|
||||
parsed = Graph().parse(data=turtle, format="turtle")
|
||||
|
||||
acme = URIRef("https://semantica.dev/ns#Acme%20Corp")
|
||||
jane = URIRef("https://semantica.dev/ns#Jane%20Doe")
|
||||
assert (acme, RDF.type, URIRef("https://semantica.dev/ns#ORG")) in parsed
|
||||
jane_type = URIRef("https://semantica.dev/ns#PERSON")
|
||||
assert (jane, RDF.type, jane_type) in parsed
|
||||
assert (
|
||||
jane,
|
||||
URIRef("https://semantica.dev/ns#works_for"),
|
||||
acme,
|
||||
) in parsed
|
||||
|
||||
|
||||
def test_turtle_preserves_absolute_iris():
|
||||
"""Already-valid absolute resource IRIs remain unchanged."""
|
||||
turtle = RDFExporter().export_to_rdf(
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"id": "https://example.org/entities/jane",
|
||||
"text": "Jane",
|
||||
"type": "urn:example:Person",
|
||||
}
|
||||
],
|
||||
"relationships": [],
|
||||
},
|
||||
format="turtle",
|
||||
)
|
||||
parsed = Graph().parse(data=turtle, format="turtle")
|
||||
|
||||
assert (
|
||||
URIRef("https://example.org/entities/jane"),
|
||||
RDF.type,
|
||||
URIRef("urn:example:Person"),
|
||||
) in parsed
|
||||
Reference in New Issue
Block a user