From 86a8100121196c631a6d4828624feb90dc7fbf4a Mon Sep 17 00:00:00 2001 From: Niko PLP Date: Tue, 9 Apr 2024 21:30:19 +0300 Subject: [PATCH] remove python --- Cargo.lock | 125 ---- Cargo.toml | 1 - python/Cargo.lock | 1 - python/Cargo.toml | 23 - python/README.md | 80 -- python/docs/conf.py | 38 - python/docs/index.rst | 79 -- python/docs/io.rst | 14 - python/docs/migration.rst | 34 - python/docs/model.rst | 37 - python/docs/sparql.rst | 23 - python/docs/store.rst | 5 - python/generate_stubs.py | 506 ------------- python/mypy_allowlist.txt | 2 - python/pyproject.toml | 56 -- python/requirements.dev.txt | 6 - python/src/io.rs | 372 ---------- python/src/lib.rs | 30 - python/src/model.rs | 1380 ----------------------------------- python/src/sparql.rs | 241 ------ python/src/store.rs | 907 ----------------------- python/tests/test_io.py | 114 --- python/tests/test_model.py | 380 ---------- python/tests/test_store.py | 375 ---------- 24 files changed, 4829 deletions(-) delete mode 120000 python/Cargo.lock delete mode 100644 python/Cargo.toml delete mode 100644 python/README.md delete mode 100644 python/docs/conf.py delete mode 100644 python/docs/index.rst delete mode 100644 python/docs/io.rst delete mode 100644 python/docs/migration.rst delete mode 100644 python/docs/model.rst delete mode 100644 python/docs/sparql.rst delete mode 100644 python/docs/store.rst delete mode 100644 python/generate_stubs.py delete mode 100644 python/mypy_allowlist.txt delete mode 100644 python/pyproject.toml delete mode 100644 python/requirements.dev.txt delete mode 100644 python/src/io.rs delete mode 100644 python/src/lib.rs delete mode 100644 python/src/model.rs delete mode 100644 python/src/sparql.rs delete mode 100644 python/src/store.rs delete mode 100644 python/tests/test_io.py delete mode 100644 python/tests/test_model.py delete mode 100644 python/tests/test_store.py diff --git a/Cargo.lock b/Cargo.lock index 94de6b07..5be02817 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,12 +699,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indoc" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" - [[package]] name = "instant" version = "0.1.12" @@ -846,16 +840,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.20" @@ -1131,29 +1115,6 @@ dependencies = [ "js-sys", ] -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "peeking_take_while" version = "0.1.2" @@ -1306,74 +1267,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pyo3" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" -dependencies = [ - "cfg-if", - "indoc", - "libc", - "memoffset", - "parking_lot", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" -dependencies = [ - "once_cell", - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pyoxigraph" -version = "0.3.22" -dependencies = [ - "oxigraph", - "pyo3", -] - [[package]] name = "quick-xml" version = "0.28.2" @@ -1726,12 +1619,6 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" -[[package]] -name = "smallvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" - [[package]] name = "sparesults" version = "0.1.8" @@ -1793,12 +1680,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "target-lexicon" -version = "0.12.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" - [[package]] name = "tempfile" version = "3.6.0" @@ -1950,12 +1831,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unindent" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" - [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index cb421e98..c3c69193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "lib/spargebra", "lib/sparesults", "lib/sparql-smith", - "python", "server", "testsuite" ] diff --git a/python/Cargo.lock b/python/Cargo.lock deleted file mode 120000 index 09fdd3b6..00000000 --- a/python/Cargo.lock +++ /dev/null @@ -1 +0,0 @@ -../Cargo.lock \ No newline at end of file diff --git a/python/Cargo.toml b/python/Cargo.toml deleted file mode 100644 index 68efa4dd..00000000 --- a/python/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "pyoxigraph" -version = "0.3.22" -authors = ["Tpt"] -license = "MIT OR Apache-2.0" -readme = "README.md" -keywords = ["RDF", "SPARQL", "graph-database", "database"] -repository = "https://github.com/oxigraph/oxigraph/tree/main/python" -homepage = "https://pyoxigraph.readthedocs.io/" -description = "Python bindings of Oxigraph, a SPARQL database and RDF toolkit" -edition = "2021" - -[lib] -crate-type = ["cdylib"] -name = "pyoxigraph" -doctest = false - -[features] -abi3 = ["pyo3/abi3-py37"] - -[dependencies] -oxigraph = { version = "0.3.22", path="../lib", features = ["http_client"] } -pyo3 = { version = "0.19", features = ["extension-module"] } diff --git a/python/README.md b/python/README.md deleted file mode 100644 index 42bb8d19..00000000 --- a/python/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Pyoxigraph (Oxigraph for Python) - -[![PyPI](https://img.shields.io/pypi/v/pyoxigraph)](https://pypi.org/project/pyoxigraph/) -[![Conda](https://img.shields.io/conda/vn/conda-forge/pyoxigraph)](https://anaconda.org/conda-forge/pyoxigraph) -![PyPI - Implementation](https://img.shields.io/pypi/implementation/pyoxigraph) -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyoxigraph) -[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) -[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -Pyoxigraph is a graph database library implementing the [SPARQL](https://www.w3.org/TR/sparql11-overview/) standard. -It is a Python library written on top of [Oxigraph](https://crates.io/crates/oxigraph). - -Pyoxigraph offers two stores with [SPARQL 1.1](https://www.w3.org/TR/sparql11-overview/) capabilities. -One of the store is in-memory, and the other one is disk based. - -It also provides a set of utility functions for reading, writing and processing RDF files 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/). - -Pyoxigraph is distributed [on Pypi](https://pypi.org/project/pyoxigraph/) and [on conda-forge](https://anaconda.org/conda-forge/pyoxigraph). -Run `pip install pyoxigraph` to install it. - -There exists also a small library providing [rdflib](https://rdflib.readthedocs.io) stores using pyoxigraph: [oxrdflib](https://github.com/oxigraph/oxrdflib). - -Pyoxigraph documentation is [available on the Oxigraph website](https://pyoxigraph.readthedocs.io/). - -## Build the development version - -To build and install the development version of pyoxigraph you need to clone this git repository including submodules (`git clone --recursive https://github.com/oxigraph/oxigraph.git`) -and to run `pip install .` in the `python` directory (the one this README is in). - -## Help - -Feel free to use [GitHub discussions](https://github.com/oxigraph/oxigraph/discussions) or [the Gitter chat](https://gitter.im/oxigraph/community) to ask questions or talk about Oxigraph. -[Bug reports](https://github.com/oxigraph/oxigraph/issues) are also very welcome. - -If you need advanced support or are willing to pay to get some extra features, feel free to reach out to [Tpt](https://github.com/Tpt). - -## How to contribute - -Pyoxigraph is written in Rust using [PyO3](https://github.com/PyO3/pyo3). - -Pyoxigraph is built using [Maturin](https://github.com/PyO3/maturin). -Maturin could be installed using the `pip install 'maturin>=0.9,<0.10'`. -To install a development version of Oxigraph just run `maturin develop` in this README directory. - -### Tests - -The Python bindings tests are written in Python. -To run them use `python -m unittest` in the `tests` directory. - -### Docs - -The Sphinx documentation can be generated and viewed in the browser using the following command: - -``` -sphinx-autobuild docs docs/_build/html -``` - -Note that you will need to have [sphinx-autobuild](https://pypi.org/project/sphinx-autobuild/) installed. - -Alternatively, you can use `sphinx-build` with Python's `http.server` to achieve the same thing. - -## License - -This project is licensed under either of - -- Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) -- MIT license ([LICENSE-MIT](../LICENSE-MIT) or - http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/python/docs/conf.py b/python/docs/conf.py deleted file mode 100644 index 007e6426..00000000 --- a/python/docs/conf.py +++ /dev/null @@ -1,38 +0,0 @@ -import datetime -import sys -from pathlib import Path - -import pyoxigraph - -sys.path.insert(0, str(Path(__file__).parent.parent.absolute())) - -# -- Project information ----------------------------------------------------- - -project = "pyoxigraph" -copyright = f"{datetime.date.today().year}, Oxigraph contributors" -author = pyoxigraph.__author__ -version = pyoxigraph.__version__ -release = pyoxigraph.__version__ - -# -- General configuration --------------------------------------------------- - -extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx"] - -exclude_patterns = ["build", "Thumbs.db", ".DS_Store"] - -# -- Options for HTML output ------------------------------------------------- - -html_theme = "furo" -html_static_path = [] -html_logo = "../../logo.svg" -html_favicon = "../../logo.svg" -html_theme_options = {"body_max_width": None} -html_baseurl = "https://pyoxigraph.readthedocs.io/en/stable/" - -# -- Options for doctests ------------------------------------------------- - -doctest_global_setup = "from pyoxigraph import *\nimport io" - -# -- Options for intersphinx ------------------------------------------------- - -intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/python/docs/index.rst b/python/docs/index.rst deleted file mode 100644 index 47ba57d2..00000000 --- a/python/docs/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -pyoxigraph |release| -==================== - -.. image:: https://img.shields.io/pypi/v/pyoxigraph - :alt: PyPI - :target: https://pypi.org/project/pyoxigraph/ -.. image:: https://img.shields.io/conda/vn/conda-forge/pyoxigraph - :alt: conda-forge - :target: https://anaconda.org/conda-forge/pyoxigraph -.. image:: https://img.shields.io/pypi/implementation/pyoxigraph - :alt: PyPI - Implementation -.. image:: https://img.shields.io/pypi/pyversions/pyoxigraph - :alt: PyPI - Python Version -.. image:: https://img.shields.io/pypi/l/pyoxigraph - :alt: PyPI - License - - -Pyoxigraph is a Python graph database library implementing the `SPARQL `_ standard. - -It is built on top of `Oxigraph `_ using `PyO3 `_. - -It also provides a set of utility functions for reading, writing, and processing RDF files in -`Turtle `_, -`TriG `_, -`N-Triples `_, -`N-Quads `_ and -`RDF/XML `_. - -Pyoxigraph is distributed `on Pypi `_ and `on conda-forge `_. - -There is also a small library providing a `rdflib `_ store using pyoxigraph: `oxrdflib `_. - -Oxigraph and pyoxigraph source code are on `GitHub `_. - - -Installation -"""""""""""" - -Pyoxigraph is distributed on `Pypi `_. - -To install it, run the usual ``pip install pyoxigraph`` - - -Example -""""""" - -Insert the triple `` "example"`` and print the name of ```` in SPARQL: - -:: - - from pyoxigraph import * - - store = Store() - ex = NamedNode('http://example/') - schema_name = NamedNode('http://schema.org/name') - store.add(Quad(ex, schema_name, Literal('example'))) - for binding in store.query('SELECT ?name WHERE { ?name }'): - print(binding['name'].value) - - -Table of contents -""""""""""""""""" - -.. toctree:: - - model - io - store - sparql - migration - - -Help -"""" - -Feel free to use `GitHub discussions `_ or `the Gitter chat `_ to ask questions or talk about Oxigraph. -`Bug reports `_ are also very welcome. - -If you need advanced support or are willing to pay to get some extra features, feel free to reach out to `Tpt `_. diff --git a/python/docs/io.rst b/python/docs/io.rst deleted file mode 100644 index 8be36f43..00000000 --- a/python/docs/io.rst +++ /dev/null @@ -1,14 +0,0 @@ -RDF Parsing and Serialization -============================= - -Oxigraph provides functions to parse and serialize RDF files: - - -Parsing -""""""" -.. autofunction:: pyoxigraph.parse - - -Serialization -""""""""""""" -.. autofunction:: pyoxigraph.serialize diff --git a/python/docs/migration.rst b/python/docs/migration.rst deleted file mode 100644 index 0911835a..00000000 --- a/python/docs/migration.rst +++ /dev/null @@ -1,34 +0,0 @@ -Migration Guide -=============== - -From 0.2 to 0.3 -""""""""""""""" - -* Python 3.6 and ``manylinux2010`` (`PEP 571 `_) support have been removed. The new minimal versions are Python 3.7 and ``manylinux2014`` (`PEP 599 `_). -* The on-disk storage system has been rebuilt on top of `RocksDB `_. - It is now implemented by the :py:class:`.Store` class that keeps the same API as the late :py:class:`.SledStore` class. - - To migrate you have to dump the store content using pyoxigraph **0.2** and the following code: - - .. code-block:: python - - from pyoxigraph import SledStore - store = SledStore('MY_STORAGE_PATH') - with open('temp_file.nq', 'wb') as fp: - store.dump(fp, "application/n-quads") - - And then upgrade to pyoxigraph **0.3** and run: - - .. code-block:: python - - from pyoxigraph import Store - store = Store('MY_NEW_STORAGE_PATH') - with open('temp_file.nq', 'rb') as fp: - store.bulk_load(fp, "application/n-quads") - -* The in-memory storage class :py:class:`.MemoryStore` has been merged into the :py:class:`.Store` class that provides the exact same API as the late :py:class:`.MemoryStore`. - On platforms other than Linux, a temporary directory is created when opening the :py:class:`.Store` and automatically removed when it is garbage collected. No data is written in this directory. -* :py:class:`.Store` operations are now transactional using 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). -* `RDF-star `_ is now supported (including serialization formats and SPARQL-star). :py:class:`.Triple` can now be used in :py:attr:`.Triple.object`, :py:attr:`.Triple.object`, :py:attr:`.Quad.subject` and :py:attr:`.Quad.object`. diff --git a/python/docs/model.rst b/python/docs/model.rst deleted file mode 100644 index 1950e853..00000000 --- a/python/docs/model.rst +++ /dev/null @@ -1,37 +0,0 @@ -RDF Model -========= - -Oxigraph provides python classes to represents basic RDF concepts: - - -`IRIs `_ -""""""""""""""""""""""""""""""""""""""""""""""""""""""" -.. autoclass:: pyoxigraph.NamedNode - :members: - - -`Blank Nodes `_ -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -.. autoclass:: pyoxigraph.BlankNode - :members: - - -`Literals `_ -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -.. autoclass:: pyoxigraph.Literal - :members: - - -`Triples `_ -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -.. autoclass:: pyoxigraph.Triple - :members: - - -Quads (`triples `_ in a `RDF dataset `_) -""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" -.. autoclass:: pyoxigraph.Quad - :members: - -.. autoclass:: pyoxigraph.DefaultGraph - :members: diff --git a/python/docs/sparql.rst b/python/docs/sparql.rst deleted file mode 100644 index 4c6a0479..00000000 --- a/python/docs/sparql.rst +++ /dev/null @@ -1,23 +0,0 @@ -SPARQL utility objects -============================= - -Oxigraph provides also some utilities related to SPARQL queries: - - -Variable -"""""""" -.. autoclass:: pyoxigraph.Variable - :members: - - -``SELECT`` solutions -"""""""""""""""""""" -.. autoclass:: pyoxigraph.QuerySolutions - :members: -.. autoclass:: pyoxigraph.QuerySolution - :members: - -``CONSTRUCT`` results -""""""""""""""""""""" -.. autoclass:: pyoxigraph.QueryTriples - :members: diff --git a/python/docs/store.rst b/python/docs/store.rst deleted file mode 100644 index 2fea9ab3..00000000 --- a/python/docs/store.rst +++ /dev/null @@ -1,5 +0,0 @@ -RDF Store -========= - -.. autoclass:: pyoxigraph.Store - :members: diff --git a/python/generate_stubs.py b/python/generate_stubs.py deleted file mode 100644 index 8a36f1b4..00000000 --- a/python/generate_stubs.py +++ /dev/null @@ -1,506 +0,0 @@ -import argparse -import ast -import importlib -import inspect -import logging -import re -import subprocess -from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union - - -def _path_to_type(*elements: str) -> ast.AST: - base: ast.AST = ast.Name(id=elements[0], ctx=AST_LOAD) - for e in elements[1:]: - base = ast.Attribute(value=base, attr=e, ctx=AST_LOAD) - return base - - -AST_LOAD = ast.Load() -AST_ELLIPSIS = ast.Ellipsis() -AST_STORE = ast.Store() -AST_TYPING_ANY = _path_to_type("typing", "Any") -GENERICS = { - "iterable": _path_to_type("typing", "Iterable"), - "iterator": _path_to_type("typing", "Iterator"), - "list": _path_to_type("typing", "List"), - "io": _path_to_type("typing", "IO"), -} -OBJECT_MEMBERS = dict(inspect.getmembers(object)) - - -BUILTINS: Dict[str, Union[None, Tuple[List[ast.AST], ast.AST]]] = { - "__annotations__": None, - "__bool__": ([], _path_to_type("bool")), - "__bytes__": ([], _path_to_type("bytes")), - "__class__": None, - "__contains__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__del__": None, - "__delattr__": ([_path_to_type("str")], _path_to_type("None")), - "__delitem__": ([AST_TYPING_ANY], AST_TYPING_ANY), - "__dict__": None, - "__dir__": None, - "__doc__": None, - "__eq__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__format__": ([_path_to_type("str")], _path_to_type("str")), - "__ge__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__getattribute__": ([_path_to_type("str")], AST_TYPING_ANY), - "__getitem__": ([AST_TYPING_ANY], AST_TYPING_ANY), - "__gt__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__hash__": ([], _path_to_type("int")), - "__init__": ([], _path_to_type("None")), - "__init_subclass__": None, - "__iter__": ([], AST_TYPING_ANY), - "__le__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__len__": ([], _path_to_type("int")), - "__lt__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__module__": None, - "__ne__": ([AST_TYPING_ANY], _path_to_type("bool")), - "__new__": None, - "__next__": ([], AST_TYPING_ANY), - "__reduce__": None, - "__reduce_ex__": None, - "__repr__": ([], _path_to_type("str")), - "__setattr__": ([_path_to_type("str"), AST_TYPING_ANY], _path_to_type("None")), - "__setitem__": ([AST_TYPING_ANY, AST_TYPING_ANY], AST_TYPING_ANY), - "__sizeof__": None, - "__str__": ([], _path_to_type("str")), - "__subclasshook__": None, -} - - -def module_stubs(module: Any) -> ast.Module: - types_to_import = {"typing"} - classes = [] - functions = [] - for member_name, member_value in inspect.getmembers(module): - element_path = [module.__name__, member_name] - if member_name.startswith("__"): - pass - elif inspect.isclass(member_value): - classes.append( - class_stubs(member_name, member_value, element_path, types_to_import) - ) - elif inspect.isbuiltin(member_value): - functions.append( - function_stub( - member_name, - member_value, - element_path, - types_to_import, - in_class=False, - ) - ) - else: - logging.warning(f"Unsupported root construction {member_name}") - return ast.Module( - body=[ast.Import(names=[ast.alias(name=t)]) for t in sorted(types_to_import)] - + classes - + functions, - type_ignores=[], - ) - - -def class_stubs( - cls_name: str, cls_def: Any, element_path: List[str], types_to_import: Set[str] -) -> ast.ClassDef: - attributes: List[ast.AST] = [] - methods: List[ast.AST] = [] - magic_methods: List[ast.AST] = [] - constants: List[ast.AST] = [] - for member_name, member_value in inspect.getmembers(cls_def): - current_element_path = [*element_path, member_name] - if member_name == "__init__": - try: - inspect.signature(cls_def) # we check it actually exists - methods = [ - function_stub( - member_name, - cls_def, - current_element_path, - types_to_import, - in_class=True, - ), - *methods, - ] - except ValueError as e: - if "no signature found" not in str(e): - raise ValueError( - f"Error while parsing signature of {cls_name}.__init_" - ) from e - elif ( - member_value == OBJECT_MEMBERS.get(member_name) - or BUILTINS.get(member_name, ()) is None - ): - pass - elif inspect.isdatadescriptor(member_value): - attributes.extend( - data_descriptor_stub( - member_name, member_value, current_element_path, types_to_import - ) - ) - elif inspect.isroutine(member_value): - (magic_methods if member_name.startswith("__") else methods).append( - function_stub( - member_name, - member_value, - current_element_path, - types_to_import, - in_class=True, - ) - ) - elif member_name == "__match_args__": - constants.append( - ast.AnnAssign( - target=ast.Name(id=member_name, ctx=AST_STORE), - annotation=ast.Subscript( - value=_path_to_type("typing", "Tuple"), - slice=ast.Tuple( - elts=[_path_to_type("str"), ast.Ellipsis()], ctx=AST_LOAD - ), - ctx=AST_LOAD, - ), - value=ast.Constant(member_value), - simple=1, - ) - ) - else: - logging.warning( - f"Unsupported member {member_name} of class {'.'.join(element_path)}" - ) - - doc = inspect.getdoc(cls_def) - doc_comment = build_doc_comment(doc) if doc else None - return ast.ClassDef( - cls_name, - bases=[], - keywords=[], - body=( - ([doc_comment] if doc_comment else []) - + attributes - + methods - + magic_methods - + constants - ) - or [AST_ELLIPSIS], - decorator_list=[_path_to_type("typing", "final")], - ) - - -def data_descriptor_stub( - data_desc_name: str, - data_desc_def: Any, - element_path: List[str], - types_to_import: Set[str], -) -> Union[Tuple[ast.AnnAssign, ast.Expr], Tuple[ast.AnnAssign]]: - annotation = None - doc_comment = None - - doc = inspect.getdoc(data_desc_def) - if doc is not None: - annotation = returns_stub(data_desc_name, doc, element_path, types_to_import) - m = re.findall(r"^ *:return: *(.*) *$", doc, re.MULTILINE) - if len(m) == 1: - doc_comment = m[0] - elif len(m) > 1: - raise ValueError( - f"Multiple return annotations found with :return: in {'.'.join(element_path)} documentation" - ) - - assign = ast.AnnAssign( - target=ast.Name(id=data_desc_name, ctx=AST_STORE), - annotation=annotation or AST_TYPING_ANY, - simple=1, - ) - doc_comment = build_doc_comment(doc_comment) if doc_comment else None - return (assign, doc_comment) if doc_comment else (assign,) - - -def function_stub( - fn_name: str, - fn_def: Any, - element_path: List[str], - types_to_import: Set[str], - *, - in_class: bool, -) -> ast.FunctionDef: - body: List[ast.AST] = [] - doc = inspect.getdoc(fn_def) - if doc is not None: - doc_comment = build_doc_comment(doc) - if doc_comment is not None: - body.append(doc_comment) - - decorator_list = [] - if in_class and hasattr(fn_def, "__self__"): - decorator_list.append(ast.Name("staticmethod")) - - return ast.FunctionDef( - fn_name, - arguments_stub(fn_name, fn_def, doc or "", element_path, types_to_import), - body or [AST_ELLIPSIS], - decorator_list=decorator_list, - returns=returns_stub(fn_name, doc, element_path, types_to_import) - if doc - else None, - lineno=0, - ) - - -def arguments_stub( - callable_name: str, - callable_def: Any, - doc: str, - element_path: List[str], - types_to_import: Set[str], -) -> ast.arguments: - real_parameters: Mapping[str, inspect.Parameter] = inspect.signature( - callable_def - ).parameters - if callable_name == "__init__": - real_parameters = { - "self": inspect.Parameter("self", inspect.Parameter.POSITIONAL_ONLY), - **real_parameters, - } - - parsed_param_types = {} - optional_params = set() - - # Types for magic functions types - builtin = BUILTINS.get(callable_name) - if isinstance(builtin, tuple): - param_names = list(real_parameters.keys()) - if param_names and param_names[0] == "self": - del param_names[0] - for name, t in zip(param_names, builtin[0]): - parsed_param_types[name] = t - - # Types from comment - for match in re.findall(r"^ *:type *([a-z_]+): ([^\n]*) *$", doc, re.MULTILINE): - if match[0] not in real_parameters: - raise ValueError( - f"The parameter {match[0]} of {'.'.join(element_path)} " - "is defined in the documentation but not in the function signature" - ) - type = match[1] - if type.endswith(", optional"): - optional_params.add(match[0]) - type = type[:-10] - parsed_param_types[match[0]] = convert_type_from_doc( - type, element_path, types_to_import - ) - - # we parse the parameters - posonlyargs = [] - args = [] - vararg = None - kwonlyargs = [] - kw_defaults = [] - kwarg = None - defaults = [] - for param in real_parameters.values(): - if param.name != "self" and param.name not in parsed_param_types: - raise ValueError( - f"The parameter {param.name} of {'.'.join(element_path)} " - "has no type definition in the function documentation" - ) - param_ast = ast.arg( - arg=param.name, annotation=parsed_param_types.get(param.name) - ) - - default_ast = None - if param.default != param.empty: - default_ast = ast.Constant(param.default) - if param.name not in optional_params: - raise ValueError( - f"Parameter {param.name} of {'.'.join(element_path)} " - "is optional according to the type but not flagged as such in the doc" - ) - elif param.name in optional_params: - raise ValueError( - f"Parameter {param.name} of {'.'.join(element_path)} " - "is optional according to the documentation but has no default value" - ) - - if param.kind == param.POSITIONAL_ONLY: - posonlyargs.append(param_ast) - defaults.append(default_ast) - elif param.kind == param.POSITIONAL_OR_KEYWORD: - args.append(param_ast) - defaults.append(default_ast) - elif param.kind == param.VAR_POSITIONAL: - vararg = param_ast - elif param.kind == param.KEYWORD_ONLY: - kwonlyargs.append(param_ast) - kw_defaults.append(default_ast) - elif param.kind == param.VAR_KEYWORD: - kwarg = param_ast - - return ast.arguments( - posonlyargs=posonlyargs, - args=args, - vararg=vararg, - kwonlyargs=kwonlyargs, - kw_defaults=kw_defaults, - defaults=defaults, - kwarg=kwarg, - ) - - -def returns_stub( - callable_name: str, doc: str, element_path: List[str], types_to_import: Set[str] -) -> Optional[ast.AST]: - m = re.findall(r"^ *:rtype: *([^\n]*) *$", doc, re.MULTILINE) - if len(m) == 0: - builtin = BUILTINS.get(callable_name) - if isinstance(builtin, tuple) and builtin[1] is not None: - return builtin[1] - raise ValueError( - f"The return type of {'.'.join(element_path)} " - "has no type definition using :rtype: in the function documentation" - ) - if len(m) > 1: - raise ValueError( - f"Multiple return type annotations found with :rtype: for {'.'.join(element_path)}" - ) - return convert_type_from_doc(m[0], element_path, types_to_import) - - -def convert_type_from_doc( - type_str: str, element_path: List[str], types_to_import: Set[str] -) -> ast.AST: - type_str = type_str.strip() - return parse_type_to_ast(type_str, element_path, types_to_import) - - -def parse_type_to_ast( - type_str: str, element_path: List[str], types_to_import: Set[str] -) -> ast.AST: - # let's tokenize - tokens = [] - current_token = "" - for c in type_str: - if "a" <= c <= "z" or "A" <= c <= "Z" or c == ".": - current_token += c - else: - if current_token: - tokens.append(current_token) - current_token = "" - if c != " ": - tokens.append(c) - if current_token: - tokens.append(current_token) - - # let's first parse nested parenthesis - stack: List[List[Any]] = [[]] - for token in tokens: - if token == "(": - children: List[str] = [] - stack[-1].append(children) - stack.append(children) - elif token == ")": - stack.pop() - else: - stack[-1].append(token) - - # then it's easy - def parse_sequence(sequence: List[Any]) -> ast.AST: - # we split based on "or" - or_groups: List[List[str]] = [[]] - for e in sequence: - if e == "or": - or_groups.append([]) - else: - or_groups[-1].append(e) - if any(not g for g in or_groups): - raise ValueError( - f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" - ) - - new_elements: List[ast.AST] = [] - for group in or_groups: - if len(group) == 1 and isinstance(group[0], str): - parts = group[0].split(".") - if any(not p for p in parts): - raise ValueError( - f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" - ) - if len(parts) > 1: - types_to_import.add(parts[0]) - new_elements.append(_path_to_type(*parts)) - elif ( - len(group) == 2 - and isinstance(group[0], str) - and isinstance(group[1], list) - ): - if group[0] not in GENERICS: - raise ValueError( - f"Constructor {group[0]} is not supported in type '{type_str}' used by {'.'.join(element_path)}" - ) - new_elements.append( - ast.Subscript( - value=GENERICS[group[0]], - slice=parse_sequence(group[1]), - ctx=AST_LOAD, - ) - ) - else: - raise ValueError( - f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" - ) - return ( - ast.Subscript( - value=_path_to_type("typing", "Union"), - slice=ast.Tuple(elts=new_elements, ctx=AST_LOAD), - ctx=AST_LOAD, - ) - if len(new_elements) > 1 - else new_elements[0] - ) - - return parse_sequence(stack[0]) - - -def build_doc_comment(doc: str) -> Optional[ast.Expr]: - lines = [line.strip() for line in doc.split("\n")] - clean_lines = [] - for line in lines: - if line.startswith((":type", ":rtype")): - continue - clean_lines.append(line) - text = "\n".join(clean_lines).strip() - return ast.Expr(value=ast.Constant(text)) if text else None - - -def format_with_black(code: str) -> str: - result = subprocess.run( - ["python", "-m", "black", "-t", "py37", "--pyi", "-"], - input=code.encode(), - capture_output=True, - ) - result.check_returncode() - return result.stdout.decode() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Extract Python type stub from a python module." - ) - parser.add_argument( - "module_name", help="Name of the Python module for which generate stubs" - ) - parser.add_argument( - "out", - help="Name of the Python stub file to write to", - type=argparse.FileType("wt"), - ) - parser.add_argument( - "--black", help="Formats the generated stubs using Black", action="store_true" - ) - args = parser.parse_args() - stub_content = ast.unparse(module_stubs(importlib.import_module(args.module_name))) - stub_content = stub_content.replace( - ", /", "" - ) # TODO: remove when targeting Python 3.8+ - if args.black: - stub_content = format_with_black(stub_content) - args.out.write(stub_content) diff --git a/python/mypy_allowlist.txt b/python/mypy_allowlist.txt deleted file mode 100644 index bae62500..00000000 --- a/python/mypy_allowlist.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyoxigraph.pyoxigraph -pyoxigraph.DefaultGraph.__init__ \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml deleted file mode 100644 index e17c6a01..00000000 --- a/python/pyproject.toml +++ /dev/null @@ -1,56 +0,0 @@ -[build-system] -requires = ["maturin~=1.0"] -build-backend = "maturin" - -[project] -# Most of the metadata are in Cargo.toml and injected by maturin -name = "pyoxigraph" -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Rust", - "Topic :: Database :: Database Engines/Servers", - "Topic :: Software Development :: Libraries :: Python Modules", -] -requires-python = ">=3.7" - -[project.urls] -Changelog = "https://github.com/oxigraph/oxigraph/blob/main/CHANGELOG.md" -Documentation = "https://pyoxigraph.readthedocs.io/" -Homepage = "https://pyoxigraph.readthedocs.io/" -Source = "https://github.com/oxigraph/oxigraph/tree/main/python" -Tracker = "https://github.com/oxigraph/oxigraph/issues" - -[tool.ruff] -line-length = 120 -select = [ - "ARG", - "B", - "C40", - "E", - "F", - "FBT", - "I", - "ICN", - "ISC", - "N", - "PIE", - "PTH", - "RET", - "RUF", - "SIM", - "T10", - "TCH", - "TID", - "UP", - "W", - "YTT" -] diff --git a/python/requirements.dev.txt b/python/requirements.dev.txt deleted file mode 100644 index 8e235e25..00000000 --- a/python/requirements.dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -black~=23.1 -furo -maturin~=1.0 -mypy~=1.0 -ruff~=0.0.255 -sphinx~=5.3 diff --git a/python/src/io.rs b/python/src/io.rs deleted file mode 100644 index c7e84757..00000000 --- a/python/src/io.rs +++ /dev/null @@ -1,372 +0,0 @@ -#![allow(clippy::needless_option_as_deref)] - -use crate::model::{PyQuad, PyTriple}; -use oxigraph::io::read::{ParseError, QuadReader, TripleReader}; -use oxigraph::io::{ - DatasetFormat, DatasetParser, DatasetSerializer, GraphFormat, GraphParser, GraphSerializer, -}; -use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError}; -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use pyo3::{intern, wrap_pyfunction}; -use std::cmp::max; -use std::error::Error; -use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Write}; -use std::path::{Path, PathBuf}; - -pub fn add_to_module(module: &PyModule) -> PyResult<()> { - module.add_wrapped(wrap_pyfunction!(parse))?; - module.add_wrapped(wrap_pyfunction!(serialize)) -} - -/// Parses RDF graph and dataset serialization formats. -/// -/// 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 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 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 -/// :return: an iterator of RDF triples or quads depending on the format. -/// :rtype: iterator(Triple) or iterator(Quad) -/// :raises ValueError: if the MIME type is not supported. -/// :raises SyntaxError: if the provided data is invalid. -/// -/// >>> input = io.BytesIO(b'

"1" .') -/// >>> list(parse(input, "text/turtle", base_iri="http://example.com/")) -/// [ predicate= object=>>] -#[pyfunction] -#[pyo3(signature = (input, mime_type, *, base_iri = None))] -pub fn parse( - input: PyObject, - mime_type: &str, - base_iri: Option<&str>, - py: Python<'_>, -) -> PyResult { - 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) - }; - if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - let mut parser = GraphParser::from_format(graph_format); - if let Some(base_iri) = base_iri { - parser = parser - .with_base_iri(base_iri) - .map_err(|e| PyValueError::new_err(e.to_string()))?; - } - Ok(PyTripleReader { - inner: py.allow_threads(|| parser.read_triples(input).map_err(map_parse_error))?, - } - .into_py(py)) - } else if let Some(dataset_format) = DatasetFormat::from_media_type(mime_type) { - let mut parser = DatasetParser::from_format(dataset_format); - if let Some(base_iri) = base_iri { - parser = parser - .with_base_iri(base_iri) - .map_err(|e| PyValueError::new_err(e.to_string()))?; - } - Ok(PyQuadReader { - inner: py.allow_threads(|| parser.read_quads(input).map_err(map_parse_error))?, - } - .into_py(py)) - } else { - Err(PyValueError::new_err(format!( - "Not supported MIME type: {mime_type}" - ))) - } -} - -/// Serializes an RDF graph or dataset. -/// -/// 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 RDF triples and quads to serialize. -/// :type input: iterable(Triple) or iterable(Quad) -/// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. -/// :type output: io(bytes) or str or pathlib.Path -/// :param mime_type: the MIME type of the RDF serialization. -/// :type mime_type: str -/// :rtype: None -/// :raises ValueError: if the MIME type is not supported. -/// :raises TypeError: if a triple is given during a quad format serialization or reverse. -/// -/// >>> output = io.BytesIO() -/// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], output, "text/turtle") -/// >>> output.getvalue() -/// b' "1" .\n' -#[pyfunction] -pub fn serialize(input: &PyAny, output: PyObject, mime_type: &str, py: Python<'_>) -> PyResult<()> { - let output = if let Ok(path) = output.extract::(py) { - PyWritable::from_file(&path, py).map_err(map_io_err)? - } else { - PyWritable::from_data(output) - }; - if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - let mut writer = GraphSerializer::from_format(graph_format) - .triple_writer(output) - .map_err(map_io_err)?; - for i in input.iter()? { - writer - .write(&*i?.extract::>()?) - .map_err(map_io_err)?; - } - writer.finish().map_err(map_io_err)?; - Ok(()) - } else if let Some(dataset_format) = DatasetFormat::from_media_type(mime_type) { - let mut writer = DatasetSerializer::from_format(dataset_format) - .quad_writer(output) - .map_err(map_io_err)?; - for i in input.iter()? { - writer - .write(&*i?.extract::>()?) - .map_err(map_io_err)?; - } - writer.finish().map_err(map_io_err)?; - Ok(()) - } else { - Err(PyValueError::new_err(format!( - "Not supported MIME type: {mime_type}" - ))) - } -} - -#[pyclass(name = "TripleReader", module = "pyoxigraph")] -pub struct PyTripleReader { - inner: TripleReader, -} - -#[pymethods] -impl PyTripleReader { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self, py: Python<'_>) -> PyResult> { - py.allow_threads(|| { - self.inner - .next() - .map(|q| Ok(q.map_err(map_parse_error)?.into())) - .transpose() - }) - } -} - -#[pyclass(name = "QuadReader", module = "pyoxigraph")] -pub struct PyQuadReader { - inner: QuadReader, -} - -#[pymethods] -impl PyQuadReader { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self, py: Python<'_>) -> PyResult> { - py.allow_threads(|| { - self.inner - .next() - .map(|q| Ok(q.map_err(map_parse_error)?.into())) - .transpose() - }) - } -} - -pub enum PyReadable { - Bytes(Cursor>), - Io(BufReader), - File(BufReader), -} - -impl PyReadable { - pub fn from_file(file: &Path, py: Python<'_>) -> io::Result { - Ok(Self::File(BufReader::new( - py.allow_threads(|| File::open(file))?, - ))) - } - - pub fn from_data(data: PyObject, py: Python<'_>) -> Self { - if let Ok(bytes) = data.extract::>(py) { - Self::Bytes(Cursor::new(bytes)) - } else if let Ok(string) = data.extract::(py) { - Self::Bytes(Cursor::new(string.into_bytes())) - } else { - Self::Io(BufReader::new(PyIo(data))) - } - } -} - -impl Read for PyReadable { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - match self { - Self::Bytes(bytes) => bytes.read(buf), - Self::Io(io) => io.read(buf), - Self::File(file) => file.read(buf), - } - } -} - -impl BufRead for PyReadable { - fn fill_buf(&mut self) -> io::Result<&[u8]> { - match self { - Self::Bytes(bytes) => bytes.fill_buf(), - Self::Io(io) => io.fill_buf(), - Self::File(file) => file.fill_buf(), - } - } - - fn consume(&mut self, amt: usize) { - match self { - Self::Bytes(bytes) => bytes.consume(amt), - Self::Io(io) => io.consume(amt), - Self::File(file) => file.consume(amt), - } - } -} - -pub enum PyWritable { - Io(BufWriter), - File(BufWriter), -} - -impl PyWritable { - pub fn from_file(file: &Path, py: Python<'_>) -> io::Result { - Ok(Self::File(BufWriter::new( - py.allow_threads(|| File::create(file))?, - ))) - } - - pub fn from_data(data: PyObject) -> Self { - Self::Io(BufWriter::new(PyIo(data))) - } -} - -impl Write for PyWritable { - fn write(&mut self, buf: &[u8]) -> io::Result { - match self { - Self::Io(io) => io.write(buf), - Self::File(file) => file.write(buf), - } - } - - fn flush(&mut self) -> io::Result<()> { - match self { - Self::Io(io) => io.flush(), - Self::File(file) => file.flush(), - } - } -} - -pub struct PyIo(PyObject); - -impl Read for PyIo { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - Python::with_gil(|py| { - if buf.is_empty() { - return Ok(0); - } - let to_read = max(1, buf.len() / 4); // We divide by 4 because TextIO works with number of characters and not with number of bytes - let read = self - .0 - .as_ref(py) - .call_method1(intern!(py, "read"), (to_read,)) - .map_err(to_io_err)?; - let bytes = read - .extract::<&[u8]>() - .or_else(|e| read.extract::<&str>().map(str::as_bytes).map_err(|_| e)) - .map_err(to_io_err)?; - buf[..bytes.len()].copy_from_slice(bytes); - Ok(bytes.len()) - }) - } -} - -impl Write for PyIo { - fn write(&mut self, buf: &[u8]) -> io::Result { - Python::with_gil(|py| { - self.0 - .as_ref(py) - .call_method1(intern!(py, "write"), (PyBytes::new(py, buf),)) - .map_err(to_io_err)? - .extract::() - .map_err(to_io_err) - }) - } - - fn flush(&mut self) -> io::Result<()> { - Python::with_gil(|py| { - self.0.as_ref(py).call_method0(intern!(py, "flush"))?; - Ok(()) - }) - } -} - -fn to_io_err(error: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::Other, error.into()) -} - -pub fn map_io_err(error: io::Error) -> PyErr { - if error - .get_ref() - .map_or(false, <(dyn Error + Send + Sync + 'static)>::is::) - { - *error.into_inner().unwrap().downcast().unwrap() - } else { - PyIOError::new_err(error.to_string()) - } -} - -pub fn map_parse_error(error: ParseError) -> PyErr { - match error { - ParseError::Syntax(error) => PySyntaxError::new_err(error.to_string()), - ParseError::Io(error) => map_io_err(error), - } -} - -/// Release the GIL -/// There should not be ANY use of pyo3 code inside of this method!!! -/// -/// Code from pyo3: https://github.com/PyO3/pyo3/blob/a67180c8a42a0bc0fdc45b651b62c0644130cf47/src/python.rs#L366 -#[allow(unsafe_code)] -pub fn allow_threads_unsafe(f: impl FnOnce() -> T) -> T { - struct RestoreGuard { - tstate: *mut pyo3::ffi::PyThreadState, - } - - impl Drop for RestoreGuard { - fn drop(&mut self) { - unsafe { - pyo3::ffi::PyEval_RestoreThread(self.tstate); - } - } - } - - let _guard = RestoreGuard { - tstate: unsafe { pyo3::ffi::PyEval_SaveThread() }, - }; - f() -} diff --git a/python/src/lib.rs b/python/src/lib.rs deleted file mode 100644 index 170d78b8..00000000 --- a/python/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod io; -mod model; -mod sparql; -mod store; - -use crate::model::*; -use crate::sparql::*; -use crate::store::*; -use pyo3::prelude::*; - -/// Oxigraph Python bindings -#[pymodule] -fn pyoxigraph(_py: Python<'_>, module: &PyModule) -> PyResult<()> { - module.add("__package__", "pyoxigraph")?; - module.add("__version__", env!("CARGO_PKG_VERSION"))?; - module.add("__author__", env!("CARGO_PKG_AUTHORS").replace(':', "\n"))?; - - 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::()?; - module.add_class::()?; - module.add_class::()?; - io::add_to_module(module) -} diff --git a/python/src/model.rs b/python/src/model.rs deleted file mode 100644 index 26791351..00000000 --- a/python/src/model.rs +++ /dev/null @@ -1,1380 +0,0 @@ -use oxigraph::model::*; -use oxigraph::sparql::Variable; -use pyo3::basic::CompareOp; -use pyo3::exceptions::{PyIndexError, PyNotImplementedError, PyTypeError, PyValueError}; -use pyo3::prelude::*; -use pyo3::types::{PyDict, PyTuple}; -use pyo3::PyTypeInfo; -use std::collections::hash_map::DefaultHasher; -use std::hash::Hash; -use std::hash::Hasher; -use std::vec::IntoIter; - -/// An RDF `node identified by an IRI `_. -/// -/// :param value: the IRI as a string. -/// :type value: str -/// :raises ValueError: if the IRI is not valid according to `RFC 3987 `_. -/// -/// The :py:func:`str` function provides a serialization compatible with NTriples, Turtle, and SPARQL: -/// -/// >>> str(NamedNode('http://example.com')) -/// '' -#[pyclass(frozen, name = "NamedNode", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)] -pub struct PyNamedNode { - inner: NamedNode, -} - -impl From for PyNamedNode { - fn from(inner: NamedNode) -> Self { - Self { inner } - } -} - -impl From for NamedNode { - fn from(node: PyNamedNode) -> Self { - node.inner - } -} - -impl From for NamedOrBlankNode { - fn from(node: PyNamedNode) -> Self { - node.inner.into() - } -} - -impl From for Subject { - fn from(node: PyNamedNode) -> Self { - node.inner.into() - } -} - -impl From for Term { - fn from(node: PyNamedNode) -> Self { - node.inner.into() - } -} - -impl From for GraphName { - fn from(node: PyNamedNode) -> Self { - node.inner.into() - } -} - -#[pymethods] -impl PyNamedNode { - #[new] - fn new(value: String) -> PyResult { - Ok(NamedNode::new(value) - .map_err(|e| PyValueError::new_err(e.to_string()))? - .into()) - } - - /// :return: the named node IRI. - /// :rtype: str - /// - /// >>> NamedNode("http://example.com").value - /// 'http://example.com' - #[getter] - fn value(&self) -> &str { - self.inner.as_str() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - let mut buffer = String::new(); - named_node_repr(self.inner.as_ref(), &mut buffer); - buffer - } - - fn __hash__(&self) -> u64 { - hash(&self.inner) - } - - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult { - if let Ok(other) = other.extract::>() { - Ok(op.matches(self.cmp(&other))) - } else if PyBlankNode::is_type_of(other) - || PyLiteral::is_type_of(other) - || PyDefaultGraph::is_type_of(other) - { - eq_compare_other_type(op) - } else { - Err(PyTypeError::new_err( - "NamedNode could only be compared with RDF terms", - )) - } - } - - /// :rtype: typing.Any - fn __getnewargs__(&self) -> (&str,) { - (self.value(),) - } - - /// :rtype: NamedNode - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: NamedNode - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str,) { - ("value",) - } -} - -/// An RDF `blank node `_. -/// -/// :param value: the `blank node ID `_ (if not present, a random blank node ID is automatically generated). -/// :type value: str or None, optional -/// :raises ValueError: if the blank node ID is invalid according to NTriples, Turtle, and SPARQL grammars. -/// -/// The :py:func:`str` function provides a serialization compatible with NTriples, Turtle, and SPARQL: -/// -/// >>> str(BlankNode('ex')) -/// '_:ex' -#[pyclass(frozen, name = "BlankNode", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct PyBlankNode { - inner: BlankNode, -} - -impl From for PyBlankNode { - fn from(inner: BlankNode) -> Self { - Self { inner } - } -} - -impl From for BlankNode { - fn from(node: PyBlankNode) -> Self { - node.inner - } -} - -impl From for NamedOrBlankNode { - fn from(node: PyBlankNode) -> Self { - node.inner.into() - } -} - -impl From for Subject { - fn from(node: PyBlankNode) -> Self { - node.inner.into() - } -} - -impl From for Term { - fn from(node: PyBlankNode) -> Self { - node.inner.into() - } -} - -impl From for GraphName { - fn from(node: PyBlankNode) -> Self { - node.inner.into() - } -} - -#[pymethods] -impl PyBlankNode { - #[new] - #[pyo3(signature = (value = None))] - fn new(value: Option) -> PyResult { - Ok(if let Some(value) = value { - BlankNode::new(value).map_err(|e| PyValueError::new_err(e.to_string()))? - } else { - BlankNode::default() - } - .into()) - } - - /// :return: the `blank node ID `_. - /// :rtype: str - /// - /// >>> BlankNode("ex").value - /// 'ex' - #[getter] - fn value(&self) -> &str { - self.inner.as_str() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - let mut buffer = String::new(); - blank_node_repr(self.inner.as_ref(), &mut buffer); - buffer - } - - fn __hash__(&self) -> u64 { - hash(&self.inner) - } - - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult { - if let Ok(other) = other.extract::>() { - eq_compare(self, &other, op) - } else if PyNamedNode::is_type_of(other) - || PyLiteral::is_type_of(other) - || PyDefaultGraph::is_type_of(other) - { - eq_compare_other_type(op) - } else { - Err(PyTypeError::new_err( - "BlankNode could only be compared with RDF terms", - )) - } - } - - /// :rtype: typing.Any - fn __getnewargs__(&self) -> (&str,) { - (self.value(),) - } - - /// :rtype: BlankNode - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: BlankNode - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str,) { - ("value",) - } -} - -/// An RDF `literal `_. -/// -/// :param value: the literal value or `lexical form `_. -/// :type value: str -/// :param datatype: the literal `datatype IRI `_. -/// :type datatype: NamedNode or None, optional -/// :param language: the literal `language tag `_. -/// :type language: str or None, optional -/// :raises ValueError: if the language tag is not valid according to `RFC 5646 `_ (`BCP 47 `_). -/// -/// The :py:func:`str` function provides a serialization compatible with NTriples, Turtle, and SPARQL: -/// -/// >>> str(Literal('example')) -/// '"example"' -/// >>> str(Literal('example', language='en')) -/// '"example"@en' -/// >>> str(Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer'))) -/// '"11"^^' -#[pyclass(frozen, name = "Literal", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct PyLiteral { - inner: Literal, -} - -impl From for PyLiteral { - fn from(inner: Literal) -> Self { - Self { inner } - } -} - -impl From for Literal { - fn from(literal: PyLiteral) -> Self { - literal.inner - } -} - -impl From for Term { - fn from(node: PyLiteral) -> Self { - node.inner.into() - } -} - -#[pymethods] -impl PyLiteral { - #[new] - #[pyo3(signature = (value, *, datatype = None, language = None))] - fn new( - value: String, - datatype: Option, - language: Option, - ) -> PyResult { - 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(PyValueError::new_err( - "The literals with a language tag must use the rdf:langString datatype", - )); - } - } - Literal::new_language_tagged_literal(value, language) - .map_err(|e| PyValueError::new_err(e.to_string()))? - } else if let Some(datatype) = datatype { - Literal::new_typed_literal(value, datatype) - } else { - Literal::new_simple_literal(value) - } - .into()) - } - - /// :return: the literal value or `lexical form `_. - /// :rtype: str - /// - /// >>> Literal("example").value - /// 'example' - #[getter] - fn value(&self) -> &str { - self.inner.value() - } - - /// :return: the literal `language tag `_. - /// :rtype: str or None - /// - /// >>> Literal('example', language='en').language - /// 'en' - /// >>> Literal('example').language - /// - #[getter] - fn language(&self) -> Option<&str> { - self.inner.language() - } - - /// :return: the literal `datatype IRI `_. - /// :rtype: NamedNode - /// - /// >>> Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer')).datatype - /// - /// >>> Literal('example').datatype - /// - /// >>> Literal('example', language='en').datatype - /// - #[getter] - fn datatype(&self) -> PyNamedNode { - self.inner.datatype().into_owned().into() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - let mut buffer = String::new(); - literal_repr(self.inner.as_ref(), &mut buffer); - buffer - } - - fn __hash__(&self) -> u64 { - hash(&self.inner) - } - - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult { - if let Ok(other) = other.extract::>() { - eq_compare(self, &other, op) - } else if PyNamedNode::is_type_of(other) - || PyBlankNode::is_type_of(other) - || PyDefaultGraph::is_type_of(other) - { - eq_compare_other_type(op) - } else { - Err(PyTypeError::new_err( - "Literal could only be compared with RDF terms", - )) - } - } - - /// :rtype: typing.Any - fn __getnewargs_ex__<'a>(&'a self, py: Python<'a>) -> PyResult<((&'a str,), &'a PyDict)> { - let kwargs = PyDict::new(py); - if let Some(language) = self.language() { - kwargs.set_item("language", language)?; - } else { - kwargs.set_item("datatype", self.datatype().into_py(py))?; - } - Ok(((self.value(),), kwargs)) - } - - /// :rtype: Literal - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: Literal - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str,) { - ("value",) - } -} - -/// The RDF `default graph name `_. -#[pyclass(frozen, name = "DefaultGraph", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] -pub struct PyDefaultGraph {} - -impl From for GraphName { - fn from(_: PyDefaultGraph) -> Self { - Self::DefaultGraph - } -} - -#[pymethods] -impl PyDefaultGraph { - #[new] - fn new() -> Self { - Self {} - } - - /// :return: the empty string. - /// :rtype: str - #[getter] - fn value(&self) -> &str { - "" - } - - fn __str__(&self) -> &str { - "DEFAULT" - } - - fn __repr__(&self) -> &str { - "" - } - - fn __hash__(&self) -> u64 { - 0 - } - - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult { - if let Ok(other) = other.extract::>() { - eq_compare(self, &other, op) - } else if PyNamedNode::is_type_of(other) - || PyBlankNode::is_type_of(other) - || PyLiteral::is_type_of(other) - { - eq_compare_other_type(op) - } else { - Err(PyTypeError::new_err( - "DefaultGraph could only be compared with RDF terms", - )) - } - } - - /// :rtype: typing.Any - fn __getnewargs__<'a>(&self, py: Python<'a>) -> &'a PyTuple { - PyTuple::empty(py) - } - - /// :rtype: DefaultGraph - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: DefaultGraph - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } -} - -#[derive(FromPyObject)] -pub enum PyNamedOrBlankNode { - NamedNode(PyNamedNode), - BlankNode(PyBlankNode), -} - -impl From for NamedOrBlankNode { - fn from(node: PyNamedOrBlankNode) -> Self { - match node { - PyNamedOrBlankNode::NamedNode(node) => node.into(), - PyNamedOrBlankNode::BlankNode(node) => node.into(), - } - } -} - -impl From for PyNamedOrBlankNode { - fn from(node: NamedOrBlankNode) -> Self { - match node { - NamedOrBlankNode::NamedNode(node) => Self::NamedNode(node.into()), - NamedOrBlankNode::BlankNode(node) => Self::BlankNode(node.into()), - } - } -} - -impl IntoPy for PyNamedOrBlankNode { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Self::NamedNode(node) => node.into_py(py), - Self::BlankNode(node) => node.into_py(py), - } - } -} - -#[derive(FromPyObject)] -pub enum PySubject { - NamedNode(PyNamedNode), - BlankNode(PyBlankNode), - Triple(PyTriple), -} - -impl From for Subject { - fn from(node: PySubject) -> Self { - match node { - PySubject::NamedNode(node) => node.into(), - PySubject::BlankNode(node) => node.into(), - PySubject::Triple(triple) => triple.into(), - } - } -} - -impl From for PySubject { - fn from(node: Subject) -> Self { - match node { - Subject::NamedNode(node) => Self::NamedNode(node.into()), - Subject::BlankNode(node) => Self::BlankNode(node.into()), - Subject::Triple(triple) => Self::Triple(triple.as_ref().clone().into()), - } - } -} - -impl IntoPy for PySubject { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Self::NamedNode(node) => node.into_py(py), - Self::BlankNode(node) => node.into_py(py), - Self::Triple(triple) => triple.into_py(py), - } - } -} - -#[derive(FromPyObject)] -pub enum PyTerm { - NamedNode(PyNamedNode), - BlankNode(PyBlankNode), - Literal(PyLiteral), - Triple(PyTriple), -} - -impl From for Term { - fn from(term: PyTerm) -> Self { - match term { - PyTerm::NamedNode(node) => node.into(), - PyTerm::BlankNode(node) => node.into(), - PyTerm::Literal(literal) => literal.into(), - PyTerm::Triple(triple) => triple.into(), - } - } -} - -impl From for PyTerm { - fn from(term: Term) -> Self { - match term { - Term::NamedNode(node) => Self::NamedNode(node.into()), - Term::BlankNode(node) => Self::BlankNode(node.into()), - Term::Literal(literal) => Self::Literal(literal.into()), - Term::Triple(triple) => Self::Triple(triple.as_ref().clone().into()), - } - } -} - -impl IntoPy for PyTerm { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Self::NamedNode(node) => node.into_py(py), - Self::BlankNode(node) => node.into_py(py), - Self::Literal(literal) => literal.into_py(py), - Self::Triple(triple) => triple.into_py(py), - } - } -} - -/// An RDF `triple `_. -/// -/// :param subject: the triple subject. -/// :type subject: NamedNode or BlankNode or Triple -/// :param predicate: the triple predicate. -/// :type predicate: NamedNode -/// :param object: the triple object. -/// :type object: NamedNode or BlankNode or Literal or Triple -/// -/// The :py:func:`str` function provides a serialization compatible with NTriples, Turtle, and SPARQL: -/// -/// >>> str(Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) -/// ' "1"' -/// -/// A triple could also be easily destructed into its components: -/// -/// >>> (s, p, o) = Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')) -#[pyclass(frozen, name = "Triple", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct PyTriple { - inner: Triple, -} - -impl From for PyTriple { - fn from(inner: Triple) -> Self { - Self { inner } - } -} - -impl From for Triple { - fn from(triple: PyTriple) -> Self { - triple.inner - } -} - -impl<'a> From<&'a PyTriple> for TripleRef<'a> { - fn from(triple: &'a PyTriple) -> Self { - triple.inner.as_ref() - } -} - -impl From for Subject { - fn from(triple: PyTriple) -> Self { - triple.inner.into() - } -} - -impl From for Term { - fn from(triple: PyTriple) -> Self { - triple.inner.into() - } -} - -#[pymethods] -impl PyTriple { - #[new] - fn new(subject: PySubject, predicate: PyNamedNode, object: PyTerm) -> Self { - Triple::new(subject, predicate, object).into() - } - - /// :return: the triple subject. - /// :rtype: NamedNode or BlankNode or Triple - /// - /// >>> Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')).subject - /// - #[getter] - fn subject(&self) -> PySubject { - self.inner.subject.clone().into() - } - - /// :return: the triple predicate. - /// :rtype: NamedNode - /// - /// >>> Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')).predicate - /// - #[getter] - fn predicate(&self) -> PyNamedNode { - self.inner.predicate.clone().into() - } - - /// :return: the triple object. - /// :rtype: NamedNode or BlankNode or Literal or Triple - /// - /// >>> Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')).object - /// > - #[getter] - fn object(&self) -> PyTerm { - self.inner.object.clone().into() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - let mut buffer = String::new(); - triple_repr(self.inner.as_ref(), &mut buffer); - buffer - } - - fn __hash__(&self) -> u64 { - hash(&self.inner) - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - eq_compare(self, other, op) - } - - fn __len__(&self) -> usize { - 3 - } - - fn __getitem__(&self, input: usize) -> PyResult { - match input { - 0 => Ok(Term::from(self.inner.subject.clone()).into()), - 1 => Ok(Term::from(self.inner.predicate.clone()).into()), - 2 => Ok(self.inner.object.clone().into()), - _ => Err(PyIndexError::new_err("A triple has only 3 elements")), - } - } - - fn __iter__(&self) -> TripleComponentsIter { - TripleComponentsIter { - inner: vec![ - self.inner.subject.clone().into(), - self.inner.predicate.clone().into(), - self.inner.object.clone(), - ] - .into_iter(), - } - } - - /// :rtype: typing.Any - fn __getnewargs__(&self) -> (PySubject, PyNamedNode, PyTerm) { - (self.subject(), self.predicate(), self.object()) - } - - /// :rtype: Triple - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: Triple - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str, &'static str, &'static str) { - ("subject", "predicate", "object") - } -} - -#[derive(FromPyObject)] -pub enum PyGraphName { - NamedNode(PyNamedNode), - BlankNode(PyBlankNode), - DefaultGraph(PyDefaultGraph), -} - -impl From for GraphName { - fn from(graph_name: PyGraphName) -> Self { - match graph_name { - PyGraphName::NamedNode(node) => node.into(), - PyGraphName::BlankNode(node) => node.into(), - PyGraphName::DefaultGraph(default_graph) => default_graph.into(), - } - } -} - -impl From for PyGraphName { - fn from(graph_name: GraphName) -> Self { - match graph_name { - GraphName::NamedNode(node) => Self::NamedNode(node.into()), - GraphName::BlankNode(node) => Self::BlankNode(node.into()), - GraphName::DefaultGraph => Self::DefaultGraph(PyDefaultGraph::new()), - } - } -} - -impl IntoPy for PyGraphName { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Self::NamedNode(node) => node.into_py(py), - Self::BlankNode(node) => node.into_py(py), - Self::DefaultGraph(node) => node.into_py(py), - } - } -} - -/// An RDF `triple `_. -/// in a `RDF dataset `_. -/// -/// :param subject: the quad subject. -/// :type subject: NamedNode or BlankNode or Triple -/// :param predicate: the quad predicate. -/// :type predicate: NamedNode -/// :param object: the quad object. -/// :type object: NamedNode or BlankNode or Literal or Triple -/// :param graph_name: the quad graph name. If not present, the default graph is assumed. -/// :type graph_name: NamedNode or BlankNode or DefaultGraph or None, optional -/// -/// The :py:func:`str` function provides a serialization compatible with NTriples, Turtle, and SPARQL: -/// -/// >>> str(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) -/// ' "1" ' -/// -/// >>> str(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), DefaultGraph())) -/// ' "1"' -/// -/// A quad could also be easily destructed into its components: -/// -/// >>> (s, p, o, g) = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) -#[pyclass(frozen, name = "Quad", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct PyQuad { - inner: Quad, -} - -impl From for PyQuad { - fn from(inner: Quad) -> Self { - Self { inner } - } -} - -impl From for Quad { - fn from(node: PyQuad) -> Self { - node.inner - } -} - -impl<'a> From<&'a PyQuad> for QuadRef<'a> { - fn from(node: &'a PyQuad) -> Self { - node.inner.as_ref() - } -} - -#[pymethods] -impl PyQuad { - #[new] - #[pyo3(signature = (subject, predicate, object, graph_name = None))] - fn new( - subject: PySubject, - predicate: PyNamedNode, - object: PyTerm, - graph_name: Option, - ) -> Self { - Quad::new( - subject, - predicate, - object, - graph_name.unwrap_or(PyGraphName::DefaultGraph(PyDefaultGraph {})), - ) - .into() - } - - /// :return: the quad subject. - /// :rtype: NamedNode or BlankNode or Triple - /// - /// >>> Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')).subject - /// - #[getter] - fn subject(&self) -> PySubject { - self.inner.subject.clone().into() - } - - /// :return: the quad predicate. - /// :rtype: NamedNode - /// - /// >>> Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')).predicate - /// - #[getter] - fn predicate(&self) -> PyNamedNode { - self.inner.predicate.clone().into() - } - - /// :return: the quad object. - /// :rtype: NamedNode or BlankNode or Literal or Triple - /// - /// >>> Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')).object - /// > - #[getter] - fn object(&self) -> PyTerm { - self.inner.object.clone().into() - } - - /// :return: the quad graph name. - /// :rtype: NamedNode or BlankNode or DefaultGraph - /// - /// >>> Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')).graph_name - /// - #[getter] - fn graph_name(&self) -> PyGraphName { - self.inner.graph_name.clone().into() - } - - /// :return: the quad underlying triple. - /// :rtype: Triple - /// - /// >>> Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')).triple - /// predicate= object=>> - #[getter] - fn triple(&self) -> PyTriple { - Triple::from(self.inner.clone()).into() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - let mut buffer = String::new(); - buffer.push_str(" QuadComponentsIter { - QuadComponentsIter { - inner: vec![ - Some(self.inner.subject.clone().into()), - Some(self.inner.predicate.clone().into()), - Some(self.inner.object.clone()), - match self.inner.graph_name.clone() { - GraphName::NamedNode(node) => Some(node.into()), - GraphName::BlankNode(node) => Some(node.into()), - GraphName::DefaultGraph => None, - }, - ] - .into_iter(), - } - } - - /// :rtype: typing.Any - fn __getnewargs__(&self) -> (PySubject, PyNamedNode, PyTerm, PyGraphName) { - ( - self.subject(), - self.predicate(), - self.object(), - self.graph_name(), - ) - } - - /// :rtype: Quad - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: Quad - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str, &'static str, &'static str, &'static str) { - ("subject", "predicate", "object", "graph_name") - } -} - -/// A SPARQL query variable. -/// -/// :param value: the variable name as a string. -/// :type value: str -/// :raises ValueError: if the variable name is invalid according to the SPARQL grammar. -/// -/// The :py:func:`str` function provides a serialization compatible with SPARQL: -/// -/// >>> str(Variable('foo')) -/// '?foo' -#[pyclass(frozen, name = "Variable", module = "pyoxigraph")] -#[derive(Eq, PartialEq, Debug, Clone, Hash)] -pub struct PyVariable { - inner: Variable, -} - -impl From for PyVariable { - fn from(inner: Variable) -> Self { - Self { inner } - } -} - -impl From for Variable { - fn from(variable: PyVariable) -> Self { - variable.inner - } -} - -impl<'a> From<&'a PyVariable> for &'a Variable { - fn from(variable: &'a PyVariable) -> Self { - &variable.inner - } -} - -#[pymethods] -impl PyVariable { - #[new] - fn new(value: String) -> PyResult { - Ok(Variable::new(value) - .map_err(|e| PyValueError::new_err(e.to_string()))? - .into()) - } - - /// :return: the variable name. - /// :rtype: str - /// - /// >>> Variable("foo").value - /// 'foo' - #[getter] - fn value(&self) -> &str { - self.inner.as_str() - } - - fn __str__(&self) -> String { - self.inner.to_string() - } - - fn __repr__(&self) -> String { - format!("", self.inner.as_str()) - } - - fn __hash__(&self) -> u64 { - hash(&self.inner) - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - eq_compare(self, other, op) - } - - /// :rtype: typing.Any - fn __getnewargs__(&self) -> (&str,) { - (self.value(),) - } - - /// :rtype: Variable - fn __copy__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - /// :type memo: typing.Any - /// :rtype: Variable - #[allow(unused_variables)] - fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ PyAny) -> PyRef<'a, Self> { - slf - } - - #[classattr] - fn __match_args__() -> (&'static str,) { - ("value",) - } -} - -pub struct PyNamedNodeRef<'a>(PyRef<'a, PyNamedNode>); - -impl<'a> From<&'a PyNamedNodeRef<'a>> for NamedNodeRef<'a> { - fn from(value: &'a PyNamedNodeRef<'a>) -> Self { - value.0.inner.as_ref() - } -} - -impl<'a> TryFrom<&'a PyAny> for PyNamedNodeRef<'a> { - type Error = PyErr; - - fn try_from(value: &'a PyAny) -> PyResult { - if let Ok(node) = value.extract::>() { - Ok(Self(node)) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an RDF named node", - value.get_type().name()?, - ))) - } - } -} - -pub enum PyNamedOrBlankNodeRef<'a> { - NamedNode(PyRef<'a, PyNamedNode>), - BlankNode(PyRef<'a, PyBlankNode>), -} - -impl<'a> From<&'a PyNamedOrBlankNodeRef<'a>> for NamedOrBlankNodeRef<'a> { - fn from(value: &'a PyNamedOrBlankNodeRef<'a>) -> Self { - match value { - PyNamedOrBlankNodeRef::NamedNode(value) => value.inner.as_ref().into(), - PyNamedOrBlankNodeRef::BlankNode(value) => value.inner.as_ref().into(), - } - } -} - -impl<'a> TryFrom<&'a PyAny> for PyNamedOrBlankNodeRef<'a> { - type Error = PyErr; - - fn try_from(value: &'a PyAny) -> PyResult { - if let Ok(node) = value.extract::>() { - Ok(Self::NamedNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::BlankNode(node)) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an RDF named or blank node", - value.get_type().name()?, - ))) - } - } -} - -pub enum PySubjectRef<'a> { - NamedNode(PyRef<'a, PyNamedNode>), - BlankNode(PyRef<'a, PyBlankNode>), - Triple(PyRef<'a, PyTriple>), -} - -impl<'a> From<&'a PySubjectRef<'a>> for SubjectRef<'a> { - fn from(value: &'a PySubjectRef<'a>) -> Self { - match value { - PySubjectRef::NamedNode(value) => value.inner.as_ref().into(), - PySubjectRef::BlankNode(value) => value.inner.as_ref().into(), - PySubjectRef::Triple(value) => (&value.inner).into(), - } - } -} - -impl<'a> TryFrom<&'a PyAny> for PySubjectRef<'a> { - type Error = PyErr; - - fn try_from(value: &'a PyAny) -> PyResult { - if let Ok(node) = value.extract::>() { - Ok(Self::NamedNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::BlankNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::Triple(node)) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an RDF named or blank node", - value.get_type().name()?, - ))) - } - } -} - -pub enum PyTermRef<'a> { - NamedNode(PyRef<'a, PyNamedNode>), - BlankNode(PyRef<'a, PyBlankNode>), - Literal(PyRef<'a, PyLiteral>), - Triple(PyRef<'a, PyTriple>), -} - -impl<'a> From<&'a PyTermRef<'a>> for TermRef<'a> { - fn from(value: &'a PyTermRef<'a>) -> Self { - match value { - PyTermRef::NamedNode(value) => value.inner.as_ref().into(), - PyTermRef::BlankNode(value) => value.inner.as_ref().into(), - PyTermRef::Literal(value) => value.inner.as_ref().into(), - PyTermRef::Triple(value) => (&value.inner).into(), - } - } -} - -impl<'a> From<&'a PyTermRef<'a>> for Term { - fn from(value: &'a PyTermRef<'a>) -> Self { - TermRef::from(value).into() - } -} - -impl<'a> TryFrom<&'a PyAny> for PyTermRef<'a> { - type Error = PyErr; - - fn try_from(value: &'a PyAny) -> PyResult { - if let Ok(node) = value.extract::>() { - Ok(Self::NamedNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::BlankNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::Literal(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::Triple(node)) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an RDF term", - value.get_type().name()?, - ))) - } - } -} - -pub enum PyGraphNameRef<'a> { - NamedNode(PyRef<'a, PyNamedNode>), - BlankNode(PyRef<'a, PyBlankNode>), - DefaultGraph, -} - -impl<'a> From<&'a PyGraphNameRef<'a>> for GraphNameRef<'a> { - fn from(value: &'a PyGraphNameRef<'a>) -> Self { - match value { - PyGraphNameRef::NamedNode(value) => value.inner.as_ref().into(), - PyGraphNameRef::BlankNode(value) => value.inner.as_ref().into(), - PyGraphNameRef::DefaultGraph => Self::DefaultGraph, - } - } -} - -impl<'a> From<&'a PyGraphNameRef<'a>> for GraphName { - fn from(value: &'a PyGraphNameRef<'a>) -> Self { - GraphNameRef::from(value).into() - } -} - -impl<'a> TryFrom<&'a PyAny> for PyGraphNameRef<'a> { - type Error = PyErr; - - fn try_from(value: &'a PyAny) -> PyResult { - if let Ok(node) = value.extract::>() { - Ok(Self::NamedNode(node)) - } else if let Ok(node) = value.extract::>() { - Ok(Self::BlankNode(node)) - } else if value.extract::>().is_ok() { - Ok(Self::DefaultGraph) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an RDF graph name", - value.get_type().name()?, - ))) - } - } -} - -fn eq_compare(a: &T, b: &T, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(a == b), - CompareOp::Ne => Ok(a != b), - _ => Err(PyNotImplementedError::new_err( - "Ordering is not implemented", - )), - } -} - -fn eq_compare_other_type(op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(false), - CompareOp::Ne => Ok(true), - _ => Err(PyNotImplementedError::new_err( - "Ordering is not implemented", - )), - } -} - -fn hash(t: &impl Hash) -> u64 { - let mut s = DefaultHasher::new(); - t.hash(&mut s); - s.finish() -} - -fn named_node_repr(node: NamedNodeRef<'_>, buffer: &mut String) { - buffer.push_str(""), - } -} - -fn triple_repr(triple: TripleRef<'_>, buffer: &mut String) { - buffer.push_str(", -} - -#[pymethods] -impl TripleComponentsIter { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self) -> Option { - self.inner.next().map(PyTerm::from) - } -} - -#[pyclass(module = "pyoxigraph")] -pub struct QuadComponentsIter { - inner: IntoIter>, -} - -#[pymethods] -impl QuadComponentsIter { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self, py: Python<'_>) -> Option { - self.inner.next().map(move |t| { - if let Some(t) = t { - PyTerm::from(t).into_py(py) - } else { - PyDefaultGraph {}.into_py(py) - } - }) - } -} diff --git a/python/src/sparql.rs b/python/src/sparql.rs deleted file mode 100644 index 01298fa6..00000000 --- a/python/src/sparql.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::io::{allow_threads_unsafe, map_io_err, map_parse_error}; -use crate::map_storage_error; -use crate::model::*; -use oxigraph::model::Term; -use oxigraph::sparql::*; -use pyo3::basic::CompareOp; -use pyo3::exceptions::{ - PyNotImplementedError, PyRuntimeError, PySyntaxError, PyTypeError, PyValueError, -}; - -use pyo3::prelude::*; -use std::vec::IntoIter; - -pub fn parse_query( - query: &str, - base_iri: Option<&str>, - use_default_graph_as_union: bool, - default_graph: Option<&PyAny>, - named_graphs: Option<&PyAny>, -) -> PyResult { - let mut query = allow_threads_unsafe(|| Query::parse(query, base_iri)) - .map_err(|e| map_evaluation_error(e.into()))?; - - if use_default_graph_as_union && default_graph.is_some() { - return Err(PyValueError::new_err( - "The query() method use_default_graph_as_union and default_graph arguments should not be set at the same time", - )); - } - - if use_default_graph_as_union { - query.dataset_mut().set_default_graph_as_union(); - } - - if let Some(default_graph) = default_graph { - if let Ok(default_graphs) = default_graph.iter() { - query.dataset_mut().set_default_graph( - default_graphs - .map(|graph| Ok(graph?.extract::()?.into())) - .collect::>()?, - ) - } else if let Ok(default_graph) = default_graph.extract::() { - query - .dataset_mut() - .set_default_graph(vec![default_graph.into()]); - } else { - return Err(PyValueError::new_err( - format!("The query() method default_graph argument should be a NamedNode, a BlankNode, the DefaultGraph or a not empty list of them. {} found", default_graph.get_type() - ))); - } - } - - if let Some(named_graphs) = named_graphs { - query.dataset_mut().set_available_named_graphs( - named_graphs - .iter()? - .map(|graph| Ok(graph?.extract::()?.into())) - .collect::>()?, - ) - } - - Ok(query) -} - -pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyObject { - match results { - QueryResults::Solutions(inner) => PyQuerySolutions { inner }.into_py(py), - QueryResults::Graph(inner) => PyQueryTriples { inner }.into_py(py), - QueryResults::Boolean(b) => b.into_py(py), - } -} - -/// Tuple associating variables and terms that are the result of a SPARQL ``SELECT`` query. -/// -/// It is the equivalent of a row in SQL. -/// -/// 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 = 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')] -/// -/// >>> solution['s'] -/// -/// >>> solution[0] -/// -/// >>> s, p, o = solution -/// >>> s -/// -#[pyclass(frozen, unsendable, name = "QuerySolution", module = "pyoxigraph")] -pub struct PyQuerySolution { - inner: QuerySolution, -} - -#[pymethods] -impl PyQuerySolution { - fn __repr__(&self) -> String { - let mut buffer = String::new(); - buffer.push_str("'); - buffer - } - - fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult { - match op { - CompareOp::Eq => Ok(self.inner == other.inner), - CompareOp::Ne => Ok(self.inner != other.inner), - _ => Err(PyNotImplementedError::new_err( - "Ordering is not implemented", - )), - } - } - - fn __len__(&self) -> usize { - self.inner.len() - } - - fn __getitem__(&self, input: &PyAny) -> PyResult> { - if let Ok(key) = usize::extract(input) { - Ok(self.inner.get(key).map(|term| PyTerm::from(term.clone()))) - } else if let Ok(key) = <&str>::extract(input) { - Ok(self.inner.get(key).map(|term| PyTerm::from(term.clone()))) - } else if let Ok(key) = input.extract::>() { - Ok(self - .inner - .get(<&Variable>::from(&*key)) - .map(|term| PyTerm::from(term.clone()))) - } else { - Err(PyTypeError::new_err(format!( - "{} is not an integer of a string", - input.get_type().name()?, - ))) - } - } - - #[allow(clippy::unnecessary_to_owned)] - fn __iter__(&self) -> SolutionValueIter { - SolutionValueIter { - inner: self.inner.values().to_vec().into_iter(), - } - } -} - -#[pyclass(module = "pyoxigraph")] -pub struct SolutionValueIter { - inner: IntoIter>, -} - -#[pymethods] -impl SolutionValueIter { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self) -> Option> { - self.inner.next().map(|v| v.map(PyTerm::from)) - } -} - -/// An iterator of :py:class:`QuerySolution` returned by a SPARQL ``SELECT`` query -/// -/// >>> 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 }')) -/// [>] -#[pyclass(unsendable, name = "QuerySolutions", module = "pyoxigraph")] -pub struct PyQuerySolutions { - inner: QuerySolutionIter, -} - -#[pymethods] -impl PyQuerySolutions { - /// :return: the ordered list of all variables that could appear in the query results - /// :rtype: list(Variable) - /// - /// >>> store = Store() - /// >>> store.query('SELECT ?s WHERE { ?s ?p ?o }').variables - /// [] - #[getter] - fn variables(&self) -> Vec { - self.inner - .variables() - .iter() - .map(|v| v.clone().into()) - .collect() - } - - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self) -> PyResult> { - Ok(allow_threads_unsafe(|| self.inner.next()) - .transpose() - .map_err(map_evaluation_error)? - .map(move |inner| PyQuerySolution { inner })) - } -} - -/// An iterator of :py:class:`Triple` returned by a SPARQL ``CONSTRUCT`` or ``DESCRIBE`` 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=>>] -#[pyclass(unsendable, name = "QueryTriples", module = "pyoxigraph")] -pub struct PyQueryTriples { - inner: QueryTripleIter, -} - -#[pymethods] -impl PyQueryTriples { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef { - slf - } - - fn __next__(&mut self) -> PyResult> { - Ok(allow_threads_unsafe(|| self.inner.next()) - .transpose() - .map_err(map_evaluation_error)? - .map(Into::into)) - } -} - -pub fn map_evaluation_error(error: EvaluationError) -> PyErr { - match error { - EvaluationError::Parsing(error) => PySyntaxError::new_err(error.to_string()), - EvaluationError::Storage(error) => map_storage_error(error), - EvaluationError::Io(error) => map_io_err(error), - EvaluationError::GraphParsing(error) => map_parse_error(error), - EvaluationError::Query(error) => PyValueError::new_err(error.to_string()), - _ => PyRuntimeError::new_err(error.to_string()), - } -} diff --git a/python/src/store.rs b/python/src/store.rs deleted file mode 100644 index 957c1b5c..00000000 --- a/python/src/store.rs +++ /dev/null @@ -1,907 +0,0 @@ -#![allow(clippy::needless_option_as_deref)] - -use crate::io::{allow_threads_unsafe, map_io_err, map_parse_error, PyReadable, PyWritable}; -use crate::model::*; -use crate::sparql::*; -use oxigraph::io::{DatasetFormat, GraphFormat}; -use oxigraph::model::{GraphName, GraphNameRef}; -use oxigraph::sparql::Update; -use oxigraph::store::{self, LoaderError, SerializerError, StorageError, Store}; -use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError}; -use pyo3::prelude::*; -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 IOError: 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 IOError: 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 IOError: 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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``) - /// * `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 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 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 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 MIME type is not supported or the `to_graph` parameter is given with a quad file. - /// :raises SyntaxError: if the provided data is invalid. - /// :raises IOError: if an I/O 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, mime_type, *, base_iri = None, to_graph = None))] - fn load( - &self, - input: PyObject, - mime_type: &str, - base_iri: Option<&str>, - to_graph: Option<&PyAny>, - py: Python<'_>, - ) -> PyResult<()> { - 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(graph_format) = GraphFormat::from_media_type(mime_type) { - self.inner - .load_graph( - input, - graph_format, - to_graph_name.as_ref().unwrap_or(&GraphName::DefaultGraph), - base_iri, - ) - .map_err(map_loader_error) - } 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_loader_error) - } else { - Err(PyValueError::new_err(format!( - "Not supported MIME type: {mime_type}" - ))) - } - }) - } - - /// 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``) - /// * `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 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 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 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 MIME type is not supported or the `to_graph` parameter is given with a quad file. - /// :raises SyntaxError: if the provided data is invalid. - /// :raises IOError: if an I/O 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, mime_type, *, base_iri = None, to_graph = None))] - fn bulk_load( - &self, - input: PyObject, - mime_type: &str, - base_iri: Option<&str>, - to_graph: Option<&PyAny>, - py: Python<'_>, - ) -> PyResult<()> { - 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(graph_format) = GraphFormat::from_media_type(mime_type) { - self.inner - .bulk_loader() - .load_graph( - input, - graph_format, - &to_graph_name.unwrap_or(GraphName::DefaultGraph), - base_iri, - ) - .map_err(map_loader_error) - } 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 - .bulk_loader() - .load_dataset(input, dataset_format, base_iri) - .map_err(map_loader_error) - } 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 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 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 - /// :rtype: None - /// :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 = 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, mime_type, *, from_graph = None))] - fn dump( - &self, - output: PyObject, - mime_type: &str, - from_graph: Option<&PyAny>, - py: Python<'_>, - ) -> PyResult<()> { - let output = if let Ok(path) = output.extract::(py) { - PyWritable::from_file(&path, py).map_err(map_io_err)? - } else { - PyWritable::from_data(output) - }; - let from_graph_name = if let Some(graph_name) = from_graph { - Some(GraphName::from(&PyGraphNameRef::try_from(graph_name)?)) - } else { - None - }; - py.allow_threads(|| { - if let Some(graph_format) = GraphFormat::from_media_type(mime_type) { - self.inner - .dump_graph( - output, - graph_format, - &from_graph_name.unwrap_or(GraphName::DefaultGraph), - ) - .map_err(map_serializer_error) - } 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_serializer_error) - } 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: iterator(NamedNode or BlankNode) - /// :raises IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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 IOError: if an I/O 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) => PyIOError::new_err(error.to_string()), - _ => 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), - } -} - -pub fn map_serializer_error(error: SerializerError) -> PyErr { - match error { - SerializerError::Storage(error) => map_storage_error(error), - SerializerError::Io(error) => PyIOError::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), - } - } -} diff --git a/python/tests/test_io.py b/python/tests/test_io.py deleted file mode 100644 index e7519f5d..00000000 --- a/python/tests/test_io.py +++ /dev/null @@ -1,114 +0,0 @@ -import unittest -from io import BytesIO, StringIO, UnsupportedOperation -from tempfile import NamedTemporaryFile, TemporaryFile - -from pyoxigraph import Literal, NamedNode, Quad, Triple, parse, serialize - -EXAMPLE_TRIPLE = Triple( - NamedNode("http://example.com/foo"), - NamedNode("http://example.com/p"), - Literal("éù"), -) -EXAMPLE_QUAD = Quad( - NamedNode("http://example.com/foo"), - NamedNode("http://example.com/p"), - Literal("1"), - NamedNode("http://example.com/g"), -) - - -class TestParse(unittest.TestCase): - def test_parse_file(self) -> None: - with NamedTemporaryFile() as fp: - fp.write('

