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,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()
|