Python: Introduces enums for RDF and SPARQL result formats

pull/670/head
Tpt 12 months ago committed by Thomas Tanon
parent d19947414e
commit ddf589ea14
  1. 11
      python/docs/io.rst
  2. 13
      python/docs/model.rst
  3. 17
      python/docs/sparql.rst
  4. 3
      python/docs/store.rst
  5. 11
      python/generate_stubs.py
  6. 336
      python/src/io.rs
  7. 2
      python/src/lib.rs
  8. 2
      python/src/model.rs
  9. 273
      python/src/sparql.rs
  10. 80
      python/src/store.rs
  11. 50
      python/tests/test_io.py
  12. 40
      python/tests/test_store.py

@ -1,14 +1,21 @@
RDF Parsing and Serialization RDF Parsing and Serialization
============================= =============================
.. py:currentmodule:: pyoxigraph
Oxigraph provides functions to parse and serialize RDF files: Oxigraph provides functions to parse and serialize RDF files:
Parsing Parsing
""""""" """""""
.. autofunction:: pyoxigraph.parse .. autofunction:: parse
Serialization Serialization
""""""""""""" """""""""""""
.. autofunction:: pyoxigraph.serialize .. autofunction:: serialize
Formats
"""""""
.. autoclass:: RdfFormat
:members:

@ -1,37 +1,38 @@
RDF Model RDF Model
========= =========
.. py:currentmodule:: pyoxigraph
Oxigraph provides python classes to represents basic RDF concepts: Oxigraph provides python classes to represents basic RDF concepts:
`IRIs <https://www.w3.org/TR/rdf11-concepts/#dfn-iri>`_ `IRIs <https://www.w3.org/TR/rdf11-concepts/#dfn-iri>`_
""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""
.. autoclass:: pyoxigraph.NamedNode .. autoclass:: NamedNode
:members: :members:
`Blank Nodes <https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node>`_ `Blank Nodes <https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node>`_
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.. autoclass:: pyoxigraph.BlankNode .. autoclass:: BlankNode
:members: :members:
`Literals <https://www.w3.org/TR/rdf11-concepts/#dfn-literal>`_ `Literals <https://www.w3.org/TR/rdf11-concepts/#dfn-literal>`_
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.. autoclass:: pyoxigraph.Literal .. autoclass:: Literal
:members: :members:
`Triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_ `Triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.. autoclass:: pyoxigraph.Triple .. autoclass:: Triple
:members: :members:
Quads (`triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_ in a `RDF dataset <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_) Quads (`triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_ in a `RDF dataset <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_)
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
.. autoclass:: pyoxigraph.Quad .. autoclass:: Quad
:members: :members:
.. autoclass:: pyoxigraph.DefaultGraph .. autoclass:: DefaultGraph
:members: :members:

@ -1,32 +1,33 @@
SPARQL utility objects SPARQL utility objects
============================= ======================
.. py:currentmodule:: pyoxigraph
Oxigraph provides also some utilities related to SPARQL queries: Oxigraph provides also some utilities related to SPARQL queries:
Variable Variable
"""""""" """"""""
.. autoclass:: pyoxigraph.Variable .. autoclass:: Variable
:members: :members:
``SELECT`` solutions ``SELECT`` solutions
"""""""""""""""""""" """"""""""""""""""""
.. autoclass:: pyoxigraph.QuerySolutions .. autoclass:: QuerySolutions
:members: :members:
.. autoclass:: pyoxigraph.QuerySolution .. autoclass:: QuerySolution
:members: :members:
``ASK`` results ``ASK`` results
""""""""""""""" """""""""""""""
.. autoclass:: pyoxigraph.QueryBoolean .. autoclass:: QueryBoolean
:members: :members:
``CONSTRUCT`` results ``CONSTRUCT`` results
""""""""""""""""""""" """""""""""""""""""""
.. autoclass:: pyoxigraph.QueryTriples .. autoclass:: QueryTriples
:members: :members:
Query results parsing Query results parsing
""""""""""""""""""""" """""""""""""""""""""
.. autoclass:: pyoxigraph.parse_query_results .. autofunction:: parse_query_results
.. autoclass:: QueryResultsFormat
:members: :members:

@ -1,5 +1,6 @@
RDF Store RDF Store
========= =========
.. py:currentmodule:: pyoxigraph
.. autoclass:: pyoxigraph.Store .. autoclass:: Store
:members: :members:

@ -141,6 +141,17 @@ def class_stubs(cls_name: str, cls_def: Any, element_path: List[str], types_to_i
simple=1, simple=1,
) )
) )
elif member_value is not None:
constants.append(
ast.AnnAssign(
target=ast.Name(id=member_name, ctx=ast.Store()),
annotation=concatenated_path_to_type(
member_value.__class__.__name__, element_path, types_to_import
),
value=ast.Ellipsis(),
simple=1,
)
)
else: else:
logging.warning(f"Unsupported member {member_name} of class {'.'.join(element_path)}") logging.warning(f"Unsupported member {member_name} of class {'.'.join(element_path)}")

