cacheado 1.0.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.
@@ -0,0 +1,187 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from cache_types import _CacheKey
7
+
8
+ if TYPE_CHECKING:
9
+ from cache import Cache
10
+
11
+ from protocols.cache_policy_manager_protocol import ICachePolicyManager
12
+ from protocols.eviction_policy import IEvictionPolicy
13
+
14
+
15
+ class CachePolicyManager(ICachePolicyManager):
16
+ """
17
+ Manages cache maintenance policies (e.g., eviction, cleanup).
18
+
19
+ This class handles background cleanup and delegates eviction logic
20
+ to an injected IEvictionPolicy. Optimized with __slots__ for memory efficiency.
21
+ """
22
+
23
+ __slots__ = ("_cache", "_cleanup_interval", "_policy", "_global_max_size", "_stop_event", "_cleanup_thread")
24
+
25
+ def __init__(
26
+ self, cache_instance: "Cache", cleanup_interval: int, policy: IEvictionPolicy, max_size: Optional[int] = None
27
+ ):
28
+ """
29
+ Initializes the policy manager.
30
+
31
+ Args:
32
+ cache_instance (Cache): The main Cache instance.
33
+ cleanup_interval (int): The interval (in seconds) for the cleanup loop.
34
+ policy (IEvictionPolicy): The injected eviction policy (e.g., LRUPolicy).
35
+ max_size (Optional[int]): The maximum number of items allowed globally.
36
+ """
37
+ self._cache = cache_instance
38
+ self._cleanup_interval = cleanup_interval
39
+ self._policy = policy
40
+ self._global_max_size = max_size
41
+ self._stop_event = threading.Event()
42
+ self._cleanup_thread: Optional[threading.Thread] = None
43
+ logging.info(
44
+ f"CachePolicyManager initialized with policy={policy.__class__.__name__}, "
45
+ f"max_size={max_size}, cleanup_interval={cleanup_interval}s"
46
+ )
47
+
48
+ def start_background_cleanup(self) -> None:
49
+ """
50
+ Starts the background daemon thread for cache cleanup.
51
+ """
52
+ try:
53
+ if self._cleanup_thread is None or not self._cleanup_thread.is_alive():
54
+ self._stop_event.clear()
55
+ self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True, name="CacheCleanupThread")
56
+ self._cleanup_thread.start()
57
+ logging.info("Cache cleanup thread started.")
58
+ except Exception as e:
59
+ logging.error(f"Failed to start cache cleanup thread: {e}")
60
+ raise
61
+
62
+ def stop_background_cleanup(self) -> None:
63
+ """Stops the background cleanup thread gracefully."""
64
+ try:
65
+ if self._cleanup_thread and self._cleanup_thread.is_alive():
66
+ self._stop_event.set()
67
+ self._cleanup_thread.join()
68
+ logging.info("Cache cleanup thread stopped.")
69
+ except Exception as e:
70
+ logging.error(f"Error stopping cache cleanup thread: {e}")
71
+
72
+ def _cleanup_loop(self) -> None:
73
+ """
74
+ The main loop for the garbage collector thread.
75
+
76
+ Periodically scans keys and triggers passive eviction for expired items.
77
+ """
78
+ while not self._stop_event.wait(self._cleanup_interval):
79
+ try:
80
+ if self._cache is None:
81
+ continue
82
+
83
+ all_keys = self._cache._get_all_keys_from_storage()
84
+ if not all_keys:
85
+ continue
86
+
87
+ logging.info(f"Background cleanup: checking {len(all_keys)} keys.")
88
+
89
+ current_time = time.monotonic()
90
+ expired_count = 0
91
+
92
+ for key in all_keys:
93
+ value_tuple = self._cache._get_value_no_lock_from_storage(key)
94
+ if value_tuple and current_time > value_tuple[1]:
95
+ namespace = key[1]
96
+ self._cache._internal_get(key, namespace)
97
+ expired_count += 1
98
+
99
+ if expired_count > 0:
100
+ logging.info(f"Background cleanup: {expired_count} expired keys removed.")
101
+
102
+ except Exception as e:
103
+ logging.error(f"Error in cache cleanup thread: {e}", exc_info=True)
104
+
105
+ def notify_set(self, key: _CacheKey, namespace: str, max_items: Optional[int]) -> Optional[_CacheKey]:
106
+ """
107
+ Delegates 'set' notification to the eviction policy.
108
+
109
+ Args:
110
+ key (_CacheKey): The key that was set.
111
+ namespace (str): The namespace of the key.
112
+ max_items (Optional[int]): The max_items limit for this namespace.
113
+
114
+ Returns:
115
+ Optional[_CacheKey]: A key to evict, or None.
116
+ """
117
+ try:
118
+ return self._policy.notify_set(key, namespace, max_items, self._global_max_size)
119
+ except Exception as e:
120
+ logging.error(f"Error in policy notify_set: {e}")
121
+ return None
122
+
123
+ def notify_get(self, key: _CacheKey, namespace: str) -> None:
124
+ """
125
+ Delegates 'get' notification to the eviction policy.
126
+
127
+ Args:
128
+ key (_CacheKey): The key that was accessed.
129
+ namespace (str): The namespace of the key.
130
+ """
131
+ try:
132
+ self._policy.notify_get(key, namespace)
133
+ except Exception as e:
134
+ logging.error(f"Error in policy notify_get: {e}")
135
+
136
+ def notify_evict(self, key: _CacheKey, namespace: str) -> None:
137
+ """
138
+ Delegates 'evict' notification to the eviction policy.
139
+
140
+ Args:
141
+ key (_CacheKey): The key that was evicted.
142
+ namespace (str): The namespace of the key.
143
+ """
144
+ try:
145
+ self._policy.notify_evict(key, namespace)
146
+ except Exception as e:
147
+ logging.error(f"Error in policy notify_evict: {e}")
148
+
149
+ def notify_clear(self) -> None:
150
+ """Delegates 'clear' notification to the eviction policy."""
151
+ try:
152
+ self._policy.notify_clear()
153
+ except Exception as e:
154
+ logging.error(f"Error in policy notify_clear: {e}")
155
+
156
+ def get_namespace_count(self) -> int:
157
+ """
158
+ Gets the total number of tracked namespaces from the policy.
159
+
160
+ Returns:
161
+ int: The count of namespaces.
162
+ """
163
+ return self._policy.get_namespace_count()
164
+
165
+ def get_global_size(self) -> int:
166
+ """
167
+ Gets the global item count from the policy.
168
+
169
+ Returns:
170
+ int: The global item count.
171
+ """
172
+ return self._policy.get_global_size()
173
+
174
+ @property
175
+ def policy(self) -> IEvictionPolicy:
176
+ """Returns the eviction policy instance."""
177
+ return self._policy
178
+
179
+ @property
180
+ def global_max_size(self) -> Optional[int]:
181
+ """Returns the global maximum cache size."""
182
+ return self._global_max_size
183
+
184
+ @property
185
+ def cleanup_interval(self) -> int:
186
+ """Returns the cleanup interval in seconds."""
187
+ return self._cleanup_interval
File without changes
@@ -0,0 +1,228 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from protocols.scope import IScope
5
+
6
+
7
+ @dataclass
8
+ class ScopeLevel:
9
+ """Represents a level in the scope hierarchy."""
10
+
11
+ name: str
12
+ param_name: str
13
+ children: Optional[List["ScopeLevel"]] = None
14
+
15
+ def __post_init__(self):
16
+ if not self.name:
17
+ raise ValueError("name cannot be empty")
18
+ if self.children is None:
19
+ self.children = []
20
+
21
+
22
+ class ScopeConfig(IScope):
23
+ """
24
+ Hierarchical scope configuration for cache with multiple tree support.
25
+
26
+ Example:
27
+ ``` python
28
+ # Multiple independent trees
29
+ org_tree = ScopeLevel("organization", "org_id", [
30
+ ScopeLevel("user", "user_id")
31
+ ])
32
+ car_tree = ScopeLevel("car", "car_id", [
33
+ ScopeLevel("door", "door_id", [
34
+ ScopeLevel("tire", "tire_id")
35
+ ])
36
+ ])
37
+ config = ScopeConfig([org_tree, car_tree])
38
+ ```
39
+ """
40
+
41
+ __slots__ = ("_root_levels", "_all_levels", "_level_names", "_param_mapping")
42
+
43
+ def __init__(self, root_levels: Optional[List[ScopeLevel]] = None):
44
+ """
45
+ Initializes scope configuration with global as implicit root.
46
+
47
+ Args:
48
+ root_levels: List of global child levels (optional).
49
+ """
50
+ global_level = ScopeLevel("global", "", root_levels or [])
51
+
52
+ self._root_levels = [global_level]
53
+ self._all_levels = self._flatten_levels(self._root_levels)
54
+ self._level_names = [level.name for level in self._all_levels]
55
+ self._param_mapping = {level.name: level.param_name for level in self._all_levels}
56
+
57
+ if len(set(self._level_names)) != len(self._level_names):
58
+ raise ValueError("Scope level names must be unique")
59
+
60
+ def _flatten_levels(self, levels: List[ScopeLevel]) -> List[ScopeLevel]:
61
+ """Flattens the level tree into a list."""
62
+ result = []
63
+ for level in levels:
64
+ result.append(level)
65
+ if level.children:
66
+ result.extend(self._flatten_levels(level.children))
67
+ return result
68
+
69
+ @property
70
+ def root_levels(self) -> List[ScopeLevel]:
71
+ """Returns the configured root scope levels."""
72
+ return self._root_levels.copy()
73
+
74
+ @property
75
+ def all_levels(self) -> List[ScopeLevel]:
76
+ """Returns all scope levels (flattened)."""
77
+ return self._all_levels.copy()
78
+
79
+ @property
80
+ def level_names(self) -> List[str]:
81
+ """Returns the scope level names."""
82
+ return self._level_names.copy()
83
+
84
+ def get_param_name(self, level_name: str) -> str:
85
+ """
86
+ Returns the parameter name for a specific level.
87
+
88
+ Args:
89
+ level_name: Name of the scope level.
90
+
91
+ Returns:
92
+ Corresponding parameter name.
93
+
94
+ Raises:
95
+ ValueError: If the level doesn't exist.
96
+ """
97
+ if level_name not in self._param_mapping:
98
+ raise ValueError(f"Unknown scope level: {level_name}")
99
+ return self._param_mapping[level_name]
100
+
101
+ def build_scope_path(self, scope_params: Dict[str, Any]) -> str:
102
+ """
103
+ Builds the scope path based on provided parameters.
104
+
105
+ Args:
106
+ scope_params: Dictionary with scope parameters.
107
+
108
+ Returns:
109
+ String representing the hierarchical scope path.
110
+ """
111
+
112
+ def build_path_recursive(levels: List[ScopeLevel], path_parts: List[str]) -> List[str]:
113
+ for level in levels:
114
+ if level.name == "global":
115
+ if level.children:
116
+ return build_path_recursive(level.children, path_parts)
117
+ return path_parts
118
+
119
+ param_value = scope_params.get(level.param_name)
120
+ if param_value is not None:
121
+ new_path = path_parts + [f"{level.name}:{param_value}"]
122
+ if level.children:
123
+ child_path = build_path_recursive(level.children, new_path)
124
+ if len(child_path) > len(new_path):
125
+ return child_path
126
+ return new_path
127
+ return path_parts
128
+
129
+ path_parts = build_path_recursive(self._root_levels, [])
130
+ return "/".join(path_parts) if path_parts else "global"
131
+
132
+ def validate_scope_params(self, target_level: str, scope_params: Dict[str, Any]) -> None:
133
+ """
134
+ Validates that required parameters are present for the target level.
135
+
136
+ Args:
137
+ target_level: Desired scope level.
138
+ scope_params: Provided parameters.
139
+
140
+ Raises:
141
+ ValueError: If mandatory parameters are missing.
142
+ """
143
+ if target_level == "global":
144
+ return
145
+
146
+ if target_level not in self._level_names:
147
+ raise ValueError(f"Unknown scope level: {target_level}")
148
+
149
+ path_to_target = self._find_path_to_level(target_level)
150
+ if not path_to_target:
151
+ raise ValueError(f"Cannot find path to scope level: {target_level}")
152
+
153
+ for level in path_to_target:
154
+ if level.param_name and (level.param_name not in scope_params or scope_params[level.param_name] is None):
155
+ raise ValueError(f"Missing required parameter '{level.param_name}' for scope level '{level.name}'")
156
+
157
+ def _find_path_to_level(self, target_level: str) -> Optional[List[ScopeLevel]]:
158
+ """Finds the hierarchical path to a specific level."""
159
+
160
+ def search_recursive(levels: List[ScopeLevel], path: List[ScopeLevel]) -> Optional[List[ScopeLevel]]:
161
+ for level in levels:
162
+ current_path = path + [level]
163
+ if level.name == target_level:
164
+ return current_path
165
+ if level.children:
166
+ result = search_recursive(level.children, current_path)
167
+ if result:
168
+ return result
169
+ return None
170
+
171
+ return search_recursive(self._root_levels, [])
172
+
173
+ def get_parent_scope_path(self, scope_path: str) -> Optional[str]:
174
+ """
175
+ Returns the parent scope path.
176
+
177
+ Args:
178
+ scope_path: Current scope path.
179
+
180
+ Returns:
181
+ Parent scope path or None if global.
182
+ """
183
+ if scope_path == "global":
184
+ return None
185
+
186
+ parts = scope_path.split("/")
187
+ if len(parts) <= 1:
188
+ return "global"
189
+
190
+ return "/".join(parts[:-1])
191
+
192
+ def is_descendant_of(self, child_path: str, parent_path: str) -> bool:
193
+ """
194
+ Checks if one scope is a descendant of another.
195
+
196
+ Args:
197
+ child_path: Child scope path.
198
+ parent_path: Parent scope path.
199
+
200
+ Returns:
201
+ True if child_path is descendant of parent_path.
202
+ """
203
+ if parent_path == "global":
204
+ return True
205
+
206
+ if child_path == "global":
207
+ return False
208
+
209
+ return child_path.startswith(parent_path + "/") or child_path == parent_path
210
+
211
+ def get_scope_tree_for_level(self, level_name: str) -> Optional[ScopeLevel]:
212
+ """Returns the root tree that contains the specified level."""
213
+
214
+ def find_root(levels: List[ScopeLevel], target: str) -> Optional[ScopeLevel]:
215
+ for root in levels:
216
+ if self._level_exists_in_tree(root, target):
217
+ return root
218
+ return None
219
+
220
+ return find_root(self._root_levels, level_name)
221
+
222
+ def _level_exists_in_tree(self, root: ScopeLevel, target: str) -> bool:
223
+ """Checks if a level exists in the tree."""
224
+ if root.name == target:
225
+ return True
226
+ if root.children:
227
+ return any(self._level_exists_in_tree(child, target) for child in root.children)
228
+ return False
cache_types.py ADDED
@@ -0,0 +1,6 @@
1
+ from typing import Any, Callable, Tuple, TypeVar, Union
2
+
3
+ _CacheKey = Tuple[str, str, Tuple[Any, ...]]
4
+ _CacheValue = Tuple[Any, float]
5
+ _FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
6
+ _CacheScope = Union[str, Tuple[str, ...]]