amber-lmdb 0.1.0__tar.gz

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.
Files changed (79) hide show
  1. amber_lmdb-0.1.0/CONTRIBUTING.md +130 -0
  2. amber_lmdb-0.1.0/LICENSE +21 -0
  3. amber_lmdb-0.1.0/PKG-INFO +353 -0
  4. amber_lmdb-0.1.0/README.md +325 -0
  5. amber_lmdb-0.1.0/benchmarks/README.md +122 -0
  6. amber_lmdb-0.1.0/benchmarks/benchmark_storage.py +754 -0
  7. amber_lmdb-0.1.0/guides/bounded-values-and-schemas.md +310 -0
  8. amber_lmdb-0.1.0/guides/combining-and-tracking-stores.md +272 -0
  9. amber_lmdb-0.1.0/guides/connection-lifecycle.md +179 -0
  10. amber_lmdb-0.1.0/guides/lmdb-3.md +100 -0
  11. amber_lmdb-0.1.0/guides/pytorch.md +122 -0
  12. amber_lmdb-0.1.0/guides/records-and-indexes.md +248 -0
  13. amber_lmdb-0.1.0/guides/scanning-stores.md +122 -0
  14. amber_lmdb-0.1.0/pyproject.toml +89 -0
  15. amber_lmdb-0.1.0/pyproject.toml.orig +63 -0
  16. amber_lmdb-0.1.0/src/amber_lmdb/__init__.py +165 -0
  17. amber_lmdb-0.1.0/src/amber_lmdb/__main__.py +5 -0
  18. amber_lmdb-0.1.0/src/amber_lmdb/_ownership.py +125 -0
  19. amber_lmdb-0.1.0/src/amber_lmdb/arrays.py +164 -0
  20. amber_lmdb-0.1.0/src/amber_lmdb/budgets.py +64 -0
  21. amber_lmdb-0.1.0/src/amber_lmdb/chunks.py +280 -0
  22. amber_lmdb-0.1.0/src/amber_lmdb/cli.py +696 -0
  23. amber_lmdb-0.1.0/src/amber_lmdb/codec.py +1469 -0
  24. amber_lmdb-0.1.0/src/amber_lmdb/collections.py +317 -0
  25. amber_lmdb-0.1.0/src/amber_lmdb/constraints.py +347 -0
  26. amber_lmdb-0.1.0/src/amber_lmdb/errors.py +86 -0
  27. amber_lmdb-0.1.0/src/amber_lmdb/fingerprints.py +315 -0
  28. amber_lmdb-0.1.0/src/amber_lmdb/generations.py +421 -0
  29. amber_lmdb-0.1.0/src/amber_lmdb/history.py +115 -0
  30. amber_lmdb-0.1.0/src/amber_lmdb/indexes.py +829 -0
  31. amber_lmdb-0.1.0/src/amber_lmdb/inspection.py +291 -0
  32. amber_lmdb-0.1.0/src/amber_lmdb/integrity.py +133 -0
  33. amber_lmdb-0.1.0/src/amber_lmdb/keys.py +139 -0
  34. amber_lmdb-0.1.0/src/amber_lmdb/lmdb.py +3750 -0
  35. amber_lmdb-0.1.0/src/amber_lmdb/manifest.py +285 -0
  36. amber_lmdb-0.1.0/src/amber_lmdb/merging.py +132 -0
  37. amber_lmdb-0.1.0/src/amber_lmdb/overlay.py +494 -0
  38. amber_lmdb-0.1.0/src/amber_lmdb/physical.py +107 -0
  39. amber_lmdb-0.1.0/src/amber_lmdb/progress.py +112 -0
  40. amber_lmdb-0.1.0/src/amber_lmdb/py.typed +0 -0
  41. amber_lmdb-0.1.0/src/amber_lmdb/scanning.py +608 -0
  42. amber_lmdb-0.1.0/src/amber_lmdb/schema.py +294 -0
  43. amber_lmdb-0.1.0/src/amber_lmdb/tensors.py +43 -0
  44. amber_lmdb-0.1.0/tests/fixtures/amber_lmdb_integrity_v1.json +8 -0
  45. amber_lmdb-0.1.0/tests/fixtures/amber_lmdb_integrity_v2.json +42 -0
  46. amber_lmdb-0.1.0/tests/fixtures/mmfabric_0_2_2.json +27 -0
  47. amber_lmdb-0.1.0/tests/test_array_header_parsing.py +172 -0
  48. amber_lmdb-0.1.0/tests/test_benchmark_storage.py +126 -0
  49. amber_lmdb-0.1.0/tests/test_cache_retention.py +349 -0
  50. amber_lmdb-0.1.0/tests/test_chunked_arrays.py +432 -0
  51. amber_lmdb-0.1.0/tests/test_collections.py +188 -0
  52. amber_lmdb-0.1.0/tests/test_completion_budgets.py +218 -0
  53. amber_lmdb-0.1.0/tests/test_completion_deletion.py +359 -0
  54. amber_lmdb-0.1.0/tests/test_completion_integrity.py +258 -0
  55. amber_lmdb-0.1.0/tests/test_completion_schema_cache.py +223 -0
  56. amber_lmdb-0.1.0/tests/test_derived_writer.py +283 -0
  57. amber_lmdb-0.1.0/tests/test_disk_compatibility.py +68 -0
  58. amber_lmdb-0.1.0/tests/test_environment_lifecycle.py +113 -0
  59. amber_lmdb-0.1.0/tests/test_evaluation_regressions.py +82 -0
  60. amber_lmdb-0.1.0/tests/test_fingerprints.py +172 -0
  61. amber_lmdb-0.1.0/tests/test_generations.py +60 -0
  62. amber_lmdb-0.1.0/tests/test_guides.py +29 -0
  63. amber_lmdb-0.1.0/tests/test_indexes.py +191 -0
  64. amber_lmdb-0.1.0/tests/test_inspection.py +372 -0
  65. amber_lmdb-0.1.0/tests/test_listing.py +168 -0
  66. amber_lmdb-0.1.0/tests/test_lmdb.py +730 -0
  67. amber_lmdb-0.1.0/tests/test_lmdb3.py +193 -0
  68. amber_lmdb-0.1.0/tests/test_lmdb_torch.py +128 -0
  69. amber_lmdb-0.1.0/tests/test_manifest.py +157 -0
  70. amber_lmdb-0.1.0/tests/test_overlay.py +210 -0
  71. amber_lmdb-0.1.0/tests/test_progress.py +118 -0
  72. amber_lmdb-0.1.0/tests/test_scanning.py +329 -0
  73. amber_lmdb-0.1.0/tests/test_schema.py +109 -0
  74. amber_lmdb-0.1.0/tests/test_tensor_reads.py +97 -0
  75. amber_lmdb-0.1.0/tests/test_trusted_reads.py +144 -0
  76. amber_lmdb-0.1.0/tests/test_typed_batches.py +140 -0
  77. amber_lmdb-0.1.0/tests/test_usability_fixes.py +662 -0
  78. amber_lmdb-0.1.0/tests/test_value_limits.py +350 -0
  79. amber_lmdb-0.1.0/tests/test_verify_threads.py +169 -0
