(20251125) added a new kind of caching that is similar to the Async Redis Cache module that we had previously, but now allows more kinds of cache to be used.

This commit is contained in:
2025-11-25 17:14:33 +05:30
parent 46cad058de
commit 0dcbeb3ea3
4 changed files with 1200 additions and 0 deletions
+390
View File
@@ -0,0 +1,390 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
CREATED: Tue, 25th Nov, 2025
UPDATED: Tue, 25th Nov, 2025
OBJECTIVE:
To define a caching class that uses in-RAM dicts to hold cached data.
REFERENCES:
N/A
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
from unittest import case
sys.path.append(".")
sys.path.append("..")
# Other utils:
from utils_v2.string import json
from utils_v2.serialization.pickle_serializer import PickleSerializer
from utils_v2.date_time import date_time
from utils_v2.string import regex
from utils_v2.cache_v2.base import AsyncCachingBase
# To work with datatypes:
from typing import List, Any
# For async activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class AsyncLocalCache(AsyncCachingBase):
def __init__(
self,
debug = False,
debug_prefix = "R-Cache | "
):
"""
Implements a simple cache in RAM that holds and returns all native datatypes like ints, floats, bools,
strings, dicts, lists, sets, and tuples :)
:param debug: Whether, or not, you want to show debugging messages from the start.
:param debug_prefix: The prefix text to show with the debugging messages.
"""
# Invoke the parent class's constructor:
super().__init__(
debug = debug,
debug_prefix = debug_prefix
)
# Create a local dict that will hold the cached data:
self.__cached_data = {}
# ┳┓ ┓ •
# ┃┃┏┓┣┓┓┏┏┓┏┓┓┏┓┏┓
# ┻┛┗ ┗┛┗┻┗┫┗┫┗┛┗┗┫
# ┛ ┛ ┛
def enable_debug(self):
self.__printer.enable()
def disable_debug(self):
self.__printer.disable()
# ┓┏ ┓
# ┣┫┏┓┃┏┓┏┓┏┓┏
# ┛┗┗ ┗┣┛┗ ┛ ┛
# ┛
@staticmethod
def now_utc() -> int | float:
"""
Returns the current UTC time as a timestamp.
:return: The current UTC time as a timestamp.
"""
# Return the current time in UTC as a timestamp:
return date_time.get_current_utc_date_time(as_string = False).timestamp()
async def clear_expired_keys(self) -> None:
"""
Clears all the expired cached data.
:return: None.
"""
# Keys to delete:
to_del = []
# Enlist all the keys that need to be deleted:
for k, v in self.__cached_data.items():
if self.now_utc() >= v["exp"]: to_del.append(k)
# Delete the expired keys:
for k in to_del:
self.__cached_data.pop(k)
# ┏┓
# ┃ ┏┓┏┓┏┓
# ┗┛┗┛┛ ┗
async def list_keys(
self,
match: str = ".*",
raise_exception: bool = False
) -> List[str] | None:
"""
To get a list of all the keys that match the pattern.
:param match: To match a glob-style pattern. THIS IS NOT FULL-FLEDGED REGEX.
:param raise_exception: Whether to raise or suppress exceptions.
:return: The list of keys if successful, else None.
"""
# Clear the expired keys:
await self.clear_expired_keys()
# Start with an empty list:
keys_list = []
try:
# Enlist all the keys,
# test the pattern against all the keys and keep only those that match:
for k in self.__cached_data.keys():
if regex.match(text = str(k), pattern = match):
keys_list.append(k)
# Done here:
return list(set(keys_list)) if keys_list else None
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return None
async def ttl(
self,
key: str | bytes,
raise_exception: bool = False
) -> int | float | None:
"""
To get the no. of seconds till the expiry of some key.
:param key: The key to check the expiry of.
:param raise_exception: Whether to raise or suppress exceptions.
:return: -1 if the key is persistent (i.e., no expiry time set), -2 if the key does not exist, or the time left
in seconds if the key exists and is not persistent. If something goes wrong, you will get a null value.
"""
# Clear the expired keys:
await self.clear_expired_keys()
# Start by assuming failure:
ttl = None
try:
# Get the expiry:
exp_utc = self.__cached_data.get(key, {}).get("exp", None)
if exp_utc is None: ttl = -2
else: ttl = exp_utc - self.now_utc()
return ttl
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return None
async def set(
self,
key: str | bytes,
value: Any,
expiry: float = None,
raise_exception: bool = False
) -> bool:
"""
Saves some value to the cache. If an expiry is specified, the data will be deleted after that many seconds.
:param key: The key with which the data will be stored and retrieved.
:param value: The value to store.
:param expiry: The time in seconds after which the data will expire. Must be a positive number.
:param raise_exception: Whether to raise or suppress exceptions.
:return: True if cached, else False.
"""
# Clear the expired keys:
await self.clear_expired_keys()
try:
# If the expiry is not specified,
# make it an unreasonably far off future date:
if expiry is None:
expiry = date_time.get_current_utc_date_time(as_string = False) + date_time.timedelta(days = 365)
expiry = expiry.timestamp()
# Here we actually try to set the data:
self.__cached_data[key] = {
"val": value,
"exp": self.now_utc() + expiry
}
# Done here:
return True
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return False
async def get(
self,
key: str | bytes,
raise_exception: bool = False,
on_fail: Any = None
) -> Any:
"""
Retrieve the cached value.
:param key: The key with which the data was saved.
:param raise_exception: Whether to raise or suppress exceptions.
:param on_fail: What to return if the process fails due to an exception.
:return: The retrieved data or null if not found.
"""
# Clear the expired keys:
await self.clear_expired_keys()
try:
# Here we try to fetch the data:
data = self.__cached_data.get(key, {}).get("val", None)
return data
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return on_fail
async def delete(
self,
key: str | bytes,
raise_exception: bool = False
) -> bool:
"""
Prematurely delete the value from the cache before it expires.
:param key: The key with which the data was saved.
:param raise_exception: Whether to raise or suppress exceptions.
:return: True if deleted, else False.
"""
# Clear the expired keys:
await self.clear_expired_keys()
try:
# Try to manually delete the key before expiry:
response = self.__cached_data.pop(key, None)
return True if response else False
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return False
async def count(
self,
key: str | bytes,
value: int = 1,
expiry: float = None,
raise_exception: bool = False
) -> int | None:
"""
To use simple counters. If the counter (identified by the 'key') exists, it will be incremented, else the
counter will be created and the value will be incremented from 0.
:param key: The name of the counter.
:param value: The amount to increment the value by. Send negative values to count backwards.
:param expiry: The time (in seconds) in which the counter expires. Starts from the time the counter is created.
This value has to be an integer. If a float is passed, the value will be rounded off.
:param raise_exception: Whether to raise or suppress exceptions.
:return: The latest value of the counter. Will be null if something went wrong and the exception was suppressed.
"""
# Clear the expired keys:
await self.clear_expired_keys()
try:
# Check if
already_existed = False if self.__cached_data.get(key, None) is None else True
# If it already exists, we just increment the value;
# else we create the value and increment the value starting from zero:
if already_existed: self.__cached_data[key]["val"] = self.__cached_data[key]["val"] + value
else: self.__cached_data[key] = {
"val": value,
"exp": self.now_utc() + expiry
}
# Return the new value:
return self.__cached_data[key]["val"]
# If an exception occurs in th eprocess:
except Exception as exception:
self.__printer(exception)
if raise_exception: raise
else: return None
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass