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,399 @@
1
+ # Copyright 2026 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
+ """Experimental FlatBuffers codegen for objects (see objectbox.experimental).
16
+
17
+ Generates Python source for a marshal and an unmarshal function per entity and compiles them once (like dataclasses
18
+ or attrs generate __init__).
19
+ - marshal (put) does what FastBuilder.marshal does, but unrolled for the entity's properties: no loops over property
20
+ descriptions and no dispatch on property kinds. The layouts (see fast_builder) are shared with a FastBuilder, so both
21
+ produce the same bytes as the generic flatbuffers.Builder.
22
+ - unmarshal (get, query) does what _Entity._unmarshal_table does (via flatbuffers.Table), with the vtable read at
23
+ once and one struct call per field. Like the reference, it reads any valid FlatBuffer of the entity (it follows the
24
+ vtable; fields may be missing or in any order), and it returns the same values (incl. the reference's quirks).
25
+
26
+ The generated source contains no values: defaults, properties, numpy dtypes etc. are passed via the namespace of
27
+ exec. It is registered in linecache, so tracebacks show the generated lines.
28
+ Null values: the generated marshal checks each value for None; an object with a null value is marshalled by
29
+ FastBuilder.marshal_with_nulls (generic, layout per null combination; see FastBuilder), objects without nulls are
30
+ unaffected but for these checks. Reading is unchanged: a missing field reads as the default, as in the reference.
31
+ """
32
+
33
+ import hashlib
34
+ import importlib.machinery
35
+ import itertools
36
+ import keyword
37
+ import linecache
38
+ import os
39
+ import struct
40
+ from array import array
41
+ import sys
42
+ from datetime import datetime, timezone
43
+
44
+ from objectbox.model.fast_builder import FastBuilder, _pads, _uint32, _STRING, _BYTES, _NUMPY, _FLEX, _INT8_LIST, \
45
+ _FIELD_SCALAR, _FIELD_OFFSET, _FIELD_ID, _FIELD_DATE, _FIELD_BOOL
46
+ from objectbox import experimental
47
+ from objectbox.c import OBXPropertyType_String, OBXPropertyType_ByteVector, OBXPropertyType_Flex, \
48
+ OBXPropertyType_BoolVector, OBXPropertyType_ShortVector, OBXPropertyType_CharVector, OBXPropertyType_IntVector, \
49
+ OBXPropertyType_LongVector, OBXPropertyType_FloatVector, OBXPropertyType_DoubleVector, OBXPropertyType_Date, \
50
+ OBXPropertyType_DateNano
51
+ from objectbox.utils import date_value_to_int
52
+
53
+ _counter = itertools.count(1) # unique file names for linecache
54
+
55
+
56
+ class CodegenBuilder:
57
+ """Marshals and unmarshals objects of one entity with generated code; created after the entity's IDs are synced.
58
+ The generated functions are thread-safe: all state is local (layouts are created lazily, see FastBuilder).
59
+ The sources are generated at creation; each function is compiled at its first use (attribute marshal or
60
+ unmarshal), as compiling takes most of the time (e.g. ~1 ms per function for 38 properties): a process that
61
+ only puts or only reads objects of an entity compiles only one."""
62
+
63
+ def __init__(self, entity):
64
+ fast_builder = FastBuilder(entity)
65
+ self.source = _generate_source(fast_builder)
66
+ self.filename = _register(self.source, entity, "marshal")
67
+ self._marshal_namespace = _namespace(fast_builder)
68
+
69
+ self.unmarshal_source, self._unmarshal_namespace = _generate_unmarshal(entity)
70
+ self.unmarshal_filename = _register(self.unmarshal_source, entity, "unmarshal")
71
+ self._entity_name = entity._name
72
+
73
+ def __getattr__(self, name):
74
+ # Only called if the attribute is not set yet: compiles the function and sets it on the instance.
75
+ # Concurrent first calls may compile it twice; equal, so either is fine.
76
+ if name == "marshal":
77
+ source, filename, namespace = self.source, self.filename, self._marshal_namespace
78
+ elif name == "unmarshal":
79
+ source, filename, namespace = self.unmarshal_source, self.unmarshal_filename, self._unmarshal_namespace
80
+ else:
81
+ raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, name))
82
+ namespace = dict(namespace) # exec adds the function (and __builtins__)
83
+ exec(_compile(source, filename, self._entity_name, name), namespace)
84
+ function = namespace[name]
85
+ setattr(self, name, function)
86
+ return function
87
+
88
+
89
+ def _compile(source: str, filename: str, entity_name: str, kind: str):
90
+ """Compiles the source, or loads its compiled code from the cache directory (experimental._codegen_cache_dir):
91
+ the source is written there as a .py file (named by its hash), which Python compiles and caches as usual
92
+ (__pycache__ or sys.pycache_prefix, validated against the source).
93
+ Any problem with the cache: compiles without it."""
94
+ cache_dir = experimental._codegen_cache_dir
95
+ if cache_dir is not None:
96
+ try:
97
+ return _compile_cached(source, cache_dir, entity_name, kind)
98
+ except Exception:
99
+ pass
100
+ return compile(source, filename, "exec")
101
+
102
+
103
+ def _compile_cached(source: str, cache_dir: str, entity_name: str, kind: str):
104
+ data = source.encode("utf-8")
105
+ name = "objectbox_codegen_%s_%s_%s" % (kind, entity_name, hashlib.sha256(data).hexdigest()[:24])
106
+ path = os.path.join(cache_dir, name + ".py")
107
+ try:
108
+ with open(path, "rb") as file:
109
+ matches = file.read() == data
110
+ except FileNotFoundError:
111
+ matches = False
112
+ if not matches: # new (or e.g. damaged): write atomically, also if processes race
113
+ os.makedirs(cache_dir, exist_ok=True)
114
+ temp_path = "%s.%d.%s.tmp" % (path, os.getpid(), os.urandom(4).hex())
115
+ try:
116
+ with open(temp_path, "wb") as file:
117
+ file.write(data)
118
+ os.replace(temp_path, path)
119
+ finally:
120
+ if os.path.exists(temp_path):
121
+ os.remove(temp_path)
122
+ return importlib.machinery.SourceFileLoader(name, path).get_code(name)
123
+
124
+
125
+ def _register(source: str, entity, kind: str) -> str:
126
+ """Registers the source in linecache under a unique file name, so tracebacks show the generated lines."""
127
+ filename = "<objectbox codegen %s %s #%d>" % (kind, entity._name, next(_counter))
128
+ linecache.cache[filename] = (len(source), None, source.splitlines(True), filename) # mtime None: kept
129
+ return filename
130
+
131
+
132
+ def _namespace(fast_builder: FastBuilder) -> dict:
133
+ namespace = {
134
+ "_layouts": fast_builder._layouts,
135
+ "_layout": fast_builder._layout,
136
+ "_marshal_with_nulls": fast_builder.marshal_with_nulls, # objects with null values: generic, slower
137
+ "_pads": _pads,
138
+ "_uint32_pack": _uint32.pack,
139
+ "_date_value_to_int": date_value_to_int,
140
+ "_asarray": getattr(fast_builder._np, "asarray", None), # numpy and flatbuffers only if needed
141
+ "_FlexBuilder": fast_builder._flex_builder,
142
+ "_bytes": bytes,
143
+ "_array": array,
144
+ "_isinstance": isinstance,
145
+ "_getattr": getattr,
146
+ "_str": str,
147
+ "_len": len,
148
+ }
149
+ for i, (kind, name, prop, default, dtype) in enumerate(fast_builder._offset_props):
150
+ namespace.update({"_n%d" % i: name, "_p%d" % i: prop, "_d%d" % i: default, "_t%d" % i: dtype})
151
+ for j, (kind, name, prop, data) in enumerate(fast_builder._fields_in_memory_order):
152
+ namespace.update({"_fn%d" % j: name, "_fp%d" % j: prop, "_fd%d" % j: data})
153
+ if not fast_builder._offset_props:
154
+ namespace["_layout0"] = fast_builder._layout(0) # no strings or vectors: always residue 0
155
+ return namespace
156
+
157
+
158
+ def _get(name: str, name_var: str) -> str:
159
+ """Expression reading the attribute from obj; getattr for names that are no plain identifiers."""
160
+ if name.isidentifier() and not keyword.iskeyword(name):
161
+ return "obj." + name
162
+ return "_getattr(obj, %s)" % name_var
163
+
164
+
165
+ def _generate_source(fast_builder: FastBuilder) -> str:
166
+ """Source of marshal(obj, id); mirrors FastBuilder.marshal step by step (see there)."""
167
+ lines = ["def marshal(obj, id):"]
168
+ add = lines.append
169
+ offset_props = fast_builder._offset_props
170
+
171
+ # Step 1: strings and vectors, in creation order; per property: data d<i>, its length l<i> and padding z<i>, and
172
+ # its offset o<i> (bytes written so far after it)
173
+ if offset_props:
174
+ add(" off = 0")
175
+ for i, (kind, name, prop, default, dtype) in enumerate(offset_props):
176
+ add(" v = " + _get(name, "_n%d" % i))
177
+ if kind == _STRING:
178
+ add(" if v is _p%d: v = ''" % i)
179
+ add(" elif v is None: return _marshal_with_nulls(obj, id)")
180
+ add(" d%d = v.encode('utf-8')" % i)
181
+ add(" n = _len(d%d)" % i)
182
+ add(" pad = -(off + n + 1) & 3")
183
+ add(" z%d = _pads[pad + 1]" % i)
184
+ add(" l%d = _uint32_pack(n)" % i)
185
+ add(" off += pad + 1 + n + 4")
186
+ elif kind == _NUMPY:
187
+ add(" if v is _p%d: v = _d%d" % (i, i))
188
+ add(" elif v is None: return _marshal_with_nulls(obj, id)")
189
+ add(" a = _asarray(v, dtype=_t%d)" % i)
190
+ add(" if a.ndim > 1: raise TypeError('multidimensional-ndarray passed to CreateNumpyVector')")
191
+ add(" if a.dtype.str[0] != '<': a = a.byteswap(inplace=False)")
192
+ add(" d%d = a.tobytes(order='C')" % i)
193
+ add(" n = _len(d%d)" % i)
194
+ add(" pad = -(off + n) & 3")
195
+ add(" off += pad")
196
+ add(" pad_align = -(off + n) & %d" % (dtype.alignment - 1))
197
+ add(" z%d = _pads[pad_align + pad]" % i)
198
+ add(" l%d = _uint32_pack(a.size)" % i)
199
+ add(" off += pad_align + n + 4")
200
+ else: # _BYTES, _INT8_LIST, _FLEX
201
+ if kind == _INT8_LIST:
202
+ add(" if v is _p%d: v = _d%d" % (i, i))
203
+ add(" elif v is None: return _marshal_with_nulls(obj, id)")
204
+ add(" d%d = _array('b', v).tobytes()" % i)
205
+ elif kind == _FLEX:
206
+ add(" if v is _p%d: v = None" % i)
207
+ add(" flex = _FlexBuilder()")
208
+ add(" flex.Add(v)")
209
+ add(" d%d = _bytes(flex.Finish())" % i)
210
+ else:
211
+ add(" if v is _p%d: v = _d%d" % (i, i))
212
+ add(" elif v is None: return _marshal_with_nulls(obj, id)")
213
+ add(" if not _isinstance(v, _bytes): raise TypeError('non-byte vector passed to CreateByteVector')")
214
+ add(" d%d = v" % i)
215
+ add(" n = _len(d%d)" % i)
216
+ add(" pad = -(off + n) & 3")
217
+ add(" z%d = _pads[pad]" % i)
218
+ add(" l%d = _uint32_pack(n)" % i)
219
+ add(" off += pad + n + 4")
220
+ add(" o%d = off" % i)
221
+
222
+ # Steps 2-4: the layout for the residue, then the fields in memory order
223
+ if offset_props:
224
+ add(" layout = _layouts[off & 7]")
225
+ add(" if layout is None: layout = _layout(off & 7)")
226
+ add(" adjust = layout.offset_adjust")
227
+ else:
228
+ add(" layout = _layout0")
229
+ values = []
230
+ offset_field = 0
231
+ for j, (kind, name, prop, data) in enumerate(fast_builder._fields_in_memory_order):
232
+ if kind == _FIELD_OFFSET:
233
+ values.append("off + adjust[%d] - o%d" % (offset_field, data))
234
+ offset_field += 1
235
+ elif kind == _FIELD_ID:
236
+ values.append("id")
237
+ else:
238
+ add(" s%d = %s" % (j, _get(name, "_fn%d" % j)))
239
+ if kind == _FIELD_SCALAR:
240
+ add(" if s%d is _fp%d: s%d = _fd%d" % (j, j, j, j))
241
+ add(" elif s%d is None: return _marshal_with_nulls(obj, id)" % j)
242
+ values.append("s%d" % j)
243
+ elif kind == _FIELD_DATE:
244
+ add(" if s%d is None: return _marshal_with_nulls(obj, id)" % j)
245
+ values.append("_date_value_to_int(0.0 if s%d is _fp%d else s%d, _fd%d)" % (j, j, j, j))
246
+ else: # _FIELD_BOOL
247
+ assert kind == _FIELD_BOOL
248
+ add(" if s%d is _fp%d: s%d = False" % (j, j, j))
249
+ add(" elif s%d is None: return _marshal_with_nulls(obj, id)" % j)
250
+ add(" elif not (False <= s%d <= True): raise TypeError('bad number %%s for type bool' %% _str(s%d))"
251
+ % (j, j))
252
+ values.append("s%d" % j)
253
+
254
+ parts = ["layout.prefix", "layout.fields.pack(%s)" % ", ".join(values)]
255
+ for i in reversed(range(len(offset_props))):
256
+ parts += ["l%d" % i, "d%d" % i, "z%d" % i]
257
+ add(" return b''.join((%s,))" % ", ".join(parts))
258
+ return "\n".join(lines) + "\n"
259
+
260
+
261
+ # Reading: how _Entity._unmarshal_table reads each property type (via flatbuffers.Table)
262
+ _STRUCTS = {} # struct format: unpack_from
263
+
264
+
265
+ def _unpack(fmt: str):
266
+ unpack = _STRUCTS.get(fmt)
267
+ if unpack is None:
268
+ unpack = _STRUCTS[fmt] = struct.Struct(fmt).unpack_from
269
+ return unpack
270
+
271
+
272
+ _numpy_read_dtypes = { # as Table.GetVectorAsNumpy with these flatbuffers.number_types (note: chars as int16)
273
+ OBXPropertyType_BoolVector: "bool",
274
+ OBXPropertyType_ShortVector: "int16",
275
+ OBXPropertyType_CharVector: "int16",
276
+ OBXPropertyType_IntVector: "int32",
277
+ OBXPropertyType_LongVector: "int64",
278
+ OBXPropertyType_FloatVector: "float32",
279
+ OBXPropertyType_DoubleVector: "float64",
280
+ }
281
+
282
+
283
+ def absent_value_factory(prop):
284
+ """Callable without arguments returning the value read for a property missing from an object (written as null,
285
+ or by an older model): its default, as an unset property is written. For most types, the type itself (e.g. str()
286
+ gives ''); datetime, numpy arrays and Flex cannot be created that way."""
287
+ ob_type = prop._ob_type
288
+ if ob_type == OBXPropertyType_Flex:
289
+ return lambda: None
290
+ if prop._py_type == datetime:
291
+ epoch = datetime.fromtimestamp(0, timezone.utc)
292
+ return lambda: epoch # immutable
293
+ if prop._is_ndarray:
294
+ import numpy as np # imported already: numpy arrays are only read with numpy
295
+ dtype = np.dtype(_numpy_read_dtypes[ob_type]).newbyteorder("<")
296
+ return lambda: np.array([], dtype=dtype)
297
+ return prop._py_type
298
+
299
+
300
+ def _set(name: str, name_var: str) -> str:
301
+ """Statement prefix assigning the attribute on obj; setattr for names that are no plain identifiers."""
302
+ if name.isidentifier() and not keyword.iskeyword(name):
303
+ return " obj.%s = " % name
304
+ return " _setattr(obj, %s, " % name_var
305
+
306
+
307
+ def _generate_unmarshal(entity):
308
+ """Source and namespace of unmarshal(data); mirrors _Entity._unmarshal_table property by property."""
309
+ namespace = {
310
+ "_new": entity._user_type,
311
+ "_u32": _unpack("<I"),
312
+ "_s32": _unpack("<i"),
313
+ "_u16": _unpack("<H"),
314
+ "_fromtimestamp": datetime.fromtimestamp,
315
+ "_utc": timezone.utc,
316
+ "_setattr": setattr,
317
+ }
318
+ properties = entity._properties
319
+ num_slots = max([prop._fb_slot for prop in properties] + [-1]) + 1
320
+ namespace["_vtable"] = _unpack("<%dH" % num_slots)
321
+ full_vtable_size = 4 + 2 * num_slots
322
+ lines = [
323
+ "def unmarshal(data):",
324
+ " pos = _u32(data, 0)[0]",
325
+ " vt = pos - _s32(data, pos)[0]",
326
+ " vt_size = _u16(data, vt)[0]",
327
+ " if vt_size >= %d:" % full_vtable_size,
328
+ " e = _vtable(data, vt + 4)",
329
+ " else: # fewer fields in the vtable (e.g. written by an older model); missing ones read as 0 (= absent)",
330
+ " e = _vtable(data[:vt + vt_size] + b'\\0' * %d, vt + 4)" % (full_vtable_size - 4),
331
+ " obj = _new()",
332
+ ]
333
+ add = lines.append
334
+ for k, prop in enumerate(properties):
335
+ ob_type = prop._ob_type
336
+ py_type = prop._py_type
337
+ namespace["_n%d" % k] = prop.name
338
+ namespace["_default%d" % k] = py_type
339
+ namespace["_absent%d" % k] = absent_value_factory(prop)
340
+ to_list = ".tolist()" if py_type == list else "" # list types: read as numpy arrays
341
+ add(" o = e[%d]" % prop._fb_slot)
342
+ add(" if o:")
343
+ if ob_type in (OBXPropertyType_String, OBXPropertyType_ByteVector, OBXPropertyType_Flex) or \
344
+ ob_type in _numpy_read_dtypes:
345
+ add(" p = pos + o")
346
+ add(" p += _u32(data, p)[0]")
347
+ add(" n = _u32(data, p)[0]")
348
+ add(" p += 4")
349
+ if ob_type == OBXPropertyType_String:
350
+ add(" v = data[p:p + n].decode()") # UTF-8 (the default; faster without the argument)
351
+ elif ob_type == OBXPropertyType_ByteVector:
352
+ if py_type == list: # Int8List: signed bytes
353
+ namespace["_array"] = array
354
+ add(" v = _array('b', data[p:p + n]).tolist()")
355
+ else:
356
+ add(" v = _default%d(data[p:p + n])" % k)
357
+ elif ob_type == OBXPropertyType_Flex:
358
+ import flatbuffers.flexbuffers # imports numpy (if available), so only if needed
359
+ namespace["_flex_loads"] = flatbuffers.flexbuffers.Loads
360
+ add(" v = _flex_loads(data[p:p + n])")
361
+ else:
362
+ import numpy as np
363
+ namespace["_frombuffer"] = np.frombuffer
364
+ namespace["_dtype%d" % k] = np.dtype(_numpy_read_dtypes[ob_type]).newbyteorder("<")
365
+ add(" v = _frombuffer(data, dtype=_dtype%d, count=n, offset=p)%s" % (k, to_list))
366
+ else:
367
+ namespace["_get%d" % k] = _unpack("<" + prop._fb_format)
368
+ read = "_get%d(data, pos + o)[0]" % k
369
+ if ob_type == OBXPropertyType_Date and py_type == datetime:
370
+ read = "_fromtimestamp(%s / 1000.0, _utc)" % read # tz positionally: faster
371
+ elif ob_type == OBXPropertyType_Date and py_type == float:
372
+ read = "%s / 1000.0" % read
373
+ elif ob_type == OBXPropertyType_DateNano and py_type == datetime:
374
+ read = "_fromtimestamp(%s / 1000000000.0, _utc)" % read
375
+ elif ob_type == OBXPropertyType_DateNano and py_type == float:
376
+ read = "%s / 1000000000.0" % read
377
+ add(" v = " + read)
378
+ add(" else:")
379
+ add(" v = _absent%d() # default (empty) value if not present in the object" % k)
380
+ assign = _set(prop.name, "_n%d" % k)
381
+ add(assign + "v" + (")" if assign.startswith(" _setattr") else ""))
382
+ add(" return obj")
383
+ return "\n".join(lines) + "\n", namespace
384
+
385
+
386
+ def same_values(a, b) -> bool:
387
+ """Whether two unmarshalled values are equal, incl. their types (numpy arrays: dtype and bytes; NaN equals NaN)."""
388
+ if type(a) is not type(b):
389
+ return False
390
+ np = sys.modules.get("numpy")
391
+ if np is not None and isinstance(a, np.ndarray):
392
+ return a.dtype == b.dtype and a.shape == b.shape and a.tobytes() == b.tobytes()
393
+ if isinstance(a, float):
394
+ return a == b or (a != a and b != b)
395
+ if isinstance(a, (list, tuple)):
396
+ return len(a) == len(b) and all(same_values(x, y) for x, y in zip(a, b))
397
+ if isinstance(a, dict):
398
+ return a.keys() == b.keys() and all(same_values(a[key], b[key]) for key in a)
399
+ return a == b