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,91 @@
1
+ """Parallel execution utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import concurrent.futures
6
+ import time
7
+ from collections.abc import Callable, Iterator, Sequence
8
+ from itertools import chain
9
+ from typing import TypeVar
10
+
11
+ from diffract.core.utils.math import mean
12
+
13
+ from .runtime import ParallelContext, should_parallelize
14
+
15
+ T = TypeVar("T")
16
+ R = TypeVar("R")
17
+
18
+
19
+ def map_maybe_parallel(
20
+ items: Sequence[T],
21
+ fn: Callable[[T], R],
22
+ *,
23
+ parallel: ParallelContext | None,
24
+ ) -> Iterator[R]:
25
+ """Map fn over items with automatic parallelization decision.
26
+
27
+ Uses a small parallel pilot batch to estimate average task cost and
28
+ decide whether remaining work should be parallelized.
29
+
30
+ Args:
31
+ items: Sequence of items to process.
32
+ fn: Function to apply to each item.
33
+ parallel: Optional parallel context for execution.
34
+
35
+ Returns:
36
+ List of results from applying fn to each item.
37
+ """
38
+ if not items:
39
+ return iter([])
40
+
41
+ if parallel is None:
42
+ return map(fn, items)
43
+
44
+ executor = parallel.executor
45
+ workers = parallel.workers
46
+ overhead = parallel.calibration.submit_overhead_per_task_s
47
+
48
+ n = len(items)
49
+ if n <= 1 or workers <= 1:
50
+ return map(fn, items)
51
+
52
+ # Fast path: for small batches, avoid pilot overhead and preserve order.
53
+ if n <= workers:
54
+ return executor.map(fn, items)
55
+
56
+ # If overhead is already comparable to expected work, skip parallelism.
57
+ # This is a cheap guard for very small/fast tasks.
58
+ overhead = max(0.0, overhead)
59
+ pilot_n = min(n, workers)
60
+
61
+ def _timed(idx: int, item: T) -> tuple[int, float, R]:
62
+ t0 = time.perf_counter()
63
+ value = fn(item)
64
+ dt = max(0.0, time.perf_counter() - t0)
65
+ return idx, dt, value
66
+
67
+ futures = [executor.submit(_timed, i, items[i]) for i in range(pilot_n)]
68
+
69
+ pilot_results: list[R | None] = [None] * pilot_n
70
+ dts: list[float] = []
71
+ for fut in concurrent.futures.as_completed(futures):
72
+ idx, dt, value = fut.result()
73
+ pilot_results[idx] = value
74
+ dts.append(dt)
75
+
76
+ rest = items[pilot_n:]
77
+ if not rest:
78
+ return iter(pilot_results)
79
+
80
+ avg = mean(dts)
81
+ if should_parallelize(
82
+ avg_task_s=avg,
83
+ n_tasks=len(rest),
84
+ workers=workers,
85
+ overhead_per_task_s=overhead,
86
+ ):
87
+ rest_results = executor.map(fn, rest)
88
+ else:
89
+ rest_results = map(fn, rest)
90
+
91
+ return chain(pilot_results, rest_results)
@@ -0,0 +1,91 @@
1
+ """Runtime helpers for parallel execution decisions.
2
+
3
+ This module intentionally does not depend on dependency-injector. It provides
4
+ small, testable building blocks that can be wired via DI at the application
5
+ level.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import functools
12
+ import time
13
+ from collections.abc import Callable
14
+ from typing import Any, TypeVar
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True, slots=True)
20
+ class ParallelCalibration:
21
+ """Thread-pool overhead calibration data."""
22
+
23
+ workers: int
24
+ submit_overhead_per_task_s: float
25
+
26
+
27
+ @dataclasses.dataclass(frozen=True, slots=True)
28
+ class ParallelContext:
29
+ """Per-method parallel execution context."""
30
+
31
+ executor: Any
32
+ calibration: ParallelCalibration
33
+ workers: int
34
+
35
+
36
+ def calibrate_thread_pool_overhead(
37
+ *, submit: Callable[[Callable[[int], int], int], object], workers: int
38
+ ) -> ParallelCalibration:
39
+ """Measure approximate per-task overhead for a running thread pool.
40
+
41
+ Args:
42
+ submit: Callable with signature like executor.submit(fn, arg).
43
+ workers: Configured worker count for the pool.
44
+
45
+ Returns:
46
+ Calibration object. The overhead is measured by submitting a small batch
47
+ of no-op tasks and waiting for completion.
48
+ """
49
+
50
+ def _noop(_: int) -> int:
51
+ return 0
52
+
53
+ n = max(1, workers * (workers + 1))
54
+ t0 = time.perf_counter()
55
+ futures = [submit(_noop, i) for i in range(n)]
56
+ for fut in futures:
57
+ # Executor API: future-like object with result()
58
+ _ = fut.result()
59
+ dt = max(0.0, time.perf_counter() - t0)
60
+ overhead = dt / n if n > 0 else 0.0
61
+ return ParallelCalibration(workers=workers, submit_overhead_per_task_s=overhead)
62
+
63
+
64
+ @functools.lru_cache(maxsize=32)
65
+ def get_thread_pool_calibration(workers: int) -> ParallelCalibration:
66
+ """Get cached overhead calibration for a worker count."""
67
+ import concurrent.futures
68
+
69
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
70
+ return calibrate_thread_pool_overhead(
71
+ submit=executor.submit,
72
+ workers=workers,
73
+ )
74
+
75
+
76
+ def should_parallelize(
77
+ *,
78
+ avg_task_s: float,
79
+ n_tasks: int,
80
+ workers: int,
81
+ overhead_per_task_s: float,
82
+ ) -> bool:
83
+ """Return True if parallel execution is expected to be faster."""
84
+ if n_tasks <= 1 or workers <= 1:
85
+ return False
86
+ avg = max(0.0, avg_task_s)
87
+ overhead = max(0.0, overhead_per_task_s)
88
+ sequential = avg * n_tasks
89
+ parallel_compute = (avg * n_tasks) / workers
90
+ parallel_total = parallel_compute + overhead * n_tasks
91
+ return sequential > parallel_total
@@ -0,0 +1,35 @@
1
+ """Storage management module providing persistent data storage backends.
2
+
3
+ This module offers a unified interface for persistent storage with support for
4
+ multiple backends including HDF5. It uses dependency injection for flexible
5
+ configuration and backend selection, providing durability guarantees for
6
+ cached computations.
7
+
8
+ Features:
9
+ - HDF5-based persistent storage with compression
10
+ - Hierarchical data organization by objects and fields
11
+ - Support for NumPy arrays, JSON-serializable data, and arbitrary objects
12
+ - Atomic write operations and data integrity
13
+ - Configurable compression and chunking strategies
14
+
15
+ Example:
16
+ >>> from diffract.core.storage import StorageContainer
17
+ >>> container = StorageContainer()
18
+ >>> storage_manager = container.storage_manager()
19
+ >>> storage_manager.set_field("obj123", "result", numpy_array)
20
+ """
21
+
22
+ from .containers import StorageContainer
23
+ from .hdf5_manager import HDF5StorageManager
24
+ from .interface import DEFAULT_TABLE, UID, IStorageManager
25
+ from .ram_manager import RAMStorageManager
26
+ from .zarr_manager import ZarrStorageManager
27
+
28
+ __all__ = [
29
+ "DEFAULT_TABLE",
30
+ "UID",
31
+ "HDF5StorageManager",
32
+ "IStorageManager",
33
+ "StorageContainer",
34
+ "ZarrStorageManager",
35
+ ]
@@ -0,0 +1,330 @@
1
+ import json
2
+ import pickle
3
+ import threading
4
+ from abc import abstractmethod
5
+ from contextlib import suppress
6
+ from types import TracebackType
7
+ from typing import Any, Self
8
+
9
+ import numpy as np
10
+
11
+ from diffract.core.storage.interface import DEFAULT_TABLE, UID, IStorageManager
12
+
13
+
14
+ class BaseStorageManager(IStorageManager):
15
+ """Base storage manager with batching support and table separation."""
16
+
17
+ def __init__(
18
+ self,
19
+ batch_size_limit_bytes: int = 50 * 1024 * 1024,
20
+ batch_soft_limit_ratio: float = 0.9,
21
+ use_json: bool = True,
22
+ ) -> None:
23
+ self._batch_size_limit_bytes = batch_size_limit_bytes
24
+ self._batch_soft_limit_bytes = int(
25
+ batch_size_limit_bytes * batch_soft_limit_ratio
26
+ )
27
+ self._use_json = use_json
28
+
29
+ self._pending_set_bytes: int = 0
30
+ # Keys are (table, obj_uid, field_name)
31
+ self._set_field_sizes: dict[tuple[str, UID, str], int] = {}
32
+ self._set_field_batch: dict[tuple[str, UID, str], Any] = {}
33
+ self._erase_field_batch: set[tuple[str, UID, str]] = set()
34
+ self._erase_obj_batch: set[tuple[str, UID]] = set()
35
+ self._erase_field_for_all_batch: set[tuple[str, str]] = set()
36
+
37
+ self._thread_local = threading.local() # Thread-local context depth
38
+ self._performing_batch_operations: bool = False
39
+
40
+ def has_field(
41
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
42
+ ) -> bool:
43
+ """Check if field exists.
44
+
45
+ Args:
46
+ obj_uid: Object unique identifier.
47
+ field_name: Field name.
48
+ table: Table name for logical data separation.
49
+
50
+ Returns:
51
+ True if field exists, False otherwise.
52
+ """
53
+ self._auto_flush_before_read()
54
+ return self._has_field(obj_uid, field_name, table=table)
55
+
56
+ def get_field(
57
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
58
+ ) -> Any:
59
+ """Retrieve field value.
60
+
61
+ Args:
62
+ obj_uid: Object unique identifier.
63
+ field_name: Field name.
64
+ table: Table name for logical data separation.
65
+
66
+ Returns:
67
+ Field value.
68
+
69
+ Raises:
70
+ KeyError: If field not found.
71
+ """
72
+ self._auto_flush_before_read()
73
+ return self._get_field(obj_uid, field_name, table=table)
74
+
75
+ def get_field_metadata(
76
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
77
+ ) -> dict[str, Any] | None:
78
+ """Return stored metadata for a field if present."""
79
+ self._auto_flush_before_read()
80
+ return self._get_field_metadata(obj_uid, field_name, table=table)
81
+
82
+ def list_fields(
83
+ self, obj_uid: UID = None, *, table: str = DEFAULT_TABLE
84
+ ) -> list[str]:
85
+ """List fields.
86
+
87
+ Args:
88
+ obj_uid: Optional object unique identifier. If None, lists all fields.
89
+ table: Table name for logical data separation.
90
+
91
+ Returns:
92
+ List of field names.
93
+ """
94
+ self._auto_flush_before_read()
95
+ return self._list_fields(obj_uid, table=table)
96
+
97
+ def list_objs(self, *, table: str = DEFAULT_TABLE) -> list[str]:
98
+ """List all objects.
99
+
100
+ Args:
101
+ table: Table name for logical data separation.
102
+
103
+ Returns:
104
+ List of object unique identifiers.
105
+ """
106
+ self._auto_flush_before_read()
107
+ return self._list_objs(table=table)
108
+
109
+ def list_objs_has_field(
110
+ self, field_name: str, *, table: str = DEFAULT_TABLE
111
+ ) -> list[UID]:
112
+ """List objects with specific field.
113
+
114
+ Args:
115
+ field_name: Field name.
116
+ table: Table name for logical data separation.
117
+
118
+ Returns:
119
+ List of object unique identifiers.
120
+ """
121
+ self._auto_flush_before_read()
122
+ return self._list_objs_has_field(field_name, table=table)
123
+
124
+ def set_field(
125
+ self, obj_uid: UID, field_name: str, value: Any, *, table: str = DEFAULT_TABLE
126
+ ) -> None:
127
+ """Store field value.
128
+
129
+ Args:
130
+ obj_uid: Object unique identifier.
131
+ field_name: Field name.
132
+ value: Value to store.
133
+ table: Table name for logical data separation.
134
+ """
135
+ key = (table, obj_uid, field_name)
136
+ prev_size = self._set_field_sizes.pop(key, 0)
137
+ if prev_size:
138
+ self._pending_set_bytes = max(0, self._pending_set_bytes - prev_size)
139
+
140
+ estimated = self._estimate_value_size(value)
141
+ self._set_field_sizes[key] = estimated
142
+ self._pending_set_bytes += estimated
143
+ self._set_field_batch[key] = value
144
+
145
+ if not self._in_batch_context:
146
+ self._flush_set_field_batch()
147
+ return
148
+
149
+ if (
150
+ self._batch_size_limit_bytes > 0
151
+ and self._pending_set_bytes >= self._batch_soft_limit_bytes
152
+ ):
153
+ self._perform_batch_operations()
154
+
155
+ def erase_field(
156
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
157
+ ) -> None:
158
+ """Remove specific field.
159
+
160
+ Args:
161
+ obj_uid: Object unique identifier.
162
+ field_name: Field name.
163
+ table: Table name for logical data separation.
164
+ """
165
+ self._erase_field_batch.add((table, obj_uid, field_name))
166
+ if not self._in_batch_context:
167
+ self._flush_erase_field_batch()
168
+
169
+ def erase_obj(self, obj_uid: UID, *, table: str = DEFAULT_TABLE) -> None:
170
+ """Remove entire object and all its fields.
171
+
172
+ Args:
173
+ obj_uid: Object unique identifier.
174
+ table: Table name for logical data separation.
175
+ """
176
+ self._erase_obj_batch.add((table, obj_uid))
177
+ if not self._in_batch_context:
178
+ self._flush_erase_obj_batch()
179
+
180
+ def erase_field_for_all(
181
+ self, field_name: str, *, table: str = DEFAULT_TABLE
182
+ ) -> None:
183
+ """Remove field from all objects.
184
+
185
+ Args:
186
+ field_name: Field name.
187
+ table: Table name for logical data separation.
188
+ """
189
+ self._erase_field_for_all_batch.add((table, field_name))
190
+ if not self._in_batch_context:
191
+ self._flush_erase_field_for_all_batch()
192
+
193
+ def clear(self, *, table: str | None = None) -> None:
194
+ """Clear data from storage.
195
+
196
+ Args:
197
+ table: If provided, clear only this table. If None, clear all data.
198
+ """
199
+ self._set_field_batch.clear()
200
+ self._erase_field_batch.clear()
201
+ self._erase_obj_batch.clear()
202
+ self._erase_field_for_all_batch.clear()
203
+
204
+ self._clear(table=table)
205
+
206
+ def _auto_flush_before_read(self) -> None:
207
+ if self._in_batch_context and self._has_pending_operations:
208
+ self._perform_batch_operations()
209
+
210
+ @property
211
+ def _context_depth(self) -> int:
212
+ """Thread-local context depth for nested batch contexts."""
213
+ return getattr(self._thread_local, "context_depth", 0)
214
+
215
+ @_context_depth.setter
216
+ def _context_depth(self, value: int) -> None:
217
+ """Set thread-local context depth."""
218
+ self._thread_local.context_depth = value
219
+
220
+ def _estimate_value_size(self, value: Any) -> int:
221
+ """Best-effort estimate of serialized size in bytes."""
222
+ if isinstance(value, np.ndarray):
223
+ return int(value.nbytes)
224
+ if self._use_json:
225
+ with suppress(TypeError, ValueError):
226
+ return len(json.dumps(value, ensure_ascii=False).encode("utf-8"))
227
+ return len(pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL))
228
+
229
+ @property
230
+ def _in_batch_context(self) -> bool:
231
+ """True if inside a batch context (with statement)."""
232
+ return self._context_depth > 0
233
+
234
+ @property
235
+ def _has_pending_operations(self) -> bool:
236
+ """True if there are pending buffered operations that require flushing."""
237
+ return bool(
238
+ self._set_field_batch
239
+ or self._erase_field_batch
240
+ or self._erase_obj_batch
241
+ or self._erase_field_for_all_batch
242
+ )
243
+
244
+ def _perform_batch_operations(self) -> None:
245
+ """Execute all pending batch operations."""
246
+ if not self._performing_batch_operations:
247
+ self._performing_batch_operations = True
248
+ try:
249
+ self._flush_set_field_batch()
250
+ self._flush_erase_field_batch()
251
+ self._flush_erase_obj_batch()
252
+ self._flush_erase_field_for_all_batch()
253
+ finally:
254
+ self._performing_batch_operations = False
255
+
256
+ @abstractmethod
257
+ def _has_field(
258
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
259
+ ) -> bool: ...
260
+
261
+ @abstractmethod
262
+ def _get_field(
263
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
264
+ ) -> Any: ...
265
+
266
+ @abstractmethod
267
+ def _get_field_metadata(
268
+ self, obj_uid: UID, field_name: str, *, table: str = DEFAULT_TABLE
269
+ ) -> dict[str, Any] | None: ...
270
+
271
+ @abstractmethod
272
+ def _list_fields(
273
+ self, obj_uid: UID = None, *, table: str = DEFAULT_TABLE
274
+ ) -> list[str]: ...
275
+
276
+ @abstractmethod
277
+ def _list_objs(self, *, table: str = DEFAULT_TABLE) -> list[str]: ...
278
+
279
+ @abstractmethod
280
+ def _list_objs_has_field(
281
+ self, field_name: str, *, table: str = DEFAULT_TABLE
282
+ ) -> list[UID]: ...
283
+
284
+ @abstractmethod
285
+ def _flush_set_field_batch(self) -> None: ...
286
+
287
+ @abstractmethod
288
+ def _flush_erase_field_batch(self) -> None: ...
289
+
290
+ @abstractmethod
291
+ def _flush_erase_obj_batch(self) -> None: ...
292
+
293
+ @abstractmethod
294
+ def _flush_erase_field_for_all_batch(self) -> None: ...
295
+
296
+ @abstractmethod
297
+ def _clear(self, *, table: str | None = None) -> None: ...
298
+
299
+ def __enter__(self) -> Self:
300
+ """Enter batch mode: queue operations and flush them on exit.
301
+
302
+ Supports nested context managers via reference counting. Only the
303
+ outermost entry clears batches; only the outermost exit flushes.
304
+ """
305
+ if self._context_depth == 0:
306
+ self._set_field_batch.clear()
307
+ self._set_field_sizes.clear()
308
+ self._pending_set_bytes = 0
309
+ self._erase_field_batch.clear()
310
+ self._erase_obj_batch.clear()
311
+ self._erase_field_for_all_batch.clear()
312
+ self._context_depth += 1
313
+
314
+ return self
315
+
316
+ def __exit__(
317
+ self,
318
+ exc_type: type[BaseException] | None,
319
+ exc_val: BaseException | None,
320
+ exc_tb: TracebackType | None,
321
+ ) -> None:
322
+ """Exit batch write context manager.
323
+
324
+ Only the outermost exit flushes operations and commits/rollbacks.
325
+ """
326
+ try:
327
+ if self._context_depth == 1:
328
+ self._perform_batch_operations()
329
+ finally:
330
+ self._context_depth = max(0, self._context_depth - 1)
@@ -0,0 +1,122 @@
1
+ """Dependency injection containers for storage management.
2
+
3
+ This module provides dependency injection configuration for storage managers
4
+ using the dependency-injector library. It supports multiple storage backends
5
+ with configurable selection for persistent data storage.
6
+
7
+ Example:
8
+ >>> container = StorageContainer()
9
+ >>> container.config.backend.from_value("hdf5")
10
+ >>> container.config.hdf5.path.from_value("/path/to/storage.h5")
11
+ >>> storage_manager = container.storage_manager()
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from collections.abc import Generator
18
+ from contextlib import contextmanager
19
+
20
+ from dependency_injector import containers, providers
21
+
22
+ from diffract.core.utils.build import build_with_defaults
23
+
24
+ from .hdf5_manager import HDF5StorageManager
25
+ from .hybrid_manager import HybridStorageManager
26
+ from .interface import IStorageManager
27
+ from .ram_manager import RAMStorageManager
28
+ from .sqlite_manager import SQLiteStorageManager
29
+ from .zarr_manager import ZarrStorageManager
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class StorageContainer(containers.DeclarativeContainer):
35
+ """Dependency injection container for storage-related components.
36
+
37
+ Provides configurable storage manager selection between different
38
+ persistent storage backends. Currently supports HDF5 for hierarchical
39
+ data storage with compression and atomic operations.
40
+
41
+ The container manages storage backend lifecycle and configuration,
42
+ ensuring proper initialization and resource management.
43
+
44
+ Attributes:
45
+ config: Configuration provider for storage settings.
46
+ hdf5_manager: Singleton HDF5 storage manager provider.
47
+ storage_manager: Selector for active storage backend.
48
+ """
49
+
50
+ config = providers.Configuration()
51
+
52
+ hdf5_manager = providers.Singleton(
53
+ build_with_defaults,
54
+ HDF5StorageManager,
55
+ config.hdf5.as_(dict),
56
+ )
57
+
58
+ sqlite_manager = providers.Singleton(
59
+ build_with_defaults,
60
+ SQLiteStorageManager,
61
+ config.sqlite.as_(dict),
62
+ )
63
+
64
+ zarr_manager = providers.Singleton(
65
+ build_with_defaults,
66
+ ZarrStorageManager,
67
+ config.zarr.as_(dict),
68
+ )
69
+
70
+ ram_manager = providers.Singleton(RAMStorageManager)
71
+
72
+ # Selectors for hybrid storage backends
73
+ _light_backends = providers.Selector(
74
+ config.hybrid.light,
75
+ sqlite=sqlite_manager,
76
+ ram=ram_manager,
77
+ hdf5=hdf5_manager,
78
+ zarr=zarr_manager,
79
+ )
80
+
81
+ _heavy_backends = providers.Selector(
82
+ config.hybrid.heavy,
83
+ hdf5=hdf5_manager,
84
+ zarr=zarr_manager,
85
+ sqlite=sqlite_manager,
86
+ ram=ram_manager,
87
+ )
88
+
89
+ hybrid_manager = providers.Singleton(
90
+ HybridStorageManager,
91
+ light_storage=_light_backends,
92
+ heavy_storage=_heavy_backends,
93
+ array_threshold=config.hybrid.array_threshold,
94
+ )
95
+
96
+ storage_manager = providers.Selector(
97
+ config.backend,
98
+ hdf5=hdf5_manager,
99
+ ram=ram_manager,
100
+ sqlite=sqlite_manager,
101
+ hybrid=hybrid_manager,
102
+ zarr=zarr_manager,
103
+ )
104
+
105
+ storage_manager_resource = providers.Resource(
106
+ lambda manager: _connect_wrapper(manager),
107
+ storage_manager,
108
+ )
109
+
110
+
111
+ @contextmanager
112
+ def _connect_wrapper(manager: IStorageManager) -> Generator[None, None, None]:
113
+ logger.debug("Init resource: %s", manager.__class__.__name__)
114
+ if hasattr(manager, "connect"):
115
+ logger.debug("Init connection for: %s", manager.__class__.__name__)
116
+ manager.connect()
117
+ try:
118
+ yield manager
119
+ finally:
120
+ if hasattr(manager, "close"):
121
+ logger.debug("Close connection for: %s", manager.__class__.__name__)
122
+ manager.close()