#![allow(clippy::needless_option_as_deref)] use crate::io::{ allow_threads_unsafe, map_io_err, map_parse_error, rdf_format, PyReadable, PyWritable, }; use crate::model::*; use crate::sparql::*; use oxigraph::model::{GraphName, GraphNameRef}; use oxigraph::sparql::Update; use oxigraph::store::{self, LoaderError, SerializerError, StorageError, Store}; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; use std::io::BufWriter; use std::path::PathBuf; /// RDF store. /// /// It encodes a `RDF dataset `_ and allows to query it using SPARQL. /// It is based on the `RocksDB `_ key-value database. /// /// This store ensures the "repeatable read" isolation level: the store only exposes changes that have /// been "committed" (i.e. no partial writes) and the exposed state does not change for the complete duration /// of a read operation (e.g. a SPARQL query) or a read/write operation (e.g. a SPARQL update). /// /// The :py:class:`Store` constructor opens a read-write instance. /// To open a static read-only instance use :py:func:`Store.read_only` /// and to open a read-only instance that tracks a read-write instance use :py:func:`Store.secondary`. /// /// :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. /// In this case, the store data are kept in memory and never written on disk. /// :type path: str or pathlib.Path or None, optional /// :raises OSError: if the target directory contains invalid data or could not be accessed. /// /// The :py:func:`str` function provides a serialization of the store in NQuads: /// /// >>> 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(frozen, name = "Store", module = "pyoxigraph")] #[derive(Clone)] pub struct PyStore { inner: Store, } #[pymethods] impl PyStore { #[new] #[pyo3(signature = (path = None))] fn new(path: Option, py: Python<'_>) -> PyResult { py.allow_threads(|| { Ok(Self { inner: if let Some(path) = path { Store::open(path) } else { Store::new() } .map_err(map_storage_error)?, }) }) } /// Opens a read-only store from disk. /// /// Opening as read-only while having an other process writing the database is undefined behavior. /// :py:func:`Store.secondary` should be used in this case. /// /// :param path: path to the primary read-write instance data. /// :type path: str /// :return: the opened store. /// :rtype: Store /// :raises OSError: if the target directory contains invalid data or could not be accessed. #[staticmethod] fn read_only(path: &str, py: Python<'_>) -> PyResult { py.allow_threads(|| { Ok(Self { inner: Store::open_read_only(path).map_err(map_storage_error)?, }) }) } /// Opens a read-only clone of a running read-write store. /// /// Changes done while this process is running will be replicated after a possible lag. /// /// It should only be used if a primary instance opened with :py:func:`Store` is running at the same time. /// /// If you want to simple read-only store use :py:func:`Store.read_only`. /// /// :param primary_path: path to the primary read-write instance data. /// :type primary_path: str /// :param secondary_path: path to an other directory for the secondary instance cache. If not given a temporary directory will be used. /// :type secondary_path: str or None, optional /// :return: the opened store. /// :rtype: Store /// :raises OSError: if the target directories contain invalid data or could not be accessed. #[staticmethod] #[pyo3(signature = (primary_path, secondary_path = None))] fn secondary( primary_path: &str, secondary_path: Option<&str>, py: Python<'_>, ) -> PyResult { py.allow_threads(|| { Ok(Self { inner: if let Some(secondary_path) = secondary_path { Store::open_persistent_secondary(primary_path, secondary_path) } else { Store::open_secondary(primary_path) } .map_err(map_storage_error)?, }) }) } /// Adds a quad to the store. /// /// :param quad: the quad to add. /// :type quad: Quad /// :rtype: None /// :raises OSError: if an error happens during the quad insertion. /// /// >>> 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=>] fn add(&self, quad: &PyQuad, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| { self.inner.insert(quad).map_err(map_storage_error)?; Ok(()) }) } /// Adds atomically a set of quads to this store. /// /// Insertion is done in a transactional manner: either the full operation succeeds or nothing is written to the database. /// The :py:func:`bulk_extend` method is also available for much faster loading of a large number of quads but without transactional guarantees. /// /// :param quads: the quads to add. /// :type quads: iterable(Quad) /// :rtype: None /// :raises OSError: if an error happens during the quad insertion. /// /// >>> store = Store() /// >>> store.extend([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))]) /// >>> list(store) /// [ predicate= object=> graph_name=>] fn extend(&self, quads: &PyAny, py: Python<'_>) -> PyResult<()> { let quads = quads .iter()? .map(|q| q?.extract()) .collect::>>()?; py.allow_threads(|| { self.inner.extend(quads).map_err(map_storage_error)?; Ok(()) }) } /// Adds a set of quads to this store. /// /// This function is designed to be as fast as possible **without** transactional guarantees. /// Only a part of the data might be written to the store. /// /// :param quads: the quads to add. /// :type quads: iterable(Quad) /// :rtype: None /// :raises OSError: if an error happens during the quad insertion. /// /// >>> store = Store() /// >>> store.bulk_extend([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))]) /// >>> list(store) /// [ predicate= object=> graph_name=>] fn bulk_extend(&self, quads: &PyAny) -> PyResult<()> { self.inner .bulk_loader() .load_ok_quads::( quads.iter()?.map(|q| q?.extract::()), )?; Ok(()) } /// Removes a quad from the store. /// /// :param quad: the quad to remove. /// :type quad: Quad /// :rtype: None /// :raises OSError: if an error happens during the quad removal. /// /// >>> 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) /// >>> list(store) /// [] fn remove(&self, quad: &PyQuad, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| { self.inner.remove(quad).map_err(map_storage_error)?; Ok(()) }) } /// 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 Triple 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 Triple or None /// :param graph_name: the quad graph name. To match only the default graph, use :py:class:`DefaultGraph`. To match everything use :py:const:`None`. /// :type graph_name: NamedNode or BlankNode or DefaultGraph or None, optional /// :return: an iterator of the quads matching the pattern. /// :rtype: iterator(Quad) /// :raises OSError: if an error happens during the quads lookup. /// /// >>> 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=>] #[pyo3(signature = (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(Into::into), predicate.as_ref().map(Into::into), object.as_ref().map(Into::into), graph_name.as_ref().map(Into::into), ), }) } /// Executes a `SPARQL 1.1 query `_. /// /// :param query: the query to execute. /// :type query: str /// :param base_iri: the base IRI used to resolve the relative IRIs in the SPARQL query or :py:const:`None` if relative IRI resolution should not be done. /// :type base_iri: str or None, optional /// :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. /// :raises OSError: if an error happens while reading the store. /// /// ``SELECT`` query: /// /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> [solution['s'] for solution in store.query('SELECT ?s WHERE { ?s ?p ?o }')] /// [] /// /// ``CONSTRUCT`` query: /// /// >>> 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 = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.query('ASK { ?s ?p ?o }') /// True #[pyo3(signature = (query, *, base_iri = None, use_default_graph_as_union = false, default_graph = None, named_graphs = None))] fn query( &self, query: &str, base_iri: Option<&str>, use_default_graph_as_union: bool, default_graph: Option<&PyAny>, named_graphs: Option<&PyAny>, py: Python<'_>, ) -> PyResult { let query = parse_query( query, base_iri, use_default_graph_as_union, default_graph, named_graphs, )?; let results = allow_threads_unsafe(|| self.inner.query(query)).map_err(map_evaluation_error)?; Ok(query_results_to_python(py, results)) } /// Executes a `SPARQL 1.1 update `_. /// /// Updates are applied in a transactional manner: either the full operation succeeds or nothing is written to the database. /// /// :param update: the update to execute. /// :type update: str /// :param base_iri: the base IRI used to resolve the relative IRIs in the SPARQL update or :py:const:`None` if relative IRI resolution should not be done. /// :type base_iri: str or None, optional /// :rtype: None /// :raises SyntaxError: if the provided update is invalid. /// :raises OSError: if an error happens while reading the store. /// /// ``INSERT DATA`` update: /// /// >>> store = Store() /// >>> store.update('INSERT DATA { "1" }') /// >>> list(store) /// [ predicate= object=> graph_name=>] /// /// ``DELETE DATA`` update: /// /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.update('DELETE DATA { "1" }') /// >>> list(store) /// [] /// /// ``DELETE`` update: /// /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.update('DELETE WHERE { ?p ?o }') /// >>> list(store) /// [] #[pyo3(signature = (update, *, base_iri = None))] fn update(&self, update: &str, base_iri: Option<&str>, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| { let update = Update::parse(update, base_iri).map_err(|e| map_evaluation_error(e.into()))?; self.inner.update(update).map_err(map_evaluation_error) }) } /// Loads an RDF serialization into the store. /// /// Loads are applied in a transactional manner: either the full operation succeeds or nothing is written to the database. /// The :py:func:`bulk_load` method is also available for much faster loading of big files but without transactional guarantees. /// /// Beware, the full file is loaded into memory. /// /// It currently supports the following formats: /// /// * `N-Triples `_ (``application/n-triples`` or ``nt``) /// * `N-Quads `_ (``application/n-quads`` or ``nq``) /// * `Turtle `_ (``text/turtle`` or ``ttl``) /// * `TriG `_ (``application/trig`` or ``trig``) /// * `N3 `_ (``text/n3`` or ``n3``) /// * `RDF/XML `_ (``application/rdf+xml`` or ``rdf``) /// /// It supports also some media type and extension aliases. /// For example, ``application/turtle`` could also be used for `Turtle `_ /// and ``application/xml`` or ``xml`` for `RDF/XML `_. /// /// :param input: The binary I/O object or file path to read from. For example, it could be a file path as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :type input: io(bytes) or io(str) or str or pathlib.Path /// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. /// :type format: 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 the triples should be stored. By default, the default graph is used. /// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional /// :rtype: None /// :raises ValueError: if the format is not supported. /// :raises SyntaxError: if the provided data is invalid. /// :raises OSError: if an error happens during a quad insertion. /// /// >>> 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=>] #[pyo3(signature = (input, format, *, base_iri = None, to_graph = None))] fn load( &self, input: PyObject, format: &str, base_iri: Option<&str>, to_graph: Option<&PyAny>, py: Python<'_>, ) -> PyResult<()> { let format = rdf_format(format)?; let to_graph_name = if let Some(graph_name) = to_graph { Some(GraphName::from(&PyGraphNameRef::try_from(graph_name)?)) } else { None }; let input = if let Ok(path) = input.extract::(py) { PyReadable::from_file(&path, py).map_err(map_io_err)? } else { PyReadable::from_data(input, py) }; py.allow_threads(|| { if let Some(to_graph_name) = to_graph_name { self.inner .load_graph(input, format, to_graph_name, base_iri) } else { self.inner.load_dataset(input, format, base_iri) } .map_err(map_loader_error) }) } /// Loads an RDF serialization into the store. /// /// This function is designed to be as fast as possible on big files **without** transactional guarantees. /// If the file is invalid only a piece of it might be written to the store. /// /// The :py:func:`load` method is also available for loads with transactional guarantees. /// /// It currently supports the following formats: /// /// * `N-Triples `_ (``application/n-triples`` or ``nt``) /// * `N-Quads `_ (``application/n-quads`` or ``nq``) /// * `Turtle `_ (``text/turtle`` or ``ttl``) /// * `TriG `_ (``application/trig`` or ``trig``) /// * `N3 `_ (``text/n3`` or ``n3``) /// * `RDF/XML `_ (``application/rdf+xml`` or ``rdf``) /// /// It supports also some media type and extension aliases. /// For example, ``application/turtle`` could also be used for `Turtle `_ /// and ``application/xml`` or ``xml`` for `RDF/XML `_. /// /// :param input: The binary I/O object or file path to read from. For example, it could be a file path as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``. /// :type input: io(bytes) or io(str) or str or pathlib.Path /// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. /// :type format: 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 the triples should be stored. By default, the default graph is used. /// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional /// :rtype: None /// :raises ValueError: if the format is not supported. /// :raises SyntaxError: if the provided data is invalid. /// :raises OSError: if an error happens during a quad insertion. /// /// >>> store = Store() /// >>> store.bulk_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=>] #[pyo3(signature = (input, format, *, base_iri = None, to_graph = None))] fn bulk_load( &self, input: PyObject, format: &str, base_iri: Option<&str>, to_graph: Option<&PyAny>, py: Python<'_>, ) -> PyResult<()> { let format = rdf_format(format)?; let to_graph_name = if let Some(graph_name) = to_graph { Some(GraphName::from(&PyGraphNameRef::try_from(graph_name)?)) } else { None }; let input = if let Ok(path) = input.extract::(py) { PyReadable::from_file(&path, py).map_err(map_io_err)? } else { PyReadable::from_data(input, py) }; py.allow_threads(|| { if let Some(to_graph_name) = to_graph_name { self.inner .bulk_loader() .load_graph(input, format, to_graph_name, base_iri) } else { self.inner .bulk_loader() .load_dataset(input, format, base_iri) } .map_err(map_loader_error) }) } /// Dumps the store quads or triples into a file. /// /// It currently supports the following formats: /// /// * `N-Triples `_ (``application/n-triples`` or ``nt``) /// * `N-Quads `_ (``application/n-quads`` or ``nq``) /// * `Turtle `_ (``text/turtle`` or ``ttl``) /// * `TriG `_ (``application/trig`` or ``trig``) /// * `N3 `_ (``text/n3`` or ``n3``) /// * `RDF/XML `_ (``application/rdf+xml`` or ``rdf``) /// /// It supports also some media type and extension aliases. /// For example, ``application/turtle`` could also be used for `Turtle `_ /// and ``application/xml`` or ``xml`` for `RDF/XML `_. /// /// :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')``. /// :type output: io(bytes) or str or pathlib.Path /// :param format: the format of the RDF serialization using a media type like ``text/turtle`` or an extension like `ttl`. /// :type format: str /// :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 /// :rtype: 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 OSError: if an error happens during a quad lookup /// /// >>> 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")) /// >>> output.getvalue() /// b' "1" .\n' #[pyo3(signature = (output, format, *, from_graph = None))] fn dump( &self, output: PyObject, format: &str, from_graph: Option<&PyAny>, py: Python<'_>, ) -> PyResult<()> { let format = rdf_format(format)?; let from_graph_name = if let Some(graph_name) = from_graph { Some(GraphName::from(&PyGraphNameRef::try_from(graph_name)?)) } else { None }; let output = if let Ok(path) = output.extract::(py) { PyWritable::from_file(&path, py).map_err(map_io_err)? } else { PyWritable::from_data(output) }; py.allow_threads(|| { let output = BufWriter::new(output); if let Some(from_graph_name) = &from_graph_name { self.inner.dump_graph(output, format, from_graph_name) } else { self.inner.dump_dataset(output, format) } .map_err(map_serializer_error)? .into_inner() .map_err(|e| map_io_err(e.into_error()))? .close() .map_err(map_io_err) }) } /// Returns an iterator over all the store named graphs. /// /// :return: an iterator of the store graph names. /// :rtype: iterator(NamedNode or BlankNode) /// :raises OSError: if an error happens during the named graphs lookup. /// /// >>> 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()) /// [] fn named_graphs(&self) -> GraphNameIter { GraphNameIter { inner: self.inner.named_graphs(), } } /// Returns if the store contains the given named graph. /// /// :param graph_name: the name of the named graph. /// :type graph_name: NamedNode or BlankNode or DefaultGraph /// :rtype: bool /// :raises OSError: if an error happens during the named graph lookup. /// /// >>> store = Store() /// >>> store.add_graph(NamedNode('http://example.com/g')) /// >>> store.contains_named_graph(NamedNode('http://example.com/g')) /// True fn contains_named_graph(&self, graph_name: &PyAny) -> PyResult { let graph_name = GraphName::from(&PyGraphNameRef::try_from(graph_name)?); match graph_name { GraphName::DefaultGraph => Ok(true), GraphName::NamedNode(graph_name) => self.inner.contains_named_graph(&graph_name), GraphName::BlankNode(graph_name) => self.inner.contains_named_graph(&graph_name), } .map_err(map_storage_error) } /// Adds a named graph to the store. /// /// :param graph_name: the name of the name graph to add. /// :type graph_name: NamedNode or BlankNode or DefaultGraph /// :rtype: None /// :raises OSError: if an error happens during the named graph insertion. /// /// >>> store = Store() /// >>> store.add_graph(NamedNode('http://example.com/g')) /// >>> list(store.named_graphs()) /// [] fn add_graph(&self, graph_name: &PyAny, py: Python<'_>) -> PyResult<()> { let graph_name = GraphName::from(&PyGraphNameRef::try_from(graph_name)?); py.allow_threads(|| { match graph_name { GraphName::DefaultGraph => Ok(()), GraphName::NamedNode(graph_name) => { self.inner.insert_named_graph(&graph_name).map(|_| ()) } GraphName::BlankNode(graph_name) => { self.inner.insert_named_graph(&graph_name).map(|_| ()) } } .map_err(map_storage_error) }) } /// Clears a graph from the store without removing it. /// /// :param graph_name: the name of the name graph to clear. /// :type graph_name: NamedNode or BlankNode or DefaultGraph /// :rtype: None /// :raises OSError: if an error happens during the operation. /// /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> store.clear_graph(NamedNode('http://example.com/g')) /// >>> list(store) /// [] /// >>> list(store.named_graphs()) /// [] fn clear_graph(&self, graph_name: &PyAny, py: Python<'_>) -> PyResult<()> { let graph_name = GraphName::from(&PyGraphNameRef::try_from(graph_name)?); py.allow_threads(|| { self.inner .clear_graph(&graph_name) .map_err(map_storage_error) }) } /// Removes a graph from the store. /// /// The default graph will not be removed but just cleared. /// /// :param graph_name: the name of the name graph to remove. /// :type graph_name: NamedNode or BlankNode or DefaultGraph /// :rtype: None /// :raises OSError: if an error happens during the named graph removal. /// /// >>> store = Store() /// >>> store.add(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.named_graphs()) /// [] fn remove_graph(&self, graph_name: &PyAny, py: Python<'_>) -> PyResult<()> { let graph_name = GraphName::from(&PyGraphNameRef::try_from(graph_name)?); py.allow_threads(|| { match graph_name { GraphName::DefaultGraph => self.inner.clear_graph(GraphNameRef::DefaultGraph), GraphName::NamedNode(graph_name) => { self.inner.remove_named_graph(&graph_name).map(|_| ()) } GraphName::BlankNode(graph_name) => { self.inner.remove_named_graph(&graph_name).map(|_| ()) } } .map_err(map_storage_error) }) } /// Clears the store by removing all its contents. /// /// :rtype: None /// :raises OSError: if an error happens during the operation. /// /// >>> store = Store() /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> store.clear() /// >>> list(store) /// [] /// >>> list(store.named_graphs()) /// [] fn clear(&self, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| self.inner.clear().map_err(map_storage_error)) } /// Flushes all buffers and ensures that all writes are saved on disk. /// /// Flushes are automatically done using background threads but might lag a little bit. /// /// :rtype: None /// :raises OSError: if an error happens during the flush. fn flush(&self, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| self.inner.flush().map_err(map_storage_error)) } /// Optimizes the database for future workload. /// /// Useful to call after a batch upload or another similar operation. /// /// :rtype: None /// :raises OSError: if an error happens during the optimization. fn optimize(&self, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| self.inner.optimize().map_err(map_storage_error)) } /// Creates database backup into the `target_directory`. /// /// After its creation, the backup is usable using :py:class:`Store` constructor. /// like a regular pyxigraph database and operates independently from the original database. /// /// Warning: Backups are only possible for on-disk databases created by providing a path to :py:class:`Store` constructor. /// Temporary in-memory databases created without path are not compatible with the backup system. /// /// Warning: An error is raised if the ``target_directory`` already exists. /// /// If the target directory is in the same file system as the current database, /// the database content will not be fully copied /// but hard links will be used to point to the original database immutable snapshots. /// This allows cheap regular backups. /// /// If you want to move your data to another RDF storage system, you should have a look at the :py:func:`dump_dataset` function instead. /// /// :param target_directory: the directory name to save the database to. /// :type target_directory: str /// :rtype: None /// :raises OSError: if an error happens during the backup. fn backup(&self, target_directory: &str, py: Python<'_>) -> PyResult<()> { py.allow_threads(|| { self.inner .backup(target_directory) .map_err(map_storage_error) }) } fn __str__(&self, py: Python<'_>) -> String { py.allow_threads(|| self.inner.to_string()) } fn __bool__(&self) -> PyResult { Ok(!self.inner.is_empty().map_err(map_storage_error)?) } fn __len__(&self) -> PyResult { self.inner.len().map_err(map_storage_error) } fn __contains__(&self, quad: &PyQuad) -> PyResult { self.inner.contains(quad).map_err(map_storage_error) } fn __iter__(&self) -> QuadIter { QuadIter { inner: self.inner.iter(), } } } #[pyclass(unsendable, module = "pyoxigraph")] pub struct QuadIter { inner: store::QuadIter, } #[pymethods] impl QuadIter { fn __iter__(slf: PyRef<'_, Self>) -> PyRef { slf } fn __next__(&mut self) -> PyResult> { self.inner .next() .map(|q| Ok(q.map_err(map_storage_error)?.into())) .transpose() } } #[pyclass(unsendable, module = "pyoxigraph")] pub struct GraphNameIter { inner: store::GraphNameIter, } #[pymethods] impl GraphNameIter { fn __iter__(slf: PyRef<'_, Self>) -> PyRef { slf } fn __next__(&mut self) -> PyResult> { self.inner .next() .map(|q| Ok(q.map_err(map_storage_error)?.into())) .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(TryFrom::try_from(subject)?) }, if predicate.is_none() { None } else { Some(TryFrom::try_from(predicate)?) }, if object.is_none() { None } else { Some(TryFrom::try_from(object)?) }, if let Some(graph_name) = graph_name { if graph_name.is_none() { None } else { Some(TryFrom::try_from(graph_name)?) } } else { None }, )) } pub fn map_storage_error(error: StorageError) -> PyErr { match error { StorageError::Io(error) => error.into(), _ => PyRuntimeError::new_err(error.to_string()), } } pub fn map_loader_error(error: LoaderError) -> PyErr { match error { LoaderError::Storage(error) => map_storage_error(error), LoaderError::Parsing(error) => map_parse_error(error), LoaderError::InvalidBaseIri { .. } => PyValueError::new_err(error.to_string()), } } pub fn map_serializer_error(error: SerializerError) -> PyErr { match error { SerializerError::Storage(error) => map_storage_error(error), SerializerError::Io(error) => error.into(), SerializerError::DatasetFormatExpected(_) => PyValueError::new_err(error.to_string()), } } enum PythonOrStorageError { Python(PyErr), Storage(StorageError), } impl From for PythonOrStorageError { fn from(error: PyErr) -> Self { Self::Python(error) } } impl From for PythonOrStorageError { fn from(error: StorageError) -> Self { Self::Storage(error) } } impl From for PyErr { fn from(error: PythonOrStorageError) -> Self { match error { PythonOrStorageError::Python(error) => error, PythonOrStorageError::Storage(error) => map_storage_error(error), } } }