ob-dump-reader 0.0.0.dev0__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.
- ob_dump_reader/__init__.py +93 -0
- ob_dump_reader/__main__.py +30 -0
- ob_dump_reader/__pycache__/__init__.cpython-314.pyc +0 -0
- ob_dump_reader/__pycache__/__main__.cpython-314.pyc +0 -0
- ob_dump_reader/_constants.py +4 -0
- ob_dump_reader/aio.py +99 -0
- ob_dump_reader/decode_helpers.py +98 -0
- ob_dump_reader-0.0.0.dev0.dist-info/METADATA +174 -0
- ob_dump_reader-0.0.0.dev0.dist-info/RECORD +13 -0
- ob_dump_reader-0.0.0.dev0.dist-info/WHEEL +5 -0
- ob_dump_reader-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- ob_dump_reader-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- ob_dump_reader-0.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from collections.abc import Buffer
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import lmdb
|
|
7
|
+
|
|
8
|
+
from ob_dump_reader._constants import (
|
|
9
|
+
KEY_TYPE_DATA,
|
|
10
|
+
KEY_TYPE_RELATION,
|
|
11
|
+
RELATION_DIRECTION_FORWARD,
|
|
12
|
+
)
|
|
13
|
+
from ob_dump_reader.decode_helpers import read_uint32be
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"ObRecord",
|
|
17
|
+
"read_objectbox_records",
|
|
18
|
+
"read_objectbox_to_many_targets",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ObRecord:
|
|
24
|
+
entity_id: int
|
|
25
|
+
object_id: int
|
|
26
|
+
data: Buffer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_objectbox_records(
|
|
30
|
+
objectbox_dir: PathLike,
|
|
31
|
+
on_record: Callable[[ObRecord], None],
|
|
32
|
+
) -> None:
|
|
33
|
+
# readonly=True is enough on its own — LMDB's MVCC design lets any number
|
|
34
|
+
# of readers run safely alongside one concurrent writer, in any process,
|
|
35
|
+
# so no write transaction is needed just to open the store.
|
|
36
|
+
with lmdb.Environment(
|
|
37
|
+
str(objectbox_dir),
|
|
38
|
+
map_size=512 * 1024 * 1024,
|
|
39
|
+
max_dbs=4,
|
|
40
|
+
readonly=True,
|
|
41
|
+
) as db:
|
|
42
|
+
with db.begin(write=False) as txn:
|
|
43
|
+
with txn.cursor() as cursor:
|
|
44
|
+
for key, value in cursor:
|
|
45
|
+
if key and len(key) == 8 and key[0] == KEY_TYPE_DATA:
|
|
46
|
+
entity_id = key[3] // 4
|
|
47
|
+
object_id = read_uint32be(key, 4)
|
|
48
|
+
|
|
49
|
+
on_record(
|
|
50
|
+
ObRecord(
|
|
51
|
+
entity_id=entity_id,
|
|
52
|
+
object_id=object_id,
|
|
53
|
+
data=bytes(value), # copy out of the mmap'd page
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_objectbox_to_many_targets(
|
|
59
|
+
objectbox_dir: PathLike, relation_id: int, source_object_id: int
|
|
60
|
+
) -> list[int]:
|
|
61
|
+
"""Resolves a `ToMany` relation's forward-direction target ids.
|
|
62
|
+
|
|
63
|
+
Not part of the FlatBuffers table at all — a separate LMDB key range,
|
|
64
|
+
12 bytes: type(0x08) + 2 reserved bytes + ((relation_id << 2) |
|
|
65
|
+
direction) + source_id (u32 BE) + target_id (u32 BE), value always
|
|
66
|
+
empty. `direction` 0 is the declared forward direction; 2 is an
|
|
67
|
+
auto-maintained reverse index for ObjectBox's own query engine, not
|
|
68
|
+
needed for a one-directional dump.
|
|
69
|
+
"""
|
|
70
|
+
prefix = bytes(
|
|
71
|
+
[
|
|
72
|
+
KEY_TYPE_RELATION,
|
|
73
|
+
0x00,
|
|
74
|
+
0x00,
|
|
75
|
+
(relation_id << 2) | RELATION_DIRECTION_FORWARD,
|
|
76
|
+
]
|
|
77
|
+
) + source_object_id.to_bytes(4, "big")
|
|
78
|
+
|
|
79
|
+
targets: list[int] = []
|
|
80
|
+
with lmdb.Environment(
|
|
81
|
+
str(objectbox_dir),
|
|
82
|
+
map_size=512 * 1024 * 1024,
|
|
83
|
+
max_dbs=4,
|
|
84
|
+
readonly=True,
|
|
85
|
+
) as db:
|
|
86
|
+
with db.begin(write=False) as txn:
|
|
87
|
+
with txn.cursor() as cursor:
|
|
88
|
+
if cursor.set_range(prefix):
|
|
89
|
+
for key, _ in cursor:
|
|
90
|
+
if bytes(key[:8]) != prefix:
|
|
91
|
+
break
|
|
92
|
+
targets.append(read_uint32be(key, 8))
|
|
93
|
+
return targets
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import ob_dump_reader as ob
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="ob_dump_reader",
|
|
9
|
+
description=(
|
|
10
|
+
"Walk an ObjectBox LMDB store and print each record's entity id, "
|
|
11
|
+
"object id, and raw FlatBuffers data length. Decoding the data "
|
|
12
|
+
"itself needs a flatc-generated schema — see the package README."
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"objectbox_dir",
|
|
17
|
+
help="path to the ObjectBox data directory (containing data.mdb)",
|
|
18
|
+
)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
def on_record(record: ob.ObRecord) -> None:
|
|
22
|
+
print(
|
|
23
|
+
f"entity={record.entity_id} object={record.object_id} bytes={len(record.data)}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ob.read_objectbox_records(args.objectbox_dir, on_record)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
Binary file
|
|
Binary file
|
ob_dump_reader/aio.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from collections.abc import Buffer
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from typing import Any, Callable, Coroutine
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import lmdb
|
|
7
|
+
import lmdb.aio
|
|
8
|
+
|
|
9
|
+
from ob_dump_reader._constants import (
|
|
10
|
+
KEY_TYPE_DATA,
|
|
11
|
+
KEY_TYPE_RELATION,
|
|
12
|
+
RELATION_DIRECTION_FORWARD,
|
|
13
|
+
)
|
|
14
|
+
from ob_dump_reader.decode_helpers import read_uint32be
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"ObRecord",
|
|
18
|
+
"read_objectbox_records",
|
|
19
|
+
"read_objectbox_to_many_targets",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ObRecord:
|
|
25
|
+
entity_id: int
|
|
26
|
+
object_id: int
|
|
27
|
+
data: Buffer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def read_objectbox_records(
|
|
31
|
+
objectbox_dir: PathLike | str,
|
|
32
|
+
on_record: Callable[[ObRecord], Coroutine[Any, Any, None]],
|
|
33
|
+
) -> None:
|
|
34
|
+
# AsyncEnvironment wraps a plain (sync) Environment — it doesn't take a
|
|
35
|
+
# path/map_size/etc. itself (lmdb.aio's own docstring example). Cursor
|
|
36
|
+
# has no __aiter__, and its iternext() collects every remaining entry
|
|
37
|
+
# into a list before returning (nothing streams past the executor
|
|
38
|
+
# boundary) — stepping via first()/next() instead keeps this a true
|
|
39
|
+
# per-record callback, not "load the whole database, then callback".
|
|
40
|
+
env = lmdb.Environment(
|
|
41
|
+
str(objectbox_dir),
|
|
42
|
+
map_size=512 * 1024 * 1024,
|
|
43
|
+
max_dbs=4,
|
|
44
|
+
readonly=True,
|
|
45
|
+
)
|
|
46
|
+
aenv = lmdb.aio.wrap(env)
|
|
47
|
+
async with aenv:
|
|
48
|
+
async with aenv.begin(write=False) as txn:
|
|
49
|
+
async with txn.cursor() as cursor:
|
|
50
|
+
has_item = await cursor.first()
|
|
51
|
+
while has_item:
|
|
52
|
+
key, value = cursor.item()
|
|
53
|
+
if key and len(key) == 8 and key[0] == KEY_TYPE_DATA:
|
|
54
|
+
entity_id = key[3] // 4
|
|
55
|
+
object_id = read_uint32be(key, 4)
|
|
56
|
+
|
|
57
|
+
await on_record(
|
|
58
|
+
ObRecord(
|
|
59
|
+
entity_id=entity_id,
|
|
60
|
+
object_id=object_id,
|
|
61
|
+
data=bytes(value),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
has_item = await cursor.next()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def read_objectbox_to_many_targets(
|
|
68
|
+
objectbox_dir: PathLike | str, relation_id: int, source_object_id: int
|
|
69
|
+
) -> list[int]:
|
|
70
|
+
"""Async counterpart of `ob_dump_reader.read_objectbox_to_many_targets` —
|
|
71
|
+
see its docstring for the key format."""
|
|
72
|
+
prefix = bytes(
|
|
73
|
+
[
|
|
74
|
+
KEY_TYPE_RELATION,
|
|
75
|
+
0x00,
|
|
76
|
+
0x00,
|
|
77
|
+
(relation_id << 2) | RELATION_DIRECTION_FORWARD,
|
|
78
|
+
]
|
|
79
|
+
) + source_object_id.to_bytes(4, "big")
|
|
80
|
+
|
|
81
|
+
targets: list[int] = []
|
|
82
|
+
env = lmdb.Environment(
|
|
83
|
+
str(objectbox_dir),
|
|
84
|
+
map_size=512 * 1024 * 1024,
|
|
85
|
+
max_dbs=4,
|
|
86
|
+
readonly=True,
|
|
87
|
+
)
|
|
88
|
+
aenv = lmdb.aio.wrap(env)
|
|
89
|
+
async with aenv:
|
|
90
|
+
async with aenv.begin(write=False) as txn:
|
|
91
|
+
async with txn.cursor() as cursor:
|
|
92
|
+
has_item = await cursor.set_range(prefix)
|
|
93
|
+
while has_item:
|
|
94
|
+
key, _ = cursor.item()
|
|
95
|
+
if bytes(key[:8]) != prefix:
|
|
96
|
+
break
|
|
97
|
+
targets.append(read_uint32be(key, 8))
|
|
98
|
+
has_item = await cursor.next()
|
|
99
|
+
return targets
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from collections.abc import Buffer
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import orjson as json
|
|
5
|
+
except ImportError:
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
from flatbuffers import flexbuffers
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"uint32be",
|
|
12
|
+
"read_uint32be",
|
|
13
|
+
"bytes_to_hex",
|
|
14
|
+
"bytes_to_uuid_string",
|
|
15
|
+
"try_parse_json_string",
|
|
16
|
+
"decode_flex",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def uint32be(v: int) -> list[int]:
|
|
21
|
+
return [
|
|
22
|
+
(v >> 24) & 0xFF,
|
|
23
|
+
(v >> 16) & 0xFF,
|
|
24
|
+
(v >> 8) & 0xFF,
|
|
25
|
+
v & 0xFF,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_uint32be(buf: Buffer, offset: int) -> int:
|
|
30
|
+
return (
|
|
31
|
+
(buf[offset] << 24)
|
|
32
|
+
| (buf[offset + 1] << 16)
|
|
33
|
+
| (buf[offset + 2] << 8)
|
|
34
|
+
| buf[offset + 3]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def bytes_to_hex(b: Buffer | list[int]) -> str:
|
|
39
|
+
return bytes(b).hex()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bytes_to_uuid_string(b: bytes | list[int]) -> str:
|
|
43
|
+
hex_str = bytes(b).hex()
|
|
44
|
+
if len(hex_str) != 32:
|
|
45
|
+
return hex_str
|
|
46
|
+
return (
|
|
47
|
+
f"{hex_str[0:8]}-{hex_str[8:12]}-"
|
|
48
|
+
f"{hex_str[12:16]}-{hex_str[16:20]}-"
|
|
49
|
+
f"{hex_str[20:32]}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def try_parse_json_string(s: str) -> Any:
|
|
54
|
+
try:
|
|
55
|
+
return json.loads(s)
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
return s
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _flex_ref_to_native(ref: Any) -> Any:
|
|
61
|
+
# flatbuffers.flexbuffers.Ref has no `.json`/`str()` serialization of its
|
|
62
|
+
# own (an earlier version of this function assumed `str(root)` gave JSON
|
|
63
|
+
# — it gives a debug repr like "Ref(buf[21:], ...)", not decodable data;
|
|
64
|
+
# caught by actually calling this against real FlexBuffers bytes, not
|
|
65
|
+
# assumed). Is*/As* are properties, not methods.
|
|
66
|
+
if ref.IsNull:
|
|
67
|
+
return None
|
|
68
|
+
if ref.IsBool:
|
|
69
|
+
return ref.AsBool
|
|
70
|
+
if ref.IsInt:
|
|
71
|
+
return ref.AsInt
|
|
72
|
+
if ref.IsFloat:
|
|
73
|
+
return ref.AsFloat
|
|
74
|
+
if ref.IsString:
|
|
75
|
+
return ref.AsString
|
|
76
|
+
if ref.IsBlob:
|
|
77
|
+
return bytes_to_hex(ref.AsBlob)
|
|
78
|
+
if ref.IsMap:
|
|
79
|
+
m = ref.AsMap
|
|
80
|
+
return {
|
|
81
|
+
key.AsKey: _flex_ref_to_native(value)
|
|
82
|
+
for key, value in zip(m.Keys, m.Values)
|
|
83
|
+
}
|
|
84
|
+
if ref.IsVector or ref.IsTypedVector or ref.IsFixedTypedVector:
|
|
85
|
+
vec = (
|
|
86
|
+
ref.AsVector
|
|
87
|
+
if ref.IsVector
|
|
88
|
+
else ref.AsTypedVector
|
|
89
|
+
if ref.IsTypedVector
|
|
90
|
+
else ref.AsFixedTypedVector
|
|
91
|
+
)
|
|
92
|
+
return [_flex_ref_to_native(vec[i]) for i in range(len(vec))]
|
|
93
|
+
raise ValueError(f"Unsupported FlexBuffers value: {ref}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def decode_flex(b: Buffer) -> Any:
|
|
97
|
+
root = flexbuffers.GetRoot(bytes(b))
|
|
98
|
+
return _flex_ref_to_native(root)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ob-dump-reader
|
|
3
|
+
Version: 0.0.0.dev0
|
|
4
|
+
Summary: Minimal ObjectBox LMDB reader toolkit
|
|
5
|
+
Author: o-murphy
|
|
6
|
+
Project-URL: Homepage, https://github.com/o-murphy/ob-dump
|
|
7
|
+
Project-URL: Bug Reports, https://github.com/o-murphy/ob-dump/issues
|
|
8
|
+
Project-URL: Source, https://github.com/o-murphy/ob-dump
|
|
9
|
+
Project-URL: Changelog, https://github.com/o-murphy/ob-dump/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: objectbox,lmdb,flatbuffers
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: flatbuffers>=25.12.19
|
|
26
|
+
Requires-Dist: lmdb>=2.2.1
|
|
27
|
+
Provides-Extra: orjson
|
|
28
|
+
Requires-Dist: orjson==3.11.9; extra == "orjson"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# ob-dump-reader
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
Minimal ObjectBox LMDB reader toolkit for Python. Its core job: walk a
|
|
37
|
+
`data.mdb` file and hand you each stored object's raw FlatBuffers table
|
|
38
|
+
bytes, plus its entity id and object id — no per-field FlatBuffers or
|
|
39
|
+
schema knowledge for that part, just [`lmdb`](https://pypi.org/project/lmdb/)
|
|
40
|
+
(the [py-lmdb](https://github.com/jnwatson/py-lmdb) bindings). It also
|
|
41
|
+
covers the handful of things `flatc --python` output can't: `ToMany`
|
|
42
|
+
relations (a separate LMDB structure, not a table field) and `Flex`/
|
|
43
|
+
`ExternalPropertyType` fields (`flatc` only knows the base FlatBuffers
|
|
44
|
+
type, not ObjectBox's semantic annotation on top) — see "Beyond `flatc`"
|
|
45
|
+
below.
|
|
46
|
+
|
|
47
|
+
Decoding those raw bytes into typed objects is left to the official
|
|
48
|
+
`flatc --python` compiler — not this package, and not [`ob-dump`](..)'s own
|
|
49
|
+
C++ core either. This is a deliberate toolkit split, not a missing feature:
|
|
50
|
+
see the parent project's [`docs/BACKLOG.md`](../docs/BACKLOG.md#schema-export---schema-and---fbs)
|
|
51
|
+
for the reasoning.
|
|
52
|
+
|
|
53
|
+
## Workflow
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
# 1. Generate a schema JSON (entityId -> table name/shape) and a .fbs from
|
|
57
|
+
# your ObjectBox model, using the ob_dump CLI (see ../README.md):
|
|
58
|
+
ob_dump --schema objectbox-model.json -o schema.json
|
|
59
|
+
ob_dump --fbs objectbox-model.json -o schema.fbs
|
|
60
|
+
|
|
61
|
+
# 2. Generate typed Python classes with the *official* FlatBuffers compiler.
|
|
62
|
+
flatc --python -o models/ schema.fbs
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import ob_dump_reader as ob
|
|
67
|
+
from models.Ammo import Ammo # from flatc --python, see step 2 above
|
|
68
|
+
|
|
69
|
+
AMMO_ENTITY_ID = 1 # from schema.json
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def on_record(record: ob.ObRecord) -> None:
|
|
73
|
+
if record.entity_id == AMMO_ENTITY_ID:
|
|
74
|
+
ammo = Ammo.GetRootAs(record.data) # flatc-generated class decodes the bytes
|
|
75
|
+
print(f"{ammo.Name()}: {ammo.BcG1()}")
|
|
76
|
+
# ... insert into whatever your new database is.
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ob.read_objectbox_records("/path/to/objectbox/dir", on_record)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Async
|
|
83
|
+
|
|
84
|
+
`ob_dump_reader.aio` mirrors the same API as `async`/`await` coroutines
|
|
85
|
+
(built on [py-lmdb's own `lmdb.aio`](https://py-lmdb.readthedocs.io/en/latest/)
|
|
86
|
+
executor-based wrapper — LMDB itself has no native async I/O, so this
|
|
87
|
+
dispatches the same blocking calls to a thread executor rather than
|
|
88
|
+
avoiding them):
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import ob_dump_reader.aio as ob
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def on_record(record: ob.ObRecord) -> None:
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
await ob.read_objectbox_records("/path/to/objectbox/dir", on_record)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Reads are in place, no copy
|
|
102
|
+
|
|
103
|
+
`read_objectbox_records`/`read_objectbox_to_many_targets` read
|
|
104
|
+
`data.mdb`/`lock.mdb` directly, with no temporary copy step — they open
|
|
105
|
+
the LMDB environment itself `readonly=True` (matching `ob-dump`'s own C++
|
|
106
|
+
`LmdbReader`), so they never need a write-capable handle. This is exactly
|
|
107
|
+
what LMDB's MVCC design is for: any number of readers can safely run
|
|
108
|
+
alongside one concurrent writer, in any process, with zero risk to the
|
|
109
|
+
original data — even while a live ObjectBox process has the same store
|
|
110
|
+
open.
|
|
111
|
+
|
|
112
|
+
## Beyond `flatc`: ToMany relations and Flex/ExternalPropertyType fields
|
|
113
|
+
|
|
114
|
+
`ob_dump --schema` lists each property's `externalType` and each entity's
|
|
115
|
+
`relations` (id, name, target entity) when present — use that to know
|
|
116
|
+
which of your fields need one of these:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import ob_dump_reader as ob
|
|
120
|
+
from ob_dump_reader.decode_helpers import (
|
|
121
|
+
decode_flex,
|
|
122
|
+
bytes_to_hex,
|
|
123
|
+
bytes_to_uuid_string,
|
|
124
|
+
try_parse_json_string,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ToMany: not part of the FlatBuffers table at all, so flatc has no
|
|
128
|
+
# accessor for it — relation_id/source_object_id come from `ob_dump --schema`
|
|
129
|
+
# and the record you're looking at (record.object_id).
|
|
130
|
+
author_ids = ob.read_objectbox_to_many_targets(db_dir, relation_id, record.object_id)
|
|
131
|
+
|
|
132
|
+
# Flex: flatc gives you the raw bytes (a bytes/bytearray field) —
|
|
133
|
+
# decode with decode_flex.
|
|
134
|
+
value = decode_flex(ammo.SomeFlexField())
|
|
135
|
+
|
|
136
|
+
# ExternalPropertyType: flatc gives you the base type's plain value
|
|
137
|
+
# (a byte blob for Uuid/Int128/Decimal128/Bson, a string for
|
|
138
|
+
# Json/JavaScript/JsonToNative) — decode with the matching helper.
|
|
139
|
+
uuid = bytes_to_uuid_string(ammo.SomeUuidField())
|
|
140
|
+
blob = bytes_to_hex(ammo.SomeBsonField())
|
|
141
|
+
parsed = try_parse_json_string(ammo.SomeJsonField())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Every one of these mirrors `ob-dump`'s own C++ decode (`src/fb_decode.cpp`)
|
|
145
|
+
exactly — same hex/UUID formatting, same JSON-parse-with-string-fallback
|
|
146
|
+
for `JavaScript`, same forward-only relation direction (see
|
|
147
|
+
`docs/BACKLOG.md` "ToMany relations" for why: the backward direction is an
|
|
148
|
+
auto-maintained index for ObjectBox's own query engine, not needed for a
|
|
149
|
+
one-directional dump).
|
|
150
|
+
|
|
151
|
+
## Why this shape
|
|
152
|
+
|
|
153
|
+
The alternative — an FFI wrapper around `ob-dump`'s C++ core — would mean
|
|
154
|
+
building/vendoring native C++ from a PyPI package for every platform. Not
|
|
155
|
+
needed here: [`py-lmdb`](https://github.com/jnwatson/py-lmdb) already gives
|
|
156
|
+
a mature, actively-maintained Python binding to LMDB, and `flatc --python`
|
|
157
|
+
already gives an officially generated, correct decoder. This package is
|
|
158
|
+
only the small piece connecting the two — LMDB traversal and the
|
|
159
|
+
ObjectBox key format (`docs/BACKLOG.md` in the parent project) — and stays
|
|
160
|
+
that small on purpose.
|
|
161
|
+
|
|
162
|
+
## Integrity & Licensing
|
|
163
|
+
|
|
164
|
+
`ob-dump` was developed as an independent implementation for reading data stored in the ObjectBox format. It adheres to a "Clean Room Design" approach regarding binary software:
|
|
165
|
+
- **Purpose-Limited:** Built solely to support data recovery and migration to another database, for projects whose own license is incompatible with `objectbox-c`'s (a closed-source binary — see "Why this exists" in the parent project's [`docs/BACKLOG.md`](../docs/BACKLOG.md)). Not intended as, and not pursued as, a competing product to ObjectBox itself — no write support, no query engine, no ongoing-database use case, strictly a one-time read-only export path out of an existing store.
|
|
166
|
+
- **No Reverse Engineering:** We have performed no decompilation, disassembly, or any other analysis of the closed-source `objectbox-c` binary.
|
|
167
|
+
- **Open Specification:** Data parsing is based exclusively on public formats (LMDB and FlatBuffers) and the open-source code of the official schema generator ([`objectbox_generator`](https://github.com/objectbox/objectbox-dart/tree/main/generator), licensed under Apache 2.0).
|
|
168
|
+
- **Model-Driven:** The decoding process is driven by the user-provided `objectbox-model.json` file, which is an open, user-accessible schema definition.
|
|
169
|
+
|
|
170
|
+
This approach ensures full licensing integrity: `ob-dump` is an independent software project that contains no proprietary or misappropriated code, making it suitable for integration into projects with any licensing requirements.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ob_dump_reader/__init__.py,sha256=xZo6JxEXBEyael1cjrdEdLQFX6n89yvfCrwN1MqA8Vo,2914
|
|
2
|
+
ob_dump_reader/__main__.py,sha256=9YXM28A2mbVlhj6dy80KFRx0oGCD_uGcxVhP4klP9mU,841
|
|
3
|
+
ob_dump_reader/_constants.py,sha256=oCFwqNMfDc1OS1Ow4Rpr8VoFzuvs-qaznJdwytJUwXc,129
|
|
4
|
+
ob_dump_reader/aio.py,sha256=E4m_AJjdxNjW-hjvFcmU9gTWjPo6TE796nscBygsAak,3182
|
|
5
|
+
ob_dump_reader/decode_helpers.py,sha256=MSEzI1PMtTEU7__6akrBIswBQCgkjEXC7j3Mtav1L8k,2493
|
|
6
|
+
ob_dump_reader/__pycache__/__init__.cpython-314.pyc,sha256=Vag19DQHJHiF8Ca_Oyz9oyfcqU6tWMTCJVeKIqRu72U,5199
|
|
7
|
+
ob_dump_reader/__pycache__/__main__.cpython-314.pyc,sha256=m_3zu3mF08ZAsYoGLaav6E_b0w4FmiX_IEItUcnqju8,517
|
|
8
|
+
ob_dump_reader-0.0.0.dev0.dist-info/licenses/LICENSE,sha256=B0rOBJOtqUXUOlNfEGwYAreh9Ch-S3LcsQ9PgRToFbc,1085
|
|
9
|
+
ob_dump_reader-0.0.0.dev0.dist-info/METADATA,sha256=KEXqhdRVsNV9TcTD69grJGOUBTcJUmlmmw5IfcAUKp8,8043
|
|
10
|
+
ob_dump_reader-0.0.0.dev0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
ob_dump_reader-0.0.0.dev0.dist-info/entry_points.txt,sha256=auDiE-9V3YNFv8-TU42PlJnqVSZOuwDJyjz5_E7Y0b4,64
|
|
12
|
+
ob_dump_reader-0.0.0.dev0.dist-info/top_level.txt,sha256=69BfNbTE2kG0migwG2gFnv7vb0qjG-e9vIS0CZfkvTU,15
|
|
13
|
+
ob_dump_reader-0.0.0.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmytro Yaroshenko (o-murphy)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ob_dump_reader
|