Source code for kapps_ogm.node.node_data_formatter
"""Data formatting and sanitization utilities for Node instances."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import logging
from rdflib import BNode
from kapps_triplestore_interface import IRI
if TYPE_CHECKING:
from .core import Node
from kapps_ogm.ogm import OGM
logger = logging.getLogger("kapps_node_formatter")
logger.setLevel(logging.INFO)
[docs]
def sanitize_data(
data: Dict,
node: "Node",
known_nodes: Optional[Dict[int, "Node"]] = None,
) -> tuple[dict[IRI, List[Any]], Optional[Union[IRI, BNode]]]:
"""
Recursively converts provided data dict into a unified format.
The output is a dict with items in any of the forms:
- IRI: List[Any] # literal property -> list of literal values
- IRI: List[Node] # class property -> list of Node instances with IRI ids
- IRI: List[Node] # complex property -> list of Node instances with blank ids
In the input, property keys can be either str or IRI. Class and complex property
values may be represented as dicts.
Args:
data: Raw data dictionary to sanitize.
ogm: OGM instance for creating nested Node instances.
Returns:
tuple[dict[IRI, List[Any]], Optional[Union[IRI, BNode]]]: Sanitized data with IRI keys and
list values, and the node ID if the data carried one.
Raises:
ValueError: If data cannot be converted into the expected format.
"""
from .core import Node
if not isinstance(data, dict):
raise ValueError("Data must be a dictionary")
sanitized_data = {}
node_id = None
for property_iri, domain_list in data.items():
# Catch special cases
if property_iri == "id":
# An anonymous node fetched from the store arrives already identified — as an IRI once
# skolemised, as a BNode while still in the store's blank-node form. Either is the
# node's actual address and must survive verbatim.
node_id = (
domain_list
if isinstance(domain_list, (IRI, BNode))
else IRI(domain_list)
)
continue
try:
property_iri = IRI(property_iri)
except Exception as e:
try:
property_iri = IRI.from_lined(property_iri)
except Exception:
raise ValueError(f"Invalid property in data: {property_iri}") from e
if not isinstance(domain_list, list):
raise ValueError(
f"Property {property_iri} data must be a list, got {type(domain_list)}"
)
sanitized_data[property_iri] = []
for domain_instance in domain_list:
if isinstance(domain_instance, Node):
# Already a Node, use as is
sanitized_data[property_iri].append(domain_instance)
elif isinstance(domain_instance, dict):
dict_id = id(domain_instance)
if known_nodes is not None and dict_id in known_nodes:
# Reuse existing Node to avoid cycles
node = known_nodes[dict_id]
else:
# Convert dict to Node
node = Node.from_sanitize_data(
ogm=node.ogm,
data=domain_instance,
known_nodes=known_nodes,
)
sanitized_data[property_iri].append(node)
else:
sanitized_data[property_iri].append(domain_instance)
logger.debug(
f"Converted property {property_iri} with {len(sanitized_data[property_iri])} items to Node list"
)
return sanitized_data, node_id