Skip to content

Commit 402cbd5

Browse files
committed
Merge branch '29-entity-uniform-api-for-accessing-entity-properties' into dev
2 parents f94d1e4 + 4411cdf commit 402cbd5

File tree

12 files changed

+336
-232
lines changed

12 files changed

+336
-232
lines changed

example/ollama/llamas.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,8 @@ class DocumentEmbedding:
5757
model="mxbai-embed-large"
5858
)
5959

60-
61-
embedding_prop: Property = DocumentEmbedding.get_property("embedding")
6260
query = box.query(
63-
embedding_prop.nearest_neighbor(response["embedding"], 1)
61+
DocumentEmbedding.embedding.nearest_neighbor(response["embedding"], 1)
6462
).build()
6563

6664
results = query.find_with_scores()

example/vectorsearch-cities/__main__.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def __init__(self, *args):
2525
new_db = not os.path.exists(dbdir)
2626
self._store = objectbox.Store(model=get_objectbox_model(),directory=dbdir)
2727
self._box = self._store.box(City)
28-
self._name_prop: Property = City.get_property("name")
29-
self._location_prop: Property = City.get_property("location")
3028
if new_db:
3129
with open(os.path.join(os.path.dirname(__file__), 'cities.csv')) as f:
3230
r = csv.reader(f)
@@ -40,8 +38,7 @@ def __init__(self, *args):
4038

4139
def do_ls(self, name: str = ""):
4240
"""list all cities or starting with <prefix>\nusage: ls [<prefix>]"""
43-
qb = self._box.query()
44-
qb.starts_with_string(self._name_prop, name)
41+
qb = self._box.query( City.name.starts_with(name) )
4542
query = qb.build()
4643
list_cities(query.find())
4744

@@ -57,15 +54,15 @@ def do_city_neighbors(self, args: str):
5754
num = 5
5855
if len(args) == 2:
5956
num = int(args[1])
60-
qb = self._box.query()
61-
qb.equals_string(self._name_prop, city)
57+
qb = self._box.query( City.name.equals(city) )
6258
query = qb.build()
6359
cities = query.find()
6460
if len(cities) == 1:
6561
location = cities[0].location
66-
qb = self._box.query()
67-
qb.nearest_neighbors_f32(self._location_prop, location, num+1) # +1 for the city
68-
qb.not_equals_string(self._name_prop, city)
62+
# +1 for the city
63+
qb = self._box.query(
64+
City.location.nearest_neighbor(location, num+1) & City.name.not_equals(city)
65+
)
6966
neighbors = qb.build().find_with_scores()
7067
list_cities_with_scores(neighbors)
7168
else:
@@ -81,8 +78,9 @@ def do_neighbors(self, args):
8178
raise ValueError()
8279
num = int(args[0])
8380
geocoord = [ float(args[1]), float(args[2]) ]
84-
qb = self._box.query()
85-
qb.nearest_neighbors_f32(self._location_prop, geocoord, num)
81+
qb = self._box.query(
82+
City.location.nearest_neighbor(geocoord, num)
83+
)
8684
neighbors = qb.build().find_with_scores()
8785
list_cities_with_scores(neighbors)
8886
except ValueError:

objectbox/box.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, store: Store, entity: _Entity):
2727

2828
self._store = store
2929
self._entity = entity
30-
self._c_box = obx_box(store._c_store, entity.id)
30+
self._c_box = obx_box(store._c_store, entity._id)
3131

3232
def is_empty(self) -> bool:
3333
is_empty = ctypes.c_bool()
@@ -50,16 +50,16 @@ def put(self, *objects):
5050
return self._put_one(objects[0])
5151

5252
def _put_one(self, obj) -> int:
53-
id = object_id = self._entity.get_object_id(obj)
53+
id = object_id = self._entity._get_object_id(obj)
5454

5555
if not id:
5656
id = obx_box_id_for_put(self._c_box, 0)
5757

