Skip to content

Commit baa3c3a

Browse files
author
Ivan Dlugos
committed
introduce Box with basic CRUD, implement entity binary marshalling
1 parent 1bbe959 commit baa3c3a

10 files changed

Lines changed: 315 additions & 64 deletions

File tree

objectbox/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from objectbox.box import Box
12
from objectbox.builder import Builder
23
from objectbox.model import Model
4+
from objectbox.objectbox import ObjectBox
5+
from objectbox.c import NotFoundException
36

47
__all__ = [
8+
'Box',
59
'Builder',
610
'Model',
11+
'ObjectBox',
12+
'NotFoundException',
713
]

objectbox/box.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from objectbox.model.entity import _Entity
2+
from objectbox.objectbox import ObjectBox
3+
from objectbox.c import *
4+
5+
6+
class Box:
7+
def __init__(self, ob: ObjectBox, entity: _Entity):
8+
if not isinstance(entity, _Entity):
9+
raise Exception("Given type is not an Entity")
10+
11+
self._ob = ob
12+
self._entity = entity
13+
self._c_box = obx_box(ob._c_store, entity.id)
14+
15+
def is_empty(self) -> bool:
16+
is_empty = ctypes.c_bool()
17+
obx_box_is_empty(self._c_box, ctypes.byref(is_empty))
18+
return bool(is_empty.value)
19+
20+
def count(self, limit: int = 0) -> int:
21+
count = ctypes.c_uint64()
22+
obx_box_count(self._c_box, limit, ctypes.byref(count))
23+
return int(count.value)
24+
25+
def put(self, object) -> int:
26+
id = object_id = self._entity.get_object_id(object)
27+
28+
if not id:
29+
id = obx_box_id_for_put(self._c_box, 0)
30+
31+
data = self._entity.marshal(object, id)
32+
obx_box_put(self._c_box, id, bytes(data), len(data), OBXPutMode_PUT)
33+
34+
if id != object_id:
35+
self._entity.set_object_id(object, id)
36+
return id
37+
38+
def get(self, id: int):
39+
c_data = ctypes.c_void_p()
40+
c_size = ctypes.c_size_t()
41+
obx_box_get(self._c_box, id, ctypes.byref(c_data), ctypes.byref(c_size))
42+
43+
# TODO verify which of the following two approaches is better.
44+
45+
# slice the data from the pointer
46+
# data = ctypes.cast(c_data, ctypes.POINTER(ctypes.c_char))[:c_size.value]
47+
48+
# create a memory view
49+
data = memoryview(ctypes.cast(c_data, ctypes.POINTER(ctypes.c_ubyte * c_size.value))[0]).tobytes()
50+
51+
return self._entity.unmarshal(data)
52+
53+
def remove(self, id: int):
54+
obx_box_remove(self._c_box, id)

objectbox/c.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class OBX_store_options(ctypes.Structure):
4343
]
4444

4545
def p(self) -> 'ctypes.POINTER(OBX_store_options)':
46-
return ctypes.pointer(self)
46+
return ctypes.byref(self)
4747

4848

4949
OBX_store_options_p = ctypes.POINTER(OBX_store_options)
@@ -118,7 +118,7 @@ class OBX_query(ctypes.Structure):
118118
C.obx_last_error_code.restype = obx_err
119119

120120

