From b113273f1238728f1b0fda0dbabdef2bcb90bb11 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 12 Jan 2026 21:53:33 +0100 Subject: [PATCH 1/4] chore!: discontinue old namespace_tools functionality --- lodkit/namespace_tools/__init__.py | 0 lodkit/namespace_tools/_exceptions.py | 21 --- lodkit/namespace_tools/_messages.py | 37 ----- lodkit/namespace_tools/namespace_graph.py | 40 ----- lodkit/namespace_tools/ontology_namespaces.py | 81 --------- lodkit/namespace_tools/utils.py | 157 ------------------ 6 files changed, 336 deletions(-) delete mode 100644 lodkit/namespace_tools/__init__.py delete mode 100644 lodkit/namespace_tools/_exceptions.py delete mode 100644 lodkit/namespace_tools/_messages.py delete mode 100644 lodkit/namespace_tools/namespace_graph.py delete mode 100644 lodkit/namespace_tools/ontology_namespaces.py delete mode 100644 lodkit/namespace_tools/utils.py diff --git a/lodkit/namespace_tools/__init__.py b/lodkit/namespace_tools/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lodkit/namespace_tools/_exceptions.py b/lodkit/namespace_tools/_exceptions.py deleted file mode 100644 index 906de42..0000000 --- a/lodkit/namespace_tools/_exceptions.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Exceptions for RDF Namespace functionality.""" - - -class OntologyNamespaceException(Exception): - """Base exception for indicating errors during OntologyNamespace construction.""" - - -class NamespaceDelimiterException(OntologyNamespaceException): - """Exception indicating that determining a delimiter failed.""" - - -class MultiOntologyHeadersException(OntologyNamespaceException): - """Exception indicating that more than one ontology header is defined in the ontology.""" - - -class NoOntologyHeaderException(OntologyNamespaceException): - """Exception indicating that no ontology header is defined in the ontology.""" - - -class MissingOntologyClassAttributeException(Exception): - """Exception indicating that a class-level attribute 'ontology' is required but missing.""" diff --git a/lodkit/namespace_tools/_messages.py b/lodkit/namespace_tools/_messages.py deleted file mode 100644 index 227f1c6..0000000 --- a/lodkit/namespace_tools/_messages.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Messages and message constructor functions for RDF Namespace exceptions/warnings.""" - -_namespace_delimiter_exception_message = ( - "Unable to determine namespace delimiter.\n" - "The namespace in the ontology header does not specify a common URI entity delimiter (# or /) " - "and attempting to retrieve a delimiter by matching declared namespaces failed.\n" -) - -_no_ontology_header_message = ( - "Unable to detect ontology namespace. " - "An ontology namespace should be defined in the ontology header.\n" - "See https://www.w3.org/TR/owl-ref/#Ontology-def." -) - -_missing_ontology_attribute_message = ( - "ClosedOntologyNamespace subclasses expect an 'ontology' class attribute.\n" - "If dynamic namespace generation from an ontology is not needed, " - "use rdflib.ClosedNamespace or rdflib.DefinedNamespace instead." -) - - -def _namespace_delimiter_warning_message(namespace: str) -> str: - """Warning message constructor for signalling a missing entity delimiter.""" - _message = ( - f"The derived Ontology namespace '{namespace}' " - "does not feature a common URI entity delimiter (#, /)." - ) - return _message - - -def _multi_header_message(namespace_assertions: list[str]) -> str: - """Error message constructor for MultiOntologyHeadersExceptions.""" - _message = ( - "Only a single ontology namespace permissible.\n" - f"Found {', '.join(namespace_assertions)}" - ) - return _message diff --git a/lodkit/namespace_tools/namespace_graph.py b/lodkit/namespace_tools/namespace_graph.py deleted file mode 100644 index 98fa36d..0000000 --- a/lodkit/namespace_tools/namespace_graph.py +++ /dev/null @@ -1,40 +0,0 @@ -"""NamespaceGraph: rdflib.Graph extension for convenient namespace binding.""" - -from rdflib import Graph, Namespace - - -class NamespaceGraph(Graph): - """Simple rdflib.Graph subclass for easy and convenient namespace binding. - - Public class-level attributes of NamespaceGraph subclasses are interpreted as namespaces - and automatically bound for instances of that graph class. - - Example: - - class CLSGraph(NamespaceGraph): - crm = Namespace("http://www.cidoc-crm.org/cidoc-crm/") - crmcls = Namespace("https://clscor.io/ontologies/CRMcls/") - clscore = Namespace("https://clscor.io/entity/") - - graph = CLSGraph() - - ns_check: bool = all( - ns in map(lambda x: x[0], graph.namespaces()) - for ns in ("crm", "crmcls", "clscore") - ) - - print(ns_check) # True - """ - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - for name, namespace in self._bindings.items(): - self.bind(name, namespace) - - def __init_subclass__(cls) -> None: - cls._bindings = { - name: Namespace(namespace) - for name, namespace in cls.__dict__.items() - if not name.startswith("_") - } diff --git a/lodkit/namespace_tools/ontology_namespaces.py b/lodkit/namespace_tools/ontology_namespaces.py deleted file mode 100644 index 8634624..0000000 --- a/lodkit/namespace_tools/ontology_namespaces.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Functionality for Ontology Derived Dynamic (ODD) namespaces.""" - -from lodkit.namespace_tools._exceptions import MissingOntologyClassAttributeException -from lodkit.namespace_tools._messages import _missing_ontology_attribute_message -from lodkit.namespace_tools.utils import ( - _TGraphParseSource, - _get_namespace_from_ontology, - _get_ontology_graph, - _get_terms_from_ontology, -) -from rdflib import Graph, Namespace, URIRef -from rdflib.namespace import ClosedNamespace, DefinedNamespace - - -class ClosedOntologyNamespace(ClosedNamespace): - """Ontology-derived ClosedNamespace. - - Namespace members are determined by parsing an Ontology for entities. - Trying to access an undefined member results in an AttributeError. - - rdflib.ClosedNamespaces are meant to be instantiated, - so this extension is implemented to take an ontology argument upon instantiation. - See https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.namespace.html#rdflib.namespace.ClosedNamespace. - - Example: - - crm = ClosedOntologyNamespace(ontology="./CIDOC_CRM_v7.1.3.ttl") - crm.E39_Actor # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Actor') - crm.E39_Author # AttributeError - """ - - def __new__(cls, ontology: _TGraphParseSource, strict_delimiters: bool = True): - _ontology: Graph = _get_ontology_graph(ontology) - _namespace: Namespace = _get_namespace_from_ontology( - _ontology, strict_delimiters - ) - _terms: list[str] = _get_terms_from_ontology(_ontology, _namespace) - - return super().__new__(cls, uri=_namespace, terms=_terms) - - -class DefinedOntologyNamespace(DefinedNamespace): - """Ontology-derived DefinedNamespace. - - Namespace members are determined by parsing an Ontology for entities. - Trying to access an undefined member emits a UserWarning. - - rdflib.DefinedNameSpace is meant to be extended, - parameters for namespace generation are set as class-level attributes in the subclass; - so this extension is implemented to expect an 'ontology' class attribute. - See https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.namespace.html#rdflib.namespace.DefinedNamespace. - - Example: - - class crm(DefinedOntologyNamespace): - ontology = "./CIDOC_CRM_v7.1.3.ttl" - - crm.E39_Actor # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Actor') - crm.E39_Author # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Author') + UserWarning - """ - - def __init_subclass__(cls) -> None: - ontology: _TGraphParseSource = cls._get_ontology_attribute() - - _ontology: Graph = _get_ontology_graph(ontology) - _namespace: Namespace = _get_namespace_from_ontology(_ontology) - _terms: list[str] = _get_terms_from_ontology(_ontology, _namespace) - - cls._NS = _namespace - cls.__annotations__ = cls.__annotations__ | {term: URIRef for term in _terms} - - @classmethod - def _get_ontology_attribute(cls) -> _TGraphParseSource: - try: - ontology: _TGraphParseSource = cls.ontology - except AttributeError: - raise MissingOntologyClassAttributeException( - _missing_ontology_attribute_message - ) from None - else: - return ontology diff --git a/lodkit/namespace_tools/utils.py b/lodkit/namespace_tools/utils.py deleted file mode 100644 index fac50e6..0000000 --- a/lodkit/namespace_tools/utils.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Utility functions for Ontology Namespaces.""" - -from collections.abc import Iterator -import logging -from pathlib import PurePath -import re -from typing import Annotated, IO, TextIO, TypeAlias, cast - -from lodkit.namespace_tools._exceptions import ( - MultiOntologyHeadersException, - NamespaceDelimiterException, - NoOntologyHeaderException, -) -from lodkit.namespace_tools._messages import ( - _multi_header_message, - _namespace_delimiter_exception_message, - _namespace_delimiter_warning_message, - _no_ontology_header_message, -) -from rdflib import Graph, Namespace, OWL, RDF, RDFS, URIRef -from rdflib.parser import InputSource - - -logger = logging.getLogger(__name__) - - -_TGraphParseSource: Annotated[ - TypeAlias, - """Source parameter type for rdflib.Graph.parse. - This is the exact type defined in RDFLib. - """, -] = IO[bytes] | TextIO | InputSource | str | bytes | PurePath - - -def _get_ontology_graph(ontology_reference: Graph | _TGraphParseSource) -> Graph: # type: ignore - """Get a graph object from a _TGraphOrPath.""" - graph = ( - ontology_reference - if isinstance(ontology_reference, Graph) - else Graph().parse(source=ontology_reference) - ) - - return graph - - -def _delimited_namespace_p(namespace: str) -> bool: - """Check if a namespace uses '#' or '/' as URI entity delimiters.""" - if re.search(r"(/|#)$", namespace): - return True - return False - - -def _delimiter_check_invoke_side_effects( - namespace: str, strict_delimiters: bool -) -> None: - """Check if namespace is delimited and invoke side effects according to strict_delimiters.""" - if not _delimited_namespace_p(namespace): - if strict_delimiters: - raise NamespaceDelimiterException(_namespace_delimiter_exception_message) - else: - logger.warning(_namespace_delimiter_warning_message(namespace)) - - -def _resolve_namespace_from_namespace_assertion( - namespace_assertion: Namespace, ontology: Graph, strict_delimiters: bool -) -> Namespace: - """Helper for _get_namespace_from_ontology. - - Resolve a namespace from""" - if _delimited_namespace_p(namespace_assertion): - namespace = Namespace(namespace_assertion) - return namespace - else: - for _, ns in ontology.namespaces(): - if namespace_assertion in ns: - namespace = Namespace(ns) - break - else: - namespace = namespace_assertion - - _delimiter_check_invoke_side_effects(namespace, strict_delimiters) - return namespace - - -def _get_namespace_from_ontology( - ontology: Graph, strict_delimiters: bool = True -) -> Namespace: - """Get the ontology namespace from an ontology graph. - - The ontology namespace is expected to be declared in the ontology header. - See https://www.w3.org/TR/owl-ref/#Ontology-def. - - If the namespace asserted in the ontology header does not exhibit either - a fragment or URI path entity delimiter ('#' or '/'), try to match the header namespace - with a declared namespace which then takes precedence (regardless of delimiters). - - If a namespace does not have a #|/ delimiter, and strict=True, an error is raised; - else the namespace is returned and a warning emitted. - """ - # cast: RDFLib triple subjects are typed as graph._SubjectType/terms.Node - namespace_assertions = cast( - list[Namespace], [uri for uri in ontology.subjects(RDF.type, OWL.Ontology)] - ) - - match namespace_assertions: - case [URIRef()]: - namespace_assertion = namespace_assertions[0] - namespace = _resolve_namespace_from_namespace_assertion( - namespace_assertion=namespace_assertion, - ontology=ontology, - strict_delimiters=strict_delimiters, - ) - return namespace - case [URIRef(), *_]: - raise MultiOntologyHeadersException( - _multi_header_message(namespace_assertions) - ) - case []: - raise NoOntologyHeaderException(_no_ontology_header_message) - case _: # pragma: no cover - raise Exception("This should never happen.") - - -def _split_uri(uri: str) -> tuple[str, URIRef]: - """Split a URI on entity delimiter.""" - *_, last = re.split("(#|/)", uri) - return (last, URIRef(uri)) - - -def _get_terms_from_ontology(ontology: Graph, namespace: Namespace) -> list[str]: - """Get the names of all terms of an ontology. - - Ontology terms are terms that are - 1. in the ontology namespace and - 2. instances of either rdfs:Class, rdf:Property, - owl:Class, owl:ObjectProperty or owl:DatatypeProperty. - - Note: Terms get set-casted to prevent duplicates on inferred graphs. - """ - - def _get_terms() -> Iterator[str]: - _entity_classes: tuple[URIRef, ...] = ( - RDFS.Class, - RDF.Property, - OWL.Class, - OWL.ObjectProperty, - OWL.DatatypeProperty, - ) - - for s, _, o in ontology.triples((None, RDF.type, None)): - # cast: s is a URIRef/str and has special behavior for containment checks against Namesapce - s = cast(URIRef, s) - if (o in _entity_classes) and (s in namespace): - name, _ = _split_uri(s) - yield name - - return list(set(_get_terms())) From e2f7b8dbb772e9dc5cc78935cca5fc29c6c04bef Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 12 Jan 2026 21:54:20 +0100 Subject: [PATCH 2/4] feat: implement ClosedOntologyNamespace ClosedOntologyNamespace allows to populate a class namespace based on an ontology or generally and RDF graph source. The RDF source is queried for RDF class and property definitions and matching name/IRI pairs are added to the class namespace. For dictionary operations, namespace entries can be accessed via the ClosedOntologyNamespace.mapping MappingProxyType. In case of RDF term names conflicting with class namespace names, the class namespace names take precedence; conflicting RDF terms are still accessible through the ClosedOntologyNamespace.mapping proxy. --- lodkit/__init__.py | 6 +- lodkit/namespace_tools/ontology_namespace.py | 106 +++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 lodkit/namespace_tools/ontology_namespace.py diff --git a/lodkit/__init__.py b/lodkit/__init__.py index a98fd30..8f6d6e7 100644 --- a/lodkit/__init__.py +++ b/lodkit/__init__.py @@ -1,9 +1,9 @@ """Entry point for LODkit.""" -from lodkit.namespace_tools.namespace_graph import NamespaceGraph -from lodkit.namespace_tools.ontology_namespaces import ( +from lodkit.namespace_tools.ontology_namespace import ( ClosedOntologyNamespace, - DefinedOntologyNamespace, + EmptySolutionException, + NoSolutionException, ) from lodkit.rdf_importer import RDFImporter, enable_rdf_import from lodkit.triple_tools.triple_chain import TripleChain diff --git a/lodkit/namespace_tools/ontology_namespace.py b/lodkit/namespace_tools/ontology_namespace.py new file mode 100644 index 0000000..f97e707 --- /dev/null +++ b/lodkit/namespace_tools/ontology_namespace.py @@ -0,0 +1,106 @@ +from types import MappingProxyType + +from lodkit.types import GraphParseSource +from rdflib import Graph, URIRef +from rdflib.query import Result + + +class NoSolutionException(Exception): ... + + +class EmptySolutionException(Exception): ... # pragma: no cover + + +class ClosedOntologyNamespace: + """Ontology-based Namespace constructor. + + ClosedOntologyNamespace allows constructing a namespace + based on an Ontology or generally an RDF graph source. + + Given a lodkit.types.GraphParseSource or an rdflib.Graph, + the source is queried for RDF class and property definition assertions. + RDF term names are extracted by splitting the last IRI segment delimited by + '#', '/' or ':' and matching name/IRI pairs are registered in the namespace mapping. + + Namespace members are accessible as both attributes and items of a given + `ClosedOntologyNamespace` instance, i.e. attribute and item access is routed + to `ClosedOntologyNamespace.mapping`. For dictionary operations over the namespace mapping, + the public `ClosedOntologyNamespace.mapping` can be accessed directly. + + In the case of RDF term names conflicting with class namespace names, + the class namespace names take precedence for attribute access; + conflicting RDF terms are still accessible via item lookup + or through the `ClosedOntologyNamespace.mapping` proxy. + + """ + + _query = """ + prefix rdf: + prefix rdfs: + prefix owl: + + select distinct ?name ?uri + where { + values ?type { + rdfs:Class + owl:Class + + rdf:Property + owl:ObjectProperty + owl:DatatypeProperty + owl:AnnotationProperty + + owl:NamedIndividual + } + + ?uri a ?type . + filter (isIRI(?uri)) + + bind (replace(str(?uri), "^.*[#/:]", "") AS ?name) + filter (?name != "") + } + """ + + def __init__(self, source: GraphParseSource | Graph, *parse_args, **parse_kwargs): + self.source = source + + graph: Graph = ( + self.source + if isinstance(self.source, Graph) + else Graph().parse(source=self.source, *parse_args, **parse_kwargs) + ) + sparql_result: Result = graph.query(self._query) + + self.mapping: MappingProxyType[str, URIRef] = self._get_uris( + sparql_result=sparql_result + ) + + def __repr__(self) -> str: # pragma: no cover + return f"<{self.__class__.__name__} source={self.source!r}>" + + def __getattr__(self, value): + return self[value] + + def __getitem__(self, key: str) -> URIRef: + try: + return self.mapping[key] + except KeyError: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{key}'." + ) + + def _get_uris(self, sparql_result: Result) -> MappingProxyType[str, URIRef]: + _bindings = sparql_result.bindings + + match _bindings: + case []: + raise NoSolutionException() + case [{**items}] if not items: # pragma: no cover (unreachable) + raise EmptySolutionException() + case _: + return MappingProxyType( + { + str(binding["name"]): binding["uri"] # type: ignore + for binding in _bindings + } + ) From c4c6458f57102a9b926dd4fa02f4e18b4028e675 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Tue, 13 Jan 2026 13:35:33 +0100 Subject: [PATCH 3/4] test: implement ClosedOntologyNamespace tests --- .../test_closed_ontology_namespace.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/test_namespace_tools/test_closed_ontology_namespace.py diff --git a/tests/test_namespace_tools/test_closed_ontology_namespace.py b/tests/test_namespace_tools/test_closed_ontology_namespace.py new file mode 100644 index 0000000..a27d9f0 --- /dev/null +++ b/tests/test_namespace_tools/test_closed_ontology_namespace.py @@ -0,0 +1,133 @@ +"""Pytest entry point for ClosedOntologyNamespace tests.""" + +import pytest + +from lodkit import ClosedOntologyNamespace, NoSolutionException +from rdflib import Graph, RDF, RDFS, URIRef + + +def test_closed_ns_term_types(): + """Check if all RDF type terms are recognized by ClosedOntologyNamespace.""" + + data = """ + @prefix ex: . + @prefix rdf: . + @prefix rdfs: . + @prefix owl: . + + a rdfs:Class . + a owl:Class . + + a rdf:Property . + a owl:ObjectProperty . + a owl:DatatypeProperty . + a owl:AnnotationProperty . + + a owl:NamedIndividual . + """ + + g = Graph().parse(data=data, format="ttl") + ns = ClosedOntologyNamespace(source=g) + + expected = { + "rdf_class": URIRef("urn:rdf_class"), + "owl_class": URIRef("urn:owl_class"), + "rdf_property": URIRef("urn:rdf_property"), + "owl_objectproperty": URIRef("urn:owl_objectproperty"), + "owl_datatypeproperty": URIRef("urn:owl_datatypeproperty"), + "owl_annotationproperty": URIRef("urn:owl_annotationproperty"), + "owl_namedindividual": URIRef("urn:owl_namedindividual"), + } + + assert ns.mapping == expected + + +def test_closed_ns_delimited_iris(): + """Check name extraction logic for all supported delimiters.""" + data = """ + @prefix rdfs: . + + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + a rdfs:Class . + """ + g = Graph().parse(data=data, format="ttl") + ns = ClosedOntologyNamespace(source=g) + + expected = { + "s1": URIRef("urn:s1"), + "s2": URIRef("urn:example/s2"), + "s3": URIRef("https://example.com/s3"), + "s4": URIRef("https://example.com#s4"), + "s5": URIRef("https://example.com/something#s5"), + "nose": URIRef("foo://example.com:8042/over/there?name=ferret#nose"), + "there?name=ferret": URIRef("foo://example.com:8042/over/there?name=ferret"), + } + + assert ns.mapping == expected + + # getitem checks + for k, v in ns.mapping.items(): + assert ns[k] == expected[k] + + +def test_closed_ns_name_collisions(): + """Check handling of collisions between RDF term names and class namespace names. + + In the case of RDF term names conflicting with class namespace names, + class namespace names take precedence over term names for attribute access. + + Getitem access on ClosedOntologyNamespace directly and ClosedOntologyNamespace.mapping + retrieve the RDF term however. + """ + + g = Graph() + + triples = [ + (URIRef("urn:mapping"), RDF.type, RDFS.Class), + (URIRef("urn:source"), RDF.type, RDFS.Class), + ] + + for triple in triples: + g.add(triple) + + ns = ClosedOntologyNamespace(source=g) + + expected = {"mapping": URIRef("urn:mapping"), "source": URIRef("urn:source")} + + assert ns.source is g + assert ns.mapping == expected + + assert ns["mapping"] == URIRef("urn:mapping") + assert ns["source"] == URIRef("urn:source") + + +def test_closed_ns_no_solution(): + """Check that NoSolutionException is raised if no classes/properties are found.""" + + g = Graph() + + with pytest.raises(NoSolutionException): + ClosedOntologyNamespace(source=g) + + +def test_closed_ns_attribute_error(): + """Check that attribute and getitem access failure results in an AttributeError.""" + + g = Graph() + g.add( + (URIRef("urn:s"), RDF.type, RDFS.Class) + ) # add a class assertion to avoid NoSolutionException + + ns = ClosedOntologyNamespace(source=g) + + with pytest.raises(AttributeError): + ns.dne + + with pytest.raises(AttributeError): + ns["dne"] From b8985e00b73aad86b21a523c96113a468420653e Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Wed, 14 Jan 2026 16:17:48 +0100 Subject: [PATCH 4/4] docs: add README section on ClosedOntologyNamespace --- README.md | 58 ++++++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2a4aaeb..b42b5d0 100644 --- a/README.md +++ b/README.md @@ -269,9 +269,7 @@ RDF import functionality is available after registering `lodkit.RDFImporter` wit ## URI Tools -### uritools.utils - -## URIConstructor +### URIConstructor The `URIConstructor` class provides namespaced URI constructor functionality. @@ -292,47 +290,41 @@ make_uri("test") == make_uri("test") # True ## Namespace Tools -### NamespaceGraph -`lodkit.NamespaceGraph` is a simple rdflib.Graph subclass for easy and convenient namespace binding. +### ClosedOntologyNamespace -```python -from lodkit import NamespaceGraph -from rdflib import Namespace +`ClosedOntologyNamespace` is an `rdflib.namespace.ClosedNamespace`-inspired utility class that constructs an immutable (*closed*) mapping of RDF term names to IRIs based on an Ontology or generally an RDF graph source. -class CLSGraph(NamespaceGraph): - crm = Namespace("http://www.cidoc-crm.org/cidoc-crm/") - crmcls = Namespace("https://clscor.io/ontologies/CRMcls/") - clscore = Namespace("https://clscor.io/entity/") +Given a `lodkit.types.GraphParseSource` or an `rdflib.Graph`, a `MappingProxyType[str, rdflib.URIRef]` mapping is created and stored in `ClosedOntologyNamespace.mapping` by -graph = CLSGraph() +1. Querying the RDF source for RDF class and property definitions (RDF/RDFS/OWL class/property type assertions and OWL named individual assertions) -ns_check: bool = all( - ns in map(lambda x: x[0], graph.namespaces()) - for ns in ("crm", "crmcls", "clscore") -) +2. Deriving RDF term names by extracting the last IRI component delimited by `#`, `/` or `:` for generating the RDF term name -> IRI mapping. -print(ns_check) # True -``` -## ClosedOntologyNamespace, DefinedOntologyNamespace -`lodkit.ClosedOntologyNamespace` and `lodkit.DefinedOntologyNamespace` are `rdflib.ClosedNamespace` and `rdflib.DefinedNameSpace` subclasses -that are able to load namespace members based on an ontology. +Namespace members are accessible as both attributes and items of a given `ClosedOntologyNamespace` instance, i.e. attribute and item access is routed to `ClosedOntologyNamespace.mapping`. +For dictionary operations over the namespace mapping, the public `ClosedOntologyNamespace.mapping` can be accessed directly. -```python -crm = ClosedOntologyNamespace(ontology="./CIDOC_CRM_v7.1.3.ttl") -crm.E39_Actor # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Actor') -crm.E39_Author # AttributeError -``` +The following example loads a remote Ontology and accesses namespace members using attribute and item lookup. ```python -class crm(DefinedOntologyNamespace): - ontology = "./CIDOC_CRM_v7.1.3.ttl" +from lodkit import ClosedOntologyNamespace + +crm = ClosedOntologyNamespace( + source="https://cidoc-crm.org/rdfs/7.1.3/CIDOC_CRM_v7.1.3.rdf" +) -crm.E39_Actor # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Actor') -crm.E39_Author # URIRef('http://www.cidoc-crm.org/cidoc-crm/E39_Author') + UserWarning +crm.E92_Spacetime_Volume # URIRef('http://www.cidoc-crm.org/cidoc-crm/E92_Spacetime_Volume') +crm["E52_Time-Span"] # URIRef('http://www.cidoc-crm.org/cidoc-crm/E52_Time-Span') + +crm.E21_Author # AttributeError +crm["E21-Person"] # AttributeError ``` +> Note that lookup failure for both attribute and item access on `ClosedOntologyNamespace` objects raises an `AttributeError`! + +In the case of RDF term names conflicting with class namespace names, the class namespace names take precedence for attribute access; conflicting RDF terms are still accessible via item lookup or through the `ClosedOntologyNamespace.mapping` proxy. -Note that `rdflib.ClosedNamespaces` are meant to be instantiated and `rdflib.DefinedNameSpaces` are meant to be extended, -which is reflected in `lodkit.ClosedOntologyNamespace` and `lodkit.DefinedOntologyNamespace`. +> Note that *currently* `ClosedOntologyNamespace` is a highly dynamic runtime construct and does not support static analysis and IDE completion for namespace entries. + +