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.
Files changed (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. 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