121-
class CError(Exception):
121+
class CoreException(Exception):
122122
codes = {
123123
0: "SUCCESS",
124124
404: "NOT_FOUND",
@@ -150,19 +150,25 @@ class CError(Exception):
150150
def __init__(self, code):
151151
self.code = code
152152
self.message = py_str(C.obx_last_error_message())
153-
super(CError, self).__init__("%d (%s) - %s" % (code, self.codes[code], self.message))
153+
super(CoreException, self).__init__("%d (%s) - %s" % (code, self.codes[code], self.message))
154154

155155

156-
# check obx_err and raise an error
156+
class NotFoundException(CoreException):
157+
pass
158+
159+
160+
# assert the the returned obx_err is empty
157161
def check_obx_err(code: obx_err, func, args):
158-
if code != 0:
159-
raise CError(code)
162+
if code == 404:
163+
raise NotFoundException(code)
164+
elif code != 0:
165+
raise CoreException(code)
160166

161167

162-
# check if the returned pointer is null and raise an error
163-
def check_ptr_result(result, func, args):
168+
# assert that the returned pointer/int is non-empty
169+
def check_result(result, func, args):
164170
if not result:
165-
raise CError(C.obx_last_error_code())
171+
raise CoreException(C.obx_last_error_code())
166172
return result
167173

168174

@@ -173,7 +179,7 @@ def fn(name: str, restype: type, argtypes):
173179
if restype is obx_err:
174180
func.errcheck = check_obx_err
175181
elif restype is not None:
176-
func.errcheck = check_ptr_result
182+
func.errcheck = check_result
177183
func.restype = restype
178184

179185
func.argtypes = argtypes
@@ -220,6 +226,28 @@ def c_str(string: str) -> ctypes.c_char_p:
220226
# obx_err (OBX_store* store);
221227
obx_store_close = fn('obx_store_close', obx_err, [OBX_store_p])
222228

229+
# OBX_box* (OBX_store* store, obx_schema_id entity_id);
230+
obx_box = fn('obx_box', OBX_box_p, [OBX_store_p, obx_schema_id])
231+
232+
# obx_err (OBX_box* box, obx_id id, void** data, size_t* size);
233+
obx_box_get = fn('obx_box_get', obx_err,
234+
[OBX_box_p, obx_id, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_size_t)])
235+
236+
# obx_id (OBX_box* box, obx_id id_or_zero);
237+
obx_box_id_for_put = fn('obx_box_id_for_put', obx_id, [OBX_box_p, obx_id])
238+
239+
# obx_err (OBX_box* box, obx_id id, const void* data, size_t size, OBXPutMode mode);
240+
obx_box_put = fn('obx_box_put', obx_err, [OBX_box_p, obx_id, ctypes.c_void_p, ctypes.c_size_t, OBXPutMode])
241+
242+
# obx_err (OBX_box* box, obx_id id);
243+
obx_box_remove = fn('obx_box_remove', obx_err, [OBX_box_p, obx_id])
244+
245+
# obx_err (OBX_box* box, bool* out_is_empty);
246+
obx_box_is_empty = fn('obx_box_is_empty', obx_err, [OBX_box_p, ctypes.POINTER(ctypes.c_bool)])
247+
248+
# obx_err obx_box_count(OBX_box* box, uint64_t limit, uint64_t* out_count);
249+
obx_box_count = fn('obx_box_count', obx_err, [OBX_box_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64)])
250+
223251
OBXPropertyType_Bool = 1
224252
OBXPropertyType_Byte = 2
225253
OBXPropertyType_Short = 3
@@ -249,38 +277,38 @@ def c_str(string: str) -> ctypes.c_char_p:
249277
OBXPropertyFlags_INDEX_HASH64 = 4096
250278
OBXPropertyFlags_UNSIGNED = 8192
251279

252-
OBXDebugFlags_LOG_TRANSACTIONS_READ = 1,
253-
OBXDebugFlags_LOG_TRANSACTIONS_WRITE = 2,
254-
OBXDebugFlags_LOG_QUERIES = 4,
255-
OBXDebugFlags_LOG_QUERY_PARAMETERS = 8,
256-
OBXDebugFlags_LOG_ASYNC_QUEUE = 16,
280+
OBXDebugFlags_LOG_TRANSACTIONS_READ = 1
281+
OBXDebugFlags_LOG_TRANSACTIONS_WRITE = 2
282+
OBXDebugFlags_LOG_QUERIES = 4
283+
OBXDebugFlags_LOG_QUERY_PARAMETERS = 8
284+
OBXDebugFlags_LOG_ASYNC_QUEUE = 16
257285

258286
# Standard put ("insert or update")
259-
OBXPutMode_PUT = 1,
287+
OBXPutMode_PUT = 1
260288

261289
# Put succeeds only if the entity does not exist yet.
262-
OBXPutMode_INSERT = 2,
290+
OBXPutMode_INSERT = 2
263291

