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.
@@ -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()
@@ -0,0 +1,4 @@
1
+ KEY_TYPE_DATA: int = 0x18
2
+ KEY_TYPE_RELATION: int = 0x08
3
+ RELATION_DIRECTION_FORWARD: int = 0
4
+ RELATION_DIRECTION_BACKWARD: int = 2
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
+ ![GitHub License](https://img.shields.io/github/license/o-murphy/ob-dump)
34
+ ![PyPI Version](https://img.shields.io/pypi/v/ob_dump_reader?logo=pypi)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ob-dump-reader = ob_dump_reader.__main__:main
@@ -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