parent
427d675c9b
commit
77edc05ced
@ -1 +0,0 @@ |
||||
Subproject commit aecd720a5fc2bf7eae9649265a017b68605a8c87 |
@ -1,38 +0,0 @@ |
||||
[package] |
||||
name = "pyoxigraph" |
||||
version.workspace = true |
||||
authors.workspace = true |
||||
license.workspace = true |
||||
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.workspace = true |
||||
rust-version.workspace = true |
||||
publish = false |
||||
|
||||
[lib] |
||||
crate-type = ["cdylib"] |
||||
name = "pyoxigraph" |
||||
doctest = false |
||||
doc = false |
||||
|
||||
[features] |
||||
abi3 = ["pyo3/abi3-py38"] |
||||
rocksdb-pkg-config = ["oxigraph/rocksdb-pkg-config"] |
||||
|
||||
[dependencies] |
||||
pyo3 = { workspace = true, features = ["extension-module"] } |
||||
|
||||
[target.'cfg(any(target_family = "windows", target_os = "macos", target_os = "ios"))'.dependencies] |
||||
oxigraph = { workspace = true, features = ["http-client-native-tls"] } |
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies] |
||||
oxigraph.workspace = true |
||||
|
||||
[target.'cfg(not(any(target_family = "windows", target_os = "macos", target_os = "ios", target_family = "wasm")))'.dependencies] |
||||
oxigraph = { workspace = true, features = ["http-client-rustls-native"] } |
||||
|
||||
[lints] |
||||
workspace = true |
@ -1,83 +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) |
||||
|
||||
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). |
||||
|
||||
Note that by default the installation will not use [cpython stable ABI](https://docs.python.org/3/c-api/stable.html). |
||||
Use `--features abi3` feature to use cpython stable ABI. |
||||
|
||||
## 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. |
@ -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)} |
@ -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 <https://www.w3.org/TR/sparql11-overview/>`_ standard. |
||||
|
||||
It is built on top of `Oxigraph <https://crates.io/crates/oxigraph>`_ using `PyO3 <https://pyo3.rs/>`_. |
||||
|
||||
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>`_. |
||||
|
||||
There is also a small library providing a `rdflib <https://rdflib.readthedocs.io>`_ store using pyoxigraph: `oxrdflib <https://github.com/oxigraph/oxrdflib>`_. |
||||
|
||||
Oxigraph and pyoxigraph source code are on `GitHub <https://github.com/oxigraph/oxigraph/tree/main/python>`_. |
||||
|
||||
|
||||
Installation |
||||
"""""""""""" |
||||
|
||||
Pyoxigraph is distributed on `Pypi <https://pypi.org/project/pyoxigraph/>`_. |
||||
|
||||
To install it, run the usual ``pip install pyoxigraph`` |
||||
|
||||
|
||||
Example |
||||
""""""" |
||||
|
||||
Insert the triple ``<http://example/> <http://schema.org/name> "example"`` and print the name of ``<http://example/>`` 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 { <http://example/> <http://schema.org/name> ?name }'): |
||||
print(binding['name'].value) |
||||
|
||||
|
||||
Table of contents |
||||
""""""""""""""""" |
||||
|
||||
.. toctree:: |
||||
|
||||
model |
||||
io |
||||
store |
||||
sparql |
||||
migration |
||||
|
||||
|
||||
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>`_. |
@ -1,21 +0,0 @@ |
||||
RDF Parsing and Serialization |
||||
============================= |
||||
.. py:currentmodule:: pyoxigraph |
||||
|
||||
Oxigraph provides functions to parse and serialize RDF files: |
||||
|
||||
|
||||
Parsing |
||||
""""""" |
||||
.. autofunction:: parse |
||||
|
||||
|
||||
Serialization |
||||
""""""""""""" |
||||
.. autofunction:: serialize |
||||
|
||||
|
||||
Formats |
||||
""""""" |
||||
.. autoclass:: RdfFormat |
||||
:members: |
@ -1,47 +0,0 @@ |
||||
Migration Guide |
||||
=============== |
||||
|
||||
From 0.3 to 0.4 |
||||
""""""""""""""" |
||||
|
||||
* Python 3.7 and ``musllinux_1_1`` support have been removed. |
||||
* :py:class:`OSError` is now raised instead of :py:class:`IOError` on OS errors. |
||||
* The ``mime_type`` parameter have been renamed to ``format`` in I/O functions. |
||||
Using :py:class:`RdfFormat` is recommended to describe formats. |
||||
* Boolean SPARQL results are now encoded with the :py:class:`QueryBoolean` class and not a simple :py:class:`bool`. |
||||
* A `path` parameter has been added to all I/O method to read from a file. |
||||
The existing ``input`` parameter now consider :py:class:`str` values to be a serialization to parse. |
||||
For example, ``parse(path="foo.ttl")`` will parse the file ``foo.ttl`` whereas ``parse("foo", format=RdfFormat.N_TRIPLES)`` will parse a N-Triples file which content is ``foo``. |
||||
|
||||
|
||||
From 0.2 to 0.3 |
||||
""""""""""""""" |
||||
|
||||
* Python 3.6 and ``manylinux2010`` (`PEP 571 <https://www.python.org/dev/peps/pep-0571/>`_) support have been removed. The new minimal versions are Python 3.7 and ``manylinux2014`` (`PEP 599 <https://www.python.org/dev/peps/pep-0599/>`_). |
||||
* The on-disk storage system has been rebuilt on top of `RocksDB <http://rocksdb.org/>`_. |
||||
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 <https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html>`_ 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`. |
@ -1,47 +0,0 @@ |
||||
RDF Model |
||||
========= |
||||
.. py:currentmodule:: pyoxigraph |
||||
|
||||
Oxigraph provides python classes to represents basic RDF concepts: |
||||
|
||||
|
||||
`IRIs <https://www.w3.org/TR/rdf11-concepts/#dfn-iri>`_ |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: NamedNode |
||||
:members: |
||||
|
||||
|
||||
`Blank Nodes <https://www.w3.org/TR/rdf11-concepts/#dfn-blank-node>`_ |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: BlankNode |
||||
:members: |
||||
|
||||
|
||||
`Literals <https://www.w3.org/TR/rdf11-concepts/#dfn-literal>`_ |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: Literal |
||||
:members: |
||||
|
||||
|
||||
`Triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_ |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: Triple |
||||
:members: |
||||
|
||||
|
||||
Quads (`triples <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-triple>`_ in a `RDF dataset <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_) |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: Quad |
||||
:members: |
||||
|
||||
.. autoclass:: DefaultGraph |
||||
:members: |
||||
|
||||
|
||||
`Datasets <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_ |
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" |
||||
.. autoclass:: Dataset |
||||
:members: |
||||
|
||||
.. autoclass:: CanonicalizationAlgorithm |
||||
:members: |
@ -1,33 +0,0 @@ |
||||
SPARQL utility objects |
||||
====================== |
||||
.. py:currentmodule:: pyoxigraph |
||||
|
||||
Oxigraph provides also some utilities related to SPARQL queries: |
||||
|
||||
Variable |
||||
"""""""" |
||||
.. autoclass:: Variable |
||||
:members: |
||||
|
||||
``SELECT`` solutions |
||||
"""""""""""""""""""" |
||||
.. autoclass:: QuerySolutions |
||||
:members: |
||||
.. autoclass:: QuerySolution |
||||
:members: |
||||
|
||||
``ASK`` results |
||||
""""""""""""""" |
||||
.. autoclass:: QueryBoolean |
||||
:members: |
||||
|
||||
``CONSTRUCT`` results |
||||
""""""""""""""""""""" |
||||
.. autoclass:: QueryTriples |
||||
:members: |
||||
|
||||
Query results parsing |
||||
""""""""""""""""""""" |
||||
.. autofunction:: parse_query_results |
||||
.. autoclass:: QueryResultsFormat |
||||
:members: |
@ -1,6 +0,0 @@ |
||||
RDF Store |
||||
========= |
||||
.. py:currentmodule:: pyoxigraph |
||||
|
||||
.. autoclass:: Store |
||||
:members: |
@ -1,438 +0,0 @@ |
||||
import argparse |
||||
import ast |
||||
import importlib |
||||
import inspect |
||||
import logging |
||||
import re |
||||
import subprocess |
||||
from functools import reduce |
||||
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 |
||||
|
||||
|
||||
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__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__del__": None, |
||||
"__delattr__": ([path_to_type("str")], path_to_type("None")), |
||||
"__delitem__": ([path_to_type("typing", "Any")], path_to_type("typing", "Any")), |
||||
"__dict__": None, |
||||
"__dir__": None, |
||||
"__doc__": None, |
||||
"__eq__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__format__": ([path_to_type("str")], path_to_type("str")), |
||||
"__ge__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__getattribute__": ([path_to_type("str")], path_to_type("typing", "Any")), |
||||
"__getitem__": ([path_to_type("typing", "Any")], path_to_type("typing", "Any")), |
||||
"__gt__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__hash__": ([], path_to_type("int")), |
||||
"__init__": ([], path_to_type("None")), |
||||
"__init_subclass__": None, |
||||
"__iter__": ([], path_to_type("typing", "Any")), |
||||
"__le__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__len__": ([], path_to_type("int")), |
||||
"__lt__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__module__": None, |
||||
"__ne__": ([path_to_type("typing", "Any")], path_to_type("bool")), |
||||
"__new__": None, |
||||
"__next__": ([], path_to_type("typing", "Any")), |
||||
"__reduce__": None, |
||||
"__reduce_ex__": None, |
||||
"__repr__": ([], path_to_type("str")), |
||||
"__setattr__": ( |
||||
[path_to_type("str"), path_to_type("typing", "Any")], |
||||
path_to_type("None"), |
||||
), |
||||
"__setitem__": ( |
||||
[path_to_type("typing", "Any"), path_to_type("typing", "Any")], |
||||
path_to_type("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("tuple"), |
||||
slice=ast.Tuple(elts=[path_to_type("str"), ast.Ellipsis()], ctx=ast.Load()), |
||||
ctx=ast.Load(), |
||||
), |
||||
value=ast.Constant(member_value), |
||||
simple=1, |
||||
) |
||||
) |
||||
elif member_value is not None: |
||||
constants.append( |
||||
ast.AnnAssign( |
||||
target=ast.Name(id=member_name, ctx=ast.Store()), |
||||
annotation=concatenated_path_to_type( |
||||
member_value.__class__.__name__, element_path, types_to_import |
||||
), |
||||
value=ast.Ellipsis(), |
||||
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 path_to_type("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): |
||||
new_elements.append(concatenated_path_to_type(group[0], element_path, types_to_import)) |
||||
elif len(group) == 2 and isinstance(group[0], str) and isinstance(group[1], list): |
||||
new_elements.append( |
||||
ast.Subscript( |
||||
value=concatenated_path_to_type(group[0], element_path, types_to_import), |
||||
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 reduce(lambda left, right: ast.BinOp(left=left, op=ast.BitOr(), right=right), new_elements) |
||||
|
||||
return parse_sequence(stack[0]) |
||||
|
||||
|
||||
def concatenated_path_to_type(path: str, element_path: List[str], types_to_import: Set[str]) -> ast.AST: |
||||
parts = path.split(".") |
||||
if any(not p for p in parts): |
||||
raise ValueError(f"Not able to parse type '{path}' used by {'.'.join(element_path)}") |
||||
if len(parts) > 1: |
||||
types_to_import.add(".".join(parts[:-1])) |
||||
return path_to_type(*parts) |
||||
|
||||
|
||||
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_ruff(file: str) -> None: |
||||
subprocess.check_call(["python", "-m", "ruff", "format", file]) |
||||
|
||||
|
||||
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("--ruff", help="Formats the generated stubs using Ruff", action="store_true") |
||||
args = parser.parse_args() |
||||
stub_content = ast.unparse(module_stubs(importlib.import_module(args.module_name))) |
||||
args.out.write(stub_content) |
||||
if args.ruff: |
||||
format_with_ruff(args.out.name) |
@ -1,2 +0,0 @@ |
||||
pyoxigraph.pyoxigraph |
||||
pyoxigraph.DefaultGraph.__init__ |
@ -1,59 +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.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.8" |
||||
|
||||
[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.maturin] |
||||
strip = true |
||||
|
||||
[tool.ruff] |
||||
line-length = 120 |
||||
|
||||
[tool.ruff.lint] |
||||
select = [ |
||||
"ARG", |
||||
"B", |
||||
"C40", |
||||
"E", |
||||
"F", |
||||
"FBT", |
||||
"I", |
||||
"ICN", |
||||
"N", |
||||
"PIE", |
||||
"PTH", |
||||
"RET", |
||||
"RUF", |
||||
"SIM", |
||||
"T10", |
||||
"TCH", |
||||
"TID", |
||||
"UP", |
||||
"W", |
||||
"YTT" |
||||
] |
@ -1,6 +0,0 @@ |
||||
furo |
||||
maturin~=1.0 |
||||
mypy~=1.0 |
||||
ruff~=0.3.0 |
||||
sphinx~=7.0 |
||||
sphinx-lint~=0.9.1 |
@ -1,327 +0,0 @@ |
||||
use crate::model::{hash, PyGraphNameRef, PyNamedNodeRef, PyQuad, PySubjectRef, PyTermRef}; |
||||
use oxigraph::model::dataset::{CanonicalizationAlgorithm, Dataset}; |
||||
use oxigraph::model::{Quad, QuadRef}; |
||||
use pyo3::exceptions::PyKeyError; |
||||
use pyo3::prelude::*; |
||||
|
||||
/// An in-memory `RDF dataset <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_.
|
||||
///
|
||||
/// It can accommodate a fairly large number of quads (in the few millions).
|
||||
///
|
||||
/// Use :py:class:`Store` if you need on-disk persistence or SPARQL.
|
||||
///
|
||||
/// Warning: It interns the strings and does not do any garbage collection yet:
|
||||
/// if you insert and remove a lot of different terms, memory will grow without any reduction.
|
||||
///
|
||||
/// :param quads: some quads to initialize the dataset with.
|
||||
/// :type quads: collections.abc.Iterable[Quad] or None, optional
|
||||
///
|
||||
/// The :py:class:`str` function provides an N-Quads serialization:
|
||||
///
|
||||
/// >>> str(Dataset([Quad(NamedNode('http://example.com/s'), NamedNode('http://example.com/p'), NamedNode('http://example.com/o'), NamedNode('http://example.com/g'))]))
|
||||
/// '<http://example.com/s> <http://example.com/p> <http://example.com/o> <http://example.com/g> .\n'
|
||||
#[pyclass(name = "Dataset", module = "pyoxigraph")] |
||||
#[derive(Eq, PartialEq, Debug, Clone)] |
||||
pub struct PyDataset { |
||||
inner: Dataset, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyDataset { |
||||
#[new] |
||||
#[pyo3(signature = (quads = None))] |
||||
fn new(quads: Option<&Bound<'_, PyAny>>) -> PyResult<Self> { |
||||
let mut inner = Dataset::new(); |
||||
if let Some(quads) = quads { |
||||
for quad in quads.iter()? { |
||||
inner.insert(&*quad?.extract::<PyRef<'_, PyQuad>>()?); |
||||
} |
||||
} |
||||
Ok(Self { inner }) |
||||
} |
||||
|
||||
/// Looks for the quads with the given subject.
|
||||
///
|
||||
/// :param subject: the quad subject.
|
||||
/// :type subject: NamedNode or BlankNode or Triple
|
||||
/// :return: an iterator of the quads.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
///
|
||||
/// >>> store = Dataset([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store.quads_for_subject(NamedNode('http://example.com')))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
pub fn quads_for_subject(&self, subject: PySubjectRef<'_>) -> QuadIter { |
||||
QuadIter { |
||||
inner: self |
||||
.inner |
||||
.quads_for_subject(&subject) |
||||
.map(QuadRef::into_owned) |
||||
.collect::<Vec<_>>() |
||||
.into_iter(), |
||||
} |
||||
} |
||||
|
||||
/// Looks for the quads with the given predicate.
|
||||
///
|
||||
/// :param predicate: the quad predicate.
|
||||
/// :type predicate: NamedNode
|
||||
/// :return: an iterator of the quads.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
///
|
||||
/// >>> store = Dataset([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store.quads_for_predicate(NamedNode('http://example.com/p')))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
pub fn quads_for_predicate(&self, predicate: PyNamedNodeRef<'_>) -> QuadIter { |
||||
QuadIter { |
||||
inner: self |
||||
.inner |
||||
.quads_for_predicate(&predicate) |
||||
.map(QuadRef::into_owned) |
||||
.collect::<Vec<_>>() |
||||
.into_iter(), |
||||
} |
||||
} |
||||
|
||||
/// Looks for the quads with the given object.
|
||||
///
|
||||
/// :param object: the quad object.
|
||||
/// :type object: NamedNode or BlankNode or Literal or Triple
|
||||
/// :return: an iterator of the quads.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
///
|
||||
/// >>> store = Dataset([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store.quads_for_object(Literal('1')))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
pub fn quads_for_object(&self, object: PyTermRef<'_>) -> QuadIter { |
||||
QuadIter { |
||||
inner: self |
||||
.inner |
||||
.quads_for_object(&object) |
||||
.map(QuadRef::into_owned) |
||||
.collect::<Vec<_>>() |
||||
.into_iter(), |
||||
} |
||||
} |
||||
|
||||
/// Looks for the quads with the given graph name.
|
||||
///
|
||||
/// :param graph_name: the quad graph name.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
|
||||
/// :return: an iterator of the quads.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
///
|
||||
/// >>> store = Dataset([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store.quads_for_graph_name(NamedNode('http://example.com/g')))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
pub fn quads_for_graph_name(&self, graph_name: PyGraphNameRef<'_>) -> QuadIter { |
||||
QuadIter { |
||||
inner: self |
||||
.inner |
||||
.quads_for_graph_name(&graph_name) |
||||
.map(QuadRef::into_owned) |
||||
.collect::<Vec<_>>() |
||||
.into_iter(), |
||||
} |
||||
} |
||||
|
||||
/// Adds a quad to the dataset.
|
||||
///
|
||||
/// :param quad: the quad to add.
|
||||
/// :type quad: Quad
|
||||
/// :rtype: None
|
||||
///
|
||||
/// >>> quad = Quad(NamedNode('http://example.com/s'), NamedNode('http://example.com/p'), NamedNode('http://example.com/o'), NamedNode('http://example.com/g'))
|
||||
/// >>> dataset = Dataset()
|
||||
/// >>> dataset.add(quad)
|
||||
/// >>> quad in dataset
|
||||
/// True
|
||||
fn add(&mut self, quad: &PyQuad) { |
||||
self.inner.insert(quad); |
||||
} |
||||
|
||||
/// Removes a quad from the dataset and raises an exception if it is not in the set.
|
||||
///
|
||||
/// :param quad: the quad to remove.
|
||||
/// :type quad: Quad
|
||||
/// :rtype: None
|
||||
/// :raises KeyError: if the element was not in the set.
|
||||
///
|
||||
/// >>> quad = Quad(NamedNode('http://example.com/s'), NamedNode('http://example.com/p'), NamedNode('http://example.com/o'), NamedNode('http://example.com/g'))
|
||||
/// >>> dataset = Dataset([quad])
|
||||
/// >>> dataset.remove(quad)
|
||||
/// >>> quad in dataset
|
||||
/// False
|
||||
fn remove(&mut self, quad: &PyQuad) -> PyResult<()> { |
||||
if self.inner.remove(quad) { |
||||
Ok(()) |
||||
} else { |
||||
Err(PyKeyError::new_err(format!( |
||||
"{} is not in the Dataset", |
||||
QuadRef::from(quad) |
||||
))) |
||||
} |
||||
} |
||||
|
||||
/// Removes a quad from the dataset if it is present.
|
||||
///
|
||||
/// :param quad: the quad to remove.
|
||||
/// :type quad: Quad
|
||||
/// :rtype: None
|
||||
///
|
||||
/// >>> quad = Quad(NamedNode('http://example.com/s'), NamedNode('http://example.com/p'), NamedNode('http://example.com/o'), NamedNode('http://example.com/g'))
|
||||
/// >>> dataset = Dataset([quad])
|
||||
/// >>> dataset.discard(quad)
|
||||
/// >>> quad in dataset
|
||||
/// False
|
||||
fn discard(&mut self, quad: &PyQuad) { |
||||
self.inner.remove(quad); |
||||
} |
||||
|
||||
/// Removes all quads from the dataset.
|
||||
///
|
||||
/// :rtype: None
|
||||
///
|
||||
/// >>> quad = Quad(NamedNode('http://example.com/s'), NamedNode('http://example.com/p'), NamedNode('http://example.com/o'), NamedNode('http://example.com/g'))
|
||||
/// >>> dataset = Dataset([quad])
|
||||
/// >>> dataset.clear()
|
||||
/// >>> len(dataset)
|
||||
/// 0
|
||||
fn clear(&mut self) { |
||||
self.inner.clear() |
||||
} |
||||
|
||||
/// Canonicalizes the dataset by renaming blank nodes.
|
||||
///
|
||||
/// Warning: Blank node ids depends on the current shape of the graph. Adding a new quad might change the ids of a lot of blank nodes.
|
||||
/// Hence, this canonization might not be suitable for diffs.
|
||||
///
|
||||
/// Warning: This implementation worst-case complexity is in *O(b!)* with *b* the number of blank nodes in the input dataset.
|
||||
///
|
||||
/// :param algorithm: the canonicalization algorithm to use.
|
||||
/// :type algorithm: CanonicalizationAlgorithm
|
||||
/// :rtype: None
|
||||
///
|
||||
/// >>> d1 = Dataset([Quad(BlankNode(), NamedNode('http://example.com/p'), BlankNode())])
|
||||
/// >>> d2 = Dataset([Quad(BlankNode(), NamedNode('http://example.com/p'), BlankNode())])
|
||||
/// >>> d1 == d2
|
||||
/// False
|
||||
/// >>> d1.canonicalize(CanonicalizationAlgorithm.UNSTABLE)
|
||||
/// >>> d2.canonicalize(CanonicalizationAlgorithm.UNSTABLE)
|
||||
/// >>> d1 == d2
|
||||
/// True
|
||||
fn canonicalize(&mut self, algorithm: &PyCanonicalizationAlgorithm) { |
||||
self.inner.canonicalize(algorithm.inner) |
||||
} |
||||
|
||||
fn __str__(&self) -> String { |
||||
self.inner.to_string() |
||||
} |
||||
|
||||
fn __bool__(&self) -> bool { |
||||
self.inner.is_empty() |
||||
} |
||||
|
||||
fn __eq__(&self, other: &Self) -> bool { |
||||
self.inner == other.inner |
||||
} |
||||
|
||||
fn __ne__(&self, other: &Self) -> bool { |
||||
self.inner != other.inner |
||||
} |
||||
|
||||
fn __len__(&self) -> usize { |
||||
self.inner.len() |
||||
} |
||||
|
||||
fn __contains__(&self, quad: &PyQuad) -> bool { |
||||
self.inner.contains(quad) |
||||
} |
||||
|
||||
fn __iter__(&self) -> QuadIter { |
||||
// TODO: very inefficient
|
||||
QuadIter { |
||||
inner: self |
||||
.inner |
||||
.iter() |
||||
.map(QuadRef::into_owned) |
||||
.collect::<Vec<_>>() |
||||
.into_iter(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[pyclass(unsendable, module = "pyoxigraph")] |
||||
pub struct QuadIter { |
||||
inner: std::vec::IntoIter<Quad>, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl QuadIter { |
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self) -> Option<PyQuad> { |
||||
Some(self.inner.next()?.into()) |
||||
} |
||||
} |
||||
|
||||
/// RDF canonicalization algorithms.
|
||||
///
|
||||
/// The following algorithms are supported:
|
||||
///
|
||||
/// * :py:attr:`CanonicalizationAlgorithm.UNSTABLE`: an unstable algorithm preferred by PyOxigraph.
|
||||
#[pyclass(name = "CanonicalizationAlgorithm", module = "pyoxigraph")] |
||||
#[derive(Clone)] |
||||
pub struct PyCanonicalizationAlgorithm { |
||||
inner: CanonicalizationAlgorithm, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyCanonicalizationAlgorithm { |
||||
/// The algorithm preferred by PyOxigraph.
|
||||
///
|
||||
/// Warning: Might change between Oxigraph versions. No stability guaranties.
|
||||
#[classattr] |
||||
const UNSTABLE: Self = Self { |
||||
inner: CanonicalizationAlgorithm::Unstable, |
||||
}; |
||||
|
||||
fn __repr__(&self) -> String { |
||||
format!( |
||||
"<CanonicalizationAlgorithm {}>", |
||||
match self.inner { |
||||
CanonicalizationAlgorithm::Unstable => "unstable", |
||||
_ => "unknown", |
||||
} |
||||
) |
||||
} |
||||
|
||||
fn __hash__(&self) -> u64 { |
||||
hash(&self.inner) |
||||
} |
||||
|
||||
fn __eq__(&self, other: &Self) -> bool { |
||||
self.inner == other.inner |
||||
} |
||||
|
||||
fn __ne__(&self, other: &Self) -> bool { |
||||
self.inner != other.inner |
||||
} |
||||
|
||||
/// :rtype: CanonicalizationAlgorithm
|
||||
fn __copy__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
/// :type memo: typing.Any
|
||||
/// :rtype: CanonicalizationAlgorithm
|
||||
#[allow(unused_variables)] |
||||
fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ Bound<'_, PyAny>) -> PyRef<'a, Self> { |
||||
slf |
||||
} |
||||
} |
@ -1,640 +0,0 @@ |
||||
#![allow(clippy::needless_option_as_deref)] |
||||
|
||||
use crate::model::{hash, PyQuad, PyTriple}; |
||||
use oxigraph::io::{FromReadQuadReader, RdfFormat, RdfParseError, RdfParser, RdfSerializer}; |
||||
use oxigraph::model::QuadRef; |
||||
use pyo3::exceptions::{PyDeprecationWarning, PySyntaxError, PyValueError}; |
||||
use pyo3::intern; |
||||
use pyo3::prelude::*; |
||||
use pyo3::types::{PyBytes, PyString}; |
||||
use std::cmp::max; |
||||
use std::ffi::OsStr; |
||||
use std::fs::File; |
||||
use std::io::{self, BufWriter, Cursor, Read, Write}; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::sync::OnceLock; |
||||
|
||||
/// Parses RDF graph and dataset serialization formats.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
|
||||
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :param path: The file path to read from. Replaces the ``input`` parameter.
|
||||
/// :type path: str or os.PathLike[str] or None, optional
|
||||
/// :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 without_named_graphs: Sets that the parser must fail when parsing a named graph.
|
||||
/// :type without_named_graphs: bool, optional
|
||||
/// :param rename_blank_nodes: Renames the blank nodes identifiers from the ones set in the serialization to random ids. This allows to avoid identifier conflicts when merging graphs together.
|
||||
/// :type rename_blank_nodes: bool, optional
|
||||
/// :return: an iterator of RDF triples or quads depending on the format.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises SyntaxError: if the provided data is invalid.
|
||||
/// :raises OSError: if a system error happens while reading the file.
|
||||
///
|
||||
/// >>> list(parse(input=b'<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/"))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<DefaultGraph>>]
|
||||
#[pyfunction] |
||||
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, without_named_graphs = false, rename_blank_nodes = false))] |
||||
pub fn parse( |
||||
input: Option<PyReadableInput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
path: Option<PathBuf>, |
||||
base_iri: Option<&str>, |
||||
without_named_graphs: bool, |
||||
rename_blank_nodes: bool, |
||||
py: Python<'_>, |
||||
) -> PyResult<PyObject> { |
||||
let input = PyReadable::from_args(&path, input, py)?; |
||||
let format = lookup_rdf_format(format, path.as_deref())?; |
||||
let mut parser = RdfParser::from_format(format); |
||||
if let Some(base_iri) = base_iri { |
||||
parser = parser |
||||
.with_base_iri(base_iri) |
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?; |
||||
} |
||||
if without_named_graphs { |
||||
parser = parser.without_named_graphs(); |
||||
} |
||||
if rename_blank_nodes { |
||||
parser = parser.rename_blank_nodes(); |
||||
} |
||||
Ok(PyQuadReader { |
||||
inner: parser.parse_read(input), |
||||
file_path: path, |
||||
} |
||||
.into_py(py)) |
||||
} |
||||
|
||||
/// Serializes an RDF graph or dataset.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `canonical <https://www.w3.org/TR/n-triples/#canonical-ntriples>`_ `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :param input: the RDF triples and quads to serialize.
|
||||
/// :type input: collections.abc.Iterable[Triple] or collections.abc.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')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
|
||||
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :return: :py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set.
|
||||
/// :rtype: bytes or None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises TypeError: if a triple is given during a quad format serialization or reverse.
|
||||
/// :raises OSError: if a system error happens while writing the file.
|
||||
///
|
||||
/// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], format=RdfFormat.TURTLE)
|
||||
/// b'<http://example.com> <http://example.com/p> "1" .\n'
|
||||
///
|
||||
/// >>> import io
|
||||
/// >>> output = io.BytesIO()
|
||||
/// >>> serialize([Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))], output, RdfFormat.TURTLE)
|
||||
/// >>> output.getvalue()
|
||||
/// b'<http://example.com> <http://example.com/p> "1" .\n'
|
||||
#[pyfunction] |
||||
#[pyo3(signature = (input, output = None, format = None))] |
||||
pub fn serialize<'py>( |
||||
input: &Bound<'py, PyAny>, |
||||
output: Option<PyWritableOutput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
py: Python<'py>, |
||||
) -> PyResult<Option<Bound<'py, PyBytes>>> { |
||||
PyWritable::do_write( |
||||
|output, file_path| { |
||||
let format = lookup_rdf_format(format, file_path.as_deref())?; |
||||
let mut writer = RdfSerializer::from_format(format).serialize_to_write(output); |
||||
for i in input.iter()? { |
||||
let i = i?; |
||||
if let Ok(triple) = i.extract::<PyRef<'_, PyTriple>>() { |
||||
writer.write_triple(&*triple) |
||||
} else { |
||||
let quad = i.extract::<PyRef<'_, PyQuad>>()?; |
||||
let quad = QuadRef::from(&*quad); |
||||
if !quad.graph_name.is_default_graph() && !format.supports_datasets() { |
||||
return Err(PyValueError::new_err( |
||||
"The {format} format does not support named graphs", |
||||
)); |
||||
} |
||||
writer.write_quad(quad) |
||||
}?; |
||||
} |
||||
Ok(writer.finish()?) |
||||
}, |
||||
output, |
||||
py, |
||||
) |
||||
} |
||||
|
||||
#[pyclass(name = "QuadReader", module = "pyoxigraph")] |
||||
pub struct PyQuadReader { |
||||
inner: FromReadQuadReader<PyReadable>, |
||||
file_path: Option<PathBuf>, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyQuadReader { |
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<PyQuad>> { |
||||
py.allow_threads(|| { |
||||
Ok(self |
||||
.inner |
||||
.next() |
||||
.transpose() |
||||
.map_err(|e| map_parse_error(e, self.file_path.clone()))? |
||||
.map(PyQuad::from)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/// RDF serialization formats.
|
||||
///
|
||||
/// The following formats are supported:
|
||||
///
|
||||
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// >>> RdfFormat.N3.media_type
|
||||
/// 'text/n3'
|
||||
#[pyclass(name = "RdfFormat", module = "pyoxigraph")] |
||||
#[derive(Clone)] |
||||
pub struct PyRdfFormat { |
||||
inner: RdfFormat, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyRdfFormat { |
||||
/// `N3 <https://w3c.github.io/N3/spec/>`_
|
||||
#[classattr] |
||||
const N3: Self = Self { |
||||
inner: RdfFormat::N3, |
||||
}; |
||||
/// `N-Quads <https://www.w3.org/TR/n-quads/>`_
|
||||
#[classattr] |
||||
const N_QUADS: Self = Self { |
||||
inner: RdfFormat::NQuads, |
||||
}; |
||||
/// `N-Triples <https://www.w3.org/TR/n-triples/>`_
|
||||
#[classattr] |
||||
const N_TRIPLES: Self = Self { |
||||
inner: RdfFormat::NTriples, |
||||
}; |
||||
/// `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_
|
||||
#[classattr] |
||||
const RDF_XML: Self = Self { |
||||
inner: RdfFormat::RdfXml, |
||||
}; |
||||
/// `TriG <https://www.w3.org/TR/trig/>`_
|
||||
#[classattr] |
||||
const TRIG: Self = Self { |
||||
inner: RdfFormat::TriG, |
||||
}; |
||||
/// `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
#[classattr] |
||||
const TURTLE: Self = Self { |
||||
inner: RdfFormat::Turtle, |
||||
}; |
||||
|
||||
/// :return: the format canonical IRI according to the `Unique URIs for file formats registry <https://www.w3.org/ns/formats/>`_.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.iri
|
||||
/// 'http://www.w3.org/ns/formats/N-Triples'
|
||||
#[getter] |
||||
fn iri(&self) -> &'static str { |
||||
self.inner.iri() |
||||
} |
||||
|
||||
/// :return: the format `IANA media type <https://tools.ietf.org/html/rfc2046>`_.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.media_type
|
||||
/// 'application/n-triples'
|
||||
#[getter] |
||||
fn media_type(&self) -> &'static str { |
||||
self.inner.media_type() |
||||
} |
||||
|
||||
/// :return: the format `IANA-registered <https://tools.ietf.org/html/rfc2046>`_ file extension.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.file_extension
|
||||
/// 'nt'
|
||||
#[getter] |
||||
pub fn file_extension(&self) -> &'static str { |
||||
self.inner.file_extension() |
||||
} |
||||
|
||||
/// :return: the format name.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.name
|
||||
/// 'N-Triples'
|
||||
#[getter] |
||||
pub const fn name(&self) -> &'static str { |
||||
self.inner.name() |
||||
} |
||||
|
||||
/// :return: if the formats supports `RDF datasets <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_ and not only `RDF graphs <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-graph>`_.
|
||||
/// :rtype: bool
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.supports_datasets
|
||||
/// False
|
||||
/// >>> RdfFormat.N_QUADS.supports_datasets
|
||||
/// True
|
||||
#[getter] |
||||
pub fn supports_datasets(&self) -> bool { |
||||
self.inner.supports_datasets() |
||||
} |
||||
|
||||
/// :return: if the formats supports `RDF-star quoted triples <https://w3c.github.io/rdf-star/cg-spec/2021-12-17.html#dfn-quoted>`_.
|
||||
/// :rtype: bool
|
||||
///
|
||||
/// >>> RdfFormat.N_TRIPLES.supports_rdf_star
|
||||
/// True
|
||||
/// >>> RdfFormat.RDF_XML.supports_rdf_star
|
||||
/// False
|
||||
#[getter] |
||||
pub const fn supports_rdf_star(&self) -> bool { |
||||
self.inner.supports_rdf_star() |
||||
} |
||||
|
||||
/// Looks for a known format from a media type.
|
||||
///
|
||||
/// It supports some media type aliases.
|
||||
/// For example, "application/xml" is going to return RDF/XML even if it is not its canonical media type.
|
||||
///
|
||||
/// :param media_type: the media type.
|
||||
/// :type media_type: str
|
||||
/// :return: :py:class:`RdfFormat` if the media type is known or :py:const:`None` if not.
|
||||
/// :rtype: RdfFormat or None
|
||||
///
|
||||
/// >>> RdfFormat.from_media_type("text/turtle; charset=utf-8")
|
||||
/// <RdfFormat Turtle>
|
||||
#[staticmethod] |
||||
pub fn from_media_type(media_type: &str) -> Option<Self> { |
||||
Some(Self { |
||||
inner: RdfFormat::from_media_type(media_type)?, |
||||
}) |
||||
} |
||||
|
||||
/// Looks for a known format from an extension.
|
||||
///
|
||||
/// It supports some aliases.
|
||||
///
|
||||
/// :param extension: the extension.
|
||||
/// :type extension: str
|
||||
/// :return: :py:class:`RdfFormat` if the extension is known or :py:const:`None` if not.
|
||||
/// :rtype: RdfFormat or None
|
||||
///
|
||||
/// >>> RdfFormat.from_extension("nt")
|
||||
/// <RdfFormat N-Triples>
|
||||
#[staticmethod] |
||||
pub fn from_extension(extension: &str) -> Option<Self> { |
||||
Some(Self { |
||||
inner: RdfFormat::from_extension(extension)?, |
||||
}) |
||||
} |
||||
|
||||
fn __str__(&self) -> &'static str { |
||||
self.inner.name() |
||||
} |
||||
|
||||
fn __repr__(&self) -> String { |
||||
format!("<RdfFormat {}>", self.inner.name()) |
||||
} |
||||
|
||||
fn __hash__(&self) -> u64 { |
||||
hash(&self.inner) |
||||
} |
||||
|
||||
fn __eq__(&self, other: &Self) -> bool { |
||||
self.inner == other.inner |
||||
} |
||||
|
||||
fn __ne__(&self, other: &Self) -> bool { |
||||
self.inner != other.inner |
||||
} |
||||
|
||||
/// :rtype: RdfFormat
|
||||
fn __copy__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
/// :type memo: typing.Any
|
||||
/// :rtype: RdfFormat
|
||||
#[allow(unused_variables)] |
||||
fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ Bound<'_, PyAny>) -> PyRef<'a, Self> { |
||||
slf |
||||
} |
||||
} |
||||
|
||||
pub enum PyReadable { |
||||
Bytes(Cursor<Vec<u8>>), |
||||
Io(PyIo), |
||||
File(File), |
||||
} |
||||
|
||||
impl PyReadable { |
||||
pub fn from_args( |
||||
path: &Option<PathBuf>, |
||||
input: Option<PyReadableInput>, |
||||
py: Python<'_>, |
||||
) -> PyResult<Self> { |
||||
match (path, input) { |
||||
(Some(_), Some(_)) => Err(PyValueError::new_err( |
||||
"input and file_path can't be both set at the same time", |
||||
)), |
||||
(Some(path), None) => Ok(Self::File(py.allow_threads(|| File::open(path))?)), |
||||
(None, Some(input)) => Ok(input.into()), |
||||
(None, None) => Err(PyValueError::new_err( |
||||
"Either input or file_path must be set", |
||||
)), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Read for PyReadable { |
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
||||
match self { |
||||
Self::Bytes(bytes) => bytes.read(buf), |
||||
Self::Io(io) => io.read(buf), |
||||
Self::File(file) => file.read(buf), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(FromPyObject)] |
||||
pub enum PyReadableInput { |
||||
String(String), |
||||
Bytes(Vec<u8>), |
||||
Io(PyObject), |
||||
} |
||||
|
||||
impl From<PyReadableInput> for PyReadable { |
||||
fn from(input: PyReadableInput) -> Self { |
||||
match input { |
||||
PyReadableInput::String(string) => Self::Bytes(Cursor::new(string.into_bytes())), |
||||
PyReadableInput::Bytes(bytes) => Self::Bytes(Cursor::new(bytes)), |
||||
PyReadableInput::Io(io) => Self::Io(PyIo(io)), |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub enum PyWritable { |
||||
Bytes(Vec<u8>), |
||||
Io(PyIo), |
||||
File(File), |
||||
} |
||||
|
||||
impl PyWritable { |
||||
pub fn do_write( |
||||
write: impl FnOnce(BufWriter<Self>, Option<PathBuf>) -> PyResult<BufWriter<Self>>, |
||||
output: Option<PyWritableOutput>, |
||||
py: Python<'_>, |
||||
) -> PyResult<Option<Bound<'_, PyBytes>>> { |
||||
let (output, file_path) = match output { |
||||
Some(PyWritableOutput::Path(file_path)) => ( |
||||
Self::File(py.allow_threads(|| File::create(&file_path))?), |
||||
Some(file_path), |
||||
), |
||||
Some(PyWritableOutput::Io(object)) => (Self::Io(PyIo(object)), None), |
||||
None => (Self::Bytes(Vec::new()), None), |
||||
}; |
||||
let writer = write(BufWriter::new(output), file_path)?; |
||||
py.allow_threads(|| writer.into_inner())?.close(py) |
||||
} |
||||
|
||||
fn close(self, py: Python<'_>) -> PyResult<Option<Bound<'_, PyBytes>>> { |
||||
match self { |
||||
Self::Bytes(bytes) => Ok(Some(PyBytes::new_bound(py, &bytes))), |
||||
Self::File(mut file) => { |
||||
py.allow_threads(|| { |
||||
file.flush()?; |
||||
file.sync_all() |
||||
})?; |
||||
Ok(None) |
||||
} |
||||
Self::Io(mut io) => { |
||||
py.allow_threads(|| io.flush())?; |
||||
Ok(None) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Write for PyWritable { |
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
||||
match self { |
||||
Self::Bytes(bytes) => bytes.write(buf), |
||||
Self::Io(io) => io.write(buf), |
||||
Self::File(file) => file.write(buf), |
||||
} |
||||
} |
||||
|
||||
fn flush(&mut self) -> io::Result<()> { |
||||
match self { |
||||
Self::Bytes(_) => Ok(()), |
||||
Self::Io(io) => io.flush(), |
||||
Self::File(file) => file.flush(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(FromPyObject)] |
||||
pub enum PyWritableOutput { |
||||
Path(PathBuf), |
||||
Io(PyObject), |
||||
} |
||||
|
||||
pub struct PyIo(PyObject); |
||||
|
||||
impl Read for PyIo { |
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
||||
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 |
||||
.bind(py) |
||||
.call_method1(intern!(py, "read"), (to_read,))?; |
||||
Ok(if let Ok(bytes) = read.extract::<&[u8]>() { |
||||
buf[..bytes.len()].copy_from_slice(bytes); |
||||
bytes.len() |
||||
} else { |
||||
// TODO: Python 3.10+ use directly .extract<&str>
|
||||
let string = read.extract::<Bound<'_, PyString>>()?; |
||||
let str = string.to_cow()?; |
||||
buf[..str.len()].copy_from_slice(str.as_bytes()); |
||||
str.len() |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl Write for PyIo { |
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
||||
Python::with_gil(|py| { |
||||
Ok(self |
||||
.0 |
||||
.bind(py) |
||||
.call_method1(intern!(py, "write"), (PyBytes::new_bound(py, buf),))? |
||||
.extract::<usize>()?) |
||||
}) |
||||
} |
||||
|
||||
fn flush(&mut self) -> io::Result<()> { |
||||
Python::with_gil(|py| { |
||||
self.0.bind(py).call_method0(intern!(py, "flush"))?; |
||||
Ok(()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
pub fn lookup_rdf_format( |
||||
format: Option<PyRdfFormatInput>, |
||||
path: Option<&Path>, |
||||
) -> PyResult<RdfFormat> { |
||||
if let Some(format) = format { |
||||
return match format { |
||||
PyRdfFormatInput::Object(format) => Ok(format.inner), |
||||
PyRdfFormatInput::MediaType(media_type) => { |
||||
deprecation_warning("Using string to specify a RDF format is deprecated, please use a RdfFormat object instead.")?; |
||||
RdfFormat::from_media_type(&media_type).ok_or_else(|| { |
||||
PyValueError::new_err(format!( |
||||
"The media type {media_type} is not supported by pyoxigraph" |
||||
)) |
||||
}) |
||||
} |
||||
}; |
||||
} |
||||
let Some(path) = path else { |
||||
return Err(PyValueError::new_err( |
||||
"The format parameter is required when a file path is not given", |
||||
)); |
||||
}; |
||||
let Some(ext) = path.extension().and_then(OsStr::to_str) else { |
||||
return Err(PyValueError::new_err(format!( |
||||
"The file name {} has no extension to guess a file format from", |
||||
path.display() |
||||
))); |
||||
}; |
||||
RdfFormat::from_extension(ext) |
||||
.ok_or_else(|| PyValueError::new_err(format!("Not supported RDF format extension: {ext}"))) |
||||
} |
||||
|
||||
#[derive(FromPyObject)] |
||||
pub enum PyRdfFormatInput { |
||||
Object(PyRdfFormat), |
||||
MediaType(String), |
||||
} |
||||
|
||||
pub fn map_parse_error(error: RdfParseError, file_path: Option<PathBuf>) -> PyErr { |
||||
match error { |
||||
RdfParseError::Syntax(error) => { |
||||
// Python 3.9 does not support end line and end column
|
||||
if python_version() >= (3, 10) { |
||||
let params = if let Some(location) = error.location() { |
||||
( |
||||
file_path, |
||||
Some(location.start.line + 1), |
||||
Some(location.start.column + 1), |
||||
None::<Vec<u8>>, |
||||
Some(location.end.line + 1), |
||||
Some(location.end.column + 1), |
||||
) |
||||
} else { |
||||
(None, None, None, None, None, None) |
||||
}; |
||||
PySyntaxError::new_err((error.to_string(), params)) |
||||
} else { |
||||
let params = if let Some(location) = error.location() { |
||||
( |
||||
file_path, |
||||
Some(location.start.line + 1), |
||||
Some(location.start.column + 1), |
||||
None::<Vec<u8>>, |
||||
) |
||||
} else { |
||||
(None, None, None, None) |
||||
}; |
||||
PySyntaxError::new_err((error.to_string(), params)) |
||||
} |
||||
} |
||||
RdfParseError::Io(error) => error.into(), |
||||
} |
||||
} |
||||
|
||||
/// 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<T>(_py: Python<'_>, f: impl FnOnce() -> T) -> T { |
||||
struct RestoreGuard { |
||||
tstate: *mut pyo3::ffi::PyThreadState, |
||||
} |
||||
|
||||
impl Drop for RestoreGuard { |
||||
fn drop(&mut self) { |
||||
// SAFETY: not cloned so called once
|
||||
unsafe { |
||||
pyo3::ffi::PyEval_RestoreThread(self.tstate); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// SAFETY: we have the restore part in Drop to make sure it's properly executed
|
||||
let tstate = unsafe { pyo3::ffi::PyEval_SaveThread() }; |
||||
let _guard = RestoreGuard { tstate }; |
||||
f() |
||||
} |
||||
|
||||
pub fn python_version() -> (u8, u8) { |
||||
static VERSION: OnceLock<(u8, u8)> = OnceLock::new(); |
||||
*VERSION.get_or_init(|| { |
||||
Python::with_gil(|py| { |
||||
let v = py.version_info(); |
||||
(v.major, v.minor) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
pub fn deprecation_warning(message: &str) -> PyResult<()> { |
||||
Python::with_gil(|py| { |
||||
PyErr::warn_bound(py, &py.get_type_bound::<PyDeprecationWarning>(), message, 0) |
||||
}) |
||||
} |
@ -1,47 +0,0 @@ |
||||
#![allow(
|
||||
clippy::unused_self, |
||||
clippy::trivially_copy_pass_by_ref, |
||||
unused_qualifications |
||||
)] |
||||
|
||||
mod dataset; |
||||
mod io; |
||||
mod model; |
||||
mod sparql; |
||||
mod store; |
||||
|
||||
use crate::dataset::*; |
||||
use crate::io::*; |
||||
use crate::model::*; |
||||
use crate::sparql::*; |
||||
use crate::store::*; |
||||
use pyo3::prelude::*; |
||||
|
||||
/// Oxigraph Python bindings
|
||||
#[pymodule] |
||||
fn pyoxigraph(_py: Python<'_>, module: &Bound<'_, 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::<PyNamedNode>()?; |
||||
module.add_class::<PyBlankNode>()?; |
||||
module.add_class::<PyLiteral>()?; |
||||
module.add_class::<PyDefaultGraph>()?; |
||||
module.add_class::<PyTriple>()?; |
||||
module.add_class::<PyQuad>()?; |
||||
module.add_class::<PyDataset>()?; |
||||
module.add_class::<PyStore>()?; |
||||
module.add_class::<PyVariable>()?; |
||||
module.add_class::<PyQuerySolutions>()?; |
||||
module.add_class::<PyQuerySolution>()?; |
||||
module.add_class::<PyQueryBoolean>()?; |
||||
module.add_class::<PyQueryTriples>()?; |
||||
module.add_class::<PyRdfFormat>()?; |
||||
module.add_class::<PyQueryResultsFormat>()?; |
||||
module.add_class::<PyCanonicalizationAlgorithm>()?; |
||||
module.add_wrapped(wrap_pyfunction!(parse))?; |
||||
module.add_wrapped(wrap_pyfunction!(parse_query_results))?; |
||||
module.add_wrapped(wrap_pyfunction!(serialize))?; |
||||
Ok(()) |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,740 +0,0 @@ |
||||
use crate::io::*; |
||||
use crate::model::*; |
||||
use crate::store::map_storage_error; |
||||
use oxigraph::io::RdfSerializer; |
||||
use oxigraph::model::Term; |
||||
use oxigraph::sparql::results::{ |
||||
FromReadQueryResultsReader, FromReadSolutionsReader, QueryResultsFormat, |
||||
QueryResultsParseError, QueryResultsParser, QueryResultsSerializer, |
||||
}; |
||||
use oxigraph::sparql::{ |
||||
EvaluationError, Query, QueryResults, QuerySolution, QuerySolutionIter, QueryTripleIter, |
||||
Variable, |
||||
}; |
||||
use pyo3::basic::CompareOp; |
||||
use pyo3::exceptions::{PyRuntimeError, PySyntaxError, PyValueError}; |
||||
use pyo3::prelude::*; |
||||
use pyo3::types::{PyBytes, PyString}; |
||||
use std::ffi::OsStr; |
||||
use std::io; |
||||
use std::path::{Path, PathBuf}; |
||||
use std::vec::IntoIter; |
||||
|
||||
pub fn parse_query( |
||||
query: &str, |
||||
base_iri: Option<&str>, |
||||
use_default_graph_as_union: bool, |
||||
default_graph: Option<&Bound<'_, PyAny>>, |
||||
named_graphs: Option<&Bound<'_, PyAny>>, |
||||
py: Python<'_>, |
||||
) -> PyResult<Query> { |
||||
let mut query = allow_threads_unsafe(py, || 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::<PyGraphName>()?.into())) |
||||
.collect::<PyResult<_>>()?, |
||||
) |
||||
} else if let Ok(default_graph) = default_graph.extract::<PyGraphName>() { |
||||
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::<PyNamedOrBlankNode>()?.into())) |
||||
.collect::<PyResult<_>>()?, |
||||
) |
||||
} |
||||
|
||||
Ok(query) |
||||
} |
||||
|
||||
pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyObject { |
||||
match results { |
||||
QueryResults::Solutions(inner) => PyQuerySolutions { |
||||
inner: PyQuerySolutionsVariant::Query(inner), |
||||
} |
||||
.into_py(py), |
||||
QueryResults::Graph(inner) => PyQueryTriples { inner }.into_py(py), |
||||
QueryResults::Boolean(inner) => PyQueryBoolean { inner }.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')]
|
||||
/// <NamedNode value=http://example.com>
|
||||
/// >>> solution['s']
|
||||
/// <NamedNode value=http://example.com>
|
||||
/// >>> solution[0]
|
||||
/// <NamedNode value=http://example.com>
|
||||
/// >>> s, p, o = solution
|
||||
/// >>> s
|
||||
/// <NamedNode value=http://example.com>
|
||||
#[pyclass(frozen, name = "QuerySolution", module = "pyoxigraph")] |
||||
pub struct PyQuerySolution { |
||||
inner: QuerySolution, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyQuerySolution { |
||||
fn __repr__(&self) -> String { |
||||
let mut buffer = String::new(); |
||||
buffer.push_str("<QuerySolution"); |
||||
for (k, v) in self.inner.iter() { |
||||
buffer.push(' '); |
||||
buffer.push_str(k.as_str()); |
||||
buffer.push('='); |
||||
term_repr(v.as_ref(), &mut buffer) |
||||
} |
||||
buffer.push('>'); |
||||
buffer |
||||
} |
||||
|
||||
fn __eq__(&self, other: &Self) -> bool { |
||||
self.inner == other.inner |
||||
} |
||||
|
||||
fn __ne__(&self, other: &Self) -> bool { |
||||
self.inner != other.inner |
||||
} |
||||
|
||||
fn __len__(&self) -> usize { |
||||
self.inner.len() |
||||
} |
||||
|
||||
fn __getitem__(&self, key: PySolutionKey<'_>) -> PyResult<Option<PyTerm>> { |
||||
Ok(match key { |
||||
PySolutionKey::Usize(key) => self.inner.get(key), |
||||
PySolutionKey::Str(key) => self.inner.get(key.to_cow()?.as_ref()), |
||||
PySolutionKey::Variable(key) => self.inner.get(<&Variable>::from(&*key)), |
||||
} |
||||
.map(|term| PyTerm::from(term.clone()))) |
||||
} |
||||
|
||||
#[allow(clippy::unnecessary_to_owned)] |
||||
fn __iter__(&self) -> SolutionValueIter { |
||||
SolutionValueIter { |
||||
inner: self.inner.values().to_vec().into_iter(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(FromPyObject)] |
||||
pub enum PySolutionKey<'a> { |
||||
Usize(usize), |
||||
Str(Bound<'a, PyString>), // TODO: Python 3.10+: use &str
|
||||
Variable(PyRef<'a, PyVariable>), |
||||
} |
||||
|
||||
#[pyclass(module = "pyoxigraph")] |
||||
pub struct SolutionValueIter { |
||||
inner: IntoIter<Option<Term>>, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl SolutionValueIter { |
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self) -> Option<Option<PyTerm>> { |
||||
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 }'))
|
||||
/// [<QuerySolution s=<NamedNode value=http://example.com>>]
|
||||
#[pyclass(unsendable, name = "QuerySolutions", module = "pyoxigraph")] |
||||
pub struct PyQuerySolutions { |
||||
inner: PyQuerySolutionsVariant, |
||||
} |
||||
enum PyQuerySolutionsVariant { |
||||
Query(QuerySolutionIter), |
||||
Reader { |
||||
iter: FromReadSolutionsReader<PyReadable>, |
||||
file_path: Option<PathBuf>, |
||||
}, |
||||
} |
||||
|
||||
#[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
|
||||
/// [<Variable value=s>]
|
||||
#[getter] |
||||
fn variables(&self) -> Vec<PyVariable> { |
||||
match &self.inner { |
||||
PyQuerySolutionsVariant::Query(inner) => { |
||||
inner.variables().iter().map(|v| v.clone().into()).collect() |
||||
} |
||||
PyQuerySolutionsVariant::Reader { iter, .. } => { |
||||
iter.variables().iter().map(|v| v.clone().into()).collect() |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Writes the query results into a file.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
|
||||
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
|
||||
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
|
||||
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
|
||||
///
|
||||
/// :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')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
|
||||
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
|
||||
/// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: QueryResultsFormat or None, optional
|
||||
/// :rtype: bytes or None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises OSError: if a system error happens while writing the file.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> results = store.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
||||
/// >>> results.serialize(format=QueryResultsFormat.JSON)
|
||||
/// b'{"head":{"vars":["s","p","o"]},"results":{"bindings":[{"s":{"type":"uri","value":"http://example.com"},"p":{"type":"uri","value":"http://example.com/p"},"o":{"type":"literal","value":"1"}}]}}'
|
||||
#[pyo3(signature = (output = None, format = None))] |
||||
fn serialize<'py>( |
||||
&mut self, |
||||
output: Option<PyWritableOutput>, |
||||
format: Option<PyQueryResultsFormatInput>, |
||||
py: Python<'py>, |
||||
) -> PyResult<Option<Bound<'py, PyBytes>>> { |
||||
PyWritable::do_write( |
||||
|output, file_path| { |
||||
let format = lookup_query_results_format(format, file_path.as_deref())?; |
||||
let mut writer = QueryResultsSerializer::from_format(format) |
||||
.serialize_solutions_to_write( |
||||
output, |
||||
match &self.inner { |
||||
PyQuerySolutionsVariant::Query(inner) => inner.variables().to_vec(), |
||||
PyQuerySolutionsVariant::Reader { iter, .. } => { |
||||
iter.variables().to_vec() |
||||
} |
||||
}, |
||||
)?; |
||||
match &mut self.inner { |
||||
PyQuerySolutionsVariant::Query(inner) => { |
||||
for solution in inner { |
||||
writer.write(&solution.map_err(map_evaluation_error)?)?; |
||||
} |
||||
} |
||||
PyQuerySolutionsVariant::Reader { iter, file_path } => { |
||||
for solution in iter { |
||||
writer.write(&solution.map_err(|e| { |
||||
map_query_results_parse_error(e, file_path.clone()) |
||||
})?)?; |
||||
} |
||||
} |
||||
} |
||||
|
||||
Ok(writer.finish()?) |
||||
}, |
||||
output, |
||||
py, |
||||
) |
||||
} |
||||
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<PyQuerySolution>> { |
||||
Ok(match &mut self.inner { |
||||
PyQuerySolutionsVariant::Query(inner) => allow_threads_unsafe(py, || { |
||||
inner.next().transpose().map_err(map_evaluation_error) |
||||
}), |
||||
PyQuerySolutionsVariant::Reader { iter, file_path } => iter |
||||
.next() |
||||
.transpose() |
||||
.map_err(|e| map_query_results_parse_error(e, file_path.clone())), |
||||
}? |
||||
.map(move |inner| PyQuerySolution { inner })) |
||||
} |
||||
} |
||||
|
||||
/// A boolean returned by a SPARQL ``ASK`` query.
|
||||
///
|
||||
/// It can be easily casted to a regular boolean using the :py:func:`bool` function.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> bool(store.query('ASK { ?s ?p ?o }'))
|
||||
/// True
|
||||
#[pyclass(unsendable, name = "QueryBoolean", module = "pyoxigraph")] |
||||
pub struct PyQueryBoolean { |
||||
inner: bool, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyQueryBoolean { |
||||
/// Writes the query results into a file.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
|
||||
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
|
||||
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
|
||||
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
|
||||
///
|
||||
/// :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')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
|
||||
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
|
||||
/// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: QueryResultsFormat or None, optional
|
||||
/// :rtype: bytes or None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises OSError: if a system error happens while writing the file.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> results = store.query("ASK { ?s ?p ?o }")
|
||||
/// >>> results.serialize(format=QueryResultsFormat.JSON)
|
||||
/// b'{"head":{},"boolean":true}'
|
||||
#[pyo3(signature = (output = None, format = None))] |
||||
fn serialize<'py>( |
||||
&mut self, |
||||
output: Option<PyWritableOutput>, |
||||
format: Option<PyQueryResultsFormatInput>, |
||||
py: Python<'py>, |
||||
) -> PyResult<Option<Bound<'py, PyBytes>>> { |
||||
PyWritable::do_write( |
||||
|output, file_path| { |
||||
let format = lookup_query_results_format(format, file_path.as_deref())?; |
||||
py.allow_threads(|| { |
||||
Ok(QueryResultsSerializer::from_format(format) |
||||
.serialize_boolean_to_write(output, self.inner)?) |
||||
}) |
||||
}, |
||||
output, |
||||
py, |
||||
) |
||||
} |
||||
|
||||
fn __bool__(&self) -> bool { |
||||
self.inner |
||||
} |
||||
|
||||
fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool { |
||||
op.matches(self.inner.cmp(&other.inner)) |
||||
} |
||||
|
||||
fn __hash__(&self) -> u64 { |
||||
self.inner.into() |
||||
} |
||||
|
||||
fn __repr__(&self) -> String { |
||||
format!("<QueryBoolean {}>", self.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 }'))
|
||||
/// [<Triple subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>>>]
|
||||
#[pyclass(unsendable, name = "QueryTriples", module = "pyoxigraph")] |
||||
pub struct PyQueryTriples { |
||||
inner: QueryTripleIter, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyQueryTriples { |
||||
/// Writes the query results into a file.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `canonical <https://www.w3.org/TR/n-triples/#canonical-ntriples>`_ `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :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')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
|
||||
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :rtype: bytes or None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises OSError: if a system error happens while writing the file.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }")
|
||||
/// >>> results.serialize(format=RdfFormat.N_TRIPLES)
|
||||
/// b'<http://example.com> <http://example.com/p> "1" .\n'
|
||||
#[pyo3(signature = (output = None, format = None))] |
||||
fn serialize<'py>( |
||||
&mut self, |
||||
output: Option<PyWritableOutput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
py: Python<'py>, |
||||
) -> PyResult<Option<Bound<'py, PyBytes>>> { |
||||
PyWritable::do_write( |
||||
|output, file_path| { |
||||
let format = lookup_rdf_format(format, file_path.as_deref())?; |
||||
let mut writer = RdfSerializer::from_format(format).serialize_to_write(output); |
||||
for triple in &mut self.inner { |
||||
writer.write_triple(&triple.map_err(map_evaluation_error)?)?; |
||||
} |
||||
Ok(writer.finish()?) |
||||
}, |
||||
output, |
||||
py, |
||||
) |
||||
} |
||||
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<PyTriple>> { |
||||
Ok(allow_threads_unsafe(py, || self.inner.next()) |
||||
.transpose() |
||||
.map_err(map_evaluation_error)? |
||||
.map(Into::into)) |
||||
} |
||||
} |
||||
|
||||
/// Parses SPARQL query results.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
|
||||
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
|
||||
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/json`` could also be used for `JSON <https://www.w3.org/TR/sparql11-results-json/>`_.
|
||||
///
|
||||
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
|
||||
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
|
||||
/// :param format: the format of the query results serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: QueryResultsFormat or None, optional
|
||||
/// :param path: The file path to read from. Replaces the ``input`` parameter.
|
||||
/// :type path: str or os.PathLike[str] or None, optional
|
||||
/// :return: an iterator of :py:class:`QuerySolution` or a :py:class:`bool`.
|
||||
/// :rtype: QuerySolutions or QueryBoolean
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises SyntaxError: if the provided data is invalid.
|
||||
/// :raises OSError: if a system error happens while reading the file.
|
||||
///
|
||||
/// >>> list(parse_query_results('?s\t?p\t?o\n<http://example.com/s>\t<http://example.com/s>\t1\n', QueryResultsFormat.TSV))
|
||||
/// [<QuerySolution s=<NamedNode value=http://example.com/s> p=<NamedNode value=http://example.com/s> o=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#integer>>>]
|
||||
///
|
||||
/// >>> parse_query_results('{"head":{},"boolean":true}', QueryResultsFormat.JSON)
|
||||
/// <QueryBoolean true>
|
||||
#[pyfunction] |
||||
#[pyo3(signature = (input = None, format = None, *, path = None))] |
||||
pub fn parse_query_results( |
||||
input: Option<PyReadableInput>, |
||||
format: Option<PyQueryResultsFormatInput>, |
||||
path: Option<PathBuf>, |
||||
py: Python<'_>, |
||||
) -> PyResult<PyObject> { |
||||
let input = PyReadable::from_args(&path, input, py)?; |
||||
let format = lookup_query_results_format(format, path.as_deref())?; |
||||
let results = QueryResultsParser::from_format(format) |
||||
.parse_read(input) |
||||
.map_err(|e| map_query_results_parse_error(e, path.clone()))?; |
||||
Ok(match results { |
||||
FromReadQueryResultsReader::Solutions(iter) => PyQuerySolutions { |
||||
inner: PyQuerySolutionsVariant::Reader { |
||||
iter, |
||||
file_path: path, |
||||
}, |
||||
} |
||||
.into_py(py), |
||||
FromReadQueryResultsReader::Boolean(inner) => PyQueryBoolean { inner }.into_py(py), |
||||
}) |
||||
} |
||||
|
||||
/// `SPARQL query <https://www.w3.org/TR/sparql11-query/>`_ results serialization formats.
|
||||
///
|
||||
/// The following formats are supported:
|
||||
///
|
||||
/// * `XML <https://www.w3.org/TR/rdf-sparql-XMLres/>`_ (:py:attr:`QueryResultsFormat.XML`)
|
||||
/// * `JSON <https://www.w3.org/TR/sparql11-results-json/>`_ (:py:attr:`QueryResultsFormat.JSON`)
|
||||
/// * `CSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.CSV`)
|
||||
/// * `TSV <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_ (:py:attr:`QueryResultsFormat.TSV`)
|
||||
#[pyclass(name = "QueryResultsFormat", module = "pyoxigraph")] |
||||
#[derive(Clone)] |
||||
pub struct PyQueryResultsFormat { |
||||
inner: QueryResultsFormat, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyQueryResultsFormat { |
||||
/// `SPARQL Query Results CSV Format <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_
|
||||
#[classattr] |
||||
const CSV: Self = Self { |
||||
inner: QueryResultsFormat::Csv, |
||||
}; |
||||
/// `SPARQL Query Results JSON Format <https://www.w3.org/TR/sparql11-results-json/>`_
|
||||
#[classattr] |
||||
const JSON: Self = Self { |
||||
inner: QueryResultsFormat::Json, |
||||
}; |
||||
/// `SPARQL Query Results TSV Format <https://www.w3.org/TR/sparql11-results-csv-tsv/>`_
|
||||
#[classattr] |
||||
const TSV: Self = Self { |
||||
inner: QueryResultsFormat::Tsv, |
||||
}; |
||||
/// `SPARQL Query Results XML Format <https://www.w3.org/TR/rdf-sparql-XMLres/>`_
|
||||
#[classattr] |
||||
const XML: Self = Self { |
||||
inner: QueryResultsFormat::Xml, |
||||
}; |
||||
|
||||
/// :return: the format canonical IRI according to the `Unique URIs for file formats registry <https://www.w3.org/ns/formats/>`_.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> QueryResultsFormat.JSON.iri
|
||||
/// 'http://www.w3.org/ns/formats/SPARQL_Results_JSON'
|
||||
#[getter] |
||||
fn iri(&self) -> &'static str { |
||||
self.inner.iri() |
||||
} |
||||
|
||||
/// :return: the format `IANA media type <https://tools.ietf.org/html/rfc2046>`_.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> QueryResultsFormat.JSON.media_type
|
||||
/// 'application/sparql-results+json'
|
||||
#[getter] |
||||
fn media_type(&self) -> &'static str { |
||||
self.inner.media_type() |
||||
} |
||||
|
||||
/// :return: the format `IANA-registered <https://tools.ietf.org/html/rfc2046>`_ file extension.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> QueryResultsFormat.JSON.file_extension
|
||||
/// 'srj'
|
||||
#[getter] |
||||
fn file_extension(&self) -> &'static str { |
||||
self.inner.file_extension() |
||||
} |
||||
|
||||
/// :return: the format name.
|
||||
/// :rtype: str
|
||||
///
|
||||
/// >>> QueryResultsFormat.JSON.name
|
||||
/// 'SPARQL Results in JSON'
|
||||
#[getter] |
||||
pub const fn name(&self) -> &'static str { |
||||
self.inner.name() |
||||
} |
||||
|
||||
/// Looks for a known format from a media type.
|
||||
///
|
||||
/// It supports some media type aliases.
|
||||
/// For example, "application/xml" is going to return :py:const:`QueryResultsFormat.XML` even if it is not its canonical media type.
|
||||
///
|
||||
/// :param media_type: the media type.
|
||||
/// :type media_type: str
|
||||
/// :return: :py:class:`QueryResultsFormat` if the media type is known or :py:const:`None` if not.
|
||||
/// :rtype: QueryResultsFormat or None
|
||||
///
|
||||
/// >>> QueryResultsFormat.from_media_type("application/sparql-results+json; charset=utf-8")
|
||||
/// <QueryResultsFormat SPARQL Results in JSON>
|
||||
#[staticmethod] |
||||
fn from_media_type(media_type: &str) -> Option<Self> { |
||||
Some(Self { |
||||
inner: QueryResultsFormat::from_media_type(media_type)?, |
||||
}) |
||||
} |
||||
|
||||
/// Looks for a known format from an extension.
|
||||
///
|
||||
/// It supports some aliases.
|
||||
///
|
||||
/// :param extension: the extension.
|
||||
/// :type extension: str
|
||||
/// :return: :py:class:`QueryResultsFormat` if the extension is known or :py:const:`None` if not.
|
||||
/// :rtype: QueryResultsFormat or None
|
||||
///
|
||||
/// >>> QueryResultsFormat.from_extension("json")
|
||||
/// <QueryResultsFormat SPARQL Results in JSON>
|
||||
#[staticmethod] |
||||
fn from_extension(extension: &str) -> Option<Self> { |
||||
Some(Self { |
||||
inner: QueryResultsFormat::from_extension(extension)?, |
||||
}) |
||||
} |
||||
|
||||
fn __str__(&self) -> &'static str { |
||||
self.inner.name() |
||||
} |
||||
|
||||
fn __repr__(&self) -> String { |
||||
format!("<QueryResultsFormat {}>", self.inner.name()) |
||||
} |
||||
|
||||
fn __hash__(&self) -> u64 { |
||||
hash(&self.inner) |
||||
} |
||||
|
||||
fn __eq__(&self, other: &Self) -> bool { |
||||
self.inner == other.inner |
||||
} |
||||
|
||||
fn __ne__(&self, other: &Self) -> bool { |
||||
self.inner != other.inner |
||||
} |
||||
|
||||
/// :rtype: QueryResultsFormat
|
||||
fn __copy__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
/// :type memo: typing.Any
|
||||
/// :rtype: QueryResultsFormat
|
||||
#[allow(unused_variables)] |
||||
fn __deepcopy__<'a>(slf: PyRef<'a, Self>, memo: &'_ Bound<'_, PyAny>) -> PyRef<'a, Self> { |
||||
slf |
||||
} |
||||
} |
||||
|
||||
pub fn lookup_query_results_format( |
||||
format: Option<PyQueryResultsFormatInput>, |
||||
path: Option<&Path>, |
||||
) -> PyResult<QueryResultsFormat> { |
||||
if let Some(format) = format { |
||||
return match format { |
||||
PyQueryResultsFormatInput::Object(format) => Ok(format.inner), |
||||
PyQueryResultsFormatInput::MediaType(media_type) => { |
||||
deprecation_warning("Using a string to specify a query results format is deprecated, please use a QueryResultsFormat object instead.")?; |
||||
QueryResultsFormat::from_media_type(&media_type).ok_or_else(|| { |
||||
PyValueError::new_err(format!( |
||||
"The media type {media_type} is not supported by pyoxigraph" |
||||
)) |
||||
}) |
||||
} |
||||
}; |
||||
} |
||||
let Some(path) = path else { |
||||
return Err(PyValueError::new_err( |
||||
"The format parameter is required when a file path is not given", |
||||
)); |
||||
}; |
||||
let Some(ext) = path.extension().and_then(OsStr::to_str) else { |
||||
return Err(PyValueError::new_err(format!( |
||||
"The file name {} has no extension to guess a file format from", |
||||
path.display() |
||||
))); |
||||
}; |
||||
QueryResultsFormat::from_extension(ext) |
||||
.ok_or_else(|| PyValueError::new_err(format!("Not supported RDF format extension: {ext}"))) |
||||
} |
||||
|
||||
#[derive(FromPyObject)] |
||||
pub enum PyQueryResultsFormatInput { |
||||
Object(PyQueryResultsFormat), |
||||
MediaType(String), |
||||
} |
||||
|
||||
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::GraphParsing(error) => map_parse_error(error, None), |
||||
EvaluationError::ResultsParsing(error) => map_query_results_parse_error(error, None), |
||||
EvaluationError::ResultsSerialization(error) => error.into(), |
||||
EvaluationError::Service(error) => match error.downcast::<io::Error>() { |
||||
Ok(error) => (*error).into(), |
||||
Err(error) => PyRuntimeError::new_err(error.to_string()), |
||||
}, |
||||
_ => PyRuntimeError::new_err(error.to_string()), |
||||
} |
||||
} |
||||
|
||||
pub fn map_query_results_parse_error( |
||||
error: QueryResultsParseError, |
||||
file_path: Option<PathBuf>, |
||||
) -> PyErr { |
||||
match error { |
||||
QueryResultsParseError::Syntax(error) => { |
||||
// Python 3.9 does not support end line and end column
|
||||
if python_version() >= (3, 10) { |
||||
let params = if let Some(location) = error.location() { |
||||
( |
||||
file_path, |
||||
Some(location.start.line + 1), |
||||
Some(location.start.column + 1), |
||||
None::<Vec<u8>>, |
||||
Some(location.end.line + 1), |
||||
Some(location.end.column + 1), |
||||
) |
||||
} else { |
||||
(None, None, None, None, None, None) |
||||
}; |
||||
PySyntaxError::new_err((error.to_string(), params)) |
||||
} else { |
||||
let params = if let Some(location) = error.location() { |
||||
( |
||||
file_path, |
||||
Some(location.start.line + 1), |
||||
Some(location.start.column + 1), |
||||
None::<Vec<u8>>, |
||||
) |
||||
} else { |
||||
(None, None, None, None) |
||||
}; |
||||
PySyntaxError::new_err((error.to_string(), params)) |
||||
} |
||||
} |
||||
QueryResultsParseError::Io(error) => error.into(), |
||||
} |
||||
} |
@ -1,865 +0,0 @@ |
||||
#![allow(clippy::needless_option_as_deref)] |
||||
|
||||
use crate::io::{ |
||||
allow_threads_unsafe, lookup_rdf_format, map_parse_error, PyRdfFormatInput, PyReadable, |
||||
PyReadableInput, PyWritable, PyWritableOutput, |
||||
}; |
||||
use crate::model::*; |
||||
use crate::sparql::*; |
||||
use oxigraph::io::RdfParser; |
||||
use oxigraph::model::GraphNameRef; |
||||
use oxigraph::sparql::Update; |
||||
use oxigraph::store::{self, LoaderError, SerializerError, StorageError, Store}; |
||||
use pyo3::exceptions::{PyRuntimeError, PyValueError}; |
||||
use pyo3::prelude::*; |
||||
use pyo3::types::PyBytes; |
||||
use std::path::PathBuf; |
||||
|
||||
/// RDF store.
|
||||
///
|
||||
/// It encodes a `RDF dataset <https://www.w3.org/TR/rdf11-concepts/#dfn-rdf-dataset>`_ and allows to query it using SPARQL.
|
||||
/// It is based on the `RocksDB <https://rocksdb.org/>`_ 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 os.PathLike[str] or None, optional
|
||||
/// :raises OSError: if the target directory contains invalid data or could not be accessed.
|
||||
///
|
||||
/// The :py:class:`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)
|
||||
/// '<http://example.com> <http://example.com/p> "1" <http://example.com/g> .\n'
|
||||
#[pyclass(frozen, name = "Store", module = "pyoxigraph")] |
||||
#[derive(Clone)] |
||||
pub struct PyStore { |
||||
inner: Store, |
||||
} |
||||
|
||||
#[pymethods] |
||||
impl PyStore { |
||||
#[cfg(not(target_family = "wasm"))] |
||||
#[new] |
||||
#[pyo3(signature = (path = None))] |
||||
fn new(path: Option<PathBuf>, py: Python<'_>) -> PyResult<Self> { |
||||
py.allow_threads(|| { |
||||
Ok(Self { |
||||
inner: if let Some(path) = path { |
||||
Store::open(path) |
||||
} else { |
||||
Store::new() |
||||
} |
||||
.map_err(map_storage_error)?, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
#[cfg(target_family = "wasm")] |
||||
#[new] |
||||
fn new(py: Python<'_>) -> PyResult<Self> { |
||||
py.allow_threads(|| { |
||||
Ok(Self { |
||||
inner: Store::new().map_err(map_storage_error)?, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
/// Opens a read-only store from disk.
|
||||
///
|
||||
/// Opening as read-only while having an other process writing the database is undefined behavior.
|
||||
/// :py:func:`Store.secondary` should be used in this case.
|
||||
///
|
||||
/// :param path: path to the primary read-write instance data.
|
||||
/// :type path: str
|
||||
/// :return: the opened store.
|
||||
/// :rtype: Store
|
||||
/// :raises OSError: if the target directory contains invalid data or could not be accessed.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
#[staticmethod] |
||||
fn read_only(path: &str, py: Python<'_>) -> PyResult<Self> { |
||||
py.allow_threads(|| { |
||||
Ok(Self { |
||||
inner: Store::open_read_only(path).map_err(map_storage_error)?, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
/// Opens a read-only clone of a running read-write store.
|
||||
///
|
||||
/// Changes done while this process is running will be replicated after a possible lag.
|
||||
///
|
||||
/// It should only be used if a primary instance opened with :py:func:`Store` is running at the same time.
|
||||
///
|
||||
/// If you want to simple read-only store use :py:func:`Store.read_only`.
|
||||
///
|
||||
/// :param primary_path: path to the primary read-write instance data.
|
||||
/// :type primary_path: str
|
||||
/// :param secondary_path: path to an other directory for the secondary instance cache. If not given a temporary directory will be used.
|
||||
/// :type secondary_path: str or None, optional
|
||||
/// :return: the opened store.
|
||||
/// :rtype: Store
|
||||
/// :raises OSError: if the target directories contain invalid data or could not be accessed.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
#[staticmethod] |
||||
#[pyo3(signature = (primary_path, secondary_path = None))] |
||||
fn secondary( |
||||
primary_path: &str, |
||||
secondary_path: Option<&str>, |
||||
py: Python<'_>, |
||||
) -> PyResult<Self> { |
||||
py.allow_threads(|| { |
||||
Ok(Self { |
||||
inner: if let Some(secondary_path) = secondary_path { |
||||
Store::open_persistent_secondary(primary_path, secondary_path) |
||||
} else { |
||||
Store::open_secondary(primary_path) |
||||
} |
||||
.map_err(map_storage_error)?, |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
/// Adds a quad to the store.
|
||||
///
|
||||
/// :param quad: the quad to add.
|
||||
/// :type quad: Quad
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the quad insertion.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
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: collections.abc.Iterable[Quad]
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the quad insertion.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.extend([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
fn extend(&self, quads: &Bound<'_, PyAny>, py: Python<'_>) -> PyResult<()> { |
||||
let quads = quads |
||||
.iter()? |
||||
.map(|q| q?.extract()) |
||||
.collect::<PyResult<Vec<PyQuad>>>()?; |
||||
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: collections.abc.Iterable[Quad]
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the quad insertion.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.bulk_extend([Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))])
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
fn bulk_extend(&self, quads: &Bound<'_, PyAny>) -> PyResult<()> { |
||||
self.inner |
||||
.bulk_loader() |
||||
.load_ok_quads::<PyErr, PythonOrStorageError>( |
||||
quads.iter()?.map(|q| q?.extract::<PyQuad>()), |
||||
)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Removes a quad from the store.
|
||||
///
|
||||
/// :param quad: the quad to remove.
|
||||
/// :type quad: Quad
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the quad removal.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> quad = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))
|
||||
/// >>> store.add(quad)
|
||||
/// >>> store.remove(quad)
|
||||
/// >>> list(store)
|
||||
/// []
|
||||
fn remove(&self, quad: &PyQuad, py: Python<'_>) -> PyResult<()> { |
||||
py.allow_threads(|| { |
||||
self.inner.remove(quad).map_err(map_storage_error)?; |
||||
Ok(()) |
||||
}) |
||||
} |
||||
|
||||
/// Looks for the quads matching a given pattern.
|
||||
///
|
||||
/// :param subject: the quad subject or :py:const:`None` to match everything.
|
||||
/// :type subject: NamedNode or BlankNode or Triple or None
|
||||
/// :param predicate: the quad predicate or :py:const:`None` to match everything.
|
||||
/// :type predicate: NamedNode or None
|
||||
/// :param object: the quad object or :py:const:`None` to match everything.
|
||||
/// :type object: NamedNode or BlankNode or Literal or Triple or None
|
||||
/// :param graph_name: the quad graph name. To match only the default graph, use :py:class:`DefaultGraph`. To match everything use :py:const:`None`.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph or None, optional
|
||||
/// :return: an iterator of the quads matching the pattern.
|
||||
/// :rtype: collections.abc.Iterator[Quad]
|
||||
/// :raises OSError: if an error happens during the quads lookup.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> list(store.quads_for_pattern(NamedNode('http://example.com'), None, None, None))
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
#[pyo3(signature = (subject, predicate, object, graph_name = None))] |
||||
fn quads_for_pattern( |
||||
&self, |
||||
subject: Option<PySubjectRef<'_>>, |
||||
predicate: Option<PyNamedNodeRef<'_>>, |
||||
object: Option<PyTermRef<'_>>, |
||||
graph_name: Option<PyGraphNameRef<'_>>, |
||||
) -> QuadIter { |
||||
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 <https://www.w3.org/TR/sparql11-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 QueryBoolean or QueryTriples
|
||||
/// :raises SyntaxError: if the provided query is invalid.
|
||||
/// :raises OSError: if an error happens while reading the store.
|
||||
///
|
||||
/// ``SELECT`` query:
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> [solution['s'] for solution in store.query('SELECT ?s WHERE { ?s ?p ?o }')]
|
||||
/// [<NamedNode value=http://example.com>]
|
||||
///
|
||||
/// ``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 }'))
|
||||
/// [<Triple subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>>>]
|
||||
///
|
||||
/// ``ASK`` query:
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> bool(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<&Bound<'_, PyAny>>, |
||||
named_graphs: Option<&Bound<'_, PyAny>>, |
||||
py: Python<'_>, |
||||
) -> PyResult<PyObject> { |
||||
let query = parse_query( |
||||
query, |
||||
base_iri, |
||||
use_default_graph_as_union, |
||||
default_graph, |
||||
named_graphs, |
||||
py, |
||||
)?; |
||||
let results = |
||||
allow_threads_unsafe(py, || self.inner.query(query)).map_err(map_evaluation_error)?; |
||||
Ok(query_results_to_python(py, results)) |
||||
} |
||||
|
||||
/// Executes a `SPARQL 1.1 update <https://www.w3.org/TR/sparql11-update/>`_.
|
||||
///
|
||||
/// Updates are applied in a transactional manner: either the full operation succeeds or nothing is written to the database.
|
||||
///
|
||||
/// :param update: the update to execute.
|
||||
/// :type update: str
|
||||
/// :param base_iri: the base IRI used to resolve the relative IRIs in the SPARQL update or :py:const:`None` if relative IRI resolution should not be done.
|
||||
/// :type base_iri: str or None, optional
|
||||
/// :rtype: None
|
||||
/// :raises SyntaxError: if the provided update is invalid.
|
||||
/// :raises OSError: if an error happens while reading the store.
|
||||
///
|
||||
/// ``INSERT DATA`` update:
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.update('INSERT DATA { <http://example.com> <http://example.com/p> "1" }')
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<DefaultGraph>>]
|
||||
///
|
||||
/// ``DELETE DATA`` update:
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> store.update('DELETE DATA { <http://example.com> <http://example.com/p> "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 { <http://example.com> ?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 <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
|
||||
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :param path: The file path to read from. Replaces the ``input`` parameter.
|
||||
/// :type path: str or os.PathLike[str] or None, optional
|
||||
/// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done.
|
||||
/// :type base_iri: str or None, optional
|
||||
/// :param to_graph: if it is a file composed of triples, the graph in which the triples should be stored. By default, the default graph is used.
|
||||
/// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional
|
||||
/// :rtype: None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises SyntaxError: if the provided data is invalid.
|
||||
/// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.load(input='<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))] |
||||
fn load( |
||||
&self, |
||||
input: Option<PyReadableInput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
path: Option<PathBuf>, |
||||
base_iri: Option<&str>, |
||||
to_graph: Option<PyGraphNameRef<'_>>, |
||||
py: Python<'_>, |
||||
) -> PyResult<()> { |
||||
let to_graph_name = to_graph.as_ref().map(GraphNameRef::from); |
||||
let input = PyReadable::from_args(&path, input, py)?; |
||||
let format = lookup_rdf_format(format, path.as_deref())?; |
||||
py.allow_threads(|| { |
||||
let mut parser = RdfParser::from_format(format); |
||||
if let Some(base_iri) = base_iri { |
||||
parser = parser |
||||
.with_base_iri(base_iri) |
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?; |
||||
} |
||||
if let Some(to_graph_name) = to_graph_name { |
||||
parser = parser.with_default_graph(to_graph_name); |
||||
} |
||||
self.inner |
||||
.load_from_read(parser, input) |
||||
.map_err(|e| map_loader_error(e, path)) |
||||
}) |
||||
} |
||||
|
||||
/// 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 <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :param input: The :py:class:`str`, :py:class:`bytes` or I/O object to read from. For example, it could be the file content as a string or a file reader opened in binary mode with ``open('my_file.ttl', 'rb')``.
|
||||
/// :type input: bytes or str or typing.IO[bytes] or typing.IO[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :param path: The file path to read from. Replaces the ``input`` parameter.
|
||||
/// :type path: str or os.PathLike[str] or None, optional
|
||||
/// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done.
|
||||
/// :type base_iri: str or None, optional
|
||||
/// :param to_graph: if it is a file composed of triples, the graph in which the triples should be stored. By default, the default graph is used.
|
||||
/// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional
|
||||
/// :rtype: None
|
||||
/// :raises ValueError: if the format is not supported.
|
||||
/// :raises SyntaxError: if the provided data is invalid.
|
||||
/// :raises OSError: if an error happens during a quad insertion or if a system error happens while reading the file.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.bulk_load(input=b'<foo> <p> "1" .', format=RdfFormat.TURTLE, base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
|
||||
/// >>> list(store)
|
||||
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
#[pyo3(signature = (input = None, format = None, *, path = None, base_iri = None, to_graph = None))] |
||||
fn bulk_load( |
||||
&self, |
||||
input: Option<PyReadableInput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
path: Option<PathBuf>, |
||||
base_iri: Option<&str>, |
||||
to_graph: Option<PyGraphNameRef<'_>>, |
||||
py: Python<'_>, |
||||
) -> PyResult<()> { |
||||
let to_graph_name = to_graph.as_ref().map(GraphNameRef::from); |
||||
let input = PyReadable::from_args(&path, input, py)?; |
||||
let format = lookup_rdf_format(format, path.as_deref())?; |
||||
py.allow_threads(|| { |
||||
let mut parser = RdfParser::from_format(format); |
||||
if let Some(base_iri) = base_iri { |
||||
parser = parser |
||||
.with_base_iri(base_iri) |
||||
.map_err(|e| PyValueError::new_err(e.to_string()))?; |
||||
} |
||||
if let Some(to_graph_name) = to_graph_name { |
||||
parser = parser.with_default_graph(to_graph_name); |
||||
} |
||||
self.inner |
||||
.load_from_read(parser, input) |
||||
.map_err(|e| map_loader_error(e, path)) |
||||
}) |
||||
} |
||||
|
||||
/// Dumps the store quads or triples into a file.
|
||||
///
|
||||
/// It currently supports the following formats:
|
||||
///
|
||||
/// * `N-Triples <https://www.w3.org/TR/n-triples/>`_ (:py:attr:`RdfFormat.N_TRIPLES`)
|
||||
/// * `N-Quads <https://www.w3.org/TR/n-quads/>`_ (:py:attr:`RdfFormat.N_QUADS`)
|
||||
/// * `Turtle <https://www.w3.org/TR/turtle/>`_ (:py:attr:`RdfFormat.TURTLE`)
|
||||
/// * `TriG <https://www.w3.org/TR/trig/>`_ (:py:attr:`RdfFormat.TRIG`)
|
||||
/// * `N3 <https://w3c.github.io/N3/spec/>`_ (:py:attr:`RdfFormat.N3`)
|
||||
/// * `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_ (:py:attr:`RdfFormat.RDF_XML`)
|
||||
///
|
||||
/// It supports also some media type and extension aliases.
|
||||
/// For example, ``application/turtle`` could also be used for `Turtle <https://www.w3.org/TR/turtle/>`_
|
||||
/// and ``application/xml`` or ``xml`` for `RDF/XML <https://www.w3.org/TR/rdf-syntax-grammar/>`_.
|
||||
///
|
||||
/// :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')``. If :py:const:`None`, a :py:class:`bytes` buffer is returned with the serialized content.
|
||||
/// :type output: typing.IO[bytes] or str or os.PathLike[str] or None, optional
|
||||
/// :param format: the format of the RDF serialization. If :py:const:`None`, the format is guessed from the file name extension.
|
||||
/// :type format: RdfFormat or None, optional
|
||||
/// :param from_graph: the store graph from which dump the triples. Required if the serialization format does not support named graphs. If it does supports named graphs the full dataset is written.
|
||||
/// :type from_graph: NamedNode or BlankNode or DefaultGraph or None, optional
|
||||
/// :return: :py:class:`bytes` with the serialization if the ``output`` parameter is :py:const:`None`, :py:const:`None` if ``output`` is set.
|
||||
/// :rtype: bytes or None
|
||||
/// :raises ValueError: if the format is not supported or the `from_graph` parameter is not given with a syntax not supporting named graphs.
|
||||
/// :raises OSError: if an error happens during a quad lookup or file writing.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
|
||||
/// >>> store.dump(format=RdfFormat.TRIG)
|
||||
/// b'<http://example.com> <http://example.com/p> "1" .\n'
|
||||
///
|
||||
/// >>> import io
|
||||
/// >>> 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, RdfFormat.TURTLE, from_graph=NamedNode("http://example.com/g"))
|
||||
/// >>> output.getvalue()
|
||||
/// b'<http://example.com> <http://example.com/p> "1" .\n'
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
#[pyo3(signature = (output = None, format = None, *, from_graph = None))] |
||||
fn dump<'py>( |
||||
&self, |
||||
output: Option<PyWritableOutput>, |
||||
format: Option<PyRdfFormatInput>, |
||||
from_graph: Option<PyGraphNameRef<'_>>, |
||||
py: Python<'py>, |
||||
) -> PyResult<Option<Bound<'py, PyBytes>>> { |
||||
let from_graph_name = from_graph.as_ref().map(GraphNameRef::from); |
||||
PyWritable::do_write( |
||||
|output, file_path| { |
||||
py.allow_threads(|| { |
||||
let format = lookup_rdf_format(format, file_path.as_deref())?; |
||||
if let Some(from_graph_name) = from_graph_name { |
||||
self.inner |
||||
.dump_graph_to_write(from_graph_name, format, output) |
||||
} else { |
||||
self.inner.dump_to_write(format, output) |
||||
} |
||||
.map_err(map_serializer_error) |
||||
}) |
||||
}, |
||||
output, |
||||
py, |
||||
) |
||||
} |
||||
|
||||
/// Returns an iterator over all the store named graphs.
|
||||
///
|
||||
/// :return: an iterator of the store graph names.
|
||||
/// :rtype: collections.abc.Iterator[NamedNode or BlankNode]
|
||||
/// :raises OSError: if an error happens during the named graphs lookup.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> list(store.named_graphs())
|
||||
/// [<NamedNode value=http://example.com/g>]
|
||||
fn named_graphs(&self) -> GraphNameIter { |
||||
GraphNameIter { |
||||
inner: self.inner.named_graphs(), |
||||
} |
||||
} |
||||
|
||||
/// Returns if the store contains the given named graph.
|
||||
///
|
||||
/// :param graph_name: the name of the named graph.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
|
||||
/// :rtype: bool
|
||||
/// :raises OSError: if an error happens during the named graph lookup.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add_graph(NamedNode('http://example.com/g'))
|
||||
/// >>> store.contains_named_graph(NamedNode('http://example.com/g'))
|
||||
/// True
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
fn contains_named_graph( |
||||
&self, |
||||
graph_name: PyGraphNameRef<'_>, |
||||
py: Python<'_>, |
||||
) -> PyResult<bool> { |
||||
let graph_name = GraphNameRef::from(&graph_name); |
||||
py.allow_threads(|| { |
||||
match graph_name { |
||||
GraphNameRef::DefaultGraph => Ok(true), |
||||
GraphNameRef::NamedNode(graph_name) => self.inner.contains_named_graph(graph_name), |
||||
GraphNameRef::BlankNode(graph_name) => self.inner.contains_named_graph(graph_name), |
||||
} |
||||
.map_err(map_storage_error) |
||||
}) |
||||
} |
||||
|
||||
/// Adds a named graph to the store.
|
||||
///
|
||||
/// :param graph_name: the name of the name graph to add.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the named graph insertion.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add_graph(NamedNode('http://example.com/g'))
|
||||
/// >>> list(store.named_graphs())
|
||||
/// [<NamedNode value=http://example.com/g>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
fn add_graph(&self, graph_name: PyGraphNameRef<'_>, py: Python<'_>) -> PyResult<()> { |
||||
let graph_name = GraphNameRef::from(&graph_name); |
||||
py.allow_threads(|| { |
||||
match graph_name { |
||||
GraphNameRef::DefaultGraph => Ok(()), |
||||
GraphNameRef::NamedNode(graph_name) => { |
||||
self.inner.insert_named_graph(graph_name).map(|_| ()) |
||||
} |
||||
GraphNameRef::BlankNode(graph_name) => { |
||||
self.inner.insert_named_graph(graph_name).map(|_| ()) |
||||
} |
||||
} |
||||
.map_err(map_storage_error) |
||||
}) |
||||
} |
||||
|
||||
/// Clears a graph from the store without removing it.
|
||||
///
|
||||
/// :param graph_name: the name of the name graph to clear.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the operation.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> store.clear_graph(NamedNode('http://example.com/g'))
|
||||
/// >>> list(store)
|
||||
/// []
|
||||
/// >>> list(store.named_graphs())
|
||||
/// [<NamedNode value=http://example.com/g>]
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
fn clear_graph(&self, graph_name: PyGraphNameRef<'_>, py: Python<'_>) -> PyResult<()> { |
||||
let graph_name = GraphNameRef::from(&graph_name); |
||||
py.allow_threads(|| { |
||||
self.inner |
||||
.clear_graph(graph_name) |
||||
.map_err(map_storage_error) |
||||
}) |
||||
} |
||||
|
||||
/// Removes a graph from the store.
|
||||
///
|
||||
/// The default graph will not be removed but just cleared.
|
||||
///
|
||||
/// :param graph_name: the name of the name graph to remove.
|
||||
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the named graph removal.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> store.remove_graph(NamedNode('http://example.com/g'))
|
||||
/// >>> list(store.named_graphs())
|
||||
/// []
|
||||
#[allow(clippy::needless_pass_by_value)] |
||||
fn remove_graph(&self, graph_name: PyGraphNameRef<'_>, py: Python<'_>) -> PyResult<()> { |
||||
let graph_name = GraphNameRef::from(&graph_name); |
||||
py.allow_threads(|| { |
||||
match graph_name { |
||||
GraphNameRef::DefaultGraph => self.inner.clear_graph(GraphNameRef::DefaultGraph), |
||||
GraphNameRef::NamedNode(graph_name) => { |
||||
self.inner.remove_named_graph(graph_name).map(|_| ()) |
||||
} |
||||
GraphNameRef::BlankNode(graph_name) => { |
||||
self.inner.remove_named_graph(graph_name).map(|_| ()) |
||||
} |
||||
} |
||||
.map_err(map_storage_error) |
||||
}) |
||||
} |
||||
|
||||
/// Clears the store by removing all its contents.
|
||||
///
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the operation.
|
||||
///
|
||||
/// >>> store = Store()
|
||||
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
|
||||
/// >>> store.clear()
|
||||
/// >>> list(store)
|
||||
/// []
|
||||
/// >>> list(store.named_graphs())
|
||||
/// []
|
||||
fn clear(&self, py: Python<'_>) -> PyResult<()> { |
||||
py.allow_threads(|| self.inner.clear().map_err(map_storage_error)) |
||||
} |
||||
|
||||
/// Flushes all buffers and ensures that all writes are saved on disk.
|
||||
///
|
||||
/// Flushes are automatically done using background threads but might lag a little bit.
|
||||
///
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the flush.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
fn flush(&self, py: Python<'_>) -> PyResult<()> { |
||||
py.allow_threads(|| self.inner.flush().map_err(map_storage_error)) |
||||
} |
||||
|
||||
/// Optimizes the database for future workload.
|
||||
///
|
||||
/// Useful to call after a batch upload or another similar operation.
|
||||
///
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the optimization.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
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 or os.PathLike[str]
|
||||
/// :rtype: None
|
||||
/// :raises OSError: if an error happens during the backup.
|
||||
#[cfg(not(target_family = "wasm"))] |
||||
fn backup(&self, target_directory: PathBuf, 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<bool> { |
||||
Ok(!self.inner.is_empty().map_err(map_storage_error)?) |
||||
} |
||||
|
||||
fn __len__(&self) -> PyResult<usize> { |
||||
self.inner.len().map_err(map_storage_error) |
||||
} |
||||
|
||||
fn __contains__(&self, quad: &PyQuad) -> PyResult<bool> { |
||||
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<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self) -> PyResult<Option<PyQuad>> { |
||||
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<'_, Self> { |
||||
slf |
||||
} |
||||
|
||||
fn __next__(&mut self) -> PyResult<Option<PyNamedOrBlankNode>> { |
||||
self.inner |
||||
.next() |
||||
.map(|q| Ok(q.map_err(map_storage_error)?.into())) |
||||
.transpose() |
||||
} |
||||
} |
||||
|
||||
pub fn map_storage_error(error: StorageError) -> PyErr { |
||||
match error { |
||||
StorageError::Io(error) => error.into(), |
||||
_ => PyRuntimeError::new_err(error.to_string()), |
||||
} |
||||
} |
||||
|
||||
pub fn map_loader_error(error: LoaderError, file_path: Option<PathBuf>) -> PyErr { |
||||
match error { |
||||
LoaderError::Storage(error) => map_storage_error(error), |
||||
LoaderError::Parsing(error) => map_parse_error(error, file_path), |
||||
LoaderError::InvalidBaseIri { .. } => PyValueError::new_err(error.to_string()), |
||||
} |
||||
} |
||||
|
||||
pub fn map_serializer_error(error: SerializerError) -> PyErr { |
||||
match error { |
||||
SerializerError::Storage(error) => map_storage_error(error), |
||||
SerializerError::Io(error) => error.into(), |
||||
SerializerError::DatasetFormatExpected(_) => PyValueError::new_err(error.to_string()), |
||||
} |
||||
} |
||||
|
||||
enum PythonOrStorageError { |
||||
Python(PyErr), |
||||
Storage(StorageError), |
||||
} |
||||
|
||||
impl From<PyErr> for PythonOrStorageError { |
||||
fn from(error: PyErr) -> Self { |
||||
Self::Python(error) |
||||
} |
||||
} |
||||
|
||||
impl From<StorageError> for PythonOrStorageError { |
||||
fn from(error: StorageError) -> Self { |
||||
Self::Storage(error) |
||||
} |
||||
} |
||||
impl From<PythonOrStorageError> for PyErr { |
||||
fn from(error: PythonOrStorageError) -> Self { |
||||
match error { |
||||
PythonOrStorageError::Python(error) => error, |
||||
PythonOrStorageError::Storage(error) => map_storage_error(error), |
||||
} |
||||
} |
||||
} |
@ -1,37 +0,0 @@ |
||||
# type: ignore |
||||
import inspect |
||||
from doctest import DocTestFinder, DocTestSuite |
||||
|
||||
import pyoxigraph |
||||
|
||||
|
||||
class ExtendedDocTestFinder(DocTestFinder): |
||||
""" |
||||
More aggressive doctest lookup |
||||
""" |
||||
|
||||
def _find(self, tests, obj, name, module, source_lines, globs, seen): |
||||
# If we've already processed this object, then ignore it. |
||||
if id(obj) in seen: |
||||
return |
||||
seen[id(obj)] = 1 |
||||
|
||||
# Find a test for this object, and add it to the list of tests. |
||||
test = self._get_test(obj, name, module, globs, source_lines) |
||||
if test is not None: |
||||
tests.append(test) |
||||
|
||||
# Look for tests in a module's contained objects. |
||||
if inspect.ismodule(obj) or inspect.isclass(obj): |
||||
for valname, val in obj.__dict__.items(): |
||||
if valname == "__doc__": |
||||
continue |
||||
# Special handling for staticmethod/classmethod. |
||||
if isinstance(val, (staticmethod, classmethod)): |
||||
val = val.__func__ |
||||
self._find(tests, val, f"{name}.{valname}", module, source_lines, globs, seen) |
||||
|
||||
|
||||
def load_tests(_loader, tests, _ignore): |
||||
tests.addTests(DocTestSuite(pyoxigraph, test_finder=ExtendedDocTestFinder())) |
||||
return tests |
@ -1,266 +0,0 @@ |
||||
import sys |
||||
import unittest |
||||
from io import BytesIO, StringIO, UnsupportedOperation |
||||
from tempfile import NamedTemporaryFile, TemporaryFile |
||||
|
||||
from pyoxigraph import ( |
||||
Literal, |
||||
NamedNode, |
||||
Quad, |
||||
QueryBoolean, |
||||
QueryResultsFormat, |
||||
QuerySolutions, |
||||
RdfFormat, |
||||
parse, |
||||
parse_query_results, |
||||
serialize, |
||||
) |
||||
|
||||
EXAMPLE_TRIPLE = Quad( |
||||
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(suffix=".ttl") as fp: |
||||
fp.write('<foo> <p> "éù" .'.encode()) |
||||
fp.flush() |
||||
self.assertEqual( |
||||
list(parse(path=fp.name, base_iri="http://example.com/")), |
||||
[EXAMPLE_TRIPLE], |
||||
) |
||||
|
||||
def test_parse_not_existing_file(self) -> None: |
||||
with self.assertRaises(IOError) as _: |
||||
parse(path="/tmp/not-existing-oxigraph-file.ttl", format=RdfFormat.TURTLE) |
||||
|
||||
def test_parse_str(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
'<foo> <p> "éù" .', |
||||
RdfFormat.TURTLE, |
||||
base_iri="http://example.com/", |
||||
) |
||||
), |
||||
[EXAMPLE_TRIPLE], |
||||
) |
||||
|
||||
def test_parse_bytes(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
'<foo> <p> "éù" .'.encode(), |
||||
RdfFormat.TURTLE, |
||||
base_iri="http://example.com/", |
||||
) |
||||
), |
||||
[EXAMPLE_TRIPLE], |
||||
) |
||||
|
||||
def test_parse_str_io(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
StringIO('<foo> <p> "éù" .'), |
||||
RdfFormat.TURTLE, |
||||
base_iri="http://example.com/", |
||||
) |
||||
), |
||||
[EXAMPLE_TRIPLE], |
||||
) |
||||
|
||||
def test_parse_long_str_io(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
StringIO('<foo> <p> "éù" .\n' * 1024), |
||||
RdfFormat.TURTLE, |
||||
base_iri="http://example.com/", |
||||
) |
||||
), |
||||
[EXAMPLE_TRIPLE] * 1024, |
||||
) |
||||
|
||||
def test_parse_bytes_io(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
BytesIO('<foo> <p> "éù" .'.encode()), |
||||
RdfFormat.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, RdfFormat.N_TRIPLES)) |
||||
|
||||
def test_parse_quad(self) -> None: |
||||
self.assertEqual( |
||||
list( |
||||
parse( |
||||
'<g> { <foo> <p> "1" }', |
||||
RdfFormat.TRIG, |
||||
base_iri="http://example.com/", |
||||
) |
||||
), |
||||
[EXAMPLE_QUAD], |
||||
) |
||||
|
||||
def test_parse_syntax_error(self) -> None: |
||||
with NamedTemporaryFile() as fp: |
||||
fp.write(b"@base <http://example.com/> .\n") |
||||
fp.write(b'<foo> "p" "1"') |
||||
fp.flush() |
||||
with self.assertRaises(SyntaxError) as ctx: |
||||
list(parse(path=fp.name, format=RdfFormat.TURTLE)) |
||||
self.assertEqual(ctx.exception.filename, fp.name) |
||||
self.assertEqual(ctx.exception.lineno, 2) |
||||
self.assertEqual(ctx.exception.offset, 7) |
||||
if sys.version_info >= (3, 10): |
||||
self.assertEqual(ctx.exception.end_lineno, 2) |
||||
self.assertEqual(ctx.exception.end_offset, 10) |
||||
|
||||
def test_parse_without_named_graphs(self) -> None: |
||||
with self.assertRaises(SyntaxError) as _: |
||||
list( |
||||
parse( |
||||
'<g> { <foo> <p> "1" }', |
||||
RdfFormat.TRIG, |
||||
base_iri="http://example.com/", |
||||
without_named_graphs=True, |
||||
) |
||||
) |
||||
|
||||
def test_parse_rename_blank_nodes(self) -> None: |
||||
self.assertNotEqual( |
||||
list( |
||||
parse( |
||||
'_:s <http://example.com/p> "o" .', |
||||
RdfFormat.N_TRIPLES, |
||||
rename_blank_nodes=True, |
||||
) |
||||
), |
||||
list( |
||||
parse( |
||||
'_:s <http://example.com/p> "o" .', |
||||
RdfFormat.N_TRIPLES, |
||||
rename_blank_nodes=True, |
||||
) |
||||
), |
||||
) |
||||
|
||||
|
||||
class TestSerialize(unittest.TestCase): |
||||
def test_serialize_to_bytes(self) -> None: |
||||
self.assertEqual( |
||||
(serialize([EXAMPLE_TRIPLE.triple], None, RdfFormat.TURTLE) or b"").decode(), |
||||
'<http://example.com/foo> <http://example.com/p> "éù" .\n', |
||||
) |
||||
|
||||
def test_serialize_to_bytes_io(self) -> None: |
||||
output = BytesIO() |
||||
serialize([EXAMPLE_TRIPLE.triple], output, RdfFormat.TURTLE) |
||||
self.assertEqual( |
||||
output.getvalue().decode(), |
||||
'<http://example.com/foo> <http://example.com/p> "éù" .\n', |
||||
) |
||||
|
||||
def test_serialize_to_file(self) -> None: |
||||
with NamedTemporaryFile(suffix=".ttl") as fp: |
||||
serialize([EXAMPLE_TRIPLE], fp.name) |
||||
self.assertEqual( |
||||
fp.read().decode(), |
||||
'<http://example.com/foo> <http://example.com/p> "éù" .\n', |
||||
) |
||||
|
||||
def test_serialize_io_error(self) -> None: |
||||
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("rb") as fp: |
||||
serialize([EXAMPLE_TRIPLE], fp, RdfFormat.TURTLE) |
||||
|
||||
def test_serialize_quad(self) -> None: |
||||
output = BytesIO() |
||||
serialize([EXAMPLE_QUAD], output, RdfFormat.TRIG) |
||||
self.assertEqual( |
||||
output.getvalue(), |
||||
b'<http://example.com/g> {\n\t<http://example.com/foo> <http://example.com/p> "1" .\n}\n', |
||||
) |
||||
|
||||
|
||||
class TestParseQuerySolutions(unittest.TestCase): |
||||
def test_parse_file(self) -> None: |
||||
with NamedTemporaryFile(suffix=".tsv") as fp: |
||||
fp.write(b'?s\t?p\t?o\n<http://example.com/s>\t<http://example.com/s>\t"1"\n') |
||||
fp.flush() |
||||
r = parse_query_results(path=fp.name) |
||||
self.assertIsInstance(r, QuerySolutions) |
||||
results = list(r) # type: ignore[arg-type] |
||||
self.assertEqual(results[0]["s"], NamedNode("http://example.com/s")) |
||||
self.assertEqual(results[0][2], Literal("1")) |
||||
|
||||
def test_parse_not_existing_file(self) -> None: |
||||
with self.assertRaises(IOError) as _: |
||||
parse_query_results(path="/tmp/not-existing-oxigraph-file.ttl", format=QueryResultsFormat.JSON) |
||||
|
||||
def test_parse_str(self) -> None: |
||||
result = parse_query_results("true", QueryResultsFormat.TSV) |
||||
self.assertIsInstance(result, QueryBoolean) |
||||
self.assertTrue(result) |
||||
|
||||
def test_parse_bytes(self) -> None: |
||||
result = parse_query_results(b"false", QueryResultsFormat.TSV) |
||||
self.assertIsInstance(result, QueryBoolean) |
||||
self.assertFalse(result) |
||||
|
||||
def test_parse_str_io(self) -> None: |
||||
result = parse_query_results("true", QueryResultsFormat.TSV) |
||||
self.assertIsInstance(result, QueryBoolean) |
||||
self.assertTrue(result) |
||||
|
||||
def test_parse_bytes_io(self) -> None: |
||||
result = parse_query_results(BytesIO(b"false"), QueryResultsFormat.TSV) |
||||
self.assertIsInstance(result, QueryBoolean) |
||||
self.assertFalse(result) |
||||
|
||||
def test_parse_io_error(self) -> None: |
||||
with self.assertRaises(UnsupportedOperation) as _, TemporaryFile("wb") as fp: |
||||
parse_query_results(fp, QueryResultsFormat.XML) |
||||
|
||||
def test_parse_syntax_error_json(self) -> None: |
||||
with NamedTemporaryFile() as fp: |
||||
fp.write(b"{]") |
||||
fp.flush() |
||||
with self.assertRaises(SyntaxError) as ctx: |
||||
list(parse_query_results(path=fp.name, format=QueryResultsFormat.JSON)) # type: ignore[arg-type] |
||||
self.assertEqual(ctx.exception.filename, fp.name) |
||||
self.assertEqual(ctx.exception.lineno, 1) |
||||
self.assertEqual(ctx.exception.offset, 2) |
||||
if sys.version_info >= (3, 10): |
||||
self.assertEqual(ctx.exception.end_lineno, 1) |
||||
self.assertEqual(ctx.exception.end_offset, 3) |
||||
|
||||
def test_parse_syntax_error_tsv(self) -> None: |
||||
with NamedTemporaryFile() as fp: |
||||
fp.write(b"?a\t?test\n") |
||||
fp.write(b"1\t<foo >\n") |
||||
fp.flush() |
||||
with self.assertRaises(SyntaxError) as ctx: |
||||
list(parse_query_results(path=fp.name, format=QueryResultsFormat.TSV)) # type: ignore[arg-type] |
||||
self.assertEqual(ctx.exception.filename, fp.name) |
||||
self.assertEqual(ctx.exception.lineno, 2) |
||||
self.assertEqual(ctx.exception.offset, 3) |
||||
if sys.version_info >= (3, 10): |
||||
self.assertEqual(ctx.exception.end_lineno, 2) |
||||
self.assertEqual(ctx.exception.end_offset, 9) |
@ -1,376 +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")), "<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"^^<http://www.w3.org/2001/XMLSchema#integer>', |
||||
) |
||||
|
||||
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"), |
||||
) |
||||
), |
||||
"<http://example.com/s> <http://example.com/p> <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"), |
||||
) |
||||
), |
||||
"<http://example.com/s> <http://example.com/p> <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() |
@ -1,418 +0,0 @@ |
||||
import gc |
||||
import sys |
||||
import unittest |
||||
from io import BytesIO, StringIO, UnsupportedOperation |
||||
from pathlib import Path |
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory, TemporaryFile |
||||
from typing import Any |
||||
|
||||
from pyoxigraph import ( |
||||
BlankNode, |
||||
DefaultGraph, |
||||
NamedNode, |
||||
Quad, |
||||
QueryBoolean, |
||||
QueryResultsFormat, |
||||
QuerySolution, |
||||
QuerySolutions, |
||||
QueryTriples, |
||||
RdfFormat, |
||||
Store, |
||||
Triple, |
||||
Variable, |
||||
) |
||||
|
||||
foo = NamedNode("http://foo") |
||||
bar = NamedNode("http://bar") |
||||
baz = NamedNode("http://baz") |
||||
triple = Triple(foo, foo, foo) |
||||
graph = NamedNode("http://graph") |
||||
is_wasm = sys.platform == "emscripten" |
||||
|
||||
|
||||
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) |
||||
|
||||
@unittest.skipIf(is_wasm, "Not supported with WASM") |
||||
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_select_query_dump(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz)) |
||||
results: QuerySolutions = store.query("SELECT ?s WHERE { ?s ?p ?o }") # type: ignore[assignment] |
||||
self.assertIsInstance(results, QuerySolutions) |
||||
output = BytesIO() |
||||
results.serialize(output, QueryResultsFormat.CSV) |
||||
self.assertEqual( |
||||
output.getvalue().decode(), |
||||
"s\r\nhttp://foo\r\n", |
||||
) |
||||
|
||||
def test_ask_query_dump(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz)) |
||||
results: QueryBoolean = store.query("ASK { ?s ?p ?o }") # type: ignore[assignment] |
||||
self.assertIsInstance(results, QueryBoolean) |
||||
output = BytesIO() |
||||
results.serialize(output, QueryResultsFormat.CSV) |
||||
self.assertEqual( |
||||
output.getvalue().decode(), |
||||
"true", |
||||
) |
||||
|
||||
def test_construct_query_dump(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz)) |
||||
results: QueryTriples = store.query("CONSTRUCT WHERE { ?s ?p ?o }") # type: ignore[assignment] |
||||
self.assertIsInstance(results, QueryTriples) |
||||
output = BytesIO() |
||||
results.serialize(output, RdfFormat.N_TRIPLES) |
||||
self.assertEqual( |
||||
output.getvalue().decode(), |
||||
"<http://foo> <http://bar> <http://baz> .\n", |
||||
) |
||||
|
||||
def test_update_insert_data(self) -> None: |
||||
store = Store() |
||||
store.update("INSERT DATA { <http://foo> <http://foo> <http://foo> }") |
||||
self.assertEqual(len(store), 1) |
||||
|
||||
def test_update_delete_data(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, foo, foo)) |
||||
store.update("DELETE DATA { <http://foo> <http://foo> <http://foo> }") |
||||
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) |
||||
|
||||
@unittest.skipIf(is_wasm, "Not supported with WASM") |
||||
def test_update_load(self) -> None: |
||||
store = Store() |
||||
store.update("LOAD <https://www.w3.org/1999/02/22-rdf-syntax-ns>") |
||||
self.assertGreater(len(store), 100) |
||||
|
||||
def test_update_star(self) -> None: |
||||
store = Store() |
||||
store.update("PREFIX : <http://www.example.org/> INSERT DATA { :alice :claims << :bob :age 23 >> }") |
||||
results: Any = store.query( |
||||
"PREFIX : <http://www.example.org/> 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( |
||||
b"<http://foo> <http://bar> <http://baz> .", |
||||
RdfFormat.N_TRIPLES, |
||||
) |
||||
self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) |
||||
|
||||
def test_load_ntriples_to_named_graph(self) -> None: |
||||
store = Store() |
||||
store.load( |
||||
"<http://foo> <http://bar> <http://baz> .", |
||||
RdfFormat.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"<http://foo> <http://bar> <> ."), |
||||
RdfFormat.TURTLE, |
||||
base_iri="http://baz", |
||||
) |
||||
self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) |
||||
|
||||
def test_load_nquads(self) -> None: |
||||
store = Store() |
||||
store.load( |
||||
StringIO("<http://foo> <http://bar> <http://baz> <http://graph>."), |
||||
RdfFormat.N_QUADS, |
||||
) |
||||
self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) |
||||
|
||||
def test_load_trig_with_base_iri(self) -> None: |
||||
store = Store() |
||||
store.load( |
||||
"<http://graph> { <http://foo> <http://bar> <> . }", |
||||
RdfFormat.TRIG, |
||||
base_iri="http://baz", |
||||
) |
||||
self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) |
||||
|
||||
def test_load_file(self) -> None: |
||||
with NamedTemporaryFile(suffix=".nq") as fp: |
||||
fp.write(b"<http://foo> <http://bar> <http://baz> <http://graph>.") |
||||
fp.flush() |
||||
store = Store() |
||||
store.load(path=fp.name) |
||||
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, RdfFormat.N_TRIPLES) |
||||
|
||||
def test_dump_ntriples(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz, graph)) |
||||
output = BytesIO() |
||||
store.dump(output, RdfFormat.N_TRIPLES, from_graph=graph) |
||||
self.assertEqual( |
||||
output.getvalue(), |
||||
b"<http://foo> <http://bar> <http://baz> .\n", |
||||
) |
||||
|
||||
def test_dump_nquads(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz, graph)) |
||||
self.assertEqual( |
||||
store.dump(format=RdfFormat.N_QUADS), |
||||
b"<http://foo> <http://bar> <http://baz> <http://graph> .\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, RdfFormat.TRIG) |
||||
self.assertEqual( |
||||
output.getvalue(), |
||||
b"<http://foo> <http://bar> <http://baz> .\n" |
||||
b"<http://graph> {\n\t<http://foo> <http://bar> <http://baz> .\n}\n", |
||||
) |
||||
|
||||
def test_dump_file(self) -> None: |
||||
with NamedTemporaryFile(delete=False) as fp: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, baz, graph)) |
||||
file_name = Path(fp.name) |
||||
store.dump(file_name, RdfFormat.N_QUADS) |
||||
self.assertEqual( |
||||
file_name.read_text(), |
||||
"<http://foo> <http://bar> <http://baz> <http://graph> .\n", |
||||
) |
||||
|
||||
def test_dump_with_io_error(self) -> None: |
||||
store = Store() |
||||
store.add(Quad(foo, bar, bar)) |
||||
with self.assertRaises(OSError) as _, TemporaryFile("rb") as fp: |
||||
store.dump(fp, RdfFormat.TRIG) |
||||
|
||||
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), []) |
||||
|
||||
@unittest.skipIf(is_wasm, "Not supported with WASM") |
||||
def test_read_only(self) -> None: |
||||
quad = Quad(foo, bar, baz, graph) |
||||
with TemporaryDirectory() as dir: |
||||
store = Store(dir) |
||||
store.add(quad) |
||||
del store |
||||
gc.collect() |
||||
store = Store.read_only(dir) |
||||
self.assertEqual(list(store), [quad]) |
||||
|
||||
@unittest.skipIf(is_wasm, "Not supported with WASM") |
||||
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() |
Loading…
Reference in new issue