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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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