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
objectbox/objectbox.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import objectbox.store
|
|
17
|
+
from warnings import warn
|
|
18
|
+
|
|
19
|
+
class ObjectBox(objectbox.store.Store):
|
|
20
|
+
"""*Deprecated Interface*"""
|
|
21
|
+
def __init__(self, c_store):
|
|
22
|
+
"""This throws a deprecation warning on initialization."""
|
|
23
|
+
warn(f'{self.__class__.__name__} will be deprecated, use Store from objectbox.store.', DeprecationWarning, stacklevel=2)
|
|
24
|
+
super().__init__(c_store=c_store)
|
objectbox/query.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
from objectbox.c import *
|
|
20
|
+
from objectbox.exceptions import IllegalArgumentError, IllegalStateError
|
|
21
|
+
from objectbox.model.properties import Property, PropertyType
|
|
22
|
+
from objectbox.utils import date_value_to_int
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ObjectWithScore(NamedTuple):
|
|
29
|
+
""" An object with its query score, e.g. the distance of a nearest neighbor search (see
|
|
30
|
+
:meth:`Query.find_with_scores`). A tuple: unpack it via ``for obj, score in query.find_with_scores():``. """
|
|
31
|
+
object: Any
|
|
32
|
+
score: float
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IdWithScore(NamedTuple):
|
|
36
|
+
""" An object ID with its query score, e.g. the distance of a nearest neighbor search (see
|
|
37
|
+
:meth:`Query.find_ids_with_scores`). A tuple: unpack it via ``for id, score in query.find_ids_with_scores():``. """
|
|
38
|
+
id: int
|
|
39
|
+
score: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Query:
|
|
43
|
+
"""
|
|
44
|
+
Query is a reusable object that allows you to find objects in the database.
|
|
45
|
+
It is created by calling ``Box.query()`` with conditions inside and finalizing with ``build()``.
|
|
46
|
+
Note: Query objects are not thread-safe and should not be shared between threads.
|
|
47
|
+
"""
|
|
48
|
+
def __init__(self, c_query, box: 'Box', alias_properties: Optional[Dict[str, Tuple['_Entity', int]]] = None):
|
|
49
|
+
""" This is an internal constructor. Use ``Box.query()`` instead. """
|
|
50
|
+
self._c_query_or_none = c_query # None once closed
|
|
51
|
+
self._box = box
|
|
52
|
+
self._entity = self._box._entity
|
|
53
|
+
self._store = box._store
|
|
54
|
+
self._alias_properties = alias_properties or {} # Alias -> (entity, property ID) of the condition
|
|
55
|
+
self._store._add_native_resource(self)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def _c_query(self):
|
|
59
|
+
c_query = self._c_query_or_none
|
|
60
|
+
if c_query is None:
|
|
61
|
+
self._store._c_store # Raises if the store is closed (which closed the query)
|
|
62
|
+
raise IllegalStateError("The query is closed")
|
|
63
|
+
return c_query
|
|
64
|
+
|
|
65
|
+
def close(self):
|
|
66
|
+
""" Closes the query, freeing its native resources; it can not be used afterwards. Closing again does
|
|
67
|
+
nothing. Queries not closed explicitly are closed when they are garbage collected or the store is closed.
|
|
68
|
+
A query can also be used in a ``with`` statement, which closes it at the end. """
|
|
69
|
+
c_query = self._c_query_or_none
|
|
70
|
+
if c_query is not None:
|
|
71
|
+
self._c_query_or_none = None
|
|
72
|
+
self._store._remove_native_resource(self)
|
|
73
|
+
obx_query_close(c_query)
|
|
74
|
+
|
|
75
|
+
def is_closed(self) -> bool:
|
|
76
|
+
return self._c_query_or_none is None
|
|
77
|
+
|
|
78
|
+
def __enter__(self) -> 'Query':
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
82
|
+
self.close()
|
|
83
|
+
|
|
84
|
+
def __del__(self):
|
|
85
|
+
try:
|
|
86
|
+
self.close()
|
|
87
|
+
except Exception: # E.g. during interpreter shutdown
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def find(self) -> list:
|
|
91
|
+
""" Finds a list of objects matching query. """
|
|
92
|
+
with self._store.read_tx(): # We need a read transaction to ensure the object data stays valid
|
|
93
|
+
# OBX_bytes_array*
|
|
94
|
+
c_bytes_array_p = obx_query_find(self._c_query)
|
|
95
|
+
try:
|
|
96
|
+
# OBX_bytes_array
|
|
97
|
+
c_bytes_array = c_bytes_array_p.contents
|
|
98
|
+
|
|
99
|
+
result = []
|
|
100
|
+
for i in range(c_bytes_array.count):
|
|
101
|
+
# OBX_bytes
|
|
102
|
+
c_bytes = c_bytes_array.data[i]
|
|
103
|
+
data = c_voidp_as_bytes(c_bytes.data, c_bytes.size)
|
|
104
|
+
result.append(self._box._entity._unmarshal(data))
|
|
105
|
+
return result
|
|
106
|
+
finally:
|
|
107
|
+
obx_bytes_array_free(c_bytes_array_p)
|
|
108
|
+
|
|
109
|
+
def find_first(self):
|
|
110
|
+
""" Finds the first object matching the query, or None if there is none.
|
|
111
|
+
"First" is by ID (ascending), unless an order was set (see ``QueryBuilder.order()``). """
|
|
112
|
+
return self._find_one(obx_query_find_first)
|
|
113
|
+
|
|
114
|
+
def find_unique(self):
|
|
115
|
+
""" Finds the only object matching the query, or None if there is none.
|
|
116
|
+
Raises a ``NonUniqueResultError`` if more than one object matches. """
|
|
117
|
+
return self._find_one(obx_query_find_unique)
|
|
118
|
+
|
|
119
|
+
def _find_one(self, c_find):
|
|
120
|
+
with self._store.read_tx(): # We need a read transaction to ensure the object data stays valid
|
|
121
|
+
c_data = ctypes.c_void_p()
|
|
122
|
+
c_size = ctypes.c_size_t()
|
|
123
|
+
code = c_find(self._c_query, ctypes.byref(c_data), ctypes.byref(c_size))
|
|
124
|
+
if code == DbErrorCode.OBX_NOT_FOUND:
|
|
125
|
+
return None
|
|
126
|
+
check_obx_err(code, c_find, None)
|
|
127
|
+
data = c_voidp_as_bytes(c_data, c_size.value)
|
|
128
|
+
return self._box._entity._unmarshal(data)
|
|
129
|
+
|
|
130
|
+
def find_ids(self) -> List[int]:
|
|
131
|
+
""" Finds a list of object IDs matching query.
|
|
132
|
+
The result is sorted by ID (ascending order), unless an order was set (see ``QueryBuilder.order()``). """
|
|
133
|
+
c_id_array_p = obx_query_find_ids(self._c_query)
|
|
134
|
+
try:
|
|
135
|
+
c_id_array: OBX_id_array = c_id_array_p.contents
|
|
136
|
+
if c_id_array.count == 0:
|
|
137
|
+
return []
|
|
138
|
+
ids = ctypes.cast(c_id_array.ids, ctypes.POINTER(obx_id * c_id_array.count))
|
|
139
|
+
return list(ids.contents)
|
|
140
|
+
finally:
|
|
141
|
+
obx_id_array_free(c_id_array_p)
|
|
142
|
+
|
|
143
|
+
def find_with_scores(self) -> List[ObjectWithScore]:
|
|
144
|
+
""" Finds objects matching the query associated to their query score (e.g. distance in NN search).
|
|
145
|
+
The result is sorted by score in ascending order; each is an :class:`ObjectWithScore` (``object``, ``score``),
|
|
146
|
+
a tuple. """
|
|
147
|
+
with self._store.read_tx(): # We need a read transaction to ensure the object data stays valid
|
|
148
|
+
c_bytes_score_array_p = obx_query_find_with_scores(self._c_query)
|
|
149
|
+
try:
|
|
150
|
+
# OBX_bytes_score_array
|
|
151
|
+
c_bytes_score_array: OBX_bytes_score_array = c_bytes_score_array_p.contents
|
|
152
|
+
result = []
|
|
153
|
+
for i in range(c_bytes_score_array.count):
|
|
154
|
+
c_bytes_score: OBX_bytes_score = c_bytes_score_array.bytes_scores[i]
|
|
155
|
+
data = c_voidp_as_bytes(c_bytes_score.data, c_bytes_score.size)
|
|
156
|
+
score = c_bytes_score.score
|
|
157
|
+
|
|
158
|
+
object_ = self._box._entity._unmarshal(data)
|
|
159
|
+
result.append(ObjectWithScore(object_, score))
|
|
160
|
+
return result
|
|
161
|
+
finally:
|
|
162
|
+
obx_bytes_score_array_free(c_bytes_score_array_p)
|
|
163
|
+
|
|
164
|
+
def find_ids_with_scores(self) -> List[IdWithScore]:
|
|
165
|
+
""" Finds object IDs matching the query associated to their query score (e.g. distance in NN search).
|
|
166
|
+
The resulting list is sorted by score in ascending order; each is an :class:`IdWithScore` (``id``,
|
|
167
|
+
``score``), a tuple. """
|
|
168
|
+
c_id_score_array_p = obx_query_find_ids_with_scores(self._c_query)
|
|
169
|
+
try:
|
|
170
|
+
# OBX_id_score_array
|
|
171
|
+
c_id_score_array: OBX_bytes_score_array = c_id_score_array_p.contents
|
|
172
|
+
result = []
|
|
173
|
+
for i in range(c_id_score_array.count):
|
|
174
|
+
c_id_score: OBX_id_score = c_id_score_array.ids_scores[i]
|
|
175
|
+
result.append(IdWithScore(c_id_score.id, c_id_score.score))
|
|
176
|
+
return result
|
|
177
|
+
finally:
|
|
178
|
+
obx_id_score_array_free(c_id_score_array_p)
|
|
179
|
+
|
|
180
|
+
def find_ids_by_score(self) -> List[int]:
|
|
181
|
+
""" Finds object IDs matching the query ordered by their query score (e.g. distance in NN search).
|
|
182
|
+
The resulting list of IDs is sorted by score in ascending order. """
|
|
183
|
+
# TODO extract utility function for ID array conversion
|
|
184
|
+
c_id_array_p = obx_query_find_ids_by_score(self._c_query)
|
|
185
|
+
try:
|
|
186
|
+
c_id_array: OBX_id_array = c_id_array_p.contents
|
|
187
|
+
if c_id_array.count == 0:
|
|
188
|
+
return []
|
|
189
|
+
ids = ctypes.cast(c_id_array.ids, ctypes.POINTER(obx_id * c_id_array.count))
|
|
190
|
+
return list(ids.contents)
|
|
191
|
+
finally:
|
|
192
|
+
obx_id_array_free(c_id_array_p)
|
|
193
|
+
|
|
194
|
+
def find_ids_by_score_numpy(self) -> np.array:
|
|
195
|
+
""" Finds object IDs matching the query ordered by their query score (e.g. distance in NN search).
|
|
196
|
+
The resulting list of IDs is sorted by score in ascending order. """
|
|
197
|
+
# TODO extract utility function for ID array conversion
|
|
198
|
+
c_id_array_p = obx_query_find_ids_by_score(self._c_query)
|
|
199
|
+
try:
|
|
200
|
+
c_id_array: OBX_id_array = c_id_array_p.contents
|
|
201
|
+
c_count = c_id_array.count
|
|
202
|
+
import numpy as np
|
|
203
|
+
numpy_array = np.empty(c_count, dtype=np.uint64)
|
|
204
|
+
if c_count > 0:
|
|
205
|
+
c_ids = ctypes.cast(c_id_array.ids, ctypes.POINTER(obx_id))
|
|
206
|
+
ctypes.memmove(numpy_array.ctypes.data, c_ids, numpy_array.nbytes)
|
|
207
|
+
return numpy_array
|
|
208
|
+
finally:
|
|
209
|
+
obx_id_array_free(c_id_array_p)
|
|
210
|
+
|
|
211
|
+
def count(self) -> int:
|
|
212
|
+
""" Counts the objects that are matched by this query.
|
|
213
|
+
In other words, it gives the size of the result set). """
|
|
214
|
+
count = ctypes.c_uint64()
|
|
215
|
+
obx_query_count(self._c_query, ctypes.byref(count))
|
|
216
|
+
return int(count.value)
|
|
217
|
+
|
|
218
|
+
def remove(self) -> int:
|
|
219
|
+
""" Removes the objects that are matched by this query.
|
|
220
|
+
|
|
221
|
+
:return: Number of objects removed.
|
|
222
|
+
"""
|
|
223
|
+
count = ctypes.c_uint64()
|
|
224
|
+
obx_query_remove(self._c_query, ctypes.byref(count))
|
|
225
|
+
return int(count.value)
|
|
226
|
+
|
|
227
|
+
def offset(self, offset: int) -> 'Query':
|
|
228
|
+
""" Configure an offset for this query.
|
|
229
|
+
All methods that support offset will return/process objects starting at this offset.
|
|
230
|
+
Example use case: use together with limit to get a slice of the whole result, e.g. for "result paging".
|
|
231
|
+
Call with offset=0 to reset to the default behavior, i.e. starting from the first element. """
|
|
232
|
+
obx_query_offset(self._c_query, Query._check_count("offset", offset))
|
|
233
|
+
return self
|
|
234
|
+
|
|
235
|
+
def limit(self, limit: int) -> 'Query':
|
|
236
|
+
""" Configure a limit for this query.
|
|
237
|
+
All methods that support limit will return/process only the given number of objects.
|
|
238
|
+
Example use case: use together with offset to get a slice of the whole result, e.g. for "result paging".
|
|
239
|
+
Call with limit=0 to reset to the default behavior - zero limit means no limit applied. """
|
|
240
|
+
obx_query_limit(self._c_query, Query._check_count("limit", limit))
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _check_count(name: str, value) -> int:
|
|
245
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
246
|
+
raise TypeError(f"{name} must be an int, got {value!r}")
|
|
247
|
+
if value < 0: # Would wrap around in C (size_t), e.g. an offset beyond all results
|
|
248
|
+
raise IllegalArgumentError(f"{name} must not be negative, got {value}")
|
|
249
|
+
return value
|
|
250
|
+
|
|
251
|
+
@overload
|
|
252
|
+
def set_parameter(self, prop: Union[int, str, 'Property'],
|
|
253
|
+
value: Union[str, int, float, bytes, datetime]) -> 'Query': ...
|
|
254
|
+
|
|
255
|
+
@overload
|
|
256
|
+
def set_parameter(self, prop: Union[int, str, 'Property'],
|
|
257
|
+
value: Union[List[str], List[int], List[float], List[datetime], np.ndarray]) -> 'Query': ...
|
|
258
|
+
|
|
259
|
+
def set_parameter(self, prop, value) -> 'Query':
|
|
260
|
+
""" Changes the value of the query condition on the given property, e.g. a string or a number, the list of
|
|
261
|
+
a "one of" condition or the vector of a nearest neighbor search. The type of the property decides which
|
|
262
|
+
kind of value is set; e.g. a list sets the vector for a vector property and the "one of" values otherwise.
|
|
263
|
+
For conditions with two values (e.g. ``between()``), use :meth:`set_parameters`.
|
|
264
|
+
If there are multiple conditions on the property, give them an alias and use :meth:`set_parameter_alias`.
|
|
265
|
+
|
|
266
|
+
Example: ``query.set_parameter(Person.name, "Joe")``
|
|
267
|
+
|
|
268
|
+
:param prop:
|
|
269
|
+
The property (or its name or ID).
|
|
270
|
+
:param value:
|
|
271
|
+
A string, integer, float, bytes, or a list of strings or integers ("one of" conditions), or a vector
|
|
272
|
+
(list or numpy array of floats). For a nearest neighbor search, an integer sets the maximum result count.
|
|
273
|
+
For ``Date``/``DateNano`` properties, also a ``datetime`` (or a float with seconds; integers are taken
|
|
274
|
+
as they are, i.e. milliseconds or nanoseconds).
|
|
275
|
+
:return:
|
|
276
|
+
This query (to chain calls).
|
|
277
|
+
:raises TypeError:
|
|
278
|
+
If the value does not fit the property type.
|
|
279
|
+
"""
|
|
280
|
+
entity, prop_id = self._entity_and_property_id(prop)
|
|
281
|
+
kind, value = _parameter_value(self._property_type(entity, prop_id), value)
|
|
282
|
+
return self._set_parameter(entity, prop_id, None, kind, value)
|
|
283
|
+
|
|
284
|
+
@overload
|
|
285
|
+
def set_parameter_alias(self, alias: str, value: Union[str, int, float, bytes, datetime]) -> 'Query': ...
|
|
286
|
+
|
|
287
|
+
@overload
|
|
288
|
+
def set_parameter_alias(self, alias: str,
|
|
289
|
+
value: Union[List[str], List[int], List[float], List[datetime], np.ndarray]) -> 'Query':
|
|
290
|
+
...
|
|
291
|
+
|
|
292
|
+
def set_parameter_alias(self, alias: str, value) -> 'Query':
|
|
293
|
+
""" Like :meth:`set_parameter`, for the condition having the given alias
|
|
294
|
+
(see ``PropertyQueryCondition.alias()`` and ``QueryBuilder.alias()``).
|
|
295
|
+
|
|
296
|
+
Example: ``query.set_parameter_alias("name", "Joe")``
|
|
297
|
+
"""
|
|
298
|
+
prop_type = self._alias_property_type(alias)
|
|
299
|
+
type_size = None
|
|
300
|
+
if prop_type is None and (isinstance(value, (list, tuple)) or is_ndarray(value)):
|
|
301
|
+
# 0 for non-scalar types (strings, vectors), so it tells integer lists from vectors
|
|
302
|
+
type_size = obx_query_param_alias_get_type_size(self._c_query, c_str(alias))
|
|
303
|
+
kind, value = _parameter_value(prop_type, value, type_size)
|
|
304
|
+
return self._set_parameter(None, None, alias, kind, value)
|
|
305
|
+
|
|
306
|
+
def set_parameters(self, prop: Union[int, str, 'Property'], value_a: Union[int, float, datetime],
|
|
307
|
+
value_b: Union[int, float, datetime]) -> 'Query':
|
|
308
|
+
""" Changes the two values of the query condition on the given property, i.e. the range of ``between()``.
|
|
309
|
+
The type of the property decides how; see :meth:`set_parameter` for single values.
|
|
310
|
+
|
|
311
|
+
Example: ``query.set_parameters(Person.age, 18, 30)``
|
|
312
|
+
|
|
313
|
+
:return:
|
|
314
|
+
This query (to chain calls).
|
|
315
|
+
:raises TypeError:
|
|
316
|
+
If the values do not fit the property type.
|
|
317
|
+
"""
|
|
318
|
+
entity, prop_id = self._entity_and_property_id(prop)
|
|
319
|
+
kind, value_a, value_b = _parameter_values(self._property_type(entity, prop_id), value_a, value_b)
|
|
320
|
+
return self._set_parameter(entity, prop_id, None, kind, value_a, value_b)
|
|
321
|
+
|
|
322
|
+
def set_parameters_alias(self, alias: str, value_a: Union[int, float, datetime],
|
|
323
|
+
value_b: Union[int, float, datetime]) -> 'Query':
|
|
324
|
+
""" Like :meth:`set_parameters`, for the condition having the given alias
|
|
325
|
+
(see ``PropertyQueryCondition.alias()``). """
|
|
326
|
+
kind, value_a, value_b = _parameter_values(self._alias_property_type(alias), value_a, value_b)
|
|
327
|
+
return self._set_parameter(None, None, alias, kind, value_a, value_b)
|
|
328
|
+
|
|
329
|
+
def describe(self) -> str:
|
|
330
|
+
""" Describes the query (e.g. its entity and the properties of its conditions), e.g. for debugging;
|
|
331
|
+
the format may change. See also :meth:`describe_parameters`. """
|
|
332
|
+
return obx_query_describe(self._c_query).decode('utf-8')
|
|
333
|
+
|
|
334
|
+
def describe_parameters(self) -> str:
|
|
335
|
+
""" Describes the query conditions with their current parameter values, e.g. for logging or debugging. """
|
|
336
|
+
return obx_query_describe_params(self._c_query).decode('utf-8')
|
|
337
|
+
|
|
338
|
+
def _set_parameter(self, entity, prop_id: Optional[int], alias: Optional[str], kind: str, *values) -> 'Query':
|
|
339
|
+
""" Calls the C function for the kind of parameter (see _parameter_functions) for the property or alias. """
|
|
340
|
+
if kind == "ints": # The C API has separate functions for 32-bit and 64-bit integer properties
|
|
341
|
+
if alias is None:
|
|
342
|
+
type_size = obx_query_param_get_type_size(self._c_query, entity._id, prop_id)
|
|
343
|
+
else:
|
|
344
|
+
type_size = obx_query_param_alias_get_type_size(self._c_query, c_str(alias))
|
|
345
|
+
kind = "int32s" if type_size == 4 else "int64s"
|
|
346
|
+
by_property, by_alias, to_c_args = _parameter_functions[kind]
|
|
347
|
+
if alias is None:
|
|
348
|
+
by_property(self._c_query, entity._id, prop_id, *to_c_args(*values))
|
|
349
|
+
else:
|
|
350
|
+
by_alias(self._c_query, c_str(alias), *to_c_args(*values))
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def _entity_and_property_id(self, prop) -> Tuple['_Entity', int]:
|
|
354
|
+
""" The entity is the query's, or another one's for its Property, e.g. of a linked entity (see
|
|
355
|
+
QueryBuilder.link()). """
|
|
356
|
+
if isinstance(prop, Property) and prop._entity is not None:
|
|
357
|
+
return prop._entity, prop.id
|
|
358
|
+
return self._entity, self._entity._get_property_id(prop)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _property_type(entity, prop_id: int) -> int:
|
|
362
|
+
for prop in entity._properties:
|
|
363
|
+
if prop.id == prop_id:
|
|
364
|
+
return prop._ob_type
|
|
365
|
+
raise IllegalArgumentError(f"Property ID {prop_id} not found in entity {entity._name}")
|
|
366
|
+
|
|
367
|
+
def _alias_property_type(self, alias: str) -> Optional[int]:
|
|
368
|
+
""" None if the property is unknown (no property recorded for the alias). """
|
|
369
|
+
entity_and_prop_id = self._alias_properties.get(alias)
|
|
370
|
+
return self._property_type(*entity_and_prop_id) if entity_and_prop_id is not None else None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# Kind of parameter: (C function by property, C function by alias, converts the value(s) to the C arguments)
|
|
374
|
+
_parameter_functions = {
|
|
375
|
+
"string": (obx_query_param_string, obx_query_param_alias_string, lambda v: (c_str(v),)),
|
|
376
|
+
"strings": (obx_query_param_strings, obx_query_param_alias_strings,
|
|
377
|
+
lambda v: (c_array([c_str(s) for s in v], ctypes.c_char_p), len(v))),
|
|
378
|
+
"int": (obx_query_param_int, obx_query_param_alias_int, lambda v: (v,)),
|
|
379
|
+
"int32s": (obx_query_param_int32s, obx_query_param_alias_int32s,
|
|
380
|
+
lambda v: (c_array(v, ctypes.c_int32), len(v))),
|
|
381
|
+
"int64s": (obx_query_param_int64s, obx_query_param_alias_int64s,
|
|
382
|
+
lambda v: (c_array(v, ctypes.c_int64), len(v))),
|
|
383
|
+
"2ints": (obx_query_param_2ints, obx_query_param_alias_2ints, lambda a, b: (a, b)),
|
|
384
|
+
"double": (obx_query_param_double, obx_query_param_alias_double, lambda v: (v,)),
|
|
385
|
+
"2doubles": (obx_query_param_2doubles, obx_query_param_alias_2doubles, lambda a, b: (a, b)),
|
|
386
|
+
"bytes": (obx_query_param_bytes, obx_query_param_alias_bytes, lambda v: (bytes(v), len(v))),
|
|
387
|
+
"vector_f32": (obx_query_param_vector_float32, obx_query_param_alias_vector_float32,
|
|
388
|
+
lambda v: (c_array(v, ctypes.c_float), len(v))),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _parameter_value(prop_type: Optional[int], value, type_size: Optional[int] = None) -> Tuple[str, Any]:
|
|
393
|
+
""" Returns the kind of parameter (see _parameter_functions) and the (converted) value.
|
|
394
|
+
The property type is None if unknown (no property recorded for the alias); then the value decides, and for
|
|
395
|
+
lists, the type size (0 for strings and vectors). """
|
|
396
|
+
if isinstance(value, (list, tuple)) or is_ndarray(value):
|
|
397
|
+
if prop_type == OBXPropertyType_FloatVector or (prop_type is None and (is_ndarray(value) or (
|
|
398
|
+
value and not type_size and not all(isinstance(v, str) for v in value)))):
|
|
399
|
+
if is_ndarray(value):
|
|
400
|
+
if value.dtype != "float32":
|
|
401
|
+
raise TypeError(f"A numpy vector must have dtype float32, got: {value.dtype}")
|
|
402
|
+
elif not all(_is_number(v) for v in value):
|
|
403
|
+
raise TypeError("A vector parameter must be a list of numbers or a numpy array")
|
|
404
|
+
return "vector_f32", value
|
|
405
|
+
if prop_type == OBXPropertyType_String or (prop_type is None and not type_size):
|
|
406
|
+
if not all(isinstance(v, str) for v in value):
|
|
407
|
+
raise TypeError("The values for a string property must be strings")
|
|
408
|
+
return "strings", value
|
|
409
|
+
if prop_type is None or prop_type in _integer_types:
|
|
410
|
+
return "ints", [_int_parameter(prop_type, v) for v in value]
|
|
411
|
+
raise TypeError(f"A list parameter is not supported for property type {_type_name(prop_type)}")
|
|
412
|
+
if isinstance(value, str):
|
|
413
|
+
if prop_type not in (None, OBXPropertyType_String):
|
|
414
|
+
raise TypeError(f"A string value does not fit property type {_type_name(prop_type)}")
|
|
415
|
+
return "string", value
|
|
416
|
+
if isinstance(value, (bytes, bytearray)):
|
|
417
|
+
if prop_type not in (None, OBXPropertyType_ByteVector):
|
|
418
|
+
raise TypeError(f"A bytes value does not fit property type {_type_name(prop_type)}")
|
|
419
|
+
return "bytes", value
|
|
420
|
+
if prop_type in _float_types or (prop_type is None and isinstance(value, float)):
|
|
421
|
+
if not _is_number(value):
|
|
422
|
+
raise TypeError(f"A {type(value).__name__} value does not fit property type {_type_name(prop_type)}")
|
|
423
|
+
return "double", float(value)
|
|
424
|
+
# Integer types; vector properties take an int as the maximum result count of a nearest neighbor search
|
|
425
|
+
if prop_type is None or prop_type in _integer_types or prop_type == OBXPropertyType_FloatVector:
|
|
426
|
+
return "int", _int_parameter(prop_type, value)
|
|
427
|
+
raise TypeError(f"A {type(value).__name__} value does not fit property type {_type_name(prop_type)}")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parameter_values(prop_type: Optional[int], value_a, value_b) -> Tuple[str, Any, Any]:
|
|
431
|
+
""" Like _parameter_value() for two values (e.g. between). """
|
|
432
|
+
if prop_type in _float_types or (prop_type is None and (isinstance(value_a, float) or isinstance(value_b, float))):
|
|
433
|
+
if not _is_number(value_a) or not _is_number(value_b):
|
|
434
|
+
raise TypeError(f"Two numbers expected for property type {_type_name(prop_type)}")
|
|
435
|
+
return "2doubles", float(value_a), float(value_b)
|
|
436
|
+
if prop_type is None or prop_type in _integer_types:
|
|
437
|
+
return "2ints", _int_parameter(prop_type, value_a), _int_parameter(prop_type, value_b)
|
|
438
|
+
raise TypeError(f"Two values are not supported for property type {_type_name(prop_type)}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
_integer_types = (OBXPropertyType_Bool, OBXPropertyType_Byte, OBXPropertyType_Short, OBXPropertyType_Char,
|
|
442
|
+
OBXPropertyType_Int, OBXPropertyType_Long, OBXPropertyType_Relation, OBXPropertyType_Date,
|
|
443
|
+
OBXPropertyType_DateNano)
|
|
444
|
+
_float_types = (OBXPropertyType_Float, OBXPropertyType_Double)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _is_number(value) -> bool:
|
|
448
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _int_parameter(prop_type: Optional[int], value) -> int:
|
|
452
|
+
""" Converts dates (datetime or float seconds) to milliseconds or nanoseconds for Date/DateNano properties. """
|
|
453
|
+
if prop_type == OBXPropertyType_Date:
|
|
454
|
+
return date_value_to_int(value, 1000)
|
|
455
|
+
if prop_type == OBXPropertyType_DateNano:
|
|
456
|
+
return date_value_to_int(value, 1000000000)
|
|
457
|
+
if not isinstance(value, int):
|
|
458
|
+
raise TypeError(f"A {type(value).__name__} value does not fit property type {_type_name(prop_type)}")
|
|
459
|
+
return value
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _type_name(prop_type: Optional[int]) -> str:
|
|
463
|
+
if prop_type is None:
|
|
464
|
+
return "unknown (no property recorded for the alias)"
|
|
465
|
+
try:
|
|
466
|
+
return PropertyType(prop_type).name
|
|
467
|
+
except ValueError:
|
|
468
|
+
return str(prop_type)
|