Skip to content

Commit

Permalink
Merge pull request #11 from objectbox/small-scalars
Browse files Browse the repository at this point in the history
Scalar types support
  • Loading branch information
vaind authored Dec 6, 2021
2 parents dd2dde1 + b0de1f1 commit 854cfa1
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 55 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# IDE
.idea
.vscode

# Environment
.venv/
Expand Down
15 changes: 7 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
33 changes: 23 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand All @@ -22,33 +25,43 @@ 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/
rm -rf dist/
rm -rf *.egg-info

publish: ## Publish the package built by `make build`
python -m twine upload --verbose dist/objectbox*.whl
set -e ; \
${PYTHON} -m twine upload --verbose dist/objectbox*.whl
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
```
Expand Down
3 changes: 2 additions & 1 deletion objectbox/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
'Entity',
'Id',
'IdUid',
'Property'
'Property',
'PropertyType'
]
2 changes: 2 additions & 0 deletions objectbox/model/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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:
Expand Down
61 changes: 44 additions & 17 deletions objectbox/model/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
22 changes: 14 additions & 8 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,26 @@ 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

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'')
10 changes: 7 additions & 3 deletions tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""):
Expand Down
11 changes: 8 additions & 3 deletions tests/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 854cfa1

Please sign in to comment.