Source code for flpq_data.graphs.readwrite.rdf

"""Read (and write) a graph from (and to) RDF file."""

import logging
import pathlib
import urllib.parse
from typing import Any, Iterable, Iterator, Tuple, Union

import networkx as nx
import rdflib
from rdflib.term import Node as RdfNode

from flpq_data.graphs.readwrite.graph import iter_edges_from_graph

__all__ = [
    "iter_edges_from_rdf",
    "rdf_from_edges",
    "graph_from_rdf",
    "graph_to_rdf",
]

#: The IRI prefixes of the RDF encoding (the "RDF encoding" section of
#: ``docs/graphs/index.rst``).
_NODE_IRI_PREFIX = "urn:flpq:node:"
_LABEL_IRI_PREFIX = "urn:flpq:label:"


def _node_id(term: RdfNode) -> str:
    """Returns the node id behind an RDF endpoint term.

    The current encoding uses IRIs ``urn:flpq:node:<id>`` (the percent-encoded
    id is decoded); the legacy form used blank nodes, which the Turtle
    serializer writes as anonymous — their ids are opaque, so the full term
    string is returned.

    Parameters
    ----------
    term : Node
        An RDF endpoint (subject or object) term.

    Returns
    -------
    node_id : str
        The node id of the term.

    Raises
    ------
    ValueError
        If the term is neither a blank node nor a ``urn:flpq:node:`` IRI.
    """
    if isinstance(term, rdflib.BNode):
        return str(term)
    text = str(term)
    if not text.startswith(_NODE_IRI_PREFIX):
        raise ValueError(f"Unrecognized node term {term!r}")
    return urllib.parse.unquote(text[len(_NODE_IRI_PREFIX) :])


def _label(term: RdfNode) -> str:
    """Returns the edge label behind an RDF predicate term.

    The current encoding uses IRIs ``urn:flpq:label:<label>`` (the
    percent-encoded label is decoded); the legacy form used Literals.

    Parameters
    ----------
    term : Node
        An RDF predicate term.

    Returns
    -------
    label : str
        The edge label of the term.

    Raises
    ------
    ValueError
        If the term is neither a Literal nor a ``urn:flpq:label:`` IRI.
    """
    if isinstance(term, rdflib.Literal):
        return str(term)
    text = str(term)
    if not text.startswith(_LABEL_IRI_PREFIX):
        raise ValueError(f"Unrecognized predicate term {term!r}")
    return urllib.parse.unquote(text[len(_LABEL_IRI_PREFIX) :])


[docs] def iter_edges_from_rdf( path: Union[pathlib.Path, str], ) -> Iterator[Tuple[str, str, str]]: """Yields the edges of an RDF file as a stream. Reads both the current encoding (IRI nodes ``urn:flpq:node:<id>`` and IRI predicates ``urn:flpq:label:<label>``) and the legacy form written by older versions (blank-node endpoints and Literal predicates). rdflib has no streaming parser, so the file is materialized in memory. Parameters ---------- path : Union[Path, str] The path to the RDF file with which the edges will be created. Examples -------- >>> import pathlib, tempfile >>> d = pathlib.Path(tempfile.mkdtemp()) >>> p = d / "g.ttl" >>> _ = p.write_text( ... "<urn:flpq:node:0> <urn:flpq:label:a> <urn:flpq:node:1> .\\n" ... "<urn:flpq:node:1> <urn:flpq:label:b_5> <urn:flpq:node:2> .\\n" ... ) >>> sorted(iter_edges_from_rdf(p)) [('0', 'a', '1'), ('1', 'b_5', '2')] Returns ------- edges : Iterator[Tuple[str, str, str]] The ``(u, label, v)`` edge tuples (the order is not guaranteed — rdflib iterates in store order). """ # The format is always Turtle (the writer emits it); parse it explicitly # instead of guessing from the file extension (.rdf means RDF/XML). tmp = rdflib.Graph() tmp.parse(str(path), format="turtle") for subj, pred, obj in tmp: yield _node_id(subj), _label(pred), _node_id(obj) logging.info(f"Stream edges from {path=}")
def _iri(prefix: str, value: Any) -> str: """Returns the IRI ``prefix + value`` with the variable part encoded.""" return prefix + urllib.parse.quote(str(value), safe="")
[docs] def rdf_from_edges( edges: Iterable[Tuple[Any, str, Any]], path: Union[pathlib.Path, str] ) -> pathlib.Path: """Writes an edge stream to an RDF file. Emits valid RDF 1.1 Turtle — one triple per line with IRI nodes ``urn:flpq:node:<id>`` and IRI predicates ``urn:flpq:label:<label>``, the variable components percent-encoded (the "RDF encoding" section of ``docs/graphs/index.rst``). The file is written line by line without an in-memory RDF store, so the memory stays O(1) in the number of edges. Parameters ---------- edges : Iterable[Tuple[Any, str, Any]] The ``(u, label, v)`` edge tuples to write. path : Union[Path, str] The path to the RDF file where the edges will be saved. Examples -------- >>> import pathlib, tempfile >>> p = pathlib.Path(tempfile.mkdtemp()) / "g.ttl" >>> _ = rdf_from_edges([(0, "a b", 1)], p) >>> p.read_text().strip() '<urn:flpq:node:0> <urn:flpq:label:a%20b> <urn:flpq:node:1> .' Returns ------- path : Path Path to the RDF file where the edges will be saved. """ with open(path, "w") as f: for u, label, v in edges: f.write( f"<{_iri(_NODE_IRI_PREFIX, u)}> " f"<{_iri(_LABEL_IRI_PREFIX, label)}> " f"<{_iri(_NODE_IRI_PREFIX, v)}> .\n" ) dest = pathlib.Path(path).resolve() logging.info(f"Save edges to {dest=}") return dest
[docs] def graph_from_rdf(path: Union[pathlib.Path, str]) -> nx.MultiDiGraph: """Loads a graph from RDF file. Parameters ---------- path : Union[Path, str] The path to the RDF file with which the graph will be created. Examples -------- >>> from flpq_data import * >>> import pathlib, tempfile >>> d = pathlib.Path(tempfile.mkdtemp()) >>> g = graph_from_text(["0 a 1", "1 b 2"]) >>> path = graph_to_rdf(g, d / "g.ttl") >>> generations = graph_from_rdf(path) >>> generations.number_of_nodes() 3 >>> generations.number_of_edges() 2 Returns ------- g : MultiDiGraph Loaded graph. """ graph = nx.MultiDiGraph() for u, label, v in iter_edges_from_rdf(path): graph.add_edge(u, v, label=label) logging.info(f"Load {graph=} from {path=}") return graph
[docs] def graph_to_rdf( graph: nx.MultiDiGraph, path: Union[pathlib.Path, str] ) -> pathlib.Path: """Saves the ``graph`` to the RDF file by ``path``. The file holds valid RDF 1.1 Turtle — one triple per edge with IRI nodes and predicates (see :func:`rdf_from_edges` and the "RDF encoding" section of ``docs/graphs/index.rst``). Parameters ---------- graph : MultiDiGraph Graph to save. path : Union[Path, str] The path to the file where the graph will be saved. Examples -------- >>> from flpq_data import * >>> import pathlib, tempfile >>> d = pathlib.Path(tempfile.mkdtemp()) >>> g = graph_from_text(["0 a 1", "1 b 2"]) >>> path = graph_to_rdf(g, d / "g.ttl") Returns ------- path : Path Path to the file where the graph will be saved. """ dest = rdf_from_edges(iter_edges_from_graph(graph), path) logging.info(f"Save {graph=} to {dest=}") return dest