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,865 @@
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
+
15
+ from enum import IntEnum
16
+ from datetime import datetime
17
+ import sys
18
+ from dataclasses import dataclass
19
+
20
+ from objectbox.c import *
21
+ from objectbox.condition import PropertyQueryCondition, PropertyQueryConditionOp
22
+ from objectbox.model.iduid import IdUid
23
+
24
+ class PropertyType(IntEnum):
25
+ bool = OBXPropertyType_Bool
26
+ byte = OBXPropertyType_Byte
27
+ short = OBXPropertyType_Short
28
+ char = OBXPropertyType_Char
29
+ int = OBXPropertyType_Int
30
+ long = OBXPropertyType_Long
31
+ float = OBXPropertyType_Float
32
+ double = OBXPropertyType_Double
33
+ string = OBXPropertyType_String
34
+ date = OBXPropertyType_Date
35
+ dateNano = OBXPropertyType_DateNano
36
+ flex = OBXPropertyType_Flex
37
+ relation = OBXPropertyType_Relation
38
+ boolVector = OBXPropertyType_BoolVector
39
+ byteVector = OBXPropertyType_ByteVector
40
+ shortVector = OBXPropertyType_ShortVector
41
+ charVector = OBXPropertyType_CharVector
42
+ intVector = OBXPropertyType_IntVector
43
+ longVector = OBXPropertyType_LongVector
44
+ floatVector = OBXPropertyType_FloatVector
45
+ doubleVector = OBXPropertyType_DoubleVector
46
+ # stringVector = OBXPropertyType_StringVector
47
+
48
+
49
+ # FlatBuffers struct format per type (as the packer of the flatbuffers.number_types flags, see Property._fb_type);
50
+ # strings, vectors and flex are offsets ('I'). This avoids importing flatbuffers, which imports numpy, if available.
51
+ _fb_formats = {
52
+ PropertyType.bool: '?',
53
+ PropertyType.byte: 'b',
54
+ PropertyType.short: 'h',
55
+ PropertyType.char: 'b',
56
+ PropertyType.int: 'i',
57
+ PropertyType.long: 'q',
58
+ PropertyType.float: 'f',
59
+ PropertyType.double: 'd',
60
+ PropertyType.string: 'I',
61
+ PropertyType.date: 'q',
62
+ PropertyType.dateNano: 'q',
63
+ PropertyType.flex: 'I',
64
+ PropertyType.relation: 'q',
65
+ PropertyType.boolVector: 'I',
66
+ PropertyType.byteVector: 'I',
67
+ PropertyType.shortVector: 'I',
68
+ PropertyType.charVector: 'I',
69
+ PropertyType.intVector: 'I',
70
+ PropertyType.longVector: 'I',
71
+ PropertyType.floatVector: 'I',
72
+ PropertyType.doubleVector: 'I',
73
+ # PropertyType.stringVector: 'I',
74
+ }
75
+
76
+ # flatbuffers.number_types flags per struct format
77
+ _fb_flags_names = {'?': 'BoolFlags', 'b': 'Int8Flags', 'h': 'Int16Flags', 'i': 'Int32Flags', 'q': 'Int64Flags',
78
+ 'f': 'Float32Flags', 'd': 'Float64Flags', 'I': 'UOffsetTFlags'}
79
+
80
+
81
+ def _ndarray() -> type:
82
+ """numpy.ndarray; imports numpy, so only call it where numpy is needed (e.g. vector properties)."""
83
+ import numpy
84
+ return numpy.ndarray
85
+
86
+
87
+ class IndexType(IntEnum):
88
+ VALUE = OBXPropertyFlags_INDEXED
89
+ HASH = OBXPropertyFlags_INDEX_HASH
90
+ HASH64 = OBXPropertyFlags_INDEX_HASH64
91
+
92
+
93
+ class Index:
94
+ """Property index, e.g. ``name = String(index=Index())``.
95
+
96
+ The default type is HASH for String properties (indexes a hash of the value, which takes less space) and VALUE for
97
+ other types, like in the other ObjectBox languages. Hash indexes are only supported for String properties.
98
+ Note: to use the index of a String property, query it with case-sensitive conditions (the default)."""
99
+
100
+ # TODO HNSW isn't a `type` but HASH and HASH64 are, remove type member and make HashIndex and Hash64Index classes?
101
+
102
+ def __init__(self, type: Optional[IndexType] = None, uid: int = 0):
103
+ self.type = type # None: the default for the property type (_type_for())
104
+
105
+ self.iduid = IdUid(0, uid)
106
+
107
+ @property
108
+ def id(self):
109
+ """ Index Id """
110
+ return self.iduid.id
111
+
112
+ @property
113
+ def uid(self):
114
+ """ Index UId """
115
+ return self.iduid.uid
116
+
117
+ def has_uid(self):
118
+ return self.iduid.uid != 0
119
+
120
+ def _type_for(self, ob_type: int) -> IndexType:
121
+ if self.type is not None:
122
+ return self.type
123
+ return IndexType.HASH if ob_type == OBXPropertyType_String else IndexType.VALUE
124
+
125
+
126
+ class ConflictStrategy(IntEnum):
127
+ """What happens when putting an object violates a :class:`Unique` constraint."""
128
+
129
+ FAIL = 0
130
+ """Putting raises a ``UniqueViolatedError`` (the default)."""
131
+
132
+ REPLACE = 1
133
+ """The existing object with the same value is removed, and the put object is stored (with its own ID).
134
+ Only one property of an entity can use it; required for all unique properties of a Sync entity."""
135
+
136
+
137
+ class Unique:
138
+ """Unique constraint of a property, e.g. ``email = String(unique=Unique())``: putting an object with a value
139
+ that another object of the entity already has raises a ``UniqueViolatedError`` (by default; see ``on_conflict``).
140
+
141
+ A unique property is indexed: it gets an :class:`Index` (of the default type) unless one is given via ``index=``,
142
+ e.g. to set its type. Like in the other ObjectBox languages, not supported for the ID and for types that do not
143
+ support an index (e.g. floating point and vector properties)."""
144
+
145
+ def __init__(self, on_conflict: ConflictStrategy = ConflictStrategy.FAIL):
146
+ self.on_conflict = on_conflict
147
+
148
+
149
+ class HnswFlags(IntEnum):
150
+ """
151
+ Vector-Search HNSW Index Flags
152
+ """
153
+
154
+ NONE = 0
155
+ """
156
+
157
+ """
158
+
159
+ DEBUG_LOGS = 1
160
+ """
161
+ Enables debug logs.
162
+ """
163
+
164
+ DEBUG_LOGS_DETAILED = 2
165
+ """
166
+ Enables "high volume" debug logs, e.g. individual gets/puts.
167
+ """
168
+
169
+ VECTOR_CACHE_SIMD_PADDING_OFF = 4
170
+ """
171
+ Padding for SIMD is enabled by default, which uses more memory but may be faster. This flag turns it off.
172
+ """
173
+
174
+ REPARATION_LIMIT_CANDIDATES = 8
175
+ """
176
+ If the speed of removing nodes becomes a concern in your use case, you can speed it up by setting this flag.
177
+ By default, repairing the graph after node removals creates more connections to improve the graph's quality.
178
+ The extra costs for this are relatively low (e.g. vs. regular indexing), and thus the default is recommended.
179
+ """
180
+
181
+ class VectorDistanceType(IntEnum):
182
+ """
183
+ Vector-search distance computation strategy type
184
+ """
185
+
186
+ UNKNOWN = OBXVectorDistanceType_UNKNOWN
187
+ """
188
+ Not a real type, just best practice (e.g. forward compatibility)
189
+ """
190
+
191
+ EUCLIDEAN = OBXVectorDistanceType_EUCLIDEAN
192
+ """
193
+ The default; typically 'euclidean squared' internally."
194
+ """
195
+
196
+ COSINE = OBXVectorDistanceType_COSINE
197
+ """
198
+ Cosine similarity compares two vectors irrespective of their magnitude (compares the angle of two vectors).
199
+ Often used for document or semantic similarity.
200
+ Value range: 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction)
201
+ """
202
+
203
+ DOT_PRODUCT = OBXVectorDistanceType_DOT_PRODUCT
204
+ """
205
+ For normalized vectors (vector length == 1.0), the dot product is equivalent to the cosine similarity.
206
+ Because of this, the dot product is often preferred as it performs better.
207
+ Value range (normalized vectors): 0.0 - 2.0 (0.0: same direction, 1.0: orthogonal, 2.0: opposite direction)
208
+ """
209
+
210
+ DOT_PRODUCT_NON_NORMALIZED = OBXVectorDistanceType_DOT_PRODUCT_NON_NORMALIZED
211
+ """
212
+ A custom dot product similarity measure that does not require the vectors to be normalized.
213
+ Note: this is no replacement for cosine similarity (like DotProduct for normalized vectors is).
214
+ The non-linear conversion provides a high precision over the entire float range (for the raw dot product).
215
+ The higher the dot product, the lower the distance is (the nearer the vectors are).
216
+ The more negative the dot product, the higher the distance is (the farther the vectors are).
217
+ Value range: 0.0 - 2.0 (nonlinear; 0.0: nearest, 1.0: orthogonal, 2.0: farthest)
218
+ """
219
+
220
+ class HnswIndex:
221
+ """Vector-Search HNSW Property Index"""
222
+ def __init__(self,
223
+ dimensions: int,
224
+ neighbors_per_node: Optional[int] = None,
225
+ indexing_search_count: Optional[int] = None,
226
+ flags: HnswFlags = HnswFlags.NONE,
227
+ distance_type: VectorDistanceType = VectorDistanceType.EUCLIDEAN,
228
+ reparation_backlink_probability: Optional[float] = None,
229
+ vector_cache_hint_size_kb: Optional[int] = None,
230
+ uid: int = 0):
231
+ """
232
+ :param dimensions:
233
+ Vector dimensionality.
234
+ :param neighbors_per_node:
235
+ Maximum number of neighbors per node (aka "M").
236
+ Higher number increases the graph connectivity which can lead to better results, but higher resources usage.
237
+ If no value is set, a default value taken (currently 30).
238
+ Try e.g. 16 for faster but less accurate results, or 64 for more accurate results.
239
+ :param indexing_search_count:
240
+ Maximum number of neighbors searched while indexing (aka "efConstruction").
241
+ If not set, internally default value is currently set to 100, which can change in future version.
242
+ The default value serves as a starting point that can likely be optimized for specific datasets and use cases.
243
+ The higher the value, the more accurate the search, but the longer the indexing will take.
244
+ If indexing time is not a major concern, a value of at least 200 is recommended to improve search quality. blah.
245
+ :param flags:
246
+ Set flags.
247
+ :param distance_type:
248
+ Set distance strategy type.
249
+ :param reparation_backlink_probability:
250
+ When repairing the graph after a node was removed, this gives the probability of adding backlinks to the repaired
251
+ neighbors. The default is 1.0 (aka "always") as this should be worth a bit of extra costs as it improves the graph's quality.
252
+ :param vector_cache_hint_size_kb:
253
+ Vector cache hint size: a non-binding hint of the maximum size of the vector cache in KB, an int
254
+ (default: 2097152 or 2 GB/GiB).
255
+ """
256
+ self.dimensions = dimensions
257
+ self.neighbors_per_node = neighbors_per_node
258
+ self.indexing_search_count = indexing_search_count
259
+ self.flags = flags
260
+ self.distance_type = distance_type
261
+ self.reparation_backlink_probability = reparation_backlink_probability
262
+ if vector_cache_hint_size_kb is not None and (not isinstance(vector_cache_hint_size_kb, int) or
263
+ isinstance(vector_cache_hint_size_kb, bool)):
264
+ raise TypeError(f"vector_cache_hint_size_kb must be an int (KB), got {vector_cache_hint_size_kb!r}")
265
+ self.vector_cache_hint_size_kb = vector_cache_hint_size_kb
266
+
267
+ self.iduid = IdUid(0, uid)
268
+
269
+ @property
270
+ def id(self):
271
+ """ Index Id """
272
+ return self.iduid.id
273
+
274
+ @property
275
+ def uid(self):
276
+ """ Index Uid """
277
+ return self.iduid.uid
278
+
279
+ def has_uid(self):
280
+ """ Returns true if Uid is set. """
281
+ return self.uid != 0
282
+
283
+
284
+ class Property:
285
+ def __init__(self, pytype: Type, uid: int = 0, **kwargs):
286
+ Property._check_args(kwargs, ('index', 'type', 'unique'))
287
+ self.iduid = IdUid(0, uid)
288
+ self.name = "" # Python attribute name; set in Entity.fill_properties()
289
+ self._db_name = "" # Name in the database and model JSON (camelCase by default); set in Entity.fill_properties()
290
+ self._entity = None # The _Entity this property belongs to; set in Entity.fill_properties()
291
+ self.index = Property._index_arg(kwargs.get('index', None))
292
+ self.unique = Property._unique_arg(kwargs.get('unique', None))
293
+ if self.unique is not None:
294
+ if isinstance(self.index, HnswIndex):
295
+ raise TypeError("A unique property can not have a HnswIndex")
296
+ if self.index is None:
297
+ self.index = Index()
298
+
299
+ self._py_type = pytype
300
+ self._is_ndarray = pytype is getattr(sys.modules.get('numpy'), 'ndarray', None) # numpy imported if ndarray
301
+ self._ob_type = kwargs['type'] if 'type' in kwargs else self._determine_ob_type()
302
+ self._fb_format = _fb_formats[self._ob_type] # struct format of the field (offsets: 'I')
303
+
304
+ self._flags = 0
305
+ self._set_flags()
306
+
307
+ self._fb_slot = None
308
+ self._fb_v_offset = None
309
+
310
+ def _condition_value(self, value):
311
+ """ Converts a value for a query condition (overridden, e.g. for dates). """
312
+ return value
313
+
314
+ @staticmethod
315
+ def _check_args(kwargs: dict, allowed):
316
+ unknown = set(kwargs) - set(allowed)
317
+ if 'id' in unknown:
318
+ raise TypeError("Properties do not take an id: IDs are assigned via the model JSON file "
319
+ "(to identify a property, e.g. when renaming it, use uid=)")
320
+ if unknown:
321
+ raise TypeError(f"Unexpected property argument(s): {', '.join(sorted(unknown))}")
322
+
323
+ @staticmethod
324
+ def _unique_arg(unique):
325
+ if unique is Unique: # The class, e.g. String(unique=Unique), like property classes used without parentheses
326
+ return Unique()
327
+ if unique is not None and not isinstance(unique, Unique):
328
+ raise TypeError(f"unique must be a Unique (e.g. Unique() or Unique(ConflictStrategy.REPLACE)), "
329
+ f"got {unique!r}")
330
+ return unique
331
+
332
+ @staticmethod
333
+ def _index_arg(index):
334
+ if index is Index: # The class, e.g. String(index=Index), like property classes used without parentheses
335
+ return Index()
336
+ if index is not None and not isinstance(index, (Index, HnswIndex)):
337
+ raise TypeError(f"index must be an Index (e.g. Index() or Index(IndexType.VALUE)) or a HnswIndex, "
338
+ f"got {index!r}")
339
+ return index
340
+
341
+ def __getattr__(self, name):
342
+ # Only called if the attribute is not set: _fb_type is resolved on first use, then set on the instance.
343
+ if name == '_fb_type' and '_fb_format' in self.__dict__:
344
+ import flatbuffers.number_types # imports numpy (if available), so only when needed
345
+ self._fb_type = getattr(flatbuffers.number_types, _fb_flags_names[self.__dict__['_fb_format']])
346
+ return self._fb_type
347
+ raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, name))
348
+
349
+ @property
350
+ def id(self):
351
+ """ Property Id """
352
+ return self.iduid.id
353
+
354
+ @property
355
+ def uid(self):
356
+ """ Property Uid """
357
+ return self.iduid.uid
358
+
359
+ def has_uid(self):
360
+ """ Returns true if property has a valid Uid """
361
+ return self.uid != 0
362
+
363
+ def is_id(self) -> bool:
364
+ """ Check if Property is an Id Property. """
365
+ return isinstance(self, Id)
366
+
367
+ def on_sync(self):
368
+ """ Method called once ID/UID are synced with the model file. """
369
+ assert self.iduid.is_assigned()
370
+ self._fb_slot = self.id - 1
371
+ self._fb_v_offset = 4 + 2 * self._fb_slot
372
+
373
+ def _determine_ob_type(self) -> OBXPropertyType:
374
+ """ Tries to infer the OBX property type from the Python type. """
375
+ ts = self._py_type
376
+ if ts == str:
377
+ return OBXPropertyType_String
378
+ elif ts == int:
379
+ return OBXPropertyType_Long
380
+ elif ts == bytes: # or ts == bytearray: might require further tests on read objects due to mutability
381
+ return OBXPropertyType_ByteVector
382
+ elif ts == list or self._is_ndarray:
383
+ return OBXPropertyType_DoubleVector
384
+ elif ts == float:
385
+ return OBXPropertyType_Double
386
+ elif ts == bool:
387
+ return OBXPropertyType_Bool
388
+ else:
389
+ raise TypeError("unknown property type %s" % ts)
390
+
391
+ def _set_flags(self):
392
+ if self.is_id():
393
+ self._flags |= OBXPropertyFlags_ID
394
+
395
+ if self.index is not None:
396
+ self._flags |= OBXPropertyFlags_INDEXED
397
+ if isinstance(self.index, Index): # Generic index
398
+ self._flags |= self.index._type_for(self._ob_type)
399
+
400
+ if self.unique is not None:
401
+ self._flags |= OBXPropertyFlags_UNIQUE
402
+ if self.unique.on_conflict == ConflictStrategy.REPLACE:
403
+ self._flags |= OBXPropertyFlags_UNIQUE_ON_CONFLICT_REPLACE
404
+
405
+ def is_null(self) -> PropertyQueryCondition:
406
+ """ *Is null* condition to be passed to :func:`Box.query`: matches objects without a value for this property,
407
+ i.e. it was put with None (unset properties are stored with their default value, not null) """
408
+ self._assert_ids_assigned()
409
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.IS_NULL, {})
410
+
411
+ def is_not_null(self) -> PropertyQueryCondition:
412
+ """ *Is not null* condition to be passed to :func:`Box.query`: matches objects with a value for this property """
413
+ self._assert_ids_assigned()
414
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_NULL, {})
415
+
416
+ def _assert_ids_assigned(self):
417
+ # Using assert(s) so they can be optionally disabled for performance
418
+ assert self.iduid.is_assigned(), f"Property \"{self.name}\" ID not assigned"
419
+ if self.index is not None:
420
+ assert self.index.iduid.is_assigned(), f"Property \"{self.name}\" index ID not assigned"
421
+
422
+ class _NumericProperty(Property):
423
+ """Common class for numeric conditions.
424
+ Implicitly no support for equals/not_equals, see also _IntProperty below.
425
+ """
426
+ def __init__(self, py_type : Type, **kwargs):
427
+ super(_NumericProperty, self).__init__(py_type, **kwargs)
428
+
429
+ def greater_than(self, value) -> PropertyQueryCondition:
430
+ """ *Greater-than* (``>``) condition to be passed to :func:`objectbox.Box.query` """
431
+ self._assert_ids_assigned()
432
+ args = {'value': self._condition_value(value)}
433
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args)
434
+
435
+ def greater_or_equal(self, value) -> PropertyQueryCondition:
436
+ """ *Greater-or-equal* (``>=``) condition to be passed to :func:`Box.query` """
437
+ self._assert_ids_assigned()
438
+ args = {'value': self._condition_value(value)}
439
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args)
440
+
441
+ def less_than(self, value) -> PropertyQueryCondition:
442
+ """ *Less-than* (``<``) condition to be passed to :func:`Box.query` """
443
+ self._assert_ids_assigned()
444
+ args = {'value': self._condition_value(value)}
445
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args)
446
+
447
+ def less_or_equal(self, value) -> PropertyQueryCondition:
448
+ """ *Less-or-equal* (``<=``) condition to be passed to :func:`Box.query` """
449
+ self._assert_ids_assigned()
450
+ args = {'value': self._condition_value(value)}
451
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args)
452
+
453
+ def between(self, a, b) -> PropertyQueryCondition:
454
+ """ *Between* a and b (``a <= x <= b``) condition to be passed to :func:`Box.query` """
455
+ self._assert_ids_assigned()
456
+ args = {'a': self._condition_value(a), 'b': self._condition_value(b)}
457
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.BETWEEN, args)
458
+
459
+ class _IntProperty(_NumericProperty):
460
+ """Integer-based conditions.
461
+ Adds support for equals/not_equals.
462
+ """
463
+ def __init__(self, py_type : Type, **kwargs):
464
+ super(_IntProperty, self).__init__(py_type, **kwargs)
465
+
466
+ def _condition_value(self, value):
467
+ if not isinstance(value, int): # Includes bool (an int)
468
+ raise TypeError(f"Property {self.name} takes int values in conditions, got {value!r}")
469
+ return value
470
+
471
+ def equals(self, value) -> PropertyQueryCondition:
472
+ """ *Equals* (``==``) condition to be passed to :func:`Box.query` """
473
+ self._assert_ids_assigned()
474
+ args = {'value': self._condition_value(value)}
475
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args)
476
+
477
+ def not_equals(self, value) -> PropertyQueryCondition:
478
+ """ *Not equals* (``!=``) condition to be passed to :func:`Box.query` """
479
+ self._assert_ids_assigned()
480
+ args = {'value': self._condition_value(value)}
481
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_EQ, args)
482
+
483
+ def one_of(self, values: List[int]) -> PropertyQueryCondition:
484
+ """ *one of* (``in``) condition to be passed to :func:`Box.query`;
485
+ matches if the property value equals any of the given values.
486
+ Supported for 32-bit (Int32) and 64-bit (Int64, Id, Date, DateNano, Relation) integer properties;
487
+ raises a ``TypeError`` for other properties (e.g. Int8, Int16, Bool). """
488
+ self._assert_ids_assigned()
489
+ self._assert_one_of_supported()
490
+ args = {'values': [self._condition_value(value) for value in values]}
491
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ONE_OF, args)
492
+
493
+ def not_one_of(self, values: List[int]) -> PropertyQueryCondition:
494
+ """ *not one of* (``not in``) condition to be passed to :func:`Box.query`;
495
+ matches if the property value equals none of the given values.
496
+ Supported for 32-bit (Int32) and 64-bit (Int64, Id, Date, DateNano, Relation) integer properties;
497
+ raises a ``TypeError`` for other properties (e.g. Int8, Int16, Bool). """
498
+ self._assert_ids_assigned()
499
+ self._assert_one_of_supported()
500
+ args = {'values': [self._condition_value(value) for value in values]}
501
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_ONE_OF, args)
502
+
503
+ def _assert_one_of_supported(self):
504
+ # The C API only supports 32-bit and 64-bit integers for "in" conditions
505
+ if self._ob_type not in (PropertyType.int, PropertyType.long, PropertyType.date, PropertyType.dateNano,
506
+ PropertyType.relation):
507
+ raise TypeError(f"Property \"{self.name}\" of type {PropertyType(self._ob_type).name} "
508
+ f"does not support one_of()/not_one_of(); supported are 32-bit and 64-bit integers")
509
+
510
+
511
+ # ID property (primary key)
512
+ class Id(_IntProperty):
513
+ """ID property: the object ID (an int), 0 for a new object; put() assigns it.
514
+
515
+ By default, IDs are assigned by ObjectBox only: putting an object with an ID that was not assigned by ObjectBox
516
+ raises (an IllegalArgumentError). Set ``assignable=True`` to put objects with IDs of your own (e.g. from another
517
+ system); new objects with ID 0 still get IDs assigned (higher than any ID put before)."""
518
+ def __init__(self, *, uid: int = 0, py_type: type = int, assignable: bool = False, **kwargs):
519
+ Property._check_args(kwargs, ())
520
+ super(Id, self).__init__(py_type, uid=uid)
521
+ self.assignable = assignable
522
+ if assignable:
523
+ self._flags |= OBXPropertyFlags_ID_SELF_ASSIGNABLE
524
+
525
+ # Bool property
526
+ class Bool(_IntProperty):
527
+ """Boolean Property"""
528
+ def __init__(self, *, uid: int = 0, **kwargs):
529
+ super(Bool, self).__init__(bool, type=PropertyType.bool, uid=uid, **kwargs)
530
+
531
+ # String property with starts/ends_with
532
+ class String(Property):
533
+ """String Property"""
534
+ def __init__(self, *, uid: int = 0, **kwargs):
535
+ super(String, self).__init__(str, type=PropertyType.string, uid=uid, **kwargs)
536
+
537
+ def starts_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
538
+ """ *starts with* (string-prefix) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
539
+ self._assert_ids_assigned()
540
+ args = {'value': value, 'case_sensitive': case_sensitive}
541
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.STARTS_WITH, args)
542
+
543
+ def ends_with(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
544
+ """ *ends with* (string-suffix) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
545
+ self._assert_ids_assigned()
546
+ args = {'value': value, 'case_sensitive': case_sensitive}
547
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ENDS_WITH, args)
548
+
549
+ def equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
550
+ """ *equals* (``==``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
551
+ self._assert_ids_assigned()
552
+ args = {'value': value, 'case_sensitive': case_sensitive}
553
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args)
554
+
555
+ def not_equals(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
556
+ """ *not-equals* (``~=``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
557
+ self._assert_ids_assigned()
558
+ args = {'value': value, 'case_sensitive': case_sensitive}
559
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NOT_EQ, args)
560
+
561
+ def contains(self, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
562
+ """ *contains string* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
563
+ self._assert_ids_assigned()
564
+ args = {'value': value, 'case_sensitive': case_sensitive}
565
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.CONTAINS, args)
566
+
567
+ def greater_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
568
+ """ *greater-than* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
569
+ self._assert_ids_assigned()
570
+ args = {'value': value, 'case_sensitive': case_sensitive}
571
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args)
572
+
573
+ def greater_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
574
+ """ *greater-or-equal* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
575
+ self._assert_ids_assigned()
576
+ args = {'value': value, 'case_sensitive': case_sensitive}
577
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args)
578
+
579
+ def less_than(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
580
+ """ *less-than* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
581
+ self._assert_ids_assigned()
582
+ args = {'value': value, 'case_sensitive': case_sensitive}
583
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args)
584
+
585
+ def less_or_equal(self, value, case_sensitive: bool = True) -> PropertyQueryCondition:
586
+ """ *less-or-equal* condition (opt-in: case-sensitive) to be passed to :func:`Box.query` """
587
+ self._assert_ids_assigned()
588
+ args = {'value': value, 'case_sensitive': case_sensitive}
589
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args)
590
+
591
+ def one_of(self, values: List[str], case_sensitive: bool = True) -> PropertyQueryCondition:
592
+ """ *one of* (``in``) condition (opt-in: case-sensitive) to be passed to :func:`Box.query`;
593
+ matches if the property value equals any of the given values. """
594
+ self._assert_ids_assigned()
595
+ args = {'values': values, 'case_sensitive': case_sensitive, 'string': True}
596
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.ONE_OF, args)
597
+
598
+ # Signed Integer Numeric Properties
599
+ class Int8(_IntProperty):
600
+ """Integer 8-bit Property"""
601
+ def __init__(self, *, uid: int = 0, **kwargs):
602
+ super(Int8, self).__init__(int, type=PropertyType.byte, uid=uid, **kwargs)
603
+ class Int16(_IntProperty):
604
+ """Integer 16-bit Property"""
605
+ def __init__(self, *, uid: int = 0, **kwargs):
606
+ super(Int16, self).__init__(int, type=PropertyType.short, uid=uid, **kwargs)
607
+ class Int32(_IntProperty):
608
+ """Integer 32-bit Property"""
609
+ def __init__(self, *, uid: int = 0, **kwargs):
610
+ super(Int32, self).__init__(int, type=PropertyType.int, uid=uid, **kwargs)
611
+ class Int64(_IntProperty):
612
+ """Integer 64-bit Property"""
613
+ def __init__(self, *, uid: int = 0, **kwargs):
614
+ super(Int64, self).__init__(int, type=PropertyType.long, uid=uid, **kwargs)
615
+
616
+
617
+ class Relation(_IntProperty):
618
+ """To-one relation: the ID of an object of the target entity (an int; 0 for none), e.g.
619
+ ``customer_id = Relation(Customer)``. Unlike an ``Int64``, ObjectBox knows the target: e.g. removing the target
620
+ object resets the relation to 0. The relation is indexed (no index for 0).
621
+
622
+ Name it like the relation with an ``_id`` suffix (e.g. ``customer_id`` for a customer): it is stored as
623
+ ``customerId``, as a ``ToOne`` named ``customer`` in Java, Kotlin and Dart stores its target ID, so the model and
624
+ data match theirs (e.g. for Sync).
625
+
626
+ :param target: the target entity, i.e. the class decorated with ``@Entity()``, or its name, e.g. for a relation to
627
+ the entity itself (``parent_id = Relation("Node")``) or to an entity defined later.
628
+ """
629
+
630
+ def __init__(self, target, *, uid: int = 0, **kwargs):
631
+ Property._check_args(kwargs, ())
632
+ from objectbox.model.entity import _Entity
633
+ if not isinstance(target, (_Entity, str)):
634
+ raise TypeError(f"Relation target must be an entity (a class decorated with @Entity()) or an entity name, "
635
+ f"got {target!r}")
636
+ super(Relation, self).__init__(int, type=PropertyType.relation, uid=uid, index=Index())
637
+ self._flags |= OBXPropertyFlags_INDEX_PARTIAL_SKIP_ZERO
638
+ self._target = target
639
+
640
+ @property
641
+ def _target_name(self) -> str:
642
+ return self._target if isinstance(self._target, str) else self._target._name
643
+
644
+
645
+ class SyncClock(Int64):
646
+ """Sync clock property (64-bit integer) of a Sync entity (see ``@SyncEntity()``): a "hybrid logical clock" to
647
+ resolve Sync conflicts. These clock values allow "last write wins" conflict resolution.
648
+
649
+ There can be only one sync clock per Sync entity type; which is also recommended for basic conflict resolution.
650
+ For new objects, keep the value 0 (the default) to reserve "a slot" in the object data. ObjectBox Sync updates
651
+ this property automatically on put operations.
652
+ As a hybrid clock, it combines a wall clock with a logical counter to compensate for some clock skew effects.
653
+ To get a timestamp from a value, see ``Sync.sync_clock_timestamp()``.
654
+ """
655
+ def __init__(self, *, uid: int = 0, **kwargs):
656
+ Property._check_args(kwargs, ())
657
+ super(SyncClock, self).__init__(uid=uid)
658
+ self._flags |= OBXPropertyFlags_SYNC_CLOCK
659
+
660
+
661
+ class SyncPrecedence(Int64):
662
+ """Sync precedence property (64-bit integer) of a Sync entity (see ``@SyncEntity()``) to customize Sync conflict
663
+ resolution: developer-assigned precedence values are then used to resolve conflicts via "higher precedence wins".
664
+ Defining and assigning precedence values are completely in the hands of the developer.
665
+
666
+ There can be only one sync precedence per Sync entity type.
667
+ Typically, it is combined with a ``SyncClock``, with the latter being the tie-breaker for equal precedence values.
668
+ """
669
+ def __init__(self, *, uid: int = 0, **kwargs):
670
+ Property._check_args(kwargs, ())
671
+ super(SyncPrecedence, self).__init__(uid=uid)
672
+ self._flags |= OBXPropertyFlags_SYNC_PRECEDENCE
673
+
674
+ # Floating-Point Numeric Properties
675
+ class Float32(_NumericProperty):
676
+ """Floating-point 32-bit Property"""
677
+ def __init__(self, *, uid: int = 0, **kwargs):
678
+ super(Float32, self).__init__(float, type=PropertyType.float, uid=uid, **kwargs)
679
+
680
+ class Float64(_NumericProperty):
681
+ """Floating-point 64-bit Property"""
682
+ def __init__(self, *, uid: int = 0, **kwargs):
683
+ super(Float64, self).__init__(float, type=PropertyType.double, uid=uid, **kwargs)
684
+
685
+ # Date Properties
686
+ class Date(_IntProperty):
687
+ """Date property: stored as milliseconds since the Unix epoch. Values (also in query conditions) can be a
688
+ ``datetime``, a ``float`` (seconds) or an ``int`` (milliseconds); ``py_type`` is the type read back."""
689
+ _multiplier = 1000 # From seconds
690
+
691
+ def __init__(self, py_type=datetime, *, uid: int = 0, **kwargs):
692
+ super(Date, self).__init__(py_type, type=PropertyType.date, uid=uid, **kwargs)
693
+
694
+ def _condition_value(self, value):
695
+ from objectbox.utils import date_value_to_int
696
+ return date_value_to_int(value, self._multiplier)
697
+
698
+ class DateNano(_IntProperty):
699
+ """Date property with nanosecond resolution: stored as nanoseconds since the Unix epoch. Values (also in query
700
+ conditions) can be a ``datetime``, a ``float`` (seconds) or an ``int`` (nanoseconds); ``py_type`` is the type
701
+ read back."""
702
+ _multiplier = 1000000000 # From seconds
703
+
704
+ def __init__(self, py_type=datetime, *, uid: int = 0, **kwargs):
705
+ super(DateNano, self).__init__(py_type, type=PropertyType.dateNano, uid=uid, **kwargs)
706
+
707
+ _condition_value = Date._condition_value
708
+
709
+ # Bytes Property
710
+ class Bytes(_NumericProperty):
711
+ """Bytes blob Property"""
712
+ def __init__(self, *, uid: int = 0, **kwargs):
713
+ super(Bytes, self).__init__(bytes, type=PropertyType.byteVector, uid=uid, **kwargs)
714
+
715
+ def equals(self, value) -> PropertyQueryCondition:
716
+ """ *byte-string equals* (``==``) condition to be passed to :func:`Box.query` """
717
+ self._assert_ids_assigned()
718
+ args = {'value': value}
719
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.EQ, args)
720
+
721
+ def greater_than(self, value) -> PropertyQueryCondition:
722
+ """ *byte-string greater-than* (``==``) condition to be passed to :func:`Box.query` """
723
+ self._assert_ids_assigned()
724
+ args = {'value': value}
725
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GT, args)
726
+
727
+ def greater_or_equal(self, value) -> PropertyQueryCondition:
728
+ """ *byte-string greater-or-equal* (``>=``) condition to be passed to :func:`Box.query` """
729
+ self._assert_ids_assigned()
730
+ args = {'value': value}
731
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.GTE, args)
732
+
733
+ def less_than(self, value) -> PropertyQueryCondition:
734
+ """ *byte-string less-than* (``<``) condition to be passed to :func:`Box.query` """
735
+ self._assert_ids_assigned()
736
+ args = {'value': value}
737
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LT, args)
738
+
739
+ def less_or_equal(self, value) -> PropertyQueryCondition:
740
+ """ *byte-string less-or-equal* (``<``) condition to be passed to :func:`Box.query` """
741
+ self._assert_ids_assigned()
742
+ args = {'value': value}
743
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.LTE, args)
744
+
745
+ # Flex Property
746
+ class Flex(Property):
747
+ """Flex dictionary-compatible Property"""
748
+ def __init__(self, *, uid: int = 0, **kwargs):
749
+ super(Flex, self).__init__(Generic, type=PropertyType.flex, uid=uid, **kwargs)
750
+ def contains_key_value(self, key: str, value: str, case_sensitive: bool = True) -> PropertyQueryCondition:
751
+ """ *contains key/valuel* condition to be passed to :func:`Box.query` """
752
+ self._assert_ids_assigned()
753
+ args = {'key': key, 'value': value, 'case_sensitive': case_sensitive}
754
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.CONTAINS_KEY_VALUE, args)
755
+
756
+ class _VectorProperty(Property):
757
+ def __init__(self, py_type : Type, **kwargs):
758
+ super(_VectorProperty, self).__init__(py_type, **kwargs)
759
+
760
+ class BoolVector(_VectorProperty):
761
+ """Boolean Vector Property"""
762
+ def __init__(self, *, uid: int = 0, **kwargs):
763
+ super(BoolVector, self).__init__(_ndarray(), type=PropertyType.boolVector, uid=uid, **kwargs)
764
+ class Int8Vector(_VectorProperty):
765
+ """Integer 8-bit Vector Property"""
766
+ def __init__(self, *, uid: int = 0, **kwargs):
767
+ super(Int8Vector, self).__init__(bytes, type=PropertyType.byteVector, uid=uid, **kwargs)
768
+
769
+ class Int16Vector(_VectorProperty):
770
+ """Integer 16-bit Vector Property"""
771
+ def __init__(self, *, uid: int = 0, **kwargs):
772
+ super(Int16Vector, self).__init__(_ndarray(), type=PropertyType.shortVector, uid=uid, **kwargs)
773
+
774
+ class CharVector(_VectorProperty):
775
+ """Char 16-bit Vector Property"""
776
+ def __init__(self, *, uid: int = 0, **kwargs):
777
+ super(CharVector, self).__init__(_ndarray(), type=PropertyType.charVector, uid=uid, **kwargs)
778
+
779
+ class Int32Vector(_VectorProperty):
780
+ """Integer 32-bit Vector Property"""
781
+ def __init__(self, *, uid: int = 0, **kwargs):
782
+ super(Int32Vector, self).__init__(_ndarray(), type=PropertyType.intVector, uid=uid, **kwargs)
783
+
784
+ class Int64Vector(_VectorProperty):
785
+ """Integer 64-bit Vector Property"""
786
+ def __init__(self, *, uid: int = 0, **kwargs):
787
+ super(Int64Vector, self).__init__(_ndarray(), type=PropertyType.longVector, uid=uid, **kwargs)
788
+
789
+ class Float32Vector(_VectorProperty):
790
+ """Floating-point 32-bit Vector Property"""
791
+ def __init__(self, *, uid: int = 0, **kwargs):
792
+ super(Float32Vector, self).__init__(_ndarray(), type=PropertyType.floatVector, uid=uid, **kwargs)
793
+ def nearest_neighbors(self, query_vector, max_result_count: int) -> PropertyQueryCondition:
794
+ """ *Nearest neighbors* condition to be passed to :func:`objectbox.Box.query`: performs an approximate nearest
795
+ neighbor (ANN) search to find objects near to the given query vector. This requires the property to have an
796
+ :class:`HnswIndex`. Get the results ordered by distance via ``find_with_scores()`` or
797
+ ``find_ids_with_scores()``.
798
+
799
+ :param query_vector:
800
+ The vector to find the nearest neighbors of (a list of floats or a numpy float32 array); its dimensions
801
+ should be at least the dimensions of the property.
802
+ :param max_result_count:
803
+ The maximum number of objects to return by this condition. Hint: it can also be used as the "ef" HNSW
804
+ parameter to increase the search quality in combination with a query limit, e.g. 100 with
805
+ ``query.limit(10)`` may give 10 results of better quality than 10 (quality/performance tradeoff).
806
+ """
807
+ self._assert_ids_assigned()
808
+ args = {'query_vector': query_vector, 'max_result_count': max_result_count}
809
+ return PropertyQueryCondition(self.id, PropertyQueryConditionOp.NEAREST_NEIGHBOR, args)
810
+
811
+ def nearest_neighbor(self, query_vector, element_count: int) -> PropertyQueryCondition:
812
+ """ Deprecated: use :meth:`nearest_neighbors` (same arguments; named like in the other ObjectBox
813
+ languages). """
814
+ import warnings
815
+ warnings.warn("nearest_neighbor() is deprecated, use nearest_neighbors()", DeprecationWarning, stacklevel=2)
816
+ return self.nearest_neighbors(query_vector, element_count)
817
+
818
+ class Float64Vector(_VectorProperty):
819
+ """Floating-point 64-bit Vector Property"""
820
+ def __init__(self, *, uid: int = 0, **kwargs):
821
+ super(Float64Vector, self).__init__(_ndarray(), type=PropertyType.doubleVector, uid=uid, **kwargs)
822
+
823
+ class _ListProperty(Property):
824
+ def __init__(self, **kwargs):
825
+ super(_ListProperty, self).__init__(list, **kwargs)
826
+
827
+ class BoolList(_ListProperty):
828
+ """Boolean List Property"""
829
+ def __init__(self, *, uid: int = 0, **kwargs):
830
+ super(BoolList, self).__init__(type=PropertyType.boolVector, uid=uid, **kwargs)
831
+
832
+ class Int8List(_ListProperty):
833
+ """Integer 8-bit List Property"""
834
+ def __init__(self, *, uid: int = 0, **kwargs):
835
+ super(Int8List, self).__init__(type=PropertyType.byteVector, uid=uid, **kwargs)
836
+
837
+ class Int16List(_ListProperty):
838
+ """Integer 16-bit List Property"""
839
+ def __init__(self, *, uid: int = 0, **kwargs):
840
+ super(Int16List, self).__init__(type=PropertyType.shortVector, uid=uid, **kwargs)
841
+
842
+ class Int32List(_ListProperty):
843
+ """Integer 32-bit List Property"""
844
+ def __init__(self, *, uid: int = 0, **kwargs):
845
+ super(Int32List, self).__init__(type=PropertyType.intVector, uid=uid, **kwargs)
846
+
847
+ class Int64List(_ListProperty):
848
+ """Integer 64-bit List Property"""
849
+ def __init__(self, *, uid: int = 0, **kwargs):
850
+ super(Int64List, self).__init__(type=PropertyType.longVector, uid=uid, **kwargs)
851
+
852
+ class Float32List(_ListProperty):
853
+ """Floating-point 32-bit List Property"""
854
+ def __init__(self, *, uid: int = 0, **kwargs):
855
+ super(Float32List, self).__init__(type=PropertyType.floatVector, uid=uid, **kwargs)
856
+
857
+ class Float64List(_ListProperty):
858
+ """Floating-point 64-bit List Property"""
859
+ def __init__(self, *, uid: int = 0, **kwargs):
860
+ super(Float64List, self).__init__(type=PropertyType.doubleVector, uid=uid, **kwargs)
861
+
862
+ class CharList(_ListProperty):
863
+ """Char 16-bit List Property"""
864
+ def __init__(self, *, uid: int = 0, **kwargs):
865
+ super(CharList, self).__init__(type=PropertyType.charVector, uid=uid, **kwargs)