Skip to content

Commit a99da80

Browse files
SpitzHTill
authored andcommitted
Rename node API and remove TreeDB namespace
Adopt BaseNode and DataNode as the public model and move the implementation fully into the lotdb package so the storage and measurement APIs use one consistent namespace.
1 parent b311521 commit a99da80

21 files changed

Lines changed: 1055 additions & 126 deletions

README.md

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@ LOTDB stands for Light Object Tree DB.
66

77
## What it is
88

9-
LOTDB stores data in `StorageTree` nodes:
9+
LOTDB stores data in `BaseNode` nodes:
1010
- each node can contain child nodes
1111
- each node can store arbitrary attributes
1212
- the full tree can be persisted with ZODB
1313

1414
This is especially useful for dataset pipelines where folder structure, file references, and metadata all belong together.
1515

16+
LOTDB now supports both:
17+
- file references for external assets
18+
- native ZODB blob storage for large sensor measurements and array payloads
19+
- backend-linked measurements via blob or Zarr
20+
1621
## Installation
1722

1823
Local editable install:
@@ -26,9 +31,9 @@ With optional IO helpers:
2631
## Basic usage
2732

2833
```python
29-
from lotdb import StorageTree
34+
from lotdb import BaseNode
3035

31-
tree = StorageTree(key="dataset")
36+
tree = BaseNode(key="dataset")
3237
node = tree.get_node_path(["speaker_01", "session_a", "clip_001"])
3338
node.set_attribute("label", "hello")
3439

@@ -49,25 +54,87 @@ db.close_connection()
4954
db.close()
5055
```
5156

52-
## Backward compatibility
57+
## Sensor/blob storage usage
58+
59+
```python
60+
import numpy as np
61+
from lotdb import BlobReader, BlobWriter, LOTDB
62+
63+
db = LOTDB(path="./data", name="lotdb.fs", new=True)
64+
tree = db.open_connection()
65+
66+
node = tree.get_node_path(["sensor_01", "capture_0001"])
67+
BlobWriter.write_array(
68+
node,
69+
np.random.randn(1000, 6).astype("float32"),
70+
blob_attribute="measurement",
71+
metadata={"samplerate_hz": 1000, "sensor_type": "imu"},
72+
)
73+
74+
db.commit()
75+
measurement = BlobReader.read_array(node, "measurement")
76+
db.close_connection()
77+
db.close()
78+
```
79+
80+
## Data backend usage
81+
82+
```python
83+
import numpy as np
84+
from lotdb import DataReader, DataWriter, LOTDB
85+
86+
db = LOTDB(path="./data", name="lotdb.fs", new=True)
87+
tree = db.open_connection()
88+
node = tree.get_node_path(["sensor_01", "capture_0002"])
89+
90+
DataWriter.write_array(
91+
node,
92+
np.random.randn(2000, 6).astype("float32"),
93+
samplerate_hz=1000,
94+
backend="zarr", # or "blob"
95+
data_attribute="imu",
96+
database=db,
97+
metadata={"sensor_type": "imu", "unit": "m/s^2"},
98+
)
99+
100+
for block in DataReader.iter_blocks(
101+
node,
102+
"imu",
103+
block_size=0.5,
104+
block_unit="seconds",
105+
):
106+
process(block)
107+
108+
second_one_to_two = DataReader.read_seconds(node, "imu", 1.0, 2.0)
109+
110+
db.commit()
111+
db.close_connection()
112+
db.close()
113+
```
114+
115+
## Data node API
53116

54-
The old short method names still work:
55-
- `ga()`
56-
- `gn()`
57-
- `gns()`
58-
- `gna()`
117+
```python
118+
data_node = tree.get_data_node(
119+
["sensor_01", "capture_0003"],
120+
samplerate_hz=1000,
121+
backend="zarr",
122+
data_attribute="imu",
123+
)
59124

60-
Readable wrappers were added so the API is easier to maintain and easier for weaker coding models to follow.
125+
data_node.write_data(data, database=db)
126+
127+
for block in data_node.iter_data_blocks(0.25, block_unit="seconds"):
128+
process(block)
129+
130+
window = data_node.read_seconds(1.0, 2.0)
131+
```
61132

62-
Import compatibility is also preserved for now:
63-
- new import: `import lotdb`
64-
- legacy imports still work: `import TreeDB`, `import StorageTree`
65-
- new database class: `LOTDB`
66-
- legacy alias still works: `StorageTreeDatabase`
133+
This is the recommended high-level API for large binary/data payloads.
67134

68135
## Development
69136

70-
- source packages live in `src/lotdb` and `src/TreeDB`
137+
- source package lives in `src/lotdb`
71138
- tests live in `tests/`
72139
- API notes live in `docs/API.md`
73140

docs/API.md

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Core classes
44

