vcti-datanode 1.0.1__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.
- vcti/datanode/__init__.py +22 -0
- vcti/datanode/lazy_node.py +139 -0
- vcti/datanode/node.py +142 -0
- vcti/datanode/py.typed +0 -0
- vcti_datanode-1.0.1.dist-info/METADATA +83 -0
- vcti_datanode-1.0.1.dist-info/RECORD +9 -0
- vcti_datanode-1.0.1.dist-info/WHEEL +5 -0
- vcti_datanode-1.0.1.dist-info/licenses/LICENSE +8 -0
- vcti_datanode-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.datanode — lightweight data-plus-attributes node containers.
|
|
4
|
+
|
|
5
|
+
``DataNode`` pairs data with a metadata/attributes dict; ``LazyDataNode``
|
|
6
|
+
adds on-demand ``load()`` / ``release()`` over a loader callback for
|
|
7
|
+
out-of-core data. Both are tree-agnostic value types usable as node
|
|
8
|
+
payloads or anywhere a data + metadata unit is useful.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
from .lazy_node import LazyDataNode
|
|
14
|
+
from .node import DataNode
|
|
15
|
+
|
|
16
|
+
__version__ = version("vcti-datanode")
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"__version__",
|
|
20
|
+
"DataNode",
|
|
21
|
+
"LazyDataNode",
|
|
22
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""LazyDataNode — DataNode with on-demand loading and release of data.
|
|
4
|
+
|
|
5
|
+
Designed for large datasets (e.g. CAE models) where keeping all data in
|
|
6
|
+
memory at once is impractical. Attributes stay resident; data is loaded via
|
|
7
|
+
a caller-supplied callback and can be released to free memory.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .node import DataNode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LazyDataNode(DataNode):
|
|
17
|
+
"""DataNode with lazy loading and release of data.
|
|
18
|
+
|
|
19
|
+
Extends :class:`DataNode` with a loader callback for on-demand data
|
|
20
|
+
loading. Attributes remain in memory at all times (for filtering and
|
|
21
|
+
bookkeeping); data starts unloaded and is fetched via ``load()`` when
|
|
22
|
+
needed, then can be freed via ``release()`` and reloaded later.
|
|
23
|
+
|
|
24
|
+
The loader is a closure — it captures whatever file handle, offset,
|
|
25
|
+
dataset path, or other context it needs. ``LazyDataNode`` does not know
|
|
26
|
+
or care how the data is stored externally.
|
|
27
|
+
|
|
28
|
+
Equality is the inherited :class:`DataNode` semantics — data plus
|
|
29
|
+
attributes only. The loader and the load-state are **not** part of
|
|
30
|
+
identity, so two unloaded nodes with equal attributes compare equal even
|
|
31
|
+
if their loaders would produce different data.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
data: The data (None when unloaded, populated after ``load()``).
|
|
35
|
+
attributes: Dictionary of metadata attributes (always resident).
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> import numpy as np
|
|
39
|
+
>>> def make_loader():
|
|
40
|
+
... return lambda: np.array([1.0, 2.0, 3.0])
|
|
41
|
+
>>> node = LazyDataNode(
|
|
42
|
+
... loader=make_loader(),
|
|
43
|
+
... attributes={'units': 'mm', 'name': 'displacement'},
|
|
44
|
+
... )
|
|
45
|
+
>>> node.is_loaded
|
|
46
|
+
False
|
|
47
|
+
>>> node.load()
|
|
48
|
+
array([1., 2., 3.])
|
|
49
|
+
>>> node.is_loaded
|
|
50
|
+
True
|
|
51
|
+
>>> node.release()
|
|
52
|
+
>>> node.is_loaded
|
|
53
|
+
False
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__slots__ = ("_loader",)
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
loader: Callable[[], Any],
|
|
61
|
+
attributes: dict[str, Any] | None = None,
|
|
62
|
+
*,
|
|
63
|
+
data: Any = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Initialize a LazyDataNode.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
loader: Callable that returns the data when invoked.
|
|
69
|
+
Must be idempotent — multiple calls should return
|
|
70
|
+
equivalent data.
|
|
71
|
+
attributes: Optional metadata dictionary. Defaults to empty dict.
|
|
72
|
+
data: Optional pre-loaded data. When provided, the node
|
|
73
|
+
starts in the loaded state.
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(data=data, attributes=attributes)
|
|
76
|
+
self._loader = loader
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_loaded(self) -> bool:
|
|
80
|
+
"""Return True if data is currently in memory."""
|
|
81
|
+
return self.data is not None
|
|
82
|
+
|
|
83
|
+
def load(self) -> Any:
|
|
84
|
+
"""Load data via the loader callback.
|
|
85
|
+
|
|
86
|
+
If data is already loaded, returns it without calling the loader
|
|
87
|
+
again. To force a reload, call ``release()`` first.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The data.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
RuntimeError: If the loader returns None.
|
|
94
|
+
Exception: Any exception raised by the loader propagates
|
|
95
|
+
unchanged, preserving its type (e.g. ``FileNotFoundError``)
|
|
96
|
+
so callers can handle distinct failures distinctly.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> import numpy as np
|
|
100
|
+
>>> node = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
|
|
101
|
+
>>> node.is_loaded
|
|
102
|
+
False
|
|
103
|
+
>>> data = node.load()
|
|
104
|
+
>>> node.is_loaded
|
|
105
|
+
True
|
|
106
|
+
"""
|
|
107
|
+
if self.data is None:
|
|
108
|
+
result = self._loader()
|
|
109
|
+
if result is None:
|
|
110
|
+
raise RuntimeError("Loader returned None")
|
|
111
|
+
self.data = result
|
|
112
|
+
return self.data
|
|
113
|
+
|
|
114
|
+
def release(self) -> None:
|
|
115
|
+
"""Free data from memory.
|
|
116
|
+
|
|
117
|
+
The node keeps its attributes intact; data can be reloaded later via
|
|
118
|
+
``load()``.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> import numpy as np
|
|
122
|
+
>>> node = LazyDataNode(loader=lambda: np.array([1, 2, 3]))
|
|
123
|
+
>>> node.load() # doctest: +ELLIPSIS
|
|
124
|
+
array(...)
|
|
125
|
+
>>> node.release()
|
|
126
|
+
>>> node.is_loaded
|
|
127
|
+
False
|
|
128
|
+
"""
|
|
129
|
+
self.data = None
|
|
130
|
+
|
|
131
|
+
def _data_status(self) -> str:
|
|
132
|
+
if self.data is None:
|
|
133
|
+
return "unloaded"
|
|
134
|
+
return super()._data_status()
|
|
135
|
+
|
|
136
|
+
def _data_detail_lines(self) -> list[str]:
|
|
137
|
+
if self.data is None:
|
|
138
|
+
return [" Data: unloaded"]
|
|
139
|
+
return super()._data_detail_lines()
|
vcti/datanode/node.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""DataNode — a container pairing data with a metadata/attributes dict.
|
|
4
|
+
|
|
5
|
+
A small, dependency-light value type for "a unit of data plus its
|
|
6
|
+
attributes." It is commonly used as a node payload in a tree, but is not
|
|
7
|
+
tied to any tree or container — use it anywhere a data + metadata pair is
|
|
8
|
+
useful.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataNode:
|
|
17
|
+
"""Container for data and a metadata/attributes dictionary.
|
|
18
|
+
|
|
19
|
+
Each instance can carry:
|
|
20
|
+
|
|
21
|
+
- **Data** — an ``np.ndarray``, a masked array, or any array-like /
|
|
22
|
+
arbitrary object (the field is untyped so it integrates by duck typing
|
|
23
|
+
with other array containers without a hard dependency on them).
|
|
24
|
+
- **Attributes** — a metadata dictionary.
|
|
25
|
+
- Both, or neither.
|
|
26
|
+
|
|
27
|
+
It is intentionally minimal: just ``data`` + ``attributes``, with value
|
|
28
|
+
equality and readable ``repr``/``str``.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
data: The data (``np.ndarray``, array-like, arbitrary object, or
|
|
32
|
+
``None``).
|
|
33
|
+
attributes: Dictionary of metadata attributes.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> import numpy as np
|
|
37
|
+
>>> node = DataNode(
|
|
38
|
+
... data=np.array([1.0, 2.0, 3.0]),
|
|
39
|
+
... attributes={'units': 'mm', 'name': 'displacement'},
|
|
40
|
+
... )
|
|
41
|
+
>>> group = DataNode(attributes={'type': 'results'})
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__slots__ = ("attributes", "data")
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other: object) -> bool:
|
|
47
|
+
if not isinstance(other, DataNode):
|
|
48
|
+
return NotImplemented
|
|
49
|
+
|
|
50
|
+
# Compare the cheap attributes dict first so an early mismatch
|
|
51
|
+
# short-circuits before any (potentially large) data comparison.
|
|
52
|
+
if self.attributes != other.attributes:
|
|
53
|
+
return False
|
|
54
|
+
return self._data_equals(other.data)
|
|
55
|
+
|
|
56
|
+
def _data_equals(self, other_data: Any) -> bool:
|
|
57
|
+
"""Return whether this node's data equals ``other_data``.
|
|
58
|
+
|
|
59
|
+
For ordinary data this does not raise: any pairing of None, ndarrays,
|
|
60
|
+
other array-likes, or plain objects resolves to a bool. (A custom
|
|
61
|
+
object whose own ``__eq__`` raises is the one exception — that
|
|
62
|
+
propagates.) ndarrays compare by shape, dtype, and ``np.array_equal``
|
|
63
|
+
(note: NaN != NaN, so arrays containing NaN are unequal; for masked
|
|
64
|
+
arrays the mask is not consulted — the underlying data is compared).
|
|
65
|
+
A mismatch in kind — one side an ndarray, the other not — is treated
|
|
66
|
+
as unequal rather than coerced.
|
|
67
|
+
"""
|
|
68
|
+
a, b = self.data, other_data
|
|
69
|
+
if a is None or b is None:
|
|
70
|
+
return a is None and b is None
|
|
71
|
+
|
|
72
|
+
a_is_array = isinstance(a, np.ndarray)
|
|
73
|
+
b_is_array = isinstance(b, np.ndarray)
|
|
74
|
+
if a_is_array and b_is_array:
|
|
75
|
+
return bool(a.shape == b.shape and a.dtype == b.dtype and np.array_equal(a, b))
|
|
76
|
+
if a_is_array or b_is_array:
|
|
77
|
+
# One ndarray, one not: different kinds, treat as unequal rather
|
|
78
|
+
# than letting an array-valued ``==`` blow up the bool reduction.
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
result = a == b
|
|
82
|
+
if isinstance(result, bool):
|
|
83
|
+
return result
|
|
84
|
+
# Non-ndarray array-likes whose ``==`` yields a non-scalar: reduce
|
|
85
|
+
# element-wise so we still return a plain, non-raising bool.
|
|
86
|
+
return bool(np.all(result))
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
data: Any = None,
|
|
91
|
+
attributes: dict[str, Any] | None = None,
|
|
92
|
+
):
|
|
93
|
+
"""Initialize a DataNode.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
data: Optional data (any array-like object or None).
|
|
97
|
+
attributes: Optional metadata dictionary. Defaults to empty dict.
|
|
98
|
+
"""
|
|
99
|
+
self.data = data
|
|
100
|
+
self.attributes = attributes if attributes is not None else {}
|
|
101
|
+
|
|
102
|
+
def _data_status(self) -> str:
|
|
103
|
+
"""Return a short description of the data state for repr/str."""
|
|
104
|
+
if self.data is None:
|
|
105
|
+
return "None"
|
|
106
|
+
if isinstance(self.data, np.ndarray):
|
|
107
|
+
return f"shape={self.data.shape}, dtype={self.data.dtype}"
|
|
108
|
+
return f"type={type(self.data).__name__}"
|
|
109
|
+
|
|
110
|
+
def _data_detail_lines(self) -> list[str]:
|
|
111
|
+
"""Return detail lines about the data for __str__."""
|
|
112
|
+
if self.data is None:
|
|
113
|
+
return [" Data: None"]
|
|
114
|
+
if isinstance(self.data, np.ndarray):
|
|
115
|
+
return [
|
|
116
|
+
f" Data shape: {self.data.shape}",
|
|
117
|
+
f" Data dtype: {self.data.dtype}",
|
|
118
|
+
]
|
|
119
|
+
return [f" Data type: {type(self.data).__name__}"]
|
|
120
|
+
|
|
121
|
+
def _attributes_detail_lines(self) -> list[str]:
|
|
122
|
+
"""Return detail lines about attributes for __str__."""
|
|
123
|
+
if not self.attributes:
|
|
124
|
+
return [" Attributes: None"]
|
|
125
|
+
lines = [f" Attributes ({len(self.attributes)}):"]
|
|
126
|
+
for key, value in list(self.attributes.items())[:5]:
|
|
127
|
+
value_str = str(value)
|
|
128
|
+
if len(value_str) > 50:
|
|
129
|
+
value_str = value_str[:47] + "..."
|
|
130
|
+
lines.append(f" {key}: {value_str}")
|
|
131
|
+
if len(self.attributes) > 5:
|
|
132
|
+
lines.append(f" ... and {len(self.attributes) - 5} more")
|
|
133
|
+
return lines
|
|
134
|
+
|
|
135
|
+
def __repr__(self) -> str:
|
|
136
|
+
return f"{type(self).__name__}(data={self._data_status()}, attrs={len(self.attributes)})"
|
|
137
|
+
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
parts = [f"{type(self).__name__}:"]
|
|
140
|
+
parts.extend(self._data_detail_lines())
|
|
141
|
+
parts.extend(self._attributes_detail_lines())
|
|
142
|
+
return "\n".join(parts)
|
vcti/datanode/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-datanode
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Lightweight data-plus-attributes node containers, with an optional lazily-loaded variant for out-of-core data.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
Requires-Python: <3.15,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == "test"
|
|
12
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
13
|
+
Provides-Extra: lint
|
|
14
|
+
Requires-Dist: ruff; extra == "lint"
|
|
15
|
+
Provides-Extra: typecheck
|
|
16
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Data Node
|
|
20
|
+
|
|
21
|
+
Lightweight data-plus-attributes node containers, with an optional
|
|
22
|
+
lazily-loaded variant for out-of-core data.
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
`vcti-datanode` provides two small, tree-agnostic value types:
|
|
27
|
+
|
|
28
|
+
- **`DataNode`** — pairs `data` (an `np.ndarray`, any array-like, or an
|
|
29
|
+
arbitrary object) with an `attributes` metadata dict. Value equality and
|
|
30
|
+
readable `repr`/`str`. That's it.
|
|
31
|
+
- **`LazyDataNode`** — a `DataNode` whose data is fetched on demand through a
|
|
32
|
+
loader callback (`load()`) and can be freed (`release()`) and reloaded
|
|
33
|
+
later. Attributes stay resident; only the data comes and goes. Built for
|
|
34
|
+
large datasets (e.g. CAE models) where holding everything in memory at once
|
|
35
|
+
is impractical.
|
|
36
|
+
|
|
37
|
+
They are commonly used as **node payloads** in a tree, but depend on nothing
|
|
38
|
+
but numpy and can be used anywhere a "data + metadata" unit is useful.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install vcti-datanode
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### In `requirements.txt`
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
vcti-datanode>=1.0.1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### In `pyproject.toml` dependencies
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
dependencies = [
|
|
56
|
+
"vcti-datanode>=1.0.1",
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import numpy as np
|
|
66
|
+
from vcti.datanode import DataNode, LazyDataNode
|
|
67
|
+
|
|
68
|
+
# Eager: data held directly.
|
|
69
|
+
node = DataNode(data=np.array([1.0, 2.0, 3.0]), attributes={"units": "mm"})
|
|
70
|
+
|
|
71
|
+
# Lazy: data fetched on demand, freed when not needed.
|
|
72
|
+
lazy = LazyDataNode(loader=lambda: np.load("displacement.npy"),
|
|
73
|
+
attributes={"name": "displacement"})
|
|
74
|
+
lazy.is_loaded # False
|
|
75
|
+
arr = lazy.load() # loader runs once; cached thereafter
|
|
76
|
+
lazy.release() # frees the data; attributes remain
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Dependencies
|
|
82
|
+
|
|
83
|
+
- `numpy>=1.26` — used for array-aware value equality and repr.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
vcti/datanode/__init__.py,sha256=9-PTZ4o1k7A4Rb1PVkESi4vcPmgsoEKVSVDjeFdGzhY,655
|
|
2
|
+
vcti/datanode/lazy_node.py,sha256=dsv_VtJoqUwIEmO2l0MJgDE9B-kDmwqWVGonavy8faw,4623
|
|
3
|
+
vcti/datanode/node.py,sha256=bYwIANZ4SQhXWY9Zy6uP8YpxhjWP7Me12Zgk8TzUkxk,5503
|
|
4
|
+
vcti/datanode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
vcti_datanode-1.0.1.dist-info/licenses/LICENSE,sha256=gqRj-E4YRsT7mZ52W76LG6aTTFv6iEOK9QR_fV5EdrI,369
|
|
6
|
+
vcti_datanode-1.0.1.dist-info/METADATA,sha256=xWVgCXxtKW-ywJ7I0UHcR-HGUGsKDWgY5GFqxDUJBWY,2296
|
|
7
|
+
vcti_datanode-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
vcti_datanode-1.0.1.dist-info/top_level.txt,sha256=Jl6AIAI3Xhru_BFQAhD_13VeXLmZQd9BqBNUaAKNgKs,5
|
|
9
|
+
vcti_datanode-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|