264292
# Put succeeds only if the entity already exist.
265-
OBXPutMode_UPDATE = 3,
293+
OBXPutMode_UPDATE = 3
266294

267295
# The given ID (non-zero) is guaranteed to be new; don't use unless you know exactly what you are doing!
268296
# This is primarily used internally. Wrong usage leads to inconsistent data (e.g. index data not updated)!
269297
OBXPutMode_PUT_ID_GUARANTEED_TO_BE_NEW = 4
270298

271299
# Reverts the order from ascending (default) to descending.
272-
OBXOrderFlags_DESCENDING = 1,
300+
OBXOrderFlags_DESCENDING = 1
273301

274302
# Makes upper case letters (e.g. "Z") be sorted before lower case letters (e.g. "a").
275303
# If not specified, the default is case insensitive for ASCII characters.
276-
OBXOrderFlags_CASE_SENSITIVE = 2,
304+
OBXOrderFlags_CASE_SENSITIVE = 2
277305

278306
# For scalars only: changes the comparison to unsigned (default is signed).
279-
OBXOrderFlags_UNSIGNED = 4,
307+
OBXOrderFlags_UNSIGNED = 4
280308

281309
# null values will be put last.
282310
# If not specified, by default null values will be put first.
283-
OBXOrderFlags_NULLS_LAST = 8,
311+
OBXOrderFlags_NULLS_LAST = 8
284312

285313
# null values should be treated equal to zero (scalars only).
286-
OBXOrderFlags_NULLS_ZERO = 16,
314+
OBXOrderFlags_NULLS_ZERO = 16

objectbox/model/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
'Model',
77
'Entity',
88
'Id',
9-
'IdUid'
9+
'IdUid',
10+
'Property'
1011
]

objectbox/model/entity.py

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,136 @@
1-
from typing import List, Dict
2-
1+
import flatbuffers
2+
from typing import List
3+
from objectbox.c import *
34
from objectbox.model.properties import Property
45

56

6-
# entity decorator
7+
# _Entity class holds model information as well as conversions between python objects and FlatBuffers (ObjectBox data)
78
class _Entity(object):
89
def __init__(self, cls, id: int, uid: int):
910
# currently, ID and UID are mandatory and are not fetched from the model.json
1011
if id <= 0:
11-
raise ValueError("invalid or no 'id; given in the @Entity annotation")
12+
raise Exception("invalid or no 'id; given in the @Entity annotation")
1213

1314
if uid <= 0:
14-
raise ValueError("invalid or no 'uid' given in the @Entity annotation")
15+
raise Exception("invalid or no 'uid' given in the @Entity annotation")
1516

1617
self.cls = cls
1718
self.name: str = cls.__name__
1819
self.id = id
1920
self.uid = uid
2021

22+
self.last_property_id: 'IdUid' = None # set in model.entity()
23+
2124
self.properties: List[Property] = list()
22-
self.fillProperties()
25+
self.offset_properties: List[Property] = list()
26+
self.id_property: Property = None
27+
self.fill_properties()
2328

2429
def __call__(self):
2530
return self.cls()
2631

27-
def fillProperties(self):
32+
def fill_properties(self):
2833
# TODO allow subclassing and support entities with __slots__ defined
2934
variables = dict(vars(self.cls))
3035

3136
# filter only subclasses of Property
3237
variables = {k: v for k, v in variables.items() if issubclass(type(v), Property)}
3338

