Source code for djangordf.namespaces
"""Namespace utilities for djangordf.
Holds the ``LangString`` dataclass (used by ``LangStringProperty``)
and the process-wide ``NamespaceRegistry`` — a thin wrapper over
rdflib namespaces that gives users a single place to register prefix
bindings and a single ``resolve()`` that converts CURIEs into
``URIRef`` objects.
"""
from dataclasses import dataclass
from typing import Dict, Union
from rdflib import Graph, Namespace, URIRef
from rdflib.namespace import DCTERMS, FOAF, OWL, RDF, RDFS, SKOS, XSD
[docs]
@dataclass(frozen=True)
class LangString:
"""A language-tagged literal, paired with a BCP 47 language tag.
Used by ``LangStringProperty`` to round-trip ``rdf:langString``
values cleanly between Python and the triple store.
"""
value: str
lang: str
_FULL_IRI_PREFIXES = ("http://", "https://", "urn:")
_DEFAULT_BINDINGS: Dict[str, Namespace] = {
"rdf": Namespace(str(RDF)),
"rdfs": Namespace(str(RDFS)),
"owl": Namespace(str(OWL)),
"xsd": Namespace(str(XSD)),
"skos": Namespace(str(SKOS)),
"dct": Namespace(str(DCTERMS)),
"foaf": Namespace(str(FOAF)),
}
[docs]
class NamespaceRegistry:
"""Per-process registry of prefix -> namespace bindings.
Seeded with the common RDF/OWL/SKOS/Dublin Core/FOAF prefixes;
extended through :py:meth:`register` (typically from
``settings.DJANGORDF_NAMESPACES`` via ``apply_namespace_settings``).
"""
def __init__(self) -> None:
self._bindings: Dict[str, Namespace] = dict(_DEFAULT_BINDINGS)
[docs]
def register(self, prefix: str, uri: Union[str, Namespace]) -> None:
"""Add or overwrite a prefix binding.
Raw strings are wrapped in ``rdflib.Namespace`` so concatenation
produces ``URIRef`` objects with no extra ceremony.
"""
if not isinstance(uri, Namespace):
uri = Namespace(str(uri))
self._bindings[prefix] = uri
[docs]
def bindings(self) -> Dict[str, Namespace]:
"""Snapshot of current prefix -> namespace bindings."""
return dict(self._bindings)
[docs]
def bind_to_graph(self, graph: Graph) -> None:
"""Bind every prefix in this registry on ``graph`` so its
Turtle output uses pretty prefixes instead of full IRIs."""
for prefix, ns in self._bindings.items():
graph.bind(prefix, ns, override=True)
[docs]
def resolve(self, value) -> URIRef:
"""Turn a CURIE (``"skos:Concept"``) or a full IRI into a
``URIRef``. Full IRIs pass straight through. Unknown prefixes
raise ``ValueError`` so misconfiguration is loud."""
if isinstance(value, URIRef):
return value
if not isinstance(value, str):
raise TypeError(f"Cannot resolve {value!r} as CURIE or IRI")
if value.startswith(_FULL_IRI_PREFIXES):
return URIRef(value)
if ":" in value:
prefix, local = value.split(":", 1)
if prefix in self._bindings:
return URIRef(self._bindings[prefix] + local)
raise ValueError(f"Unknown CURIE prefix: {prefix!r}")
return URIRef(value)
registry = NamespaceRegistry()
[docs]
def apply_namespace_settings(extra: Union[Dict[str, str], None] = None) -> None:
"""Feed ``settings.DJANGORDF_NAMESPACES`` (or the ``extra`` dict for
tests) into the module-level :data:`registry`.
Safe to call before Django is configured: if settings access raises
``ImproperlyConfigured`` and no explicit ``extra`` was given, this
is a no-op.
"""
if extra is None:
try:
from django.conf import settings
extra = getattr(settings, "DJANGORDF_NAMESPACES", {})
except Exception:
return
for prefix, uri in (extra or {}).items():
registry.register(prefix, uri)