@ -1,9 +1,8 @@
#![allow(clippy::needless_option_as_deref)] #![allow(clippy::needless_option_as_deref)]
use crate::model::{PyQuad, PyTriple}; use crate::model::{hash, PyQuad, PyTriple};
use oxigraph::io::{FromReadQuadReader, ParseError, RdfFormat, RdfParser, RdfSerializer}; use oxigraph::io::{FromReadQuadReader, ParseError, RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::QuadRef; use oxigraph::model::QuadRef;
use oxigraph::sparql::results::QueryResultsFormat;
use pyo3::exceptions::{PySyntaxError, PyValueError}; use pyo3::exceptions::{PySyntaxError, PyValueError};
use pyo3::intern; use pyo3::intern;
use pyo3::prelude::*; use pyo3::prelude::*;
@ -19,12 +18,12 @@ use std::sync::OnceLock;
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -32,8 +31,8 @@ use std::sync::OnceLock;
/// ///
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional /// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: RdfFormat or None, optional
/// :param path: The file path to read from. Replaces the ``input`` parameter. /// :param path: The file path to read from. Replaces the ``input`` parameter.
/// :type path: str or os.PathLike[str] or None, optional /// :type path: str or os.PathLike[str] or None, optional
/// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done. /// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done.
@ -48,13 +47,13 @@ use std::sync::OnceLock;
/// :raises SyntaxError: if the provided data is invalid. /// :raises SyntaxError: if the provided data is invalid.
/// :raises OSError: if a system error happens while reading the file. /// :raises OSError: if a system error happens while reading the file.
/// ///
/// >>> list(parse(input=b'<foo> <p> "1" .', format="text/turtle", base_iri="http://example.com/")) /// >>> list(parse(input=b'<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/"))
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<DefaultGraph>>] /// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<DefaultGraph>>]
#[pyfunction] #[pyfunction]
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, without_named_graphs = false, rename_blank_nodes = false))] #[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, without_named_graphs = false, rename_blank_nodes = false))]
pub fn parse( pub fn parse(
input: Option<PyReadableInput>, input: Option<PyReadableInput>,
format: Option<&str>, format: Option<PyRdfFormat>,
path: Option<PathBuf>, path: Option<PathBuf>,
base_iri: Option<&str>, base_iri: Option<&str>,
without_named_graphs: bool, without_named_graphs: bool,
@ -62,7 +61,7 @@ pub fn parse(
py: Python<'_>, py: Python<'_>,
) -> PyResult<PyObject> { ) -> PyResult<PyObject> {
let input = PyReadable::from_args(&path, input, py)?; let input = PyReadable::from_args(&path, input, py)?;
let format = parse_format(format, path.as_deref())?; let format = lookup_rdf_format(format, path.as_deref())?;
let mut parser = RdfParser::from_format(format); let mut parser = RdfParser::from_format(format);
if let Some(base_iri) = base_iri { if let Some(base_iri) = base_iri {
parser = parser parser = parser
@ -86,12 +85,12 @@ pub fn parse(
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `canonical <https://www.w3.org/TR/n-triples/#canonical-ntriples>`_ `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -101,31 +100,32 @@ pub fn parse(
/// :type input: collections.abc.Iterable[Triple] or collections.abc.Iterable[Quad] /// :type input: collections.abc.Iterable[Triple] or collections.abc.Iterable[Quad]
/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content. /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional /// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: RdfFormat or None, optional
/// :return: py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set. /// :return: :py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set.
/// :rtype: bytes or None /// :rtype: bytes or None
/// :raises ValueError: if the format is not supported. /// :raises ValueError: if the format is not supported.
/// :raises TypeError: if a triple is given during a quad format serialization or reverse. /// :raises TypeError: if a triple is given during a quad format serialization or reverse.
/// :raises OSError: if a system error happens while writing the file. /// :raises OSError: if a system error happens while writing the file.
/// ///
/// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], format="ttl") /// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], format=RdfFormat.TURTLE)
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
/// ///
/// >>> output = io.BytesIO() /// >>> output = io.BytesIO()
/// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], output, "text/turtle") /// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], output, RdfFormat.TURTLE)
/// >>> output.getvalue() /// >>> output.getvalue()
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
#[pyfunction] #[pyfunction]
#[pyo3(signature = (input, output = None, format = None))] #[pyo3(signature = (input, output = None, format = None))]
pub fn serialize<'a>( pub fn serialize<'a>(
input: &PyAny, input: &PyAny,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, format: Option<PyRdfFormat>,
py: Python<'a>, py: Python<'a>,
) -> PyResult<Option<&'a PyBytes>> { ) -> PyResult<Option<&'a PyBytes>> {
PyWritable::do_write( PyWritable::do_write(
|output, format| { |output, file_path| {
let format = lookup_rdf_format(format, file_path.as_deref())?;
let mut writer = RdfSerializer::from_format(format).serialize_to_write(output); let mut writer = RdfSerializer::from_format(format).serialize_to_write(output);
for i in input.iter()? { for i in input.iter()? {
let i = i?; let i = i?;
@ -145,7 +145,6 @@ pub fn serialize<'a>(
Ok(writer.finish()?) Ok(writer.finish()?)
}, },
output, output,
format,
py, py,
) )
} }
@ -174,6 +173,193 @@ impl PyQuadReader {
} }
} }
/// RDF serialization formats.
///
/// The following formats are supported:
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
#[pyclass(name = "RdfFormat", module = "pyoxigraph")]
#[derive(Clone)]
pub struct PyRdfFormat {
inner: RdfFormat,
}
#[pymethods]
impl PyRdfFormat {
/// `N3 <https://w3c.github.io/N3/spec/>`_
#[classattr]
const N3: Self = Self {
inner: RdfFormat::N3,
};
/// `N-Quads <https://www.w3.org/TR/n-quads/>`_
#[classattr]
const N_QUADS: Self = Self {
inner: RdfFormat::NQuads,
};
/// `N-Triples <https://www.w3.org/TR/n-triples/>`_
#[classattr]
const N_TRIPLES: Self = Self {
inner: RdfFormat::NTriples,
};
/// `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_
#[classattr]
const RDF_XML: Self = Self {
inner: RdfFormat::RdfXml,
};
/// `TriG <https://www.w3.org/TR/trig/>`_
#[classattr]
const TRIG: Self = Self {
inner: RdfFormat::TriG,
};
/// `Turtle <https://www.w3.org/TR/turtle/>`_
#[classattr]
const TURTLE: Self = Self {
inner: RdfFormat::Turtle,
};
/// :return: the format canonical IRI according to the `Unique URIs for file formats registry <https://www.w3.org/ns/formats/>`_.
/// :rtype: str
///
/// >>> RdfFormat.N_TRIPLES.iri
/// 'http://www.w3.org/ns/formats/N-Triples'
#[getter]
fn iri(&self) -> &'static str {
self.inner.iri()
}
/// :return: the format `IANA media type <https://tools.ietf.org/html/rfc2046>`_.
/// :rtype: str
///
/// >>> RdfFormat.N_TRIPLES.media_type
/// 'application/n-triples'
#[getter]
fn media_type(&self) -> &'static str {
self.inner.media_type()
}
/// :return: the format `IANA-registered <https://tools.ietf.org/html/rfc2046>`_ file extension.
/// :rtype: str
///
/// >>> RdfFormat.N_TRIPLES.file_extension
/// 'nt'
#[getter]
pub fn file_extension(&self) -> &'static str {
self.inner.file_extension()
}
/// :return: the format name.
/// :rtype: str
///
/// >>> RdfFormat.N_TRIPLES.name
/// 'N-Triples'
#[getter]
pub const fn name(&self) -> &'static str {
self.inner.name()
}
/// :return: if the formats supports `RDF datasets <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_ and not only `RDF graphs <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-graph>`_.
/// :rtype: bool
///
/// >>> RdfFormat.N_TRIPLES.supports_datasets
/// False
/// >>> RdfFormat.N_QUADS.supports_datasets
/// True
#[getter]
pub fn supports_datasets(&self) -> bool {
self.inner.supports_datasets()
}
/// :return: if the formats supports `RDF-star quoted triples <https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#dfn-quoted>`_.
/// :rtype: bool
///
/// >>> RdfFormat.N_TRIPLES.supports_rdf_star
/// True
/// >>> RdfFormat.RDF_XML.supports_rdf_star
/// False
#[getter]
pub const fn supports_rdf_star(&self) -> bool {
self.inner.supports_rdf_star()
}
/// Looks for a known format from a media type.
///
/// It supports some media type aliases.
/// For example, "application/xml" is going to return RDF/XML even if it is not its canonical media type.
///
/// :param media_type: the media type.
/// :type media_type: str
/// :return: :py:class:`RdfFormat` if the media type is known or :py:const:`None` if not.
/// :rtype: RdfFormat or None
///
/// >>> RdfFormat.from_media_type("text/turtle; charset=utf-8")
/// <RdfFormat Turtle>
#[staticmethod]
pub fn from_media_type(media_type: &str) -> Option<Self> {
Some(Self {
inner: RdfFormat::from_media_type(media_type)?,
})
}
/// Looks for a known format from an extension.
///
/// It supports some aliases.
///
/// :param extension: the extension.
/// :type extension: str
/// :return: :py:class:`RdfFormat` if the extension is known or :py:const:`None` if not.
/// :rtype: RdfFormat or None
///
/// >>> RdfFormat.from_extension("nt")
/// <RdfFormat N-Triples>
#[staticmethod]
pub fn from_extension(extension: &str) -> Option<Self> {
Some(Self {
inner: RdfFormat::from_extension(extension)?,
})
}
fn __str__(&self) -> &'static str {
self.inner.name()
}
fn __repr__(&self) -> String {
format!("<RdfFormat {}>", self.inner.name())
}
fn __hash__(&self) -> u64 {
hash(&self.inner)
}
fn __eq__(&self, other: &Self) -> bool {
self.inner == other.inner
}
fn __ne__(&self, other: &Self) -> bool {
self.inner != other.inner
}
/// :rtype: RdfFormat
fn __copy__(slf: PyRef<'_, Self>) -> PyRef<Self> {
slf
}
/// :type memo: typing.Any
/// :rtype: RdfFormat
#[allow(unused_variables)]
fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> {
slf
}
}
pub enum PyReadable { pub enum PyReadable {
Bytes(Cursor<Vec<u8>>), Bytes(Cursor<Vec<u8>>),
Io(PyIo), Io(PyIo),
@ -233,24 +419,20 @@ pub enum PyWritable {
} }
impl PyWritable { impl PyWritable {
pub fn do_write<'a, F: Format>( pub fn do_write(
write: impl FnOnce(BufWriter<Self>, F) -> PyResult<BufWriter<Self>>, write: impl FnOnce(BufWriter<Self>, Option<PathBuf>) -> PyResult<BufWriter<Self>>,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, py: Python<'_>,
py: Python<'a>, ) -> PyResult<Option<&PyBytes>> {
) -> PyResult<Option<&'a PyBytes>> { let (output, file_path) = match output {
let file_path = output.and_then(|output| output.extract::<PathBuf>().ok()); Some(PyWritableOutput::Path(file_path)) => (
let format = parse_format::<F>(format, file_path.as_deref())?; Self::File(py.allow_threads(|| File::create(&file_path))?),
let output = if let Some(output) = output { Some(file_path),
if let Some(file_path) = &file_path { ),
Self::File(py.allow_threads(|| File::create(file_path))?) Some(PyWritableOutput::Io(object)) => (Self::Io(PyIo(object)), None),
} else { None => (Self::Bytes(Vec::new()), None),
Self::Io(PyIo(output.into()))
}
} else {
PyWritable::Bytes(Vec::new())
}; };
let writer = write(BufWriter::new(output), format)?; let writer = write(BufWriter::new(output), file_path)?;
py.allow_threads(|| writer.into_inner())?.close(py) py.allow_threads(|| writer.into_inner())?.close(py)
} }
@ -290,6 +472,12 @@ impl Write for PyWritable {
} }
} }
#[derive(FromPyObject)]
pub enum PyWritableOutput {
Path(PathBuf),
Io(PyObject),
}
pub struct PyIo(PyObject); pub struct PyIo(PyObject);
impl Read for PyIo { impl Read for PyIo {
@ -331,57 +519,23 @@ impl Write for PyIo {
} }
} }
pub trait Format: Sized { pub fn lookup_rdf_format(format: Option<PyRdfFormat>, path: Option<&Path>) -> PyResult<RdfFormat> {
fn from_media_type(media_type: &str) -> Option<Self>; if let Some(format) = format {
fn from_extension(extension: &str) -> Option<Self>; return Ok(format.inner);
}
impl Format for RdfFormat {
fn from_media_type(media_type: &str) -> Option<Self> {
Self::from_media_type(media_type)
}
fn from_extension(extension: &str) -> Option<Self> {
Self::from_extension(extension)
}
}
impl Format for QueryResultsFormat {
fn from_media_type(media_type: &str) -> Option<Self> {
Self::from_media_type(media_type)
} }
let Some(path) = path else {
fn from_extension(extension: &str) -> Option<Self> {
Self::from_extension(extension)
}
}
pub fn parse_format<F: Format>(format: Option<&str>, path: Option<&Path>) -> PyResult<F> {
let format = if let Some(format) = format {
format
} else if let Some(path) = path {
if let Some(ext) = path.extension().and_then(OsStr::to_str) {
ext
} else {
return Err(PyValueError::new_err(format!(
"The file name {} has no extension to guess a file format from",
path.display()
)));
}
} else {
return Err(PyValueError::new_err( return Err(PyValueError::new_err(
"The format parameter is required when a file path is not given", "The format parameter is required when a file path is not given",
)); ));
}; };
if format.contains('/') { let Some(ext) = path.extension().and_then(OsStr::to_str) else {
F::from_media_type(format).ok_or_else(|| { return Err(PyValueError::new_err(format!(
PyValueError::new_err(format!("Not supported RDF format media type: {format}")) "The file name {} has no extension to guess a file format from",
}) path.display()
} else { )));
F::from_extension(format).ok_or_else(|| { };
PyValueError::new_err(format!("Not supported RDF format extension: {format}")) RdfFormat::from_extension(ext)
}) .ok_or_else(|| PyValueError::new_err(format!("Not supported RDF format extension: {ext}")))
}
} }
pub fn map_parse_error(error: ParseError, file_path: Option<PathBuf>) -> PyErr { pub fn map_parse_error(error: ParseError, file_path: Option<PathBuf>) -> PyErr {

@ -35,6 +35,8 @@ fn pyoxigraph(_py: Python<'_>, module: &PyModule) -> PyResult<()> {
module.add_class::<PyQuerySolution>()?; module.add_class::<PyQuerySolution>()?;
module.add_class::<PyQueryBoolean>()?; module.add_class::<PyQueryBoolean>()?;
module.add_class::<PyQueryTriples>()?; module.add_class::<PyQueryTriples>()?;
module.add_class::<PyRdfFormat>()?;
module.add_class::<PyQueryResultsFormat>()?;
module.add_wrapped(wrap_pyfunction!(parse))?; module.add_wrapped(wrap_pyfunction!(parse))?;
module.add_wrapped(wrap_pyfunction!(parse_query_results))?; module.add_wrapped(wrap_pyfunction!(parse_query_results))?;
module.add_wrapped(wrap_pyfunction!(serialize))?; module.add_wrapped(wrap_pyfunction!(serialize))?;

@ -1276,7 +1276,7 @@ fn eq_compare_other_type(op: CompareOp) -> PyResult<bool> {
} }
} }
fn hash(t: &impl Hash) -> u64 { pub(crate) fn hash(t: &impl Hash) -> u64 {
let mut s = DefaultHasher::new(); let mut s = DefaultHasher::new();
t.hash(&mut s); t.hash(&mut s);
s.finish() s.finish()

@ -4,8 +4,8 @@ use crate::store::map_storage_error;
use oxigraph::io::RdfSerializer; use oxigraph::io::RdfSerializer;
use oxigraph::model::Term; use oxigraph::model::Term;
use oxigraph::sparql::results::{ use oxigraph::sparql::results::{
FromReadQueryResultsReader, FromReadSolutionsReader, ParseError, QueryResultsParser, FromReadQueryResultsReader, FromReadSolutionsReader, ParseError, QueryResultsFormat,
QueryResultsSerializer, QueryResultsParser, QueryResultsSerializer,
}; };
use oxigraph::sparql::{ use oxigraph::sparql::{
EvaluationError, Query, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter, EvaluationError, Query, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter,
@ -15,8 +15,9 @@ use pyo3::basic::CompareOp;
use pyo3::exceptions::{PyRuntimeError, PySyntaxError, PyValueError}; use pyo3::exceptions::{PyRuntimeError, PySyntaxError, PyValueError};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyBytes; use pyo3::types::PyBytes;
use std::ffi::OsStr;
use std::io; use std::io;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::vec::IntoIter; use std::vec::IntoIter;
pub fn parse_query( pub fn parse_query(
@ -214,18 +215,18 @@ impl PyQuerySolutions {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (``application/sparql-results+xml`` or ``srx``) /// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (``application/sparql-results+json`` or ``srj``) /// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/csv`` or ``csv``) /// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/tab-separated-values`` or ``tsv``) /// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_. /// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
/// ///
/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content. /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional /// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
/// :param format: the format of the query results serialization using a media type like ``text/csv`` or an extension like `csv`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: QueryResultsFormat or None, optional
/// :rtype: bytes or None /// :rtype: bytes or None
/// :raises ValueError: if the format is not supported. /// :raises ValueError: if the format is not supported.
/// :raises OSError: if a system error happens while writing the file. /// :raises OSError: if a system error happens while writing the file.
@ -233,17 +234,18 @@ impl PyQuerySolutions {
/// >>> store = Store() /// >>> store = Store()
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> results = store.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") /// >>> results = store.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
/// >>> results.serialize(format="json") /// >>> results.serialize(format=QueryResultsFormat.JSON)
/// b'{"head":{"vars":["s","p","o"]},"results":{"bindings":[{"s":{"type":"uri","value":"http://example.com"},"p":{"type":"uri","value":"http://example.com/p"},"o":{"type":"literal","value":"1"}}]}}' /// b'{"head":{"vars":["s","p","o"]},"results":{"bindings":[{"s":{"type":"uri","value":"http://example.com"},"p":{"type":"uri","value":"http://example.com/p"},"o":{"type":"literal","value":"1"}}]}}'
#[pyo3(signature = (output = None, /, format = None))] #[pyo3(signature = (output = None, format = None))]
fn serialize<'a>( fn serialize<'a>(
&mut self, &mut self,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, format: Option<PyQueryResultsFormat>,
py: Python<'a>, py: Python<'a>,
) -> PyResult<Option<&'a PyBytes>> { ) -> PyResult<Option<&'a PyBytes>> {
PyWritable::do_write( PyWritable::do_write(
|output, format| { |output, file_path| {
let format = lookup_query_results_format(format, file_path.as_deref())?;
let mut writer = QueryResultsSerializer::from_format(format) let mut writer = QueryResultsSerializer::from_format(format)
.serialize_solutions_to_write( .serialize_solutions_to_write(
output, output,
@ -272,7 +274,6 @@ impl PyQuerySolutions {
Ok(writer.finish()?) Ok(writer.finish()?)
}, },
output, output,
format,
py, py,
) )
} }
@ -314,18 +315,18 @@ impl PyQueryBoolean {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (``application/sparql-results+xml`` or ``srx``) /// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (``application/sparql-results+json`` or ``srj``) /// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/csv`` or ``csv``) /// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/tab-separated-values`` or ``tsv``) /// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_. /// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
/// ///
/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content. /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional /// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
/// :param format: the format of the query results serialization using a media type like ``text/csv`` or an extension like `csv`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: QueryResultsFormat or None, optional
/// :rtype: bytes or None /// :rtype: bytes or None
/// :raises ValueError: if the format is not supported. /// :raises ValueError: if the format is not supported.
/// :raises OSError: if a system error happens while writing the file. /// :raises OSError: if a system error happens while writing the file.
@ -333,24 +334,24 @@ impl PyQueryBoolean {
/// >>> store = Store() /// >>> store = Store()
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> results = store.query("ASK { ?s ?p ?o }") /// >>> results = store.query("ASK { ?s ?p ?o }")
/// >>> results.serialize(format="json") /// >>> results.serialize(format=QueryResultsFormat.JSON)
/// b'{"head":{},"boolean":true}' /// b'{"head":{},"boolean":true}'
#[pyo3(signature = (output = None, /, format = None))] #[pyo3(signature = (output = None, format = None))]
fn serialize<'a>( fn serialize<'a>(
&mut self, &mut self,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, format: Option<PyQueryResultsFormat>,
py: Python<'a>, py: Python<'a>,
) -> PyResult<Option<&'a PyBytes>> { ) -> PyResult<Option<&'a PyBytes>> {
PyWritable::do_write( PyWritable::do_write(
|output, format| { |output, file_path| {
let format = lookup_query_results_format(format, file_path.as_deref())?;
py.allow_threads(|| { py.allow_threads(|| {
Ok(QueryResultsSerializer::from_format(format) Ok(QueryResultsSerializer::from_format(format)
.serialize_boolean_to_write(output, self.inner)?) .serialize_boolean_to_write(output, self.inner)?)
}) })
}, },
output, output,
format,
py, py,
) )
} }
@ -389,12 +390,12 @@ impl PyQueryTriples {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `canonical <https://www.w3.org/TR/n-triples/#canonical-ntriples>`_ `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -402,8 +403,8 @@ impl PyQueryTriples {
/// ///
/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content. /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional /// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: RdfFormat or None, optional
/// :rtype: bytes or None /// :rtype: bytes or None
/// :raises ValueError: if the format is not supported. /// :raises ValueError: if the format is not supported.
/// :raises OSError: if a system error happens while writing the file. /// :raises OSError: if a system error happens while writing the file.
@ -411,17 +412,18 @@ impl PyQueryTriples {
/// >>> store = Store() /// >>> store = Store()
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") /// >>> results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }")
/// >>> results.serialize(format="nt") /// >>> results.serialize(format=RdfFormat.N_TRIPLES)
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
#[pyo3(signature = (output = None, /, format = None))] #[pyo3(signature = (output = None, format = None))]
fn serialize<'a>( fn serialize<'a>(
&mut self, &mut self,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, format: Option<PyRdfFormat>,
py: Python<'a>, py: Python<'a>,
) -> PyResult<Option<&'a PyBytes>> { ) -> PyResult<Option<&'a PyBytes>> {
PyWritable::do_write( PyWritable::do_write(
|output, format| { |output, file_path| {
let format = lookup_rdf_format(format, file_path.as_deref())?;
let mut writer = RdfSerializer::from_format(format).serialize_to_write(output); let mut writer = RdfSerializer::from_format(format).serialize_to_write(output);
for triple in &mut self.inner { for triple in &mut self.inner {
writer.write_triple(&triple.map_err(map_evaluation_error)?)?; writer.write_triple(&triple.map_err(map_evaluation_error)?)?;
@ -429,7 +431,6 @@ impl PyQueryTriples {
Ok(writer.finish()?) Ok(writer.finish()?)
}, },
output, output,
format,
py, py,
) )
} }
@ -450,18 +451,17 @@ impl PyQueryTriples {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (``application/sparql-results+xml`` or ``srx``) /// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (``application/sparql-results+json`` or ``srj``) /// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/csv`` or ``csv``) /// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (``text/tab-separated-values`` or ``tsv``)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_. /// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
/// ///
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional /// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: QueryResultsFormat or None, optional
/// :param path: The file path to read from. Replaces the ``input`` parameter. /// :param path: The file path to read from. Replaces the ``input`` parameter.
/// :type path: str or os.PathLike[str] or None, optional /// :type path: str or os.PathLike[str] or None, optional
/// :return: an iterator of :py:class:`QuerySolution` or a :py:class:`bool`. /// :return: an iterator of :py:class:`QuerySolution` or a :py:class:`bool`.
@ -470,21 +470,21 @@ impl PyQueryTriples {
/// :raises SyntaxError: if the provided data is invalid. /// :raises SyntaxError: if the provided data is invalid.
/// :raises OSError: if a system error happens while reading the file. /// :raises OSError: if a system error happens while reading the file.
/// ///
/// >>> list(parse_query_results('?s\t?p\t?o\n<http://example.com/s>\t<http://example.com/s>\t1\n', "text/tsv")) /// >>> list(parse_query_results('?s\t?p\t?o\n<http://example.com/s>\t<http://example.com/s>\t1\n', QueryResultsFormat.TSV))
/// [<QuerySolution s=<NamedNode value=http://example.com/s> p=<NamedNode value=http://example.com/s> o=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#integer>>>] /// [<QuerySolution s=<NamedNode value=http://example.com/s> p=<NamedNode value=http://example.com/s> o=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#integer>>>]
/// ///
/// >>> parse_query_results('{"head":{},"boolean":true}', "application/sparql-results+json") /// >>> parse_query_results('{"head":{},"boolean":true}', QueryResultsFormat.JSON)
/// <QueryBoolean true> /// <QueryBoolean true>
#[pyfunction] #[pyfunction]
#[pyo3(signature = (input = None, format = None, *, path = None))] #[pyo3(signature = (input = None, format = None, *, path = None))]
pub fn parse_query_results( pub fn parse_query_results(
input: Option<PyReadableInput>, input: Option<PyReadableInput>,
format: Option<&str>, format: Option<PyQueryResultsFormat>,
path: Option<PathBuf>, path: Option<PathBuf>,
py: Python<'_>, py: Python<'_>,
) -> PyResult<PyObject> { ) -> PyResult<PyObject> {
let input = PyReadable::from_args(&path, input, py)?; let input = PyReadable::from_args(&path, input, py)?;
let format = parse_format(format, path.as_deref())?; let format = lookup_query_results_format(format, path.as_deref())?;
let results = QueryResultsParser::from_format(format) let results = QueryResultsParser::from_format(format)
.parse_read(input) .parse_read(input)
.map_err(|e| map_query_results_parse_error(e, path.clone()))?; .map_err(|e| map_query_results_parse_error(e, path.clone()))?;
@ -500,6 +500,177 @@ pub fn parse_query_results(
}) })
} }
/// `SPARQL query <https://www.w3.org/TR/sparql11-query/>`_ results serialization formats.
///
/// The following formats are supported:
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
#[pyclass(name = "QueryResultsFormat", module = "pyoxigraph")]
#[derive(Clone)]
pub struct PyQueryResultsFormat {
inner: QueryResultsFormat,
}
#[pymethods]
impl PyQueryResultsFormat {
/// `SPARQL Query Results XML Format <https://www.w3.org/TR/rdf-sparql-XMLres/>`_
#[classattr]
const XML: Self = Self {
inner: QueryResultsFormat::Xml,
};
/// `SPARQL Query Results JSON Format <https://www.w3.org/TR/sparql11-results-json/>`_
#[classattr]
const JSON: Self = Self {
inner: QueryResultsFormat::Json,
};
/// `SPARQL Query Results CSV Format <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_
#[classattr]
const CSV: Self = Self {
inner: QueryResultsFormat::Csv,
};
/// `SPARQL Query Results TSV Format <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_
#[classattr]
const TSV: Self = Self {
inner: QueryResultsFormat::Tsv,
};
/// :return: the format canonical IRI according to the `Unique URIs for file formats registry <https://www.w3.org/ns/formats/>`_.
/// :rtype: str
///
/// >>> QueryResultsFormat.JSON.iri
/// 'http://www.w3.org/ns/formats/SPARQL_Results_JSON'
#[getter]
fn iri(&self) -> &'static str {
self.inner.iri()
}
/// :return: the format `IANA media type <https://tools.ietf.org/html/rfc2046>`_.
/// :rtype: str
///
/// >>> QueryResultsFormat.JSON.media_type
/// 'application/sparql-results+json'
#[getter]
fn media_type(&self) -> &'static str {
self.inner.media_type()
}
/// :return: the format `IANA-registered <https://tools.ietf.org/html/rfc2046>`_ file extension.
/// :rtype: str
///
/// >>> QueryResultsFormat.JSON.file_extension
/// 'srj'
#[getter]
fn file_extension(&self) -> &'static str {
self.inner.file_extension()
}
/// :return: the format name.
/// :rtype: str
///
/// >>> QueryResultsFormat.JSON.name
/// 'SPARQL Results in JSON'
#[getter]
pub const fn name(&self) -> &'static str {
self.inner.name()
}
/// Looks for a known format from a media type.
///
/// It supports some media type aliases.
/// For example, "application/xml" is going to return :py:const:`QueryResultsFormat.XML` even if it is not its canonical media type.
///
/// :param media_type: the media type.
/// :type media_type: str
/// :return: :py:class:`QueryResultsFormat` if the media type is known or :py:const:`None` if not.
/// :rtype: QueryResultsFormat or None
///
/// >>> QueryResultsFormat.from_media_type("application/sparql-results+json; charset=utf-8")
/// <QueryResultsFormat SPARQL Results in JSON>
#[staticmethod]
fn from_media_type(media_type: &str) -> Option<Self> {
Some(Self {
inner: QueryResultsFormat::from_media_type(media_type)?,
})
}
/// Looks for a known format from an extension.
///
/// It supports some aliases.
///
/// :param extension: the extension.
/// :type extension: str
/// :return: :py:class:`QueryResultsFormat` if the extension is known or :py:const:`None` if not.
/// :rtype: QueryResultsFormat or None
///
/// >>> QueryResultsFormat.from_extension("json")
/// <QueryResultsFormat SPARQL Results in JSON>
#[staticmethod]
fn from_extension(extension: &str) -> Option<Self> {
Some(Self {
inner: QueryResultsFormat::from_extension(extension)?,
})
}
fn __str__(&self) -> &'static str {
self.inner.name()
}
fn __repr__(&self) -> String {
format!("<QueryResultsFormat {}>", self.inner.name())
}
fn __hash__(&self) -> u64 {
hash(&self.inner)
}
fn __eq__(&self, other: &Self) -> bool {
self.inner == other.inner
}
fn __ne__(&self, other: &Self) -> bool {
self.inner != other.inner
}
/// :rtype: QueryResultsFormat
fn __copy__(slf: PyRef<'_, Self>) -> PyRef<Self> {
slf
}
/// :type memo: typing.Any
/// :rtype: QueryResultsFormat
#[allow(unused_variables)]
fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> {
slf
}
}
pub fn lookup_query_results_format(
format: Option<PyQueryResultsFormat>,
path: Option<&Path>,
) -> PyResult<QueryResultsFormat> {
if let Some(format) = format {
return Ok(format.inner);
}
let Some(path) = path else {
return Err(PyValueError::new_err(
"The format parameter is required when a file path is not given",
));
};
let Some(ext) = path.extension().and_then(OsStr::to_str) else {
return Err(PyValueError::new_err(format!(
"The file name {} has no extension to guess a file format from",
path.display()
)));
};
QueryResultsFormat::from_extension(ext)
.ok_or_else(|| PyValueError::new_err(format!("Not supported RDF format extension: {ext}")))
}
pub fn map_evaluation_error(error: EvaluationError) -> PyErr { pub fn map_evaluation_error(error: EvaluationError) -> PyErr {
match error { match error {
EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()), EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()),

@ -1,11 +1,11 @@
#![allow(clippy::needless_option_as_deref)] #![allow(clippy::needless_option_as_deref)]
use crate::io::{ use crate::io::{
allow_threads_unsafe, map_parse_error, parse_format, PyReadable, PyReadableInput, PyWritable, allow_threads_unsafe, lookup_rdf_format, map_parse_error, PyRdfFormat, PyReadable,
PyReadableInput, PyWritable, PyWritableOutput,
}; };
use crate::model::*; use crate::model::*;
use crate::sparql::*; use crate::sparql::*;
use oxigraph::io::RdfFormat;
use oxigraph::model::{GraphName, GraphNameRef}; use oxigraph::model::{GraphName, GraphNameRef};
use oxigraph::sparql::Update; use oxigraph::sparql::Update;
use oxigraph::store::{self, LoaderError, SerializerError, StorageError, Store}; use oxigraph::store::{self, LoaderError, SerializerError, StorageError, Store};
@ -351,12 +351,12 @@ impl PyStore {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -364,8 +364,8 @@ impl PyStore {
/// ///
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional /// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: RdfFormat or None, optional
/// :param path: The file path to read from. Replaces the ``input`` parameter. /// :param path: The file path to read from. Replaces the ``input`` parameter.
/// :type path: str or os.PathLike[str] or None, optional /// :type path: str or os.PathLike[str] or None, optional
/// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done. /// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done.
@ -378,14 +378,14 @@ impl PyStore {
/// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file. /// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file.
/// ///
/// >>> store = Store() /// >>> store = Store()
/// >>> store.load(input='<foo> <p> "1" .', format="text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> store.load(input='<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
/// >>> list(store) /// >>> list(store)
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>] /// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))] #[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))]
fn load( fn load(
&self, &self,
input: Option<PyReadableInput>, input: Option<PyReadableInput>,
format: Option<&str>, format: Option<PyRdfFormat>,
path: Option<PathBuf>, path: Option<PathBuf>,
base_iri: Option<&str>, base_iri: Option<&str>,
to_graph: Option<&PyAny>, to_graph: Option<&PyAny>,
@ -397,7 +397,7 @@ impl PyStore {
None None
}; };
let input = PyReadable::from_args(&path, input, py)?; let input = PyReadable::from_args(&path, input, py)?;
let format: RdfFormat = parse_format(format, path.as_deref())?; let format = lookup_rdf_format(format, path.as_deref())?;
py.allow_threads(|| { py.allow_threads(|| {
if let Some(to_graph_name) = to_graph_name { if let Some(to_graph_name) = to_graph_name {
self.inner self.inner
@ -418,12 +418,12 @@ impl PyStore {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -431,7 +431,7 @@ impl PyStore {
/// ///
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional /// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: str or None, optional
/// :param path: The file path to read from. Replaces the ``input`` parameter. /// :param path: The file path to read from. Replaces the ``input`` parameter.
/// :type path: str or os.PathLike[str] or None, optional /// :type path: str or os.PathLike[str] or None, optional
@ -445,14 +445,14 @@ impl PyStore {
/// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file. /// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file.
/// ///
/// >>> store = Store() /// >>> store = Store()
/// >>> store.bulk_load(input=b'<foo> <p> "1" .', format="text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> store.bulk_load(input=b'<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
/// >>> list(store) /// >>> list(store)
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>] /// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))] #[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))]
fn bulk_load( fn bulk_load(
&self, &self,
input: Option<PyReadableInput>, input: Option<PyReadableInput>,
format: Option<&str>, format: Option<PyRdfFormat>,
path: Option<PathBuf>, path: Option<PathBuf>,
base_iri: Option<&str>, base_iri: Option<&str>,
to_graph: Option<&PyAny>, to_graph: Option<&PyAny>,
@ -464,7 +464,7 @@ impl PyStore {
None None
}; };
let input = PyReadable::from_args(&path, input, py)?; let input = PyReadable::from_args(&path, input, py)?;
let format: RdfFormat = parse_format(format, path.as_deref())?; let format = lookup_rdf_format(format, path.as_deref())?;
py.allow_threads(|| { py.allow_threads(|| {
if let Some(to_graph_name) = to_graph_name { if let Some(to_graph_name) = to_graph_name {
self.inner self.inner
@ -483,12 +483,12 @@ impl PyStore {
/// ///
/// It currently supports the following formats: /// It currently supports the following formats:
/// ///
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (``application/n-triples`` or ``nt``) /// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (``application/n-quads`` or ``nq``) /// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (``text/turtle`` or ``ttl``) /// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
/// * `TriG <https://www.w3.org/TR/trig/>`_ (``application/trig`` or ``trig``) /// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (``text/n3`` or ``n3``) /// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (``application/rdf+xml`` or ``rdf``) /// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
/// ///
/// It supports also some media type and extension aliases. /// It supports also some media type and extension aliases.
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_ /// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
@ -496,31 +496,31 @@ impl PyStore {
/// ///
/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content. /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional /// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
/// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. If :py:const:`None`, the format is guessed from the file name extension. /// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
/// :type format: str or None, optional /// :type format: RdfFormat or None, optional
/// :param from_graph: the store graph from which dump the triples. Required if the serialization format does not support named graphs. If it does supports named graphs the full dataset is written. /// :param from_graph: the store graph from which dump the triples. Required if the serialization format does not support named graphs. If it does supports named graphs the full dataset is written.
/// :type from_graph: NamedNode or BlankNode or DefaultGraph or None, optional /// :type from_graph: NamedNode or BlankNode or DefaultGraph or None, optional
/// :return: py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set. /// :return: :py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set.
/// :rtype: bytes or None /// :rtype: bytes or None
/// :raises ValueError: if the format is not supported or the `from_graph` parameter is not given with a syntax not supporting named graphs. /// :raises ValueError: if the format is not supported or the `from_graph` parameter is not given with a syntax not supporting named graphs.
/// :raises OSError: if an error happens during a quad lookup or file writing. /// :raises OSError: if an error happens during a quad lookup or file writing.
/// ///
/// >>> store = Store() /// >>> store = Store()
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> store.dump(format="trig") /// >>> store.dump(format=RdfFormat.TRIG)
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
/// ///
/// >>> store = Store() /// >>> store = Store()
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
/// >>> output = io.BytesIO() /// >>> output = io.BytesIO()
/// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g")) /// >>> store.dump(output, RdfFormat.TURTLE, from_graph=NamedNode("http://example.com/g"))
/// >>> output.getvalue() /// >>> output.getvalue()
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
#[pyo3(signature = (output = None, /, format = None, *, from_graph = None))] #[pyo3(signature = (output = None, format = None, *, from_graph = None))]
fn dump<'a>( fn dump<'a>(
&self, &self,
output: Option<&PyAny>, output: Option<PyWritableOutput>,
format: Option<&str>, format: Option<PyRdfFormat>,
from_graph: Option<&PyAny>, from_graph: Option<&PyAny>,
py: Python<'a>, py: Python<'a>,
) -> PyResult<Option<&'a PyBytes>> { ) -> PyResult<Option<&'a PyBytes>> {
@ -529,9 +529,10 @@ impl PyStore {
} else { } else {
None None
}; };
PyWritable::do_write::<RdfFormat>( PyWritable::do_write(
|output, format| { |output, file_path| {
py.allow_threads(|| { py.allow_threads(|| {
let format = lookup_rdf_format(format, file_path.as_deref())?;
if let Some(from_graph_name) = &from_graph_name { if let Some(from_graph_name) = &from_graph_name {
self.inner.dump_graph(output, format, from_graph_name) self.inner.dump_graph(output, format, from_graph_name)
} else { } else {
@ -541,7 +542,6 @@ impl PyStore {
}) })
}, },
output, output,
format,
py, py,
) )
} }

@ -8,7 +8,9 @@ from pyoxigraph import (
NamedNode, NamedNode,
Quad, Quad,
QueryBoolean, QueryBoolean,
QueryResultsFormat,
QuerySolutions, QuerySolutions,
RdfFormat,
parse, parse,
parse_query_results, parse_query_results,
serialize, serialize,
@ -39,14 +41,14 @@ class TestParse(unittest.TestCase):
def test_parse_not_existing_file(self) -> None: def test_parse_not_existing_file(self) -> None:
with self.assertRaises(IOError) as _: with self.assertRaises(IOError) as _:
parse(path="/tmp/not-existing-oxigraph-file.ttl", format="text/turtle") parse(path="/tmp/not-existing-oxigraph-file.ttl", format=RdfFormat.TURTLE)
def test_parse_str(self) -> None: def test_parse_str(self) -> None:
self.assertEqual( self.assertEqual(
list( list(
parse( parse(
'<foo> <p> "éù" .', '<foo> <p> "éù" .',
"text/turtle", RdfFormat.TURTLE,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -58,7 +60,7 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
'<foo> <p> "éù" .'.encode(), '<foo> <p> "éù" .'.encode(),
"text/turtle", RdfFormat.TURTLE,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -70,7 +72,7 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
StringIO('<foo> <p> "éù" .'), StringIO('<foo> <p> "éù" .'),
"text/turtle", RdfFormat.TURTLE,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -82,7 +84,7 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
StringIO('<foo> <p> "éù" .\n' * 1024), StringIO('<foo> <p> "éù" .\n' * 1024),
"text/turtle", RdfFormat.TURTLE,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -94,7 +96,7 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
BytesIO('<foo> <p> "éù" .'.encode()), BytesIO('<foo> <p> "éù" .'.encode()),
"text/turtle", RdfFormat.TURTLE,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -103,14 +105,14 @@ class TestParse(unittest.TestCase):
def test_parse_io_error(self) -> None: def test_parse_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp:
list(parse(fp, "nt")) list(parse(fp, RdfFormat.N_TRIPLES))
def test_parse_quad(self) -> None: def test_parse_quad(self) -> None:
self.assertEqual( self.assertEqual(
list( list(
parse( parse(
'<g> { <foo> <p> "1" }', '<g> { <foo> <p> "1" }',
"application/trig", RdfFormat.TRIG,
base_iri="http://example.com/", base_iri="http://example.com/",
) )
), ),
@ -123,7 +125,7 @@ class TestParse(unittest.TestCase):
fp.write(b'<foo> "p" "1"') fp.write(b'<foo> "p" "1"')
fp.flush() fp.flush()
with self.assertRaises(SyntaxError) as ctx: with self.assertRaises(SyntaxError) as ctx:
list(parse(path=fp.name, format="text/turtle")) list(parse(path=fp.name, format=RdfFormat.TURTLE))
self.assertEqual(ctx.exception.filename, fp.name) self.assertEqual(ctx.exception.filename, fp.name)
self.assertEqual(ctx.exception.lineno, 2) self.assertEqual(ctx.exception.lineno, 2)
self.assertEqual(ctx.exception.offset, 7) self.assertEqual(ctx.exception.offset, 7)
@ -136,7 +138,7 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
'<g> { <foo> <p> "1" }', '<g> { <foo> <p> "1" }',
"application/trig", RdfFormat.TRIG,
base_iri="http://example.com/", base_iri="http://example.com/",
without_named_graphs=True, without_named_graphs=True,
) )
@ -147,14 +149,14 @@ class TestParse(unittest.TestCase):
list( list(
parse( parse(
'_:s <http://example.com/p> "o" .', '_:s <http://example.com/p> "o" .',
"application/n-triples", RdfFormat.N_TRIPLES,
rename_blank_nodes=True, rename_blank_nodes=True,
) )
), ),
list( list(
parse( parse(
'_:s <http://example.com/p> "o" .', '_:s <http://example.com/p> "o" .',
"application/n-triples", RdfFormat.N_TRIPLES,
rename_blank_nodes=True, rename_blank_nodes=True,
) )
), ),
@ -164,13 +166,13 @@ class TestParse(unittest.TestCase):
class TestSerialize(unittest.TestCase): class TestSerialize(unittest.TestCase):
def test_serialize_to_bytes(self) -> None: def test_serialize_to_bytes(self) -> None:
self.assertEqual( self.assertEqual(
(serialize([EXAMPLE_TRIPLE.triple], None, "text/turtle") or b"").decode(), (serialize([EXAMPLE_TRIPLE.triple], None, RdfFormat.TURTLE) or b"").decode(),
'<http://example.com/foo> <http://example.com/p> "éù" .\n', '<http://example.com/foo> <http://example.com/p> "éù" .\n',
) )
def test_serialize_to_bytes_io(self) -> None: def test_serialize_to_bytes_io(self) -> None:
output = BytesIO() output = BytesIO()
serialize([EXAMPLE_TRIPLE.triple], output, "text/turtle") serialize([EXAMPLE_TRIPLE.triple], output, RdfFormat.TURTLE)
self.assertEqual( self.assertEqual(
output.getvalue().decode(), output.getvalue().decode(),
'<http://example.com/foo> <http://example.com/p> "éù" .\n', '<http://example.com/foo> <http://example.com/p> "éù" .\n',
@ -186,11 +188,11 @@ class TestSerialize(unittest.TestCase):
def test_serialize_io_error(self) -> None: def test_serialize_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp:
serialize([EXAMPLE_TRIPLE], fp, "text/turtle") serialize([EXAMPLE_TRIPLE], fp, RdfFormat.TURTLE)
def test_serialize_quad(self) -> None: def test_serialize_quad(self) -> None:
output = BytesIO() output = BytesIO()
serialize([EXAMPLE_QUAD], output, "application/trig") serialize([EXAMPLE_QUAD], output, RdfFormat.TRIG)
self.assertEqual( self.assertEqual(
output.getvalue(), output.getvalue(),
b'<http://example.com/g> {\n\t<http://example.com/foo> <http://example.com/p> "1" .\n}\n', b'<http://example.com/g> {\n\t<http://example.com/foo> <http://example.com/p> "1" .\n}\n',
@ -210,38 +212,38 @@ class TestParseQuerySolutions(unittest.TestCase):
def test_parse_not_existing_file(self) -> None: def test_parse_not_existing_file(self) -> None:
with self.assertRaises(IOError) as _: with self.assertRaises(IOError) as _:
parse_query_results(path="/tmp/not-existing-oxigraph-file.ttl", format="application/json") parse_query_results(path="/tmp/not-existing-oxigraph-file.ttl", format=QueryResultsFormat.JSON)
def test_parse_str(self) -> None: def test_parse_str(self) -> None:
result = parse_query_results("true", "tsv") result = parse_query_results("true", QueryResultsFormat.TSV)
self.assertIsInstance(result, QueryBoolean) self.assertIsInstance(result, QueryBoolean)
self.assertTrue(result) self.assertTrue(result)
def test_parse_bytes(self) -> None: def test_parse_bytes(self) -> None:
result = parse_query_results(b"false", "tsv") result = parse_query_results(b"false", QueryResultsFormat.TSV)
self.assertIsInstance(result, QueryBoolean) self.assertIsInstance(result, QueryBoolean)
self.assertFalse(result) self.assertFalse(result)
def test_parse_str_io(self) -> None: def test_parse_str_io(self) -> None:
result = parse_query_results("true", "tsv") result = parse_query_results("true", QueryResultsFormat.TSV)
self.assertIsInstance(result, QueryBoolean) self.assertIsInstance(result, QueryBoolean)
self.assertTrue(result) self.assertTrue(result)
def test_parse_bytes_io(self) -> None: def test_parse_bytes_io(self) -> None:
result = parse_query_results(BytesIO(b"false"), "tsv") result = parse_query_results(BytesIO(b"false"), QueryResultsFormat.TSV)
self.assertIsInstance(result, QueryBoolean) self.assertIsInstance(result, QueryBoolean)
self.assertFalse(result) self.assertFalse(result)
def test_parse_io_error(self) -> None: def test_parse_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp:
parse_query_results(fp, "srx") parse_query_results(fp, QueryResultsFormat.XML)
def test_parse_syntax_error_json(self) -> None: def test_parse_syntax_error_json(self) -> None:
with NamedTemporaryFile() as fp: with NamedTemporaryFile() as fp:
fp.write(b"{]") fp.write(b"{]")
fp.flush() fp.flush()
with self.assertRaises(SyntaxError) as ctx: with self.assertRaises(SyntaxError) as ctx:
list(parse_query_results(path=fp.name, format="srj")) # type: ignore[arg-type] list(parse_query_results(path=fp.name, format=QueryResultsFormat.JSON)) # type: ignore[arg-type]
self.assertEqual(ctx.exception.filename, fp.name) self.assertEqual(ctx.exception.filename, fp.name)
self.assertEqual(ctx.exception.lineno, 1) self.assertEqual(ctx.exception.lineno, 1)
self.assertEqual(ctx.exception.offset, 2) self.assertEqual(ctx.exception.offset, 2)
@ -255,7 +257,7 @@ class TestParseQuerySolutions(unittest.TestCase):
fp.write(b"1\t<foo >\n") fp.write(b"1\t<foo >\n")
fp.flush() fp.flush()
with self.assertRaises(SyntaxError) as ctx: with self.assertRaises(SyntaxError) as ctx:
list(parse_query_results(path=fp.name, format="tsv")) # type: ignore[arg-type] list(parse_query_results(path=fp.name, format=QueryResultsFormat.TSV)) # type: ignore[arg-type]
self.assertEqual(ctx.exception.filename, fp.name) self.assertEqual(ctx.exception.filename, fp.name)
self.assertEqual(ctx.exception.lineno, 2) self.assertEqual(ctx.exception.lineno, 2)
self.assertEqual(ctx.exception.offset, 3) self.assertEqual(ctx.exception.offset, 3)

@ -9,9 +9,12 @@ from pyoxigraph import (
DefaultGraph, DefaultGraph,
NamedNode, NamedNode,
Quad, Quad,
QueryBoolean,
QueryResultsFormat,
QuerySolution, QuerySolution,
QuerySolutions, QuerySolutions,
QueryTriples, QueryTriples,
RdfFormat,
Store, Store,
Triple, Triple,
Variable, Variable,
@ -190,9 +193,10 @@ class TestStore(unittest.TestCase):
def test_select_query_dump(self) -> None: def test_select_query_dump(self) -> None:
store = Store() store = Store()
store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz))
results = store.query("SELECT ?s WHERE { ?s ?p ?o }") results: QuerySolutions = store.query("SELECT ?s WHERE { ?s ?p ?o }") # type: ignore[assignment]
self.assertIsInstance(results, QuerySolutions)
output = BytesIO() output = BytesIO()
results.serialize(output, "csv") results.serialize(output, QueryResultsFormat.CSV)
self.assertEqual( self.assertEqual(
output.getvalue().decode(), output.getvalue().decode(),
"s\r\nhttp://foo\r\n", "s\r\nhttp://foo\r\n",
@ -201,9 +205,10 @@ class TestStore(unittest.TestCase):
def test_ask_query_dump(self) -> None: def test_ask_query_dump(self) -> None:
store = Store() store = Store()
store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz))
results = store.query("ASK { ?s ?p ?o }") results: QueryBoolean = store.query("ASK { ?s ?p ?o }") # type: ignore[assignment]
self.assertIsInstance(results, QueryBoolean)
output = BytesIO() output = BytesIO()
results.serialize(output, "csv") results.serialize(output, QueryResultsFormat.CSV)
self.assertEqual( self.assertEqual(
output.getvalue().decode(), output.getvalue().decode(),
"true", "true",
@ -212,9 +217,10 @@ class TestStore(unittest.TestCase):
def test_construct_query_dump(self) -> None: def test_construct_query_dump(self) -> None:
store = Store() store = Store()
store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz))
results = store.query("CONSTRUCT WHERE { ?s ?p ?o }") results: QueryTriples = store.query("CONSTRUCT WHERE { ?s ?p ?o }") # type: ignore[assignment]
self.assertIsInstance(results, QueryTriples)
output = BytesIO() output = BytesIO()
results.serialize(output, "nt") results.serialize(output, RdfFormat.N_TRIPLES)
self.assertEqual( self.assertEqual(
output.getvalue().decode(), output.getvalue().decode(),
"<http://foo> <http://bar> <http://baz> .\n", "<http://foo> <http://bar> <http://baz> .\n",
@ -254,7 +260,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.load( store.load(
b"<http://foo> <http://bar> <http://baz> .", b"<http://foo> <http://bar> <http://baz> .",
"application/n-triples", RdfFormat.N_TRIPLES,
) )
self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())})
@ -262,7 +268,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.load( store.load(
"<http://foo> <http://bar> <http://baz> .", "<http://foo> <http://bar> <http://baz> .",
"application/n-triples", RdfFormat.N_TRIPLES,
to_graph=graph, to_graph=graph,
) )
self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)})
@ -271,7 +277,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.load( store.load(
BytesIO(b"<http://foo> <http://bar> <> ."), BytesIO(b"<http://foo> <http://bar> <> ."),
"text/turtle", RdfFormat.TURTLE,
base_iri="http://baz", base_iri="http://baz",
) )
self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())})
@ -280,7 +286,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.load( store.load(
StringIO("<http://foo> <http://bar> <http://baz> <http://graph>."), StringIO("<http://foo> <http://bar> <http://baz> <http://graph>."),
"nq", RdfFormat.N_QUADS,
) )
self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)})
@ -288,7 +294,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.load( store.load(
"<http://graph> { <http://foo> <http://bar> <> . }", "<http://graph> { <http://foo> <http://bar> <> . }",
"application/trig", RdfFormat.TRIG,
base_iri="http://baz", base_iri="http://baz",
) )
self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)})
@ -303,13 +309,13 @@ class TestStore(unittest.TestCase):
def test_load_with_io_error(self) -> None: def test_load_with_io_error(self) -> None:
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp:
Store().load(fp, "application/n-triples") Store().load(fp, RdfFormat.N_TRIPLES)
def test_dump_ntriples(self) -> None: def test_dump_ntriples(self) -> None:
store = Store() store = Store()
store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, baz, graph))
output = BytesIO() output = BytesIO()
store.dump(output, "application/n-triples", from_graph=graph) store.dump(output, RdfFormat.N_TRIPLES, from_graph=graph)
self.assertEqual( self.assertEqual(
output.getvalue(), output.getvalue(),
b"<http://foo> <http://bar> <http://baz> .\n", b"<http://foo> <http://bar> <http://baz> .\n",
@ -319,7 +325,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, baz, graph))
self.assertEqual( self.assertEqual(
store.dump(format="nq"), store.dump(format=RdfFormat.N_QUADS),
b"<http://foo> <http://bar> <http://baz> <http://graph> .\n", b"<http://foo> <http://bar> <http://baz> <http://graph> .\n",
) )
@ -328,7 +334,7 @@ class TestStore(unittest.TestCase):
store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, baz, graph))
store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz))
output = BytesIO() output = BytesIO()
store.dump(output, "application/trig") store.dump(output, RdfFormat.TRIG)
self.assertEqual( self.assertEqual(
output.getvalue(), output.getvalue(),
b"<http://foo> <http://bar> <http://baz> .\n" b"<http://foo> <http://bar> <http://baz> .\n"
@ -340,7 +346,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, baz, graph))
file_name = Path(fp.name) file_name = Path(fp.name)
store.dump(file_name, "nq") store.dump(file_name, RdfFormat.N_QUADS)
self.assertEqual( self.assertEqual(
file_name.read_text(), file_name.read_text(),
"<http://foo> <http://bar> <http://baz> <http://graph> .\n", "<http://foo> <http://bar> <http://baz> <http://graph> .\n",
@ -350,7 +356,7 @@ class TestStore(unittest.TestCase):
store = Store() store = Store()
store.add(Quad(foo, bar, bar)) store.add(Quad(foo, bar, bar))
with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp: with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp:
store.dump(fp, "application/trig") store.dump(fp, RdfFormat.TRIG)
def test_write_in_read(self) -> None: def test_write_in_read(self) -> None:
store = Store() store = Store()

Loading…
Cancel
Save