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