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