@@ -0,0 +1,130 @@
1
+ # Contributing
2
+
3
+ Run `uv sync` to install the base package and development tools. The base test
4
+ suite must run without PyTorch. Test tensor support separately with
5
+ `uv sync --all-extras`, then run the suite again.
6
+
7
+ Before a change is ready:
8
+
9
+ ```console
10
+ uv run pytest
11
+ uv run ruff check .
12
+ uv run ruff format --check .
13
+ uv run pyright
14
+ uv run pyright --verifytypes amber_lmdb --ignoreexternal
15
+ uv lock --check
16
+ uv build
17
+ ```
18
+
19
+ The type checks above need the optional dependencies installed. Write docstrings,
20
+ comments, diagnostic messages and project documentation in English. Keep Unicode
21
+ sample data in tests and compatibility fixtures unchanged.
22
+
23
+ Public APIs must have annotations and documentation. Stored dictionary contents
24
+ are intentionally opaque and use `Any`; omitted generic arguments are not an
25
+ acceptable substitute.
26
+
27
+ Keep read transactions bounded and close them before decoding owned bytes. Do not
28
+ turn off LMDB locks, add global readahead changes or copy array bodies in
29
+ header-only inspection. Whole-value validation must be explicit and honor its
30
+ body/integrity-metadata copy budgets; unavailable or blocked codecs remain unverified.
31
+ Checksum verification must finish after transaction closure and before decoders; only
32
+ a reader built with `verify_checksums=False` may skip the digest, never its tag and
33
+ length checks, and store-level verification must not honor that option. Header-only
34
+ operations must not claim full-body integrity. Cooperative deletion requires retained
35
+ shared OS locks for native environment lifetimes and a gate around acquisition,
36
+ publication and retirement. Never replace or unlink the root gate or a lock file that
37
+ a live generation may use; a generation's own lease file is removed only by
38
+ deletion, under the root gate, after the generation has left both `versions` and
39
+ `.trash`, so no acquisition can reach it. Dry runs create and remove nothing.
40
+
41
+ Generation history is a safety contract. `HISTORY` is appended after every `CURRENT`
42
+ switch (publication and `set_current`), and a writer never removes a generation that
43
+ `HISTORY` lists: a root without `CURRENT` is refused with a hint, never repaired by
44
+ deletion; only generations absent from an existing `HISTORY` count as unpublished.
45
+ Readers pin the generation of their first read; `refresh()` releases it, `close()`
46
+ is final, a dropped reader releases its reference through a finalizer, and pickling
47
+ carries the pinned generation (an explicit `generation=` survives `refresh()`).
48
+ Lazy opening must stay safe for threads. These are compatibility contracts, not
49
+ implementation details. `cooperative=False` must remain read-only and must never
50
+ create or take coordination locks or LMDB lock files.
51
+
52
+ Key listing, counts, fingerprints and diffs read integrity metadata only; keep them
53
+ free of body copies. An overlay must never return or count an entry that exists in
54
+ more than one layer. Tuple keys starting with `"amber-lmdb"` are reserved for the
55
+ manifest `("amber-lmdb", "manifest")` and array chunks: writers refuse them for user
56
+ data, and fingerprints, diffs, listings and overlay conflict checks exclude them, so
57
+ neither a manifest nor a chunk changes content identity on its own. A chunked header
58
+ must list every chunk's envelope digest; readers compare each chunk's envelope with
59
+ it even when `verify_checksums=False`, writers remove a chunked array's chunks
60
+ whenever they replace or delete it, and `verify_store` checks the links both ways. A
61
+ derived writer copies its base while holding the base's shared lease.
62
+
63
+ Cache builders lock one of a fixed number of stripe files in `<directory>/.locks`;
64
+ never unlink them. The stripe count is recorded once in `.locks/STRIPES` and every
65
+ cache on the directory must use it; a different explicit count is an error, never a
66
+ silent remapping. Cache identities are validated and canonicalized before hashing;
67
+ tuples and non-string keys are refused rather than converted. `prune` removes an entry, or an abandoned temporary, only while
68
+ holding its stripe without waiting, and a builder publishes only under its stripe, so
69
+ a busy entry is skipped rather than removed. A lock timeout, a cooperative `cancel`
70
+ and process isolation are different stops; none may publish partial output.
71
+ `ValueLimits` may only lower the built-in decoding limits, and a node budget must
72
+ raise rather than truncate. The typed-value decoder must keep the previous
73
+ implementation's results and error messages; `tests/test_value_limits.py` compares
74
+ them on valid and mutated nodes. Threaded digest verification must report the first
75
+ corrupt entry in request order, like sequential verification. Progress callbacks run between pages, outside transactions, and a
76
+ cancelled report must never be `ok`. Tensor bodies are copied once into a private
77
+ bytearray that the returned tensor owns; never let a tensor alias LMDB memory or a
78
+ caller's buffer. The same holds for arrays read with `writable=True` and for
79
+ `read_as_tensor` on stored arrays.
80
+
81
+ Compressed NumPy arrays use array format version 3 and name their codec in the
82
+ header metadata; uncompressed arrays keep version 2, so older readers refuse
83
+ compressed arrays instead of misreading them. A compressed chunked header lists every
84
+ chunk's stored size before the digests. Decompression must be bounded by the size the
85
+ header implies and must reject any other outcome; integrity digests cover the stored
86
+ (compressed) bytes.
87
+
88
+ Value writers take the value first and the key and index only as keywords. The
89
+ command line prints one JSON document per command and keeps its exit codes: 0
90
+ success, 1 findings, 2 usage errors, 3 the command could not complete (with an
91
+ `{"error": ...}` document), 130 interrupted.
92
+
93
+ The frozen fixture in `tests/fixtures/mmfabric_0_2_2.json` was generated by the
94
+ untouched mmfabric 0.2.2 writer before extraction. Tests reconstruct a database from
95
+ its raw key/value/tag bytes without using the current writer. Do not regenerate
96
+ this fixture to make a format change pass. Legacy-format support is not a permanent
97
+ requirement. A deliberate breaking format needs a new identifier, a stated rebuild
98
+ or migration policy and separate fixtures/tests; it never authorizes automatic
99
+ conversion or removal of application stores. The current marker is `amber-lmdb-v1`;
100
+ old `mmfabric-lmdb-v2` stores are deliberately rejected. Rebuild from application
101
+ sources into a separately reviewed output. Golden envelope metadata is in
102
+ `tests/fixtures/amber_lmdb_integrity_v1.json` (SHA-256, version 1) and
103
+ `tests/fixtures/amber_lmdb_integrity_v2.json` (pinned version 1 and 2 digests).
104
+ These pinned digests are what make a store verifiable on any CPU; never regenerate
105
+ them to make a change pass, and keep every published envelope version readable. See
106
+ [formats and limits](guides/bounded-values-and-schemas.md) for the envelope contract.
107
+
108
+ This repository must not import or depend on consumers such as mmfabric or
109
+ dataset-cejc. Downstream compatibility modules must reuse this implementation,
110
+ not copy codecs or create separate environment/custom-codec registries.
111
+ The independence test blocks consumer imports while exercising the standalone API.
112
+ Frozen historical fixtures remain evidence of their original producer, rather than
113
+ being rewritten to match a new implementation.
114
+
115
+ ## Releasing
116
+
117
+ The package is published on PyPI as `amber-lmdb` from
118
+ https://github.com/Nekch0/amber-lmdb. A release bumps `version` in
119
+ `pyproject.toml`, passes CI on Linux and Windows for the pushed commit, and is
120
+ tagged `v<version>`. Build the wheel and source distribution from a clean checkout
121
+ of that tag, install the wheel into a fresh environment and run the tests against
122
+ it, then upload both files and attach them to a GitHub release. A version number
123
+ is never reused, even after a deleted upload.
124
+
125
+ Consumers replace local path sources with the published dependency only after both
126
+ files are available from PyPI; do not publish consumer wheels that depend on an
127
+ unavailable version.
128
+
129
+ No application data, private metadata or generated benchmark results should be
130
+ included in a distribution or source commit.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nekch0
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,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: amber-lmdb
3
+ Version: 0.1.0
4
+ Summary: Typed LMDB values, atomic generation publication, and bounded store inspection
5
+ Author: Nekch0
6
+ Author-email: Nekch0 <ptdmc.akdmc@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Typing :: Typed
14
+ Requires-Dist: lmdb>=3.0.0,<4.0.0
15
+ Requires-Dist: numpy>=2.4.6
16
+ Requires-Dist: msgpack>=1.2.1
17
+ Requires-Dist: frozendict>=2.4.7
18
+ Requires-Dist: ml-dtypes>=0.5.4 ; extra == 'dtypes'
19
+ Requires-Dist: torch>=2.12.1 ; extra == 'torch'
20
+ Requires-Python: >=3.12
21
+ Project-URL: Homepage, https://github.com/Nekch0/amber-lmdb
22
+ Project-URL: Documentation, https://github.com/Nekch0/amber-lmdb/tree/main/guides
23
+ Project-URL: Repository, https://github.com/Nekch0/amber-lmdb
24
+ Project-URL: Issues, https://github.com/Nekch0/amber-lmdb/issues
25
+ Provides-Extra: dtypes
26
+ Provides-Extra: torch
27
+ Description-Content-Type: text/markdown
28
+
29
+ # amber-lmdb
30
+
31
+ Store Python values and NumPy arrays by key in LMDB. Every write builds a complete
32
+ new generation and publishes it with an atomic `CURRENT` switch; readers stay on
33
+ the generation they opened until they ask for a newer one.
34
+
35
+ Python 3.12 or later is required.
36
+
37
+ ## Install
38
+
39
+ ```console
40
+ pip install amber-lmdb
41
+ amber-lmdb --help
42
+ ```
43
+
44
+ The base installation depends on LMDB, NumPy, MessagePack and frozendict. It does
45
+ not install or import PyTorch. Install `amber-lmdb[torch]` for tensor conversion
46
+ and `amber-lmdb[dtypes]` for NumPy extension dtypes that require ml-dtypes.
47
+
48
+ ## Concepts
49
+
50
+ - **Dataset root**: the directory you pass as `path`. It holds a `FORMAT` marker,
51
+ the `versions/` directory with one subdirectory per generation, the `CURRENT`
52
+ pointer, the `HISTORY` log and `.coordination` lock files.
53
+ - **Generation**: one complete LMDB database (`versions/v-<32 hex>`). A writer
54
+ stages a new generation beside the root and publishes it by rewriting `CURRENT`;
55
+ nothing is ever modified in place. `HISTORY` records every switch in order.
56
+ - **Pinning**: a reader opens the generation `CURRENT` selects at its first read and
57
+ keeps reading it, even after newer publications, until `refresh()`. Old
58
+ generations stay on disk until you delete them explicitly.
59
+ - **Keys**: a string, a path (stored in POSIX form) or a tuple of strings, plus an
60
+ optional index in `[0, 2**64)`. `"a"` and `("a",)` are different keys. All
61
+ entries of one key with indices form a sequence you can count and list.
62
+ - **Writer tags and typed readers**: each entry records which writer stored it, and
63
+ each reader method accepts only matching tags, so a value comes back with the type
64
+ it was written with. `read` chooses the decoder from the tag.
65
+ - **Integrity envelopes**: every entry has a digest bound to its key, tag and
66
+ length. Reads verify bodies before decoding; `verify_store` checks everything.
67
+
68
+ ## Write and read
69
+
70
+ ```python
71
+ from pathlib import Path
72
+ import numpy as np
73
+ from amber_lmdb import LMDBReader, LMDBWriter
74
+
75
+ path = Path("example.lmdb")
76
+ with LMDBWriter(path) as writer:
77
+ writer.write_dict({"name": "example", "items": 2}, key="meta")
78
+ writer.write_ndarray(np.arange(8, dtype=np.float32).reshape(2, 4), key="features", index=0)
79
+ writer.write_primitive(3, key=("session", "count"))
80
+ # A successful exit commits the remaining writes and publishes the generation.
81
+
82
+ with LMDBReader(path) as reader:
83
+ array = reader.read_as_ndarray("features", index=0)
84
+ records = reader.read_as_dict_many(["meta", "meta"])
85
+ snapshot = reader.get_snapshot()
86
+ ```
87
+
88
+ Writers take the value first and the key and index as keywords (`key=`, `index=`),
89
+ so a value and a key can never be swapped silently. Readers take the key first.
90
+ `commit()` is optional; it only bounds the size of one transaction. Batch reads copy
91
+ owned bytes during one bounded transaction and decode after it closes; single reads
92
+ also close their transactions before returning.
93
+
94
+ ## Supported values
95
+
96
+ | Writer | Values | Readers |
97
+ |---|---|---|
98
+ | `write_primitive` | `bool`, `int` (any size), `float`, `complex` | `read_as_primitive`, `read_as_any` |
99
+ | `write_str` | `str` (UTF-8) | `read_as_str`, `read_as_str_many` |
100
+ | `write_sequence` | `list`, `tuple` of supported values | `read_as_list`, `read_as_tuple`, `read_as_any` |
101
+ | `write_set` | `set`, `frozenset` | `read_as_set`, `read_as_frozenset`, `read_as_any` |
102
+ | `write_dict` | `dict`, `frozendict` | `read_as_dict`, `read_as_frozendict`, `read_as_any` |
103
+ | `write_any` | the above nested in each other (arrays too), `bytes`, `None`, built-in types, registered classes | `read_as_any` |
104
+ | `write_ndarray` | NumPy arrays of any non-object dtype, optionally chunked or compressed | `read_as_ndarray`, `read_as_tensor`, `read_array_rows` |
105
+ | `write_tensor` | PyTorch tensors (CPU copy; device and gradients are dropped) | `read_as_tensor` |
106
+ | `write` | chooses one of the writers above from the value's type | `read` |
107
+
108
+ MessagePack values (everything except strings and arrays) are limited to 64 MiB
109
+ encoded, 1,000,000 nodes and nesting depth 100 when written, so whatever is stored
110
+ can be read back. NumPy scalars are refused by the value writers with a hint
111
+ (`.item()` gives a Python number); `write_ndarray` stores them as 0-d arrays.
112
+ Masked arrays are refused because their mask would be lost. Typed reads never
113
+ convert: a stored tuple is not returned by `read_as_list`.
114
+
115
+ Arrays come back read-only, owning the bytes copied from LMDB; pass
116
+ `writable=True` for a writable array without a second copy. A `dtype=` conversion
117
+ follows NumPy's `"same_kind"` casting by default, so float-to-integer truncation
118
+ needs `casting="unsafe"`. `write_ndarray(..., compression="zlib" | "lzma" | "zstd")`
119
+ compresses values (zstd needs Python 3.14); reads decompress transparently.
120
+
121
+ ## Update a store
122
+
123
+ | Goal | How |
124
+ |---|---|
125
+ | Replace everything | `LMDBWriter(path, overwrite=True)` writes a new generation from scratch |
126
+ | Change, add or delete some entries | `LMDBWriter(path, base="current")` copies the current generation, then `write_*`, `delete`, `delete_key` |
127
+ | Add new features without rewriting | write a separate store and read both with `LMDBOverlayReader([base, features])` |
128
+ | Fold layers into one store | `merge_stores([base, features], destination)` copies entries as stored |
129
+ | Roll back | `set_current(path, "v-...")` or `amber-lmdb set-current PATH NAME` |
130
+ | Free disk space | `prune_generations(path, keep_last=N)` or `delete_generations(path, names)`; dry runs by default |
131
+
132
+ Existing readers keep their generation through all of these. Deletion requires
133
+ `same_host=True` (CLI `--apply --same-host`): it is safe only when every access goes
134
+ through this library on one host, and it refuses generations that a reader holds.
135
+ A writer lock rejects a second concurrent writer to the same root, and `CURRENT`
136
+ is never removed automatically: a root that lost `CURRENT` is refused until you
137
+ restore it with `set_current`.
138
+
139
+ ## Readers, processes and threads
140
+
141
+ A reader opens nothing until its first read, then pins that generation (see
142
+ Concepts). `refresh()` releases it so the next read follows `CURRENT`; `close()`
143
+ releases it for good, and a closed reader raises `ReaderClosedError`. A reader
144
+ dropped without closing releases its reference when it is garbage collected.
145
+
146
+ Readers can be shared by threads. Pickling a reader, as `DataLoader` workers do,
147
+ carries its pinned generation, so every copy reads the same data; call
148
+ `reader.get_snapshot()` before handing an unopened reader to workers so they all
149
+ pin one generation. Copies open their own environments. See
150
+ [PyTorch data loading](https://github.com/Nekch0/amber-lmdb/blob/main/guides/pytorch.md).
151
+
152
+ Within a process, readers of one generation share one LMDB environment; its tuning
153
+ (map size, readahead) follows the first opener. `LMDBReader(path, generation=...)`
154
+ reads a named generation. `cooperative=False` reads stores on media nothing can
155
+ change, such as read-only mounts or snapshots, without lock files; never use it
156
+ while a writer or a cleanup may touch the store.
157
+
158
+ ## Choose an API for the task
159
+
160
+ | Task | API | Boundary |
161
+ |---|---|---|
162
+ | Restore many values | `read_many`, `read_as_*_many`, `iter_read_pages` | Up to 2**20 items; byte budget 64 MiB by default, `max_bytes=None` removes it |
163
+ | Bound a single read | `read(..., max_bytes=)` and every `read_as_*` | Refused before copying when the body is larger |
164
+ | Inspect array shapes/dtypes | `read_array_headers_many` | No body copies; repeated headers share parsing within a call |
165
+ | Read row windows of large arrays | `read_array_rows`, `read_array_rows_many`; `write_ndarray(..., chunk_rows=)` | Chunked arrays copy and verify only touched chunks |
166
+ | Feed PyTorch | `read_as_tensor` (tensors and arrays), `as_tensor` | Writable tensors without the read-only warning |
167
+ | Construct indexed records | `RecordTable` | A sequence of rows `0..count-1`; optional factory and schema |
168
+ | Check mapping fields | `ValueSchema`, `RecordSchema` | Exact types or explicit shape/dtype/range/optional rules; no coercion |
169
+ | Share a derived result | `DerivedIndexCache.get_or_build`, `lookup` | Caller identity/codec; `CacheResult.hit` distinguishes a stored `None` |
170
+ | Keep a cache directory bounded | `DerivedIndexCache.prune`; CLI `cache-list`, `cache-prune` | Dry run by default; busy entries kept |
171
+ | Bound decoding of untrusted values | `LMDBReader(..., value_limits=ValueLimits(...))`, `max_decoded_nodes=` | Lowers MessagePack limits only; arrays unaffected |
172
+ | Verify large batches on idle cores | `LMDBReader(..., verify_threads=N)` | Bodies and chunks of at least 64 KiB; off by default |
173
+ | Check, count or list keys | `contains`, `count`, `index_range`, `iter_indices`, `iter_keys` | Integrity metadata only; listing costs one seek per logical key |
174
+ | Read several stores as one | `LMDBOverlayReader` | Nothing copied; an entry in more than one layer raises `OverlayConflictError` |
175
+ | Manage generations | `inspect_generations`, `set_current`, `prune_generations`, `delete_generations`, `read_history` | Deletion is explicit, cooperative and same-host |
176
+ | Diagnose a store | `inspect_store`, `verify_store`, `verify_physical` | Sampled, typed-value and physical coverage are different |
177
+ | Identify or compare content | `fingerprint_store`, `diff_stores` | Integrity envelopes only; bodies are read only to resolve algorithm changes |
178
+ | Summarize a generation at write time | `LMDBWriter(..., manifest=True)`, `reader.manifest()` | Opt-in; one reserved dictionary entry, excluded from fingerprints |
179
+ | Follow or stop long scans | `progress=`, `cancel=` on scans | Called between pages; a cancelled `verify_store` report is never `ok` |
180
+
181
+ Scans (`verify_store`, `array_statistics`, `inspect_store`, `fingerprint_store`,
182
+ `diff_stores`) also take `generation=` and `cooperative=`.
183
+
184
+ ## Errors
185
+
186
+ Every library error derives from `AmberLMDBError` and also from the built-in
187
+ exception it refines, so existing `except KeyError:` code keeps working:
188
+ `KeyNotFoundError` (`KeyError`, with `.key` and `.index`), `ReaderMismatchError`
189
+ (`ValueError`, with the stored writer tag and the readers that accept it),
190
+ `StoreNotFoundError` (`FileNotFoundError`), `ReaderClosedError`, `ReadLimitError`,
191
+ `IntegrityError`, `OverlayConflictError`, `SourceChangedError` and
192
+ `OperationCancelled`. Messages name keys in their decoded form, such as
193
+ `key ('s01', 'pitch') index 3`.
194
+
195
+ ## Command line
196
+
197
+ ```console
198
+ amber-lmdb list ./stores
199
+ amber-lmdb inspect ./example.lmdb --meta --entries 20
200
+ amber-lmdb doctor ./example.lmdb # quick sample
201
+ amber-lmdb doctor ./example.lmdb --full --progress
202
+ amber-lmdb stats ./example.lmdb
203
+ amber-lmdb generations ./example.lmdb
204
+ amber-lmdb set-current ./example.lmdb v-<32 hex>
205
+ amber-lmdb delete-generations ./example.lmdb --keep-last 2 # preview
206
+ amber-lmdb fingerprint ./example.lmdb --generation v-<32 hex>
207
+ amber-lmdb diff ./example.lmdb ./example.lmdb --before-generation v-<32 hex>
208
+ amber-lmdb manifest ./example.lmdb
209
+ python -m amber_lmdb --version
210
+ ```
211
+
212
+ Every command prints one JSON document. Exit codes: 0 success; 1 the command ran
213
+ and found something (problems, differences, refused deletions, busy cache entries);
214
+ 2 invalid usage; 3 the command could not complete (missing or unreadable store,
215
+ invalid layout, I/O error), with `{"error": {"type", "message"}}` on stdout and the
216
+ message on stderr; 130 interrupted. `--read-only-media` and `--generation` select
217
+ the reading mode and generation of the reading commands.
218
+
219
+ `doctor` without `--full` checks the layout and a bounded sample of keys, tags and
220
+ headers and reports `sample_ok`; it is **not** a full check. `doctor --full` visits
221
+ every entry, verifies checksums and decodes values within the byte limits, and
222
+ reports `ok` and `complete`. `--max-issues` caps listed issue details (with their
223
+ decoded keys), never the number of checked entries. Pass the previous `next_after`
224
+ token to `inspect --after` for another page; each call opens a fresh transaction.
225
+ Metadata with non-string dictionary keys is shown as a `mapping` list of pairs;
226
+ arrays are summarized by type, shape and dtype. The CLI never runs custom decoders.
227
+
228
+ The same inspection is available in Python:
229
+
230
+ ```python
231
+ from amber_lmdb import inspect_store
232
+
233
+ info = inspect_store("example.lmdb", with_meta=True, entry_limit=20)
234
+ # Select another unindexed dictionary key with metadata_key="description".
235
+ print(info.snapshot, info.data_entries, info.samples)
236
+ ```
237
+
238
+ `data_entries` excludes the integrity database reference that `entries` (LMDB's
239
+ own count) includes. `used_bytes` counts allocated pages of the inspected
240
+ generation; it is not the total disk footprint.
241
+
242
+ ## Backend engines and physical checks
243
+
244
+ py-lmdb 3 is required. New generations default to LMDB 1.0; pass
245
+ `LMDBWriter(..., lib_version=0)` for 0.9. Readers detect the existing file's engine.
246
+ `get_backend_info()` / `amber-lmdb backend` report available engines, and
247
+ `reader.get_lib_version()` reports the one actually opened.
248
+
249
+ `verify_physical(path)` / `doctor PATH --physical` use the upstream offline Python
250
+ page verifier without opening the native engine. Use a private immutable copy;
251
+ lock files are not verified. `LMDBWriter(..., verify_on_publish=True)` optionally
252
+ checks its exclusively owned staging data before publication. See
253
+ [py-lmdb 3](https://github.com/Nekch0/amber-lmdb/blob/main/guides/lmdb-3.md) for engine formats, trust boundaries and cost.
254
+
255
+ ## Define records and named fields
256
+
257
+ Use `ValueSchema` / `infer_schema` / `validate_schema` for exact field types.
258
+ `RecordSchema` / `FieldSpec` add nullable/optional fields, exact-type unions,
259
+ shape/dtype/range/length constraints and trusted cross-field validation; invalid
260
+ specifications, such as a NumPy scalar type or an unknown dtype name, fail when the
261
+ schema is built. `ValueView` reads declared fields under a namespace; `RecordTable`
262
+ is a read-only sequence of indexed dictionaries (`len`, `table[i]`, slices,
263
+ iteration in bounded pages) with an optional factory applied after transactions
264
+ close. Neither API introduces padding, filtering, dataset session conventions or RNG.
265
+
266
+ `DerivedIndexCache` shares caller-built results under an explicit identity: a
267
+ str-keyed mapping of JSON values or a dataclass such as `LMDBSnapshot`; tuples and
268
+ non-string keys are refused because JSON would change them. Builders share a fixed
269
+ set of lock stripes whose count is recorded in the cache directory, and `prune`
270
+ removes invalid, unwanted, expired or excess files safely.
271
+
272
+ See [bounded values and schemas](https://github.com/Nekch0/amber-lmdb/blob/main/guides/bounded-values-and-schemas.md) for byte-aware
273
+ streaming, field constraints and checksum coverage, [records and
274
+ indexes](https://github.com/Nekch0/amber-lmdb/blob/main/guides/records-and-indexes.md) for runnable invoice examples, and
275
+ [connection lifetimes](https://github.com/Nekch0/amber-lmdb/blob/main/guides/connection-lifecycle.md) for raw `open_environment`
276
+ handles, generation retention and deletion.
277
+
278
+ ## Storage format compatibility
279
+
280
+ This library has no dependency on a dataset framework or on its consumer packages.
281
+ Applications supply their own schemas, keys, constructors and cache identities.
282
+
283
+ The store marker is `amber-lmdb-v1\n`, with key/tag/length-bound integrity
284
+ envelopes. New entries use BLAKE2b-256 (envelope version 2) by default;
285
+ `LMDBWriter(..., checksum="sha256")` writes SHA-256 (version 1). Readers verify
286
+ both, per entry. `LMDBReader(path, verify_checksums=False)` skips only the body
287
+ digest for one reader, for stores whose operator takes responsibility that they are
288
+ undamaged and unaltered; tags, lengths and decoders are still checked, and
289
+ `verify_store`/`doctor` still verify. The historical `mmfabric-lmdb-v2` marker is
290
+ rejected explicitly; there is no automatic migration. This library reads its own
291
+ typed format, not arbitrary LMDB layouts.
292
+
293
+ Compressed arrays use array format version 3; readers older than this release
294
+ refuse them with an "unsupported array version" error instead of misreading them.
295
+ Uncompressed arrays keep version 2. `HISTORY` is a JSON-lines file appended after
296
+ each `CURRENT` switch; roots written before it existed are read normally and gain
297
+ one at their next publication.
298
+
299
+ Tuple keys whose first component is `"amber-lmdb"` are reserved: the optional
300
+ manifest and the chunks of arrays written with `chunk_rows` live there. Writers refuse
301
+ them for user data, and listings, fingerprints, diffs and overlay checks skip them.
302
+ A chunked array's header entry lists its chunks' digests, so its own envelope covers
303
+ the whole array and each chunk must carry exactly the listed digest.
304
+
305
+ Custom codecs require explicit registration (`register_bytes_codec`, undone with
306
+ `unregister_bytes_codec`) and `allow_custom=True` when reading. The CLI never enables
307
+ custom decoding, and no module is imported from stored type names.
308
+
309
+ ## Scan arrays or validate all stored values
310
+
311
+ `array_statistics(path)` scans headers and groups arrays by writer tag, dtype and
312
+ rank. Axis minima are bounds, not necessarily real sample shapes; smallest and
313
+ largest sample shapes are reported separately, with their keys.
314
+
315
+ `verify_store(path)` visits all data and tag entries, decodes values after each
316
+ short transaction, and detects malformed keys/tags/values and orphan tags. Values
317
+ over the copy limit (`max_value_bytes`, 64 MiB by default), unavailable optional
318
+ codecs and blocked custom decoders are reported as **unverified**, not certified.
319
+ `report.ok` requires complete coverage and no detected errors. Checksums detect
320
+ corruption, not authenticity. In-place updates abort a scan rather than mixing
321
+ transactions. See [scanning stores](https://github.com/Nekch0/amber-lmdb/blob/main/guides/scanning-stores.md).
322
+
323
+ ## Operational limits
324
+
325
+ Normal LMDB locks remain enabled. No network filesystem safety guarantee is made;
326
+ the filesystem must satisfy LMDB's locking and memory-mapping requirements.
327
+ Generation deletion assumes that every access cooperates through this library on
328
+ one host; never use it for NFS or cross-host access. Metadata can contain
329
+ transcripts or other restricted information; use `--meta` only when appropriate.
330
+ Record definitions, index builders, selection and application semantics belong in
331
+ the consumer.
332
+
333
+ ## Development
334
+
335
+ Clone the repository and run `uv sync`. Type checking requires the optional
336
+ dependencies, so use `uv sync --all-extras` before those checks. Keep a separate
337
+ base test run without PyTorch.
338
+
339
+ ```console
340
+ uv run pytest
341
+ uv run ruff check .
342
+ uv run ruff format --check .
343
+ uv run pyright
344
+ uv build
345
+ ```
346
+
347
+ Tests with PyTorch skip when the optional dependency is absent. CI tests Python
348
+ 3.12, 3.13 and 3.14 without PyTorch, and tests the tensor extra separately.
349
+
350
+ Use the [synthetic benchmark](https://github.com/Nekch0/amber-lmdb/blob/main/benchmarks/README.md) to compare batch sizes and
351
+ engines without touching existing stores.
352
+
353
+ See [CONTRIBUTING.md](https://github.com/Nekch0/amber-lmdb/blob/main/CONTRIBUTING.md) for compatibility and release checks.