34-
for k, v in variables.items():
35-
v._name = k
36-
self.properties.append(v)
39+
for k, prop in variables.items():
40+
prop._name = k
41+
self.properties.append(prop)
42+
43+
if prop._is_id:
44+
if self.id_property:
45+
raise Exception("duplicate ID property: '%s' and '%s'" % (self.id_property._name, prop._name))
46+
self.id_property = prop
47+
48+
if prop._fb_type == flatbuffers.number_types.UOffsetTFlags:
49+
assert prop._ob_type in [OBXPropertyType_String, OBXPropertyType_ByteVector], \
50+
"programming error - invalid type OB & FB type combination"
51+
self.offset_properties.append(prop)
52+
53+
if not self.id_property:
54+
raise Exception("ID property is not defined")
55+
elif self.id_property._ob_type != OBXPropertyType_Long:
56+
raise Exception("ID property must be an int")
57+
58+
def get_value(self, object, prop: Property):
59+
# in case value is not overwritten on the object, it's the Property object itself (= as defined in the Class)
60+
val = getattr(object, prop._name)
61+
if val == prop:
62+
return prop._py_type() # default (empty) value for the given type
63+
return val
64+
65+
def get_object_id(self, object) -> int:
66+
return self.get_value(object, self.id_property)
67+
68+
def set_object_id(self, object, id: int):
69+
setattr(object, self.id_property._name, id)
70+
71+
def marshal(self, object, id: int) -> bytearray:
72+
builder = flatbuffers.Builder(256)
73+
74+
# prepare some properties that need to be built in FB before starting the main object
75+
offsets = {}
76+
for prop in self.offset_properties:
77+
val = self.get_value(object, prop)
78+
if prop._ob_type == OBXPropertyType_String:
79+
offsets[prop._id] = builder.CreateString(val.encode('utf-8'))
80+
elif prop._ob_type == OBXPropertyType_ByteVector:
81+
offsets[prop._id] = builder.CreateByteVector(val)
82+
else:
83+
assert False, "programming error - invalid type OB & FB type combination"
84+
85+
# start the FlatBuffers object with the largest number of properties that were ever present in the Entity
86+
builder.StartObject(self.last_property_id.id)
87+
88+
# add properties to the FB object
89+
for prop in self.properties:
90+
if prop._id in offsets:
91+
val = offsets[prop._id]
92+
if val:
93+
builder.PrependUOffsetTRelative(val)
94+
else:
95+
val = id if prop == self.id_property else self.get_value(object, prop)
96+
builder.Prepend(prop._fb_type, val)
97+
98+
builder.Slot(prop._fb_slot)
99+
100+
builder.Finish(builder.EndObject())
101+
return builder.Output()
102+
103+
def unmarshal(self, data: bytes):
104+
pos = flatbuffers.encode.Get(flatbuffers.packer.uoffset, data, 0)
105+
table = flatbuffers.Table(data, pos)
106+
107+
# initialize an empty object
108+
obj = self.cls()
109+
110+
# fill it with the data read from FlatBuffers
111+
for prop in self.properties:
112+
o = table.Offset(prop._fb_v_offset)
113+
val = None
114+
if not o:
115+
val = prop._py_type() # use default (empty) value if not present in the object
116+
elif prop._ob_type == OBXPropertyType_String:
117+
val = table.String(o + table.Pos).decode('utf-8')
118+
elif prop._ob_type == OBXPropertyType_ByteVector:
119+
# access the FB byte vector information
120+
start = table.Vector(o)
121+
size = table.VectorLen(o)
122+
123+
# slice the vector as a requested type
124+
# TODO test this (immutability, memory safe, etc)
125+
val = prop._py_type(table.Bytes[start:start+size])
126+
else:
127+
val = table.Get(prop._fb_type, o + table.Pos)
128+
129+
setattr(obj, prop._name, val)
130+
return obj
37131

38132

39-
# wrap _Entity to allow @Entity(id=, uid=), i.e. no class argument
133+
# entity decorator - wrap _Entity to allow @Entity(id=, uid=), i.e. no class argument
40134
def Entity(cls=None, id: int = 0, uid: int = 0):
41135
if cls:
42136
return _Entity(cls, id, uid)

objectbox/model/model.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ def __init__(self):
2525

2626
def entity(self, entity: _Entity, last_property_id: IdUid):
2727
if not isinstance(entity, _Entity):
28-
raise ValueError("Given type is not an Entity. Are you passing an instance instead of a type or did you "
28+
raise Exception("Given type is not an Entity. Are you passing an instance instead of a type or did you "
2929
"forget the '@Entity' annotation?")
3030

31+
entity.last_property_id = last_property_id
32+
3133
obx_model_entity(self._c_model, c_str(entity.name), entity.id, entity.uid)
3234

3335
for v in entity.properties:

0 commit comments

Comments
 (0)