5-
### `StorageTree`
5+
### `BaseNode`
66
- Hierarchical persistent node.
77
- Stores child nodes in `node_storage`.
88
- Stores metadata/attributes in `attribute_storage`.
@@ -28,14 +28,11 @@ Important behaviors:
2828
- `delete_node(..., only_node=True)` removes a node but promotes its children.
2929

3030
### `LOTDB`
31-
- ZODB-backed storage for a root `StorageTree`.
31+
- ZODB-backed storage for a root `BaseNode`.
3232
- `open_connection()` opens a transaction-scoped connection.
3333
- `commit()` must be called explicitly for writes.
3434
- `connection()` provides a context manager that closes the connection automatically.
3535

36-
Compatibility alias:
37-
- `StorageTreeDatabase`
38-
3936
### `PathFileObj`
4037
- Persistent file path object storing root and filename separately.
4138
- Useful for keeping file references inside the tree.
@@ -48,6 +45,53 @@ Attach a `PathFileObj` to a node attribute.
4845
### `FileReader` / `FileWriter`
4946
Helpers for wav/txt/npy oriented workflows.
5047

48+
## Blob helpers
49+
50+
### `BlobObject`
51+
- ZODB-managed binary payload stored inside the database/blob directory.
52+
- Can store raw bytes or NumPy arrays.
53+
- Supports metadata like sensor type, sample rate, units, and shape.
54+
55+
### `BlobReader` / `BlobWriter`
56+
- `BlobWriter.write_bytes()` stores arbitrary binary payloads on a node.
57+
- `BlobWriter.write_array()` stores NumPy arrays in `.npy` format inside a blob.
58+
- `BlobReader.read_bytes()` and `BlobReader.read_array()` restore the payload.
59+
60+
Typical use case:
61+
- sensor measurement sequences
62+
- multichannel waveform data
63+
- binary captures that should be managed by LOTDB instead of external file paths
64+
65+
## Data helpers
66+
67+
### `DataWriter`
68+
- Stores sensor arrays using a selected backend: `blob` or `zarr`.
69+
- Attaches a data reference object to a node.
70+
- Records metadata like sample rate, dtype, shape, dataset path, and custom sensor metadata.
71+
72+
### `DataReader`
73+
- Reads data ranges in samples or seconds.
74+
- Provides `iter_blocks(...)` for streaming/block-based consumption.
75+
- Converts second-based ranges into sample indices using `samplerate_hz`.
76+
77+
### `DataObject`
78+
- Persistent data reference stored on a node.
79+
- For `blob`, the payload lives in a ZODB blob.
80+
- For `zarr`, the node stores a link to a Zarr dataset path on disk.
81+
82+
### `DataNode`
83+
- Specialized data-oriented node that inherits from `BaseNode`.
84+
- Stores default data configuration like backend, sample rate, and data attribute name.
85+
- Convenience methods:
86+
- `write_data(...)`
87+
- `read_data(...)`
88+
- `read_seconds(...)`
89+
- `iter_data_blocks(...)`
90+
91+
### `BaseNode.get_data_node()`
92+
- Creates or upgrades the final node in a path to a `DataNode`.
93+
- This is the easiest way to create sensor capture nodes with automatic backend setup.
94+
5195
## Bulk loading helpers
5296

5397
### `LOTDB.load_files_folder()`

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ io = [
2424
"pandas",
2525
"scipy",
2626
]
27+
measurements = [
28+
"zarr",
29+
]
2730
dev = [
2831
"pytest",
32+
"zarr",
2933
]
3034

3135
[tool.setuptools]
3236
package-dir = {"" = "src"}
33-
py-modules = ["StorageTree"]
3437

3538
[tool.setuptools.packages.find]
3639
where = ["src"]

src/StorageTree.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/TreeDB/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/lotdb/StorageTree.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/lotdb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from TreeDB.StorageTree import *
1+
from .api import *
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from .database import LOTDB, StorageTreeConnection, StorageTreeDatabase, load_small_files_directory, load_small_files_folder
1+
from .blobs import BlobObject, BlobReader, BlobWriter, add_blob
2+
from .database import LOTDB, NodeConnection, load_small_files_directory, load_small_files_folder
23
from .files import FileReader, FileWriter, add_file, rename_file
4+
from .measurements import DataObject, DataReader, DataWriter
35
from .paths import PathFileObj
4-
from .tree import StorageTree, StorageTreeUnconnected
6+
from .tree import BaseNode, BaseNodeUnconnected, DataNode
57
from .utils import (
68
apply_args_and_kwargs,
79
filter_node_list,
@@ -15,14 +17,21 @@
1517
)
1618

