From 3726d2cbfc50a88d18caafc52ebcf6d14777d689 Mon Sep 17 00:00:00 2001 From: Tpt Date: Thu, 15 Apr 2021 12:15:01 +0200 Subject: [PATCH] Python Drops MemoryStore and renames SledStore to Store --- python/docs/index.rst | 5 +- python/docs/store.rst | 5 + python/docs/store/memory.rst | 5 - python/docs/store/sled.rst | 5 - python/src/io.rs | 13 +- python/src/lib.rs | 10 +- python/src/memory_store.rs | 470 ------------------------- python/src/sparql.rs | 10 +- python/src/{sled_store.rs => store.rs} | 94 +++-- python/src/store_utils.rs | 54 --- python/tests/test_store.py | 71 ++-- 11 files changed, 118 insertions(+), 624 deletions(-) create mode 100644 python/docs/store.rst delete mode 100644 python/docs/store/memory.rst delete mode 100644 python/docs/store/sled.rst delete mode 100644 python/src/memory_store.rs rename python/src/{sled_store.rs => store.rs} (91%) delete mode 100644 python/src/store_utils.rs diff --git a/python/docs/index.rst b/python/docs/index.rst index 7d5f66b9..329f3e4c 100644 --- a/python/docs/index.rst +++ b/python/docs/index.rst @@ -50,7 +50,7 @@ Insert the triple `` "example"`` and p from pyoxigraph import * - store = MemoryStore() + store = Store() ex = NamedNode('http://example/') schema_name = NamedNode('http://schema.org/name') store.add(Quad(ex, schema_name, Literal('example'))) @@ -66,6 +66,5 @@ Table of contents model io - store/memory - store/sled + store sparql diff --git a/python/docs/store.rst b/python/docs/store.rst new file mode 100644 index 00000000..32bff252 --- /dev/null +++ b/python/docs/store.rst @@ -0,0 +1,5 @@ +Disk-based RDF Store +==================== + +.. autoclass:: pyoxigraph.Store + :members: diff --git a/python/docs/store/memory.rst b/python/docs/store/memory.rst deleted file mode 100644 index adb3af50..00000000 --- a/python/docs/store/memory.rst +++ /dev/null @@ -1,5 +0,0 @@ -In-Memory Store -=============== - -.. autoclass:: pyoxigraph.MemoryStore - :members: diff --git a/python/docs/store/sled.rst b/python/docs/store/sled.rst deleted file mode 100644 index 417c3163..00000000 --- a/python/docs/store/sled.rst +++ /dev/null @@ -1,5 +0,0 @@ -Disk-based Store -================ - -.. autoclass:: pyoxigraph.SledStore - :members: diff --git a/python/src/io.rs b/python/src/io.rs index 97086601..46c837db 100644 --- a/python/src/io.rs +++ b/python/src/io.rs @@ -1,10 +1,9 @@ use crate::model::{PyQuad, PyTriple}; -use crate::store_utils::map_io_err; use oxigraph::io::read::{QuadReader, TripleReader}; use oxigraph::io::{ DatasetFormat, DatasetParser, DatasetSerializer, GraphFormat, GraphParser, GraphSerializer, }; -use pyo3::exceptions::PyValueError; +use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyBytes; use pyo3::wrap_pyfunction; @@ -243,3 +242,13 @@ fn to_io_err(error: impl Into, py: Python<'_>) -> io::Error { io::Error::new(io::ErrorKind::Other, "An unknown error has occurred") } } + +pub fn map_io_err(error: io::Error) -> PyErr { + match error.kind() { + io::ErrorKind::InvalidInput => PyValueError::new_err(error.to_string()), + io::ErrorKind::InvalidData | io::ErrorKind::UnexpectedEof => { + PySyntaxError::new_err(error.to_string()) + } + _ => PyIOError::new_err(error.to_string()), + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index ac861e7f..b64d2e92 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -9,16 +9,13 @@ )] mod io; -mod memory_store; mod model; -mod sled_store; mod sparql; -mod store_utils; +mod store; -use crate::memory_store::*; use crate::model::*; -use crate::sled_store::*; use crate::sparql::*; +use crate::store::*; use pyo3::prelude::*; /// Oxigraph Python bindings @@ -34,8 +31,7 @@ fn pyoxigraph(_py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; - module.add_class::()?; - module.add_class::()?; + module.add_class::()?; module.add_class::()?; module.add_class::()?; module.add_class::()?; diff --git a/python/src/memory_store.rs b/python/src/memory_store.rs deleted file mode 100644 index 5202f38f..00000000 --- a/python/src/memory_store.rs +++ /dev/null @@ -1,470 +0,0 @@ -use crate::io::PyFileLike; -use crate::model::*; -use crate::sparql::*; -use crate::store_utils::*; -use oxigraph::io::{DatasetFormat, GraphFormat}; -use oxigraph::model::GraphNameRef; -use oxigraph::store::memory::*; -use pyo3::basic::CompareOp; -use pyo3::exceptions::{PyNotImplementedError, PyValueError}; -use pyo3::prelude::{ - pyclass, pymethods, pyproto, Py, PyAny, PyCell, PyObject, PyRef, PyRefMut, PyResult, Python, -}; -use pyo3::{PyIterProtocol, PyObjectProtocol, PySequenceProtocol}; -use std::convert::TryFrom; -use std::io::BufReader; - -/// In-memory store. -/// It encodes a `RDF dataset `_ and allows to query it using SPARQL. -/// -/// The :py:func:`str` function provides a serialization of the store in NQuads: -/// -/// >>> store = MemoryStore() -/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) -/// >>> str(store) -/// ' "1" .\n' -#[pyclass(name = "MemoryStore", module = "oxigraph")] -#[derive(Eq, PartialEq, Clone)] -#[text_signature = "()"] -pub struct PyMemoryStore { - inner: MemoryStore, -} - -#[pymethods] -impl PyMemoryStore { - #[new] - fn new() -> Self { - Self { - inner: MemoryStore::new(), - } - } - - /// Adds a quad to the store - /// - /// :param quad: the quad to add - /// :type quad: Quad - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) - /// >>> list(store) - /// [ predicate= object=> graph_name=>] - #[text_signature = "($self, quad)"] - fn add(&self, quad: PyQuad) { - self.inner.insert(quad) - } - - /// Removes a quad from the store - /// - /// :param quad: the quad to remove - /// :type quad: Quad - /// - /// >>> store = MemoryStore() - /// >>> quad = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) - /// >>> store.add(quad) - /// >>> store.remove(quad) - /// >>> list(store) - /// [] - #[text_signature = "($self, quad)"] - fn remove(&self, quad: &PyQuad) { - self.inner.remove(quad) - } - - /// Looks for the quads matching a given pattern - /// - /// :param subject: the quad subject or :py:const:`None` to match everything. - /// :type subject: NamedNode or BlankNode or None - /// :param predicate: the quad predicate or :py:const:`None` to match everything. - /// :type predicate: NamedNode or None - /// :param object: the quad object or :py:const:`None` to match everything. - /// :type object: NamedNode or BlankNode or Literal or None - /// :param graph: the quad graph name. To match only the default graph, use :py:class:`DefaultGraph`. To match everything use :py:const:`None`. - /// :type graph: NamedNode or BlankNode or DefaultGraph or None - /// :return: an iterator of the quads matching the pattern - /// :rtype: iter(Quad) - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) - /// >>> list(store.quads_for_pattern(NamedNode('http://example.com'), None, None, None)) - /// [ predicate= object=> graph_name=>] - #[text_signature = "($self, subject, predicate, object, graph_name = None)"] - fn quads_for_pattern( - &self, - subject: &PyAny, - predicate: &PyAny, - object: &PyAny, - graph_name: Option<&PyAny>, - ) -> PyResult { - let (subject, predicate, object, graph_name) = - extract_quads_pattern(subject, predicate, object, graph_name)?; - Ok(QuadIter { - inner: self.inner.quads_for_pattern( - subject.as_ref().map(|p| p.into()), - predicate.as_ref().map(|p| p.into()), - object.as_ref().map(|p| p.into()), - graph_name.as_ref().map(|p| p.into()), - ), - }) - } - - /// Executes a `SPARQL 1.1 query `_. - /// - /// :param query: the query to execute - /// :type query: str - /// :param use_default_graph_as_union: if the SPARQL query should look for triples in all the dataset graphs by default (i.e. without `GRAPH` operations). Disabled by default. - /// :type use_default_graph_as_union: bool, optional - /// :param default_graph: list of the graphs that should be used as the query default graph. By default, the store default graph is used. - /// :type default_graph: NamedNode or BlankNode or DefaultGraph or list(NamedNode or BlankNode or DefaultGraph) or None, optional - /// :param named_graphs: list of the named graphs that could be used in SPARQL `GRAPH` clause. By default, all the store named graphs are available. - /// :type named_graphs: list(NamedNode or BlankNode) or None, optional - /// :return: a :py:class:`bool` for ``ASK`` queries, an iterator of :py:class:`Triple` for ``CONSTRUCT`` and ``DESCRIBE`` queries and an iterator of :py:class:`QuerySolution` for ``SELECT`` queries. - /// :rtype: QuerySolutions or QueryTriples or bool - /// :raises SyntaxError: if the provided query is invalid - /// - /// ``SELECT`` query: - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) - /// >>> list(solution['s'] for solution in store.query('SELECT ?s WHERE { ?s ?p ?o }')) - /// [] - /// - /// ``CONSTRUCT`` query: - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) - /// >>> list(store.query('CONSTRUCT WHERE { ?s ?p ?o }')) - /// [ predicate= object=>>] - /// - /// ``ASK`` query: - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) - /// >>> store.query('ASK { ?s ?p ?o }') - /// True - #[text_signature = "($self, query, *, use_default_graph_as_union, default_graph, named_graphs)"] - #[args( - query, - "*", - use_default_graph_as_union = "false", - default_graph = "None", - named_graphs = "None" - )] - fn query( - &self, - query: &str, - use_default_graph_as_union: bool, - default_graph: Option<&PyAny>, - named_graphs: Option<&PyAny>, - py: Python<'_>, - ) -> PyResult { - let query = parse_query( - query, - use_default_graph_as_union, - default_graph, - named_graphs, - )?; - let results = self.inner.query(query).map_err(map_evaluation_error)?; - query_results_to_python(py, results) - } - - /// Executes a `SPARQL 1.1 update `_. - /// - /// :param update: the update to execute - /// :type update: str - /// :raises SyntaxError: if the provided update is invalid - /// - /// The store does not track the existence of empty named graphs. - /// This method has no ACID guarantees. - /// - /// ``INSERT DATA`` update: - /// - /// >>> store = MemoryStore() - /// >>> store.update('INSERT DATA { "1" }') - /// >>> list(store) - /// [ predicate= object=> graph_name=>] - /// - /// ``DELETE DATA`` update: - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) - /// >>> store.update('DELETE DATA { "1" }') - /// >>> list(store) - /// [] - /// - /// ``DELETE`` update: - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) - /// >>> store.update('DELETE WHERE { ?p ?o }') - /// >>> list(store) - /// [] - #[text_signature = "($self, update)"] - fn update(&self, update: &str) -> PyResult<()> { - self.inner.update(update).map_err(map_evaluation_error) - } - - /// Loads an RDF serialization into the store - /// - /// It currently supports the following formats: - /// - /// * `N-Triples `_ (``application/n-triples``) - /// * `N-Quads `_ (``application/n-quads``) - /// * `Turtle `_ (``text/turtle``) - /// * `TriG `_ (``application/trig``) - /// * `RDF/XML `_ (``application/rdf+xml``) - /// - /// It supports also some MIME type aliases. - /// For example ``application/turtle`` could also be used for `Turtle `_ - /// and ``application/xml`` for `RDF/XML `_. - /// - /// :param input: The binary I/O object to read from. For example, it could be a file opened in binary mode with ``open('my_file.ttl', 'rb')``. - /// :type input: io.RawIOBase or io.BufferedIOBase - /// :param mime_type: the MIME type of the RDF serialization - /// :type mime_type: str - /// :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 - /// :type base_iri: str or None, optional - /// :param to_graph: if it is a file composed of triples, the graph in which store the triples. By default, the default graph is used. - /// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional - /// :raises ValueError: if the MIME type is not supported or the `to_graph` parameter is given with a quad file. - /// :raises SyntaxError: if the provided data is invalid - /// - /// >>> store = MemoryStore() - /// >>> store.load(io.BytesIO(b'

"1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) - /// >>> list(store) - /// [ predicate= object=> graph_name=>] - #[text_signature = "($self, input, /, mime_type, *, base_iri = None, to_graph = None)"] - #[args(input, mime_type, "*", base_iri = "None", to_graph = "None")] - fn load( - &self, - input: PyObject, - mime_type: &str, - base_iri: Option<&str>, - to_graph: Option<&PyAny>, - ) -> PyResult<()> { - let to_graph_name = if let Some(graph_name) = to_graph { - Some(PyGraphNameRef::try_from(graph_name)?) - } else { - None - }; - let input = BufReader::new(PyFileLike::new(input)); - if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - self.inner - .load_graph( - input, - graph_format, - &to_graph_name.unwrap_or(PyGraphNameRef::DefaultGraph), - base_iri, - ) - .map_err(map_io_err) - } else if let Some(dataset_format) = DatasetFormat::from_media_type(mime_type) { - if to_graph_name.is_some() { - return Err(PyValueError::new_err( - "The target graph name parameter is not available for dataset formats", - )); - } - self.inner - .load_dataset(input, dataset_format, base_iri) - .map_err(map_io_err) - } else { - Err(PyValueError::new_err(format!( - "Not supported MIME type: {}", - mime_type - ))) - } - } - - /// Dumps the store quads or triples into a file - /// - /// It currently supports the following formats: - /// - /// * `N-Triples `_ (``application/n-triples``) - /// * `N-Quads `_ (``application/n-quads``) - /// * `Turtle `_ (``text/turtle``) - /// * `TriG `_ (``application/trig``) - /// * `RDF/XML `_ (``application/rdf+xml``) - /// - /// It supports also some MIME type aliases. - /// For example ``application/turtle`` could also be used for `Turtle `_ - /// and ``application/xml`` for `RDF/XML `_. - /// - /// :param output: The binary I/O object to write to. For example, it could be a file opened in binary mode with ``open('my_file.ttl', 'wb')``. - /// :type input: io.RawIOBase or io.BufferedIOBase - /// :param mime_type: the MIME type of the RDF serialization - /// :type mime_type: str - /// :param from_graph: if a triple based format is requested, the store graph from which dump the triples. By default, the default graph is used. - /// :type from_graph: NamedNode or BlankNode or DefaultGraph or None, optional - /// :raises ValueError: if the MIME type is not supported or the `from_graph` parameter is given with a quad syntax. - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) - /// >>> output = io.BytesIO() - /// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g")) - /// >>> output.getvalue() - /// b' "1" .\n' - #[text_signature = "($self, output, /, mime_type, *, from_graph = None)"] - #[args(output, mime_type, "*", from_graph = "None")] - fn dump(&self, output: PyObject, mime_type: &str, from_graph: Option<&PyAny>) -> PyResult<()> { - let from_graph_name = if let Some(graph_name) = from_graph { - Some(PyGraphNameRef::try_from(graph_name)?) - } else { - None - }; - let output = PyFileLike::new(output); - if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - self.inner - .dump_graph( - output, - graph_format, - &from_graph_name.unwrap_or(PyGraphNameRef::DefaultGraph), - ) - .map_err(map_io_err) - } else if let Some(dataset_format) = DatasetFormat::from_media_type(mime_type) { - if from_graph_name.is_some() { - return Err(PyValueError::new_err( - "The target graph name parameter is not available for dataset formats", - )); - } - self.inner - .dump_dataset(output, dataset_format) - .map_err(map_io_err) - } else { - Err(PyValueError::new_err(format!( - "Not supported MIME type: {}", - mime_type - ))) - } - } - - /// Returns an iterator over all the store named graphs - /// - /// :return: an iterator of the store graph names - /// :rtype: iter(NamedNode or BlankNode) - /// - /// >>> store = MemoryStore() - /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) - /// >>> list(store.named_graphs()) - /// [] - #[text_signature = "($self)"] - fn named_graphs(&self) -> GraphNameIter { - GraphNameIter { - inner: self.inner.named_graphs(), - } - } - - /// Adds a named graph to the store - /// - /// :param graph_name: the name of the name graph to add - /// :type graph_name: NamedNode or BlankNode - /// - /// >>> store = MemoryStore() - /// >>> store.add_graph(NamedNode('http://example.com/g')) - /// >>> list(store.named_graphs()) - /// [] - #[text_signature = "($self, graph_name)"] - fn add_graph(&self, graph_name: PyGraphName) { - match graph_name { - PyGraphName::DefaultGraph(_) => (), - PyGraphName::NamedNode(graph_name) => self.inner.insert_named_graph(graph_name), - PyGraphName::BlankNode(graph_name) => self.inner.insert_named_graph(graph_name), - } - } - - /// Removes a graph from the store - /// - /// The default graph will not be remove but just cleared. - /// - /// :param graph_name: the name of the name graph to remove - /// :type graph_name: NamedNode or BlankNode or DefaultGraph - /// - /// >>> store = MemoryStore() - /// >>> quad = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) - /// >>> store.remove_graph(NamedNode('http://example.com/g')) - /// >>> list(store) - /// [] - #[text_signature = "($self, graph_name)"] - fn remove_graph(&self, graph_name: &PyAny) -> PyResult<()> { - match PyGraphNameRef::try_from(graph_name)? { - PyGraphNameRef::DefaultGraph => self.inner.clear_graph(GraphNameRef::DefaultGraph), - PyGraphNameRef::NamedNode(graph_name) => self - .inner - .remove_named_graph(&PyNamedOrBlankNodeRef::NamedNode(graph_name)), - PyGraphNameRef::BlankNode(graph_name) => self - .inner - .remove_named_graph(&PyNamedOrBlankNodeRef::BlankNode(graph_name)), - } - Ok(()) - } -} - -#[pyproto] -impl PyObjectProtocol for PyMemoryStore { - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __richcmp__(&self, other: &PyCell, op: CompareOp) -> PyResult { - let other: &PyMemoryStore = &other.borrow(); - match op { - CompareOp::Eq => Ok(self == other), - CompareOp::Ne => Ok(self != other), - _ => Err(PyNotImplementedError::new_err( - "Ordering is not implemented", - )), - } - } - - fn __bool__(&self) -> bool { - !self.inner.is_empty() - } -} - -#[pyproto] -impl<'p> PySequenceProtocol<'p> for PyMemoryStore { - fn __len__(&self) -> usize { - self.inner.len() - } - - fn __contains__(&self, quad: PyQuad) -> bool { - self.inner.contains(&quad) - } -} - -#[pyproto] -impl PyIterProtocol for PyMemoryStore { - fn __iter__(slf: PyRef) -> QuadIter { - QuadIter { - inner: slf.inner.iter(), - } - } -} - -#[pyclass(unsendable, module = "oxigraph")] -pub struct QuadIter { - inner: MemoryQuadIter, -} - -#[pyproto] -impl PyIterProtocol for QuadIter { - fn __iter__(slf: PyRefMut) -> Py { - slf.into() - } - - fn __next__(mut slf: PyRefMut) -> Option { - slf.inner.next().map(|q| q.into()) - } -} - -#[pyclass(unsendable, module = "oxigraph")] -pub struct GraphNameIter { - inner: MemoryGraphNameIter, -} - -#[pyproto] -impl PyIterProtocol for GraphNameIter { - fn __iter__(slf: PyRefMut) -> Py { - slf.into() - } - - fn __next__(mut slf: PyRefMut) -> Option { - slf.inner.next().map(|q| q.into()) - } -} diff --git a/python/src/sparql.rs b/python/src/sparql.rs index f3dafd1a..b79fb756 100644 --- a/python/src/sparql.rs +++ b/python/src/sparql.rs @@ -1,5 +1,5 @@ +use crate::io::map_io_err; use crate::model::*; -use crate::store_utils::*; use oxigraph::model::Term; use oxigraph::sparql::*; use pyo3::exceptions::{PyRuntimeError, PySyntaxError, PyTypeError, PyValueError}; @@ -73,7 +73,7 @@ pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyResul /// It could be indexes by variable name (:py:class:`Variable` or :py:class:`str`) or position in the tuple (:py:class:`int`). /// Unpacking also works. /// -/// >>> store = SledStore() +/// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> solution = next(store.query('SELECT ?s ?p ?o WHERE { ?s ?p ?o }')) /// >>> solution[Variable('s')] @@ -164,7 +164,7 @@ impl PyIterProtocol for SolutionValueIter { /// An iterator of :py:class:`QuerySolution` returned by a SPARQL ``SELECT`` query /// -/// >>> store = SledStore() +/// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> list(store.query('SELECT ?s WHERE { ?s ?p ?o }')) /// [>] @@ -178,7 +178,7 @@ impl PyQuerySolutions { /// :return: the ordered list of all variables that could appear in the query results /// :rtype: list(Variable) /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.query('SELECT ?s WHERE { ?s ?p ?o }').variables /// [] #[getter] @@ -209,7 +209,7 @@ impl PyIterProtocol for PyQuerySolutions { /// An iterator of :py:class:`Triple` returned by a SPARQL ``CONSTRUCT`` or ``DESCRIBE`` query /// -/// >>> store = MemoryStore() +/// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> list(store.query('CONSTRUCT WHERE { ?s ?p ?o }')) /// [ predicate= object=>>] diff --git a/python/src/sled_store.rs b/python/src/store.rs similarity index 91% rename from python/src/sled_store.rs rename to python/src/store.rs index 9c24b406..0adc2cdc 100644 --- a/python/src/sled_store.rs +++ b/python/src/store.rs @@ -1,7 +1,6 @@ -use crate::io::PyFileLike; +use crate::io::{map_io_err, PyFileLike}; use crate::model::*; use crate::sparql::*; -use crate::store_utils::*; use oxigraph::io::{DatasetFormat, GraphFormat}; use oxigraph::model::GraphNameRef; use oxigraph::store::sled::*; @@ -10,35 +9,33 @@ use pyo3::prelude::{ pyclass, pymethods, pyproto, Py, PyAny, PyObject, PyRef, PyRefMut, PyResult, Python, }; use pyo3::{PyIterProtocol, PyObjectProtocol, PySequenceProtocol}; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::io::BufReader; -/// Store based on the `Sled `_ key-value database. +/// Disk-based RDF store. /// -/// In-memory store. /// It encodes a `RDF dataset `_ and allows to query it using SPARQL. +/// It is based on the `Sled `_ key-value database /// -/// :param path: the path of the directory in which Sled should read and write its data. If the directory does not exist, it is created. If no directory is provided a temporary one is created and removed when the Python garbage collector removes the store. +/// :param path: the path of the directory in which the store should read and write its data. If the directory does not exist, it is created. If no directory is provided a temporary one is created and removed when the Python garbage collector removes the store. /// :type path: str or None, optional /// :raises IOError: if the target directory contains invalid data or could not be accessed /// -/// Warning: Sled is not stable yet and might break its storage format. -/// /// The :py:func:`str` function provides a serialization of the store in NQuads: /// -/// >>> store = SledStore() +/// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> str(store) /// ' "1" .\n' -#[pyclass(name = "SledStore", module = "oxigraph")] +#[pyclass(name = "Store", module = "oxigraph")] #[text_signature = "(path = None)"] #[derive(Clone)] -pub struct PySledStore { +pub struct PyStore { inner: SledStore, } #[pymethods] -impl PySledStore { +impl PyStore { #[new] fn new(path: Option<&str>) -> PyResult { Ok(Self { @@ -57,7 +54,7 @@ impl PySledStore { /// :type quad: Quad /// :raises IOError: if an I/O error happens during the quad insertion /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> list(store) /// [ predicate= object=> graph_name=>] @@ -72,7 +69,7 @@ impl PySledStore { /// :type quad: Quad /// :raises IOError: if an I/O error happens during the quad removal /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> quad = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) /// >>> store.add(quad) /// >>> store.remove(quad) @@ -97,7 +94,7 @@ impl PySledStore { /// :rtype: iter(Quad) /// :raises IOError: if an I/O error happens during the quads lookup /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> list(store.quads_for_pattern(NamedNode('http://example.com'), None, None, None)) /// [ predicate= object=> graph_name=>] @@ -138,21 +135,21 @@ impl PySledStore { /// /// ``SELECT`` query: /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> list(solution['s'] for solution in store.query('SELECT ?s WHERE { ?s ?p ?o }')) /// [] /// /// ``CONSTRUCT`` query: /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> list(store.query('CONSTRUCT WHERE { ?s ?p ?o }')) /// [ predicate= object=>>] /// /// ``ASK`` query: /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.query('ASK { ?s ?p ?o }') /// True @@ -194,14 +191,14 @@ impl PySledStore { /// /// ``INSERT DATA`` update: /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.update('INSERT DATA { "1" }') /// >>> list(store) /// [ predicate= object=> graph_name=>] /// /// ``DELETE DATA`` update: /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.update('DELETE DATA { "1" }') /// >>> list(store) @@ -209,7 +206,7 @@ impl PySledStore { /// /// ``DELETE`` update: /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.update('DELETE WHERE { ?p ?o }') /// >>> list(store) @@ -245,7 +242,7 @@ impl PySledStore { /// :raises SyntaxError: if the provided data is invalid /// :raises IOError: if an I/O error happens during a quad insertion /// - /// >>> store = SledStore() + /// >>> store = Store() /// >>> store.load(io.BytesIO(b'

