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,93 @@
|
|
|
1
|
+
"""Dependency injection containers for cache management.
|
|
2
|
+
|
|
3
|
+
This module provides dependency injection configuration for cache managers
|
|
4
|
+
using the dependency-injector library. It supports multiple cache backends
|
|
5
|
+
with configurable selection.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> container = CacheContainer()
|
|
9
|
+
>>> container.config.backend.from_value("redis")
|
|
10
|
+
>>> cache_manager = container.cache_manager()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from dependency_injector import containers, providers
|
|
21
|
+
|
|
22
|
+
from diffract.core.utils.build import build_with_defaults
|
|
23
|
+
|
|
24
|
+
from .redis_manager import RedisLRUCacheManager
|
|
25
|
+
from .simple_manager import SimpleLRUCacheManager
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .interface import ICacheManager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CacheContainer(containers.DeclarativeContainer):
|
|
35
|
+
"""Dependency injection container for cache-related components.
|
|
36
|
+
|
|
37
|
+
Provides configurable cache manager selection between Redis and simple
|
|
38
|
+
in-memory implementations. Configuration is handled through the config
|
|
39
|
+
provider with backend selection and specific backend settings.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
config: Configuration provider for cache settings.
|
|
43
|
+
redis_manager: Singleton Redis cache manager provider.
|
|
44
|
+
simple_manager: Singleton simple cache manager provider.
|
|
45
|
+
cache_manager: Selector for active cache backend.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
config = providers.Configuration()
|
|
49
|
+
|
|
50
|
+
redis_manager = providers.Singleton(
|
|
51
|
+
build_with_defaults,
|
|
52
|
+
RedisLRUCacheManager,
|
|
53
|
+
config.redis.as_(dict),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
simple_manager = providers.Singleton(
|
|
57
|
+
build_with_defaults,
|
|
58
|
+
SimpleLRUCacheManager,
|
|
59
|
+
config.simple.as_(dict),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
none_manager = providers.Singleton(lambda: None)
|
|
63
|
+
|
|
64
|
+
cache_manager = providers.Selector(
|
|
65
|
+
config.backend,
|
|
66
|
+
redis=redis_manager,
|
|
67
|
+
simple=simple_manager,
|
|
68
|
+
none=none_manager,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
cache_manager_resource = providers.Resource(
|
|
72
|
+
lambda manager: _connect_wrapper(manager),
|
|
73
|
+
cache_manager,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@contextmanager
|
|
78
|
+
def _connect_wrapper(manager: ICacheManager | None) -> Iterator[ICacheManager | None]:
|
|
79
|
+
if manager is None:
|
|
80
|
+
logger.debug("Cache disabled (backend=none)")
|
|
81
|
+
yield None
|
|
82
|
+
return
|
|
83
|
+
else:
|
|
84
|
+
logger.debug("Init resource: %s", manager.__class__.__name__)
|
|
85
|
+
if hasattr(manager, "connect"):
|
|
86
|
+
logger.debug("Init connection for: %s", manager.__class__.__name__)
|
|
87
|
+
manager.connect()
|
|
88
|
+
try:
|
|
89
|
+
yield manager
|
|
90
|
+
finally:
|
|
91
|
+
if hasattr(manager, "close"):
|
|
92
|
+
logger.debug("Close connection for: %s", manager.__class__.__name__)
|
|
93
|
+
manager.close()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Cache manager interface and protocols.
|
|
2
|
+
|
|
3
|
+
This module defines the core protocol and type aliases that all cache
|
|
4
|
+
manager implementations must follow. It provides a framework-agnostic
|
|
5
|
+
contract for cache operations.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> cache_manager: ICacheManager = get_cache_manager()
|
|
9
|
+
>>> cache_manager.set_field("obj123", "result", computed_value)
|
|
10
|
+
>>> value = cache_manager.get_field("obj123", "result")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
type UID = str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class ICacheManager(Protocol):
|
|
22
|
+
"""Protocol defining the cache manager interface.
|
|
23
|
+
|
|
24
|
+
Provides a unified interface for cache operations across different
|
|
25
|
+
backends. All cache manager implementations must implement these methods
|
|
26
|
+
to ensure consistent behavior.
|
|
27
|
+
|
|
28
|
+
The cache is organized as a two-level structure where objects are
|
|
29
|
+
identified by UID and contain named fields with arbitrary values.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def has_field(self, obj_uid: UID, field_name: str) -> bool:
|
|
33
|
+
"""Check if an object has a specific field in cache.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
obj_uid: Unique identifier for the cached object.
|
|
37
|
+
field_name: Name of the field to check.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if the field exists and is not expired, False otherwise.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def get_field(self, obj_uid: UID, field_name: str) -> Any:
|
|
45
|
+
"""Retrieve a field value from cache.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
obj_uid: Unique identifier for the cached object.
|
|
49
|
+
field_name: Name of the field to retrieve.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The cached value or None if field doesn't exist or is expired.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def list_objs_has_field(self, field_name: str) -> list[UID]:
|
|
57
|
+
"""List all object UIDs that have a specific field.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
field_name: Name of the field to search for.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of UIDs for objects that have the specified field.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def set_field(self, obj_uid: UID, field_name: str, value: Any) -> None:
|
|
68
|
+
"""Store a field value in cache.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
obj_uid: Unique identifier for the cached object.
|
|
72
|
+
field_name: Name of the field to store.
|
|
73
|
+
value: Value to cache (must be pickle-serializable).
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
Exception: If storage operation fails.
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def erase_field(self, obj_uid: UID, field_name: str) -> None:
|
|
81
|
+
"""Remove a specific field from cache.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
obj_uid: Unique identifier for the cached object.
|
|
85
|
+
field_name: Name of the field to remove.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
Exception: If removal operation fails.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def erase_field_for_all(self, field_name: str) -> None:
|
|
93
|
+
"""Remove a field from all cached objects.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
field_name: Name of the field to remove from all objects.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
Exception: If removal operation fails.
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def clear(self) -> None:
|
|
104
|
+
"""Remove all cached data.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
Exception: If clear operation fails.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def get_available_bytes(self) -> int | None:
|
|
112
|
+
"""Return estimated available cache capacity in bytes.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Available bytes, or None if capacity cannot be determined.
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Redis-based LRU cache manager implementation.
|
|
2
|
+
|
|
3
|
+
This module provides a Redis-backed cache manager that implements the
|
|
4
|
+
ICacheManager protocol. It uses Redis as a RAM-only volatile cache with
|
|
5
|
+
LRU eviction and configurable memory limits.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- RAM-only storage with LRU eviction
|
|
9
|
+
- Configurable memory limits and TTL
|
|
10
|
+
- Pickle serialization for arbitrary Python objects
|
|
11
|
+
- Connection pooling and error handling
|
|
12
|
+
- Health monitoring and memory usage tracking
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> manager = RedisLRUCacheManager(host="localhost", max_memory_mb=512)
|
|
16
|
+
>>> manager.set_field("user123", "profile", user_data)
|
|
17
|
+
>>> profile = manager.get_field("user123", "profile")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import logging
|
|
24
|
+
import pickle
|
|
25
|
+
from types import TracebackType
|
|
26
|
+
from typing import Any, Self
|
|
27
|
+
|
|
28
|
+
import diffract.core.utils.imports as import_utils
|
|
29
|
+
from diffract.core.utils.exceptions import format_exception_message
|
|
30
|
+
|
|
31
|
+
from .interface import UID, ICacheManager
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
if not import_utils.is_available("redis"):
|
|
36
|
+
logger.debug("Redis not available, disabling Redis cache manager")
|
|
37
|
+
|
|
38
|
+
class RedisLRUCacheManager(ICacheManager):
|
|
39
|
+
"""Stub implementation when Redis is not available.
|
|
40
|
+
|
|
41
|
+
Raises ImportError when instantiated to indicate Redis dependency
|
|
42
|
+
is missing.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __new__(cls, *_args: Any, **_kwargs: Any) -> Self:
|
|
46
|
+
"""Raise ImportError because the optional dependency is missing."""
|
|
47
|
+
msg = "Redis package not available"
|
|
48
|
+
raise ImportError(msg)
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
redis = import_utils.require("redis")
|
|
52
|
+
RedisConnectionError = redis.exceptions.ConnectionError
|
|
53
|
+
RedisError = redis.exceptions.RedisError
|
|
54
|
+
|
|
55
|
+
class RedisLRUCacheManager(ICacheManager):
|
|
56
|
+
"""Redis-based LRU cache manager with RAM-only storage.
|
|
57
|
+
|
|
58
|
+
Implements ICacheManager using Redis as the backend storage with
|
|
59
|
+
LRU eviction policy and configurable memory limits. Designed for
|
|
60
|
+
high-performance caching with minimal persistence overhead.
|
|
61
|
+
|
|
62
|
+
The manager configures Redis for RAM-only operation by disabling
|
|
63
|
+
RDB snapshots and AOF logging, focusing on speed over durability.
|
|
64
|
+
All cached objects are serialized using pickle.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
_key_prefix: Prefix for all Redis keys to avoid collisions.
|
|
68
|
+
_ttl_seconds: Time-to-live for cache entries in seconds.
|
|
69
|
+
_max_memory_mb: Maximum memory usage in megabytes.
|
|
70
|
+
_pool: Redis connection pool for efficient connection management.
|
|
71
|
+
_redis: Redis client instance.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
host: str = "localhost",
|
|
77
|
+
port: int = 6379,
|
|
78
|
+
db: int = 0,
|
|
79
|
+
password: str | None = None,
|
|
80
|
+
*,
|
|
81
|
+
max_memory_mb: int = 256,
|
|
82
|
+
ttl_seconds: int | None = None,
|
|
83
|
+
key_prefix: str = "diffract:cache:",
|
|
84
|
+
socket_timeout: float = 5.0,
|
|
85
|
+
socket_connect_timeout: float = 5.0,
|
|
86
|
+
retry_on_timeout: bool = True,
|
|
87
|
+
max_connections: int = 64,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Initialize Redis LRU cache manager.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
host: Redis server hostname.
|
|
93
|
+
port: Redis server port number.
|
|
94
|
+
db: Redis database number to use.
|
|
95
|
+
password: Redis authentication password.
|
|
96
|
+
max_memory_mb: Maximum memory usage in MB (must be positive).
|
|
97
|
+
ttl_seconds: Time-to-live for entries in seconds (None = no expiry).
|
|
98
|
+
key_prefix: Prefix for all Redis keys.
|
|
99
|
+
socket_timeout: Socket timeout in seconds.
|
|
100
|
+
socket_connect_timeout: Connection timeout in seconds.
|
|
101
|
+
retry_on_timeout: Whether to retry on timeout.
|
|
102
|
+
max_connections: Maximum connections in pool.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If max_memory_mb is not positive.
|
|
106
|
+
RedisConnectionError: If Redis connection fails.
|
|
107
|
+
"""
|
|
108
|
+
if max_memory_mb <= 0:
|
|
109
|
+
msg = "max_memory_mb must be positive"
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
self._key_prefix = key_prefix
|
|
113
|
+
self._ttl_seconds = ttl_seconds
|
|
114
|
+
self._max_memory_mb = max_memory_mb
|
|
115
|
+
|
|
116
|
+
self._pool = redis.ConnectionPool(
|
|
117
|
+
host=host,
|
|
118
|
+
port=port,
|
|
119
|
+
db=db,
|
|
120
|
+
password=password,
|
|
121
|
+
socket_timeout=socket_timeout,
|
|
122
|
+
socket_connect_timeout=socket_connect_timeout,
|
|
123
|
+
retry_on_timeout=retry_on_timeout,
|
|
124
|
+
max_connections=max_connections,
|
|
125
|
+
decode_responses=False,
|
|
126
|
+
)
|
|
127
|
+
self._redis = redis.Redis(connection_pool=self._pool)
|
|
128
|
+
|
|
129
|
+
self._initialize_redis()
|
|
130
|
+
|
|
131
|
+
def _initialize_redis(self) -> None:
|
|
132
|
+
"""Configure Redis for RAM-only operation with LRU eviction.
|
|
133
|
+
|
|
134
|
+
Sets up Redis configuration for optimal caching performance by
|
|
135
|
+
disabling persistence and configuring memory limits.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
RedisConnectionError: If Redis initialization fails.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
self._redis.ping()
|
|
142
|
+
|
|
143
|
+
# Disable persistence for RAM-only operation
|
|
144
|
+
try:
|
|
145
|
+
self._redis.config_set("save", "")
|
|
146
|
+
self._redis.config_set("appendonly", "no")
|
|
147
|
+
except RedisError as e:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"Failed to disable Redis persistence: %s",
|
|
150
|
+
format_exception_message(e),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Configure memory limits and LRU eviction
|
|
154
|
+
self._redis.config_set("maxmemory", self._max_memory_mb * 1024 * 1024)
|
|
155
|
+
self._redis.config_set("maxmemory-policy", "allkeys-lru")
|
|
156
|
+
|
|
157
|
+
logger.info(
|
|
158
|
+
"Redis cache initialized (max_memory=%d MB, ttl=%s)",
|
|
159
|
+
self._max_memory_mb,
|
|
160
|
+
self._ttl_seconds,
|
|
161
|
+
)
|
|
162
|
+
except RedisError as e:
|
|
163
|
+
msg = f"Redis initialization failed: {format_exception_message(e)}"
|
|
164
|
+
raise RedisConnectionError(msg) from e
|
|
165
|
+
|
|
166
|
+
def _make_key(self, obj_uid: UID, field_name: str) -> bytes:
|
|
167
|
+
"""Generate Redis key for object field.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
obj_uid: Unique identifier for the cached object.
|
|
171
|
+
field_name: Name of the field.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Encoded Redis key as bytes.
|
|
175
|
+
"""
|
|
176
|
+
return f"{self._key_prefix}{obj_uid}:{field_name}".encode()
|
|
177
|
+
|
|
178
|
+
def has_field(self, obj_uid: UID, field_name: str) -> bool:
|
|
179
|
+
"""Check if an object has a specific field in cache.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
obj_uid: Unique identifier for the cached object.
|
|
183
|
+
field_name: Name of the field to check.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if the field exists, False otherwise or on error.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
return bool(self._redis.exists(self._make_key(obj_uid, field_name)))
|
|
190
|
+
except RedisError as e:
|
|
191
|
+
logger.debug(
|
|
192
|
+
"Redis has_field error for %s/%s: %s",
|
|
193
|
+
obj_uid,
|
|
194
|
+
field_name,
|
|
195
|
+
format_exception_message(e),
|
|
196
|
+
exc_info=True,
|
|
197
|
+
)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def get_field(self, obj_uid: UID, field_name: str) -> Any:
|
|
201
|
+
"""Retrieve a field value from cache.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
obj_uid: Unique identifier for the cached object.
|
|
205
|
+
field_name: Name of the field to retrieve.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The cached value or None if field doesn't exist or on error.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
key = self._make_key(obj_uid, field_name)
|
|
212
|
+
data = self._redis.get(key)
|
|
213
|
+
if data is None:
|
|
214
|
+
return None
|
|
215
|
+
try:
|
|
216
|
+
return pickle.loads(data) # noqa: S301
|
|
217
|
+
except (
|
|
218
|
+
ModuleNotFoundError,
|
|
219
|
+
ImportError,
|
|
220
|
+
AttributeError,
|
|
221
|
+
ValueError,
|
|
222
|
+
pickle.UnpicklingError,
|
|
223
|
+
EOFError,
|
|
224
|
+
):
|
|
225
|
+
# Cache may contain pickles produced by a different Python/numpy
|
|
226
|
+
# version. Treat as a cache miss and delete the corrupted entry.
|
|
227
|
+
with contextlib.suppress(RedisError):
|
|
228
|
+
self._redis.delete(key)
|
|
229
|
+
return None
|
|
230
|
+
except RedisError as e:
|
|
231
|
+
logger.debug(
|
|
232
|
+
"Redis get_field error for %s/%s: %s",
|
|
233
|
+
obj_uid,
|
|
234
|
+
field_name,
|
|
235
|
+
format_exception_message(e),
|
|
236
|
+
exc_info=True,
|
|
237
|
+
)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def list_objs_has_field(self, field_name: str) -> list[UID]:
|
|
241
|
+
"""List all object UIDs that have a specific field.
|
|
242
|
+
|
|
243
|
+
Uses Redis SCAN command for non-blocking iteration over keys.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
field_name: Name of the field to search for.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of UIDs for objects that have the specified field.
|
|
250
|
+
"""
|
|
251
|
+
prefix = self._key_prefix.encode("utf-8")
|
|
252
|
+
suffix = f":{field_name}".encode()
|
|
253
|
+
pattern = prefix + b"*" + suffix
|
|
254
|
+
|
|
255
|
+
uids: list[str] = []
|
|
256
|
+
cursor: int = 0
|
|
257
|
+
try:
|
|
258
|
+
while True:
|
|
259
|
+
cursor, keys = self._redis.scan(
|
|
260
|
+
cursor=cursor, match=pattern, count=1000
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
pre_len, suf_len = len(prefix), len(suffix)
|
|
264
|
+
|
|
265
|
+
for k in keys:
|
|
266
|
+
if k.startswith(prefix) and k.endswith(suffix):
|
|
267
|
+
uid_bytes = k[pre_len:-suf_len]
|
|
268
|
+
if uid_bytes:
|
|
269
|
+
uids.append(uid_bytes.decode("utf-8"))
|
|
270
|
+
|
|
271
|
+
if cursor == 0:
|
|
272
|
+
break
|
|
273
|
+
except RedisError as e:
|
|
274
|
+
logger.debug(
|
|
275
|
+
"Redis list_objs_has_field error for %s: %s",
|
|
276
|
+
field_name,
|
|
277
|
+
format_exception_message(e),
|
|
278
|
+
exc_info=True,
|
|
279
|
+
)
|
|
280
|
+
return []
|
|
281
|
+
else:
|
|
282
|
+
return uids
|
|
283
|
+
|
|
284
|
+
def set_field(self, obj_uid: UID, field_name: str, value: Any) -> None:
|
|
285
|
+
"""Store a field value in cache.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
obj_uid: Unique identifier for the cached object.
|
|
289
|
+
field_name: Name of the field to store.
|
|
290
|
+
value: Value to cache (must be pickle-serializable).
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
RedisError: If storage operation fails.
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
key = self._make_key(obj_uid, field_name)
|
|
297
|
+
payload = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
|
|
298
|
+
|
|
299
|
+
if self._ttl_seconds is not None:
|
|
300
|
+
self._redis.setex(key, self._ttl_seconds, payload)
|
|
301
|
+
else:
|
|
302
|
+
self._redis.set(key, payload)
|
|
303
|
+
|
|
304
|
+
except RedisError:
|
|
305
|
+
logger.exception("Redis set_field error")
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
def erase_field(self, obj_uid: UID, field_name: str) -> None:
|
|
309
|
+
"""Remove a specific field from cache.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
obj_uid: Unique identifier for the cached object.
|
|
313
|
+
field_name: Name of the field to remove.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
RedisError: If removal operation fails.
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
self._redis.delete(self._make_key(obj_uid, field_name))
|
|
320
|
+
except RedisError:
|
|
321
|
+
logger.exception("Redis erase_field error")
|
|
322
|
+
raise
|
|
323
|
+
|
|
324
|
+
def erase_field_for_all(self, field_name: str) -> None:
|
|
325
|
+
"""Remove a field from all cached objects.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
field_name: Name of the field to remove from all objects.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
RedisError: If removal operation fails.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
prefix = self._key_prefix.encode("utf-8")
|
|
335
|
+
suffix = f":{field_name}".encode()
|
|
336
|
+
pattern = prefix + b"*" + suffix
|
|
337
|
+
cursor: int = 0
|
|
338
|
+
keys_to_delete: list[bytes] = []
|
|
339
|
+
while True:
|
|
340
|
+
cursor, keys = self._redis.scan(
|
|
341
|
+
cursor=cursor, match=pattern, count=1000
|
|
342
|
+
)
|
|
343
|
+
keys_to_delete.extend(keys)
|
|
344
|
+
if cursor == 0:
|
|
345
|
+
break
|
|
346
|
+
if keys_to_delete:
|
|
347
|
+
self._redis.delete(*keys_to_delete)
|
|
348
|
+
except RedisError:
|
|
349
|
+
logger.exception("Redis erase_field_for_all error")
|
|
350
|
+
raise
|
|
351
|
+
|
|
352
|
+
def clear(self) -> None:
|
|
353
|
+
"""Remove all cached data with this manager's prefix.
|
|
354
|
+
|
|
355
|
+
Uses batched deletion to avoid blocking Redis for large datasets.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
RedisError: If clear operation fails.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
pattern = f"{self._key_prefix}*".encode()
|
|
362
|
+
|
|
363
|
+
cursor: int = 0
|
|
364
|
+
batch: list[bytes] = []
|
|
365
|
+
delete_batch_size = 5000
|
|
366
|
+
while True:
|
|
367
|
+
cursor, keys = self._redis.scan(
|
|
368
|
+
cursor=cursor, match=pattern, count=2000
|
|
369
|
+
)
|
|
370
|
+
batch.extend(keys)
|
|
371
|
+
if len(batch) >= delete_batch_size:
|
|
372
|
+
self._redis.delete(*batch)
|
|
373
|
+
batch.clear()
|
|
374
|
+
if cursor == 0:
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
if batch:
|
|
378
|
+
self._redis.delete(*batch)
|
|
379
|
+
except RedisError:
|
|
380
|
+
logger.exception("Redis clear error")
|
|
381
|
+
raise
|
|
382
|
+
|
|
383
|
+
def get_memory_usage(self) -> dict[str, Any]:
|
|
384
|
+
"""Get Redis memory usage statistics.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Dictionary with memory usage metrics including used memory,
|
|
388
|
+
max memory, evicted keys, and fragmentation ratio.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
info = self._redis.info("memory")
|
|
392
|
+
used = float(info.get("used_memory", 0))
|
|
393
|
+
return {
|
|
394
|
+
"used_memory_mb": used / (1024 * 1024),
|
|
395
|
+
"max_memory_mb": float(self._max_memory_mb),
|
|
396
|
+
"evicted_keys": info.get("evicted_keys"),
|
|
397
|
+
"mem_fragmentation_ratio": info.get("mem_fragmentation_ratio"),
|
|
398
|
+
}
|
|
399
|
+
except RedisError as e:
|
|
400
|
+
logger.warning(
|
|
401
|
+
"Redis get_memory_usage error: %s",
|
|
402
|
+
format_exception_message(e),
|
|
403
|
+
exc_info=True,
|
|
404
|
+
)
|
|
405
|
+
return {}
|
|
406
|
+
|
|
407
|
+
def get_available_bytes(self) -> int | None:
|
|
408
|
+
"""Return estimated available cache capacity in bytes."""
|
|
409
|
+
info = self.get_memory_usage()
|
|
410
|
+
if not info:
|
|
411
|
+
return None
|
|
412
|
+
used = info.get("used_memory_mb", 0) * 1024 * 1024
|
|
413
|
+
max_mem = info.get("max_memory_mb", 0) * 1024 * 1024
|
|
414
|
+
return max(0, int(max_mem - used))
|
|
415
|
+
|
|
416
|
+
def health_check(self) -> dict[str, Any]:
|
|
417
|
+
"""Perform Redis health check.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Dictionary with health status and error details if unhealthy.
|
|
421
|
+
"""
|
|
422
|
+
try:
|
|
423
|
+
ok = self._redis.ping()
|
|
424
|
+
except RedisError as e:
|
|
425
|
+
return {
|
|
426
|
+
"status": "unhealthy",
|
|
427
|
+
"error": format_exception_message(e),
|
|
428
|
+
}
|
|
429
|
+
else:
|
|
430
|
+
return {"status": "healthy" if ok else "unhealthy"}
|
|
431
|
+
|
|
432
|
+
def close(self) -> None:
|
|
433
|
+
"""Close Redis connection pool.
|
|
434
|
+
|
|
435
|
+
Safely disconnects all connections in the pool. Should be called
|
|
436
|
+
when the cache manager is no longer needed.
|
|
437
|
+
"""
|
|
438
|
+
with contextlib.suppress(Exception):
|
|
439
|
+
self._pool.disconnect()
|
|
440
|
+
|
|
441
|
+
def __enter__(self) -> Self:
|
|
442
|
+
"""Context manager entry.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Self for use in with statements.
|
|
446
|
+
"""
|
|
447
|
+
return self
|
|
448
|
+
|
|
449
|
+
def __exit__(
|
|
450
|
+
self,
|
|
451
|
+
exc_type: type[BaseException] | None,
|
|
452
|
+
exc: BaseException | None,
|
|
453
|
+
tb: TracebackType | None,
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Context manager exit.
|
|
456
|
+
|
|
457
|
+
Automatically closes the connection pool when exiting context.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
exc_type: Exception type if an exception occurred.
|
|
461
|
+
exc: Exception instance if an exception occurred.
|
|
462
|
+
tb: Traceback if an exception occurred.
|
|
463
|
+
"""
|
|
464
|
+
self.close()
|