1719
__all__ = [
18-
"StorageTree",
19-
"StorageTreeUnconnected",
20+
"BaseNode",
21+
"BaseNodeUnconnected",
22+
"DataNode",
23+
"BlobObject",
24+
"BlobReader",
25+
"BlobWriter",
26+
"DataObject",
27+
"DataReader",
28+
"DataWriter",
2029
"LOTDB",
21-
"StorageTreeConnection",
22-
"StorageTreeDatabase",
30+
"NodeConnection",
2331
"PathFileObj",
2432
"FileReader",
2533
"FileWriter",
34+
"add_blob",
2635
"add_file",
2736
"rename_file",
2837
"load_small_files_directory",

src/lotdb/blobs.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
import shutil
4+
from typing import Any, Mapping, Optional
5+
6+
import persistent
7+
from BTrees.OOBTree import OOBTree
8+
from ZODB.blob import Blob
9+
from numpy import ndarray
10+
11+
12+
class BlobObject(persistent.Persistent):
13+
def __init__(
14+
self,
15+
content_type: str = "application/octet-stream",
16+
metadata: Optional[Mapping[str, Any]] = None,
17+
) -> None:
18+
self.blob = Blob()
19+
self.metadata = OOBTree()
20+
self.metadata["content_type"] = content_type
21+
22+
for key, value in (metadata or {}).items():
23+
self.metadata[str(key)] = value
24+
25+
@property
26+
def content_type(self) -> str:
27+
return str(self.metadata.get("content_type", "application/octet-stream"))
28+
29+
def set_metadata(self, key: str, value: Any) -> Any:
30+
self.metadata[str(key)] = value
31+
return value
32+
33+
def get_metadata(self, key: str, default: Any = None) -> Any:
34+
return self.metadata.get(str(key), default)
35+
36+
def metadata_dict(self) -> dict[str, Any]:
37+
return dict(self.metadata.items())
38+
39+
def write_bytes(self, data: bytes) -> None:
40+
with self.blob.open("w") as file_handle:
41+
file_handle.write(data)
42+
43+
def read_bytes(self) -> bytes:
44+
with self.blob.open("r") as file_handle:
45+
return file_handle.read()
46+
47+
def write_array(self, array: ndarray, allow_pickle: bool = False) -> None:
48+
from numpy import save
49+
50+
with self.blob.open("w") as file_handle:
51+
save(file_handle, array, allow_pickle=allow_pickle)
52+
53+
self.metadata["format"] = "npy"
54+
self.metadata["dtype"] = str(array.dtype)
55+
self.metadata["shape"] = tuple(array.shape)
56+
57+
def read_array(self, allow_pickle: bool = False):
58+
from numpy import load
59+
60+
with self.blob.open("r") as file_handle:
61+
return load(file_handle, allow_pickle=allow_pickle)
62+
63+
def copy_for_tree(self) -> "BlobObject":
64+
clone = BlobObject(content_type=self.content_type, metadata=self.metadata_dict())
65+
with self.blob.open("r") as source_handle, clone.blob.open("w") as target_handle:
66+
shutil.copyfileobj(source_handle, target_handle)
67+
return clone
68+
69+
70+
def add_blob(
71+
node,
72+
blob_attribute: str = "_blob",
73+
data: bytes = b"",
74+
content_type: str = "application/octet-stream",
75+
metadata: Optional[Mapping[str, Any]] = None,
76+
) -> BlobObject:
77+
blob_object = BlobObject(content_type=content_type, metadata=metadata)
78+
blob_object.write_bytes(data)
79+
node.set_attribute(blob_attribute, blob_object)
80+
return blob_object
81+
82+
83+
class BlobWriter:
84+
@staticmethod
85+
def write_bytes(
86+
node,
87+
data: bytes,
88+
blob_attribute: str = "_blob",
89+
content_type: str = "application/octet-stream",
90+
metadata: Optional[Mapping[str, Any]] = None,
91+
) -> BlobObject:
92+
blob_object = BlobObject(content_type=content_type, metadata=metadata)
93+
blob_object.write_bytes(data)
94+
node.set_attribute(blob_attribute, blob_object)
95+
return blob_object
96+
97+
@staticmethod
98+
def write_array(
99+
node,
100+
array: ndarray,
101+
blob_attribute: str = "_blob_array",
102+
metadata: Optional[Mapping[str, Any]] = None,
103+
content_type: str = "application/x-npy",
104+
allow_pickle: bool = False,
105+
) -> BlobObject:
106+
blob_object = BlobObject(content_type=content_type, metadata=metadata)
107+
blob_object.write_array(array, allow_pickle=allow_pickle)
108+
node.set_attribute(blob_attribute, blob_object)
109+
return blob_object
110+
111+
112+
class BlobReader:
113+
@staticmethod
114+
def read_bytes(node, blob_attribute: str = "_blob") -> bytes:
115+
return node.get_attribute(blob_attribute).read_bytes()
116+
117+
@staticmethod
118+
def read_array(node, blob_attribute: str = "_blob_array", allow_pickle: bool = False):
119+
return node.get_attribute(blob_attribute).read_array(allow_pickle=allow_pickle)

0 commit comments

Comments
 (0)