parent
fe92703242
commit
f74bc12a18
@ -0,0 +1,31 @@ |
|||||||
|
[package] |
||||||
|
name = "oxigraph_python" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||||
|
license = "MIT/Apache-2.0" |
||||||
|
readme = "README.md" |
||||||
|
keywords = ["RDF", "N-Triples", "Turtle", "RDF/XML", "SPARQL"] |
||||||
|
repository = "https://github.com/oxigraph/oxigraph/tree/master/python" |
||||||
|
description = """ |
||||||
|
Python bindings of Oxigraph |
||||||
|
""" |
||||||
|
edition = "2018" |
||||||
|
|
||||||
|
[lib] |
||||||
|
crate-type = ["cdylib"] |
||||||
|
name = "oxigraph" |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
oxigraph = {path = "../lib", features=["sled"]} |
||||||
|
pyo3 = {version="0.11", features = ["extension-module"]} |
||||||
|
|
||||||
|
[package.metadata.maturin] |
||||||
|
classifier = [ |
||||||
|
"Development Status :: 2 - Pre-Alpha", |
||||||
|
"Intended Audience :: Developers", |
||||||
|
"License :: OSI Approved :: Apache Software License", |
||||||
|
"License :: OSI Approved :: MIT License", |
||||||
|
"Programming Language :: Python :: 3 :: Only", |
||||||
|
"Programming Language :: Rust", |
||||||
|
"Topic :: Database :: Database Engines/Servers" |
||||||
|
] |
@ -0,0 +1,233 @@ |
|||||||
|
Oxigraph for Python |
||||||
|
=================== |
||||||
|
|
||||||
|
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||||
|
|
||||||
|
This package provides a Python API on top of Oxigraph. |
||||||
|
|
||||||
|
Oxigraph is a work in progress graph database written in Rust implementing the [SPARQL](https://www.w3.org/TR/sparql11-overview/) standard. |
||||||
|
|
||||||
|
It offers two stores with [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/) capabilities. |
||||||
|
One of the store is in-memory, and the other one is disk based. |
||||||
|
|
||||||
|
The store is also able to load RDF serialized in [Turtle](https://www.w3.org/TR/turtle/), [TriG](https://www.w3.org/TR/trig/), [N-Triples](https://www.w3.org/TR/n-triples/), [N-Quads](https://www.w3.org/TR/n-quads/) and [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/). |
||||||
|
|
||||||
|
## Install |
||||||
|
|
||||||
|
To install the development version of Oxigraph you need first to install the build tool [Maturin](https://github.com/PyO3/maturin). |
||||||
|
This could be done using the usual `pip install maturin`. |
||||||
|
|
||||||
|
Then you just need to run `maturin develop` to install Oxigraph in the current Python environment. |
||||||
|
|
||||||
|
|
||||||
|
## Example |
||||||
|
|
||||||
|
Insert the triple `<http://example/> <http://schema.org/name> "example"` and print the name of `<http://example/>` in SPARQL: |
||||||
|
```python |
||||||
|
from oxigraph import * |
||||||
|
|
||||||
|
store = MemoryStore() |
||||||
|
ex = NamedNode('http://example/') |
||||||
|
schemaName = NamedNode('http://schema.org/name') |
||||||
|
store.add((ex, schemaName, Literal('example'))) |
||||||
|
for binding in store.query('SELECT ?name WHERE { <http://example/> <http://schema.org/name> ?name }'): |
||||||
|
print(binding['name'].value) |
||||||
|
``` |
||||||
|
|
||||||
|
## API |
||||||
|
|
||||||
|
### Model |
||||||
|
|
||||||
|
Oxigraph provides python classes for the basic RDF model elements. |
||||||
|
|
||||||
|
#### `NamedNode` |
||||||
|
|
||||||
|
An RDF [IRI](https://www.w3.org/TR/rdf11-concepts/#dfn-iri). |
||||||
|
```python |
||||||
|
from oxigraph import NamedNode |
||||||
|
|
||||||
|
assert NamedNode('http://example.com/foo').value == 'http://example.com/foo' |
||||||
|
assert str(NamedNode('http://example.com/foo')) == '<http://example.com/foo>' |
||||||
|
``` |
||||||
|
|
||||||
|
#### `BlankNode` |
||||||
|
|
||||||
|
An RDF [blank node](https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node). |
||||||
|
```python |
||||||
|
from oxigraph import BlankNode |
||||||
|
|
||||||
|
assert BlankNode('foo').value == 'foo' |
||||||
|
assert str(BlankNode('foo')) == 'foo' |
||||||
|
``` |
||||||
|
|
||||||
|
#### `Literal` |
||||||
|
|
||||||
|
An RDF [literal](https://www.w3.org/TR/rdf11-concepts/#dfn-literal). |
||||||
|
```python |
||||||
|
from oxigraph import NamedNode, Literal |
||||||
|
|
||||||
|
assert Literal('foo').value == 'foo' |
||||||
|
assert str(NamedNode('foo')) == '"foo"' |
||||||
|
|
||||||
|
assert Literal('foo', language='en').language == 'en' |
||||||
|
assert str(NamedNode('foo', language='en')) == '"foo"@en' |
||||||
|
|
||||||
|
assert Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer')).datatype == 'http://www.w3.org/2001/XMLSchema#integer' |
||||||
|
assert str(Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer'))) == '"foo"^^<http://www.w3.org/2001/XMLSchema#integer>' |
||||||
|
``` |
||||||
|
|
||||||
|
#### `DefaultGraph` |
||||||
|
|
||||||
|
The RDF [default graph name](https://www.w3.org/TR/rdf11-concepts/#dfn-default-graph). |
||||||
|
```python |
||||||
|
from oxigraph import DefaultGraph |
||||||
|
|
||||||
|
DefaultGraph() |
||||||
|
``` |
||||||
|
|
||||||
|
### Stores |
||||||
|
|
||||||
|
Oxigraph provides two stores: |
||||||
|
|
||||||
|
* `MemoryStore` that stores the RDF quads in memory |
||||||
|
* `SledStore` that stores the graph on disk using [Sled](https://github.com/spacejam/sled). |
||||||
|
|
||||||
|
Both stores provide a similar API. They encode an [RDF dataset](https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset). |
||||||
|
|
||||||
|
#### Constructor |
||||||
|
|
||||||
|
##### `MemoryStore` |
||||||
|
|
||||||
|
It could be constructed using: |
||||||
|
```python |
||||||
|
from oxigraph import MemoryStore |
||||||
|
|
||||||
|
store = MemoryStore() |
||||||
|
``` |
||||||
|
|
||||||
|
##### `SledStore` |
||||||
|
|
||||||
|
The following code creates a store using the directory `foo/bar` for storage. |
||||||
|
```python |
||||||
|
from oxigraph import SledStore |
||||||
|
|
||||||
|
store = SledStore('foo/bar') |
||||||
|
``` |
||||||
|
|
||||||
|
It is also possible to use a temporary directory that will be removed when the `SledStore` Python object is dropped: |
||||||
|
```python |
||||||
|
from oxigraph import SledStore |
||||||
|
|
||||||
|
store = SledStore() |
||||||
|
``` |
||||||
|
|
||||||
|
#### `add` |
||||||
|
|
||||||
|
To add a quad in the store: |
||||||
|
```python |
||||||
|
s = NamedNode('http://example.com/subject') |
||||||
|
p = NamedNode('http://example.com/predicate') |
||||||
|
o = NamedNode('http://example.com/object') |
||||||
|
g = NamedNode('http://example.com/graph') |
||||||
|
store.add((s, p, o, g)) |
||||||
|
``` |
||||||
|
|
||||||
|
If a triple is provided, it is added to the default graph i.e. `store.add((s, p, o, g))` is the same as `store.add((s, p, o, DefaultGraph()))` |
||||||
|
|
||||||
|
#### `remove` |
||||||
|
|
||||||
|
To remove a quad from the store: |
||||||
|
```python |
||||||
|
store.remove((s, p, o, g)) |
||||||
|
``` |
||||||
|
|
||||||
|
#### `__contains__` |
||||||
|
|
||||||
|
Checks if a quad is in the store: |
||||||
|
```python |
||||||
|
assert (s, p, o, g) in store |
||||||
|
``` |
||||||
|
|
||||||
|
#### `__len__` |
||||||
|
|
||||||
|
Returns the number of quads in the store: |
||||||
|
```python |
||||||
|
assert len(store) == 1 |
||||||
|
``` |
||||||
|
|
||||||
|
#### `__iter__` |
||||||
|
|
||||||
|
Iterates on all quads in the store: |
||||||
|
```python |
||||||
|
assert list(iter(store)) == [(s, p, o, g)] |
||||||
|
``` |
||||||
|
|
||||||
|
#### `match` |
||||||
|
|
||||||
|
Returns all the quads matching a given pattern using an iterator. |
||||||
|
|
||||||
|
Return all the quads with the subject `s`: |
||||||
|
```python |
||||||
|
assert list(store.match(s, None, None, None)) == [(s, p, o, g)] |
||||||
|
``` |
||||||
|
|
||||||
|
Return all the quads in the default graph: |
||||||
|
```python |
||||||
|
assert list(store.match(s, None, None, DefaultGraph())) == [] |
||||||
|
``` |
||||||
|
|
||||||
|
#### `query` |
||||||
|
|
||||||
|
Executes a [SPARQL 1.1 Query](https://www.w3.org/TR/sparql11-query/). |
||||||
|
|
||||||
|
The `ASK` queries return a boolean: |
||||||
|
```python |
||||||
|
assert store.query('ASK { ?s ?s ?s }') |
||||||
|
``` |
||||||
|
|
||||||
|
The `SELECT` queries return an iterator of query solutions that could be indexed by variable name or position in the `SELECT` clause: |
||||||
|
```python |
||||||
|
solutions = list(store.query('SELECT ?s WHERE { ?s ?p ?o }')) |
||||||
|
assert solutions[0][0] == s |
||||||
|
assert solutions[0]['s'] == s |
||||||
|
``` |
||||||
|
|
||||||
|
The `CONSTRUCT` and `DESCRIBE` queries return an iterator of query solutions that could be indexed by variable name or position in the `SELECT` clause: |
||||||
|
```python |
||||||
|
solutions = list(store.query('SELECT ?s WHERE { ?s ?p ?o }')) |
||||||
|
assert solutions[0][0] == s |
||||||
|
assert solutions[0]['s'] == s |
||||||
|
``` |
||||||
|
|
||||||
|
### `load` |
||||||
|
|
||||||
|
Loads serialized RDF triples or quad into the store. |
||||||
|
The method arguments are: |
||||||
|
1. `data`: the serialized RDF triples or quads. |
||||||
|
2. `mime_type`: the MIME type of the serialization. See below for the supported mime types. |
||||||
|
3. `base_iri`: the base IRI used to resolve the relative IRIs in the serialization. |
||||||
|
4. `to_named_graph`: for triple serialization formats, the name of the named graph the triple should be loaded to. |
||||||
|
|
||||||
|
The available formats are: |
||||||
|
* [Turtle](https://www.w3.org/TR/turtle/): `text/turtle` |
||||||
|
* [TriG](https://www.w3.org/TR/trig/): `application/trig` |
||||||
|
* [N-Triples](https://www.w3.org/TR/n-triples/): `application/n-triples` |
||||||
|
* [N-Quads](https://www.w3.org/TR/n-quads/): `application/n-quads` |
||||||
|
* [RDF XML](https://www.w3.org/TR/rdf-syntax-grammar/): `application/rdf+xml` |
||||||
|
|
||||||
|
Example of loading a Turtle file into the named graph `<http://example.com/graph>` with the base IRI `http://example.com`: |
||||||
|
```python |
||||||
|
store.load('<http://example.com> <http://example.com> <> .', mime_type='text/turtle', base_iri="http://example.com", to_graph=NamedNode('http://example.com/graph')) |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
## How to contribute |
||||||
|
|
||||||
|
The Oxigraph bindings are written in Rust using [PyO3](https://github.com/PyO3/pyo3). |
||||||
|
|
||||||
|
They are build using [Maturin](https://github.com/PyO3/maturin). |
||||||
|
Maturin could be installed using the usual `pip install maturin`. |
||||||
|
To install development version of Oxigraph just run `maturin develop`. |
||||||
|
|
||||||
|
The Python bindings tests are written in Python. |
||||||
|
To run them use the usual `python -m unittest` in the `tests` directory. |
@ -0,0 +1,3 @@ |
|||||||
|
[build-system] |
||||||
|
requires = ["maturin"] |
||||||
|
build-backend = "maturin" |
@ -0,0 +1,31 @@ |
|||||||
|
#![deny(
|
||||||
|
future_incompatible, |
||||||
|
nonstandard_style, |
||||||
|
rust_2018_idioms, |
||||||
|
trivial_casts, |
||||||
|
trivial_numeric_casts, |
||||||
|
unsafe_code, |
||||||
|
unused_qualifications |
||||||
|
)] |
||||||
|
|
||||||
|
mod memory_store; |
||||||
|
mod model; |
||||||
|
mod sled_store; |
||||||
|
mod store_utils; |
||||||
|
|
||||||
|
use crate::memory_store::*; |
||||||
|
use crate::model::*; |
||||||
|
use crate::sled_store::*; |
||||||
|
use pyo3::prelude::*; |
||||||
|
|
||||||
|
/// Oxigraph library
|
||||||
|
#[pymodule] |
||||||
|
fn oxigraph(_py: Python<'_>, module: &PyModule) -> PyResult<()> { |
||||||
|
module.add_class::<PyNamedNode>()?; |
||||||
|
module.add_class::<PyBlankNode>()?; |
||||||
|
module.add_class::<PyLiteral>()?; |
||||||
|
module.add_class::<PyDefaultGraph>()?; |
||||||
|
module.add_class::<PyMemoryStore>()?; |
||||||
|
module.add_class::<PySledStore>()?; |
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,168 @@ |
|||||||
|
use crate::model::*; |
||||||
|
use crate::store_utils::*; |
||||||
|
use oxigraph::model::*; |
||||||
|
use oxigraph::sparql::QueryOptions; |
||||||
|
use oxigraph::{DatasetSyntax, FileSyntax, GraphSyntax, MemoryStore}; |
||||||
|
use pyo3::basic::CompareOp; |
||||||
|
use pyo3::exceptions::{NotImplementedError, RuntimeError, ValueError}; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use pyo3::types::PyTuple; |
||||||
|
use pyo3::{PyIterProtocol, PyObjectProtocol, PySequenceProtocol}; |
||||||
|
use std::io::Cursor; |
||||||
|
|
||||||
|
#[pyclass(name = MemoryStore)] |
||||||
|
#[derive(Eq, PartialEq, Clone)] |
||||||
|
pub struct PyMemoryStore { |
||||||
|
inner: MemoryStore, |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PyMemoryStore { |
||||||
|
#[new] |
||||||
|
fn new() -> Self { |
||||||
|
Self { |
||||||
|
inner: MemoryStore::new(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn add(&self, quad: &PyTuple) -> PyResult<()> { |
||||||
|
self.inner.insert(extract_quad(quad)?); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn remove(&self, quad: &PyTuple) -> PyResult<()> { |
||||||
|
self.inner.remove(&extract_quad(quad)?); |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
fn r#match( |
||||||
|
&self, |
||||||
|
subject: &PyAny, |
||||||
|
predicate: &PyAny, |
||||||
|
object: &PyAny, |
||||||
|
graph_name: Option<&PyAny>, |
||||||
|
) -> PyResult<QuadIter> { |
||||||
|
let (subject, predicate, object, graph_name) = |
||||||
|
extract_quads_pattern(subject, predicate, object, graph_name)?; |
||||||
|
Ok(QuadIter { |
||||||
|
inner: Box::new(self.inner.quads_for_pattern( |
||||||
|
subject.as_ref(), |
||||||
|
predicate.as_ref(), |
||||||
|
object.as_ref(), |
||||||
|
graph_name.as_ref(), |
||||||
|
)), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fn query(&self, query: &str, py: Python<'_>) -> PyResult<PyObject> { |
||||||
|
let query = self |
||||||
|
.inner |
||||||
|
.prepare_query(query, QueryOptions::default()) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string()))?; |
||||||
|
let results = query |
||||||
|
.exec() |
||||||
|
.map_err(|e| RuntimeError::py_err(e.to_string()))?; |
||||||
|
query_results_to_python(py, results, RuntimeError::py_err) |
||||||
|
} |
||||||
|
|
||||||
|
#[args(data, mime_type, "*", base_iri = "\"\"", to_graph = "None")] |
||||||
|
fn load( |
||||||
|
&self, |
||||||
|
data: &str, |
||||||
|
mime_type: &str, |
||||||
|
base_iri: &str, |
||||||
|
to_graph: Option<&PyAny>, |
||||||
|
) -> PyResult<()> { |
||||||
|
let to_graph_name = if let Some(graph_name) = to_graph { |
||||||
|
Some(extract_graph_name(graph_name)?) |
||||||
|
} else { |
||||||
|
None |
||||||
|
}; |
||||||
|
let base_iri = if base_iri.is_empty() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(base_iri) |
||||||
|
}; |
||||||
|
|
||||||
|
if let Some(graph_syntax) = GraphSyntax::from_mime_type(mime_type) { |
||||||
|
self.inner |
||||||
|
.load_graph( |
||||||
|
Cursor::new(data), |
||||||
|
graph_syntax, |
||||||
|
&to_graph_name.unwrap_or(GraphName::DefaultGraph), |
||||||
|
base_iri, |
||||||
|
) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string())) |
||||||
|
} else if let Some(dataset_syntax) = DatasetSyntax::from_mime_type(mime_type) { |
||||||
|
if to_graph_name.is_some() { |
||||||
|
return Err(ValueError::py_err( |
||||||
|
"The target graph name parameter is not available for dataset formats", |
||||||
|
)); |
||||||
|
} |
||||||
|
self.inner |
||||||
|
.load_dataset(Cursor::new(data), dataset_syntax, base_iri) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string())) |
||||||
|
} else { |
||||||
|
Err(ValueError::py_err(format!( |
||||||
|
"Not supported MIME type: {}", |
||||||
|
mime_type |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PyMemoryStore { |
||||||
|
fn __str__(&self) -> String { |
||||||
|
self.inner.to_string() |
||||||
|
} |
||||||
|
|
||||||
|
fn __richcmp__(&self, other: &PyCell<Self>, op: CompareOp) -> PyResult<bool> { |
||||||
|
let other: &PyMemoryStore = &other.borrow(); |
||||||
|
match op { |
||||||
|
CompareOp::Eq => Ok(self == other), |
||||||
|
CompareOp::Ne => Ok(self != other), |
||||||
|
_ => Err(NotImplementedError::py_err("Ordering is not implemented")), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn __bool__(&self) -> bool { |
||||||
|
!self.inner.is_empty() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PySequenceProtocol for PyMemoryStore { |
||||||
|
fn __len__(&self) -> usize { |
||||||
|
self.inner.len() |
||||||
|
} |
||||||
|
|
||||||
|
fn __contains__(&self, quad: &PyTuple) -> PyResult<bool> { |
||||||
|
Ok(self.inner.contains(&extract_quad(quad)?)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for PyMemoryStore { |
||||||
|
fn __iter__(slf: PyRef<Self>) -> QuadIter { |
||||||
|
QuadIter { |
||||||
|
inner: Box::new(slf.inner.quads_for_pattern(None, None, None, None)), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(unsendable)] |
||||||
|
pub struct QuadIter { |
||||||
|
inner: Box<dyn Iterator<Item = Quad>>, |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for QuadIter { |
||||||
|
fn __iter__(slf: PyRefMut<Self>) -> Py<Self> { |
||||||
|
slf.into() |
||||||
|
} |
||||||
|
|
||||||
|
fn __next__(mut slf: PyRefMut<Self>) -> Option<(PyObject, PyObject, PyObject, PyObject)> { |
||||||
|
slf.inner.next().map(move |q| quad_to_python(slf.py(), q)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,422 @@ |
|||||||
|
use oxigraph::model::*; |
||||||
|
use pyo3::basic::CompareOp; |
||||||
|
use pyo3::exceptions::{NotImplementedError, TypeError, ValueError}; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use pyo3::types::PyTuple; |
||||||
|
use pyo3::PyObjectProtocol; |
||||||
|
use std::collections::hash_map::DefaultHasher; |
||||||
|
use std::hash::Hash; |
||||||
|
use std::hash::Hasher; |
||||||
|
|
||||||
|
#[pyclass(name = NamedNode)] |
||||||
|
#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)] |
||||||
|
pub struct PyNamedNode { |
||||||
|
inner: NamedNode, |
||||||
|
} |
||||||
|
|
||||||
|
impl From<NamedNode> for PyNamedNode { |
||||||
|
fn from(inner: NamedNode) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyNamedNode> for NamedNode { |
||||||
|
fn from(node: PyNamedNode) -> Self { |
||||||
|
node.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyNamedNode> for NamedOrBlankNode { |
||||||
|
fn from(node: PyNamedNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyNamedNode> for Term { |
||||||
|
fn from(node: PyNamedNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyNamedNode> for GraphName { |
||||||
|
fn from(node: PyNamedNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PyNamedNode { |
||||||
|
#[new] |
||||||
|
fn new(value: String) -> PyResult<Self> { |
||||||
|
Ok(NamedNode::new(value) |
||||||
|
.map_err(|e| ValueError::py_err(e.to_string()))? |
||||||
|
.into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn value(&self) -> &str { |
||||||
|
self.inner.as_str() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PyNamedNode { |
||||||
|
fn __str__(&self) -> String { |
||||||
|
self.inner.to_string() |
||||||
|
} |
||||||
|
|
||||||
|
fn __repr__(&self) -> String { |
||||||
|
format!("<NamedNode value={}>", self.inner.as_str()) |
||||||
|
} |
||||||
|
|
||||||
|
fn __hash__(&self) -> u64 { |
||||||
|
hash(&self.inner) |
||||||
|
} |
||||||
|
|
||||||
|
fn __richcmp__(&self, other: &PyCell<Self>, op: CompareOp) -> bool { |
||||||
|
eq_ord_compare(self, &other.borrow(), op) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(name = BlankNode)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct PyBlankNode { |
||||||
|
inner: BlankNode, |
||||||
|
} |
||||||
|
|
||||||
|
impl From<BlankNode> for PyBlankNode { |
||||||
|
fn from(inner: BlankNode) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyBlankNode> for BlankNode { |
||||||
|
fn from(node: PyBlankNode) -> Self { |
||||||
|
node.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyBlankNode> for NamedOrBlankNode { |
||||||
|
fn from(node: PyBlankNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyBlankNode> for Term { |
||||||
|
fn from(node: PyBlankNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyBlankNode> for GraphName { |
||||||
|
fn from(node: PyBlankNode) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PyBlankNode { |
||||||
|
#[new] |
||||||
|
fn new(value: Option<String>) -> PyResult<Self> { |
||||||
|
Ok(if let Some(value) = value { |
||||||
|
BlankNode::new(value).map_err(|e| ValueError::py_err(e.to_string()))? |
||||||
|
} else { |
||||||
|
BlankNode::default() |
||||||
|
} |
||||||
|
.into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn value(&self) -> &str { |
||||||
|
self.inner.as_str() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PyBlankNode { |
||||||
|
fn __str__(&self) -> String { |
||||||
|
self.inner.to_string() |
||||||
|
} |
||||||
|
|
||||||
|
fn __repr__(&self) -> String { |
||||||
|
format!("<BlankNode value={}>", self.inner.as_str()) |
||||||
|
} |
||||||
|
|
||||||
|
fn __hash__(&self) -> u64 { |
||||||
|
hash(&self.inner) |
||||||
|
} |
||||||
|
|
||||||
|
fn __richcmp__(&self, other: &PyCell<Self>, op: CompareOp) -> PyResult<bool> { |
||||||
|
eq_compare(self, &other.borrow(), op) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(name = Literal)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||||
|
pub struct PyLiteral { |
||||||
|
inner: Literal, |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Literal> for PyLiteral { |
||||||
|
fn from(inner: Literal) -> Self { |
||||||
|
Self { inner } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyLiteral> for Literal { |
||||||
|
fn from(literal: PyLiteral) -> Self { |
||||||
|
literal.inner |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<PyLiteral> for Term { |
||||||
|
fn from(node: PyLiteral) -> Self { |
||||||
|
node.inner.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PyLiteral { |
||||||
|
#[new] |
||||||
|
#[args(value, "*", language = "None", datatype = "None")] |
||||||
|
fn new( |
||||||
|
value: String, |
||||||
|
language: Option<String>, |
||||||
|
datatype: Option<PyNamedNode>, |
||||||
|
) -> PyResult<Self> { |
||||||
|
Ok(if let Some(language) = language { |
||||||
|
if let Some(datatype) = datatype { |
||||||
|
if datatype.value() != "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString" { |
||||||
|
return Err(ValueError::py_err( |
||||||
|
"The literals with a language tag must use the rdf:langString datatype", |
||||||
|
)); |
||||||
|
} |
||||||
|
} |
||||||
|
Literal::new_language_tagged_literal(value, language) |
||||||
|
.map_err(|e| ValueError::py_err(e.to_string()))? |
||||||
|
} else if let Some(datatype) = datatype { |
||||||
|
Literal::new_typed_literal(value, datatype) |
||||||
|
} else { |
||||||
|
Literal::new_simple_literal(value) |
||||||
|
} |
||||||
|
.into()) |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn value(&self) -> &str { |
||||||
|
self.inner.value() |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn language(&self) -> Option<&str> { |
||||||
|
self.inner.language() |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn datatype(&self) -> PyNamedNode { |
||||||
|
self.inner.datatype().clone().into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PyLiteral { |
||||||
|
fn __str__(&self) -> String { |
||||||
|
self.inner.to_string() |
||||||
|
} |
||||||
|
|
||||||
|
fn __repr__(&self) -> String { |
||||||
|
format!( |
||||||
|
"<Literal value={} language={} datatype={}>", |
||||||
|
self.inner.value(), |
||||||
|
self.inner.language().unwrap_or(""), |
||||||
|
self.inner.datatype().as_str() |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
fn __hash__(&self) -> u64 { |
||||||
|
hash(&self.inner) |
||||||
|
} |
||||||
|
|
||||||
|
fn __richcmp__(&self, other: &PyCell<Self>, op: CompareOp) -> PyResult<bool> { |
||||||
|
eq_compare(self, &other.borrow(), op) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(name = DefaultGraph)] |
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] |
||||||
|
pub struct PyDefaultGraph {} |
||||||
|
|
||||||
|
impl From<PyDefaultGraph> for GraphName { |
||||||
|
fn from(_: PyDefaultGraph) -> Self { |
||||||
|
GraphName::DefaultGraph |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PyDefaultGraph { |
||||||
|
#[new] |
||||||
|
fn new() -> Self { |
||||||
|
PyDefaultGraph {} |
||||||
|
} |
||||||
|
|
||||||
|
#[getter] |
||||||
|
fn value(&self) -> &str { |
||||||
|
"" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PyDefaultGraph { |
||||||
|
fn __str__(&self) -> &'p str { |
||||||
|
"DEFAULT" |
||||||
|
} |
||||||
|
|
||||||
|
fn __repr__(&self) -> &'p str { |
||||||
|
"<DefaultGraph>" |
||||||
|
} |
||||||
|
|
||||||
|
fn __hash__(&self) -> u64 { |
||||||
|
0 |
||||||
|
} |
||||||
|
|
||||||
|
fn __richcmp__(&self, other: &PyCell<Self>, op: CompareOp) -> PyResult<bool> { |
||||||
|
eq_compare(self, &other.borrow(), op) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn extract_named_node(py: &PyAny) -> PyResult<NamedNode> { |
||||||
|
if let Ok(node) = py.downcast::<PyCell<PyNamedNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else { |
||||||
|
Err(TypeError::py_err(format!( |
||||||
|
"{} is not a RDF named node", |
||||||
|
py.get_type().name(), |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn extract_named_or_blank_node(py: &PyAny) -> PyResult<NamedOrBlankNode> { |
||||||
|
if let Ok(node) = py.downcast::<PyCell<PyNamedNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else if let Ok(node) = py.downcast::<PyCell<PyBlankNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else { |
||||||
|
Err(TypeError::py_err(format!( |
||||||
|
"{} is not a RDF named or blank node", |
||||||
|
py.get_type().name(), |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn named_or_blank_node_to_python(py: Python<'_>, node: NamedOrBlankNode) -> PyObject { |
||||||
|
match node { |
||||||
|
NamedOrBlankNode::NamedNode(node) => PyNamedNode::from(node).into_py(py), |
||||||
|
NamedOrBlankNode::BlankNode(node) => PyBlankNode::from(node).into_py(py), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn extract_term(py: &PyAny) -> PyResult<Term> { |
||||||
|
if let Ok(node) = py.downcast::<PyCell<PyNamedNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else if let Ok(node) = py.downcast::<PyCell<PyBlankNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else if let Ok(literal) = py.downcast::<PyCell<PyLiteral>>() { |
||||||
|
Ok(literal.borrow().clone().into()) |
||||||
|
} else { |
||||||
|
Err(TypeError::py_err(format!( |
||||||
|
"{} is not a RDF named or blank node", |
||||||
|
py.get_type().name(), |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn term_to_python(py: Python<'_>, term: Term) -> PyObject { |
||||||
|
match term { |
||||||
|
Term::NamedNode(node) => PyNamedNode::from(node).into_py(py), |
||||||
|
Term::BlankNode(node) => PyBlankNode::from(node).into_py(py), |
||||||
|
Term::Literal(literal) => PyLiteral::from(literal).into_py(py), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn extract_graph_name(py: &PyAny) -> PyResult<GraphName> { |
||||||
|
if let Ok(node) = py.downcast::<PyCell<PyNamedNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else if let Ok(node) = py.downcast::<PyCell<PyBlankNode>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else if let Ok(node) = py.downcast::<PyCell<PyDefaultGraph>>() { |
||||||
|
Ok(node.borrow().clone().into()) |
||||||
|
} else { |
||||||
|
Err(TypeError::py_err(format!( |
||||||
|
"{} is not a valid RDF graph name", |
||||||
|
py.get_type().name(), |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn graph_name_to_python(py: Python<'_>, name: GraphName) -> PyObject { |
||||||
|
match name { |
||||||
|
GraphName::NamedNode(node) => PyNamedNode::from(node).into_py(py), |
||||||
|
GraphName::BlankNode(node) => PyBlankNode::from(node).into_py(py), |
||||||
|
GraphName::DefaultGraph => PyDefaultGraph::new().into_py(py), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn triple_to_python(py: Python<'_>, triple: Triple) -> (PyObject, PyObject, PyObject) { |
||||||
|
( |
||||||
|
named_or_blank_node_to_python(py, triple.subject), |
||||||
|
PyNamedNode::from(triple.predicate).into_py(py), |
||||||
|
term_to_python(py, triple.object), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn extract_quad(tuple: &PyTuple) -> PyResult<Quad> { |
||||||
|
let len = tuple.len(); |
||||||
|
if len != 3 && len != 4 { |
||||||
|
return Err(TypeError::py_err( |
||||||
|
"A quad should be tuple with 3 or 4 elements", |
||||||
|
)); |
||||||
|
} |
||||||
|
Ok(Quad { |
||||||
|
subject: extract_named_or_blank_node(tuple.get_item(0))?, |
||||||
|
predicate: extract_named_node(tuple.get_item(1))?, |
||||||
|
object: extract_term(tuple.get_item(2))?, |
||||||
|
graph_name: if len == 4 { |
||||||
|
extract_graph_name(tuple.get_item(3))? |
||||||
|
} else { |
||||||
|
GraphName::DefaultGraph |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn quad_to_python(py: Python<'_>, quad: Quad) -> (PyObject, PyObject, PyObject, PyObject) { |
||||||
|
( |
||||||
|
named_or_blank_node_to_python(py, quad.subject), |
||||||
|
PyNamedNode::from(quad.predicate).into_py(py), |
||||||
|
term_to_python(py, quad.object), |
||||||
|
graph_name_to_python(py, quad.graph_name), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
fn eq_compare<T: Eq>(a: &T, b: &T, op: CompareOp) -> PyResult<bool> { |
||||||
|
match op { |
||||||
|
CompareOp::Eq => Ok(a == b), |
||||||
|
CompareOp::Ne => Ok(a != b), |
||||||
|
_ => Err(NotImplementedError::py_err("Ordering is not implemented")), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn eq_ord_compare<T: Eq + Ord>(a: &T, b: &T, op: CompareOp) -> bool { |
||||||
|
match op { |
||||||
|
CompareOp::Lt => a < b, |
||||||
|
CompareOp::Le => a <= b, |
||||||
|
CompareOp::Eq => a == b, |
||||||
|
CompareOp::Ne => a != b, |
||||||
|
CompareOp::Gt => a > b, |
||||||
|
CompareOp::Ge => a >= b, |
||||||
|
} |
||||||
|
} |
||||||
|
fn hash(t: &impl Hash) -> u64 { |
||||||
|
let mut s = DefaultHasher::new(); |
||||||
|
t.hash(&mut s); |
||||||
|
s.finish() |
||||||
|
} |
@ -0,0 +1,177 @@ |
|||||||
|
use crate::model::*; |
||||||
|
use crate::store_utils::*; |
||||||
|
use oxigraph::model::*; |
||||||
|
use oxigraph::sparql::QueryOptions; |
||||||
|
use oxigraph::{DatasetSyntax, FileSyntax, GraphSyntax, Result, SledStore}; |
||||||
|
use pyo3::create_exception; |
||||||
|
use pyo3::exceptions::ValueError; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use pyo3::types::PyTuple; |
||||||
|
use pyo3::{PyIterProtocol, PyObjectProtocol, PySequenceProtocol}; |
||||||
|
use std::io::Cursor; |
||||||
|
|
||||||
|
create_exception!(oxigraph, SledError, pyo3::exceptions::RuntimeError); |
||||||
|
|
||||||
|
#[pyclass(name = SledStore)] |
||||||
|
#[derive(Clone)] |
||||||
|
pub struct PySledStore { |
||||||
|
inner: SledStore, |
||||||
|
} |
||||||
|
|
||||||
|
#[pymethods] |
||||||
|
impl PySledStore { |
||||||
|
#[new] |
||||||
|
fn new(path: Option<&str>) -> PyResult<Self> { |
||||||
|
Ok(Self { |
||||||
|
inner: if let Some(path) = path { |
||||||
|
SledStore::open(path).map_err(|e| SledError::py_err(e.to_string()))? |
||||||
|
} else { |
||||||
|
SledStore::new().map_err(|e| SledError::py_err(e.to_string()))? |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fn add(&self, quad: &PyTuple) -> PyResult<()> { |
||||||
|
self.inner |
||||||
|
.insert(&extract_quad(quad)?) |
||||||
|
.map_err(|e| SledError::py_err(e.to_string())) |
||||||
|
} |
||||||
|
|
||||||
|
fn remove(&self, quad: &PyTuple) -> PyResult<()> { |
||||||
|
self.inner |
||||||
|
.remove(&extract_quad(quad)?) |
||||||
|
.map_err(|e| SledError::py_err(e.to_string())) |
||||||
|
} |
||||||
|
|
||||||
|
fn r#match( |
||||||
|
&self, |
||||||
|
subject: &PyAny, |
||||||
|
predicate: &PyAny, |
||||||
|
object: &PyAny, |
||||||
|
graph_name: Option<&PyAny>, |
||||||
|
) -> PyResult<QuadIter> { |
||||||
|
let (subject, predicate, object, graph_name) = |
||||||
|
extract_quads_pattern(subject, predicate, object, graph_name)?; |
||||||
|
Ok(QuadIter { |
||||||
|
inner: Box::new(self.inner.quads_for_pattern( |
||||||
|
subject.as_ref(), |
||||||
|
predicate.as_ref(), |
||||||
|
object.as_ref(), |
||||||
|
graph_name.as_ref(), |
||||||
|
)), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fn query(&self, query: &str, py: Python<'_>) -> PyResult<PyObject> { |
||||||
|
let query = self |
||||||
|
.inner |
||||||
|
.prepare_query(query, QueryOptions::default()) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string()))?; |
||||||
|
let results = query.exec().map_err(|e| SledError::py_err(e.to_string()))?; |
||||||
|
query_results_to_python(py, results, SledError::py_err) |
||||||
|
} |
||||||
|
|
||||||
|
#[args(data, mime_type, "*", base_iri = "\"\"", to_graph = "None")] |
||||||
|
fn load( |
||||||
|
&self, |
||||||
|
data: &str, |
||||||
|
mime_type: &str, |
||||||
|
base_iri: &str, |
||||||
|
to_graph: Option<&PyAny>, |
||||||
|
) -> PyResult<()> { |
||||||
|
let to_graph_name = if let Some(graph_name) = to_graph { |
||||||
|
Some(extract_graph_name(graph_name)?) |
||||||
|
} else { |
||||||
|
None |
||||||
|
}; |
||||||
|
let base_iri = if base_iri.is_empty() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(base_iri) |
||||||
|
}; |
||||||
|
|
||||||
|
if let Some(graph_syntax) = GraphSyntax::from_mime_type(mime_type) { |
||||||
|
self.inner |
||||||
|
.load_graph( |
||||||
|
Cursor::new(data), |
||||||
|
graph_syntax, |
||||||
|
&to_graph_name.unwrap_or(GraphName::DefaultGraph), |
||||||
|
base_iri, |
||||||
|
) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string())) |
||||||
|
} else if let Some(dataset_syntax) = DatasetSyntax::from_mime_type(mime_type) { |
||||||
|
if to_graph_name.is_some() { |
||||||
|
return Err(ValueError::py_err( |
||||||
|
"The target graph name parameter is not available for dataset formats", |
||||||
|
)); |
||||||
|
} |
||||||
|
self.inner |
||||||
|
.load_dataset(Cursor::new(data), dataset_syntax, base_iri) |
||||||
|
.map_err(|e| ParseError::py_err(e.to_string())) |
||||||
|
} else { |
||||||
|
Err(ValueError::py_err(format!( |
||||||
|
"Not supported MIME type: {}", |
||||||
|
mime_type |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyObjectProtocol for PySledStore { |
||||||
|
fn __str__(&self) -> String { |
||||||
|
self.inner.to_string() |
||||||
|
} |
||||||
|
|
||||||
|
fn __bool__(&self) -> bool { |
||||||
|
!self.inner.is_empty() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PySequenceProtocol for PySledStore { |
||||||
|
fn __len__(&self) -> usize { |
||||||
|
self.inner.len() |
||||||
|
} |
||||||
|
|
||||||
|
fn __contains__(&self, quad: &PyTuple) -> PyResult<bool> { |
||||||
|
self.inner |
||||||
|
.contains(&extract_quad(quad)?) |
||||||
|
.map_err(|e| SledError::py_err(e.to_string())) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for PySledStore { |
||||||
|
fn __iter__(slf: PyRef<Self>) -> QuadIter { |
||||||
|
QuadIter { |
||||||
|
inner: Box::new(slf.inner.quads_for_pattern(None, None, None, None)), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(unsendable)] |
||||||
|
pub struct QuadIter { |
||||||
|
inner: Box<dyn Iterator<Item = Result<Quad>>>, |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for QuadIter { |
||||||
|
fn __iter__(slf: PyRefMut<Self>) -> Py<Self> { |
||||||
|
slf.into() |
||||||
|
} |
||||||
|
|
||||||
|
fn __next__( |
||||||
|
mut slf: PyRefMut<Self>, |
||||||
|
) -> PyResult<Option<(PyObject, PyObject, PyObject, PyObject)>> { |
||||||
|
slf.inner |
||||||
|
.next() |
||||||
|
.map(move |q| { |
||||||
|
Ok(quad_to_python( |
||||||
|
slf.py(), |
||||||
|
q.map_err(|e| SledError::py_err(e.to_string()))?, |
||||||
|
)) |
||||||
|
}) |
||||||
|
.transpose() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
use crate::model::*; |
||||||
|
use oxigraph::model::*; |
||||||
|
use oxigraph::sparql::{QueryResult, QuerySolution}; |
||||||
|
use oxigraph::Result; |
||||||
|
use pyo3::exceptions::TypeError; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use pyo3::{create_exception, PyIterProtocol, PyMappingProtocol, PyNativeType}; |
||||||
|
use std::vec::IntoIter; |
||||||
|
|
||||||
|
create_exception!(oxigraph, ParseError, pyo3::exceptions::Exception); |
||||||
|
|
||||||
|
pub fn extract_quads_pattern( |
||||||
|
subject: &PyAny, |
||||||
|
predicate: &PyAny, |
||||||
|
object: &PyAny, |
||||||
|
graph_name: Option<&PyAny>, |
||||||
|
) -> PyResult<( |
||||||
|
Option<NamedOrBlankNode>, |
||||||
|
Option<NamedNode>, |
||||||
|
Option<Term>, |
||||||
|
Option<GraphName>, |
||||||
|
)> { |
||||||
|
Ok(( |
||||||
|
if subject.is_none() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(extract_named_or_blank_node(subject)?) |
||||||
|
}, |
||||||
|
if predicate.is_none() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(extract_named_node(predicate)?) |
||||||
|
}, |
||||||
|
if object.is_none() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(extract_term(object)?) |
||||||
|
}, |
||||||
|
if let Some(graph_name) = graph_name { |
||||||
|
if graph_name.is_none() { |
||||||
|
None |
||||||
|
} else { |
||||||
|
Some(extract_graph_name(graph_name)?) |
||||||
|
} |
||||||
|
} else { |
||||||
|
None |
||||||
|
}, |
||||||
|
)) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn query_results_to_python( |
||||||
|
py: Python<'_>, |
||||||
|
results: QueryResult<'_>, |
||||||
|
error: impl Fn(String) -> PyErr, |
||||||
|
) -> PyResult<PyObject> { |
||||||
|
Ok(match results { |
||||||
|
QueryResult::Solutions(solutions) => QuerySolutionIter { |
||||||
|
inner: solutions |
||||||
|
.collect::<Result<Vec<_>>>() |
||||||
|
.map_err(|e| error(e.to_string()))? |
||||||
|
.into_iter(), |
||||||
|
} |
||||||
|
.into_py(py), |
||||||
|
QueryResult::Graph(triples) => TripleResultIter { |
||||||
|
inner: triples |
||||||
|
.collect::<Result<Vec<_>>>() |
||||||
|
.map_err(|e| error(e.to_string()))? |
||||||
|
.into_iter(), |
||||||
|
} |
||||||
|
.into_py(py), |
||||||
|
QueryResult::Boolean(b) => b.into_py(py), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(unsendable)] |
||||||
|
pub struct PyQuerySolution { |
||||||
|
inner: QuerySolution, |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyMappingProtocol for PyQuerySolution { |
||||||
|
fn __len__(&self) -> usize { |
||||||
|
self.inner.len() |
||||||
|
} |
||||||
|
|
||||||
|
fn __getitem__(&self, input: &PyAny) -> PyResult<Option<PyObject>> { |
||||||
|
if let Ok(key) = usize::extract(input) { |
||||||
|
Ok(self |
||||||
|
.inner |
||||||
|
.get(key) |
||||||
|
.map(|term| term_to_python(input.py(), term.clone()))) |
||||||
|
} else if let Ok(key) = <&str>::extract(input) { |
||||||
|
Ok(self |
||||||
|
.inner |
||||||
|
.get(key) |
||||||
|
.map(|term| term_to_python(input.py(), term.clone()))) |
||||||
|
} else { |
||||||
|
Err(TypeError::py_err(format!( |
||||||
|
"{} is not an integer of a string", |
||||||
|
input.get_type().name(), |
||||||
|
))) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(unsendable)] |
||||||
|
pub struct QuerySolutionIter { |
||||||
|
inner: IntoIter<QuerySolution>, |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for QuerySolutionIter { |
||||||
|
fn __iter__(slf: PyRefMut<Self>) -> Py<Self> { |
||||||
|
slf.into() |
||||||
|
} |
||||||
|
|
||||||
|
fn __next__(mut slf: PyRefMut<Self>) -> Option<PyQuerySolution> { |
||||||
|
slf.inner.next().map(move |inner| PyQuerySolution { inner }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[pyclass(unsendable)] |
||||||
|
pub struct TripleResultIter { |
||||||
|
inner: IntoIter<Triple>, |
||||||
|
} |
||||||
|
|
||||||
|
#[pyproto] |
||||||
|
impl PyIterProtocol for TripleResultIter { |
||||||
|
fn __iter__(slf: PyRefMut<Self>) -> Py<Self> { |
||||||
|
slf.into() |
||||||
|
} |
||||||
|
|
||||||
|
fn __next__(mut slf: PyRefMut<Self>) -> Option<(PyObject, PyObject, PyObject)> { |
||||||
|
slf.inner.next().map(move |t| triple_to_python(slf.py(), t)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import unittest |
||||||
|
from oxigraph import * |
||||||
|
|
||||||
|
XSD_STRING = NamedNode("http://www.w3.org/2001/XMLSchema#string") |
||||||
|
XSD_INTEGER = NamedNode("http://www.w3.org/2001/XMLSchema#integer") |
||||||
|
RDF_LANG_STRING = NamedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") |
||||||
|
|
||||||
|
|
||||||
|
class TestNamedNode(unittest.TestCase): |
||||||
|
def test_constructor(self): |
||||||
|
self.assertEqual(NamedNode("http://foo").value, "http://foo") |
||||||
|
|
||||||
|
def test_string(self): |
||||||
|
self.assertEqual(str(NamedNode("http://foo")), "<http://foo>") |
||||||
|
|
||||||
|
def test_equal(self): |
||||||
|
self.assertEqual(NamedNode("http://foo"), NamedNode("http://foo")) |
||||||
|
self.assertNotEqual(NamedNode("http://foo"), NamedNode("http://bar")) |
||||||
|
|
||||||
|
|
||||||
|
class TestBlankNode(unittest.TestCase): |
||||||
|
def test_constructor(self): |
||||||
|
self.assertEqual(BlankNode("foo").value, "foo") |
||||||
|
self.assertNotEqual(BlankNode(), BlankNode()) |
||||||
|
|
||||||
|
def test_string(self): |
||||||
|
self.assertEqual(str(BlankNode("foo")), "_:foo") |
||||||
|
|
||||||
|
def test_equal(self): |
||||||
|
self.assertEqual(BlankNode("foo"), BlankNode("foo")) |
||||||
|
self.assertNotEqual(BlankNode("foo"), BlankNode("bar")) |
||||||
|
# TODO self.assertNotEqual(BlankNode('foo'), NamedNode('http://foo')) |
||||||
|
# TODO self.assertNotEqual(NamedNode('http://foo'), BlankNode('foo')) |
||||||
|
|
||||||
|
|
||||||
|
class TestLiteral(unittest.TestCase): |
||||||
|
def test_constructor(self): |
||||||
|
self.assertEqual(Literal("foo").value, "foo") |
||||||
|
self.assertEqual(Literal("foo").datatype, XSD_STRING) |
||||||
|
|
||||||
|
self.assertEqual(Literal("foo", language="en").value, "foo") |
||||||
|
self.assertEqual(Literal("foo", language="en").language, "en") |
||||||
|
self.assertEqual(Literal("foo", language="en").datatype, RDF_LANG_STRING) |
||||||
|
|
||||||
|
self.assertEqual(Literal("foo", datatype=XSD_INTEGER).value, "foo") |
||||||
|
self.assertEqual(Literal("foo", datatype=XSD_INTEGER).datatype, XSD_INTEGER) |
||||||
|
|
||||||
|
def test_string(self): |
||||||
|
self.assertEqual(str(Literal("foo")), '"foo"') |
||||||
|
self.assertEqual(str(Literal("foo", language="en")), '"foo"@en') |
||||||
|
self.assertEqual( |
||||||
|
str(Literal("foo", datatype=XSD_INTEGER)), |
||||||
|
'"foo"^^<http://www.w3.org/2001/XMLSchema#integer>', |
||||||
|
) |
||||||
|
|
||||||
|
def test_equals(self): |
||||||
|
self.assertEqual(Literal("foo", datatype=XSD_STRING), Literal("foo")) |
||||||
|
self.assertEqual( |
||||||
|
Literal("foo", language="en", datatype=RDF_LANG_STRING), |
||||||
|
Literal("foo", language="en"), |
||||||
|
) |
||||||
|
# TODO self.assertNotEqual(NamedNode('http://foo'), Literal('foo')) |
||||||
|
# TODO self.assertNotEqual(Literal('foo'), NamedNode('http://foo')) |
||||||
|
# TODO self.assertNotEqual(BlankNode('foo'), Literal('foo')) |
||||||
|
# TODO self.assertNotEqual(Literal('foo'), BlankNode('foo')) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
unittest.main() |
@ -0,0 +1,156 @@ |
|||||||
|
import unittest |
||||||
|
from abc import ABC, abstractmethod |
||||||
|
|
||||||
|
from oxigraph import * |
||||||
|
|
||||||
|
foo = NamedNode("http://foo") |
||||||
|
bar = NamedNode("http://bar") |
||||||
|
baz = NamedNode("http://baz") |
||||||
|
graph = NamedNode("http://graph") |
||||||
|
|
||||||
|
|
||||||
|
class TestAbstractStore(unittest.TestCase, ABC): |
||||||
|
@abstractmethod |
||||||
|
def store(self): |
||||||
|
pass |
||||||
|
|
||||||
|
def test_add(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz)) |
||||||
|
store.add((foo, bar, baz, DefaultGraph())) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
self.assertEqual(len(store), 2) |
||||||
|
|
||||||
|
def test_remove(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz)) |
||||||
|
store.add((foo, bar, baz, DefaultGraph())) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
store.remove((foo, bar, baz)) |
||||||
|
self.assertEqual(len(store), 1) |
||||||
|
|
||||||
|
def test_len(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz)) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
self.assertEqual(len(store), 2) |
||||||
|
|
||||||
|
def test_in(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz)) |
||||||
|
store.add((foo, bar, baz, DefaultGraph())) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
self.assertTrue((foo, bar, baz) in store) |
||||||
|
self.assertTrue((foo, bar, baz, DefaultGraph()) in store) |
||||||
|
self.assertTrue((foo, bar, baz, graph) in store) |
||||||
|
self.assertTrue((foo, bar, baz, foo) not in store) |
||||||
|
|
||||||
|
def test_iter(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz, DefaultGraph())) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
self.assertEqual( |
||||||
|
list(store), [(foo, bar, baz, DefaultGraph()), (foo, bar, baz, graph)] |
||||||
|
) |
||||||
|
|
||||||
|
def test_match(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz, DefaultGraph())) |
||||||
|
store.add((foo, bar, baz, graph)) |
||||||
|
self.assertEqual( |
||||||
|
list(store.match(None, None, None)), |
||||||
|
[(foo, bar, baz, DefaultGraph()), (foo, bar, baz, graph)], |
||||||
|
) |
||||||
|
self.assertEqual( |
||||||
|
list(store.match(foo, None, None)), |
||||||
|
[(foo, bar, baz, DefaultGraph()), (foo, bar, baz, graph)], |
||||||
|
) |
||||||
|
self.assertEqual( |
||||||
|
list(store.match(None, None, None, graph)), |
||||||
|
[(foo, bar, baz, graph)], |
||||||
|
) |
||||||
|
self.assertEqual( |
||||||
|
list(store.match(foo, None, None, DefaultGraph())), |
||||||
|
[(foo, bar, baz, DefaultGraph())], |
||||||
|
) |
||||||
|
|
||||||
|
def test_ask_query(self): |
||||||
|
store = self.store() |
||||||
|
store.add((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.add((foo, bar, baz)) |
||||||
|
self.assertEqual( |
||||||
|
list(store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }")), |
||||||
|
[(foo, bar, baz)], |
||||||
|
) |
||||||
|
|
||||||
|
def test_select_query(self): |
||||||
|
store = self.store() |
||||||
|
store.add((foo, bar, baz)) |
||||||
|
results = list(store.query("SELECT ?s WHERE { ?s ?p ?o }")) |
||||||
|
self.assertEqual(len(results), 1) |
||||||
|
self.assertEqual(results[0][0], foo) |
||||||
|
self.assertEqual(results[0]["s"], foo) |
||||||
|
|
||||||
|
def test_load_ntriples_to_default_graph(self): |
||||||
|
store = self.store() |
||||||
|
store.load( |
||||||
|
"<http://foo> <http://bar> <http://baz> .", |
||||||
|
mime_type="application/n-triples", |
||||||
|
) |
||||||
|
self.assertEqual(list(store), [(foo, bar, baz, DefaultGraph())]) |
||||||
|
|
||||||
|
def test_load_ntriples_to_named_graph(self): |
||||||
|
store = self.store() |
||||||
|
store.load( |
||||||
|
"<http://foo> <http://bar> <http://baz> .", |
||||||
|
mime_type="application/n-triples", |
||||||
|
to_graph=graph, |
||||||
|
) |
||||||
|
self.assertEqual(list(store), [(foo, bar, baz, graph)]) |
||||||
|
|
||||||
|
def test_load_turtle_with_base_iri(self): |
||||||
|
store = self.store() |
||||||
|
store.load( |
||||||
|
"<http://foo> <http://bar> <> .", |
||||||
|
mime_type="text/turtle", |
||||||
|
base_iri="http://baz", |
||||||
|
) |
||||||
|
self.assertEqual(list(store), [(foo, bar, baz, DefaultGraph())]) |
||||||
|
|
||||||
|
def test_load_nquads(self): |
||||||
|
store = self.store() |
||||||
|
store.load( |
||||||
|
"<http://foo> <http://bar> <http://baz> <http://graph>.", |
||||||
|
mime_type="application/n-quads", |
||||||
|
) |
||||||
|
self.assertEqual(list(store), [(foo, bar, baz, graph)]) |
||||||
|
|
||||||
|
def test_load_trig_with_base_iri(self): |
||||||
|
store = self.store() |
||||||
|
store.load( |
||||||
|
"<http://graph> { <http://foo> <http://bar> <> . }", |
||||||
|
mime_type="application/trig", |
||||||
|
base_iri="http://baz", |
||||||
|
) |
||||||
|
self.assertEqual(list(store), [(foo, bar, baz, graph)]) |
||||||
|
|
||||||
|
|
||||||
|
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() |
Loading…
Reference in new issue