-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcache.py
203 lines (161 loc) · 6.05 KB
/
cache.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import time
import json
import functools
from typing import Dict, Any, Optional, Callable, Tuple, Union, TypeVar, cast
from datetime import datetime, timedelta
T = TypeVar('T')
class Cache:
"""
A simple in-memory cache with TTL support.
"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Cache, cls).__new__(cls)
cls._instance._init()
return cls._instance
def _init(self):
self._cache: Dict[str, Tuple[Any, float]] = {}
self._hit_count = 0
self._miss_count = 0
self._creation_time = time.time()
def get(self, key: str) -> Optional[Any]:
"""
Get a value from the cache.
Args:
key: Cache key
Returns:
Cached value or None if not found or expired
"""
if key not in self._cache:
self._miss_count += 1
return None
value, expiry = self._cache[key]
# Check if the value has expired
if expiry < time.time():
self._miss_count += 1
del self._cache[key]
return None
self._hit_count += 1
return value
def set(self, key: str, value: Any, ttl: int = 60) -> None:
"""
Set a value in the cache.
Args:
key: Cache key
value: Value to cache
ttl: Time to live in seconds (default: 60)
"""
expiry = time.time() + ttl
self._cache[key] = (value, expiry)
def delete(self, key: str) -> bool:
"""
Delete a value from the cache.
Args:
key: Cache key
Returns:
True if the key was deleted, False otherwise
"""
if key in self._cache:
del self._cache[key]
return True
return False
def clear(self) -> None:
"""Clear all cached values."""
self._cache.clear()
def delete_pattern(self, pattern: str) -> int:
"""
Delete all keys matching a pattern.
Args:
pattern: Pattern to match
Returns:
Number of keys deleted
"""
keys_to_delete = [k for k in self._cache.keys() if pattern in k]
for key in keys_to_delete:
del self._cache[key]
return len(keys_to_delete)
def stats(self) -> Dict[str, Any]:
"""
Get cache statistics.
Returns:
Dictionary with cache statistics
"""
total_requests = self._hit_count + self._miss_count
hit_rate = self._hit_count / total_requests if total_requests > 0 else 0
return {
"items": len(self._cache),
"hits": self._hit_count,
"misses": self._miss_count,
"hit_rate": hit_rate,
"uptime": time.time() - self._creation_time
}
# Create a singleton cache instance
cache = Cache()
def cached(ttl: int = 60, key_prefix: str = "") -> Callable[[Callable[..., T]], Callable[..., T]]:
"""
Decorator for caching function results.
Args:
ttl: Time to live in seconds (default: 60)
key_prefix: Prefix for the cache key (default: "")
Returns:
Decorated function
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(func)
async def async_wrapper(*args: Any, **kwargs: Any) -> T:
# Create a unique key based on the function name, args, and kwargs
key_parts = [key_prefix or func.__name__]
# Add positional arguments to the key
if args:
key_parts.append("_".join(str(arg) for arg in args))
# Add keyword arguments to the key (sorted to ensure consistent order)
if kwargs:
key_parts.append("_".join(f"{k}={v}" for k, v in sorted(kwargs.items())))
# Create the final key
cache_key = ":".join(key_parts)
# Try to get the value from the cache
cached_value = cache.get(cache_key)
if cached_value is not None:
return cached_value
# Call the function and cache the result
result = await func(*args, **kwargs)
cache.set(cache_key, result, ttl)
return result
@functools.wraps(func)
def sync_wrapper(*args: Any, **kwargs: Any) -> T:
# Create a unique key based on the function name, args, and kwargs
key_parts = [key_prefix or func.__name__]
# Add positional arguments to the key
if args:
key_parts.append("_".join(str(arg) for arg in args))
# Add keyword arguments to the key (sorted to ensure consistent order)
if kwargs:
key_parts.append("_".join(f"{k}={v}" for k, v in sorted(kwargs.items())))
# Create the final key
cache_key = ":".join(key_parts)
# Try to get the value from the cache
cached_value = cache.get(cache_key)
if cached_value is not None:
return cached_value
# Call the function and cache the result
result = func(*args, **kwargs)
cache.set(cache_key, result, ttl)
return result
# Return the appropriate wrapper based on whether the function is async or not
if asyncio.iscoroutinefunction(func):
return cast(Callable[..., T], async_wrapper)
else:
return cast(Callable[..., T], sync_wrapper)
return decorator
def invalidate_cache(pattern: str = "") -> None:
"""
Invalidate cache entries matching a pattern.
Args:
pattern: Pattern to match (default: "" which matches all keys)
"""
if pattern:
cache.delete_pattern(pattern)
else:
cache.clear()
import asyncio