58-
data = self._entity.marshal(obj, id)
58+
data = self._entity._marshal(obj, id)
5959
obx_box_put(self._c_box, id, bytes(data), len(data))
6060

6161
if id != object_id:
62-
self._entity.set_object_id(obj, id)
62+
self._entity._set_object_id(obj, id)
6363

6464
return id
6565

@@ -68,7 +68,7 @@ def _put_many(self, objects) -> None:
6868
new = {}
6969
ids = {}
7070
for k in range(len(objects)):
71-
id = self._entity.get_object_id(objects[k])
71+
id = self._entity._get_object_id(objects[k])
7272
if not id:
7373
new[k] = 0
7474
ids[k] = id
@@ -90,7 +90,7 @@ def _put_many(self, objects) -> None:
9090
# we need to keep the data around until put_many is executed because obx_bytes_array_set doesn't do a copy
9191
data = {}
9292
for k in range(len(objects)):
93-
data[k] = bytes(self._entity.marshal(objects[k], ids[k]))
93+
data[k] = bytes(self._entity._marshal(objects[k], ids[k]))
9494
key = ctypes.c_size_t(k)
9595

9696
# OBX_bytes_array.data[k] = data
@@ -106,7 +106,7 @@ def _put_many(self, objects) -> None:
106106

107107
# assign new IDs on the object
108108
for k in new.keys():
109-
self._entity.set_object_id(objects[k], ids[k])
109+
self._entity._set_object_id(objects[k], ids[k])
110110

111111
def get(self, id: int):
112112
with self._store.read_tx():
@@ -119,7 +119,7 @@ def get(self, id: int):
119119
elif code != 0:
120120
raise CoreException(code)
121121
data = c_voidp_as_bytes(c_data, c_size.value)
122-
return self._entity.unmarshal(data)
122+
return self._entity._unmarshal(data)
123123

124124
def get_all(self) -> list:
125125
with self._store.read_tx():
@@ -135,15 +135,15 @@ def get_all(self) -> list:
135135
# OBX_bytes
136136
c_bytes = c_bytes_array.data[i]
137137
data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
138-
result.append(self._entity.unmarshal(data))
138+
result.append(self._entity._unmarshal(data))
139139

140140
return result
141141
finally:
142142
obx_bytes_array_free(c_bytes_array_p)
143143

144144
def remove(self, id_or_object) -> bool:
145-
if isinstance(id_or_object, self._entity.user_type):
146-
id = self._entity.get_object_id(id_or_object)
145+
if isinstance(id_or_object, self._entity._user_type):
146+
id = self._entity._get_object_id(id_or_object)
147147
else:
148148
id = id_or_object
149149
code : obx_err = obx_box_remove(self._c_box, id)

objectbox/model/entity.py

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -31,59 +31,66 @@
3131
# _Entity class holds model information as well as conversions between python objects and FlatBuffers (ObjectBox data)
3232
class _Entity(object):
3333
def __init__(self, user_type, uid: int = 0):
34-
self.user_type = user_type
35-
self.iduid = IdUid(0, uid)
36-
self.name = user_type.__name__
37-
self.last_property_iduid = IdUid(0, 0)
38-
39-
self.properties: List[Property] = list() # List[Property]
40-
self.offset_properties = list() # List[Property]
41-
self.id_property = None
42-
self.fill_properties()
34+
self._user_type = user_type
35+
self._iduid = IdUid(0, uid)
36+
self._name = user_type.__name__
37+
self._last_property_iduid = IdUid(0, 0)
38+
39+
self._properties: List[Property] = list() # List[Property]
40+
self._offset_properties = list() # List[Property]
41+
self._id_property = None
42+
self._fill_properties()
4343
self._tl = threading.local()
4444

4545
@property
46-
def id(self) -> int:
47-
return self.iduid.id
46+
def _id(self) -> int:
47+
return self._iduid.id
4848

