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,130 @@
1
+ import threading
2
+ from collections import OrderedDict, defaultdict
3
+ from typing import DefaultDict, Optional
4
+
5
+ from cache_types import _CacheKey
6
+ from protocols.eviction_policy import IEvictionPolicy
7
+
8
+
9
+ class LRUEvictionPolicy(IEvictionPolicy):
10
+ """
11
+ Implements IEvictionPolicy using a thread-safe Least Recently Used (LRU) strategy.
12
+ Optimized with __slots__ for memory efficiency.
13
+ """
14
+
15
+ __slots__ = ("_lock", "_lru_tracker", "_namespaced_lru_trackers")
16
+
17
+ def __init__(self):
18
+ """Initializes the LRU policy trackers and lock."""
19
+ self._lock = threading.Lock()
20
+ self._lru_tracker: OrderedDict[_CacheKey, None] = OrderedDict()
21
+ self._namespaced_lru_trackers: DefaultDict[str, OrderedDict[_CacheKey, None]] = defaultdict(OrderedDict)
22
+
23
+ def notify_set(
24
+ self, key: _CacheKey, namespace: str, max_items: Optional[int], global_max_size: Optional[int]
25
+ ) -> Optional[_CacheKey]:
26
+ """
27
+ Adds an item to LRU trackers and evicts an old item if limits are hit.
28
+
29
+ Logic:
30
+ 1. Adds the new key to both global and namespace-specific LRU trackers
31
+ 2. Checks namespace limit first - if exceeded, evicts oldest item from namespace
32
+ 3. If no namespace eviction, checks global limit - if exceeded, evicts globally oldest item
33
+ 4. Removes evicted key from all relevant trackers to maintain consistency
34
+
35
+ Args:
36
+ key (_CacheKey): The key that was set.
37
+ namespace (str): The namespace of the key.
38
+ max_items (Optional[int]): The max_items limit for this namespace.
39
+ global_max_size (Optional[int]): The global max_size limit.
40
+
41
+ Returns:
42
+ Optional[_CacheKey]: The key to evict, or None.
43
+ """
44
+ with self._lock:
45
+ self._lru_tracker[key] = None
46
+ if namespace:
47
+ self._namespaced_lru_trackers[namespace][key] = None
48
+
49
+ key_to_evict: Optional[_CacheKey] = None
50
+
51
+ if max_items is not None:
52
+ ns_tracker = self._namespaced_lru_trackers[namespace]
53
+ if len(ns_tracker) > max_items:
54
+ try:
55
+ key_to_evict, _ = ns_tracker.popitem(last=False)
56
+ except (KeyError, Exception):
57
+ pass
58
+
59
+ if key_to_evict is None and global_max_size is not None:
60
+ if len(self._lru_tracker) > global_max_size:
61
+ try:
62
+ key_to_evict, _ = self._lru_tracker.popitem(last=False)
63
+ except KeyError:
64
+ pass
65
+
66
+ if key_to_evict:
67
+ self._lru_tracker.pop(key_to_evict, None)
68
+
69
+ evicted_ns = key_to_evict[1]
70
+ if evicted_ns in self._namespaced_lru_trackers:
71
+ self._namespaced_lru_trackers[evicted_ns].pop(key_to_evict, None)
72
+
73
+ return key_to_evict
74
+
75
+ def notify_get(self, key: _CacheKey, namespace: str) -> None:
76
+ """
77
+ Moves the accessed item to the end (MRU) of the LRU trackers.
78
+
79
+ Args:
80
+ key (_CacheKey): The key that was accessed.
81
+ namespace (str): The namespace of the key.
82
+ """
83
+ with self._lock:
84
+ try:
85
+ self._lru_tracker.move_to_end(key)
86
+ if namespace in self._namespaced_lru_trackers:
87
+ self._namespaced_lru_trackers[namespace].move_to_end(key)
88
+ except (KeyError, Exception):
89
+ pass
90
+
91
+ def notify_evict(self, key: _CacheKey, namespace: str) -> None:
92
+ """
93
+ Removes an item from all LRU trackers.
94
+
95
+ Args:
96
+ key (_CacheKey): The key that was evicted.
97
+ namespace (str): The namespace of the key.
98
+ """
99
+ with self._lock:
100
+ self._lru_tracker.pop(key, None)
101
+ if namespace in self._namespaced_lru_trackers:
102
+ self._namespaced_lru_trackers[namespace].pop(key, None)
103
+ if not self._namespaced_lru_trackers[namespace]:
104
+ del self._namespaced_lru_trackers[namespace]
105
+
106
+ def notify_clear(self) -> None:
107
+ """Clears all LRU trackers."""
108
+ with self._lock:
109
+ self._lru_tracker.clear()
110
+ self._namespaced_lru_trackers.clear()
111
+
112
+ def get_namespace_count(self) -> int:
113
+ """
114
+ Returns the total number of tracked namespaces.
115
+
116
+ Returns:
117
+ int: The count of namespaces.
118
+ """
119
+ with self._lock:
120
+ return len(self._namespaced_lru_trackers)
121
+
122
+ def get_global_size(self) -> int:
123
+ """
124
+ Returns the total number of items tracked by the policy.
125
+
126
+ Returns:
127
+ int: The global item count.
128
+ """
129
+ with self._lock:
130
+ return len(self._lru_tracker)
protocols/__init__.py ADDED
File without changes
protocols/cache.py ADDED
@@ -0,0 +1,183 @@
1
+ from typing import Any, Awaitable, Callable, Dict, Optional, Protocol, Union
2
+
3
+ from cache_types import _CacheScope, _FuncT
4
+ from protocols.eviction_policy import IEvictionPolicy
5
+ from protocols.storage_provider import IStorageProvider
6
+
7
+
8
+ class ICache(Protocol):
9
+ """
10
+ Interface (Protocol) defining the public API for the Cache.
11
+
12
+ This allows for Dependency Injection and testability, enabling
13
+ consumers to depend on this interface rather than the concrete
14
+ Singleton implementation.
15
+ """
16
+
17
+ def get(
18
+ self, key: Any, scope: _CacheScope = "global", organization_id: Optional[str] = None, user_id: Optional[str] = None
19
+ ) -> Optional[Any]:
20
+ """
21
+ Gets an item programmatically from the cache.
22
+
23
+ Args:
24
+ key (Any): The key to look up (must be hashable).
25
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
26
+ organization_id (Optional[str]): Required if scope='organization'.
27
+ user_id (Optional[str]): Required if scope='user'.
28
+
29
+ Returns:
30
+ Optional[Any]: The cached value or None if not found or expired.
31
+ """
32
+ ...
33
+
34
+ def set(
35
+ self,
36
+ key: Any,
37
+ value: Any,
38
+ ttl_seconds: Union[int, float],
39
+ scope: _CacheScope = "global",
40
+ organization_id: Optional[str] = None,
41
+ user_id: Optional[str] = None,
42
+ ) -> None:
43
+ """
44
+ Sets an item programmatically in the cache.
45
+
46
+ Args:
47
+ key (Any): The key (must be hashable).
48
+ value (Any): The value to store.
49
+ ttl_seconds (Union[int, float]): Time-to-live in seconds.
50
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
51
+ organization_id (Optional[str]): Required if scope='organization'.
52
+ user_id (Optional[str]): Required if scope='user'.
53
+ """
54
+ ...
55
+
56
+ def evict(
57
+ self, key: Any, scope: _CacheScope = "global", organization_id: Optional[str] = None, user_id: Optional[str] = None
58
+ ) -> None:
59
+ """
60
+ Removes a specific item programmatically from the cache.
61
+
62
+ Args:
63
+ key (Any): The key to remove (must be hashable).
64
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
65
+ organization_id (Optional[str]): Required if scope='organization'.
66
+ user_id (Optional[str]): Required if scope='user'.
67
+ """
68
+ ...
69
+
70
+ def clear(self) -> None:
71
+ """Safely clears the entire cache."""
72
+ ...
73
+
74
+ def aget(
75
+ self, key: Any, scope: _CacheScope = "global", organization_id: Optional[str] = None, user_id: Optional[str] = None
76
+ ) -> Awaitable[Optional[Any]]:
77
+ """
78
+ Asynchronously gets an item programmatically from the cache.
79
+
80
+ Args:
81
+ key (Any): The key to look up (must be hashable).
82
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
83
+ organization_id (Optional[str]): Required if scope='organization'.
84
+ user_id (Optional[str]): Required if scope='user'.
85
+
86
+ Returns:
87
+ Awaitable[Optional[Any]]: The cached value or None.
88
+ """
89
+ ...
90
+
91
+ def aset(
92
+ self,
93
+ key: Any,
94
+ value: Any,
95
+ ttl_seconds: Union[int, float],
96
+ scope: _CacheScope = "global",
97
+ organization_id: Optional[str] = None,
98
+ user_id: Optional[str] = None,
99
+ ) -> Awaitable[None]:
100
+ """
101
+ Asynchronously sets an item programmatically in the cache.
102
+
103
+ Args:
104
+ key (Any): The key (must be hashable).
105
+ value (Any): The value to store.
106
+ ttl_seconds (Union[int, float]): Time-to-live in seconds.
107
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
108
+ organization_id (Optional[str]): Required if scope='organization'.
109
+ user_id (Optional[str]): Required if scope='user'.
110
+ """
111
+ ...
112
+
113
+ def aevict(
114
+ self, key: Any, scope: _CacheScope = "global", organization_id: Optional[str] = None, user_id: Optional[str] = None
115
+ ) -> Awaitable[None]:
116
+ """
117
+ Asynchronously removes a specific item programmatically.
118
+
119
+ Args:
120
+ key (Any): The key to remove (must be hashable).
121
+ scope (_CacheScope): The scope ('global', 'organization', 'user').
122
+ organization_id (Optional[str]): Required if scope='organization'.
123
+ user_id (Optional[str]): Required if scope='user'.
124
+ """
125
+ ...
126
+
127
+ def aclear(self) -> Awaitable[None]:
128
+ """Asynchronously clears the entire cache."""
129
+ ...
130
+
131
+ def stats(self) -> Dict[str, Any]:
132
+ """
133
+ Returns a dictionary of cache observability statistics.
134
+
135
+ Returns:
136
+ Dict[str, Any]: A dict containing keys like 'hits', 'misses',
137
+ 'evictions', 'current_size', etc.
138
+ """
139
+ ...
140
+
141
+ def evict_by_scope(self, scope: _CacheScope, organization_id: Optional[str] = None, user_id: Optional[str] = None) -> int:
142
+ """
143
+ Granularly evicts all items belonging to a specific tenant.
144
+
145
+ Args:
146
+ scope (_CacheScope): The scope to target ('organization' or 'user').
147
+ organization_id (Optional[str]): Required if scope='organization'.
148
+ user_id (Optional[str]): Required if scope='user'.
149
+
150
+ Returns:
151
+ int: The number of items successfully evicted.
152
+ """
153
+ ...
154
+
155
+ def cache(
156
+ self, ttl_seconds: Union[int, float], scope: _CacheScope = "global", max_items: Optional[int] = None
157
+ ) -> Callable[[_FuncT], _FuncT]:
158
+ """
159
+ Decorator factory for caching function results.
160
+
161
+ Args:
162
+ ttl_seconds (Union[int, float]): Time-to-live for items.
163
+ scope (_CacheScope): The cache scope.
164
+ max_items (Optional[int]): Max items for this function.
165
+
166
+ Returns:
167
+ Callable[[_FuncT], _FuncT]: A decorator function.
168
+ """
169
+ ...
170
+
171
+ def configure(
172
+ self, backend: IStorageProvider, policy: IEvictionPolicy, max_size: int = 1000, cleanup_interval: int = 60
173
+ ) -> None:
174
+ """
175
+ Configures and starts the cache. Must be called once.
176
+
177
+ Args:
178
+ backend (IStorageProvider): The storage backend (e.g., InMemoryStorageProvider).
179
+ policy (IEvictionPolicy): The eviction policy (e.g., LRUEvictionPolicy).
180
+ max_size (int): Max number of items to store globally.
181
+ cleanup_interval (int): Interval (in seconds) for background cleanup.
182
+ """
183
+ ...
@@ -0,0 +1,82 @@
1
+ from typing import Optional, Protocol
2
+
3
+ from cache_types import _CacheKey
4
+ from protocols.eviction_policy import IEvictionPolicy
5
+
6
+
7
+ class ICachePolicyManager(Protocol):
8
+ """
9
+ Interface (Protocol) for cache policy management implementations.
10
+
11
+ Defines the contract for managing cache policies including
12
+ background cleanup and eviction coordination.
13
+ """
14
+
15
+ @property
16
+ def policy(self) -> IEvictionPolicy:
17
+ """Returns the eviction policy instance."""
18
+ ...
19
+
20
+ @property
21
+ def global_max_size(self) -> Optional[int]:
22
+ """Returns the global maximum cache size."""
23
+ ...
24
+
25
+ @property
26
+ def cleanup_interval(self) -> int:
27
+ """Returns the cleanup interval in seconds."""
28
+ ...
29
+
30
+ def start_background_cleanup(self) -> None:
31
+ """Starts the background cleanup thread."""
32
+ ...
33
+
34
+ def stop_background_cleanup(self) -> None:
35
+ """Stops the background cleanup thread gracefully."""
36
+ ...
37
+
38
+ def notify_set(self, key: _CacheKey, namespace: str, max_items: Optional[int]) -> Optional[_CacheKey]:
39
+ """
40
+ Notifies that an item was set and returns key to evict if needed.
41
+
42
+ Args:
43
+ key: The key that was set.
44
+ namespace: The namespace of the key.
45
+ max_items: The max_items limit for this namespace.
46
+
47
+ Returns:
48
+ Key to evict or None.
49
+ """
50
+ ...
51
+
52
+ def notify_get(self, key: _CacheKey, namespace: str) -> None:
53
+ """
54
+ Notifies that an item was accessed.
55
+
56
+ Args:
57
+ key: The key that was accessed.
58
+ namespace: The namespace of the key.
59
+ """
60
+ ...
61
+
62
+ def notify_evict(self, key: _CacheKey, namespace: str) -> None:
63
+ """
64
+ Notifies that an item was evicted.
65
+
66
+ Args:
67
+ key: The key that was evicted.
68
+ namespace: The namespace of the key.
69
+ """
70
+ ...
71
+
72
+ def notify_clear(self) -> None:
73
+ """Notifies that the entire cache was cleared."""
74
+ ...
75
+
76
+ def get_namespace_count(self) -> int:
77
+ """Returns the total number of tracked namespaces."""
78
+ ...
79
+
80
+ def get_global_size(self) -> int:
81
+ """Returns the global item count."""
82
+ ...
@@ -0,0 +1,70 @@
1
+ from typing import Optional, Protocol
2
+
3
+ from cache_types import _CacheKey
4
+
5
+
6
+ class IEvictionPolicy(Protocol):
7
+ """
8
+ Interface (Protocol) for all cache eviction policies (e.g., LRU, LFU).
9
+ Implementations MUST be thread-safe.
10
+ """
11
+
12
+ def notify_set(
13
+ self, key: _CacheKey, namespace: str, max_items: Optional[int], global_max_size: Optional[int]
14
+ ) -> Optional[_CacheKey]:
15
+ """
16
+ Notifies the policy that an item was set (added/updated).
17
+ The policy must enforce limits and return a key to evict if necessary.
18
+
19
+ Args:
20
+ key (_CacheKey): The key that was set.
21
+ namespace (str): The namespace of the key.
22
+ max_items (Optional[int]): The max_items limit for this namespace.
23
+ global_max_size (Optional[int]): The global max_size limit.
24
+
25
+ Returns:
26
+ Optional[_CacheKey]: A key to evict, or None.
27
+ """
28
+ ...
29
+
30
+ def notify_get(self, key: _CacheKey, namespace: str) -> None:
31
+ """
32
+ Notifies the policy that an item was accessed (read).
33
+
34
+ Args:
35
+ key (_CacheKey): The key that was accessed.
36
+ namespace (str): The namespace of the key.
37
+ """
38
+ ...
39
+
40
+ def notify_evict(self, key: _CacheKey, namespace: str) -> None:
41
+ """
42
+ Notifies the policy that an item was evicted (removed).
43
+
44
+ Args:
45
+ key (_CacheKey): The key that was evicted.
46
+ namespace (str): The namespace of the key.
47
+ """
48
+ ...
49
+
50
+ def notify_clear(self) -> None:
51
+ """Notifies the policy that the entire cache was cleared."""
52
+ ...
53
+
54
+ def get_namespace_count(self) -> int:
55
+ """
56
+ Returns the total number of tracked namespaces.
57
+
58
+ Returns:
59
+ int: The count of namespaces.
60
+ """
61
+ ...
62
+
63
+ def get_global_size(self) -> int:
64
+ """
65
+ Returns the total number of items tracked by the policy.
66
+
67
+ Returns:
68
+ int: The global item count.
69
+ """
70
+ ...
protocols/scope.py ADDED
@@ -0,0 +1,85 @@
1
+ from typing import Any, Dict, List, Optional, Protocol
2
+
3
+
4
+ class IScope(Protocol):
5
+ """
6
+ Interface (Protocol) for scope configuration implementations.
7
+
8
+ Defines the contract for managing hierarchical cache scoping,
9
+ allowing different implementations while maintaining type safety.
10
+ """
11
+
12
+ @property
13
+ def level_names(self) -> List[str]:
14
+ """
15
+ Returns the names of all scope levels.
16
+
17
+ Returns:
18
+ List of scope level names.
19
+ """
20
+ ...
21
+
22
+ def get_param_name(self, level_name: str) -> str:
23
+ """
24
+ Returns the parameter name for a specific scope level.
25
+
26
+ Args:
27
+ level_name: Name of the scope level.
28
+
29
+ Returns:
30
+ Corresponding parameter name.
31
+
32
+ Raises:
33
+ ValueError: If the level doesn't exist.
34
+ """
35
+ ...
36
+
37
+ def build_scope_path(self, scope_params: Dict[str, Any]) -> str:
38
+ """
39
+ Builds the scope path based on provided parameters.
40
+
41
+ Args:
42
+ scope_params: Dictionary with scope parameters.
43
+
44
+ Returns:
45
+ String representing the hierarchical scope path.
46
+ """
47
+ ...
48
+
49
+ def validate_scope_params(self, target_level: str, scope_params: Dict[str, Any]) -> None:
50
+ """
51
+ Validates that required parameters are present for the target level.
52
+
53
+ Args:
54
+ target_level: Desired scope level.
55
+ scope_params: Provided parameters.
56
+
57
+ Raises:
58
+ ValueError: If mandatory parameters are missing.
59
+ """
60
+ ...
61
+
62
+ def get_parent_scope_path(self, scope_path: str) -> Optional[str]:
63
+ """
64
+ Returns the parent scope path.
65
+
66
+ Args:
67
+ scope_path: Current scope path.
68
+
69
+ Returns:
70
+ Parent scope path or None if global.
71
+ """
72
+ ...
73
+
74
+ def is_descendant_of(self, child_path: str, parent_path: str) -> bool:
75
+ """
76
+ Checks if one scope is a descendant of another.
77
+
78
+ Args:
79
+ child_path: Child scope path.
80
+ parent_path: Parent scope path.
81
+
82
+ Returns:
83
+ True if child_path is descendant of parent_path.
84
+ """
85
+ ...
@@ -0,0 +1,67 @@
1
+ from typing import List, Optional, Protocol
2
+
3
+ from cache_types import _CacheKey, _CacheValue
4
+
5
+
6
+ class IStorageProvider(Protocol):
7
+ """
8
+ Interface (Protocol) for all storage backends (e.g., In-Memory, Redis).
9
+ Implementations MUST be thread-safe for synchronous operations.
10
+ """
11
+
12
+ def get(self, key: _CacheKey) -> Optional[_CacheValue]:
13
+ """
14
+ Atomically gets a value tuple (value, expiry) from storage.
15
+
16
+ Args:
17
+ key (_CacheKey): The internal key to get.
18
+
19
+ Returns:
20
+ Optional[_CacheValue]: The stored tuple, or None.
21
+ """
22
+ ...
23
+
24
+ def get_value_no_lock(self, key: _CacheKey) -> Optional[_CacheValue]:
25
+ """
26
+ Performs a non-locking ("dirty") read for the cleanup loop.
27
+ Only required for backends that support it.
28
+
29
+ Args:
30
+ key (_CacheKey): The internal key to look up.
31
+
32
+ Returns:
33
+ Optional[_CacheValue]: The stored tuple (value, expiry) or None.
34
+ """
35
+ ...
36
+
37
+ def set(self, key: _CacheKey, value: _CacheValue) -> None:
38
+ """
39
+ Atomically sets a value tuple (value, expiry) in storage.
40
+
41
+ Args:
42
+ key (_CacheKey): The internal key to set.
43
+ value (_CacheValue): The (value, expiry) tuple to store.
44
+ """
45
+ ...
46
+
47
+ def evict(self, key: _CacheKey) -> None:
48
+ """
49
+ Atomically evicts a key from storage.
50
+
51
+ Args:
52
+ key (_CacheKey): The internal key to evict.
53
+ """
54
+ ...
55
+
56
+ def get_all_keys(self) -> List[_CacheKey]:
57
+ """
58
+ Atomically gets a copy of all keys in storage.
59
+
60
+ Returns:
61
+ List[_CacheKey]: A list of all cache keys.
62
+ """
63
+ ...
64
+
65
+ def clear(self) -> None:
66
+ """Atomically clears the entire storage."""
67
+ ...
storages/__init__.py ADDED
File without changes