"éù" .'.encode()) - fp.flush() - self.assertEqual( - list(parse(fp.name, "text/turtle", base_iri="http://example.com/")), - [EXAMPLE_TRIPLE], - ) - - def test_parse_not_existing_file(self) -> None: - with self.assertRaises(IOError) as _: - parse("/tmp/not-existing-oxigraph-file.ttl", "text/turtle") - - def test_parse_str_io(self) -> None: - self.assertEqual( - list( - parse( - StringIO('

"éù" .'), - "text/turtle", - base_iri="http://example.com/", - ) - ), - [EXAMPLE_TRIPLE], - ) - - def test_parse_long_str_io(self) -> None: - self.assertEqual( - list( - parse( - StringIO('

"éù" .\n' * 1024), - "text/turtle", - base_iri="http://example.com/", - ) - ), - [EXAMPLE_TRIPLE] * 1024, - ) - - def test_parse_bytes_io(self) -> None: - self.assertEqual( - list( - parse( - BytesIO('

"éù" .'.encode()), - "text/turtle", - base_iri="http://example.com/", - ) - ), - [EXAMPLE_TRIPLE], - ) - - def test_parse_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: - list(parse(fp, mime_type="application/n-triples")) - - def test_parse_quad(self) -> None: - self.assertEqual( - list( - parse( - StringIO(' {

"1" }'), - "application/trig", - base_iri="http://example.com/", - ) - ), - [EXAMPLE_QUAD], - ) - - -class TestSerialize(unittest.TestCase): - def test_serialize_to_bytes_io(self) -> None: - output = BytesIO() - serialize([EXAMPLE_TRIPLE], output, "text/turtle") - self.assertEqual( - output.getvalue().decode(), - ' "éù" .\n', - ) - - def test_serialize_to_file(self) -> None: - with NamedTemporaryFile() as fp: - serialize([EXAMPLE_TRIPLE], fp.name, "text/turtle") - self.assertEqual( - fp.read().decode(), - ' "éù" .\n', - ) - - def test_serialize_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp: - serialize([EXAMPLE_TRIPLE], fp, "text/turtle") - - def test_serialize_quad(self) -> None: - output = BytesIO() - serialize([EXAMPLE_QUAD], output, "application/trig") - self.assertEqual( - output.getvalue(), - b' { "1" }\n', - ) diff --git a/python/tests/test_model.py b/python/tests/test_model.py deleted file mode 100644 index 2931d06d..00000000 --- a/python/tests/test_model.py +++ /dev/null @@ -1,380 +0,0 @@ -import copy -import pickle -import sys -import unittest - -from pyoxigraph import ( - BlankNode, - DefaultGraph, - Literal, - NamedNode, - Quad, - Triple, - Variable, -) - -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") - - -def match_works(test: unittest.TestCase, matched_value: str, constraint: str) -> None: - """Hack for Python < 3.10 compatibility""" - if sys.version_info < (3, 10): - return test.skipTest("match has been introduced by Python 3.10") - found = True - exec( - f""" -match {matched_value}: - case {constraint}: - found = True -""" - ) - test.assertTrue(found) - return None - - -class TestNamedNode(unittest.TestCase): - def test_constructor(self) -> None: - self.assertEqual(NamedNode("http://foo").value, "http://foo") - - def test_string(self) -> None: - self.assertEqual(str(NamedNode("http://foo")), "") - - def test_equal(self) -> None: - self.assertEqual(NamedNode("http://foo"), NamedNode("http://foo")) - self.assertNotEqual(NamedNode("http://foo"), NamedNode("http://bar")) - - def test_pickle(self) -> None: - node = NamedNode("http://foo") - self.assertEqual(pickle.loads(pickle.dumps(node)), node) - self.assertEqual(copy.copy(node), node) - self.assertEqual(copy.deepcopy(node), node) - - def test_basic_match(self) -> None: - match_works(self, 'NamedNode("http://foo")', 'NamedNode("http://foo")') - - def test_wildcard_match(self) -> None: - match_works(self, 'NamedNode("http://foo")', "NamedNode(x)") - - -class TestBlankNode(unittest.TestCase): - def test_constructor(self) -> None: - self.assertEqual(BlankNode("foo").value, "foo") - self.assertNotEqual(BlankNode(), BlankNode()) - - def test_string(self) -> None: - self.assertEqual(str(BlankNode("foo")), "_:foo") - - def test_equal(self) -> None: - self.assertEqual(BlankNode("foo"), BlankNode("foo")) - self.assertNotEqual(BlankNode("foo"), BlankNode("bar")) - self.assertNotEqual(BlankNode("foo"), NamedNode("http://foo")) - self.assertNotEqual(NamedNode("http://foo"), BlankNode("foo")) - - def test_pickle(self) -> None: - node = BlankNode("foo") - self.assertEqual(pickle.loads(pickle.dumps(node)), node) - self.assertEqual(copy.copy(node), node) - self.assertEqual(copy.deepcopy(node), node) - - auto = BlankNode() - self.assertEqual(pickle.loads(pickle.dumps(auto)), auto) - self.assertEqual(copy.copy(auto), auto) - self.assertEqual(copy.deepcopy(auto), auto) - - def test_basic_match(self) -> None: - match_works(self, 'BlankNode("foo")', 'BlankNode("foo")') - - def test_wildcard_match(self) -> None: - match_works(self, 'BlankNode("foo")', "BlankNode(x)") - - -class TestLiteral(unittest.TestCase): - def test_constructor(self) -> None: - 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) -> None: - self.assertEqual(str(Literal("foo")), '"foo"') - self.assertEqual(str(Literal("foo", language="en")), '"foo"@en') - self.assertEqual( - str(Literal("foo", datatype=XSD_INTEGER)), - '"foo"^^', - ) - - def test_equals(self) -> None: - self.assertEqual(Literal("foo", datatype=XSD_STRING), Literal("foo")) - self.assertEqual( - Literal("foo", language="en", datatype=RDF_LANG_STRING), - Literal("foo", language="en"), - ) - self.assertNotEqual(NamedNode("http://foo"), Literal("foo")) - self.assertNotEqual(Literal("foo"), NamedNode("http://foo")) - self.assertNotEqual(BlankNode("foo"), Literal("foo")) - self.assertNotEqual(Literal("foo"), BlankNode("foo")) - - def test_pickle(self) -> None: - simple = Literal("foo") - self.assertEqual(pickle.loads(pickle.dumps(simple)), simple) - self.assertEqual(copy.copy(simple), simple) - self.assertEqual(copy.deepcopy(simple), simple) - - lang_tagged = Literal("foo", language="en") - self.assertEqual(pickle.loads(pickle.dumps(lang_tagged)), lang_tagged) - self.assertEqual(copy.copy(lang_tagged), lang_tagged) - self.assertEqual(copy.deepcopy(lang_tagged), lang_tagged) - - number = Literal("1", datatype=XSD_INTEGER) - self.assertEqual(pickle.loads(pickle.dumps(number)), number) - self.assertEqual(copy.copy(number), number) - self.assertEqual(copy.deepcopy(number), number) - - def test_basic_match(self) -> None: - match_works( - self, 'Literal("foo", language="en")', 'Literal("foo", language="en")' - ) - match_works( - self, - 'Literal("1", datatype=XSD_INTEGER)', - 'Literal("1", datatype=NamedNode("http://www.w3.org/2001/XMLSchema#integer"))', - ) - - def test_wildcard_match(self) -> None: - match_works(self, 'Literal("foo", language="en")', "Literal(v, language=l)") - match_works( - self, 'Literal("1", datatype=XSD_INTEGER)', "Literal(v, datatype=d)" - ) - - -class TestTriple(unittest.TestCase): - def test_constructor(self) -> None: - t = Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - self.assertEqual(t.subject, NamedNode("http://example.com/s")) - self.assertEqual(t.predicate, NamedNode("http://example.com/p")) - self.assertEqual(t.object, NamedNode("http://example.com/o")) - - def test_rdf_star_constructor(self) -> None: - t = Triple( - Triple( - NamedNode("http://example.com/ss"), - NamedNode("http://example.com/sp"), - NamedNode("http://example.com/so"), - ), - NamedNode("http://example.com/p"), - Triple( - NamedNode("http://example.com/os"), - NamedNode("http://example.com/op"), - NamedNode("http://example.com/oo"), - ), - ) - self.assertEqual( - t.subject, - Triple( - NamedNode("http://example.com/ss"), - NamedNode("http://example.com/sp"), - NamedNode("http://example.com/so"), - ), - ) - self.assertEqual(t.predicate, NamedNode("http://example.com/p")) - self.assertEqual( - t.object, - Triple( - NamedNode("http://example.com/os"), - NamedNode("http://example.com/op"), - NamedNode("http://example.com/oo"), - ), - ) - - def test_mapping(self) -> None: - t = Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - self.assertEqual(t[0], NamedNode("http://example.com/s")) - self.assertEqual(t[1], NamedNode("http://example.com/p")) - self.assertEqual(t[2], NamedNode("http://example.com/o")) - - def test_destruct(self) -> None: - (s, p, o) = Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - self.assertEqual(s, NamedNode("http://example.com/s")) - self.assertEqual(p, NamedNode("http://example.com/p")) - self.assertEqual(o, NamedNode("http://example.com/o")) - - def test_string(self) -> None: - self.assertEqual( - str( - Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - ), - " ", - ) - - def test_pickle(self) -> None: - triple = Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - self.assertEqual(pickle.loads(pickle.dumps(triple)), triple) - self.assertEqual(copy.copy(triple), triple) - self.assertEqual(copy.deepcopy(triple), triple) - - def test_match(self) -> None: - match_works( - self, - 'Triple(NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), ' - 'NamedNode("http://example.com/o"))', - 'Triple(NamedNode("http://example.com/s"), NamedNode(p), o)', - ) - - -class TestDefaultGraph(unittest.TestCase): - def test_equal(self) -> None: - self.assertEqual(DefaultGraph(), DefaultGraph()) - self.assertNotEqual(DefaultGraph(), NamedNode("http://bar")) - - def test_pickle(self) -> None: - self.assertEqual(pickle.loads(pickle.dumps(DefaultGraph())), DefaultGraph()) - self.assertEqual(copy.copy(DefaultGraph()), DefaultGraph()) - self.assertEqual(copy.deepcopy(DefaultGraph()), DefaultGraph()) - - def test_match(self) -> None: - match_works(self, "DefaultGraph()", "DefaultGraph()") - - -class TestQuad(unittest.TestCase): - def test_constructor(self) -> None: - t = Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - NamedNode("http://example.com/g"), - ) - self.assertEqual(t.subject, NamedNode("http://example.com/s")) - self.assertEqual(t.predicate, NamedNode("http://example.com/p")) - self.assertEqual(t.object, NamedNode("http://example.com/o")) - self.assertEqual(t.graph_name, NamedNode("http://example.com/g")) - self.assertEqual( - t.triple, - Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ), - ) - self.assertEqual( - Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ), - Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - DefaultGraph(), - ), - ) - - def test_mapping(self) -> None: - t = Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - NamedNode("http://example.com/g"), - ) - self.assertEqual(t[0], NamedNode("http://example.com/s")) - self.assertEqual(t[1], NamedNode("http://example.com/p")) - self.assertEqual(t[2], NamedNode("http://example.com/o")) - self.assertEqual(t[3], NamedNode("http://example.com/g")) - - def test_destruct(self) -> None: - (s, p, o, g) = Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - NamedNode("http://example.com/g"), - ) - self.assertEqual(s, NamedNode("http://example.com/s")) - self.assertEqual(p, NamedNode("http://example.com/p")) - self.assertEqual(o, NamedNode("http://example.com/o")) - self.assertEqual(g, NamedNode("http://example.com/g")) - - def test_string(self) -> None: - self.assertEqual( - str( - Triple( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - ) - ), - " ", - ) - - def test_pickle(self) -> None: - quad = Quad( - NamedNode("http://example.com/s"), - NamedNode("http://example.com/p"), - NamedNode("http://example.com/o"), - NamedNode("http://example.com/g"), - ) - self.assertEqual(pickle.loads(pickle.dumps(quad)), quad) - self.assertEqual(copy.copy(quad), quad) - self.assertEqual(copy.deepcopy(quad), quad) - - def test_match(self) -> None: - match_works( - self, - 'Quad(NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), ' - 'NamedNode("http://example.com/o"), NamedNode("http://example.com/g"))', - 'Quad(NamedNode("http://example.com/s"), NamedNode(p), o, NamedNode("http://example.com/g"))', - ) - - -class TestVariable(unittest.TestCase): - def test_constructor(self) -> None: - self.assertEqual(Variable("foo").value, "foo") - - def test_string(self) -> None: - self.assertEqual(str(Variable("foo")), "?foo") - - def test_equal(self) -> None: - self.assertEqual(Variable("foo"), Variable("foo")) - self.assertNotEqual(Variable("foo"), Variable("bar")) - - def test_pickle(self) -> None: - v = Variable("foo") - self.assertEqual(pickle.loads(pickle.dumps(v)), v) - self.assertEqual(copy.copy(v), v) - self.assertEqual(copy.deepcopy(v), v) - - def test_basic_match(self) -> None: - match_works(self, 'Variable("foo")', 'Variable("foo")') - - def test_wildcard_match(self) -> None: - match_works(self, 'Variable("foo")', "Variable(x)") - - -if __name__ == "__main__": - unittest.main() diff --git a/python/tests/test_store.py b/python/tests/test_store.py deleted file mode 100644 index d6693c4e..00000000 --- a/python/tests/test_store.py +++ /dev/null @@ -1,375 +0,0 @@ -import unittest -from io import BytesIO, UnsupportedOperation -from pathlib import Path -from tempfile import NamedTemporaryFile, TemporaryDirectory, TemporaryFile -from typing import Any - -from pyoxigraph import ( - BlankNode, - DefaultGraph, - NamedNode, - Quad, - QuerySolution, - QuerySolutions, - QueryTriples, - Store, - Triple, - Variable, -) - -foo = NamedNode("http://foo") -bar = NamedNode("http://bar") -baz = NamedNode("http://baz") -triple = Triple(foo, foo, foo) -graph = NamedNode("http://graph") - - -class TestStore(unittest.TestCase): - def test_add(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - store.add(Quad(foo, bar, baz, DefaultGraph())) - store.add(Quad(foo, bar, baz, graph)) - store.add(Quad(triple, bar, baz)) - store.add(Quad(foo, bar, triple)) - self.assertEqual(len(store), 4) - - def test_extend(self) -> None: - store = Store() - store.extend( - ( - Quad(foo, bar, baz), - Quad(foo, bar, baz, graph), - Quad(foo, bar, baz, DefaultGraph()), - ) - ) - self.assertEqual(len(store), 2) - - def test_bulk_extend(self) -> None: - store = Store() - store.bulk_extend( - ( - Quad(foo, bar, baz), - Quad(foo, bar, baz, graph), - Quad(foo, bar, baz, DefaultGraph()), - ) - ) - self.assertEqual(len(store), 2) - - def test_remove(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - store.add(Quad(foo, bar, baz, DefaultGraph())) - store.add(Quad(foo, bar, baz, graph)) - store.remove(Quad(foo, bar, baz)) - self.assertEqual(len(store), 1) - - def test_len(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - store.add(Quad(foo, bar, baz, graph)) - self.assertEqual(len(store), 2) - - def test_in(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - store.add(Quad(foo, bar, baz, DefaultGraph())) - store.add(Quad(foo, bar, baz, graph)) - self.assertIn(Quad(foo, bar, baz), store) - self.assertIn(Quad(foo, bar, baz, DefaultGraph()), store) - self.assertIn(Quad(foo, bar, baz, graph), store) - self.assertNotIn(Quad(foo, bar, baz, foo), store) - - def test_iter(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, DefaultGraph())) - store.add(Quad(foo, bar, baz, graph)) - self.assertEqual( - set(store), - {Quad(foo, bar, baz, DefaultGraph()), Quad(foo, bar, baz, graph)}, - ) - - def test_quads_for_pattern(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, DefaultGraph())) - store.add(Quad(foo, bar, baz, graph)) - self.assertEqual( - set(store.quads_for_pattern(None, None, None)), - {Quad(foo, bar, baz, DefaultGraph()), Quad(foo, bar, baz, graph)}, - ) - self.assertEqual( - set(store.quads_for_pattern(foo, None, None)), - {Quad(foo, bar, baz, DefaultGraph()), Quad(foo, bar, baz, graph)}, - ) - self.assertEqual( - set(store.quads_for_pattern(None, None, None, graph)), - {Quad(foo, bar, baz, graph)}, - ) - self.assertEqual( - set(store.quads_for_pattern(foo, None, None, DefaultGraph())), - {Quad(foo, bar, baz, DefaultGraph())}, - ) - - def test_ask_query(self) -> None: - 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) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - results: Any = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") - self.assertIsInstance(results, QueryTriples) - self.assertEqual( - set(results), - {Triple(foo, bar, baz)}, - ) - - def test_select_query(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz)) - solutions: Any = store.query("SELECT ?s ?o WHERE { ?s ?p ?o }") - self.assertIsInstance(solutions, QuerySolutions) - self.assertEqual(solutions.variables, [Variable("s"), Variable("o")]) - solution = next(solutions) - self.assertIsInstance(solution, QuerySolution) - self.assertEqual(solution[0], foo) - self.assertEqual(solution[1], baz) - self.assertEqual(solution["s"], foo) - self.assertEqual(solution["o"], baz) - self.assertEqual(solution[Variable("s")], foo) - self.assertEqual(solution[Variable("o")], baz) - s, o = solution - self.assertEqual(s, foo) - self.assertEqual(o, baz) - - def test_select_query_union_default_graph(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, graph)) - results: Any = store.query("SELECT ?s WHERE { ?s ?p ?o }") - self.assertEqual(len(list(results)), 0) - results = store.query( - "SELECT ?s WHERE { ?s ?p ?o }", use_default_graph_as_union=True - ) - self.assertEqual(len(list(results)), 1) - results = store.query( - "SELECT ?s WHERE { ?s ?p ?o }", - use_default_graph_as_union=True, - named_graphs=[graph], - ) - self.assertEqual(len(list(results)), 1) - - def test_select_query_with_default_graph(self) -> None: - store = Store() - graph_bnode = BlankNode("g") - store.add(Quad(foo, bar, baz, graph)) - store.add(Quad(foo, bar, foo)) - store.add(Quad(foo, bar, bar, graph_bnode)) - results: Any = store.query("SELECT ?s WHERE { ?s ?p ?o }") - self.assertEqual(len(list(results)), 1) - results = store.query("SELECT ?s WHERE { ?s ?p ?o }", default_graph=graph) - self.assertEqual(len(list(results)), 1) - results = store.query( - "SELECT ?s WHERE { ?s ?p ?o }", - default_graph=[DefaultGraph(), graph, graph_bnode], - ) - self.assertEqual(len(list(results)), 3) - - def test_select_query_with_named_graph(self) -> None: - store = Store() - graph_bnode = BlankNode("g") - store.add(Quad(foo, bar, baz, graph)) - store.add(Quad(foo, bar, foo)) - store.add(Quad(foo, bar, bar, graph_bnode)) - store.add(Quad(foo, bar, bar, foo)) - results: Any = store.query( - "SELECT ?s WHERE { GRAPH ?g { ?s ?p ?o } }", - named_graphs=[graph, graph_bnode], - ) - self.assertEqual(len(list(results)), 2) - - def test_update_insert_data(self) -> None: - store = Store() - store.update("INSERT DATA { }") - self.assertEqual(len(store), 1) - - def test_update_delete_data(self) -> None: - store = Store() - store.add(Quad(foo, foo, foo)) - store.update("DELETE DATA { }") - self.assertEqual(len(store), 0) - - def test_update_delete_where(self) -> None: - 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) -> None: - store = Store() - store.update("LOAD ") - self.assertGreater(len(store), 100) - - def test_update_star(self) -> None: - store = Store() - store.update( - "PREFIX : INSERT DATA { :alice :claims << :bob :age 23 >> }" - ) - results: Any = store.query( - "PREFIX : SELECT ?p ?a WHERE { ?p :claims << :bob :age ?a >> }" - ) - self.assertEqual(len(list(results)), 1) - - def test_load_ntriples_to_default_graph(self) -> None: - store = Store() - store.load( - BytesIO(b" ."), - mime_type="application/n-triples", - ) - self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) - - def test_load_ntriples_to_named_graph(self) -> None: - store = Store() - store.load( - BytesIO(b" ."), - mime_type="application/n-triples", - to_graph=graph, - ) - self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - - def test_load_turtle_with_base_iri(self) -> None: - store = Store() - store.load( - BytesIO(b" <> ."), - mime_type="text/turtle", - base_iri="http://baz", - ) - self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) - - def test_load_nquads(self) -> None: - store = Store() - store.load( - BytesIO(b" ."), - mime_type="application/n-quads", - ) - self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - - def test_load_trig_with_base_iri(self) -> None: - store = Store() - store.load( - BytesIO(b" { <> . }"), - mime_type="application/trig", - base_iri="http://baz", - ) - self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - - def test_load_file(self) -> None: - with NamedTemporaryFile(delete=False) as fp: - file_name = Path(fp.name) - fp.write(b" .") - store = Store() - store.load(file_name, mime_type="application/n-quads") - file_name.unlink() - self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - - def test_load_with_io_error(self) -> None: - with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: - Store().load(fp, mime_type="application/n-triples") - - def test_dump_ntriples(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, graph)) - output = BytesIO() - store.dump(output, "application/n-triples", from_graph=graph) - self.assertEqual( - output.getvalue(), - b" .\n", - ) - - def test_dump_nquads(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, graph)) - output = BytesIO() - store.dump(output, "application/n-quads") - self.assertEqual( - output.getvalue(), - b" .\n", - ) - - def test_dump_trig(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, graph)) - store.add(Quad(foo, bar, baz)) - output = BytesIO() - store.dump(output, "application/trig") - self.assertEqual( - output.getvalue(), - b" .\n { }\n", - ) - - def test_dump_file(self) -> None: - with NamedTemporaryFile(delete=False) as fp: - file_name = Path(fp.name) - store = Store() - store.add(Quad(foo, bar, baz, graph)) - store.dump(file_name, "application/n-quads") - self.assertEqual( - file_name.read_text(), - " .\n", - ) - - def test_dump_with_io_error(self) -> None: - with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp: - Store().dump(fp, mime_type="application/rdf+xml") - - def test_write_in_read(self) -> None: - store = Store() - store.add(Quad(foo, bar, bar)) - store.add(Quad(foo, bar, baz)) - for triple in store: - store.add(Quad(triple.object, triple.predicate, triple.subject)) - self.assertEqual(len(store), 4) - - def test_add_graph(self) -> None: - store = Store() - store.add_graph(graph) - self.assertEqual(list(store.named_graphs()), [graph]) - - def test_remove_graph(self) -> None: - store = Store() - store.add(Quad(foo, bar, baz, graph)) - store.add_graph(NamedNode("http://graph2")) - store.remove_graph(graph) - store.remove_graph(NamedNode("http://graph2")) - self.assertEqual(list(store.named_graphs()), []) - self.assertEqual(list(store), []) - - def test_read_only(self) -> None: - quad = Quad(foo, bar, baz, graph) - with TemporaryDirectory() as dir: - store = Store(dir) - store.add(quad) - del store - store = Store.read_only(dir) - self.assertEqual(list(store), [quad]) - - def test_secondary(self) -> None: - quad = Quad(foo, bar, baz, graph) - with TemporaryDirectory() as dir: - store = Store(dir) - store.add(quad) - store.flush() - - secondary_store = Store.secondary(dir) - self.assertEqual(list(secondary_store), [quad]) - - store.remove(quad) - store.flush() - self.assertEqual(list(secondary_store), []) - del secondary_store - del store - - -if __name__ == "__main__": - unittest.main()