From cbb62ef8b8918ec403d317ba43064e2f304fdc6b Mon Sep 17 00:00:00 2001 From: loryruta Date: Wed, 29 May 2024 12:33:23 +0200 Subject: [PATCH 01/28] example tasks: add rm, improve cmdline --- example/tasks/main.py | 72 ++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/example/tasks/main.py b/example/tasks/main.py index 0d2a349..bac83f6 100644 --- a/example/tasks/main.py +++ b/example/tasks/main.py @@ -1,20 +1,19 @@ +import time from cmd import Cmd from objectbox import * -import time + @Entity() class Task: id = Id() text = String() - date_created = Date(py_type=int) date_finished = Date(py_type=int) - -# objectbox expects date timestamp in milliseconds since UNIX epoch +# Objectbox expects date timestamp in milliseconds since UNIX epoch def now_ms() -> int: - return time.time_ns() / 1000000 + return int(time.time_ns() / 1000000) def format_date(timestamp_ms: int) -> str: @@ -23,34 +22,63 @@ def format_date(timestamp_ms: int) -> str: class TasklistCmd(Cmd): prompt = "> " - _store = Store(directory="tasklist-db") - _box = _store.box(Task) - def do_ls(self, _): - """list tasks""" + def __init__(self): + super().__init__() + self._store = Store(directory="tasklist-db") + self._task_box = self._store.box(Task) + self._query = self._task_box.query().build() - tasks = self._box.get_all() + def add_task(self, text: str): + task = Task(text=text, date_created=now_ms()) + self._task_box.put(task) + + def remove_task(self, task_id: int) -> bool: + is_removed = self._task_box.remove(task_id) + return is_removed + + def find_tasks(self): + query = self._task_box.query().build() + return query.find() + + # *** Command line *** + + def do_ls(self, _): + """ Lists all the tasks created. """ + tasks = self.find_tasks() print("%3s %-29s %-29s %s" % ("ID", "Created", "Finished", "Text")) for task in tasks: print("%3d %-29s %-29s %s" % ( - task.id, format_date(task.date_created), format_date(task.date_finished), task.text)) + task.id, format_date(task.date_created), format_date(task.date_finished), task.text)) def do_new(self, text: str): - """create a new task with the given text (all arguments concatenated)""" - task = Task() - task.text = text - task.date_created = now_ms() - self._box.put(task) - - def do_done(self, id: str): - """mark task with the given ID as done""" - task = self._box.get(int(id)) + """ Creates a new task with the given text (all arguments concatenated). """ + self.add_task(text) + + def do_done(self, task_id: str): + """ Marks the task with the given ID as done. """ + if not task_id.isdigit() or int(task_id) <= 0: + print(f"Invalid task ID: \"{task_id}\"") + return + task = self._task_box.get(int(task_id)) + if task is None: + print(f"Task {task_id} not found") + return task.date_finished = now_ms() - self._box.put(task) + self._task_box.put(task) + + def do_rm(self, task_id: str): + """ Removes a task given its ID. """ + if not task_id.isdigit() or int(task_id) <= 0: + print(f"Invalid task ID: \"{task_id}\"") + return + is_removed = self.remove_task(int(task_id)) + if not is_removed: + print(f"Task {task_id} not found") def do_exit(self, _): - """close the program""" + """ Closes the program. """ raise SystemExit() From 712cf00e8a462bdd78339a7d995d3575f4575678 Mon Sep 17 00:00:00 2001 From: loryruta Date: Mon, 6 May 2024 10:37:42 +0200 Subject: [PATCH 02/28] scripts: add inspect_c_bindings.py --- scripts/inspect_c_bindings.py | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scripts/inspect_c_bindings.py diff --git a/scripts/inspect_c_bindings.py b/scripts/inspect_c_bindings.py new file mode 100644 index 0000000..aac3103 --- /dev/null +++ b/scripts/inspect_c_bindings.py @@ -0,0 +1,54 @@ +# Script used to inspect differences between objectbox/lib/objectbox.h and c.py (e.g. missing function declarations) +# Usage: +# python inspect_c_bindings.py + +from os import path +from pycparser import c_ast, parse_file +import pycparser_fake_libc +import objectbox.c + +script_dir = path.dirname(path.realpath(__file__)) + + +class FuncDeclVisitor(c_ast.NodeVisitor): + def __init__(self): + self.func_decls = set() + + def visit_FuncDecl(self, node: c_ast.FuncDecl): + # TODO declname is set in the return type (i.e. type field)? + func_name = None + if isinstance(node.type, c_ast.TypeDecl): + func_name = node.type.declname + elif isinstance(node.type, c_ast.PtrDecl): + func_name = node.type.type.declname + else: + raise Exception(f"Unknown node type: {node.type}") + self.func_decls.add(func_name) + + +def _parse_header_file(filename): + fake_libc_arg = "-I" + pycparser_fake_libc.directory + + ast = parse_file(filename, use_cpp=True, cpp_args=fake_libc_arg) # use_cpp = Use C Pre Processor + + visitor = FuncDeclVisitor() + visitor.visit(ast) + + num_missing = 0 + for func_decl in visitor.func_decls: + if not hasattr(objectbox.c, func_decl): + print(f"Missing function: {func_decl}") + num_missing += 1 + + print(f"Missing {num_missing}/{len(visitor.func_decls)} function declarations in c.py") + + +def _main(): + objectbox_h = path.join(script_dir, "../objectbox/lib/objectbox.h") + if not path.exists(objectbox_h): + raise Exception("File not found: objectbox/lib/objectbox.h") + _parse_header_file(objectbox_h) + + +if __name__ == "__main__": + _main() From badfc9b84983f6858621ea70b88a585e6e20fe77 Mon Sep 17 00:00:00 2001 From: loryruta Date: Wed, 29 May 2024 15:12:12 +0200 Subject: [PATCH 03/28] inspect_c_bindings: add requirements comment --- scripts/inspect_c_bindings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/inspect_c_bindings.py b/scripts/inspect_c_bindings.py index aac3103..41f84bd 100644 --- a/scripts/inspect_c_bindings.py +++ b/scripts/inspect_c_bindings.py @@ -1,6 +1,10 @@ # Script used to inspect differences between objectbox/lib/objectbox.h and c.py (e.g. missing function declarations) # Usage: -# python inspect_c_bindings.py +# python inspect_c_bindings.py +# Requirements: +# - pycparser +# - pycparser-fake-libc +# - objectbox or project root in PYTHONPATH from os import path from pycparser import c_ast, parse_file From 366f5f29301c0673892e3ce1797823cb9a7d9be7 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 08:51:32 +0200 Subject: [PATCH 04/28] examples: use explicit imports #56 --- example/ollama/main.py | 2 +- example/tasks/main.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/example/ollama/main.py b/example/ollama/main.py index 402c0c4..c967cc4 100644 --- a/example/ollama/main.py +++ b/example/ollama/main.py @@ -2,7 +2,7 @@ # using objectbox as a vector store import ollama -from objectbox import * +from objectbox import Entity, Store, Id, String, Float32Vector, HnswIndex, VectorDistanceType documents = [ "Llamas are members of the camelid family meaning they're pretty closely related to vicuñas and camels", diff --git a/example/tasks/main.py b/example/tasks/main.py index bac83f6..285eae8 100644 --- a/example/tasks/main.py +++ b/example/tasks/main.py @@ -1,7 +1,6 @@ import time from cmd import Cmd -from objectbox import * - +from objectbox import Entity, Date, Id, String, Store @Entity() class Task: From dd3dcd1c62c057853da6b3c7851329463de3e990 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Fri, 24 May 2024 17:10:25 +0200 Subject: [PATCH 05/28] adds doc folder using sphinx-autoapi for API Reference #52 --- .gitignore | 3 +- doc/Makefile | 20 ++++++++++++ doc/README.md | 18 +++++++++++ doc/conf.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 20 ++++++++++++ doc/make.bat | 35 ++++++++++++++++++++ doc/requirements.txt | 3 ++ doc/setup.sh | 3 ++ 8 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 doc/Makefile create mode 100644 doc/README.md create mode 100644 doc/conf.py create mode 100644 doc/index.rst create mode 100644 doc/make.bat create mode 100644 doc/requirements.txt create mode 100755 doc/setup.sh diff --git a/.gitignore b/.gitignore index 9daffe1..0427178 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ share/python-wheels/ *.egg MANIFEST **/__pycache__ -env/ \ No newline at end of file +env/ +doc/_build diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..56437e4 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,18 @@ +Building a Reference API +======================== + +Setup +----- + +``` +./setup.sh +``` + +Build +----- + +``` +source .venv/bin/activate +make html +``` + diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..568aab5 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,77 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'ObjectBox Python Bindings' +copyright = '2024, ObjectBox Ltd.' +author = 'ObjectBox Ltd.' +release = '4.0.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'autoapi.extension', +] +# 'sphinx.ext.inheritance_diagram', +# 'sphinx.ext.autodoc', + +# -- autoapi configuration --------------------------------------------------- +# https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html + +autoapi_dirs = ['../objectbox'] +# autoapi_template_dir = '' +# autoapi_file_patterns = ['*.py', '*.pyi'] +# autoapi_generate_api_docs = True +autoapi_options = [ + 'members', + 'inherited-members', + 'undoc-members', + # 'private-members', + # 'special-members', + # 'show-inheritance', + # 'show-inheritance-diagram', + 'show-module-summary', + 'imported-members', +] +# autoapi_ignore = ['*migrations'] +# autoapi_root = 'autoapi' +# autoapi_add_toctree_entry = True +autoapi_python_class_content = "both" # default: "class" +# autoapi_member_order = 'bysource' +# autoapi_python_use_implicit_namespaces = False +# autoapi_own_page_level = 'module' + +# Advanced: +# autoapi_keep_files = False + +# Experimental: +autodoc_typehints = 'description' + +only_top_level : bool = True + +if only_top_level: + def skip_submodules(app, what, name, obj, skip, options): + if what == "module": + skip = True + elif what == "package" and name != "objectbox": + skip = True + return skip + + + def setup(sphinx): + sphinx.connect("autoapi-skip-member", skip_submodules) + +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' + +# html_static_path = ['_static'] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..dc50ecd --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,20 @@ +ObjectBox Python Bindings +========================= + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + +Further links +============= + +* Docs for Python, Java/Kotlin and Dart/Flutter: https://docs.objectbox.io +* General Information: https://objectbox.io + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..dbde32a --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,3 @@ +sphinx +sphinx-rtd-theme +sphinx-autoapi diff --git a/doc/setup.sh b/doc/setup.sh new file mode 100755 index 0000000..76d7008 --- /dev/null +++ b/doc/setup.sh @@ -0,0 +1,3 @@ +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt From ac49d115471c170360076a5dc6d7b18454cec969 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 11:44:18 +0200 Subject: [PATCH 06/28] box: docstring fix for indentation error #52 --- objectbox/box.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/objectbox/box.py b/objectbox/box.py index cc74038..a10dfd7 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -164,7 +164,8 @@ def query(self, condition: Optional[QueryCondition] = None) -> QueryBuilder: :param condition: If given, applies the given high-level condition to the new QueryBuilder object. Useful for a user-friendly API design; for example: - ``box.query(name_property.equals("Johnny")).build()`` + + ``box.query(name_property.equals("Johnny")).build()`` """ qb = QueryBuilder(self._store, self) if condition is not None: From 681bfebd509966e0fd4313d8858a08be19c07d7d Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 12:27:22 +0200 Subject: [PATCH 07/28] docstrings added for public API #52 --- objectbox/__init__.py | 3 ++- objectbox/box.py | 1 + objectbox/builder.py | 1 + objectbox/model/model.py | 5 +++++ objectbox/model/properties.py | 33 ++++++++++++++++++++++++++++++++- objectbox/objectbox.py | 1 + objectbox/store.py | 5 +++++ objectbox/version.py | 1 + 8 files changed, 48 insertions(+), 2 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index da4601d..374993c 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -68,7 +68,8 @@ # Python binding version version = Version(4, 0, 0) - +"""ObjectBox Python version""" def version_info(): + """Returns a string with Python and core version information.""" return "ObjectBox Python version " + str(version) + " using dynamic library version " + str(version_core) diff --git a/objectbox/box.py b/objectbox/box.py index a10dfd7..8cf5941 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -21,6 +21,7 @@ class Box: + """Interface to Entities""" def __init__(self, store: Store, entity: _Entity): if not isinstance(entity, _Entity): raise Exception("Given type is not an Entity") diff --git a/objectbox/builder.py b/objectbox/builder.py index f6176cd..cad3933 100644 --- a/objectbox/builder.py +++ b/objectbox/builder.py @@ -20,6 +20,7 @@ from warnings import warn class Builder: + """*Deprecated Interface*""" def __init__(self): """This throws a deprecation warning on initialization.""" warn(f'Using {self.__class__.__name__} is deprecated, please use Store(model=, directory= ...) from objectbox.store.', DeprecationWarning, stacklevel=2) diff --git a/objectbox/model/model.py b/objectbox/model/model.py index 3845bf2..c4e4875 100644 --- a/objectbox/model/model.py +++ b/objectbox/model/model.py @@ -21,6 +21,11 @@ class Model: + """ + Database schema + + A model specifies available entities in the database. Amongst others it uses this information to supports migration over time. + """ def __init__(self): self.entities: List[_Entity] = [] diff --git a/objectbox/model/properties.py b/objectbox/model/properties.py index e9f438b..d6a7d25 100644 --- a/objectbox/model/properties.py +++ b/objectbox/model/properties.py @@ -80,6 +80,8 @@ class IndexType(IntEnum): class Index: + """Property Index""" + # TODO HNSW isn't a `type` but HASH and HASH64 are, remove type member and make HashIndex and Hash64Index classes? def __init__(self, type: IndexType = IndexType.VALUE, uid: int = 0): @@ -136,8 +138,8 @@ class VectorDistanceType(IntEnum): Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest) """ - class HnswIndex: + """HNSW Index for Vector-Search""" def __init__(self, dimensions: int, neighbors_per_node: Optional[int] = None, @@ -290,16 +292,19 @@ def not_equals(self, value) -> PropertyQueryCondition: # ID property (primary key) class Id(_IntProperty): + """Id Property""" def __init__(self, id : int = 0, uid : int = 0, py_type: type = int): super(Id, self).__init__(py_type, id=id, uid=uid) # Bool property class Bool(_IntProperty): + """Boolean Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Bool, self).__init__(bool, type=PropertyType.bool, id=id, uid=uid, **kwargs) # String property with starts/ends_with class String(Property): + """String Property""" def __init__(self, id: int = 0, uid : int = 0, **kwargs): super(String, self).__init__(str, type=PropertyType.string, id=id, uid=uid, **kwargs) @@ -352,38 +357,47 @@ def less_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCond # Signed Integer Numeric Properties class Int8(_IntProperty): + """Integer 8-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Int8, self).__init__(int, type=PropertyType.byte, id=id, uid=uid, **kwargs) class Int16(_IntProperty): + """Integer 16-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Int16, self).__init__(int, type=PropertyType.short, id=id, uid=uid, **kwargs) class Int32(_IntProperty): + """Integer 32-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Int32, self).__init__(int, type=PropertyType.int, id=id, uid=uid, **kwargs) class Int64(_IntProperty): + """Integer 64-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Int64, self).__init__(int, type=PropertyType.long, id=id, uid=uid, **kwargs) # Floating-Point Numeric Properties class Float32(_NumericProperty): + """Floating-point 32-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Float32, self).__init__(float, type=PropertyType.float, id=id, uid=uid, **kwargs) class Float64(_NumericProperty): + """Floating-point 64-bit Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Float64, self).__init__(float, type=PropertyType.double, id=id, uid=uid, **kwargs) # Date Properties class Date(_IntProperty): + """Date Property""" def __init__(self, py_type = datetime, id : int = 0, uid : int = 0, **kwargs): super(Date, self).__init__(py_type, type=PropertyType.date, id=id, uid=uid, **kwargs) class DateNano(_IntProperty): + """Date (nano-second resolution) Property""" def __init__(self, py_type = datetime, id : int = 0, uid : int = 0, **kwargs): super(DateNano, self).__init__(py_type, type=PropertyType.dateNano, id=id, uid=uid, **kwargs) # Bytes Property class Bytes(_NumericProperty): + """Bytes blob Property""" def __init__(self, id: int = 0, uid : int = 0, **kwargs): super(Bytes, self).__init__(bytes, type=PropertyType.byteVector, id=id, uid=uid, **kwargs) @@ -414,6 +428,7 @@ def less_or_equal(self, value) -> PropertyQueryCondition: # Flex Property class Flex(Property): + """Flex dictionary-compatible Property""" def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Flex, self).__init__(Generic, type=PropertyType.flex, id=id, uid=uid, **kwargs) def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> PropertyQueryCondition: @@ -426,29 +441,36 @@ def __init__(self, py_type : Type, **kwargs): super(_VectorProperty, self).__init__(py_type, **kwargs) class BoolVector(_VectorProperty): + """Boolean Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(BoolVector, self).__init__(np.ndarray, type=PropertyType.boolVector, id=id, uid=uid, **kwargs) class Int8Vector(_VectorProperty): + """Integer 8-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int8Vector, self).__init__(bytes, type=PropertyType.byteVector, id=id, uid=uid, **kwargs) class Int16Vector(_VectorProperty): + """Integer 16-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int16Vector, self).__init__(np.ndarray, type=PropertyType.shortVector, id=id, uid=uid, **kwargs) class CharVector(_VectorProperty): + """Char 16-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(CharVector, self).__init__(np.ndarray, type=PropertyType.charVector, id=id, uid=uid, **kwargs) class Int32Vector(_VectorProperty): + """Integer 32-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int32Vector, self).__init__(np.ndarray, type=PropertyType.intVector, id=id, uid=uid, **kwargs) class Int64Vector(_VectorProperty): + """Integer 64-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int64Vector, self).__init__(np.ndarray, type=PropertyType.longVector, id=id, uid=uid, **kwargs) class Float32Vector(_VectorProperty): + """Floating-point 32-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Float32Vector, self).__init__(np.ndarray, type=PropertyType.floatVector, id=id, uid=uid, **kwargs) def nearest_neighbor(self, query_vector, element_count: int) -> PropertyQueryCondition: @@ -457,6 +479,7 @@ def nearest_neighbor(self, query_vector, element_count: int) -> PropertyQueryCon return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NEAREST_NEIGHBOR, args) class Float64Vector(_VectorProperty): + """Floating-point 64-bit Vector Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Float64Vector, self).__init__(np.ndarray, type=PropertyType.doubleVector, id=id, uid=uid, **kwargs) @@ -465,33 +488,41 @@ def __init__(self, **kwargs): super(_ListProperty, self).__init__(list, **kwargs) class BoolList(_ListProperty): + """Boolean List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(BoolList, self).__init__(type=PropertyType.boolVector, id=id, uid=uid, **kwargs) class Int8List(_ListProperty): + """Integer 8-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int8List, self).__init__(type=PropertyType.byteVector, id=id, uid=uid, **kwargs) class Int16List(_ListProperty): + """Integer 16-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int16List, self).__init__(type=PropertyType.shortVector, id=id, uid=uid, **kwargs) class Int32List(_ListProperty): + """Integer 32-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int32List, self).__init__(type=PropertyType.intVector, id=id, uid=uid, **kwargs) class Int64List(_ListProperty): + """Integer 64-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Int64List, self).__init__(type=PropertyType.longVector, id=id, uid=uid, **kwargs) class Float32List(_ListProperty): + """Floating-point 32-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Float32List, self).__init__(type=PropertyType.floatVector, id=id, uid=uid, **kwargs) class Float64List(_ListProperty): + """Floating-point 64-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(Float64List, self).__init__(type=PropertyType.doubleVector, id=id, uid=uid, **kwargs) class CharList(_ListProperty): + """Char 16-bit List Property""" def __init__(self, id: int = 0, uid: int = 0, **kwargs): super(CharList, self).__init__(type=PropertyType.charVector, id=id, uid=uid, **kwargs) diff --git a/objectbox/objectbox.py b/objectbox/objectbox.py index 1a6dff7..99d405a 100644 --- a/objectbox/objectbox.py +++ b/objectbox/objectbox.py @@ -17,6 +17,7 @@ from warnings import warn class ObjectBox(objectbox.store.Store): + """*Deprecated Interface*""" def __init__(self, c_store): """This throws a deprecation warning on initialization.""" warn(f'{self.__class__.__name__} will be deprecated, use Store from objectbox.store.', DeprecationWarning, stacklevel=2) diff --git a/objectbox/store.py b/objectbox/store.py index de9d109..2318254 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -28,6 +28,7 @@ class Store: + """ObjectBox Database""" def __init__(self, model: Optional[Union[Model, str]] = "default", model_json_file: Optional[str] = None, @@ -238,6 +239,7 @@ def json_file_inside_module_path(module: Optional[ModuleType]) -> Optional[str]: return model_json_file def __del__(self): + """Destructor closes database.""" self.close() def box(self, entity: _Entity) -> 'objectbox.Box': @@ -250,12 +252,15 @@ def box(self, entity: _Entity) -> 'objectbox.Box': return objectbox.Box(self, entity) def read_tx(self): + """Returns a read-only transaction.""" return objectbox.transaction.read(self) def write_tx(self): + """Returns a write transaction.""" return objectbox.transaction.write(self) def close(self): + """Close database.""" c_store_to_close = self._c_store if c_store_to_close: self._c_store = None diff --git a/objectbox/version.py b/objectbox/version.py index ff95645..38b9d96 100644 --- a/objectbox/version.py +++ b/objectbox/version.py @@ -16,6 +16,7 @@ class Version: + """Version""" def __init__(self, major: int, minor: int, patch: int, alpha: Optional[int] = None, beta: Optional[int] = None, From af0e4dd2a4a1a0cd587250bfecaa2510143b78bd Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 12:27:52 +0200 Subject: [PATCH 08/28] objectbox top-level: resorted elements for API reference #52 --- objectbox/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 374993c..5b224ed 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -13,13 +13,13 @@ # limitations under the License. +from objectbox.store import Store from objectbox.box import Box -from objectbox.builder import Builder from objectbox.model import Model, Entity, Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType -from objectbox.store import Store -from objectbox.objectbox import ObjectBox from objectbox.c import NotFoundException, version_core, DebugFlags from objectbox.version import Version +from objectbox.builder import Builder +from objectbox.objectbox import ObjectBox __all__ = [ 'Box', From 3b9d306ad5f9ecb2d3bfca1849b48ec4dd14f388 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 12:28:17 +0200 Subject: [PATCH 09/28] entity decorator: fixed type signature and updated docstring #52 --- objectbox/model/entity.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/objectbox/model/entity.py b/objectbox/model/entity.py index 6c486db..7fb7e51 100644 --- a/objectbox/model/entity.py +++ b/objectbox/model/entity.py @@ -278,10 +278,15 @@ def _unmarshal(self, data: bytes): obx_models_by_name: Dict[str, List[_Entity]] = {} -def Entity(uid: int = 0, model: str = "default") -> Callable[[Type], _Entity]: - """ Entity decorator that wraps _Entity to allow @Entity(id=, uid=); i.e. no class arguments. """ - - def wrapper(class_): +def Entity(uid: int = 0, model: str = "default") -> _Entity: + """ + Entity decorator for user classes using syntax ``@Entity([uid=])`` + + Wraps user classes and returns a ``_Entity`` wrapper. + Use allow @Entity(id=, uid=); i.e. no class arguments. + """ + + def wrapper(class_) -> Callable[[Type], _Entity]: # Also allow defining properties as class members; we'll instantiate them here class_members = inspect.getmembers(class_, lambda a: (inspect.isclass(a) and issubclass(a, Property))) for name, member_type in class_members: From 46cf0c609dc71800c51e29faa60cd9f6c5bdf526 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 18:30:25 +0200 Subject: [PATCH 10/28] cleanup imports under objectbox.model #52 This improves significantly the API docs; it basically gives access to Propertys and its super classes to browse available conditionals. --- objectbox/__init__.py | 6 ++++-- objectbox/builder.py | 2 +- objectbox/model/idsync.py | 2 +- objectbox/store_options.py | 2 +- tests/common.py | 4 ++-- tests/test_idsync.py | 2 ++ 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 5b224ed..ece0ede 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -15,7 +15,9 @@ from objectbox.store import Store from objectbox.box import Box -from objectbox.model import Model, Entity, Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType +from objectbox.model.entity import Entity +from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType +from objectbox.model.model import Model from objectbox.c import NotFoundException, version_core, DebugFlags from objectbox.version import Version from objectbox.builder import Builder @@ -68,7 +70,7 @@ # Python binding version version = Version(4, 0, 0) -"""ObjectBox Python version""" +"""ObjectBox Python package version""" def version_info(): """Returns a string with Python and core version information.""" diff --git a/objectbox/builder.py b/objectbox/builder.py index cad3933..af316d0 100644 --- a/objectbox/builder.py +++ b/objectbox/builder.py @@ -14,7 +14,7 @@ from objectbox.c import * -from objectbox.model import Model +from objectbox.model.model import Model from objectbox.store import Store from objectbox.store_options import StoreOptions from warnings import warn diff --git a/objectbox/model/idsync.py b/objectbox/model/idsync.py index f286191..754019d 100644 --- a/objectbox/model/idsync.py +++ b/objectbox/model/idsync.py @@ -1,7 +1,7 @@ import random from typing import * from objectbox.logger import logger -from objectbox.model import Model +from objectbox.model.model import Model from objectbox.model.entity import _Entity from objectbox.model.properties import Property, Index, HnswIndex from objectbox.model.iduid import IdUid diff --git a/objectbox/store_options.py b/objectbox/store_options.py index c929682..193ae66 100644 --- a/objectbox/store_options.py +++ b/objectbox/store_options.py @@ -1,5 +1,5 @@ from objectbox.c import * -from objectbox.model import Model +from objectbox.model.model import Model class StoreOptions: diff --git a/tests/common.py b/tests/common.py index 5a82e2e..0108d7f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -36,7 +36,7 @@ def create_test_store(db_path: str = "testdata", clear_db: bool = True) -> objec def assert_equal_prop(actual, expected, default): - if isinstance(expected, objectbox.model.Property): + if isinstance(expected, objectbox.model.properties.Property): assert (actual == default) else: assert (actual == expected) @@ -49,7 +49,7 @@ def assert_equal_prop_vector(actual, expected, default): # compare approx values def assert_equal_prop_approx(actual, expected, default): - if isinstance(expected, objectbox.model.Property): + if isinstance(expected, objectbox.model.properties.Property): assert (actual == default) else: assert (pytest.approx(actual) == expected) diff --git a/tests/test_idsync.py b/tests/test_idsync.py index 9b5298a..3ed67f3 100644 --- a/tests/test_idsync.py +++ b/tests/test_idsync.py @@ -4,8 +4,10 @@ from numpy.testing import assert_approx_equal from objectbox import * from objectbox.model import * +from objectbox.model.properties import Property, PropertyType from objectbox.model.entity import _Entity from objectbox.model.idsync import sync_model +from objectbox.model.iduid import IdUid from objectbox.c import CoreException from os import path From ba8be8e14ef91b2a8e978160f1064fabb53626a1 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Wed, 29 May 2024 12:28:44 +0200 Subject: [PATCH 11/28] objectbox: added docstrings #52 - box/entity/store - properties: conditions and hnsw components - toplevel: - module docstring - added PropertyQueryCondition and HnswFlags - c: DebugFlags documentation #52 - PropertyQueryCondition: docstring improvements - VectorDistanceType: changed enum elements docstrings --- objectbox/__init__.py | 8 +- objectbox/box.py | 8 +- objectbox/c.py | 18 +++++ objectbox/condition.py | 8 +- objectbox/model/entity.py | 11 ++- objectbox/model/properties.py | 137 ++++++++++++++++++++++++++++------ objectbox/store.py | 7 +- 7 files changed, 166 insertions(+), 31 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index ece0ede..e388461 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -12,14 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""ObjectBox Python Bindings Public API""" from objectbox.store import Store from objectbox.box import Box from objectbox.model.entity import Entity -from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType +from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model from objectbox.c import NotFoundException, version_core, DebugFlags from objectbox.version import Version +from objectbox.condition import PropertyQueryCondition from objectbox.builder import Builder from objectbox.objectbox import ObjectBox @@ -65,7 +67,9 @@ 'NotFoundException', 'version', 'version_info', - 'DebugFlags' + 'DebugFlags', + 'PropertyQueryCondition', + 'HnswFlags', ] # Python binding version diff --git a/objectbox/box.py b/objectbox/box.py index 8cf5941..1c9607f 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -31,11 +31,13 @@ def __init__(self, store: Store, entity: _Entity): self._c_box = obx_box(store._c_store, entity._id) def is_empty(self) -> bool: + """Returns true if box is empty (i.e. no objects of entity type are available).""" is_empty = ctypes.c_bool() obx_box_is_empty(self._c_box, ctypes.byref(is_empty)) return bool(is_empty.value) def count(self, limit: int = 0) -> int: + """Returns the count of existing objects.""" count = ctypes.c_uint64() obx_box_count(self._c_box, limit, ctypes.byref(count)) return int(count.value) @@ -110,6 +112,7 @@ def _put_many(self, objects) -> None: self._entity._set_object_id(objects[k], ids[k]) def get(self, id: int): + """Get object by given Id or None if not found.""" with self._store.read_tx(): c_data = ctypes.c_void_p() c_size = ctypes.c_size_t() @@ -123,6 +126,7 @@ def get(self, id: int): return self._entity._unmarshal(data) def get_all(self) -> list: + """Get all objects.""" with self._store.read_tx(): # OBX_bytes_array* c_bytes_array_p = obx_box_get_all(self._c_box) @@ -143,6 +147,7 @@ def get_all(self) -> list: obx_bytes_array_free(c_bytes_array_p) def remove(self, id_or_object) -> bool: + """Remove object by id or object.""" if isinstance(id_or_object, self._entity._user_type): id = self._entity._get_object_id(id_or_object) else: @@ -155,6 +160,7 @@ def remove(self, id_or_object) -> bool: return True def remove_all(self) -> int: + """Removes all objects and returns number of removed.""" count = ctypes.c_uint64() obx_box_remove_all(self._c_box, ctypes.byref(count)) return int(count.value) @@ -166,7 +172,7 @@ def query(self, condition: Optional[QueryCondition] = None) -> QueryBuilder: If given, applies the given high-level condition to the new QueryBuilder object. Useful for a user-friendly API design; for example: - ``box.query(name_property.equals("Johnny")).build()`` + ``box.query(MyEntity.name.equals("Johnny")).build()`` """ qb = QueryBuilder(self._store, self) if condition is not None: diff --git a/objectbox/c.py b/objectbox/c.py index 7c005bd..8579070 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -86,15 +86,33 @@ def shlib_name(library: str) -> str: OBXBackupRestoreFlags = ctypes.c_int class DebugFlags(IntEnum): + """Debug flags""" + NONE = 0, + LOG_TRANSACTIONS_READ = 1, + """ Log read transactions """ + LOG_TRANSACTIONS_WRITE = 2, + """ Log write transactions """ + LOG_QUERIES = 3, + """ Log queries """ + LOG_QUERY_PARAMETERS = 8, + """ Log query parameters """ + LOG_ASYNC_QUEUE = 16, + """ Log async queue """ + LOG_CACHE_HITS = 32, + """ Log cache hits """ + LOG_CACHE_ALL = 64, + """ Log cache hits """ + LOG_TREE = 128 + """ Log tree operations """ class OBX_model(ctypes.Structure): diff --git a/objectbox/condition.py b/objectbox/condition.py index 90d32f4..d9b9364 100644 --- a/objectbox/condition.py +++ b/objectbox/condition.py @@ -11,10 +11,12 @@ class QueryCondition: def and_(self, other: QueryCondition) -> QueryCondition: + """*and* logic condition or using ``&`` operator""" return LogicQueryCondition(self, other, LogicQueryConditionOp.AND) __and__ = and_ def or_(self, other: QueryCondition) -> QueryCondition: + """*or* logic condition or using ``|`` operator""" return LogicQueryCondition(self, other, LogicQueryConditionOp.OR) __or__ = or_ @@ -74,7 +76,11 @@ class PropertyQueryConditionOp(Enum): class PropertyQueryCondition(QueryCondition): - """ A QueryCondition describing an operation to be applied on a property (e.g. name == "John", age == 24) """ + """ + Query condition + + Query conditions describe operations to be applied on a property (e.g. name == "John", age == 24) + """ _OP_MAP: Dict[PropertyQueryConditionOp, str] = { PropertyQueryConditionOp.EQ: "_apply_eq", diff --git a/objectbox/model/entity.py b/objectbox/model/entity.py index 7fb7e51..9e6d46a 100644 --- a/objectbox/model/entity.py +++ b/objectbox/model/entity.py @@ -282,8 +282,15 @@ def Entity(uid: int = 0, model: str = "default") -> _Entity: """ Entity decorator for user classes using syntax ``@Entity([uid=])`` - Wraps user classes and returns a ``_Entity`` wrapper. - Use allow @Entity(id=, uid=); i.e. no class arguments. + Example:: + + @Entity() + class MyEntity: + id = Id + name = String(index=Index) + + + Wraps the given user classes as an ``_Entity`` helper class. """ def wrapper(class_) -> Callable[[Type], _Entity]: diff --git a/objectbox/model/properties.py b/objectbox/model/properties.py index d6a7d25..d7950d2 100644 --- a/objectbox/model/properties.py +++ b/objectbox/model/properties.py @@ -91,10 +91,12 @@ def __init__(self, type: IndexType = IndexType.VALUE, uid: int = 0): @property def id(self): + """ Index Id """ return self.iduid.id @property def uid(self): + """ Index UId """ return self.iduid.uid def has_uid(self): @@ -102,44 +104,78 @@ def has_uid(self): class HnswFlags(IntEnum): + """ + Vector-Search HNSW Index Flags + """ + NONE = 0 + """ + + """ + DEBUG_LOGS = 1 + """ + Enables debug logs. + """ + DEBUG_LOGS_DETAILED = 2 + """ + Enables "high volume" debug logs, e.g. individual gets/puts. + """ + VECTOR_CACHE_SIMD_PADDING_OFF = 4 + """ + Padding for SIMD is enabled by default, which uses more memory but may be faster. This flag turns it off. + """ + REPARATION_LIMIT_CANDIDATES = 8 - + """ + If the speed of removing nodes becomes a concern in your use case, you can speed it up by setting this flag. + By default, repairing the graph after node removals creates more connections to improve the graph's quality. + The extra costs for this are relatively low (e.g. vs. regular indexing), and thus the default is recommended. + """ class VectorDistanceType(IntEnum): + """ + Vector-search distance computation strategy type + """ + UNKNOWN = OBXVectorDistanceType_UNKNOWN + """ + Not a real type, just best practice (e.g. forward compatibility) + """ + EUCLIDEAN = OBXVectorDistanceType_EUCLIDEAN + """ + The default; typically 'euclidean squared' internally." + """ + COSINE = OBXVectorDistanceType_COSINE + """ + Cosine similarity compares two vectors irrespective of their magnitude (compares the angle of two vectors). + Often used for document or semantic similarity. + Value range: 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction) + """ + DOT_PRODUCT = OBXVectorDistanceType_DOT_PRODUCT + """ + For normalized vectors (vector length == 1.0), the dot product is equivalent to the cosine similarity. + Because of this, the dot product is often preferred as it performs better. + Value range (normalized vectors): 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction) + """ + DOT_PRODUCT_NON_NORMALIZED = OBXVectorDistanceType_DOT_PRODUCT_NON_NORMALIZED - - -VectorDistanceType.UNKNOWN.__doc__ = "Not a real type, just best practice (e.g. forward compatibility)" -VectorDistanceType.EUCLIDEAN.__doc__ = "The default; typically 'euclidean squared' internally." -VectorDistanceType.COSINE.__doc__ = """ -Cosine similarity compares two vectors irrespective of their magnitude (compares the angle of two vectors). -Often used for document or semantic similarity. -Value range: 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction) -""" -VectorDistanceType.DOT_PRODUCT.__doc__ = """ -For normalized vectors (vector length == 1.0), the dot product is equivalent to the cosine similarity. -Because of this, the dot product is often preferred as it performs better. -Value range (normalized vectors): 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction) -""" -VectorDistanceType.DOT_PRODUCT_NON_NORMALIZED.__doc__ = """ -A custom dot product similarity measure that does not require the vectors to be normalized. -Note: this is no replacement for cosine similarity (like DotProduct for normalized vectors is). -The non-linear conversion provides a high precision over the entire float range (for the raw dot product). -The higher the dot product, the lower the distance is (the nearer the vectors are). -The more negative the dot product, the higher the distance is (the farther the vectors are). -Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest) -""" + """ + A custom dot product similarity measure that does not require the vectors to be normalized. + Note: this is no replacement for cosine similarity (like DotProduct for normalized vectors is). + The non-linear conversion provides a high precision over the entire float range (for the raw dot product). + The higher the dot product, the lower the distance is (the nearer the vectors are). + The more negative the dot product, the higher the distance is (the farther the vectors are). + Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest) + """ class HnswIndex: - """HNSW Index for Vector-Search""" + """Vector-Search HNSW Property Index""" def __init__(self, dimensions: int, neighbors_per_node: Optional[int] = None, @@ -149,6 +185,30 @@ def __init__(self, reparation_backlink_probability: Optional[float] = None, vector_cache_hint_size_kb: Optional[float] = None, uid: int = 0): + """ + :param dimensions: + Vector dimensionality. + :param neighbors_per_node: + Maximum number of neighbors per node (aka "M"). + Higher number increases the graph connectivity which can lead to better results, but higher resources usage. + If no value is set, a default value taken (currently 30). + Try e.g. 16 for faster but less accurate results, or 64 for more accurate results. + :param indexing_search_count: + Maximum number of neighbors searched while indexing (aka "efConstruction"). + If not set, internally default value is currently set to 100, which can change in future version. + The default value serves as a starting point that can likely be optimized for specific datasets and use cases. + The higher the value, the more accurate the search, but the longer the indexing will take. + If indexing time is not a major concern, a value of at least 200 is recommended to improve search quality. blah. + :param flags: + Set flags. + :param distance_type: + Set distance strategy type. + :param reparation_backlink_probability: + When repairing the graph after a node was removed, this gives the probability of adding backlinks to the repaired + neighbors. The default is 1.0 (aka "always") as this should be worth a bit of extra costs as it improves the graph's quality. + :param vector_cache_hint_size_kb: + Vector cache hint size. This is a non-binding hint of the maximum size of the vector cache in KB (default: 2097152 or 2 GB/GiB). + """ self.dimensions = dimensions self.neighbors_per_node = neighbors_per_node self.indexing_search_count = indexing_search_count @@ -161,13 +221,16 @@ def __init__(self, @property def id(self): + """ Index Id """ return self.iduid.id @property def uid(self): + """ Index Uid """ return self.iduid.uid def has_uid(self): + """ Returns true if Uid is set. """ return self.uid != 0 @@ -189,16 +252,20 @@ def __init__(self, pytype: Type, uid: int = 0, **kwargs): @property def id(self): + """ Property Id """ return self.iduid.id @property def uid(self): + """ Property Uid """ return self.iduid.uid def has_uid(self): + """ Returns true if property has a valid Uid """ return self.uid != 0 def is_id(self) -> bool: + """ Check if Property is an Id Property. """ return isinstance(self, Id) def on_sync(self): @@ -248,26 +315,31 @@ def __init__(self, py_type : Type, **kwargs): super(_NumericProperty, self).__init__(py_type, **kwargs) def greater_than(self, value) -> PropertyQueryCondition: + """ *Greater-than* (``>``) condition to be passed to :func:`objectbox.Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args) def greater_or_equal(self, value) -> PropertyQueryCondition: + """ *Greater-or-equal* (``>=``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args) def less_than(self, value) -> PropertyQueryCondition: + """ *Less-than* (``<``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args) def less_or_equal(self, value) -> PropertyQueryCondition: + """ *Less-or-equal* (``<=``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args) def between(self, a, b) -> PropertyQueryCondition: + """ *Between* a and b (``a <= x <= b``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'a': a, 'b': b} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.BETWEEN, args) @@ -280,11 +352,13 @@ def __init__(self, py_type : Type, **kwargs): super(_IntProperty, self).__init__(py_type, **kwargs) def equals(self, value) -> PropertyQueryCondition: + """ *Equals* (``==``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args) def not_equals(self, value) -> PropertyQueryCondition: + """ *Not equals* (``!=``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_EQ, args) @@ -309,46 +383,55 @@ def __init__(self, id: int = 0, uid : int = 0, **kwargs): super(String, self).__init__(str, type=PropertyType.string, id=id, uid=uid, **kwargs) def starts_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *starts with* (string-prefix) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.STARTS_WITH, args) def ends_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *ends with* (string-suffix) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ENDS_WITH, args) def equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *equals* (``==``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args) def not_equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *not-equals* (``~=``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_EQ, args) def contains(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *contains string* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.CONTAINS, args) def greater_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *greater-than* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args) def greater_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *greater-or-equal* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args) def less_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *less-than* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args) def less_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *less-or-equal* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args) @@ -402,26 +485,31 @@ def __init__(self, id: int = 0, uid : int = 0, **kwargs): super(Bytes, self).__init__(bytes, type=PropertyType.byteVector, id=id, uid=uid, **kwargs) def equals(self, value) -> PropertyQueryCondition: + """ *byte-string equals* (``==``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args) def greater_than(self, value) -> PropertyQueryCondition: + """ *byte-string greater-than* (``==``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args) def greater_or_equal(self, value) -> PropertyQueryCondition: + """ *byte-string greater-or-equal* (``>=``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args) def less_than(self, value) -> PropertyQueryCondition: + """ *byte-string less-than* (``<``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args) def less_or_equal(self, value) -> PropertyQueryCondition: + """ *byte-string less-or-equal* (``<``) condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'value': value} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args) @@ -432,6 +520,7 @@ class Flex(Property): def __init__(self, id : int = 0, uid : int = 0, **kwargs): super(Flex, self).__init__(Generic, type=PropertyType.flex, id=id, uid=uid, **kwargs) def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> PropertyQueryCondition: + """ *contains key/valuel* condition to be passed to :func:`Box.query` """ self._assert_ids_assigned() args = {'key': key, 'value': value, 'case_sensitive': case_sensitive} return PropertyQueryCondition(self.id, PropertyQueryConditionOp.CONTAINS_KEY_VALUE, args) diff --git a/objectbox/store.py b/objectbox/store.py index 2318254..23a55e5 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -61,6 +61,10 @@ def __init__(self, :param model: Database schema model. + :param model_json_file: + ObjectBox model JSON file. If not set defaults to locate + user-module (from call stack) and use its directory location to + use `objectbox-model.json` file. :param directory: Store directory. Defaults to "objectbox". Use prefix "memory:" to open an in-memory database, e.g. "memory:myapp" @@ -72,7 +76,7 @@ def __init__(self, Recommended only if stricter accurate limit is required. Data size must be below database size. :param file_mode: - Unix-style file mode options. Defaults to "int('644',8)". + Unix-style file mode options. Defaults to ``int('644',8)``. This option is ignored on Windows platforms. :param max_readers: Maximum number of readers (related to read transactions). @@ -247,6 +251,7 @@ def box(self, entity: _Entity) -> 'objectbox.Box': Open a box for an entity. :param entity: + :type entity: _Entity Entity type of the model """ return objectbox.Box(self, entity) From b5e00639115d3e5cfc8b585d22ff6a5114743d7f Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Fri, 31 May 2024 11:54:22 +0200 Subject: [PATCH 12/28] docs: added external links to sidebar #52 --- doc/conf.py | 2 +- doc/index.rst | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 568aab5..4c38b00 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -41,7 +41,7 @@ ] # autoapi_ignore = ['*migrations'] # autoapi_root = 'autoapi' -# autoapi_add_toctree_entry = True +autoapi_add_toctree_entry = False # default: True autoapi_python_class_content = "both" # default: "class" # autoapi_member_order = 'bysource' # autoapi_python_use_implicit_namespaces = False diff --git a/doc/index.rst b/doc/index.rst index dc50ecd..2b54691 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,7 +4,12 @@ ObjectBox Python Bindings .. toctree:: :maxdepth: 1 :caption: Contents: - + + Python API Reference + ObjectBox Homepage + ObjectBox docs + GitHub + Further links ============= From eeab1504edd2323e8fe992eec0006930869f496a Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Fri, 31 May 2024 13:52:32 +0200 Subject: [PATCH 13/28] docs: added logo, changed title from Bindings to API #52 --- doc/_static/logo-white-500px.png | Bin 0 -> 4891 bytes doc/conf.py | 5 +++-- doc/index.rst | 4 ++-- objectbox/__init__.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 doc/_static/logo-white-500px.png diff --git a/doc/_static/logo-white-500px.png b/doc/_static/logo-white-500px.png new file mode 100644 index 0000000000000000000000000000000000000000..de231b77e5760678c3729733b0053d2011acd225 GIT binary patch literal 4891 zcmaJ^XIN8Nw~m4!AksvN0uc}dB#_V~5TvSfBvc_NC3KQV0s(^bB1J)ZFCuNE2}7?6 zO2;VDn@CF#sR2hIa^rk6^W7hJ?*4K1TKie=yWX|-I_G&#jET{0R%U)?006+Mdq>+8 z060aV*?wo~XiquT?P}V_htsjb!O_k*yuCLPpy7meKmv8W>|KziNP8#$hy6%Z0Dw-! z&CCjCWoQ6(M0fy4nFkS=a0nDAC>r!dgX2_|f*U?^*d z(L%bq-3jnU!UK%V90NQYm7RodssUB~pfmz6B+ee_=jDmQLj7REfAT_U_OD|ZVc?%3 zI1iZce?(asngF%X-bkPVNKx7mEPn&2qznQp%FD_rUI#*C!OAkSH)Oza(z0NvtRhrF z3Ha9%re*VXa)z2}-});T&4UTM;&2$Kj0_%+2jS&FXm1x8u(C3Z10n-~NYf&uvHmEW zy`MA+EAlskHWKUT?S{d*p;5qJjP?#_9~?}Wru080cwzpbMPdJH6Rlw~e)bp{Fi7@S zNq+|#8vg&GUS9u1V{xX)|LXleiLqw>7^I9T5{vfncBEaLv&gSh7^s#v(jJHQHbbL5 z|L&rRD;kH!x}q^a2u%|3nxz}c35~}}{0T5LgzBQOID3>MQdb)$Orrp~xj8{~l(c1U zY07~W!Af8-SV!~5ts6=jnh+&9#anWU+FEjdW3|zaK3+%^?r*Hqf3c8%#r~QFFAOcS zHqzV87wL4%8|?-Bvu3E+9RMZwUl~gM-7w#KiIOabI6wVPRoYQ`6eo8i_=Tii%oXTuezx zp~0Ka$!gKE2;fYO-~f5dce2pTX(~(ki+)rHSB8l}+j2W5v8N$&@Sf$*1vmh3{zO+> z!_04b0}+>0B+1VDJgg}Ey=#us&l9EU7~t6_&%w>*0s$T#9+lUaqDRr&xPWL;j+R$y zaYlAO{jbnXns;>^Hofl5a9}&UdER%b78uuRi`3b4SCDJ zHTe&wL!OoF@XAG)-s;-g!kIZSmzy_(*(jghCos+p3IW=;_x8T4qp8QoFrw5Kank!x zyTczp!V3r`Cotl({QR4Y(Ea_iPR0O|#tET7NsvM_H{v{l*>PKK!LgqZA-jzsK@S6O zJq>&MW-rsq2#o{aKF_i5UP6 zYwk>CGp?qFX>#*kF0F`7N)MU0-qd}bl;o{P6grOZAY#YjE)rTx;Mj4C17??vu{WYZ+)S=PpKAYkjYvr7Q;B$VfuzQCbg?8|=uC_8n+|b~qq_x1q8m0DdC@}j zEn5+fxYjKXl?#rrR~P65b~FrRR0X%59fYed1aI@k5P=^~F0G1prW(uI#alFX(JCx%GJa`HVbjxZ^and$BmO!__Qelub~LN74~2%PvXb z6Vs&=SmsYf7~H3a4#z^E&nOw9*9TTxix>>Xelz4eJ*P%@jm2|KE=fY_!8h$x$awVG z;_=utx)*ngtRO|xoIw>2mmU_%#)-xZ=W4n?Le}Q5Vy0w8BG@my*jX%KJ9IUvj!<73 zCse>)s|;sBVLv}R4X z-)VaYqr18nTM5`YefS{%w_|NHeV4%1hGs8kVU7-*M%T!fOkOtw^hi<9VnmCN{8`F& zg+A1fle6d@!rB~}?si|;cm-kGjvNTTCAy))?phuA0Vh43T3MB`Wk4#C+SQ;}cG4=U z?c25^N_6-ZZ$7)h+PdDo9~i;UBy<~Y5k}_eh|skwep5FIfDUhdA3+}8XbiQ-2<@Tx z(1A$AV_u)`shb9Z;zO#E|heCfWyksh!8;^sTJ*nS?hCST}a z%&}|Y?(dG1sb^CK6rGKOoXsYaNb6#lq^U=Vd^0<0#6@3T&I}`}#kV{sbtR;V{t~ui z)H?l}f3pG$Kww6L2eDk7x%1qa5WhKn%!4V$x<7~#n=h85HJmnPbOm%4w{5x z)S_P-^n@|0P7#(ZGtSFRURJnz^tehVfEvFJ7}ZgW6?j8QJMGox6pz{D zfsoM$EX|%{?U5I+lQI}&GziVl%ndEe>`~E(Ug#gcWU!DG!M*yG!JRlXfLk=G(aH!4 z^^BYpwQ7bSgmX>{Dt>n(Ql!2oYF5WIo9wj;~l^1=Vu#dsLX1^@{%_1Bi26i zO611@UvYn+33`Zvf?dKb8U{7OiEpV z@-h+e$PH#_&x44$imrf1L=$#7VJ!opZ{qa=&wr00d*-N61r$UB?gRzADM`-~!FNCK zjEg5erF^ZwUMG?kCi=%TQ^2~}Y*BV{&3z0`hWEov#)wu!-sRU6Gku2siZuSgty(hm zI4K^#n)m8cW@YN#uH~#4tL+uNxn~896tbu%I)=5WS6Na%oXnB%5vaQf%jb0W-7)X%rPO^%?$cOIN+{)Rl3>1E2cSMlKl|4Hm9m(FA z)BkH`&8)81V;%E%3BXJ_#Laoh>r7K-m_qE25AXEykcZYXCGeuKaT{W0$*y}h^FT^Q zeP;mNag3P+ECm_oGYx7mz8S0w1ll5ttUCmmK^9Z=?z%8VTh&3F$0jUqU2~av&%$|z zn}}8Di28o|e(p4%WD6HA1nykof)&5c2Hh}7*x09tA&62o>T7qL`t21F$%3aE2)X3a zu&OiNYf9+yZg1{3_!^COhv8*`NKJ!FaT`Md#2nRR6`y%EF3 z#YY8NC`j$^vES^Wp<_q!Wbn~yu$Xbx+;X3rSKJs}F&8w0LolvFNwTBn#JK&o^bfZ+ zLMNmqzt_rypMP>kvhh&1!YTkQ=yTtlH57zU$ExHwS*{vk`>5j!*Fx-^usGt?ap@s>7uhOlqh2fVoV zGC+X4l&v5~0in#d$eR_-M+(q;YB8Q)9(cC4#=ngPN0gr6g5e0_*B7!KIejTSWeOtV z7Lk>1-I3%hi-%R0DtgHhpEsUP1bzN^Hu0I%Yr6^aI8$_aQ$@&mjsf19g`kQHHY4qL zixOkJU?O2F?GmJchG6$h(EG|j&~+WqcuYEfS`q^kq?odL&(T3=*^`^g(&`17<7R}e zt@vk<$0{l}LL{@nrFT!*4-7UDc>=Nw!XM8z78K{;=5dim=89eW;7j9ONtT^)NdNYSk5C zip$vpQOu9pg-gF2fQW_ZwScR~0=*pD%+FU}uyI?tzHfnOUXCnDn zB;in%`wYjc6nWo zDP0Drf&EDkOXKIJPjWqdPsk4IpjaN!dGE>1$D>EPzUc+7L#(EdHO(cUUedM4taR2! zBgfrbCS^fa&Sw!!0V3Pc~?X(qH&-MVXRIPDkCv-AV z7{7FWYUfgCFc07{ZR8CsS;I$qg3$M`AA|=Dv|{qk zWkresNl$!91|Nsh*r>Um$HwHB2Yra&}tnz&D ztLCUkGz$*&m6-mx3&ooCj5eD0YVIB!bRLZOOlYv5eUtjYg6>KBC@Ff7qs!dh^S>XLwdWt&jPB=&Zlx zGGNPN?<+ap@_s3KJZ9C7(%Y<8mtvh?p>$W;^PUR(A+oiQj$A7_ELS<01 zS-xsQtKPy^qb;Gn%7IP<*B*Ww&TFWBF_xnRdd2FwW5jt!P>CyVVRuHHgkpw|SL83h zhI)JBF42DI?ZsfvUz^ze`fQ#0#OV>~_{s9}mDj{#cQU;=a&<9>#1=8na&Ga>;~|^P z1&wqchM4G&{R_+UtS7APL}A9=u3ExZ5j>hyi_%ct-PmLj$W-0lu-jlojBmqP+p^zB z&T}6UHI~5`boxpMClumuHig&%v`6$x^z(BY^3$8q|KRKCz9H7!cSC{(-sQdM#3x~z1ZH%e8 z$_MO~kqEU1Oy78FTTcmKcZFhXNsN@jZZ`DmUhb@kBA)x Date: Fri, 31 May 2024 14:24:34 +0200 Subject: [PATCH 14/28] docs: added API overview #52 --- doc/index.rst | 3 +- doc/overview.rst | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/setup.sh | 1 + 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 doc/overview.rst diff --git a/doc/index.rst b/doc/index.rst index aa1cac7..a013862 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,7 +5,8 @@ ObjectBox Python API :maxdepth: 1 :caption: Contents: - Python API Reference + overview + API Reference ObjectBox Homepage ObjectBox docs GitHub diff --git a/doc/overview.rst b/doc/overview.rst new file mode 100644 index 0000000..679aee7 --- /dev/null +++ b/doc/overview.rst @@ -0,0 +1,82 @@ +API Overview +============ + + .. currentmodule:: objectbox + +Entity Decorator +---------------- + + .. autosummary:: + :nosignatures: + + Entity + + +Property Types +-------------- + + .. autosummary:: + :nosignatures: + + Id + Bool + String + Bytes + Int8 + Int16 + Int32 + Int64 + Float32 + Float64 + Date + DateNano + Flex + BoolVector + CharVector + Int8Vector + Int16Vector + Int32Vector + Int64Vector + Float32Vector + Float64Vector + BoolList + CharList + Int8List + Int16List + Int32List + Int64List + Float32List + Float64List + + +Index Types and Flags +--------------------- + + .. autosummary:: + :nosignatures: + + Index + HnswIndex + HnswFlags + VectorDistanceType + +Classes +------- + + .. autosummary:: + :nosignatures: + + Store + Box + Model + PropertyQueryCondition + + +Deprecated Interfaces +--------------------- + + .. autosummary:: + :nosignatures: + + ObjectBox + Builder \ No newline at end of file diff --git a/doc/setup.sh b/doc/setup.sh index 76d7008..b183fbe 100755 --- a/doc/setup.sh +++ b/doc/setup.sh @@ -1,3 +1,4 @@ python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt +pip install -e .. From 7764ac954c3f12a21193a2065ba220253d418bb1 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 3 Jun 2024 10:24:55 +0200 Subject: [PATCH 15/28] Make overview.rst show only the most important things #52 Add some informational text also. --- doc/index.rst | 2 +- doc/overview.rst | 68 +++++++++++++++--------------------------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index a013862..0b995b4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,7 +8,7 @@ ObjectBox Python API overview API Reference ObjectBox Homepage - ObjectBox docs + ObjectBox Docs GitHub diff --git a/doc/overview.rst b/doc/overview.rst index 679aee7..0d9e35f 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -1,82 +1,56 @@ API Overview ============ + +This gives an overview of the ObjectBox Python API by showing most important parts to get started. + +For beginners, we recommend our `Python database docs `_ at for more details; +especially the `Getting Started `_ guide. +This covers more than just the obligatory ``pip install --upgrade objectbox``. .. currentmodule:: objectbox -Entity Decorator ----------------- +Defining the Data Model +----------------------- + +The ``@Entity`` decorator is used to define a class as an ObjectBox entity. .. autosummary:: :nosignatures: Entity - -Property Types --------------- +Each entity class defines a set of properties. The following property types are most commonly used: .. autosummary:: :nosignatures: Id - Bool String - Bytes - Int8 - Int16 Int32 Int64 Float32 Float64 Date - DateNano - Flex - BoolVector - CharVector - Int8Vector - Int16Vector - Int32Vector - Int64Vector Float32Vector - Float64Vector - BoolList - CharList - Int8List - Int16List - Int32List - Int64List - Float32List - Float64List - -Index Types and Flags ---------------------- - - .. autosummary:: - :nosignatures: - - Index - HnswIndex - HnswFlags - VectorDistanceType +Note: ``Float32Vector`` is used for `on-device vector search `_. Classes ------- +Now, with the data model defined, you can start using the ObjectBox database. +These are the main classes to interact with: + .. autosummary:: :nosignatures: Store Box - Model - PropertyQueryCondition - + QueryBuilder + Query -Deprecated Interfaces ---------------------- +What's next +----------- - .. autosummary:: - :nosignatures: - - ObjectBox - Builder \ No newline at end of file +* `Python database docs (docs.objectbox.io) `_ +* API reference (next in this API documentation) From 173ccd36ac5a2b91b6e6beef7925d5c08a1da2bd Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 3 Jun 2024 10:32:05 +0200 Subject: [PATCH 16/28] doc/setup.sh fix to install recursive dependencies of objectbox #52 --- doc/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/setup.sh b/doc/setup.sh index b183fbe..3738d9b 100755 --- a/doc/setup.sh +++ b/doc/setup.sh @@ -1,4 +1,4 @@ python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt -pip install -e .. +pip install -r ../requirements.txt From 8fad368f400206ce6ba2bb33ba443f8b3d443fcc Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Mon, 3 Jun 2024 10:54:49 +0200 Subject: [PATCH 17/28] objectbox: imports Query and QueryBuilder #52 also added a one-line docstring --- objectbox/__init__.py | 4 ++++ objectbox/query.py | 3 +++ objectbox/query_builder.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index f54aa45..2cb6383 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -22,6 +22,8 @@ from objectbox.c import NotFoundException, version_core, DebugFlags from objectbox.version import Version from objectbox.condition import PropertyQueryCondition +from objectbox.query import Query +from objectbox.query_builder import QueryBuilder from objectbox.builder import Builder from objectbox.objectbox import ObjectBox @@ -70,6 +72,8 @@ 'DebugFlags', 'PropertyQueryCondition', 'HnswFlags', + 'Query', + 'QueryBuilder' ] # Python binding version diff --git a/objectbox/query.py b/objectbox/query.py index 8b1792e..07fa98b 100644 --- a/objectbox/query.py +++ b/objectbox/query.py @@ -16,6 +16,9 @@ class Query: + """ + Query expression + """ def __init__(self, c_query, box: 'Box'): self._c_query = c_query self._box = box diff --git a/objectbox/query_builder.py b/objectbox/query_builder.py index 2147fcc..23d04b1 100644 --- a/objectbox/query_builder.py +++ b/objectbox/query_builder.py @@ -10,6 +10,9 @@ class QueryBuilder: + """ + Query builder + """ def __init__(self, store: Store, box: 'Box'): self._box = box self._entity = box._entity From d652e0f50dac9e59d14833fc81d3aa35737b747a Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 3 Jun 2024 11:49:55 +0200 Subject: [PATCH 18/28] Add a couple of docs to Query #52 Removed QueryBuilder from overview.rst again as using it directly is not a typical thing to do. --- doc/overview.rst | 1 - objectbox/query.py | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/overview.rst b/doc/overview.rst index 0d9e35f..2bb736d 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -46,7 +46,6 @@ These are the main classes to interact with: Store Box - QueryBuilder Query What's next diff --git a/objectbox/query.py b/objectbox/query.py index 07fa98b..be64fd3 100644 --- a/objectbox/query.py +++ b/objectbox/query.py @@ -17,9 +17,12 @@ class Query: """ - Query expression + Query is a reusable object that allows you to find objects in the database. + It is created by calling ``Box.query()`` with conditions inside and finalizing with ``build()``. + Note: Query objects are not thread-safe and should not be shared between threads. """ def __init__(self, c_query, box: 'Box'): + """ This is an internal constructor. Use ``Box.query()`` instead. """ self._c_query = c_query self._box = box self._entity = self._box._entity @@ -122,20 +125,31 @@ def find_ids_by_score_numpy(self) -> np.array: obx_id_array_free(c_id_array_p) def count(self) -> int: + """ Counts the objects that are matched by this query. + In other words, it gives the size of the result set). """ count = ctypes.c_uint64() obx_query_count(self._c_query, ctypes.byref(count)) return int(count.value) def remove(self) -> int: + """ Removes the objects that are matched by this query. """ count = ctypes.c_uint64() obx_query_remove(self._c_query, ctypes.byref(count)) return int(count.value) def offset(self, offset: int) -> 'Query': + """ Configure an offset for this query. + All methods that support offset will return/process objects starting at this offset. + Example use case: use together with limit to get a slice of the whole result, e.g. for "result paging". + Call with offset=0 to reset to the default behavior, i.e. starting from the first element. """ obx_query_offset(self._c_query, offset) return self def limit(self, limit: int) -> 'Query': + """ Configure a limit for this query. + All methods that support limit will return/process only the given number of objects. + Example use case: use together with offset to get a slice of the whole result, e.g. for "result paging". + Call with limit=0 to reset to the default behavior - zero limit means no limit applied. """ obx_query_limit(self._c_query, limit) return self From eddd8288ddf3c16fe973a53b98668bd94f6b86b4 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 3 Jun 2024 12:00:34 +0200 Subject: [PATCH 19/28] Extend docs for Box.query() #52 --- objectbox/box.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/objectbox/box.py b/objectbox/box.py index 1c9607f..691b5a8 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -166,13 +166,20 @@ def remove_all(self) -> int: return int(count.value) def query(self, condition: Optional[QueryCondition] = None) -> QueryBuilder: - """ Creates a QueryBuilder for the Entity that is managed by the Box. + """ Initiates Query creation for the Entity associated by this Box. + Technically, it creates a QueryBuilder object, and you have to call build() on it to get the Query object. :param condition: - If given, applies the given high-level condition to the new QueryBuilder object. - Useful for a user-friendly API design; for example: + Applies the given condition(s) to the new QueryBuilder object. + For example, assuming you defined an @Entity called "MyEntity" with a string property "name": - ``box.query(MyEntity.name.equals("Johnny")).build()`` + ``query = box.query(MyEntity.name.equals("Johnny")).build()`` + + It's also possible to pass multiple conditions: + + ``query = box.query(MyEntity.name.equals("Johnny") & MyEntity.age.greater(21)).build()`` + + Note: ``&`` is the logical AND operator, and ``|`` is the logical OR operator. """ qb = QueryBuilder(self._store, self) if condition is not None: From c513873df62581a5ad88f534cc8e82ebdf709ef7 Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Mon, 3 Jun 2024 12:20:51 +0200 Subject: [PATCH 20/28] objectbox: imports CoreException #52 --- objectbox/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 2cb6383..d9faed9 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -19,7 +19,7 @@ from objectbox.model.entity import Entity from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model -from objectbox.c import NotFoundException, version_core, DebugFlags +from objectbox.c import CoreException, NotFoundException, version_core, DebugFlags from objectbox.version import Version from objectbox.condition import PropertyQueryCondition from objectbox.query import Query @@ -66,6 +66,7 @@ 'VectorDistanceType', 'Store', 'ObjectBox', + 'CoreException', 'NotFoundException', 'version', 'version_info', From 654f134f251b4ff09e22d90a8cd246b4aef66a03 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 3 Jun 2024 13:59:37 +0200 Subject: [PATCH 21/28] Exceptions: documents and introduce type hierarchy #52 --- objectbox/__init__.py | 2 +- objectbox/c.py | 17 ++++++++++++++--- tests/test_basics.py | 8 ++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index d9faed9..bafa517 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -19,7 +19,7 @@ from objectbox.model.entity import Entity from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model -from objectbox.c import CoreException, NotFoundException, version_core, DebugFlags +from objectbox.c import DbException, CoreException, NotFoundException, version_core, DebugFlags from objectbox.version import Version from objectbox.condition import PropertyQueryCondition from objectbox.query import Query diff --git a/objectbox/c.py b/objectbox/c.py index 8579070..2048ce5 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -246,7 +246,15 @@ class OBX_query(ctypes.Structure): C.obx_last_error_code.restype = obx_err -class CoreException(Exception): +class DbException(Exception): + """ Base class for database exceptions. """ + pass + + +# TODO rename? +class CoreException(DbException): + """ A database exception having a ``code`` attribute for error details. """ + codes = { 0: "SUCCESS", 404: "NOT_FOUND", @@ -287,8 +295,11 @@ def last(): return CoreException(C.obx_last_error()) -class NotFoundException(Exception): - pass +class NotFoundException(CoreException): + """ Raised when an object is not found. """ + + def __init__(self): + super().__init__(404) def check_obx_err(code: obx_err, func, args) -> obx_err: diff --git a/tests/test_basics.py b/tests/test_basics.py index a6738c7..3ac2ecb 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,6 +12,7 @@ # limitations under the License. import objectbox +from objectbox.c import NotFoundException, CoreException from tests.common import create_test_store @@ -32,3 +33,10 @@ def test_version(): def test_open(): store = create_test_store() store.close() + + +def test_not_found_exception(): + try: + raise NotFoundException() + except CoreException as e: + assert e.code == 404 From 56ef96f7ed2818d1597b97d544895e840d4bc4bc Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Tue, 7 May 2024 16:32:38 +0200 Subject: [PATCH 22/28] store options: log callback #42 --- objectbox/c.py | 14 +++++++++++++- objectbox/store.py | 5 +++++ objectbox/store_options.py | 5 ++++- tests/test_store_options.py | 27 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/objectbox/c.py b/objectbox/c.py index 2048ce5..75c6ff7 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -84,6 +84,15 @@ def shlib_name(library: str) -> str: OBXValidateOnOpenPagesFlags = ctypes.c_int OBXValidateOnOpenKvFlags = ctypes.c_int OBXBackupRestoreFlags = ctypes.c_int +OBXLogLevel = ctypes.c_int + +from enum import IntEnum +class LogLevel(IntEnum): + Verbose = 10 + Debug = 20 + Info = 30 + Warn = 40 + Error = 50 class DebugFlags(IntEnum): """Debug flags""" @@ -563,8 +572,11 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): # OBX_C_API void obx_opt_async_object_bytes_max_size_to_cache(OBX_store_options* opt, uint64_t value); obx_opt_async_object_bytes_max_size_to_cache = c_fn('obx_opt_async_object_bytes_max_size_to_cache', None, [OBX_store_options_p, ctypes.c_uint64]) +#typedef void obx_log_callback(OBXLogLevel log_level, const char* message, size_t message_size, void* user_data); +obx_log_callback_fn = ctypes.CFUNCTYPE(None, OBXLogLevel, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_voidp) + # OBX_C_API void obx_opt_log_callback(OBX_store_options* opt, obx_log_callback* callback, void* user_data); -# obx_opt_log_callback = c_fn('obx_opt_log_callback', None, [OBX_store_options_p, ...]) TODO +obx_opt_log_callback = c_fn('obx_opt_log_callback', None, [OBX_store_options_p, obx_log_callback_fn, ctypes.c_voidp]) # OBX_C_API void obx_opt_backup_restore(OBX_store_options* opt, const char* backup_file, uint32_t flags); obx_opt_backup_restore = c_fn('obx_opt_backup_restore', None, [OBX_store_options_p, ctypes.c_char_p, OBXBackupRestoreFlags]) diff --git a/objectbox/store.py b/objectbox/store.py index 23a55e5..28ff7b0 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -55,6 +55,7 @@ def __init__(self, async_minor_refill_max_count: Optional[int] = None, async_object_bytes_max_cache_size: Optional[int] = None, async_object_bytes_max_size_to_cache: Optional[int] = None, + log_callback: Callable[[c.OBXLogLevel, str],None] = None, c_store: Optional[c.OBX_store_p] = None): """Opens an ObjectBox database Store @@ -118,6 +119,8 @@ def __init__(self, Total cache size. Defaults to 0.5 mega bytes. :param async_object_bytes_max_size_to_cache: Maximum size for an object to be cached. + :param log_callback: + Callback function for logging with the parameters, OBXLogLevel and message. :param c_store: Internal parameter for deprecated ObjectBox interface. Do not use it; other options would be ignored if passed. """ @@ -175,6 +178,8 @@ def __init__(self, options.async_object_bytes_max_cache_size(async_object_bytes_max_cache_size) if async_object_bytes_max_size_to_cache is not None: options.async_object_bytes_max_size_to_cache(async_object_bytes_max_size_to_cache) + if log_callback is not None: + options.log_callback(log_callback) except c.CoreException: options._free() diff --git a/objectbox/store_options.py b/objectbox/store_options.py index 193ae66..8cdbae6 100644 --- a/objectbox/store_options.py +++ b/objectbox/store_options.py @@ -112,7 +112,10 @@ def async_object_bytes_max_cache_size(self, value: int): def async_object_bytes_max_size_to_cache(self, value: int): obx_opt_async_object_bytes_max_size_to_cache(self._c_handle, value) - # TODO def log_callback(self): + def log_callback(self, value: Callable[[OBXLogLevel, str],None]): + self._c_log_cb = obx_log_callback_fn(lambda level, message, size, userdata: value(level, message.decode('utf-8'))) + obx_opt_log_callback(self._c_handle, self._c_log_cb, None) + return self def backup_restore(self, backup_file: str, flags: OBXBackupRestoreFlags): raise NotImplementedError # TODO diff --git a/tests/test_store_options.py b/tests/test_store_options.py index 1e50e99..1b71e61 100644 --- a/tests/test_store_options.py +++ b/tests/test_store_options.py @@ -78,3 +78,30 @@ def test_store_with_options(): async_object_bytes_max_size_to_cache=100<<10 ) store.close() + +def test_log_callback(): + Store.remove_db_files("testdata") + remove_json_model_file() + + log_entries = [] + + def mylog(level: OBXLogLevel, message: str): + levelText = "?" + if level == LogLevel.Debug: + levelText = "DEBUG" + print(f"MYLOG: {levelText} {message}") + log_entries.append( (level, message) ) + + store = Store( + model=create_default_model(), + directory="testdata", + log_callback=mylog + ) + + box = store.box(TestEntity) + assert len(log_entries) == 2 + assert log_entries[0][0] == LogLevel.Debug + assert log_entries[0][1].startswith("Opening store:") + assert log_entries[1][0] == LogLevel.Debug + assert log_entries[1][1].startswith("Opening store:") + From a12d57c27b501044efb514536eacfee0bad70c1c Mon Sep 17 00:00:00 2001 From: Daniel Adler Date: Thu, 4 Jul 2024 16:09:28 +0200 Subject: [PATCH 23/28] CI: fixes for test:mac:x64 #42 --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f73fca2..6fb362a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,6 +58,13 @@ test:linux:aarch64: test:mac:x64: extends: .test + script: + - python3 -m venv .venv + - source .venv/bin/activate + - python3 -m pip install pytest + - rm -r objectbox # todo this is ugly; let's copy required files in a sub-folder instead? + - pip3 install --force-reinstall dist/*.whl # Artifacts from the previous stage (downloaded by default) + - python -m pytest tags: [mac, x64, shell, python3] test:windows:x64: From f9fc86a9d487d05ca1f19c6bfa34a7d199d609eb Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 13 Feb 2025 11:33:04 +0100 Subject: [PATCH 24/28] Skip test_date_value_to_int__naive for now #64 Likely a DST issue, revisit later --- tests/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5799ba2..c7ff3ba 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -65,6 +65,7 @@ def test_date_value_to_int__timezone(): assert date_value_to_int(dt_plus2, 1000) == expected +@pytest.mark.skip(reason="Disabled until DST handling is resolved") def test_date_value_to_int__naive(): dt_naive = datetime(year=2000, month=5, day=1, hour=12, minute=30, second=45, microsecond=123456) local_tz = datetime.now().astimezone().tzinfo From 9da8d62685880e9ca39952311023208588e7d0c5 Mon Sep 17 00:00:00 2001 From: loryruta Date: Wed, 5 Jun 2024 11:24:55 +0200 Subject: [PATCH 25/28] Add exceptions.py #64 Throwing exceptions of different types according to code --- objectbox/__init__.py | 3 +- objectbox/c.py | 114 ++++++++++++---------- objectbox/exceptions.py | 206 ++++++++++++++++++++++++++++++++++++++++ tests/test_basics.py | 3 +- 4 files changed, 272 insertions(+), 54 deletions(-) create mode 100644 objectbox/exceptions.py diff --git a/objectbox/__init__.py b/objectbox/__init__.py index bafa517..47acb3a 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -19,7 +19,7 @@ from objectbox.model.entity import Entity from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model -from objectbox.c import DbException, CoreException, NotFoundException, version_core, DebugFlags +from objectbox.c import DbException, CoreException, version_core, DebugFlags from objectbox.version import Version from objectbox.condition import PropertyQueryCondition from objectbox.query import Query @@ -67,7 +67,6 @@ 'Store', 'ObjectBox', 'CoreException', - 'NotFoundException', 'version', 'version_info', 'DebugFlags', diff --git a/objectbox/c.py b/objectbox/c.py index 75c6ff7..5579840 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -260,77 +260,89 @@ class DbException(Exception): pass +class CoreExceptionCode(IntEnum): + OBX_SUCCESS = 0 + OBX_NOT_FOUND = 404 + OBX_NO_SUCCESS = 1001 + OBX_TIMEOUT = 1002 + OBX_ERROR_ILLEGAL_STATE = 10001 + OBX_ERROR_ILLEGAL_ARGUMENT = 10002 + OBX_ERROR_ALLOCATION = 10003 + OBX_ERROR_NUMERIC_OVERFLOW = 10004 + OBX_ERROR_FEATURE_NOT_AVAILABLE = 10005 + OBX_ERROR_SHUTTING_DOWN = 10006 + OBX_ERROR_IO = 10007 + OBX_ERROR_BACKUP_FILE_INVALID = 10008 + OBX_ERROR_NO_ERROR_INFO = 10097 + OBX_ERROR_GENERAL = 10098 + OBX_ERROR_UNKNOWN = 10099 + OBX_ERROR_DB_FULL = 10101 + OBX_ERROR_MAX_READERS_EXCEEDED = 10102 + OBX_ERROR_STORE_MUST_SHUTDOWN = 10103 + OBX_ERROR_MAX_DATA_SIZE_EXCEEDED = 10104 + OBX_ERROR_DB_GENERAL = 10198 + OBX_ERROR_STORAGE_GENERAL = 10199 + OBX_ERROR_UNIQUE_VIOLATED = 10201 + OBX_ERROR_NON_UNIQUE_RESULT = 10202 + OBX_ERROR_PROPERTY_TYPE_MISMATCH = 10203 + OBX_ERROR_ID_ALREADY_EXISTS = 10210 + OBX_ERROR_ID_NOT_FOUND = 10211 + OBX_ERROR_TIME_SERIES = 10212 + OBX_ERROR_CONSTRAINT_VIOLATED = 10299 + OBX_ERROR_STD_ILLEGAL_ARGUMENT = 10301 + OBX_ERROR_STD_OUT_OF_RANGE = 10302 + OBX_ERROR_STD_LENGTH = 10303 + OBX_ERROR_STD_BAD_ALLOC = 10304 + OBX_ERROR_STD_RANGE = 10305 + OBX_ERROR_STD_OVERFLOW = 10306 + OBX_ERROR_STD_OTHER = 10399 + OBX_ERROR_SCHEMA = 10501 + OBX_ERROR_FILE_CORRUPT = 10502 + OBX_ERROR_FILE_PAGES_CORRUPT = 10503 + OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND = 10504 + OBX_ERROR_TREE_MODEL_INVALID = 10601 + OBX_ERROR_TREE_VALUE_TYPE_MISMATCH = 10602 + OBX_ERROR_TREE_PATH_NON_UNIQUE = 10603 + OBX_ERROR_TREE_PATH_ILLEGAL = 10604 + OBX_ERROR_TREE_OTHER = 10699 + + # TODO rename? class CoreException(DbException): - """ A database exception having a ``code`` attribute for error details. """ - - codes = { - 0: "SUCCESS", - 404: "NOT_FOUND", - 10001: "ILLEGAL_STATE", - 10002: "ILLEGAL_ARGUMENT", - 10003: "ALLOCATION", - 10097: "NO_ERROR_INFO", - 10098: "GENERAL", - 10099: "UNKNOWN", - 10101: "DB_FULL", - 10102: "MAX_READERS_EXCEEDED", - 10103: "STORE_MUST_SHUTDOWN", - 10199: "STORAGE_GENERAL", - 10201: "UNIQUE_VIOLATED", - 10202: "NON_UNIQUE_RESULT", - 10203: "PROPERTY_TYPE_MISMATCH", - 10299: "CONSTRAINT_VIOLATED", - 10301: "STD_ILLEGAL_ARGUMENT", - 10302: "STD_OUT_OF_RANGE", - 10303: "STD_LENGTH", - 10304: "STD_BAD_ALLOC", - 10305: "STD_RANGE", - 10306: "STD_OVERFLOW", - 10399: "STD_OTHER", - 10501: "SCHEMA", - 10502: "FILE_CORRUPT" - } - - def __init__(self, code: int): - self.code = code + """A database exception having a ``code`` attribute for error details.""" + code = CoreExceptionCode.OBX_NO_SUCCESS # Re-defined by the derived classes + + def __init__(self): self.message = py_str(C.obx_last_error_message()) - name = self.codes[code] if code in self.codes else "n/a" - super(CoreException, self).__init__("%d (%s) - %s" % (code, name, self.message)) + super(CoreException, self).__init__("%d (%s) - %s" % (self.code.value, self.code.name, self.message)) @staticmethod def last(): - """ Creates a CoreException of the last error that was generated in core. """ + """Creates a CoreException of the last error that was generated in core.""" return CoreException(C.obx_last_error()) -class NotFoundException(CoreException): - """ Raised when an object is not found. """ - - def __init__(self): - super().__init__(404) - - def check_obx_err(code: obx_err, func, args) -> obx_err: """ Raises an exception if obx_err is not successful. """ - if code == 404: - raise NotFoundException() - elif code != 0: - raise CoreException(code) + if code != CoreExceptionCode.OBX_SUCCESS: + from objectbox.exceptions import create_core_exception + raise create_core_exception(code) return code -def check_obx_qb_cond(code: obx_qb_cond, func, args) -> obx_qb_cond: +def check_obx_qb_cond(qb_cond: obx_qb_cond, func, args) -> obx_qb_cond: """ Raises an exception if obx_qb_cond is not successful. """ - if code == 0: - raise CoreException(code) - return code + if qb_cond == 0: + from objectbox.exceptions import create_core_exception + raise create_core_exception(C.obx_last_error_code()) + return qb_cond # assert that the returned pointer/int is non-empty def check_result(result, func, args): if not result: - raise CoreException(C.obx_last_error_code()) + from objectbox.exceptions import create_core_exception + raise create_core_exception(C.obx_last_error_code()) return result diff --git a/objectbox/exceptions.py b/objectbox/exceptions.py new file mode 100644 index 0000000..6d8c9b3 --- /dev/null +++ b/objectbox/exceptions.py @@ -0,0 +1,206 @@ +from typing import Dict, Type + +from objectbox.c import CoreException, CoreExceptionCode + + +class NotFoundException(CoreException): + """Raised when an object is not found.""" + code = CoreExceptionCode.OBX_NOT_FOUND + + +class NoSuccessException(CoreException): + code = CoreExceptionCode.OBX_NO_SUCCESS + + +class TimeoutException(CoreException): + code = CoreExceptionCode.OBX_TIMEOUT + + +class IllegalStateError(CoreException): + code = CoreExceptionCode.OBX_ERROR_ILLEGAL_STATE + + +class IllegalArgumentError(CoreException): + code = CoreExceptionCode.OBX_ERROR_ILLEGAL_ARGUMENT + + +class AllocationError(CoreException): + code = CoreExceptionCode.OBX_ERROR_ALLOCATION + + +class NumericOverflowError(CoreException): + code = CoreExceptionCode.OBX_ERROR_NUMERIC_OVERFLOW + + +class FeatureNotAvailable(CoreException): + code = CoreExceptionCode.OBX_ERROR_FEATURE_NOT_AVAILABLE + + +class ShuttingDownError(CoreException): + code = CoreExceptionCode.OBX_ERROR_SHUTTING_DOWN + + +class IoError(CoreException): + code = CoreExceptionCode.OBX_ERROR_IO + + +class BackupFileInvalidError(CoreException): + code = CoreExceptionCode.OBX_ERROR_BACKUP_FILE_INVALID + + +class NoErrorInfoError(CoreException): + code = CoreExceptionCode.OBX_ERROR_NO_ERROR_INFO + + +class GeneralError(CoreException): + code = CoreExceptionCode.OBX_ERROR_GENERAL + + +class UnknownError(CoreException): + code = CoreExceptionCode.OBX_ERROR_UNKNOWN + + +class DbFullError(CoreException): + code = CoreExceptionCode.OBX_ERROR_DB_FULL + + +class MaxReadersExceededError(CoreException): + code = CoreExceptionCode.OBX_ERROR_MAX_READERS_EXCEEDED + + +class StoreMustShutdownError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STORE_MUST_SHUTDOWN + + +class MaxDataSizeExceededError(CoreException): + code = CoreExceptionCode.OBX_ERROR_MAX_DATA_SIZE_EXCEEDED + + +class DbGeneralError(CoreException): + code = CoreExceptionCode.OBX_ERROR_DB_GENERAL + + +class StorageGeneralError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STORAGE_GENERAL + + +class UniqueViolatedError(CoreException): + code = CoreExceptionCode.OBX_ERROR_UNIQUE_VIOLATED + + +class NonUniqueResultError(CoreException): + code = CoreExceptionCode.OBX_ERROR_NON_UNIQUE_RESULT + + +class PropertyTypeMismatchError(CoreException): + code = CoreExceptionCode.OBX_ERROR_PROPERTY_TYPE_MISMATCH + + +class IdAlreadyExistsError(CoreException): + code = CoreExceptionCode.OBX_ERROR_ID_ALREADY_EXISTS + + +class IdNotFoundError(CoreException): + code = CoreExceptionCode.OBX_ERROR_ID_NOT_FOUND + + +class TimeSeriesError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TIME_SERIES + + +class ConstraintViolatedError(CoreException): + code = CoreExceptionCode.OBX_ERROR_CONSTRAINT_VIOLATED + + +class StdIllegalArgumentError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_ILLEGAL_ARGUMENT + + +class StdOutOfRangeError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_OUT_OF_RANGE + + +class StdLengthError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_LENGTH + + +class StdBadAllocError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_BAD_ALLOC + + +class StdRangeError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_RANGE + + +class StdOverflowError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_OVERFLOW + + +class StdOtherError(CoreException): + code = CoreExceptionCode.OBX_ERROR_STD_OTHER + + +class SchemaError(CoreException): + code = CoreExceptionCode.OBX_ERROR_SCHEMA + + +class FileCorruptError(CoreException): + code = CoreExceptionCode.OBX_ERROR_FILE_CORRUPT + + +class FilePagesCorruptError(CoreException): + code = CoreExceptionCode.OBX_ERROR_FILE_PAGES_CORRUPT + + +class SchemaObjectNotFoundError(CoreException): + code = CoreExceptionCode.OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND + + +class TreeModelInvalidError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TREE_MODEL_INVALID + + +class TreeValueTypeMismatchError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TREE_VALUE_TYPE_MISMATCH + + +class TreePathNonUniqueError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TREE_PATH_NON_UNIQUE + + +class TreePathIllegalError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TREE_PATH_ILLEGAL + + +class TreeOtherError(CoreException): + code = CoreExceptionCode.OBX_ERROR_TREE_OTHER + + +obx_core_exceptions_map: Dict[int, Type] = {} + + +def _init_core_exceptions_map(): + import inspect + import sys + + def is_core_exception_subclass(element_) -> bool: + valid = True + valid &= inspect.isclass(element_) + valid &= hasattr(element_, "code") + return valid + + this_module = sys.modules[__name__] + for name, element in inspect.getmembers(this_module): + if is_core_exception_subclass(element): + obx_core_exceptions_map[element.code] = element + + +_init_core_exceptions_map() + + +def create_core_exception(code: int) -> CoreException: + if code == CoreExceptionCode.OBX_SUCCESS: + raise Exception(f"Can't create a CoreException for code: OBX_SUCCESS") + elif code not in obx_core_exceptions_map: + raise Exception(f"Unrecognized CoreException code: {code}") + return obx_core_exceptions_map[code]() diff --git a/tests/test_basics.py b/tests/test_basics.py index 3ac2ecb..2ad2377 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,7 +12,8 @@ # limitations under the License. import objectbox -from objectbox.c import NotFoundException, CoreException +from objectbox.c import CoreException +from objectbox.exceptions import NotFoundException from tests.common import create_test_store From decab4c12d26317d49a555d44d4877e7f4edd137 Mon Sep 17 00:00:00 2001 From: loryruta Date: Tue, 16 Jul 2024 11:07:57 +0200 Subject: [PATCH 26/28] Add ObjectBoxException, rename CoreException to StorageException #64 --- objectbox/__init__.py | 5 +- objectbox/box.py | 5 +- objectbox/c.py | 102 +++++++++--------- objectbox/exceptions.py | 233 +++++++++++++++++++++------------------- objectbox/store.py | 3 +- tests/test_basics.py | 5 +- tests/test_idsync.py | 8 +- 7 files changed, 189 insertions(+), 172 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index 47acb3a..f904be9 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -19,7 +19,8 @@ from objectbox.model.entity import Entity from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model -from objectbox.c import DbException, CoreException, version_core, DebugFlags +from objectbox.c import version_core, DebugFlags +from objectbox.exceptions import StorageException from objectbox.version import Version from objectbox.condition import PropertyQueryCondition from objectbox.query import Query @@ -66,7 +67,7 @@ 'VectorDistanceType', 'Store', 'ObjectBox', - 'CoreException', + 'StorageException', 'version', 'version_info', 'DebugFlags', diff --git a/objectbox/box.py b/objectbox/box.py index 691b5a8..d6f33a0 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -18,6 +18,7 @@ from objectbox.query_builder import QueryBuilder from objectbox.condition import QueryCondition from objectbox.c import * +from objectbox.exceptions import StorageException class Box: @@ -121,7 +122,7 @@ def get(self, id: int): if code == 404: return None elif code != 0: - raise CoreException(code) + raise StorageException.from_code(code) data = c_voidp_as_bytes(c_data, c_size.value) return self._entity._unmarshal(data) @@ -156,7 +157,7 @@ def remove(self, id_or_object) -> bool: if code == 404: return False elif code != 0: - raise CoreException(code) + raise StorageException.from_code(code) return True def remove_all(self) -> int: diff --git a/objectbox/c.py b/objectbox/c.py index 5579840..49df75a 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -94,32 +94,33 @@ class LogLevel(IntEnum): Warn = 40 Error = 50 + class DebugFlags(IntEnum): """Debug flags""" - + NONE = 0, - + LOG_TRANSACTIONS_READ = 1, """ Log read transactions """ - + LOG_TRANSACTIONS_WRITE = 2, """ Log write transactions """ - + LOG_QUERIES = 3, """ Log queries """ - + LOG_QUERY_PARAMETERS = 8, """ Log query parameters """ - + LOG_ASYNC_QUEUE = 16, """ Log async queue """ - + LOG_CACHE_HITS = 32, """ Log cache hits """ - + LOG_CACHE_ALL = 64, """ Log cache hits """ - + LOG_TREE = 128 """ Log tree operations """ @@ -255,12 +256,7 @@ class OBX_query(ctypes.Structure): C.obx_last_error_code.restype = obx_err -class DbException(Exception): - """ Base class for database exceptions. """ - pass - - -class CoreExceptionCode(IntEnum): +class StorageErrorCode(IntEnum): OBX_SUCCESS = 0 OBX_NOT_FOUND = 404 OBX_NO_SUCCESS = 1001 @@ -307,42 +303,27 @@ class CoreExceptionCode(IntEnum): OBX_ERROR_TREE_OTHER = 10699 -# TODO rename? -class CoreException(DbException): - """A database exception having a ``code`` attribute for error details.""" - code = CoreExceptionCode.OBX_NO_SUCCESS # Re-defined by the derived classes - - def __init__(self): - self.message = py_str(C.obx_last_error_message()) - super(CoreException, self).__init__("%d (%s) - %s" % (self.code.value, self.code.name, self.message)) - - @staticmethod - def last(): - """Creates a CoreException of the last error that was generated in core.""" - return CoreException(C.obx_last_error()) - - def check_obx_err(code: obx_err, func, args) -> obx_err: """ Raises an exception if obx_err is not successful. """ - if code != CoreExceptionCode.OBX_SUCCESS: - from objectbox.exceptions import create_core_exception - raise create_core_exception(code) + if code != StorageErrorCode.OBX_SUCCESS: + from objectbox.exceptions import create_storage_exception + raise create_storage_exception(code) return code def check_obx_qb_cond(qb_cond: obx_qb_cond, func, args) -> obx_qb_cond: """ Raises an exception if obx_qb_cond is not successful. """ if qb_cond == 0: - from objectbox.exceptions import create_core_exception - raise create_core_exception(C.obx_last_error_code()) + from objectbox.exceptions import create_storage_exception + raise create_storage_exception(C.obx_last_error_code()) return qb_cond # assert that the returned pointer/int is non-empty def check_result(result, func, args): if not result: - from objectbox.exceptions import create_core_exception - raise create_core_exception(C.obx_last_error_code()) + from objectbox.exceptions import create_storage_exception + raise create_storage_exception(C.obx_last_error_code()) return result @@ -423,10 +404,13 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): # OBX_C_API float obx_vector_distance_float32(OBXVectorDistanceType type, const float* vector1, const float* vector2, size_t dimension); -obx_vector_distance_float32 = c_fn("obx_vector_distance_float32", ctypes.c_float, [OBXVectorDistanceType, ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), ctypes.c_size_t]) +obx_vector_distance_float32 = c_fn("obx_vector_distance_float32", ctypes.c_float, + [OBXVectorDistanceType, ctypes.POINTER(ctypes.c_float), + ctypes.POINTER(ctypes.c_float), ctypes.c_size_t]) # OBX_C_API float obx_vector_distance_to_relevance(OBXVectorDistanceType type, float distance); -obx_vector_distance_to_relevance = c_fn("obx_vector_distance_to_relevance", ctypes.c_float, [OBXVectorDistanceType, ctypes.c_float]) +obx_vector_distance_to_relevance = c_fn("obx_vector_distance_to_relevance", ctypes.c_float, + [OBXVectorDistanceType, ctypes.c_float]) # OBX_model* (void); obx_model = c_fn('obx_model', OBX_model_p, []) @@ -462,7 +446,8 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): c_fn_rc('obx_model_property_index_hnsw_flags', [OBX_model_p, OBXHnswFlags]) # obx_err obx_model_property_index_hnsw_distance_type(OBX_model* model, OBXVectorDistanceType value) -obx_model_property_index_hnsw_distance_type = c_fn_rc('obx_model_property_index_hnsw_distance_type', [OBX_model_p, OBXVectorDistanceType]) +obx_model_property_index_hnsw_distance_type = c_fn_rc('obx_model_property_index_hnsw_distance_type', + [OBX_model_p, OBXVectorDistanceType]) # obx_err obx_model_property_index_hnsw_reparation_backlink_probability(OBX_model* model, float value) obx_model_property_index_hnsw_reparation_backlink_probability = \ @@ -516,10 +501,12 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_opt_model_bytes = c_fn_rc('obx_opt_model_bytes', [OBX_store_options_p, ctypes.c_void_p, ctypes.c_size_t]) # OBX_C_API obx_err obx_opt_model_bytes_direct(OBX_store_options* opt, const void* bytes, size_t size); -obx_opt_model_bytes_direct = c_fn_rc('obx_opt_model_bytes_direct', [OBX_store_options_p, ctypes.c_void_p, ctypes.c_size_t]) +obx_opt_model_bytes_direct = c_fn_rc('obx_opt_model_bytes_direct', + [OBX_store_options_p, ctypes.c_void_p, ctypes.c_size_t]) # OBX_C_API void obx_opt_validate_on_open_pages(OBX_store_options* opt, size_t page_limit, uint32_t flags); -obx_opt_validate_on_open_pages = c_fn('obx_opt_validate_on_open_pages', None, [OBX_store_options_p, ctypes.c_size_t, OBXValidateOnOpenPagesFlags]) +obx_opt_validate_on_open_pages = c_fn('obx_opt_validate_on_open_pages', None, + [OBX_store_options_p, ctypes.c_size_t, OBXValidateOnOpenPagesFlags]) # OBX_C_API void obx_opt_validate_on_open_kv(OBX_store_options* opt, uint32_t flags); obx_opt_validate_on_open_kv = c_fn('obx_opt_validate_on_open_kv', None, [OBX_store_options_p, OBXValidateOnOpenKvFlags]) @@ -546,43 +533,53 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_opt_async_max_queue_length = c_fn('obx_opt_async_max_queue_length', None, [OBX_store_options_p, ctypes.c_size_t]) # OBX_C_API void obx_opt_async_throttle_at_queue_length(OBX_store_options* opt, size_t value); -obx_opt_async_throttle_at_queue_length = c_fn('obx_opt_async_throttle_at_queue_length', None, [OBX_store_options_p, ctypes.c_size_t]) +obx_opt_async_throttle_at_queue_length = c_fn('obx_opt_async_throttle_at_queue_length', None, + [OBX_store_options_p, ctypes.c_size_t]) # OBX_C_API void obx_opt_async_throttle_micros(OBX_store_options* opt, uint32_t value); obx_opt_async_throttle_micros = c_fn('obx_opt_async_throttle_micros', None, [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_max_in_tx_duration(OBX_store_options* opt, uint32_t micros); -obx_opt_async_max_in_tx_duration = c_fn('obx_opt_async_max_in_tx_duration', None, [OBX_store_options_p, ctypes.c_uint32]) +obx_opt_async_max_in_tx_duration = c_fn('obx_opt_async_max_in_tx_duration', None, + [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_max_in_tx_operations(OBX_store_options* opt, uint32_t value); -obx_opt_async_max_in_tx_operations = c_fn('obx_opt_async_max_in_tx_operations', None, [OBX_store_options_p, ctypes.c_uint32]) +obx_opt_async_max_in_tx_operations = c_fn('obx_opt_async_max_in_tx_operations', None, + [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_pre_txn_delay(OBX_store_options* opt, uint32_t delay_micros); obx_opt_async_pre_txn_delay = c_fn('obx_opt_async_pre_txn_delay', None, [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_pre_txn_delay4(OBX_store_options* opt, uint32_t delay_micros, uint32_t delay2_micros, size_t min_queue_length_for_delay2); -obx_opt_async_pre_txn_delay4 = c_fn('obx_opt_async_pre_txn_delay4', None, [OBX_store_options_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]) +obx_opt_async_pre_txn_delay4 = c_fn('obx_opt_async_pre_txn_delay4', None, + [OBX_store_options_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]) # OBX_C_API void obx_opt_async_post_txn_delay(OBX_store_options* opt, uint32_t delay_micros); obx_opt_async_post_txn_delay = c_fn('obx_opt_async_post_txn_delay', None, [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_post_txn_delay5(OBX_store_options* opt, uint32_t delay_micros, uint32_t delay2_micros, size_t min_queue_length_for_delay2, bool subtract_processing_time); -obx_opt_async_post_txn_delay5 = c_fn('obx_opt_async_post_txn_delay5', None, [OBX_store_options_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t, ctypes.c_bool]) +obx_opt_async_post_txn_delay5 = c_fn('obx_opt_async_post_txn_delay5', None, + [OBX_store_options_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t, + ctypes.c_bool]) # OBX_C_API void obx_opt_async_minor_refill_threshold(OBX_store_options* opt, size_t queue_length); -obx_opt_async_minor_refill_threshold = c_fn('obx_opt_async_minor_refill_threshold', None, [OBX_store_options_p, ctypes.c_size_t]) +obx_opt_async_minor_refill_threshold = c_fn('obx_opt_async_minor_refill_threshold', None, + [OBX_store_options_p, ctypes.c_size_t]) # OBX_C_API void obx_opt_async_minor_refill_max_count(OBX_store_options* opt, uint32_t value); -obx_opt_async_minor_refill_max_count = c_fn('obx_opt_async_minor_refill_max_count', None, [OBX_store_options_p, ctypes.c_uint32]) +obx_opt_async_minor_refill_max_count = c_fn('obx_opt_async_minor_refill_max_count', None, + [OBX_store_options_p, ctypes.c_uint32]) # OBX_C_API void obx_opt_async_max_tx_pool_size(OBX_store_options* opt, size_t value); obx_opt_async_max_tx_pool_size = c_fn('obx_opt_async_max_tx_pool_size', None, [OBX_store_options_p, ctypes.c_size_t]) # OBX_C_API void obx_opt_async_object_bytes_max_cache_size(OBX_store_options* opt, uint64_t value); -obx_opt_async_object_bytes_max_cache_size = c_fn('obx_opt_async_object_bytes_max_cache_size', None, [OBX_store_options_p, ctypes.c_uint64]) +obx_opt_async_object_bytes_max_cache_size = c_fn('obx_opt_async_object_bytes_max_cache_size', None, + [OBX_store_options_p, ctypes.c_uint64]) # OBX_C_API void obx_opt_async_object_bytes_max_size_to_cache(OBX_store_options* opt, uint64_t value); -obx_opt_async_object_bytes_max_size_to_cache = c_fn('obx_opt_async_object_bytes_max_size_to_cache', None, [OBX_store_options_p, ctypes.c_uint64]) +obx_opt_async_object_bytes_max_size_to_cache = c_fn('obx_opt_async_object_bytes_max_size_to_cache', None, + [OBX_store_options_p, ctypes.c_uint64]) #typedef void obx_log_callback(OBXLogLevel log_level, const char* message, size_t message_size, void* user_data); obx_log_callback_fn = ctypes.CFUNCTYPE(None, OBXLogLevel, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_voidp) @@ -591,7 +588,8 @@ def c_array_pointer(py_list: Union[List[Any], np.ndarray], c_type): obx_opt_log_callback = c_fn('obx_opt_log_callback', None, [OBX_store_options_p, obx_log_callback_fn, ctypes.c_voidp]) # OBX_C_API void obx_opt_backup_restore(OBX_store_options* opt, const char* backup_file, uint32_t flags); -obx_opt_backup_restore = c_fn('obx_opt_backup_restore', None, [OBX_store_options_p, ctypes.c_char_p, OBXBackupRestoreFlags]) +obx_opt_backup_restore = c_fn('obx_opt_backup_restore', None, + [OBX_store_options_p, ctypes.c_char_p, OBXBackupRestoreFlags]) # OBX_C_API const char* obx_opt_get_directory(OBX_store_options* opt); obx_opt_get_directory = c_fn('obx_opt_get_directory', ctypes.c_char_p, [OBX_store_options_p]) diff --git a/objectbox/exceptions.py b/objectbox/exceptions.py index 6d8c9b3..3674956 100644 --- a/objectbox/exceptions.py +++ b/objectbox/exceptions.py @@ -1,206 +1,223 @@ from typing import Dict, Type -from objectbox.c import CoreException, CoreExceptionCode +from objectbox.c import py_str, C, StorageErrorCode -class NotFoundException(CoreException): - """Raised when an object is not found.""" - code = CoreExceptionCode.OBX_NOT_FOUND +class ObjectBoxException(Exception): + """The base class for all exceptions thrown by ObjectBox.""" + + def __init__(self, message: str): + super().__init__(message) + + +class StorageException(ObjectBoxException): + """The base class for all exceptions thrown by ObjectBox core. + Every exception has a code attribute for identification.""" + + code = StorageErrorCode.OBX_NO_SUCCESS # Re-defined by derived classes + def __init__(self): + self.message = py_str(C.obx_last_error_message()) + super().__init__("%d (%s) - %s" % (self.code.value, self.code.name, self.message)) -class NoSuccessException(CoreException): - code = CoreExceptionCode.OBX_NO_SUCCESS + @staticmethod + def from_code(code: int): + """Creates a StorageException of the given error code.""" + return create_storage_exception(code) + + @staticmethod + def last(): + """Creates a StorageException of the last error that was generated in core.""" + return StorageException.from_code(C.obx_last_error()) + + +class NotFoundException(StorageException): + """Raised when an object is not found.""" + code = StorageErrorCode.OBX_NOT_FOUND -class TimeoutException(CoreException): - code = CoreExceptionCode.OBX_TIMEOUT +class NoSuccessException(StorageException): + code = StorageErrorCode.OBX_NO_SUCCESS -class IllegalStateError(CoreException): - code = CoreExceptionCode.OBX_ERROR_ILLEGAL_STATE +class TimeoutException(StorageException): + code = StorageErrorCode.OBX_TIMEOUT -class IllegalArgumentError(CoreException): - code = CoreExceptionCode.OBX_ERROR_ILLEGAL_ARGUMENT +class IllegalStateError(StorageException): + code = StorageErrorCode.OBX_ERROR_ILLEGAL_STATE -class AllocationError(CoreException): - code = CoreExceptionCode.OBX_ERROR_ALLOCATION +class IllegalArgumentError(StorageException): + code = StorageErrorCode.OBX_ERROR_ILLEGAL_ARGUMENT -class NumericOverflowError(CoreException): - code = CoreExceptionCode.OBX_ERROR_NUMERIC_OVERFLOW +class AllocationError(StorageException): + code = StorageErrorCode.OBX_ERROR_ALLOCATION -class FeatureNotAvailable(CoreException): - code = CoreExceptionCode.OBX_ERROR_FEATURE_NOT_AVAILABLE +class NumericOverflowError(StorageException): + code = StorageErrorCode.OBX_ERROR_NUMERIC_OVERFLOW -class ShuttingDownError(CoreException): - code = CoreExceptionCode.OBX_ERROR_SHUTTING_DOWN +class FeatureNotAvailable(StorageException): + code = StorageErrorCode.OBX_ERROR_FEATURE_NOT_AVAILABLE -class IoError(CoreException): - code = CoreExceptionCode.OBX_ERROR_IO +class ShuttingDownError(StorageException): + code = StorageErrorCode.OBX_ERROR_SHUTTING_DOWN -class BackupFileInvalidError(CoreException): - code = CoreExceptionCode.OBX_ERROR_BACKUP_FILE_INVALID +class IoError(StorageException): + code = StorageErrorCode.OBX_ERROR_IO -class NoErrorInfoError(CoreException): - code = CoreExceptionCode.OBX_ERROR_NO_ERROR_INFO +class BackupFileInvalidError(StorageException): + code = StorageErrorCode.OBX_ERROR_BACKUP_FILE_INVALID -class GeneralError(CoreException): - code = CoreExceptionCode.OBX_ERROR_GENERAL +class NoErrorInfoError(StorageException): + code = StorageErrorCode.OBX_ERROR_NO_ERROR_INFO -class UnknownError(CoreException): - code = CoreExceptionCode.OBX_ERROR_UNKNOWN +class GeneralError(StorageException): + code = StorageErrorCode.OBX_ERROR_GENERAL -class DbFullError(CoreException): - code = CoreExceptionCode.OBX_ERROR_DB_FULL +class UnknownError(StorageException): + code = StorageErrorCode.OBX_ERROR_UNKNOWN -class MaxReadersExceededError(CoreException): - code = CoreExceptionCode.OBX_ERROR_MAX_READERS_EXCEEDED +class DbFullError(StorageException): + code = StorageErrorCode.OBX_ERROR_DB_FULL -class StoreMustShutdownError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STORE_MUST_SHUTDOWN +class MaxReadersExceededError(StorageException): + code = StorageErrorCode.OBX_ERROR_MAX_READERS_EXCEEDED -class MaxDataSizeExceededError(CoreException): - code = CoreExceptionCode.OBX_ERROR_MAX_DATA_SIZE_EXCEEDED +class StoreMustShutdownError(StorageException): + code = StorageErrorCode.OBX_ERROR_STORE_MUST_SHUTDOWN -class DbGeneralError(CoreException): - code = CoreExceptionCode.OBX_ERROR_DB_GENERAL +class MaxDataSizeExceededError(StorageException): + code = StorageErrorCode.OBX_ERROR_MAX_DATA_SIZE_EXCEEDED -class StorageGeneralError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STORAGE_GENERAL +class DbGeneralError(StorageException): + code = StorageErrorCode.OBX_ERROR_DB_GENERAL -class UniqueViolatedError(CoreException): - code = CoreExceptionCode.OBX_ERROR_UNIQUE_VIOLATED +class StorageGeneralError(StorageException): + code = StorageErrorCode.OBX_ERROR_STORAGE_GENERAL -class NonUniqueResultError(CoreException): - code = CoreExceptionCode.OBX_ERROR_NON_UNIQUE_RESULT +class UniqueViolatedError(StorageException): + code = StorageErrorCode.OBX_ERROR_UNIQUE_VIOLATED -class PropertyTypeMismatchError(CoreException): - code = CoreExceptionCode.OBX_ERROR_PROPERTY_TYPE_MISMATCH +class NonUniqueResultError(StorageException): + code = StorageErrorCode.OBX_ERROR_NON_UNIQUE_RESULT -class IdAlreadyExistsError(CoreException): - code = CoreExceptionCode.OBX_ERROR_ID_ALREADY_EXISTS +class PropertyTypeMismatchError(StorageException): + code = StorageErrorCode.OBX_ERROR_PROPERTY_TYPE_MISMATCH -class IdNotFoundError(CoreException): - code = CoreExceptionCode.OBX_ERROR_ID_NOT_FOUND +class IdAlreadyExistsError(StorageException): + code = StorageErrorCode.OBX_ERROR_ID_ALREADY_EXISTS -class TimeSeriesError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TIME_SERIES +class IdNotFoundError(StorageException): + code = StorageErrorCode.OBX_ERROR_ID_NOT_FOUND -class ConstraintViolatedError(CoreException): - code = CoreExceptionCode.OBX_ERROR_CONSTRAINT_VIOLATED +class TimeSeriesError(StorageException): + code = StorageErrorCode.OBX_ERROR_TIME_SERIES -class StdIllegalArgumentError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_ILLEGAL_ARGUMENT +class ConstraintViolatedError(StorageException): + code = StorageErrorCode.OBX_ERROR_CONSTRAINT_VIOLATED -class StdOutOfRangeError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_OUT_OF_RANGE +class StdIllegalArgumentError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_ILLEGAL_ARGUMENT -class StdLengthError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_LENGTH +class StdOutOfRangeError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_OUT_OF_RANGE -class StdBadAllocError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_BAD_ALLOC +class StdLengthError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_LENGTH -class StdRangeError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_RANGE +class StdBadAllocError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_BAD_ALLOC -class StdOverflowError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_OVERFLOW +class StdRangeError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_RANGE -class StdOtherError(CoreException): - code = CoreExceptionCode.OBX_ERROR_STD_OTHER +class StdOverflowError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_OVERFLOW -class SchemaError(CoreException): - code = CoreExceptionCode.OBX_ERROR_SCHEMA +class StdOtherError(StorageException): + code = StorageErrorCode.OBX_ERROR_STD_OTHER -class FileCorruptError(CoreException): - code = CoreExceptionCode.OBX_ERROR_FILE_CORRUPT +class SchemaError(StorageException): + code = StorageErrorCode.OBX_ERROR_SCHEMA -class FilePagesCorruptError(CoreException): - code = CoreExceptionCode.OBX_ERROR_FILE_PAGES_CORRUPT +class FileCorruptError(StorageException): + code = StorageErrorCode.OBX_ERROR_FILE_CORRUPT -class SchemaObjectNotFoundError(CoreException): - code = CoreExceptionCode.OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND +class FilePagesCorruptError(StorageException): + code = StorageErrorCode.OBX_ERROR_FILE_PAGES_CORRUPT -class TreeModelInvalidError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TREE_MODEL_INVALID +class SchemaObjectNotFoundError(StorageException): + code = StorageErrorCode.OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND -class TreeValueTypeMismatchError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TREE_VALUE_TYPE_MISMATCH +class TreeModelInvalidError(StorageException): + code = StorageErrorCode.OBX_ERROR_TREE_MODEL_INVALID -class TreePathNonUniqueError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TREE_PATH_NON_UNIQUE +class TreeValueTypeMismatchError(StorageException): + code = StorageErrorCode.OBX_ERROR_TREE_VALUE_TYPE_MISMATCH -class TreePathIllegalError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TREE_PATH_ILLEGAL +class TreePathNonUniqueError(StorageException): + code = StorageErrorCode.OBX_ERROR_TREE_PATH_NON_UNIQUE -class TreeOtherError(CoreException): - code = CoreExceptionCode.OBX_ERROR_TREE_OTHER +class TreePathIllegalError(StorageException): + code = StorageErrorCode.OBX_ERROR_TREE_PATH_ILLEGAL -obx_core_exceptions_map: Dict[int, Type] = {} +class TreeOtherError(StorageException): + code = StorageErrorCode.OBX_ERROR_TREE_OTHER -def _init_core_exceptions_map(): - import inspect - import sys +obx_storage_exceptions_map: Dict[int, Type] = {} - def is_core_exception_subclass(element_) -> bool: - valid = True - valid &= inspect.isclass(element_) - valid &= hasattr(element_, "code") - return valid - this_module = sys.modules[__name__] - for name, element in inspect.getmembers(this_module): - if is_core_exception_subclass(element): - obx_core_exceptions_map[element.code] = element +def _init_storage_exceptions_map(): + for subclass in StorageException.__subclasses__(): + obx_storage_exceptions_map[subclass.code] = subclass -_init_core_exceptions_map() +_init_storage_exceptions_map() -def create_core_exception(code: int) -> CoreException: - if code == CoreExceptionCode.OBX_SUCCESS: - raise Exception(f"Can't create a CoreException for code: OBX_SUCCESS") - elif code not in obx_core_exceptions_map: - raise Exception(f"Unrecognized CoreException code: {code}") - return obx_core_exceptions_map[code]() +def create_storage_exception(code: int) -> StorageException: + if code == StorageErrorCode.OBX_SUCCESS: + raise Exception(f"Can't create a StorageException for code: OBX_SUCCESS") + elif code not in obx_storage_exceptions_map: + raise Exception(f"Unrecognized StorageException code: {code}") + return obx_storage_exceptions_map[code]() diff --git a/objectbox/store.py b/objectbox/store.py index 28ff7b0..9255a28 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -22,6 +22,7 @@ from objectbox.model.idsync import sync_model from objectbox.store_options import StoreOptions import objectbox +from objectbox.exceptions import StorageException from objectbox.model.entity import _Entity from objectbox.model.model import Model from typing import * @@ -181,7 +182,7 @@ def __init__(self, if log_callback is not None: options.log_callback(log_callback) - except c.CoreException: + except StorageException: options._free() raise self._c_store = c.obx_store_open(options._c_handle) diff --git a/tests/test_basics.py b/tests/test_basics.py index 2ad2377..f93781a 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,8 +12,7 @@ # limitations under the License. import objectbox -from objectbox.c import CoreException -from objectbox.exceptions import NotFoundException +from objectbox.exceptions import StorageException, NotFoundException from tests.common import create_test_store @@ -39,5 +38,5 @@ def test_open(): def test_not_found_exception(): try: raise NotFoundException() - except CoreException as e: + except StorageException as e: assert e.code == 404 diff --git a/tests/test_idsync.py b/tests/test_idsync.py index 3ed67f3..e1a85b1 100644 --- a/tests/test_idsync.py +++ b/tests/test_idsync.py @@ -8,7 +8,7 @@ from objectbox.model.entity import _Entity from objectbox.model.idsync import sync_model from objectbox.model.iduid import IdUid -from objectbox.c import CoreException +from objectbox.exceptions import StorageException from os import path from tests.common import remove_json_model_file @@ -235,8 +235,8 @@ class MyEntity2: box1 = store.box(MyEntity1) assert box1.count() == 2 - # MyEntity2 is gone and should raise CoreException - with pytest.raises(CoreException): + # MyEntity2 is gone and should raise StorageException + with pytest.raises(StorageException): box2 = store.box(MyEntity2) def test_entity_rename(env): @@ -572,7 +572,7 @@ class EntityB2: # with pytest.raises(ValueError): # store_a.box(EntityB) - with pytest.raises(CoreException): + with pytest.raises(StorageException): store_a.box(EntityB2) box_b = store_b.box(EntityB) From 527e061a2a0fc915b191f97419a64bdbed7df74d Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 13 Feb 2025 11:32:15 +0100 Subject: [PATCH 27/28] Base exception shall be named DbError #64 --- objectbox/__init__.py | 4 +- objectbox/box.py | 6 +- objectbox/c.py | 16 +-- objectbox/exceptions.py | 217 +++++++++++++++++++--------------------- objectbox/store.py | 4 +- tests/test_basics.py | 6 +- tests/test_idsync.py | 6 +- 7 files changed, 126 insertions(+), 133 deletions(-) diff --git a/objectbox/__init__.py b/objectbox/__init__.py index f904be9..71bf846 100644 --- a/objectbox/__init__.py +++ b/objectbox/__init__.py @@ -20,7 +20,7 @@ from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, HnswIndex, VectorDistanceType, HnswFlags from objectbox.model.model import Model from objectbox.c import version_core, DebugFlags -from objectbox.exceptions import StorageException +from objectbox.exceptions import DbError from objectbox.version import Version from objectbox.condition import PropertyQueryCondition from objectbox.query import Query @@ -67,7 +67,7 @@ 'VectorDistanceType', 'Store', 'ObjectBox', - 'StorageException', + 'DbError', 'version', 'version_info', 'DebugFlags', diff --git a/objectbox/box.py b/objectbox/box.py index d6f33a0..5d8f6a8 100644 --- a/objectbox/box.py +++ b/objectbox/box.py @@ -18,7 +18,7 @@ from objectbox.query_builder import QueryBuilder from objectbox.condition import QueryCondition from objectbox.c import * -from objectbox.exceptions import StorageException +from objectbox.exceptions import DbError class Box: @@ -122,7 +122,7 @@ def get(self, id: int): if code == 404: return None elif code != 0: - raise StorageException.from_code(code) + raise DbError.from_code(code) data = c_voidp_as_bytes(c_data, c_size.value) return self._entity._unmarshal(data) @@ -157,7 +157,7 @@ def remove(self, id_or_object) -> bool: if code == 404: return False elif code != 0: - raise StorageException.from_code(code) + raise DbError.from_code(code) return True def remove_all(self) -> int: diff --git a/objectbox/c.py b/objectbox/c.py index 49df75a..03b7aea 100644 --- a/objectbox/c.py +++ b/objectbox/c.py @@ -256,7 +256,7 @@ class OBX_query(ctypes.Structure): C.obx_last_error_code.restype = obx_err -class StorageErrorCode(IntEnum): +class DbErrorCode(IntEnum): OBX_SUCCESS = 0 OBX_NOT_FOUND = 404 OBX_NO_SUCCESS = 1001 @@ -305,25 +305,25 @@ class StorageErrorCode(IntEnum): def check_obx_err(code: obx_err, func, args) -> obx_err: """ Raises an exception if obx_err is not successful. """ - if code != StorageErrorCode.OBX_SUCCESS: - from objectbox.exceptions import create_storage_exception - raise create_storage_exception(code) + if code != DbErrorCode.OBX_SUCCESS: + from objectbox.exceptions import create_db_error + raise create_db_error(code) return code def check_obx_qb_cond(qb_cond: obx_qb_cond, func, args) -> obx_qb_cond: """ Raises an exception if obx_qb_cond is not successful. """ if qb_cond == 0: - from objectbox.exceptions import create_storage_exception - raise create_storage_exception(C.obx_last_error_code()) + from objectbox.exceptions import create_db_error + raise create_db_error(C.obx_last_error_code()) return qb_cond # assert that the returned pointer/int is non-empty def check_result(result, func, args): if not result: - from objectbox.exceptions import create_storage_exception - raise create_storage_exception(C.obx_last_error_code()) + from objectbox.exceptions import create_db_error + raise create_db_error(C.obx_last_error_code()) return result diff --git a/objectbox/exceptions.py b/objectbox/exceptions.py index 3674956..a7ffadf 100644 --- a/objectbox/exceptions.py +++ b/objectbox/exceptions.py @@ -1,223 +1,216 @@ from typing import Dict, Type -from objectbox.c import py_str, C, StorageErrorCode +from objectbox.c import py_str, C, DbErrorCode -class ObjectBoxException(Exception): - """The base class for all exceptions thrown by ObjectBox.""" - - def __init__(self, message: str): - super().__init__(message) - - -class StorageException(ObjectBoxException): - """The base class for all exceptions thrown by ObjectBox core. +class DbError(Exception): + """The base class for all exceptions thrown by ObjectBox. Every exception has a code attribute for identification.""" - code = StorageErrorCode.OBX_NO_SUCCESS # Re-defined by derived classes + code = DbErrorCode.OBX_NO_SUCCESS # Re-defined by derived classes def __init__(self): self.message = py_str(C.obx_last_error_message()) - super().__init__("%d (%s) - %s" % (self.code.value, self.code.name, self.message)) + super().__init__(f"{self.code.value} ({self.code.name}) - {self.message}") @staticmethod def from_code(code: int): - """Creates a StorageException of the given error code.""" - return create_storage_exception(code) + """Creates a DbError of the given error code.""" + return create_db_error(code) @staticmethod def last(): - """Creates a StorageException of the last error that was generated in core.""" - return StorageException.from_code(C.obx_last_error()) + """Creates a DbError of the last error that was generated in core.""" + return DbError.from_code(C.obx_last_error()) -class NotFoundException(StorageException): +class NotFoundError(DbError): """Raised when an object is not found.""" - code = StorageErrorCode.OBX_NOT_FOUND + code = DbErrorCode.OBX_NOT_FOUND -class NoSuccessException(StorageException): - code = StorageErrorCode.OBX_NO_SUCCESS +class NoSuccessError(DbError): + code = DbErrorCode.OBX_NO_SUCCESS -class TimeoutException(StorageException): - code = StorageErrorCode.OBX_TIMEOUT +class TimeoutError(DbError): + code = DbErrorCode.OBX_TIMEOUT -class IllegalStateError(StorageException): - code = StorageErrorCode.OBX_ERROR_ILLEGAL_STATE +class IllegalStateError(DbError): + code = DbErrorCode.OBX_ERROR_ILLEGAL_STATE -class IllegalArgumentError(StorageException): - code = StorageErrorCode.OBX_ERROR_ILLEGAL_ARGUMENT +class IllegalArgumentError(DbError): + code = DbErrorCode.OBX_ERROR_ILLEGAL_ARGUMENT -class AllocationError(StorageException): - code = StorageErrorCode.OBX_ERROR_ALLOCATION +class AllocationError(DbError): + code = DbErrorCode.OBX_ERROR_ALLOCATION -class NumericOverflowError(StorageException): - code = StorageErrorCode.OBX_ERROR_NUMERIC_OVERFLOW +class NumericOverflowError(DbError): + code = DbErrorCode.OBX_ERROR_NUMERIC_OVERFLOW -class FeatureNotAvailable(StorageException): - code = StorageErrorCode.OBX_ERROR_FEATURE_NOT_AVAILABLE +class FeatureNotAvailable(DbError): + code = DbErrorCode.OBX_ERROR_FEATURE_NOT_AVAILABLE -class ShuttingDownError(StorageException): - code = StorageErrorCode.OBX_ERROR_SHUTTING_DOWN +class ShuttingDownError(DbError): + code = DbErrorCode.OBX_ERROR_SHUTTING_DOWN -class IoError(StorageException): - code = StorageErrorCode.OBX_ERROR_IO +class IoError(DbError): + code = DbErrorCode.OBX_ERROR_IO -class BackupFileInvalidError(StorageException): - code = StorageErrorCode.OBX_ERROR_BACKUP_FILE_INVALID +class BackupFileInvalidError(DbError): + code = DbErrorCode.OBX_ERROR_BACKUP_FILE_INVALID -class NoErrorInfoError(StorageException): - code = StorageErrorCode.OBX_ERROR_NO_ERROR_INFO +class NoErrorInfoError(DbError): + code = DbErrorCode.OBX_ERROR_NO_ERROR_INFO -class GeneralError(StorageException): - code = StorageErrorCode.OBX_ERROR_GENERAL +class GeneralError(DbError): + code = DbErrorCode.OBX_ERROR_GENERAL -class UnknownError(StorageException): - code = StorageErrorCode.OBX_ERROR_UNKNOWN +class UnknownError(DbError): + code = DbErrorCode.OBX_ERROR_UNKNOWN -class DbFullError(StorageException): - code = StorageErrorCode.OBX_ERROR_DB_FULL +class DbFullError(DbError): + code = DbErrorCode.OBX_ERROR_DB_FULL -class MaxReadersExceededError(StorageException): - code = StorageErrorCode.OBX_ERROR_MAX_READERS_EXCEEDED +class MaxReadersExceededError(DbError): + code = DbErrorCode.OBX_ERROR_MAX_READERS_EXCEEDED -class StoreMustShutdownError(StorageException): - code = StorageErrorCode.OBX_ERROR_STORE_MUST_SHUTDOWN +class StoreMustShutdownError(DbError): + code = DbErrorCode.OBX_ERROR_STORE_MUST_SHUTDOWN -class MaxDataSizeExceededError(StorageException): - code = StorageErrorCode.OBX_ERROR_MAX_DATA_SIZE_EXCEEDED +class MaxDataSizeExceededError(DbError): + code = DbErrorCode.OBX_ERROR_MAX_DATA_SIZE_EXCEEDED -class DbGeneralError(StorageException): - code = StorageErrorCode.OBX_ERROR_DB_GENERAL +class DbGeneralError(DbError): + code = DbErrorCode.OBX_ERROR_DB_GENERAL -class StorageGeneralError(StorageException): - code = StorageErrorCode.OBX_ERROR_STORAGE_GENERAL +class GeneralStorageError(DbError): + code = DbErrorCode.OBX_ERROR_STORAGE_GENERAL -class UniqueViolatedError(StorageException): - code = StorageErrorCode.OBX_ERROR_UNIQUE_VIOLATED +class UniqueViolatedError(DbError): + code = DbErrorCode.OBX_ERROR_UNIQUE_VIOLATED -class NonUniqueResultError(StorageException): - code = StorageErrorCode.OBX_ERROR_NON_UNIQUE_RESULT +class NonUniqueResultError(DbError): + code = DbErrorCode.OBX_ERROR_NON_UNIQUE_RESULT -class PropertyTypeMismatchError(StorageException): - code = StorageErrorCode.OBX_ERROR_PROPERTY_TYPE_MISMATCH +class PropertyTypeMismatchError(DbError): + code = DbErrorCode.OBX_ERROR_PROPERTY_TYPE_MISMATCH -class IdAlreadyExistsError(StorageException): - code = StorageErrorCode.OBX_ERROR_ID_ALREADY_EXISTS +class IdAlreadyExistsError(DbError): + code = DbErrorCode.OBX_ERROR_ID_ALREADY_EXISTS -class IdNotFoundError(StorageException): - code = StorageErrorCode.OBX_ERROR_ID_NOT_FOUND +class IdNotFoundError(DbError): + code = DbErrorCode.OBX_ERROR_ID_NOT_FOUND -class TimeSeriesError(StorageException): - code = StorageErrorCode.OBX_ERROR_TIME_SERIES +class TimeSeriesError(DbError): + code = DbErrorCode.OBX_ERROR_TIME_SERIES -class ConstraintViolatedError(StorageException): - code = StorageErrorCode.OBX_ERROR_CONSTRAINT_VIOLATED +class ConstraintViolatedError(DbError): + code = DbErrorCode.OBX_ERROR_CONSTRAINT_VIOLATED -class StdIllegalArgumentError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_ILLEGAL_ARGUMENT +class StdIllegalArgumentError(DbError): + code = DbErrorCode.OBX_ERROR_STD_ILLEGAL_ARGUMENT -class StdOutOfRangeError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_OUT_OF_RANGE +class StdOutOfRangeError(DbError): + code = DbErrorCode.OBX_ERROR_STD_OUT_OF_RANGE -class StdLengthError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_LENGTH +class StdLengthError(DbError): + code = DbErrorCode.OBX_ERROR_STD_LENGTH -class StdBadAllocError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_BAD_ALLOC +class StdBadAllocError(DbError): + code = DbErrorCode.OBX_ERROR_STD_BAD_ALLOC -class StdRangeError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_RANGE +class StdRangeError(DbError): + code = DbErrorCode.OBX_ERROR_STD_RANGE -class StdOverflowError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_OVERFLOW +class StdOverflowError(DbError): + code = DbErrorCode.OBX_ERROR_STD_OVERFLOW -class StdOtherError(StorageException): - code = StorageErrorCode.OBX_ERROR_STD_OTHER +class StdOtherError(DbError): + code = DbErrorCode.OBX_ERROR_STD_OTHER -class SchemaError(StorageException): - code = StorageErrorCode.OBX_ERROR_SCHEMA +class SchemaError(DbError): + code = DbErrorCode.OBX_ERROR_SCHEMA -class FileCorruptError(StorageException): - code = StorageErrorCode.OBX_ERROR_FILE_CORRUPT +class FileCorruptError(DbError): + code = DbErrorCode.OBX_ERROR_FILE_CORRUPT -class FilePagesCorruptError(StorageException): - code = StorageErrorCode.OBX_ERROR_FILE_PAGES_CORRUPT +class FilePagesCorruptError(DbError): + code = DbErrorCode.OBX_ERROR_FILE_PAGES_CORRUPT -class SchemaObjectNotFoundError(StorageException): - code = StorageErrorCode.OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND +class SchemaObjectNotFoundError(DbError): + code = DbErrorCode.OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND -class TreeModelInvalidError(StorageException): - code = StorageErrorCode.OBX_ERROR_TREE_MODEL_INVALID +class TreeModelInvalidError(DbError): + code = DbErrorCode.OBX_ERROR_TREE_MODEL_INVALID -class TreeValueTypeMismatchError(StorageException): - code = StorageErrorCode.OBX_ERROR_TREE_VALUE_TYPE_MISMATCH +class TreeValueTypeMismatchError(DbError): + code = DbErrorCode.OBX_ERROR_TREE_VALUE_TYPE_MISMATCH -class TreePathNonUniqueError(StorageException): - code = StorageErrorCode.OBX_ERROR_TREE_PATH_NON_UNIQUE +class TreePathNonUniqueError(DbError): + code = DbErrorCode.OBX_ERROR_TREE_PATH_NON_UNIQUE -class TreePathIllegalError(StorageException): - code = StorageErrorCode.OBX_ERROR_TREE_PATH_ILLEGAL +class TreePathIllegalError(DbError): + code = DbErrorCode.OBX_ERROR_TREE_PATH_ILLEGAL -class TreeOtherError(StorageException): - code = StorageErrorCode.OBX_ERROR_TREE_OTHER +class TreeOtherError(DbError): + code = DbErrorCode.OBX_ERROR_TREE_OTHER -obx_storage_exceptions_map: Dict[int, Type] = {} +obx_db_error_map: Dict[int, Type] = {} -def _init_storage_exceptions_map(): - for subclass in StorageException.__subclasses__(): - obx_storage_exceptions_map[subclass.code] = subclass +def _init_db_errror_map(): + for subclass in DbError.__subclasses__(): + obx_db_error_map[subclass.code] = subclass -_init_storage_exceptions_map() +_init_db_errror_map() -def create_storage_exception(code: int) -> StorageException: - if code == StorageErrorCode.OBX_SUCCESS: - raise Exception(f"Can't create a StorageException for code: OBX_SUCCESS") - elif code not in obx_storage_exceptions_map: +def create_db_error(code: int) -> DbError: + if code == DbErrorCode.OBX_SUCCESS: + raise Exception("Can't create a StorageException for code: OBX_SUCCESS") + elif code not in obx_db_error_map: raise Exception(f"Unrecognized StorageException code: {code}") - return obx_storage_exceptions_map[code]() + return obx_db_error_map[code]() diff --git a/objectbox/store.py b/objectbox/store.py index 9255a28..c41e452 100644 --- a/objectbox/store.py +++ b/objectbox/store.py @@ -22,7 +22,7 @@ from objectbox.model.idsync import sync_model from objectbox.store_options import StoreOptions import objectbox -from objectbox.exceptions import StorageException +from objectbox.exceptions import DbError from objectbox.model.entity import _Entity from objectbox.model.model import Model from typing import * @@ -182,7 +182,7 @@ def __init__(self, if log_callback is not None: options.log_callback(log_callback) - except StorageException: + except DbError: options._free() raise self._c_store = c.obx_store_open(options._c_handle) diff --git a/tests/test_basics.py b/tests/test_basics.py index f93781a..1c44c6b 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,7 +12,7 @@ # limitations under the License. import objectbox -from objectbox.exceptions import StorageException, NotFoundException +from objectbox.exceptions import DbError, NotFoundError from tests.common import create_test_store @@ -37,6 +37,6 @@ def test_open(): def test_not_found_exception(): try: - raise NotFoundException() - except StorageException as e: + raise NotFoundError() + except DbError as e: assert e.code == 404 diff --git a/tests/test_idsync.py b/tests/test_idsync.py index e1a85b1..575f039 100644 --- a/tests/test_idsync.py +++ b/tests/test_idsync.py @@ -8,7 +8,7 @@ from objectbox.model.entity import _Entity from objectbox.model.idsync import sync_model from objectbox.model.iduid import IdUid -from objectbox.exceptions import StorageException +from objectbox.exceptions import DbError from os import path from tests.common import remove_json_model_file @@ -236,7 +236,7 @@ class MyEntity2: assert box1.count() == 2 # MyEntity2 is gone and should raise StorageException - with pytest.raises(StorageException): + with pytest.raises(DbError): box2 = store.box(MyEntity2) def test_entity_rename(env): @@ -572,7 +572,7 @@ class EntityB2: # with pytest.raises(ValueError): # store_a.box(EntityB) - with pytest.raises(StorageException): + with pytest.raises(DbError): store_a.box(EntityB2) box_b = store_b.box(EntityB) From 9b38d97df428748a51d19acde728fbbbcef12288 Mon Sep 17 00:00:00 2001 From: Uwe Date: Mon, 8 Sep 2025 11:41:50 +0200 Subject: [PATCH 28/28] CI: run on new Windows runner --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6fb362a..3c05819 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,6 +69,6 @@ test:mac:x64: test:windows:x64: extends: .test - tags: [windows, x64, shell, python3] + tags: [windows, x64, python] variables: PYTHON: "python.exe"