diff --git a/Cargo.toml b/Cargo.toml index 41b900b8..d49d3b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ - "lib" + "lib", + "python" ] \ No newline at end of file diff --git a/README.md b/README.md index 9d48c1f5..33634699 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This library is a work in progress of a [RDF](https://www.w3.org/RDF/) stack imp Its goal is to provide a compliant, safe and fast implementation of W3C specifications in Rust. -The `lib` directory contains the Rust library code. +The `lib` directory contains the Rust library code and the `python` directory a beginning of Python bindings. [![Build Status](https://travis-ci.org/Tpt/rudf.svg?branch=master)](https://travis-ci.org/Tpt/rudf) [![dependency status](https://deps.rs/repo/github/Tpt/rudf/status.svg)](https://deps.rs/repo/github/Tpt/rudf) diff --git a/python/Cargo.toml b/python/Cargo.toml new file mode 100644 index 00000000..524abd07 --- /dev/null +++ b/python/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rudf_python" +version = "0.1.0" +authors = ["Tpt "] +license = "MIT/Apache-2.0" +readme = "../README.md" +keywords = ["RDF", "N-Triples", "Turtle", "RDF/XML", "SPARQL", "Python"] +repository = "https://github.com/Tpt/rudf" +description = """ +Python bindings of the Rudf library +""" + +[lib] +name = "rudf" +crate-type = ["cdylib"] + +[dependencies] +rudf = {path = "../lib"} +cpython = { version = "0.2", features = ["extension-module"]} diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 00000000..83c99e08 --- /dev/null +++ b/python/MANIFEST.in @@ -0,0 +1,2 @@ +include Cargo.toml +recursive-include src * \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..2d3b7557 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools-rust"] \ No newline at end of file diff --git a/python/rudf/__init__.py b/python/rudf/__init__.py new file mode 100644 index 00000000..056af597 --- /dev/null +++ b/python/rudf/__init__.py @@ -0,0 +1 @@ +from .rudf import * \ No newline at end of file diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 00000000..8811d49b --- /dev/null +++ b/python/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +try: + from setuptools_rust import Binding, RustExtension +except ImportError: + print('You should install the setuptool-rust package to be able to build rudf') + + +setup( + name="rudf", + version="0.1", + rust_extensions=[RustExtension("rudf.rudf", binding=Binding.RustCPython)], + packages=["rudf"], + zip_safe=False, +) \ No newline at end of file diff --git a/python/src/lib.rs b/python/src/lib.rs new file mode 100644 index 00000000..9b515459 --- /dev/null +++ b/python/src/lib.rs @@ -0,0 +1,128 @@ +#[macro_use] +extern crate cpython; +extern crate rudf; + +use cpython::exc::ValueError; +use cpython::CompareOp; +use cpython::PyErr; +use cpython::PyResult; +use cpython::Python; +use cpython::PythonObject; +use cpython::ToPyObject; +use rudf::model; +use std::collections::hash_map::DefaultHasher; +use std::error::Error; +use std::hash::Hash; +use std::hash::Hasher; +use std::str::FromStr; + +py_module_initializer!(rudf, initrudf, PyInit_rudf, |py, m| { + try!(m.add(py, "__doc__", "Rudf Python bindings")); + try!(m.add_class::(py)); + try!(m.add_class::(py)); + try!(m.add_class::(py)); + Ok(()) +}); + +fn new_value_error(py: Python, error: impl Error) -> PyErr { + PyErr::new::(py, error.description()) +} + +fn eq_compare(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() +} + +py_class!(class NamedNode |py| { + data inner: model::NamedNode; + + def __new__(_cls, value: &str) -> PyResult { + NamedNode::create_instance(py, model::NamedNode::from_str(value).map_err(|error| new_value_error(py, error))?) + } + + def value(&self) -> PyResult { + Ok(self.inner(py).as_str().to_string()) + } + + def __str__(&self) -> PyResult { + Ok(self.inner(py).to_string()) + } + + def __richcmp__(&self, other: &NamedNode, op: CompareOp) -> PyResult { + Ok(eq_compare(self.inner(py), other.inner(py), op)) + } + + def __hash__(&self) -> PyResult { + Ok(hash(self.inner(py))) + } +}); + +py_class!(class BlankNode |py| { + data inner: model::BlankNode; + + def __new__(_cls) -> PyResult { + BlankNode::create_instance(py, model::BlankNode::default()) + } + + def __str__(&self) -> PyResult { + Ok(self.inner(py).to_string()) + } + + def __richcmp__(&self, other: &BlankNode, op: CompareOp) -> PyResult { + Ok(eq_compare(self.inner(py), other.inner(py), op)) + } + + def __hash__(&self) -> PyResult { + Ok(hash(self.inner(py))) + } +}); + +py_class!(class Literal |py| { + data inner: model::Literal; + + def __new__(_cls, value: String, language: Option = None, datatype: Option = None) -> PyResult { + Literal::create_instance(py, match language { + Some(language) => model::Literal::new_language_tagged_literal(value, language), + None => match datatype { + Some(datatype) => model::Literal::new_typed_literal(value, datatype.inner(py).clone()), + None => model::Literal::new_simple_literal(value) + } + }) + } + + def value(&self) -> PyResult { + Ok(self.inner(py).value().to_string()) + } + + def language(&self) -> PyResult> { + Ok(self.inner(py).language().map(|l| l.to_string())) + } + + def datatype(&self) -> PyResult { + NamedNode::create_instance(py, self.inner(py).datatype().clone()) + } + + def __str__(&self) -> PyResult { + Ok(self.inner(py).to_string()) + } + + def __richcmp__(&self, other: &Literal, op: CompareOp) -> PyResult { + Ok(eq_compare(self.inner(py), other.inner(py), op)) + } + + def __hash__(&self) -> PyResult { + Ok(hash(self.inner(py))) + } +}); diff --git a/python/tests/test_model.py b/python/tests/test_model.py new file mode 100644 index 00000000..3172dea0 --- /dev/null +++ b/python/tests/test_model.py @@ -0,0 +1,33 @@ +import unittest +from rudf 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/') + + +class TestBlankNode(unittest.TestCase): + def test_constructor(self): + self.assertNotEqual(BlankNode(), BlankNode()) + + +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', 'en').value(), 'foo') + self.assertEqual(Literal('foo', 'en').language(), 'en') + self.assertEqual(Literal('foo', 'en').datatype(), RDF_LANG_STRING) + + self.assertEqual(Literal('foo', datatype=XSD_INTEGER).value(), 'foo') + self.assertEqual(Literal('foo', datatype=XSD_INTEGER).datatype(), XSD_INTEGER) + + +if __name__ == '__main__': + unittest.main()