ytptube/app/library/cache.py
2026-01-16 20:42:51 +03:00

178 lines
5.1 KiB
Python

import hashlib
import logging
import threading
import time
from typing import Any
from aiohttp import web
from app.library.Services import Services
from .Scheduler import Scheduler
from .Singleton import ThreadSafe
LOG = logging.getLogger("cache")
class Cache(metaclass=ThreadSafe):
def __init__(self) -> None:
"""
Initialize the Cache.
"""
self._cache: dict[str, tuple[Any, float | None]] = {}
self._lock = threading.Lock()
@staticmethod
def get_instance() -> "Cache":
"""
Get the singleton instance of Cache.
Returns:
Cache: The singleton instance of Cache.
"""
return Cache()
def attach(self, _: web.Application) -> None:
Services.get_instance().add("cache", self)
Scheduler.get_instance().add(
timer="* * * * *",
func=self.cleanup,
id=f"{__class__.__name__}.{__class__.cleanup.__name__}",
)
def set(self, key: str, value: Any, ttl: float | None = None) -> None:
"""
Synchronously set a value in the cache with an optional time-to-live.
If ttl is None, the entry never expires.
"""
expire_at = None if ttl is None else time.time() + ttl
with self._lock:
self._cache[key] = (value, expire_at)
def get(self, key: str, default: Any | None = None) -> Any | None:
"""
Synchronously retrieve a value from the cache if it exists and hasn't expired.
"""
with self._lock:
entry = self._cache.get(key)
if entry is None:
return default
value, expire_at = entry
if expire_at is not None and time.time() >= expire_at:
del self._cache[key]
return default
return value
def ttl(self, key: str) -> float | None:
"""
Synchronously retrieve the time-to-live of a key in the cache.
"""
with self._lock:
entry = self._cache.get(key)
if entry is None:
return None
_, expire_at = entry
if expire_at is None:
return None
return expire_at - time.time()
def has(self, key: str) -> bool:
"""
Synchronously check if a key exists in the cache and hasn't expired.
"""
with self._lock:
entry = self._cache.get(key)
if entry is None:
return False
_, expire_at = entry
if expire_at is not None and time.time() >= expire_at:
del self._cache[key]
return False
return True
def delete(self, key: str) -> None:
"""
Synchronously remove a key from the cache if it exists.
"""
with self._lock:
self._cache.pop(key, None)
def clear(self) -> None:
"""
Synchronously clear all items from the cache.
"""
with self._lock:
self._cache.clear()
def hash(self, key: str) -> str:
"""
Generate a SHA-256 hash for the given input string.
:param key (str): The string to hash.
:return: A hexadecimal SHA-256 hash of the input string.
"""
return hashlib.sha256(key.encode("utf-8")).hexdigest()
# Asynchronous counterparts for non-blocking interfaces
async def aset(self, key: str, value: Any, ttl: float | None = None) -> None:
"""
Asynchronously set a value in the cache.
"""
self.set(key, value, ttl)
async def aget(self, key: str, default: Any | None = None) -> Any | None:
"""
Asynchronously retrieve a value from the cache.
"""
return self.get(key, default)
async def attl(self, key: str) -> float | None:
"""
Asynchronously retrieve the time-to-live of a key in the cache.
"""
return self.ttl(key)
async def ahas(self, key: str) -> bool:
"""
Asynchronously check if a key exists in the cache.
"""
return self.has(key)
async def adelete(self, key: str) -> None:
"""
Asynchronously remove a key from the cache.
"""
self.delete(key)
async def aclear(self) -> None:
"""
Asynchronously clear all items from the cache.
"""
self.clear()
async def ahash(self, key: str) -> str:
"""
Asynchronously generate a SHA-256 hash for the given input string.
"""
return self.hash(key=key)
async def cleanup(self) -> None:
"""
Remove all expired entries from the cache.
Called periodically by the scheduler.
"""
with self._lock:
now = time.time()
expired_keys: list[str] = [
key for key, (_, expire_at) in self._cache.items() if expire_at is not None and now >= expire_at
]
for key in expired_keys:
del self._cache[key]
if expired_keys:
LOG.debug(f"Cleaned up {len(expired_keys)} expired cache entries.")