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.
@@ -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)