objectbox-api 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- objectbox/__init__.py +145 -0
- objectbox/box.py +294 -0
- objectbox/builder.py +42 -0
- objectbox/c.py +1572 -0
- objectbox/condition.py +300 -0
- objectbox/exceptions.py +238 -0
- objectbox/experimental.py +78 -0
- objectbox/logger.py +18 -0
- objectbox/model/__init__.py +63 -0
- objectbox/model/codegen.py +399 -0
- objectbox/model/entity.py +490 -0
- objectbox/model/fast_builder.py +324 -0
- objectbox/model/idsync.py +401 -0
- objectbox/model/iduid.py +32 -0
- objectbox/model/model.py +158 -0
- objectbox/model/properties.py +865 -0
- objectbox/objectbox.py +24 -0
- objectbox/query.py +468 -0
- objectbox/query_builder.py +450 -0
- objectbox/store.py +400 -0
- objectbox/store_options.py +136 -0
- objectbox/sync.py +1101 -0
- objectbox/transaction.py +60 -0
- objectbox/utils.py +60 -0
- objectbox/version.py +39 -0
- objectbox_api-5.0.0.dist-info/METADATA +63 -0
- objectbox_api-5.0.0.dist-info/RECORD +30 -0
- objectbox_api-5.0.0.dist-info/WHEEL +5 -0
- objectbox_api-5.0.0.dist-info/licenses/LICENSE +202 -0
- objectbox_api-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Copyright 2019-2024 ObjectBox Ltd. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import inspect
|
|
15
|
+
|
|
16
|
+
from array import array
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
import logging
|
|
19
|
+
from objectbox import experimental
|
|
20
|
+
from objectbox.c import *
|
|
21
|
+
from objectbox.model.codegen import CodegenBuilder, same_values, absent_value_factory
|
|
22
|
+
from objectbox.model.fast_builder import FastBuilder
|
|
23
|
+
from objectbox.model.iduid import IdUid
|
|
24
|
+
from objectbox.model.properties import Property
|
|
25
|
+
from objectbox.utils import date_value_to_int
|
|
26
|
+
import threading
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def camel_case(name: str) -> str:
|
|
31
|
+
""" Converts a snake_case name to camelCase, e.g. "date_created" to "dateCreated" (or "vec_2d" to "vec2d").
|
|
32
|
+
Leading and trailing underscores are kept, e.g. "_internal_value" becomes "_internalValue". """
|
|
33
|
+
core = name.strip("_")
|
|
34
|
+
if "_" not in core:
|
|
35
|
+
return name
|
|
36
|
+
leading = name[:len(name) - len(name.lstrip("_"))]
|
|
37
|
+
trailing = name[len(name.rstrip("_")):]
|
|
38
|
+
parts = [part for part in core.split("_") if part]
|
|
39
|
+
return leading + parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:]) + trailing
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# _Entity class holds model information as well as conversions between python objects and FlatBuffers (ObjectBox data)
|
|
43
|
+
class _Entity(object):
|
|
44
|
+
def __init__(self, user_type, uid: int = 0, python_names: bool = False):
|
|
45
|
+
self._user_type = user_type
|
|
46
|
+
self._iduid = IdUid(0, uid)
|
|
47
|
+
self._name = user_type.__name__
|
|
48
|
+
self._python_names = python_names
|
|
49
|
+
self._last_property_iduid = IdUid(0, 0)
|
|
50
|
+
|
|
51
|
+
self._properties: List[Property] = list() # List[Property]
|
|
52
|
+
self._offset_properties = list() # List[Property]
|
|
53
|
+
self._id_property = None
|
|
54
|
+
self._fill_properties()
|
|
55
|
+
self._tl = threading.local()
|
|
56
|
+
self._fast_builder = None # experimental; created on first use after a sync
|
|
57
|
+
self._codegen_builder = None # experimental; created on first use after a sync
|
|
58
|
+
self._flags = 0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _id(self) -> int:
|
|
62
|
+
return self._iduid.id
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def _uid(self) -> int:
|
|
66
|
+
return self._iduid.uid
|
|
67
|
+
|
|
68
|
+
def _has_uid(self) -> bool:
|
|
69
|
+
return self._iduid.uid != 0
|
|
70
|
+
|
|
71
|
+
def _on_sync(self):
|
|
72
|
+
""" Method called once ID/UID are synced with the model file. """
|
|
73
|
+
assert self._iduid.is_assigned()
|
|
74
|
+
for prop in self._properties:
|
|
75
|
+
prop.on_sync()
|
|
76
|
+
self._fast_builder = None # IDs (FlatBuffers slots) may have changed
|
|
77
|
+
self._codegen_builder = None
|
|
78
|
+
|
|
79
|
+
def __call__(self, **properties):
|
|
80
|
+
""" The constructor of the user Entity class. """
|
|
81
|
+
object_ = self._user_type()
|
|
82
|
+
for prop_name, prop_val in properties.items():
|
|
83
|
+
if not hasattr(object_, prop_name):
|
|
84
|
+
raise TypeError(f"Entity {self._name} has no property \"{prop_name}\"")
|
|
85
|
+
setattr(object_, prop_name, prop_val)
|
|
86
|
+
return object_
|
|
87
|
+
|
|
88
|
+
def __getattr__(self, name):
|
|
89
|
+
""" Overload to get properties via "<Entity>.<Prop>" notation. """
|
|
90
|
+
for prop in self._properties:
|
|
91
|
+
if prop.name == name:
|
|
92
|
+
return prop
|
|
93
|
+
return self.__getattribute__(name)
|
|
94
|
+
|
|
95
|
+
def _fill_properties(self):
|
|
96
|
+
# TODO allow subclassing and support entities with __slots__ defined
|
|
97
|
+
variables = dict(vars(self._user_type))
|
|
98
|
+
|
|
99
|
+
# filter only subclasses of Property
|
|
100
|
+
variables = {k: v for k, v in variables.items(
|
|
101
|
+
) if issubclass(type(v), Property)}
|
|
102
|
+
|
|
103
|
+
props_by_db_name = {}
|
|
104
|
+
for prop_name, prop in variables.items():
|
|
105
|
+
prop.name = prop_name
|
|
106
|
+
prop._entity = self
|
|
107
|
+
prop._db_name = prop_name if self._python_names else camel_case(prop_name)
|
|
108
|
+
other = props_by_db_name.get(prop._db_name)
|
|
109
|
+
if other is not None:
|
|
110
|
+
raise ValueError(f"Properties \"{other.name}\" and \"{prop.name}\" of entity {self._name} both have the "
|
|
111
|
+
f"name \"{prop._db_name}\" in the database (camelCase); rename one of them or use "
|
|
112
|
+
f"@Entity(python_names=True)")
|
|
113
|
+
props_by_db_name[prop._db_name] = prop
|
|
114
|
+
self._properties.append(prop)
|
|
115
|
+
|
|
116
|
+
if prop.is_id():
|
|
117
|
+
if self._id_property:
|
|
118
|
+
raise ValueError(f"Duplicate ID property: \"{self._id_property.name}\" and \"{prop.name}\"")
|
|
119
|
+
self._id_property = prop
|
|
120
|
+
|
|
121
|
+
if prop._fb_format == 'I': # an offset
|
|
122
|
+
assert prop._ob_type in [
|
|
123
|
+
OBXPropertyType_String,
|
|
124
|
+
OBXPropertyType_BoolVector,
|
|
125
|
+
OBXPropertyType_ByteVector,
|
|
126
|
+
OBXPropertyType_ShortVector,
|
|
127
|
+
OBXPropertyType_CharVector,
|
|
128
|
+
OBXPropertyType_IntVector,
|
|
129
|
+
OBXPropertyType_LongVector,
|
|
130
|
+
OBXPropertyType_FloatVector,
|
|
131
|
+
OBXPropertyType_DoubleVector,
|
|
132
|
+
OBXPropertyType_Flex,
|
|
133
|
+
], "programming error - invalid type OB & FB type combination"
|
|
134
|
+
self._offset_properties.append(prop)
|
|
135
|
+
|
|
136
|
+
# print('Property {}.{}: {} (ob:{} fb:{})'.format(self._name, prop.name, prop._py_type, prop._ob_type, prop._fb_type))
|
|
137
|
+
|
|
138
|
+
if not self._id_property:
|
|
139
|
+
raise ValueError(f"Entity {self._name} has no ID property (e.g. id = Id)")
|
|
140
|
+
elif self._id_property._ob_type != OBXPropertyType_Long:
|
|
141
|
+
raise TypeError(f"The ID property of entity {self._name} must be an int")
|
|
142
|
+
|
|
143
|
+
def _get_property(self, name: str):
|
|
144
|
+
""" Gets the property having the given name. """
|
|
145
|
+
for prop in self._properties:
|
|
146
|
+
if prop.name == name:
|
|
147
|
+
return prop
|
|
148
|
+
from objectbox.exceptions import IllegalArgumentError
|
|
149
|
+
raise IllegalArgumentError(f"Property \"{name}\" not found in Entity: \"{self._name}\"")
|
|
150
|
+
|
|
151
|
+
def _get_property_id(self, prop: Union[int, str, Property]) -> int:
|
|
152
|
+
""" A convenient way to get the property ID regardless having its ID, name or Property. """
|
|
153
|
+
if isinstance(prop, int):
|
|
154
|
+
return prop # We already have it!
|
|
155
|
+
elif isinstance(prop, str):
|
|
156
|
+
return self._get_property(prop).id
|
|
157
|
+
elif isinstance(prop, Property):
|
|
158
|
+
return prop.id
|
|
159
|
+
else:
|
|
160
|
+
raise TypeError(f"Expected a property (or its name or ID), got {type(prop)}")
|
|
161
|
+
|
|
162
|
+
def _get_value(self, object, prop: Property):
|
|
163
|
+
# in case value is not overwritten on the object, it's the Property object itself (= as defined in the Class)
|
|
164
|
+
val = getattr(object, prop.name)
|
|
165
|
+
if prop._is_ndarray:
|
|
166
|
+
import numpy as np # imported already: _py_type is numpy.ndarray
|
|
167
|
+
if (val == np.array(prop)).all():
|
|
168
|
+
return np.array([])
|
|
169
|
+
elif val == prop:
|
|
170
|
+
if prop._ob_type == OBXPropertyType_Date or prop._ob_type == OBXPropertyType_DateNano:
|
|
171
|
+
return 0.0 # For marshalling, prefer float over datetime
|
|
172
|
+
elif prop._ob_type == OBXPropertyType_Flex:
|
|
173
|
+
return None
|
|
174
|
+
else:
|
|
175
|
+
return prop._py_type() # default (empty) value for the given type
|
|
176
|
+
return val
|
|
177
|
+
|
|
178
|
+
def _get_object_id(self, obj) -> int:
|
|
179
|
+
return self._get_value(obj, self._id_property)
|
|
180
|
+
|
|
181
|
+
def _set_object_id(self, obj, id_: int):
|
|
182
|
+
setattr(obj, self._id_property.name, id_)
|
|
183
|
+
|
|
184
|
+
def _marshal(self, object, id: int) -> Union[bytes, bytearray]:
|
|
185
|
+
mode = experimental._flatbuffers_mode
|
|
186
|
+
if mode is None:
|
|
187
|
+
return self._marshal_builder(object, id)
|
|
188
|
+
# Concurrent first calls may create one builder each; equal, so either is fine.
|
|
189
|
+
# Created (and codegen compiled) outside the try below: a failure there must raise, not fall back silently.
|
|
190
|
+
if mode == experimental.CODEGEN:
|
|
191
|
+
builder = self._codegen_builder
|
|
192
|
+
if builder is None:
|
|
193
|
+
builder = self._codegen_builder = CodegenBuilder(self)
|
|
194
|
+
else:
|
|
195
|
+
builder = self._fast_builder
|
|
196
|
+
if builder is None:
|
|
197
|
+
builder = self._fast_builder = FastBuilder(self)
|
|
198
|
+
marshal = builder.marshal
|
|
199
|
+
try:
|
|
200
|
+
data = marshal(object, id)
|
|
201
|
+
except Exception:
|
|
202
|
+
# Values the experimental builders reject (e.g. out of range): the reference raises its usual error
|
|
203
|
+
data = None
|
|
204
|
+
verify = experimental._flatbuffers_verify
|
|
205
|
+
if data is None or verify:
|
|
206
|
+
reference = bytes(self._marshal_builder(object, id))
|
|
207
|
+
if data is None:
|
|
208
|
+
if verify:
|
|
209
|
+
raise AssertionError(f"Experimental FlatBuffers {mode} failed for {self._name}, "
|
|
210
|
+
f"but the reference did not")
|
|
211
|
+
return reference
|
|
212
|
+
if data != reference:
|
|
213
|
+
raise AssertionError(f"Experimental FlatBuffers {mode}: bytes differ from the reference for "
|
|
214
|
+
f"{self._name}:\n {mode}: {data.hex()}\n reference: {reference.hex()}")
|
|
215
|
+
return data
|
|
216
|
+
|
|
217
|
+
def _marshal_builder(self, object, id: int) -> bytearray:
|
|
218
|
+
"""Marshals with the generic flatbuffers.Builder; the reference for the experimental fast builder."""
|
|
219
|
+
import flatbuffers # imported on first use: flatbuffers imports numpy anyway
|
|
220
|
+
import numpy as np
|
|
221
|
+
if not hasattr(self._tl, "builder"):
|
|
222
|
+
self._tl.builder = flatbuffers.Builder(256)
|
|
223
|
+
builder = self._tl.builder
|
|
224
|
+
builder.Clear()
|
|
225
|
+
|
|
226
|
+
# prepare some properties that need to be built in FB before starting the main object
|
|
227
|
+
offsets = {}
|
|
228
|
+
nulls = set() # IDs of properties set to None: left out of the object (null; Flex stores None as a value)
|
|
229
|
+
for prop in self._offset_properties:
|
|
230
|
+
val = self._get_value(object, prop)
|
|
231
|
+
if val is None and prop._ob_type != OBXPropertyType_Flex:
|
|
232
|
+
nulls.add(prop.id)
|
|
233
|
+
continue
|
|
234
|
+
if prop._ob_type == OBXPropertyType_String:
|
|
235
|
+
offsets[prop.id] = builder.CreateString(val.encode('utf-8'))
|
|
236
|
+
elif prop._ob_type == OBXPropertyType_BoolVector:
|
|
237
|
+
# Using a numpy bool as it seems to be more consistent in terms of size. TBD
|
|
238
|
+
# https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.bool
|
|
239
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.bool_))
|
|
240
|
+
elif prop._ob_type == OBXPropertyType_ByteVector:
|
|
241
|
+
if prop._py_type == list: # Int8List: signed bytes
|
|
242
|
+
val = array('b', val).tobytes()
|
|
243
|
+
offsets[prop.id] = builder.CreateByteVector(val)
|
|
244
|
+
elif prop._ob_type == OBXPropertyType_ShortVector:
|
|
245
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int16))
|
|
246
|
+
elif prop._ob_type == OBXPropertyType_CharVector:
|
|
247
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.uint16))
|
|
248
|
+
elif prop._ob_type == OBXPropertyType_IntVector:
|
|
249
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int32))
|
|
250
|
+
elif prop._ob_type == OBXPropertyType_LongVector:
|
|
251
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.int64))
|
|
252
|
+
elif prop._ob_type == OBXPropertyType_FloatVector:
|
|
253
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.float32))
|
|
254
|
+
elif prop._ob_type == OBXPropertyType_DoubleVector:
|
|
255
|
+
offsets[prop.id] = builder.CreateNumpyVector(np.array(val, dtype=np.float64))
|
|
256
|
+
elif prop._ob_type == OBXPropertyType_Flex:
|
|
257
|
+
import flatbuffers.flexbuffers
|
|
258
|
+
flex_builder = flatbuffers.flexbuffers.Builder()
|
|
259
|
+
flex_builder.Add(val)
|
|
260
|
+
buffer = flex_builder.Finish()
|
|
261
|
+
offsets[prop.id] = builder.CreateByteVector(bytes(buffer))
|
|
262
|
+
else:
|
|
263
|
+
assert False, "programming error - invalid type OB & FB type combination"
|
|
264
|
+
|
|
265
|
+
# start the FlatBuffers object with the largest number of properties that were ever present in the Entity
|
|
266
|
+
builder.StartObject(self._last_property_iduid.id)
|
|
267
|
+
|
|
268
|
+
# add properties to the FB object
|
|
269
|
+
for prop in self._properties:
|
|
270
|
+
prop_id = prop.id
|
|
271
|
+
if prop_id in offsets:
|
|
272
|
+
val = offsets[prop_id]
|
|
273
|
+
if val:
|
|
274
|
+
builder.PrependUOffsetTRelative(val)
|
|
275
|
+
elif prop_id in nulls:
|
|
276
|
+
continue
|
|
277
|
+
else:
|
|
278
|
+
val = id if prop == self._id_property else self._get_value(object, prop)
|
|
279
|
+
if val is None:
|
|
280
|
+
continue # null: no field, no vtable slot
|
|
281
|
+
if prop._ob_type == OBXPropertyType_Date:
|
|
282
|
+
val = date_value_to_int(val, 1000) # convert to milliseconds
|
|
283
|
+
elif prop._ob_type == OBXPropertyType_DateNano:
|
|
284
|
+
val = date_value_to_int(val, 1000000000) # convert to nanoseconds
|
|
285
|
+
builder.Prepend(prop._fb_type, val)
|
|
286
|
+
|
|
287
|
+
builder.Slot(prop._fb_slot)
|
|
288
|
+
|
|
289
|
+
builder.Finish(builder.EndObject())
|
|
290
|
+
return builder.Output()
|
|
291
|
+
|
|
292
|
+
def _unmarshal(self, data: bytes):
|
|
293
|
+
if experimental._flatbuffers_mode != experimental.CODEGEN:
|
|
294
|
+
return self._unmarshal_table(data)
|
|
295
|
+
codegen = self._codegen_builder
|
|
296
|
+
if codegen is None: # as in _marshal: outside the try, concurrent first calls are fine
|
|
297
|
+
codegen = self._codegen_builder = CodegenBuilder(self)
|
|
298
|
+
unmarshal = codegen.unmarshal # compiled on first use, outside the try (as in _marshal)
|
|
299
|
+
try:
|
|
300
|
+
obj = unmarshal(data)
|
|
301
|
+
failed = False
|
|
302
|
+
except Exception:
|
|
303
|
+
# Invalid data or values the reference cannot read either: it raises its usual error
|
|
304
|
+
obj, failed = None, True
|
|
305
|
+
verify = experimental._flatbuffers_verify
|
|
306
|
+
if failed or verify:
|
|
307
|
+
reference = self._unmarshal_table(data)
|
|
308
|
+
if failed:
|
|
309
|
+
if verify:
|
|
310
|
+
raise AssertionError(f"Experimental FlatBuffers codegen failed to unmarshal {self._name}, "
|
|
311
|
+
f"but the reference did not")
|
|
312
|
+
return reference
|
|
313
|
+
for prop in self._properties:
|
|
314
|
+
value, expected = getattr(obj, prop.name), getattr(reference, prop.name)
|
|
315
|
+
if not same_values(value, expected):
|
|
316
|
+
raise AssertionError(f"Experimental FlatBuffers codegen: unmarshalled {self._name}.{prop.name} "
|
|
317
|
+
f"differs from the reference:\n codegen: {value!r}\n reference: {expected!r}")
|
|
318
|
+
return obj
|
|
319
|
+
|
|
320
|
+
def _unmarshal_table(self, data: bytes):
|
|
321
|
+
"""Unmarshals with flatbuffers.Table; the reference for the experimental codegen."""
|
|
322
|
+
import flatbuffers # imported on first use: flatbuffers imports numpy (if available)
|
|
323
|
+
pos = flatbuffers.encode.Get(flatbuffers.packer.uoffset, data, 0)
|
|
324
|
+
table = flatbuffers.Table(data, pos)
|
|
325
|
+
|
|
326
|
+
# initialize an empty object
|
|
327
|
+
obj = self._user_type()
|
|
328
|
+
|
|
329
|
+
# fill it with the data read from FlatBuffers
|
|
330
|
+
for prop in self._properties:
|
|
331
|
+
o = table.Offset(prop._fb_v_offset)
|
|
332
|
+
val = None
|
|
333
|
+
ob_type = prop._ob_type
|
|
334
|
+
if not o:
|
|
335
|
+
setattr(obj, prop.name, absent_value_factory(prop)()) # default (empty) value if not present
|
|
336
|
+
continue
|
|
337
|
+
elif ob_type == OBXPropertyType_String:
|
|
338
|
+
val = table.String(o + table.Pos).decode('utf-8')
|
|
339
|
+
elif ob_type == OBXPropertyType_BoolVector:
|
|
340
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.BoolFlags, o)
|
|
341
|
+
elif ob_type == OBXPropertyType_ByteVector:
|
|
342
|
+
# access the FB byte vector information
|
|
343
|
+
start = table.Vector(o)
|
|
344
|
+
size = table.VectorLen(o)
|
|
345
|
+
# slice the vector as a requested type
|
|
346
|
+
if prop._py_type == list: # Int8List: signed bytes
|
|
347
|
+
val = array('b', table.Bytes[start:start + size]) # .tolist() below
|
|
348
|
+
else:
|
|
349
|
+
val = prop._py_type(table.Bytes[start:start + size])
|
|
350
|
+
elif ob_type == OBXPropertyType_ShortVector:
|
|
351
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Int16Flags, o)
|
|
352
|
+
elif ob_type == OBXPropertyType_CharVector:
|
|
353
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Int16Flags, o)
|
|
354
|
+
elif ob_type == OBXPropertyType_Date:
|
|
355
|
+
val = table.Get(prop._fb_type, o + table.Pos) # int
|
|
356
|
+
if prop._py_type == datetime:
|
|
357
|
+
val = datetime.fromtimestamp(val / 1000.0, tz=timezone.utc)
|
|
358
|
+
elif prop._py_type == float:
|
|
359
|
+
val = val / 1000.0
|
|
360
|
+
elif ob_type == OBXPropertyType_DateNano:
|
|
361
|
+
val = table.Get(prop._fb_type, o + table.Pos) # int
|
|
362
|
+
if prop._py_type == datetime:
|
|
363
|
+
val = datetime.fromtimestamp(val / 1000000000.0, tz=timezone.utc)
|
|
364
|
+
elif prop._py_type == float:
|
|
365
|
+
val = val / 1000000000.0
|
|
366
|
+
elif ob_type == OBXPropertyType_IntVector:
|
|
367
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Int32Flags, o)
|
|
368
|
+
elif ob_type == OBXPropertyType_LongVector:
|
|
369
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Int64Flags, o)
|
|
370
|
+
elif ob_type == OBXPropertyType_FloatVector:
|
|
371
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Float32Flags, o)
|
|
372
|
+
elif ob_type == OBXPropertyType_DoubleVector:
|
|
373
|
+
val = table.GetVectorAsNumpy(flatbuffers.number_types.Float64Flags, o)
|
|
374
|
+
elif ob_type == OBXPropertyType_Flex:
|
|
375
|
+
# access the FB byte vector information
|
|
376
|
+
start = table.Vector(o)
|
|
377
|
+
size = table.VectorLen(o)
|
|
378
|
+
# slice the vector as bytes
|
|
379
|
+
buf = table.Bytes[start:start + size]
|
|
380
|
+
import flatbuffers.flexbuffers
|
|
381
|
+
val = flatbuffers.flexbuffers.Loads(buf)
|
|
382
|
+
else:
|
|
383
|
+
val = table.Get(prop._fb_type, o + table.Pos)
|
|
384
|
+
if prop._py_type == list:
|
|
385
|
+
val = val.tolist()
|
|
386
|
+
setattr(obj, prop.name, val)
|
|
387
|
+
return obj
|
|
388
|
+
|
|
389
|
+
# Dictionary of entity types (metadata) collected by the Entity decorator.
|
|
390
|
+
# Note: using a list not a set to keep the order of entities as they were defined (set would not be deterministic).
|
|
391
|
+
obx_models_by_name: Dict[str, List[_Entity]] = {}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Entity flags set on a plain class by a decorator applied before @Entity() (i.e. below it), e.g. @SyncEntity();
|
|
395
|
+
# @Entity() moves them to the _Entity and removes the attribute.
|
|
396
|
+
_ENTITY_FLAGS_ATTR = "_objectbox_entity_flags"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def Entity(uid: int = 0, model: str = "default", python_names: bool = False) -> _Entity:
|
|
400
|
+
"""
|
|
401
|
+
Entity decorator for user classes using syntax ``@Entity([uid=])``
|
|
402
|
+
|
|
403
|
+
Example::
|
|
404
|
+
|
|
405
|
+
@Entity()
|
|
406
|
+
class MyEntity:
|
|
407
|
+
id = Id
|
|
408
|
+
name = String(index=Index())
|
|
409
|
+
date_created = Date
|
|
410
|
+
|
|
411
|
+
Property names are stored in camelCase in the database and the model JSON file (e.g. ``dateCreated`` for the
|
|
412
|
+
attribute ``date_created``), like in the other ObjectBox languages, e.g. to share the model with them for Sync.
|
|
413
|
+
The Python code always uses the attribute names. Set ``python_names=True`` to store the names as written in Python.
|
|
414
|
+
A model JSON file with the other form of a name (e.g. ``date_created`` from versions before 5.0) is matched and
|
|
415
|
+
renamed, keeping the property's data.
|
|
416
|
+
|
|
417
|
+
Wraps the given user classes as an ``_Entity`` helper class.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
def wrapper(class_) -> Callable[[Type], _Entity]:
|
|
421
|
+
# Also allow defining properties as class members; we'll instantiate them here
|
|
422
|
+
class_members = inspect.getmembers(class_, lambda a: (inspect.isclass(a) and issubclass(a, Property)))
|
|
423
|
+
for name, member_type in class_members:
|
|
424
|
+
assert issubclass(member_type, Property)
|
|
425
|
+
# noinspection PyArgumentList
|
|
426
|
+
obj = member_type() # Subclasses of Property have no constructor arguments
|
|
427
|
+
setattr(class_, name, obj)
|
|
428
|
+
|
|
429
|
+
types = obx_models_by_name.get(model)
|
|
430
|
+
if types is None:
|
|
431
|
+
types = []
|
|
432
|
+
obx_models_by_name[model] = types
|
|
433
|
+
|
|
434
|
+
flags = class_.__dict__.get(_ENTITY_FLAGS_ATTR, 0) # Only the class's own, not inherited ones
|
|
435
|
+
if _ENTITY_FLAGS_ATTR in class_.__dict__:
|
|
436
|
+
delattr(class_, _ENTITY_FLAGS_ATTR)
|
|
437
|
+
entity_type = _Entity(class_, uid, python_names)
|
|
438
|
+
entity_type._flags |= flags
|
|
439
|
+
for existing in types:
|
|
440
|
+
if existing._name == entity_type._name:
|
|
441
|
+
# OK for tests, where multiple models are created with the same entity name
|
|
442
|
+
logging.warning(f"Model \"{model}\" already contains an entity type \"{entity_type._name}\"; replacing it.")
|
|
443
|
+
types.remove(existing)
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
obx_models_by_name[model].append(entity_type)
|
|
447
|
+
logging.info(f"Entity type {entity_type._name} added to model {model}")
|
|
448
|
+
return entity_type
|
|
449
|
+
|
|
450
|
+
return wrapper
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def SyncEntity(entity=None, *, shared_global_ids: bool = False):
|
|
454
|
+
"""
|
|
455
|
+
Decorator enabling Sync for an entity, i.e. its objects are synchronized with the Sync server.
|
|
456
|
+
Use it together with ``@Entity()``, in any order (above or below it).
|
|
457
|
+
Like ``@Entity()``, it is used with parentheses; ``@SyncEntity`` without parentheses works too.
|
|
458
|
+
|
|
459
|
+
Note: Sync cannot be enabled for an existing (non-synced) entity, and synced entities can only have relations to
|
|
460
|
+
other synced entities.
|
|
461
|
+
|
|
462
|
+
:param shared_global_ids:
|
|
463
|
+
Set to True to use shared global IDs for this entity: object IDs are then the same on all Sync clients instead
|
|
464
|
+
of being mapped between local and global IDs (the default). Only use this if IDs are unique across all
|
|
465
|
+
clients (e.g. assigned IDs); otherwise, clients overwrite each other's objects. Cannot be changed later.
|
|
466
|
+
|
|
467
|
+
Example::
|
|
468
|
+
|
|
469
|
+
@SyncEntity()
|
|
470
|
+
@Entity()
|
|
471
|
+
class MyEntity:
|
|
472
|
+
id = Id
|
|
473
|
+
name = String
|
|
474
|
+
"""
|
|
475
|
+
flags = OBXEntityFlags.SYNC_ENABLED
|
|
476
|
+
if shared_global_ids:
|
|
477
|
+
flags |= OBXEntityFlags.SHARED_GLOBAL_IDS
|
|
478
|
+
|
|
479
|
+
def wrapper(entity_to_sync):
|
|
480
|
+
if isinstance(entity_to_sync, _Entity): # Above @Entity()
|
|
481
|
+
entity_to_sync._flags |= flags
|
|
482
|
+
elif inspect.isclass(entity_to_sync): # Below @Entity(): it takes the flags from the class
|
|
483
|
+
setattr(entity_to_sync, _ENTITY_FLAGS_ATTR, entity_to_sync.__dict__.get(_ENTITY_FLAGS_ATTR, 0) | flags)
|
|
484
|
+
else:
|
|
485
|
+
raise TypeError("@SyncEntity() must be applied to an entity class (together with @Entity())")
|
|
486
|
+
return entity_to_sync
|
|
487
|
+
|
|
488
|
+
if entity is None: # Used with parentheses: @SyncEntity()
|
|
489
|
+
return wrapper
|
|
490
|
+
return wrapper(entity)
|