diffract-core 0.2.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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Hybrid storage manager combining light and heavy backends.
|
|
2
|
+
|
|
3
|
+
Routes small/structured data to light storage and large arrays to heavy storage.
|
|
4
|
+
Uses a sentinel value ("__HEAVY__") in light storage to indicate the data lives
|
|
5
|
+
in the heavy backend.
|
|
6
|
+
|
|
7
|
+
Thread-safety: inherits the concurrency models of the underlying managers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import types
|
|
14
|
+
from typing import Any, Self
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from .interface import DEFAULT_TABLE, UID, IStorageManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HybridStorageManager(IStorageManager):
|
|
22
|
+
"""Storage manager that routes data between light and heavy backends.
|
|
23
|
+
|
|
24
|
+
Small values and metadata go to light storage for fast random access. Large
|
|
25
|
+
data (exceeding threshold) are stored in heavy storage with a sentinel value
|
|
26
|
+
in light storage to mark the indirection.
|
|
27
|
+
|
|
28
|
+
This provides a unified interface while allowing optimization for different
|
|
29
|
+
data access patterns and storage backends.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> # Local SQLite + Cloud Zarr
|
|
33
|
+
>>> light = SQLiteStorageManager("meta.db")
|
|
34
|
+
>>> heavy = ZarrStorageManager("s3://bucket/data")
|
|
35
|
+
>>> storage = HybridStorageManager(
|
|
36
|
+
... light, heavy, array_threshold=128 * 1024 * 1024
|
|
37
|
+
... )
|
|
38
|
+
>>> with storage:
|
|
39
|
+
... storage.set_field("param_001", "weights", large_array)
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
light_storage: Storage backend for small/structured data and metadata.
|
|
43
|
+
heavy_storage: Storage backend for large data arrays.
|
|
44
|
+
array_threshold: Byte threshold above which data goes to heavy storage.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_HEAVY_SENTINEL = "__HEAVY__"
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
light_storage: IStorageManager,
|
|
52
|
+
heavy_storage: IStorageManager,
|
|
53
|
+
array_threshold: int = 128 * 1024 * 1024,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize hybrid storage manager.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
light_storage: Storage for small data and metadata.
|
|
59
|
+
heavy_storage: Blob storage for large data arrays.
|
|
60
|
+
array_threshold: Byte threshold for routing to heavy storage.
|
|
61
|
+
"""
|
|
62
|
+
self.light = light_storage
|
|
63
|
+
self.heavy = heavy_storage
|
|
64
|
+
self.array_threshold = array_threshold
|
|
65
|
+
|
|
66
|
+
def _should_use_heavy(self, value: Any) -> bool:
|
|
67
|
+
"""Determine if value should be stored in heavy storage."""
|
|
68
|
+
if isinstance(value, np.ndarray):
|
|
69
|
+
return value.nbytes >= self.array_threshold
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def has_field(
|
|
73
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Check if a field exists in storage."""
|
|
76
|
+
return self.light.has_field(obj_uid, field_name, table=table)
|
|
77
|
+
|
|
78
|
+
def get_field(
|
|
79
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Retrieve field value, following heavy storage indirection if needed.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
KeyError: If the field doesn't exist.
|
|
85
|
+
"""
|
|
86
|
+
value = self.light.get_field(obj_uid, field_name, table=table)
|
|
87
|
+
if isinstance(value, str) and value == self._HEAVY_SENTINEL:
|
|
88
|
+
return self.heavy.get_field(obj_uid, field_name, table=table)
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
def set_field(
|
|
92
|
+
self, obj_uid: UID, field_name: str, value: Any, *, table: str = DEFAULT_TABLE
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Store field value, routing data according to routing rules.
|
|
95
|
+
|
|
96
|
+
Large or heavy data goes to heavy storage with a sentinel in light storage.
|
|
97
|
+
Smaller values go directly to light storage. If a field moves between
|
|
98
|
+
backends, the old copy is erased.
|
|
99
|
+
"""
|
|
100
|
+
if self._should_use_heavy(value):
|
|
101
|
+
self.light.set_field(obj_uid, field_name, self._HEAVY_SENTINEL, table=table)
|
|
102
|
+
self.heavy.set_field(obj_uid, field_name, value, table=table)
|
|
103
|
+
else:
|
|
104
|
+
self.light.set_field(obj_uid, field_name, value, table=table)
|
|
105
|
+
self.heavy.erase_field(obj_uid, field_name, table=table)
|
|
106
|
+
|
|
107
|
+
def erase_field(
|
|
108
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Remove a field from storage."""
|
|
111
|
+
self.light.erase_field(obj_uid, field_name, table=table)
|
|
112
|
+
self.heavy.erase_field(obj_uid, field_name, table=table)
|
|
113
|
+
|
|
114
|
+
def erase_obj(self, obj_uid: UID, *, table: str = DEFAULT_TABLE) -> None:
|
|
115
|
+
"""Remove an object and all its fields from storage."""
|
|
116
|
+
self.light.erase_obj(obj_uid, table=table)
|
|
117
|
+
self.heavy.erase_obj(obj_uid, table=table)
|
|
118
|
+
|
|
119
|
+
def list_fields(
|
|
120
|
+
self, obj_uid: UID = None, *, table: str = DEFAULT_TABLE
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""List all fields for an object."""
|
|
123
|
+
return self.light.list_fields(obj_uid, table=table)
|
|
124
|
+
|
|
125
|
+
def list_objs(self, *, table: str = DEFAULT_TABLE) -> list[str]:
|
|
126
|
+
"""List all objects."""
|
|
127
|
+
return self.light.list_objs(table=table)
|
|
128
|
+
|
|
129
|
+
def list_objs_has_field(
|
|
130
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
131
|
+
) -> list[UID]:
|
|
132
|
+
"""List objects having a field."""
|
|
133
|
+
return self.light.list_objs_has_field(field_name, table=table)
|
|
134
|
+
|
|
135
|
+
def erase_field_for_all(
|
|
136
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Remove a field from all objects.
|
|
139
|
+
|
|
140
|
+
Delegates to underlying backends which handle batch optimization.
|
|
141
|
+
"""
|
|
142
|
+
self.light.erase_field_for_all(field_name, table=table)
|
|
143
|
+
self.heavy.erase_field_for_all(field_name, table=table)
|
|
144
|
+
|
|
145
|
+
def get_field_metadata(
|
|
146
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
147
|
+
) -> dict[str, Any] | None:
|
|
148
|
+
"""Return stored metadata from whichever backend holds the value."""
|
|
149
|
+
try:
|
|
150
|
+
value = self.light.get_field(obj_uid, field_name, table=table)
|
|
151
|
+
except KeyError:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if isinstance(value, str) and value == self._HEAVY_SENTINEL:
|
|
155
|
+
return self.heavy.get_field_metadata(obj_uid, field_name, table=table)
|
|
156
|
+
return self.light.get_field_metadata(obj_uid, field_name, table=table)
|
|
157
|
+
|
|
158
|
+
def clear(self, *, table: str | None = None) -> None:
|
|
159
|
+
"""Clear data from both backends.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
table: If provided, clear only this table. If None, clear all data.
|
|
163
|
+
"""
|
|
164
|
+
self.light.clear(table=table)
|
|
165
|
+
self.heavy.clear(table=table)
|
|
166
|
+
|
|
167
|
+
def connect(self) -> None:
|
|
168
|
+
"""Initialize connections for backends that need it."""
|
|
169
|
+
if hasattr(self.light, "connect"):
|
|
170
|
+
self.light.connect()
|
|
171
|
+
if hasattr(self.heavy, "connect"):
|
|
172
|
+
self.heavy.connect()
|
|
173
|
+
|
|
174
|
+
def close(self) -> None:
|
|
175
|
+
"""Close both backends. Raises ExceptionGroup on errors."""
|
|
176
|
+
exceptions: list[Exception] = []
|
|
177
|
+
|
|
178
|
+
for manager in [self.light, self.heavy]:
|
|
179
|
+
try:
|
|
180
|
+
if hasattr(manager, "close"):
|
|
181
|
+
manager.close()
|
|
182
|
+
except Exception as exc: # noqa: BLE001
|
|
183
|
+
exceptions.append(exc)
|
|
184
|
+
|
|
185
|
+
if exceptions:
|
|
186
|
+
raise ExceptionGroup("Error occurred during closing", exceptions)
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> Self:
|
|
189
|
+
"""Enter batch context for both backends."""
|
|
190
|
+
if hasattr(self.light, "__enter__"):
|
|
191
|
+
self.light.__enter__()
|
|
192
|
+
if hasattr(self.heavy, "__enter__"):
|
|
193
|
+
self.heavy.__enter__()
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def __exit__(
|
|
197
|
+
self,
|
|
198
|
+
exc_type: type[BaseException] | None,
|
|
199
|
+
exc_val: BaseException | None,
|
|
200
|
+
exc_tb: types.TracebackType | None,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Exit batch context for both backends. Raises ExceptionGroup on errors."""
|
|
203
|
+
exceptions: list[Exception] = []
|
|
204
|
+
|
|
205
|
+
for manager in [self.light, self.heavy]:
|
|
206
|
+
try:
|
|
207
|
+
if hasattr(manager, "__exit__"):
|
|
208
|
+
manager.__exit__(exc_type, exc_val, exc_tb)
|
|
209
|
+
except Exception as exc: # noqa: BLE001
|
|
210
|
+
exceptions.append(exc)
|
|
211
|
+
|
|
212
|
+
if exceptions:
|
|
213
|
+
raise ExceptionGroup("Error occurred during exiting contexts", exceptions)
|
|
214
|
+
|
|
215
|
+
def __del__(self) -> None:
|
|
216
|
+
"""Best-effort cleanup; suppresses exceptions during GC."""
|
|
217
|
+
with contextlib.suppress(Exception):
|
|
218
|
+
self.close()
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Storage manager interface and protocols.
|
|
2
|
+
|
|
3
|
+
This module defines the core protocol and type aliases that all storage
|
|
4
|
+
manager implementations must follow. It provides a framework-agnostic
|
|
5
|
+
contract for persistent data storage operations.
|
|
6
|
+
|
|
7
|
+
The interface supports hierarchical data organization where objects are
|
|
8
|
+
identified by UID, organized into tables, and can contain multiple named
|
|
9
|
+
fields with arbitrary values. All storage operations are designed to be
|
|
10
|
+
atomic and durable.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
>>> storage_manager: IStorageManager = get_storage_manager()
|
|
14
|
+
>>> storage_manager.set_field(
|
|
15
|
+
... "obj123", "result", computed_array, table="parameters"
|
|
16
|
+
... )
|
|
17
|
+
>>> value = storage_manager.get_field("obj123", "result", table="parameters")
|
|
18
|
+
>>> all_objects = storage_manager.list_objs(table="parameters")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import types
|
|
24
|
+
from typing import Any, Protocol, Self, runtime_checkable
|
|
25
|
+
|
|
26
|
+
type UID = str
|
|
27
|
+
|
|
28
|
+
DEFAULT_TABLE = "default"
|
|
29
|
+
"""Default table name for storage operations when not specified."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class IStorageManager(Protocol):
|
|
34
|
+
"""Protocol defining the storage manager interface.
|
|
35
|
+
|
|
36
|
+
Provides a unified interface for persistent storage operations across
|
|
37
|
+
different backends. All storage manager implementations must implement
|
|
38
|
+
these methods to ensure consistent behavior and data durability.
|
|
39
|
+
|
|
40
|
+
The storage is organized as a three-level structure: tables contain objects
|
|
41
|
+
identified by UID, and objects contain named fields with arbitrary values.
|
|
42
|
+
Storage operations should be atomic and provide durability guarantees.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __enter__(self) -> Self:
|
|
46
|
+
"""Enter batch operation context for optimized writes."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def __exit__(
|
|
50
|
+
self,
|
|
51
|
+
exc_type: type[BaseException] | None,
|
|
52
|
+
exc_val: BaseException | None,
|
|
53
|
+
exc_tb: types.TracebackType | None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Exit batch context and flush pending operations."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def has_field(
|
|
59
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Check if an object has a specific field in storage.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
obj_uid: Unique identifier for the stored object.
|
|
65
|
+
field_name: Name of the field to check.
|
|
66
|
+
table: Table name for logical data separation.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the field exists in storage, False otherwise.
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def get_field(
|
|
74
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
75
|
+
) -> Any:
|
|
76
|
+
"""Retrieve a field value from storage.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
obj_uid: Unique identifier for the stored object.
|
|
80
|
+
field_name: Name of the field to retrieve.
|
|
81
|
+
table: Table name for logical data separation.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The stored value.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
KeyError: If the field doesn't exist.
|
|
88
|
+
OSError: If storage read operation fails.
|
|
89
|
+
ValueError: If stored data is corrupted.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def list_fields(
|
|
94
|
+
self, obj_uid: UID = None, *, table: str = DEFAULT_TABLE
|
|
95
|
+
) -> list[str]:
|
|
96
|
+
"""List all field names for a specific object.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
obj_uid: Unique identifier for the stored object.
|
|
100
|
+
table: Table name for logical data separation.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
List of field names that exist for the specified object.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
OSError: If storage read operation fails.
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def list_objs(self, *, table: str = DEFAULT_TABLE) -> list[str]:
|
|
111
|
+
"""List all object UIDs in storage.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
table: Table name for logical data separation.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of all unique object identifiers in storage.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
OSError: If storage read operation fails.
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def list_objs_has_field(
|
|
125
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
126
|
+
) -> list[UID]:
|
|
127
|
+
"""List all object UIDs that have a specific field.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
field_name: Name of the field to search for.
|
|
131
|
+
table: Table name for logical data separation.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of UIDs for objects that have the specified field.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
OSError: If storage read operation fails.
|
|
138
|
+
"""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
def set_field(
|
|
142
|
+
self, obj_uid: UID, field_name: str, value: Any, *, table: str = DEFAULT_TABLE
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Store a field value in persistent storage.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
obj_uid: Unique identifier for the stored object.
|
|
148
|
+
field_name: Name of the field to store.
|
|
149
|
+
value: Value to store (must be serializable by the backend).
|
|
150
|
+
table: Table name for logical data separation.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
OSError: If storage write operation fails.
|
|
154
|
+
"""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def erase_field(
|
|
158
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Remove a specific field from storage.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
obj_uid: Unique identifier for the stored object.
|
|
164
|
+
field_name: Name of the field to remove.
|
|
165
|
+
table: Table name for logical data separation.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
OSError: If storage delete operation fails.
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def erase_obj(self, obj_uid: UID, *, table: str = DEFAULT_TABLE) -> None:
|
|
173
|
+
"""Remove an entire object and all its fields from storage.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
obj_uid: Unique identifier for the object to remove.
|
|
177
|
+
table: Table name for logical data separation.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
OSError: If storage delete operation fails.
|
|
181
|
+
"""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
def erase_field_for_all(
|
|
185
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Remove a field from all stored objects.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
field_name: Name of the field to remove from all objects.
|
|
191
|
+
table: Table name for logical data separation.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
OSError: If storage delete operation fails.
|
|
195
|
+
"""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
def clear(self, *, table: str | None = None) -> None:
|
|
199
|
+
"""Remove stored data.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
table: If provided, clear only this table. If None, clear all data.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
OSError: If storage clear operation fails.
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
def get_field_metadata(
|
|
210
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
211
|
+
) -> dict[str, Any] | None:
|
|
212
|
+
"""Return stored metadata (dtype/shape/kind) if available.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
obj_uid: Unique identifier for the stored object.
|
|
216
|
+
field_name: Name of the field.
|
|
217
|
+
table: Table name for logical data separation.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Metadata dictionary or None if not available.
|
|
221
|
+
"""
|
|
222
|
+
...
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Helpers for capturing lightweight value metadata (dtype, shape, kind)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
Kind = Literal["scalar", "vector", "matrix", "ndarray", "object"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_SCALAR_NDIM = 0
|
|
14
|
+
_VECTOR_NDIM = 1
|
|
15
|
+
_MATRIX_NDIM = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class ValueMetadata:
|
|
20
|
+
"""Normalized description of a stored value."""
|
|
21
|
+
|
|
22
|
+
kind: Kind
|
|
23
|
+
dtype: str | None
|
|
24
|
+
shape: tuple[int, ...] | None
|
|
25
|
+
ndim: int | None
|
|
26
|
+
is_numeric: bool
|
|
27
|
+
|
|
28
|
+
def to_jsonable(self) -> dict[str, Any]:
|
|
29
|
+
"""Convert metadata to JSON-serializable dictionary."""
|
|
30
|
+
return asdict(self)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_jsonable(cls, data: dict[str, Any]) -> ValueMetadata:
|
|
34
|
+
"""Reconstruct metadata from JSON-serializable dictionary."""
|
|
35
|
+
return cls(
|
|
36
|
+
kind=data.get("kind", "object"),
|
|
37
|
+
dtype=data.get("dtype"),
|
|
38
|
+
shape=tuple(data["shape"]) if data.get("shape") is not None else None,
|
|
39
|
+
ndim=data.get("ndim"),
|
|
40
|
+
is_numeric=bool(data.get("is_numeric", False)),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _kind_from_ndim(ndim: int) -> Kind:
|
|
45
|
+
if ndim == _SCALAR_NDIM:
|
|
46
|
+
return "scalar"
|
|
47
|
+
if ndim == _VECTOR_NDIM:
|
|
48
|
+
return "vector"
|
|
49
|
+
if ndim == _MATRIX_NDIM:
|
|
50
|
+
return "matrix"
|
|
51
|
+
return "ndarray"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def infer_value_metadata(value: Any) -> ValueMetadata:
|
|
55
|
+
"""Infer basic metadata for a value that will be stored.
|
|
56
|
+
|
|
57
|
+
Keeps the schema minimal while still being enough for visualization/signature
|
|
58
|
+
purposes.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(value, np.ndarray):
|
|
61
|
+
ndim = int(value.ndim)
|
|
62
|
+
return ValueMetadata(
|
|
63
|
+
kind=_kind_from_ndim(ndim),
|
|
64
|
+
dtype=str(value.dtype),
|
|
65
|
+
shape=tuple(int(x) for x in value.shape),
|
|
66
|
+
ndim=ndim,
|
|
67
|
+
is_numeric=bool(np.issubdtype(value.dtype, np.number)),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if isinstance(value, (int, float, bool, complex, np.generic)):
|
|
71
|
+
# Scalars treated as 0-D.
|
|
72
|
+
dtype_name = type(value).__name__
|
|
73
|
+
is_numeric = isinstance(value, (int, float, complex, np.generic, bool))
|
|
74
|
+
return ValueMetadata(
|
|
75
|
+
kind="scalar",
|
|
76
|
+
dtype=dtype_name,
|
|
77
|
+
shape=None,
|
|
78
|
+
ndim=0,
|
|
79
|
+
is_numeric=is_numeric,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Fallback: treat as opaque object.
|
|
83
|
+
return ValueMetadata(
|
|
84
|
+
kind="object",
|
|
85
|
+
dtype=type(value).__name__,
|
|
86
|
+
shape=None,
|
|
87
|
+
ndim=None,
|
|
88
|
+
is_numeric=False,
|
|
89
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""RAM-based storage manager implementation for diffract.
|
|
2
|
+
|
|
3
|
+
Provides high-performance, RAM-only storage (no disk persistence).
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> storage = RAMStorageManager()
|
|
7
|
+
>>> storage.set_field("obj1", "data", [1, 2, 3], table="parameters")
|
|
8
|
+
>>> storage.get_field("obj1", "data", table="parameters")
|
|
9
|
+
[1, 2, 3]
|
|
10
|
+
>>> storage.clear()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import types
|
|
16
|
+
from typing import Any, Self
|
|
17
|
+
|
|
18
|
+
from .interface import DEFAULT_TABLE, UID, IStorageManager
|
|
19
|
+
from .metadata import infer_value_metadata
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RAMStorageManager(IStorageManager):
|
|
23
|
+
"""RAM-only storage manager with table support."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
# Maps a table and field name to a mapping from object uid to value.
|
|
27
|
+
self._storage: dict[tuple[str, str], dict[UID, Any]] = {}
|
|
28
|
+
# Maps a table, field name and object uid to that value's metadata.
|
|
29
|
+
self._metadata: dict[tuple[str, str, UID], dict[str, Any]] = {}
|
|
30
|
+
|
|
31
|
+
def _key(self, table: str, field_name: str) -> tuple[str, str]:
|
|
32
|
+
"""Create storage key from table and field name."""
|
|
33
|
+
return (table, field_name)
|
|
34
|
+
|
|
35
|
+
def has_field(
|
|
36
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Return True if the field exists in memory."""
|
|
39
|
+
key = self._key(table, field_name)
|
|
40
|
+
return key in self._storage and obj_uid in self._storage[key]
|
|
41
|
+
|
|
42
|
+
def get_field(
|
|
43
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Get a field value from memory."""
|
|
46
|
+
key = self._key(table, field_name)
|
|
47
|
+
return self._storage[key][obj_uid]
|
|
48
|
+
|
|
49
|
+
def list_fields(
|
|
50
|
+
self, obj_uid: UID = None, *, table: str = DEFAULT_TABLE
|
|
51
|
+
) -> list[str]:
|
|
52
|
+
"""List field names (optionally only those containing the given object)."""
|
|
53
|
+
if obj_uid is None:
|
|
54
|
+
return [field_name for (tbl, field_name) in self._storage if tbl == table]
|
|
55
|
+
return [
|
|
56
|
+
field_name
|
|
57
|
+
for (tbl, field_name), objs in self._storage.items()
|
|
58
|
+
if tbl == table and obj_uid in objs
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def list_objs(self, *, table: str = DEFAULT_TABLE) -> list[str]:
|
|
62
|
+
"""List all object UIDs in specified table."""
|
|
63
|
+
seen: set[str] = set()
|
|
64
|
+
for (tbl, _), objs in self._storage.items():
|
|
65
|
+
if tbl == table:
|
|
66
|
+
seen.update(objs.keys())
|
|
67
|
+
return list(seen)
|
|
68
|
+
|
|
69
|
+
def list_objs_has_field(
|
|
70
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
71
|
+
) -> list[UID]:
|
|
72
|
+
"""List objects that have the given field."""
|
|
73
|
+
key = self._key(table, field_name)
|
|
74
|
+
if key in self._storage:
|
|
75
|
+
return list(self._storage[key].keys())
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def set_field(
|
|
79
|
+
self, obj_uid: UID, field_name: str, value: Any, *, table: str = DEFAULT_TABLE
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Store a field value in memory."""
|
|
82
|
+
key = self._key(table, field_name)
|
|
83
|
+
if key not in self._storage:
|
|
84
|
+
self._storage[key] = {}
|
|
85
|
+
self._storage[key][obj_uid] = value
|
|
86
|
+
self._metadata[(table, field_name, obj_uid)] = infer_value_metadata(
|
|
87
|
+
value
|
|
88
|
+
).to_jsonable()
|
|
89
|
+
|
|
90
|
+
def erase_field(
|
|
91
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Remove a field for an object."""
|
|
94
|
+
if self.has_field(obj_uid, field_name, table=table):
|
|
95
|
+
key = self._key(table, field_name)
|
|
96
|
+
del self._storage[key][obj_uid]
|
|
97
|
+
self._metadata.pop((table, field_name, obj_uid), None)
|
|
98
|
+
if not self._storage[key]:
|
|
99
|
+
del self._storage[key]
|
|
100
|
+
|
|
101
|
+
def erase_obj(self, obj_uid: UID, *, table: str = DEFAULT_TABLE) -> None:
|
|
102
|
+
"""Remove an object and all its fields from specified table."""
|
|
103
|
+
keys_to_remove = []
|
|
104
|
+
for (tbl, field_name), objs in self._storage.items():
|
|
105
|
+
if tbl == table and obj_uid in objs:
|
|
106
|
+
del objs[obj_uid]
|
|
107
|
+
self._metadata.pop((table, field_name, obj_uid), None)
|
|
108
|
+
if not objs:
|
|
109
|
+
keys_to_remove.append((tbl, field_name))
|
|
110
|
+
|
|
111
|
+
for key in keys_to_remove:
|
|
112
|
+
del self._storage[key]
|
|
113
|
+
|
|
114
|
+
def erase_field_for_all(
|
|
115
|
+
self, field_name: str, *, table: str = DEFAULT_TABLE
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Remove a field from all objects in specified table."""
|
|
118
|
+
key = self._key(table, field_name)
|
|
119
|
+
if key in self._storage:
|
|
120
|
+
del self._storage[key]
|
|
121
|
+
to_delete = [k for k in self._metadata if k[0] == table and k[1] == field_name]
|
|
122
|
+
for k in to_delete:
|
|
123
|
+
del self._metadata[k]
|
|
124
|
+
|
|
125
|
+
def clear(self, *, table: str | None = None) -> None:
|
|
126
|
+
"""Remove data from memory.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
table: If provided, clear only this table. If None, clear all data.
|
|
130
|
+
"""
|
|
131
|
+
if table is None:
|
|
132
|
+
self._storage.clear()
|
|
133
|
+
self._metadata.clear()
|
|
134
|
+
else:
|
|
135
|
+
keys_to_remove = [k for k in self._storage if k[0] == table]
|
|
136
|
+
for k in keys_to_remove:
|
|
137
|
+
del self._storage[k]
|
|
138
|
+
meta_to_remove = [k for k in self._metadata if k[0] == table]
|
|
139
|
+
for k in meta_to_remove:
|
|
140
|
+
del self._metadata[k]
|
|
141
|
+
|
|
142
|
+
def get_field_metadata(
|
|
143
|
+
self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
|
|
144
|
+
) -> dict[str, Any] | None:
|
|
145
|
+
"""Return stored metadata for a field if available."""
|
|
146
|
+
return self._metadata.get((table, field_name, obj_uid))
|
|
147
|
+
|
|
148
|
+
def __enter__(self) -> Self:
|
|
149
|
+
"""Enter context; returns self."""
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def __exit__(
|
|
153
|
+
self,
|
|
154
|
+
exc_type: type[BaseException] | None,
|
|
155
|
+
exc_val: BaseException | None,
|
|
156
|
+
exc_tb: types.TracebackType | None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Exit context; no-op for RAM backend."""
|
|
159
|
+
return
|