4949
@property
50-
def uid(self) -> int:
51-
return self.iduid.uid
50+
def _uid(self) -> int:
51+
return self._iduid.uid
5252

53-
def has_uid(self) -> bool:
54-
return self.iduid.uid != 0
53+
def _has_uid(self) -> bool:
54+
return self._iduid.uid != 0
5555

56-
def on_sync(self):
56+
def _on_sync(self):
5757
""" Method called once ID/UID are synced with the model file. """
58-
assert self.iduid.is_assigned()
59-
for prop in self.properties:
58+
assert self._iduid.is_assigned()
59+
for prop in self._properties:
6060
prop.on_sync()
6161

6262
def __call__(self, **properties):
6363
""" The constructor of the user Entity class. """
64-
object_ = self.user_type()
64+
object_ = self._user_type()
6565
for prop_name, prop_val in properties.items():
6666
if not hasattr(object_, prop_name):
67-
raise Exception(f"Entity {self.name} has no property \"{prop_name}\"")
67+
raise Exception(f"Entity {self._name} has no property \"{prop_name}\"")
6868
setattr(object_, prop_name, prop_val)
6969
return object_
7070

71-
def fill_properties(self):
71+
def __getattr__(self, name):
72+
""" Overload to get properties via "<Entity>.<Prop>" notation. """
73+
for prop in self._properties:
74+
if prop.name == name:
75+
return prop
76+
return self.__getattribute__(name)
77+
78+
def _fill_properties(self):
7279
# TODO allow subclassing and support entities with __slots__ defined
73-
variables = dict(vars(self.user_type))
80+
variables = dict(vars(self._user_type))
7481

7582
# filter only subclasses of Property
7683
variables = {k: v for k, v in variables.items(
7784
) if issubclass(type(v), Property)}
7885

7986
for prop_name, prop in variables.items():
8087
prop.name = prop_name
81-
self.properties.append(prop)
88+
self._properties.append(prop)
8289

8390
if prop.is_id():
84-
if self.id_property:
85-
raise Exception(f"Duplicate ID property: \"{self.id_property.name}\" and \"{prop.name}\"")
86-
self.id_property = prop
91+
if self._id_property:
92+
raise Exception(f"Duplicate ID property: \"{self._id_property.name}\" and \"{prop.name}\"")
93+
self._id_property = prop
8794

8895
if prop._fb_type == flatbuffers.number_types.UOffsetTFlags:
8996
assert prop._ob_type in [
@@ -98,34 +105,34 @@ def fill_properties(self):
98105
OBXPropertyType_DoubleVector,
99106
OBXPropertyType_Flex,
100107
], "programming error - invalid type OB & FB type combination"
101-
self.offset_properties.append(prop)
108+
self._offset_properties.append(prop)
102109

103-
# print('Property {}.{}: {} (ob:{} fb:{})'.format(self.name, prop.name, prop._py_type, prop._ob_type, prop._fb_type))
110+
# print('Property {}.{}: {} (ob:{} fb:{})'.format(self._name, prop.name, prop._py_type, prop._ob_type, prop._fb_type))
104111

105-
if not self.id_property:
112+
if not self._id_property:
106113
raise Exception("ID property is not defined")
107-
elif self.id_property._ob_type != OBXPropertyType_Long:
114+
elif self._id_property._ob_type != OBXPropertyType_Long:
108115
raise Exception("ID property must be an int")
109116

110-
def get_property(self, name: str):
117+
def _get_property(self, name: str):
111118
""" Gets the property having the given name. """
112-
for prop in self.properties:
119+
for prop in self._properties:
113120
if prop.name == name:
114121
return prop
115-
raise Exception(f"Property \"{name}\" not found in Entity: \"{self.name}\"")
122+
raise Exception(f"Property \"{name}\" not found in Entity: \"{self._name}\"")
116123

