diff --git a/.gitignore b/.gitignore index 0e6237d..7de1b26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # IDE .idea +.vscode # Environment .venv/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab7a32b..23866bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ Contributing ------------------ -Anyone can contribute, be it by coding, improving docs or just proposing a new feature. +Anyone can contribute, be it by coding, improving docs or just proposing a new feature. As a new contributor, you may want to have a look at some of the following issues: -* [**good first issue**](https://github.com/objectbox/objectbox-python/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag +* [**good first issue**](https://github.com/objectbox/objectbox-python/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag * [**help wanted**](https://github.com/objectbox/objectbox-python/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) tag -When picking up an existing issue, please let others know in the issue comment. +When picking up an existing issue, please let others know in the issue comment. Don't hesitate to reach out for guidance or to discuss a solution proposal! ### Code contributions @@ -16,20 +16,19 @@ When creating a Pull Request for code changes, please check that you cover the f ### Basic technical approach ObjectBox offers a [C API](https://github.com/objectbox/objectbox-c) which can be integrated into python using [ctypes](https://docs.python.org/dev/library/ctypes.html). -The C API is is also used by the ObjectBox language bindings for [Go](https://github.com/objectbox/objectbox-go), +The C API is is also used by the ObjectBox language bindings for [Go](https://github.com/objectbox/objectbox-go), [Swift](https://github.com/objectbox/objectbox-swift), and [Dart/Flutter](https://github.com/objectbox/objectbox-dart). These language bindings currently serve as an example for this Python implementation. Internally, ObjectBox uses [FlatBuffers](https://google.github.io/flatbuffers/) to store objects. -The main prerequisite to using the Python APIs is the ObjectBox binary library (.so, .dylib, .dll depending on your -platform) which actually implements the database functionality. The library should be placed in the -`objectbox/lib/[architecture]/` folder of the checked out repository. You can get/update it by running `make get-lib`. +The main prerequisite to using the Python APIs is the ObjectBox binary library (.so, .dylib, .dll depending on your +platform) which actually implements the database functionality. The library should be placed in the +`objectbox/lib/[architecture]/` folder of the checked out repository. You can get/update it by running `make depend`. ### Getting started as a contributor #### Initial setup If you're just getting started, run the following simple steps to set up the repository on your machine * clone this repository -* `pip install virtualenv` install [virtualenv](https://pypi.org/project/virtualenv/) if you don't have it yet * `make depend` to initialize `virtualenv` and get dependencies (objectbox-c shared library) * `make` to build and test diff --git a/Makefile b/Makefile index 89fc3f7..2a4ffa3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ +SHELL := /bin/bash VENV = .venv VENVBIN = ${VENV}/bin +PYTHON = python3 +PIP = ${PYTHON} -m pip # Detect windows - works on both 32 & 64-bit windows ifeq ($(OS),Windows_NT) @@ -9,7 +12,7 @@ endif export PATH := $(abspath ${VENVBIN}):${PATH} -.PHONY: init test build benchmark publish +.PHONY: init test build benchmark publish venv-init # Default target executed when no arguments are given to make. default_target: build test @@ -22,28 +25,37 @@ help: ## Show this help all: depend build test ## Get dependencies, clean, build and test build: ${VENV} clean ## Clean and build - python setup.py bdist_wheel + set -e ; \ + ${PYTHON} setup.py bdist_wheel ; \ ls -lh dist ${VENV}: ${VENVBIN}/activate -${VENVBIN}/activate: requirements.txt - virtualenv ${VENV} +venv-init: + ${PIP} install --user virtualenv + ${PYTHON} -m virtualenv ${VENV} + # remove packages not in the requirements.txt - pip3 freeze | grep -v -f requirements.txt - | grep -v '^#' | grep -v '^-e ' | xargs pip3 uninstall -y || echo "never mind" # install and upgrade based on the requirements.txt - python -m pip install --upgrade -r requirements.txt # let make know this is the last time requirements changed +${VENVBIN}/activate: requirements.txt + set -e ; \ + if [ ! -d "${VENV}" ] ; then make venv-init ; fi ; \ + ${PIP} freeze | grep -v -f requirements.txt - | grep -v '^#' | grep -v '^-e ' | xargs ${PIP} uninstall -y || echo "never mind" ; \ + ${PIP} install --upgrade -r requirements.txt ; \ touch ${VENVBIN}/activate depend: ${VENV} ## Prepare dependencies - python download-c-lib.py + set -e ; \ + ${PYTHON} download-c-lib.py test: ${VENV} ## Test all targets - python -m pytest --capture=no --verbose + set -e ; \ + ${PYTHON} -m pytest --capture=no --verbose benchmark: ${VENV} ## Run CRUD benchmarks - python -m benchmark + set -e ; \ + ${PYTHON} -m benchmark clean: ## Clean build artifacts rm -rf build/ @@ -51,4 +63,5 @@ clean: ## Clean build artifacts rm -rf *.egg-info publish: ## Publish the package built by `make build` - python -m twine upload --verbose dist/objectbox*.whl \ No newline at end of file + set -e ; \ + ${PYTHON} -m twine upload --verbose dist/objectbox*.whl \ No newline at end of file diff --git a/README.md b/README.md index afe2045..ff43e83 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,19 @@ from objectbox.model import * @Entity(id=1, uid=1) class Person: id = Id(id=1, uid=1001) - first_name = Property(str, id=2, uid=1002) - last_name = Property(str, id=3, uid=1003) + name = Property(str, id=2, uid=1002) + is_enabled = Property(bool, id=3, uid=1003) + # int can be stored with 64 (default), 32, 16 or 8 bit precision. + int64 = Property(int, id=4, uid=1004) + int32 = Property(int, type=PropertyType.int, id=5, uid=1005) + int16 = Property(int, type=PropertyType.short, id=6, uid=1006) + int8 = Property(int, type=PropertyType.byte, id=7, uid=1007) + # float can be stored with 64 or 32 (default) bit precision. + float64 = Property(float, id=8, uid=1008) + float32 = Property(float, type=PropertyType.float, id=9, uid=1009) + byte_array = Property(bytes, id=10, uid=1010) + # Regular properties are not stored. + transient = "" ``` ### Using ObjectBox @@ -58,16 +69,16 @@ import objectbox # Configure ObjectBox: should be done only once in the whole program and the "ob" variable should be kept around model = objectbox.Model() -model.entity(Person, last_property_id=objectbox.model.IdUid(3, 1003)) +model.entity(Person, last_property_id=objectbox.model.IdUid(10, 1010)) model.last_entity_id = objectbox.model.IdUid(1, 1) ob = objectbox.Builder().model(model).directory("db").build() # Open the box of "Person" entity. This can be called many times but you can also pass the variable around box = objectbox.Box(ob, Person) -id = box.put(Person(first_name="Joe", last_name="Green")) # Create +id = box.put(Person(name="Joe Green")) # Create person = box.get(id) # Read -person.last_name = "Black" +person.name = "Joe Black" box.put(person) # Update box.remove(person) # Delete ``` diff --git a/objectbox/model/__init__.py b/objectbox/model/__init__.py index 8e9f927..69a2e19 100644 --- a/objectbox/model/__init__.py +++ b/objectbox/model/__init__.py @@ -22,5 +22,6 @@ 'Entity', 'Id', 'IdUid', - 'Property' + 'Property', + 'PropertyType' ] diff --git a/objectbox/model/entity.py b/objectbox/model/entity.py index 160e253..595179e 100644 --- a/objectbox/model/entity.py +++ b/objectbox/model/entity.py @@ -64,6 +64,8 @@ def fill_properties(self): "programming error - invalid type OB & FB type combination" self.offset_properties.append(prop) + # print('Property {}.{}: {} (ob:{} fb:{})'.format(self.name, prop._name, prop._py_type, prop._ob_type, prop._fb_type)) + if not self.id_property: raise Exception("ID property is not defined") elif self.id_property._ob_type != OBXPropertyType_Long: diff --git a/objectbox/model/properties.py b/objectbox/model/properties.py index 72783d8..08810f2 100644 --- a/objectbox/model/properties.py +++ b/objectbox/model/properties.py @@ -12,22 +12,54 @@ # See the License for the specific language governing permissions and # limitations under the License. +from enum import IntEnum from objectbox.c import * import flatbuffers.number_types -# base property +class PropertyType(IntEnum): + bool = OBXPropertyType_Bool + byte = OBXPropertyType_Byte + short = OBXPropertyType_Short + char = OBXPropertyType_Char + int = OBXPropertyType_Int + long = OBXPropertyType_Long + float = OBXPropertyType_Float + double = OBXPropertyType_Double + string = OBXPropertyType_String + # date = OBXPropertyType_Date + # relation = OBXPropertyType_Relation + byteVector = OBXPropertyType_ByteVector + # stringVector = OBXPropertyType_StringVector + + +fb_type_map = { + PropertyType.bool: flatbuffers.number_types.BoolFlags, + PropertyType.byte: flatbuffers.number_types.Int8Flags, + PropertyType.short: flatbuffers.number_types.Int16Flags, + PropertyType.char: flatbuffers.number_types.Int8Flags, + PropertyType.int: flatbuffers.number_types.Int32Flags, + PropertyType.long: flatbuffers.number_types.Int64Flags, + PropertyType.float: flatbuffers.number_types.Float32Flags, + PropertyType.double: flatbuffers.number_types.Float64Flags, + PropertyType.string: flatbuffers.number_types.UOffsetTFlags, + # PropertyType.date: flatbuffers.number_types.Int64Flags, + # PropertyType.relation: flatbuffers.number_types.Int64Flags, + PropertyType.byteVector: flatbuffers.number_types.UOffsetTFlags, + # PropertyType.stringVector: flatbuffers.number_types.UOffsetTFlags, +} + + class Property: - def __init__(self, py_type: type, id: int, uid: int): + def __init__(self, py_type: type, id: int, uid: int, type: PropertyType = None): self._id = id self._uid = uid - self._name = "" # set in Entity.fillProperties() + self._name = "" # set in Entity.fill_properties() - self._fb_type = None # flatbuffers.number_types self._py_type = py_type - self._ob_type = OBXPropertyType(0) - self.__set_basic_type() + self._ob_type = type if type != None else self.__determine_ob_type() + self._fb_type = fb_type_map[self._ob_type] self._is_id = isinstance(self, Id) self._flags = OBXPropertyFlags(0) @@ -37,23 +69,18 @@ def __init__(self, py_type: type, id: int, uid: int): self._fb_slot = self._id - 1 self._fb_v_offset = 4 + 2*self._fb_slot - def __set_basic_type(self) -> OBXPropertyType: + def __determine_ob_type(self) -> OBXPropertyType: ts = self._py_type if ts == str: - self._ob_type = OBXPropertyType_String - self._fb_type = flatbuffers.number_types.UOffsetTFlags + return OBXPropertyType_String elif ts == int: - self._ob_type = OBXPropertyType_Long - self._fb_type = flatbuffers.number_types.Int64Flags + return OBXPropertyType_Long elif ts == bytes: # or ts == bytearray: might require further tests on read objects due to mutability - self._ob_type = OBXPropertyType_ByteVector - self._fb_type = flatbuffers.number_types.UOffsetTFlags + return OBXPropertyType_ByteVector elif ts == float: - self._ob_type = OBXPropertyType_Double - self._fb_type = flatbuffers.number_types.Float64Flags + return OBXPropertyType_Double elif ts == bool: - self._ob_type = OBXPropertyType_Bool - self._fb_type = flatbuffers.number_types.BoolFlags + return OBXPropertyType_Bool else: raise Exception("unknown property type %s" % ts) diff --git a/tests/common.py b/tests/common.py index 30ef2dd..2df4224 100644 --- a/tests/common.py +++ b/tests/common.py @@ -23,7 +23,7 @@ def autocleanup(): def load_empty_test_objectbox(name: str = "") -> objectbox.ObjectBox: model = objectbox.Model() from objectbox.model import IdUid - model.entity(TestEntity, last_property_id=IdUid(6, 1006)) + model.entity(TestEntity, last_property_id=IdUid(10, 1010)) model.last_entity_id = IdUid(1, 1) db_name = test_dir if len(name) == 0 else test_dir + "/" + name @@ -31,12 +31,18 @@ def load_empty_test_objectbox(name: str = "") -> objectbox.ObjectBox: return objectbox.Builder().model(model).directory(db_name).build() -def assert_equal(actual, expected): +def assert_equal_prop(actual, expected, default): + assert actual == expected or (isinstance( + expected, objectbox.model.Property) and actual == default) + + +def assert_equal(actual: TestEntity, expected: TestEntity): """Check that two TestEntity objects have the same property data""" assert actual.id == expected.id - assert isinstance(expected.bool, objectbox.model.Property) or actual.bool == expected.bool - assert isinstance(expected.int, objectbox.model.Property) or actual.int == expected.int - assert isinstance(expected.str, objectbox.model.Property) or actual.str == expected.str - assert isinstance(expected.float, objectbox.model.Property) or actual.float == expected.float - assert isinstance(expected.bytes, objectbox.model.Property) or actual.bytes == expected.bytes - + assert_equal_prop(actual.int64, expected.int64, 0) + assert_equal_prop(actual.int32, expected.int32, 0) + assert_equal_prop(actual.int16, expected.int16, 0) + assert_equal_prop(actual.int8, expected.int8, 0) + assert_equal_prop(actual.float64, expected.float64, 0) + assert_equal_prop(actual.float32, expected.float32, 0) + assert_equal_prop(actual.bytes, expected.bytes, b'') diff --git a/tests/model.py b/tests/model.py index e836903..8b26a80 100644 --- a/tests/model.py +++ b/tests/model.py @@ -6,9 +6,13 @@ class TestEntity: id = Id(id=1, uid=1001) str = Property(str, id=2, uid=1002) bool = Property(bool, id=3, uid=1003) - int = Property(int, id=4, uid=1004) - float = Property(float, id=5, uid=1005) - bytes = Property(bytes, id=6, uid=1006) + int64 = Property(int, type=PropertyType.long, id=4, uid=1004) + int32 = Property(int, type=PropertyType.int, id=5, uid=1005) + int16 = Property(int, type=PropertyType.short, id=6, uid=1006) + int8 = Property(int, type=PropertyType.byte, id=7, uid=1007) + float64 = Property(float, type=PropertyType.double, id=8, uid=1008) + float32 = Property(float, type=PropertyType.float, id=9, uid=1009) + bytes = Property(bytes, id=10, uid=1010) transient = "" # not "Property" so it's not stored def __init__(self, string: str = ""): diff --git a/tests/test_box.py b/tests/test_box.py index a29e830..b252426 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -21,9 +21,13 @@ def test_box_basics(): object = TestEntity() object.id = 5 object.bool = True - object.int = 42 + object.int64 = 9223372036854775807 + object.int32 = 2147483647 + object.int16 = 32767 + object.int8 = 127 object.str = "foo" - object.float = 4.2 + object.float64 = 4.2 + object.float32 = 1.5 object.bytes = bytes([1, 1, 2, 3, 5]) object.transient = "abcd" @@ -67,7 +71,8 @@ def test_box_bulk(): box.put(TestEntity("first")) - objects = [TestEntity("second"), TestEntity("third"), TestEntity("fourth"), box.get(1)] + objects = [TestEntity("second"), TestEntity("third"), + TestEntity("fourth"), box.get(1)] box.put(objects) assert box.count() == 4 assert objects[0].id == 2