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 ADDED
@@ -0,0 +1,145 @@
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
+ """ObjectBox Python API"""
16
+
17
+ from objectbox.store import Store
18
+ from objectbox.box import Box
19
+ from objectbox.model.entity import Entity, SyncEntity
20
+ from objectbox.model.properties import Id, String, Index, Bool, Int8, Int16, Int32, Int64, Float32, Float64, Bytes, BoolVector, Int8Vector, Int16Vector, Int32Vector, Int64Vector, Float32Vector, Float64Vector, CharVector, BoolList, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, CharList, Date, DateNano, Flex, Relation, IndexType, Unique, ConflictStrategy, HnswIndex, VectorDistanceType, HnswFlags, SyncClock, SyncPrecedence
21
+ from objectbox.model.model import Model
22
+ from objectbox.c import version_core, DebugFlags
23
+ # The exceptions most likely handled by applications (all are in objectbox.exceptions; its TimeoutError is not
24
+ # exported here, as it would shadow the built-in TimeoutError on "from objectbox import *")
25
+ from objectbox.exceptions import (DbError, IllegalArgumentError, IllegalStateError, UniqueViolatedError,
26
+ ConstraintViolatedError, NonUniqueResultError, IdNotFoundError,
27
+ FeatureNotAvailableError, SchemaError, DbFullError, MaxDataSizeExceededError,
28
+ MaxReadersExceededError, StoreMustShutdownError, FileCorruptError,
29
+ FilePagesCorruptError, NumericOverflowError)
30
+ from objectbox.version import Version
31
+ from objectbox.condition import PropertyQueryCondition
32
+ from objectbox.query import Query, ObjectWithScore, IdWithScore
33
+ from objectbox.query_builder import QueryBuilder
34
+ from objectbox.builder import Builder
35
+ from objectbox.objectbox import ObjectBox
36
+ from objectbox.query_builder import OrderFlags
37
+
38
+ __all__ = [
39
+ 'Box',
40
+ 'Builder',
41
+ 'Model',
42
+ 'Entity',
43
+ 'Id',
44
+ 'Bool',
45
+ 'Int8',
46
+ 'Int16',
47
+ 'Int32',
48
+ 'Int64',
49
+ 'Float32',
50
+ 'Float64',
51
+ 'Bytes',
52
+ 'String',
53
+ 'BoolVector',
54
+ 'Int8Vector',
55
+ 'Int16Vector',
56
+ 'Int32Vector',
57
+ 'Int64Vector',
58
+ 'Float32Vector',
59
+ 'Float64Vector',
60
+ 'CharVector',
61
+ 'BoolList',
62
+ 'Int8List',
63
+ 'Int16List',
64
+ 'Int32List',
65
+ 'Int64List',
66
+ 'Float32List',
67
+ 'Float64List',
68
+ 'CharList',
69
+ 'Date',
70
+ 'DateNano',
71
+ 'Flex',
72
+ 'Relation',
73
+ 'Index',
74
+ 'IndexType',
75
+ 'Unique',
76
+ 'ConflictStrategy',
77
+ 'HnswIndex',
78
+ 'VectorDistanceType',
79
+ 'Store',
80
+ 'ObjectBox',
81
+ 'DbError',
82
+ 'IllegalArgumentError',
83
+ 'IllegalStateError',
84
+ 'UniqueViolatedError',
85
+ 'ConstraintViolatedError',
86
+ 'NonUniqueResultError',
87
+ 'IdNotFoundError',
88
+ 'FeatureNotAvailableError',
89
+ 'SchemaError',
90
+ 'DbFullError',
91
+ 'MaxDataSizeExceededError',
92
+ 'MaxReadersExceededError',
93
+ 'StoreMustShutdownError',
94
+ 'FileCorruptError',
95
+ 'FilePagesCorruptError',
96
+ 'NumericOverflowError',
97
+ 'version',
98
+ 'version_info',
99
+ 'DebugFlags',
100
+ 'PropertyQueryCondition',
101
+ 'HnswFlags',
102
+ 'Query',
103
+ 'ObjectWithScore',
104
+ 'IdWithScore',
105
+ 'QueryBuilder',
106
+ 'SyncEntity',
107
+ 'SyncClock',
108
+ 'SyncPrecedence',
109
+ 'OrderFlags',
110
+ # Sync (loaded on first use; see __getattr__)
111
+ 'Sync',
112
+ 'SyncClient',
113
+ 'SyncCredentials',
114
+ 'SyncState',
115
+ 'SyncRequestUpdatesMode',
116
+ 'SyncFlags',
117
+ 'SyncStats',
118
+ 'SyncCode',
119
+ 'SyncChange',
120
+ 'SyncListener',
121
+ 'SyncLoginListener',
122
+ 'SyncConnectionListener',
123
+ 'SyncCompletedListener',
124
+ 'SyncTimeListener',
125
+ 'SyncChangeListener',
126
+ ]
127
+
128
+ _sync_names = frozenset(__all__[__all__.index('Sync'):])
129
+
130
+
131
+ def __getattr__(name: str):
132
+ # Sync names are imported on first use: importing objectbox.sync takes a few milliseconds, also for apps without
133
+ # Sync (a module __getattr__, PEP 562; also used by "from objectbox import *")
134
+ if name in _sync_names:
135
+ import objectbox.sync
136
+ return getattr(objectbox.sync, name)
137
+ raise AttributeError(f"module 'objectbox' has no attribute '{name}'")
138
+
139
+ # Python binding version
140
+ version = Version(5, 0, 0)
141
+ """ObjectBox Python package version"""
142
+
143
+ def version_info():
144
+ """Returns a string with Python and core version information."""
145
+ return "ObjectBox Python version " + str(version) + " using dynamic library version " + str(version_core)
objectbox/box.py ADDED
@@ -0,0 +1,294 @@
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
+
16
+ from objectbox.model.entity import _Entity
17
+ from objectbox.store import Store
18
+ from objectbox.query_builder import QueryBuilder
19
+ from objectbox.condition import QueryCondition
20
+ from objectbox.c import *
21
+ from objectbox.exceptions import DbError, IllegalArgumentError, IllegalStateError
22
+
23
+
24
+ class Box:
25
+ """Interface to Entities"""
26
+ def __init__(self, store: Store, entity: _Entity):
27
+ if not isinstance(entity, _Entity):
28
+ raise TypeError(f"Expected an entity (a class decorated with @Entity()), got {entity!r}")
29
+
30
+ self._store = store
31
+ self._entity = entity
32
+ self._c_box_of_open_store = obx_box(store._c_store, entity._id)
33
+
34
+ @property
35
+ def _c_box(self):
36
+ if self._store._c_store_or_none is None: # The C box would be invalid
37
+ raise IllegalStateError("Store is closed")
38
+ return self._c_box_of_open_store
39
+
40
+ def contains(self, object_id: int) -> bool:
41
+ """Checks whether the box contains the given object ID."""
42
+ contained = ctypes.c_bool()
43
+ obx_box_contains(self._c_box, object_id, ctypes.byref(contained))
44
+ return bool(contained.value)
45
+
46
+ def is_empty(self) -> bool:
47
+ """Returns true if box is empty (i.e. no objects of entity type are available)."""
48
+ is_empty = ctypes.c_bool()
49
+ obx_box_is_empty(self._c_box, ctypes.byref(is_empty))
50
+ return bool(is_empty.value)
51
+
52
+ def count(self, limit: int = 0) -> int:
53
+ """Returns the count of existing objects."""
54
+ count = ctypes.c_uint64()
55
+ obx_box_count(self._c_box, limit, ctypes.byref(count))
56
+ return int(count.value)
57
+
58
+ def put(self, *objects):
59
+ """Puts an object and returns its ID; puts a list (or tuple) of objects, or several objects passed as
60
+ arguments, in one transaction and returns their IDs (a list, in the same order).
61
+
62
+ A new object has the ID 0 and gets its ID assigned (set on the object). An object with a non-zero ID is
63
+ updated (or inserted if it does not exist, e.g. was removed); its ID must have been assigned by ObjectBox
64
+ before (else raises IllegalArgumentError), unless the ID property is ``Id(assignable=True)``."""
65
+
66
+ if len(objects) != 1:
67
+ return self._put_many(objects)
68
+ elif isinstance(objects[0], (list, tuple)):
69
+ return self._put_many(objects[0])
70
+ else:
71
+ return self._put_one(objects[0])
72
+
73
+ def _put_one(self, obj) -> int:
74
+ # A new object (ID 0) gets its ID assigned while putting (saves a C call to reserve it); this writes the ID
75
+ # into the data, so it must be a writable copy. A non-zero ID must have been assigned by ObjectBox before
76
+ # (the C library checks it), unless the ID property is assignable.
77
+ self._check_type(obj)
78
+ id = self._entity._get_object_id(obj)
79
+ if id: # The C library does not write into the data (the ID is set)
80
+ data = bytes(self._entity._marshal(obj, id))
81
+ return obx_box_put_object4(self._c_box, data, len(data), OBXPutMode_PUT)
82
+ data = self._entity._marshal(obj, 0)
83
+ size = len(data)
84
+ id = obx_box_put_object4(self._c_box, ctypes.create_string_buffer(bytes(data), size), size, OBXPutMode_PUT)
85
+ self._entity._set_object_id(obj, id)
86
+ return id
87
+
88
+ def _put_many(self, objects) -> List[int]:
89
+ if not objects:
90
+ return []
91
+ # retrieve IDs from the objects (to distinguish new objects and updates)
92
+ new = {}
93
+ ids = {}
94
+ for k in range(len(objects)):
95
+ self._check_type(objects[k])
96
+ id = self._entity._get_object_id(objects[k])
97
+ if not id:
98
+ new[k] = 0
99
+ ids[k] = id
100
+
101
+ with self._store.write_tx(): # One transaction for checking/reserving the IDs and putting
102
+ self._put_many_in_tx(objects, new, ids)
103
+
104
+ # assign new IDs on the object
105
+ for k in new.keys():
106
+ self._entity._set_object_id(objects[k], ids[k])
107
+ return list(ids.values())
108
+
109
+ def _put_many_in_tx(self, objects, new, ids) -> None:
110
+ # Check the given IDs: obx_box_put_many() takes them as reserved; obx_box_id_for_put() raises for an ID not
111
+ # assigned by ObjectBox before (and advances the ID sequence for assignable IDs). Checking the highest ID
112
+ # covers all: lower IDs were assigned before, too.
113
+ max_id = max(ids.values(), default=0)
114
+ if max_id:
115
+ obx_box_id_for_put(self._c_box, max_id)
116
+
117
+ # acquire IDs for the new objects and set them
118
+ if len(new) > 0:
119
+ c_next_id = obx_id()
120
+ obx_box_ids_for_put(self._c_box, len(new), ctypes.byref(c_next_id))
121
+ next_id = c_next_id.value
122
+ for k in new.keys():
123
+ ids[k] = next_id
124
+ next_id += 1
125
+
126
+ # allocate a C bytes array structure where we will push the object data
127
+ # OBX_bytes_array with .count = len(objects)
128
+ c_bytes_array_p = obx_bytes_array(len(objects))
129
+
130
+ try:
131
+ # we need to keep the data around until put_many is executed because obx_bytes_array_set doesn't do a copy
132
+ data = {}
133
+ for k in range(len(objects)):
134
+ data[k] = bytes(self._entity._marshal(objects[k], ids[k]))
135
+ key = ctypes.c_size_t(k)
136
+
137
+ # OBX_bytes_array.data[k] = data
138
+ obx_bytes_array_set(c_bytes_array_p, key,
139
+ data[k], len(data[k]))
140
+
141
+ c_ids = (obx_id * len(ids))(*ids.values())
142
+ obx_box_put_many(self._c_box, c_bytes_array_p,
143
+ c_ids, OBXPutMode_PUT)
144
+
145
+ finally:
146
+ obx_bytes_array_free(c_bytes_array_p)
147
+
148
+ def update(self, object_):
149
+ """Updates the existing object having the same ID as the given object.
150
+
151
+ Raises if the object has no ID (i.e. it was never put) or the ID doesn't match any stored object."""
152
+ self._check_type(object_)
153
+ object_id = self._entity._get_object_id(object_)
154
+ if not object_id:
155
+ raise IllegalArgumentError("Cannot update an object without an ID; use put() for new objects")
156
+ data = self._entity._marshal(object_, object_id)
157
+ obx_box_update(self._c_box, object_id, bytes(data), len(data))
158
+
159
+ def get(self, id: int):
160
+ """Get object by given Id or None if not found."""
161
+ c_box = self._c_box # Raises if the store is closed
162
+ tx = obx_txn_read(self._store._c_store_or_none) # not via read_tx(): less overhead for this hot path
163
+ try:
164
+ c_data = ctypes.c_void_p()
165
+ c_size = ctypes.c_size_t()
166
+ code : obx_err = obx_box_get(c_box, id, ctypes.byref(
167
+ c_data), ctypes.byref(c_size))
168
+ if code == 404:
169
+ return None
170
+ elif code != 0:
171
+ raise DbError.from_code(code)
172
+ data = c_voidp_as_bytes(c_data, c_size.value)
173
+ finally:
174
+ obx_txn_close(tx)
175
+ return self._entity._unmarshal(data)
176
+
177
+ def get_many(self, id_array: List[int]) -> list:
178
+ """Gets objects matching the given ID list.
179
+
180
+ :return: objects matching the ID list, preserving the order.
181
+ If an invalid ID is supplied, the corresponding object is None.
182
+ """
183
+
184
+ if not id_array: # Empty
185
+ return []
186
+
187
+ c_ids = (obx_id * len(id_array))(*id_array)
188
+ c_id_array = OBX_id_array(ctypes.cast(c_ids, ctypes.POINTER(obx_id)), len(id_array))
189
+
190
+ with self._store.read_tx():
191
+ c_bytes_array_p = obx_box_get_many(self._c_box, ctypes.byref(c_id_array))
192
+
193
+ result = []
194
+ try:
195
+ c_bytes_array = c_bytes_array_p.contents
196
+ for i in range(c_bytes_array.count):
197
+ c_bytes = c_bytes_array.data[i]
198
+ if c_bytes.data is None or c_bytes.size == 0: # User supplied an invalid ID
199
+ result.append(None)
200
+ else:
201
+ data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
202
+ result.append(self._entity._unmarshal(data))
203
+ finally:
204
+ obx_bytes_array_free(c_bytes_array_p)
205
+ return result
206
+
207
+ def get_all(self) -> list:
208
+ """Get all objects."""
209
+ with self._store.read_tx():
210
+ # OBX_bytes_array*
211
+ c_bytes_array_p = obx_box_get_all(self._c_box)
212
+
213
+ try:
214
+ # OBX_bytes_array
215
+ c_bytes_array = c_bytes_array_p.contents
216
+
217
+ result = list()
218
+ for i in range(c_bytes_array.count):
219
+ # OBX_bytes
220
+ c_bytes = c_bytes_array.data[i]
221
+ data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
222
+ result.append(self._entity._unmarshal(data))
223
+
224
+ return result
225
+ finally:
226
+ obx_bytes_array_free(c_bytes_array_p)
227
+
228
+ def remove(self, id_or_object) -> bool:
229
+ """Removes the object with the given ID (or the given object); returns False if it did not exist.
230
+ See :meth:`remove_many` for several objects."""
231
+ if isinstance(id_or_object, (list, tuple)):
232
+ raise TypeError("remove() takes one ID or object; use remove_many() for several")
233
+ id = self._id_of(id_or_object)
234
+ code : obx_err = obx_box_remove(self._c_box, id)
235
+ if code == 404:
236
+ return False
237
+ elif code != 0:
238
+ raise DbError.from_code(code)
239
+ return True
240
+
241
+ def remove_many(self, ids_or_objects) -> int:
242
+ """Removes the objects with the given IDs (or the given objects) in one transaction; IDs that do not exist
243
+ are skipped. Returns the number of removed objects."""
244
+ ids = [self._id_of(item) for item in ids_or_objects]
245
+ if not ids:
246
+ return 0
247
+ c_ids = (obx_id * len(ids))(*ids)
248
+ c_id_array = OBX_id_array(ctypes.cast(c_ids, ctypes.POINTER(obx_id)), len(ids))
249
+ count = ctypes.c_uint64()
250
+ obx_box_remove_many(self._c_box, ctypes.byref(c_id_array), ctypes.byref(count))
251
+ return int(count.value)
252
+
253
+ def _id_of(self, id_or_object) -> int:
254
+ if self._is_entity_object(id_or_object):
255
+ return self._entity._get_object_id(id_or_object)
256
+ if not isinstance(id_or_object, int) or isinstance(id_or_object, bool):
257
+ raise TypeError(f"Expected an ID (int) or an object of entity {self._entity._name}, got {id_or_object!r}")
258
+ return id_or_object
259
+
260
+ def _check_type(self, obj):
261
+ if not self._is_entity_object(obj):
262
+ raise TypeError(f"Expected an object of entity {self._entity._name}, got {obj!r}")
263
+
264
+ def _is_entity_object(self, obj) -> bool:
265
+ # Also accepts the same class defined again, e.g. by re-running a notebook cell
266
+ user_type = self._entity._user_type
267
+ return isinstance(obj, user_type) or type(obj).__name__ == user_type.__name__
268
+
269
+ def remove_all(self) -> int:
270
+ """Removes all objects and returns number of removed."""
271
+ count = ctypes.c_uint64()
272
+ obx_box_remove_all(self._c_box, ctypes.byref(count))
273
+ return int(count.value)
274
+
275
+ def query(self, condition: Optional[QueryCondition] = None) -> QueryBuilder:
276
+ """ Initiates Query creation for the Entity associated by this Box.
277
+ Technically, it creates a QueryBuilder object, and you have to call build() on it to get the Query object.
278
+
279
+ :param condition:
280
+ Applies the given condition(s) to the new QueryBuilder object.
281
+ For example, assuming you defined an @Entity called "MyEntity" with a string property "name":
282
+
283
+ ``query = box.query(MyEntity.name.equals("Johnny")).build()``
284
+
285
+ It's also possible to pass multiple conditions:
286
+
287
+ ``query = box.query(MyEntity.name.equals("Johnny") & MyEntity.age.greater_than(21)).build()``
288
+
289
+ Note: ``&`` is the logical AND operator, and ``|`` is the logical OR operator.
290
+ """
291
+ qb = QueryBuilder(self._store, self)
292
+ if condition is not None:
293
+ condition.apply(qb)
294
+ return qb
objectbox/builder.py ADDED
@@ -0,0 +1,42 @@
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
+
16
+ from objectbox.c import *
17
+ from objectbox.model.model import Model
18
+ from objectbox.store import Store
19
+ from objectbox.store_options import StoreOptions
20
+ from warnings import warn
21
+
22
+ class Builder:
23
+ """*Deprecated Interface*"""
24
+ def __init__(self):
25
+ """This throws a deprecation warning on initialization."""
26
+ warn(f'Using {self.__class__.__name__} is deprecated, please use Store(model=, directory= ...) from objectbox.store.', DeprecationWarning, stacklevel=2)
27
+ self._kwargs = { }
28
+
29
+ def directory(self, path: str) -> 'Builder':
30
+ self._kwargs['directory'] = path
31
+ return self
32
+
33
+ def max_db_size_in_kb(self, size_in_kb: int) -> 'Builder':
34
+ self._kwargs['max_db_size_in_kb'] = size_in_kb
35
+ return self
36
+
37
+ def model(self, model: Model) -> 'Builder':
38
+ self._kwargs['model'] = model
39
+ return self
40
+
41
+ def build(self) -> 'Store':
42
+ return Store(**self._kwargs)