117-
def get_property_id(self, prop: Union[int, str, Property]) -> int:
124+
def _get_property_id(self, prop: Union[int, str, Property]) -> int:
118125
""" A convenient way to get the property ID regardless having its ID, name or Property. """
119126
if isinstance(prop, int):
120127
return prop # We already have it!
121128
elif isinstance(prop, str):
122-
return self.get_property(prop).id
129+
return self._get_property(prop).id
123130
elif isinstance(prop, Property):
124131
return prop.id
125132
else:
126133
raise Exception(f"Unsupported Property type: {type(prop)}")
127134

128-
def get_value(self, object, prop: Property):
135+
def _get_value(self, object, prop: Property):
129136
# in case value is not overwritten on the object, it's the Property object itself (= as defined in the Class)
130137
val = getattr(object, prop.name)
131138
if prop._py_type == np.ndarray:
@@ -140,22 +147,22 @@ def get_value(self, object, prop: Property):
140147
return prop._py_type() # default (empty) value for the given type
141148
return val
142149

143-
def get_object_id(self, obj) -> int:
144-
return self.get_value(obj, self.id_property)
150+
def _get_object_id(self, obj) -> int:
151+
return self._get_value(obj, self._id_property)
145152

146-
def set_object_id(self, obj, id_: int):
147-
setattr(obj, self.id_property.name, id_)
153+
def _set_object_id(self, obj, id_: int):
154+
setattr(obj, self._id_property.name, id_)
148155

149-
def marshal(self, object, id: int) -> bytearray:
156+
def _marshal(self, object, id: int) -> bytearray:
150157
if not hasattr(self._tl, "builder"):
151158
self._tl.builder = flatbuffers.Builder(256)
152159
builder = self._tl.builder
153160
builder.Clear()
154161

155162
# prepare some properties that need to be built in FB before starting the main object
156163
offsets = {}
157-
for prop in self.offset_properties:
158-
val = self.get_value(object, prop)
164+
for prop in self._offset_properties:
165+
val = self._get_value(object, prop)
159166
if prop._ob_type == OBXPropertyType_String:
160167
offsets[prop.id] = builder.CreateString(val.encode('utf-8'))
161168
elif prop._ob_type == OBXPropertyType_BoolVector:
@@ -185,17 +192,17 @@ def marshal(self, object, id: int) -> bytearray:
185192
assert False, "programming error - invalid type OB & FB type combination"
186193

187194
# start the FlatBuffers object with the largest number of properties that were ever present in the Entity
188-
builder.StartObject(self.last_property_iduid.id)
195+
builder.StartObject(self._last_property_iduid.id)
189196

190197
# add properties to the FB object
191-
for prop in self.properties:
198+
for prop in self._properties:
192199
prop_id = prop.id
193200
if prop_id in offsets:
194201
val = offsets[prop_id]
195202
if val:
196203
builder.PrependUOffsetTRelative(val)
197204
else:
198-
val = id if prop == self.id_property else self.get_value(object, prop)
205+
val = id if prop == self._id_property else self._get_value(object, prop)
199206
if prop._ob_type == OBXPropertyType_Date:
200207
val = date_value_to_int(val, 1000) # convert to milliseconds
201208
elif prop._ob_type == OBXPropertyType_DateNano:
@@ -207,15 +214,15 @@ def marshal(self, object, id: int) -> bytearray:
207214
builder.Finish(builder.EndObject())
208215
return builder.Output()
209216

210-
def unmarshal(self, data: bytes):
217+
def _unmarshal(self, data: bytes):
211218
pos = flatbuffers.encode.Get(flatbuffers.packer.uoffset, data, 0)
212219
table = flatbuffers.Table(data, pos)
213220

214221
# initialize an empty object
215-
obj = self.user_type()
222+
obj = self._user_type()
216223

217224
# fill it with the data read from FlatBuffers
218-
for prop in self.properties:
225+
for prop in self._properties:
219226
o = table.Offset(prop._fb_v_offset)
220227
val = None
221228
ob_type = prop._ob_type

0 commit comments

Comments
 (0)