"1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> list(store) /// [ predicate= object=> graph_name=>] @@ -313,7 +310,7 @@ impl PySledStore { /// :raises ValueError: if the MIME type is not supported or the `from_graph` parameter is given with a quad syntax. /// :raises IOError: if an I/O error happens during a quad lookup /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> output = io.BytesIO() /// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g")) @@ -359,7 +356,7 @@ impl PySledStore { /// :rtype: iter(NamedNode or BlankNode) /// :raises IOError: if an I/O error happens during the named graphs lookup /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> list(store.named_graphs()) /// [] @@ -376,7 +373,7 @@ impl PySledStore { /// :type graph_name: NamedNode or BlankNode /// :raises IOError: if an I/O error happens during the named graph insertion /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> store.add_graph(NamedNode('http://example.com/g')) /// >>> list(store.named_graphs()) /// [] @@ -402,7 +399,7 @@ impl PySledStore { /// :type graph_name: NamedNode or BlankNode or DefaultGraph /// :raises IOError: if an I/O error happens during the named graph removal /// - /// >>> store = MemoryStore() + /// >>> store = Store() /// >>> quad = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) /// >>> store.remove_graph(NamedNode('http://example.com/g')) /// >>> list(store) @@ -423,7 +420,7 @@ impl PySledStore { } #[pyproto] -impl PyObjectProtocol for PySledStore { +impl PyObjectProtocol for PyStore { fn __str__(&self) -> String { self.inner.to_string() } @@ -434,7 +431,7 @@ impl PyObjectProtocol for PySledStore { } #[pyproto] -impl PySequenceProtocol for PySledStore { +impl PySequenceProtocol for PyStore { fn __len__(&self) -> usize { self.inner.len() } @@ -445,7 +442,7 @@ impl PySequenceProtocol for PySledStore { } #[pyproto] -impl PyIterProtocol for PySledStore { +impl PyIterProtocol for PyStore { fn __iter__(slf: PyRef) -> QuadIter { QuadIter { inner: slf.inner.iter(), @@ -490,3 +487,42 @@ impl PyIterProtocol for GraphNameIter { .transpose() } } + +pub fn extract_quads_pattern<'a>( + subject: &'a PyAny, + predicate: &'a PyAny, + object: &'a PyAny, + graph_name: Option<&'a PyAny>, +) -> PyResult<( + Option>, + Option>, + Option>, + Option>, +)> { + Ok(( + if subject.is_none() { + None + } else { + Some(subject.try_into()?) + }, + if predicate.is_none() { + None + } else { + Some(predicate.try_into()?) + }, + if object.is_none() { + None + } else { + Some(object.try_into()?) + }, + if let Some(graph_name) = graph_name { + if graph_name.is_none() { + None + } else { + Some(graph_name.try_into()?) + } + } else { + None + }, + )) +} diff --git a/python/src/store_utils.rs b/python/src/store_utils.rs deleted file mode 100644 index 22b8e9ac..00000000 --- a/python/src/store_utils.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::model::*; -use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError}; -use pyo3::{PyAny, PyErr, PyResult}; -use std::convert::TryInto; -use std::io; - -pub fn extract_quads_pattern<'a>( - subject: &'a PyAny, - predicate: &'a PyAny, - object: &'a PyAny, - graph_name: Option<&'a PyAny>, -) -> PyResult<( - Option>, - Option>, - Option>, - Option>, -)> { - Ok(( - if subject.is_none() { - None - } else { - Some(subject.try_into()?) - }, - if predicate.is_none() { - None - } else { - Some(predicate.try_into()?) - }, - if object.is_none() { - None - } else { - Some(object.try_into()?) - }, - if let Some(graph_name) = graph_name { - if graph_name.is_none() { - None - } else { - Some(graph_name.try_into()?) - } - } else { - None - }, - )) -} - -pub fn map_io_err(error: io::Error) -> PyErr { - match error.kind() { - io::ErrorKind::InvalidInput => PyValueError::new_err(error.to_string()), - io::ErrorKind::InvalidData | io::ErrorKind::UnexpectedEof => { - PySyntaxError::new_err(error.to_string()) - } - _ => PyIOError::new_err(error.to_string()), - } -} diff --git a/python/tests/test_store.py b/python/tests/test_store.py index e2d8af33..ecbf4e56 100644 --- a/python/tests/test_store.py +++ b/python/tests/test_store.py @@ -1,5 +1,4 @@ import unittest -from abc import ABC, abstractmethod from io import BytesIO from pyoxigraph import * @@ -10,20 +9,16 @@ baz = NamedNode("http://baz") graph = NamedNode("http://graph") -class TestAbstractStore(unittest.TestCase, ABC): - @abstractmethod - def store(self): - pass - +class TestStore(unittest.TestCase): def test_add(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) self.assertEqual(len(store), 2) def test_remove(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) @@ -31,13 +26,13 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(len(store), 1) def test_len(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, graph)) self.assertEqual(len(store), 2) def test_in(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) @@ -47,7 +42,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertNotIn(Quad(foo, bar, baz, foo), store) def test_iter(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) self.assertEqual( @@ -56,7 +51,7 @@ class TestAbstractStore(unittest.TestCase, ABC): ) def test_quads_for_pattern(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) self.assertEqual( @@ -77,13 +72,13 @@ class TestAbstractStore(unittest.TestCase, ABC): ) def test_ask_query(self): - store = self.store() + store = Store() store.add(Quad(foo, foo, foo)) self.assertTrue(store.query("ASK { ?s ?s ?s }")) self.assertFalse(store.query("ASK { FILTER(false) }")) def test_construct_query(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") self.assertIsInstance(results, QueryTriples) @@ -92,7 +87,7 @@ class TestAbstractStore(unittest.TestCase, ABC): ) def test_select_query(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz)) solutions = store.query("SELECT ?s ?o WHERE { ?s ?p ?o }") self.assertIsInstance(solutions, QuerySolutions) @@ -110,7 +105,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(o, baz) def test_select_query_union_default_graph(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, graph)) self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }"))), 0) results = store.query( @@ -125,7 +120,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(len(list(results)), 1) def test_select_query_with_default_graph(self): - store = self.store() + store = Store() graph_bnode = BlankNode("g") store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, foo)) @@ -140,7 +135,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(len(list(results)), 3) def test_select_query_with_named_graph(self): - store = self.store() + store = Store() graph_bnode = BlankNode("g") store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, foo)) @@ -153,29 +148,29 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(len(list(results)), 2) def test_update_insert_data(self): - store = self.store() + store = Store() store.update('INSERT DATA { }') self.assertEqual(len(store), 1) def test_update_delete_data(self): - store = self.store() + store = Store() store.add(Quad(foo, foo, foo)) store.update('DELETE DATA { }') self.assertEqual(len(store), 0) def test_update_delete_where(self): - store = self.store() + store = Store() store.add(Quad(foo, foo, foo)) store.update('DELETE WHERE { ?v ?v ?v }') self.assertEqual(len(store), 0) def test_update_load(self): - store = self.store() + store = Store() store.update('LOAD ') self.assertGreater(len(store), 100) def test_load_ntriples_to_default_graph(self): - store = self.store() + store = Store() store.load( BytesIO(b" ."), mime_type="application/n-triples", @@ -183,7 +178,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) def test_load_ntriples_to_named_graph(self): - store = self.store() + store = Store() store.load( BytesIO(b" ."), mime_type="application/n-triples", @@ -192,7 +187,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) def test_load_turtle_with_base_iri(self): - store = self.store() + store = Store() store.load( BytesIO(b" <> ."), mime_type="text/turtle", @@ -201,7 +196,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) def test_load_nquads(self): - store = self.store() + store = Store() store.load( BytesIO(b" ."), mime_type="application/n-quads", @@ -209,7 +204,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) def test_load_trig_with_base_iri(self): - store = self.store() + store = Store() store.load( BytesIO(b" { <> . }"), mime_type="application/trig", @@ -218,7 +213,7 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) def test_dump_ntriples(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, graph)) output = BytesIO() store.dump(output, "application/n-triples", from_graph=graph) @@ -227,7 +222,7 @@ class TestAbstractStore(unittest.TestCase, ABC): ) def test_dump_nquads(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, graph)) output = BytesIO() store.dump(output, "application/n-quads") @@ -237,7 +232,7 @@ class TestAbstractStore(unittest.TestCase, ABC): ) def test_write_in_read(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, bar)) store.add(Quad(foo, bar, baz)) for triple in store: @@ -245,12 +240,12 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(len(store), 4) def test_add_graph(self): - store = self.store() + store = Store() store.add_graph(graph) self.assertEqual(list(store.named_graphs()), [graph]) def test_remove_graph(self): - store = self.store() + store = Store() store.add(Quad(foo, bar, baz, graph)) store.add_graph(NamedNode("http://graph2")) store.remove_graph(graph) @@ -259,17 +254,5 @@ class TestAbstractStore(unittest.TestCase, ABC): self.assertEqual(list(store), []) -class TestMemoryStore(TestAbstractStore): - def store(self): - return MemoryStore() - - -class TestSledStore(TestAbstractStore): - def store(self): - return SledStore() - - -del TestAbstractStore # We do not want to expose this class to the test runner - if __name__ == "__main__": unittest.main()