diff --git a/NOTICE b/NOTICE deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index e69de29..793c231 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,153 @@ +# simple-cache + +A simple library for caching data. + +## Installation +You can install the library using `pip`: + +```bash +pip install simple-cache --index-url https://repo.jcloud-services.ddns.net/simple/ +``` + +## Full documentation + +### `CacheItem` +A cache item. + +#### Params +- `value` (type: `Any`): The value of the item +- `creation_time` (type: `Union[int, float]`): The time the item was created + +#### Exceptions + +No exceptions. + +#### Methods + +No methods. + +### `Cache` +A thread-safe cache. + +#### Parameters +- `generate_value_func` (type: `Optional[FunctionType]`, default: `None`): The function to generate a new value. It has to take one positional argument. +- `max_size` (type: `int`, default: `256`): The maximum size of the cache. +- `ttl` (type: `int`, default: `120`): The TTL (time to live) in seconds. + +#### Exceptions + +No exceptions. + +#### Methods + +##### `set` + +Sets or updates an item (thread-safe). + +###### Parameters + +- `key` (type: `str`): The key. +- `value` (type: `Any`): The value. + +###### Exceptions + +No exceptions. + +###### Returns + +`None` + +##### `delete` +Deletes an item (thread-safe). + +###### Parameters + +- `key` (type: `str`): The key. + +###### Exceptions + +- `KeyError`: If the item does not exist. + +###### Returns + +`None` + +##### `get` + +Returns an item (thread-safe). +If the item does not exist, it uses ``self.generate_value_func`` to +get the value. If no ``generate_value_func`` was specified in the +constructor of the ``Cache`` object, the value will be set to ``None``. + +###### Parameters + +- `key` (type: `str`): The key. + +###### Exceptions + +No exceptions. + +###### Returns + +The value. +Type: `Any` + +##### `clean_item` + +Removes an item if it is expired (thread-safe). + +###### Parameters + +- `key` (type: `key`): The key + +###### Exceptions + +No exceptions. + +###### Returns + +`None` + +##### `clear_expired` + +Removes all expired items (thread-safe). + +###### Parameters + +No parameters. + +###### Exceptions + +No Exceptions. + +###### Returns + +`None` + +##### `clear` + +Removes all items (thread-safe). + +###### Parameters + +No parameters. + +###### Exceptions + +No Exceptions. + +###### Returns + +`None` + +## Changelog + +### Version 0.1.0 +- initial release +- thread safety +- set, update, delete and get items +- TTL +- support for specifying a function to generate new values +- deletion of expired items when getting them +- deletion of the least used item when a new item is set and the maximum size is reached +- support for clearing the cache \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b21e2f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "simple-cache" +version = "0.1.0" +description = "A simple library for caching data." +license = "Apache-2.0" \ No newline at end of file diff --git a/src/simple_cache/__init__.py b/src/simple_cache/__init__.py new file mode 100644 index 0000000..5e864b8 --- /dev/null +++ b/src/simple_cache/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2026 jCloud Services GbR + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +''' +A simple library for caching data. +''' + +from ._core import * +from ._core import __all__ as _core__all__ + +__all__ = [ + *_core__all__ +] \ No newline at end of file diff --git a/src/simple_cache/_core.py b/src/simple_cache/_core.py new file mode 100644 index 0000000..6708159 --- /dev/null +++ b/src/simple_cache/_core.py @@ -0,0 +1,244 @@ +from heapq import nsmallest +import time +import threading +from types import FunctionType +from typing import Optional, Any, Union + +__all__ = [ + 'CacheItem', + 'Cache' +] + +def _genval(_) -> None: + ''' + The fallback function for the ``generate_value_func`` argument for the + constructor of ``Cache``. It simply returns ``None``, without performing any + additional operations. + ''' + pass + +class CacheItem: + def __init__(self, value: Any, creation_time: Union[int, float]): + ''' + A cache item. + + :param value: The value of the item + :type value: Any + :param creation_time: The time the item was created + :type creation_time: Union[int, float] + ''' + + self.value = value + self.creation_time = creation_time + +class Cache: + def __init__(self, generate_value_func: Optional[FunctionType] = None, max_size: int = 256, ttl: int = 120): + ''' + A thread-safe cache. + + :param generate_value_func: The function to generate a new value. It has to take one positional argument. + :type generate_value_func: Optional[FunctionType] + :param max_size: The maximum size of the cache. + :type max_size: int + :param ttl: The TTL (time to live) in seconds. + :type ttl: int + ''' + + if generate_value_func is None: + generate_value_func = _genval + + self._cache = dict() + self.max_size = max_size + self.ttl = ttl + self.generate_value_func = generate_value_func + self._accesses = dict() + self._all_accesses = 0 + self._lock = threading.RLock() + + def _get_least_used_keys(self, number_of_keys: int = 1) -> list: + ''' + Returns the keys that are used the least. The number of keys that are + returned can be specified with the parameter ``number_of_keys``. + + :param number_of_keys: The number of keys to return. + :type number_of_keys: int + + :return: The ``number_of_keys``-th least used keys. + :rtype: list + ''' + + if not self._accesses or number_of_keys <= 0: + return [] + + return [k for k in nsmallest(number_of_keys, self._accesses, key = lambda k: self._accesses[k])] + + def _set(self, key: str, value: Any) -> None: + ''' + Sets or updated an item. + + :param key: The key. + :type key: str + :param value: The value. + :type value: Any + ''' + + new_key = key not in self._cache.keys() + if len(self._cache) >= self.max_size and new_key: + self._clear_expired() + if len(self._cache) >= self.max_size: + least_used_keys = self._get_least_used_keys(1) + if least_used_keys: + self._delete(least_used_keys[0]) + + self._accesses[key] = 0 if new_key else self._accesses[key] + self._cache[key] = CacheItem(value, time.time()) + + def _delete(self, key: str) -> None: + ''' + Deletes an item. + + :param key: The key. + :type key: str + + :raises KeyError: If the item does not exist. + ''' + + if key in self._cache: + self._all_accesses -= self._accesses[key] + del self._cache[key] + del self._accesses[key] + else: + raise KeyError(key) + + def _get(self, key: str) -> Any: + ''' + Returns an item. + If the item does not exist, it uses ``self.generate_value_func`` to + get the value. If no ``generate_value_func`` was specified in the + constructor of the ``Cache`` object, the value will be set to ``None``. + + :param key: The key. + :type key: str + + :return: The value. + :rtype: Any + ''' + + self._clean_item(key) + if key in self._cache: + value = self._cache[key].value + else: + value = self.generate_value_func(key) + self._set(key, value) + self._accesses[key] += 1 + self._all_accesses += 1 + return value + + def _clean_item(self, key: str) -> None: + ''' + Removes an item if it is expired. + + :param key: The key. + :type key: str + ''' + + if key in self._cache: + ct = self._cache[key].creation_time + if time.time() - ct > self.ttl: + self._delete(key) + + def _clear_expired(self) -> None: + ''' + Removes all expired items. + ''' + + expired = [k for k, v in self._cache.items() if time.time() - v.creation_time > self.ttl] + for e in expired: + self._delete(e) + + def _clear(self) -> None: + ''' + Removes all items. + ''' + + self._all_accesses = 0 + self._accesses = dict() + self._cache = dict() + + def set(self, key: str, value: Any) -> None: + ''' + Sets or updates an item (thread-safe). + + :param key: The key. + :type key: str + :param value: The value. + :type value: Any + ''' + + with self._lock: + self._set(key, value) + + def delete(self, key: str) -> None: + ''' + Deletes an item (thread-safe). + + :param key: The key. + :type key: str + + :raises KeyError: If the item does not exist. + ''' + with self._lock: + self._delete(key) + + def get(self, key: str) -> Any: + ''' + Returns an item (thread-safe). + If the item does not exist, it uses ``self.generate_value_func`` to + get the value. If no ``generate_value_func`` was specified in the + constructor of the ``Cache`` object, the value will be set to ``None``. + + :param key: The key. + :type key: str + + :return: The value. + :rtype: Any + ''' + + with self._lock: + return self._get(key) + + def clean_item(self, key: str) -> None: + ''' + Removes an item if it is expired (thread-safe). + + :param key: The key. + :type key: str + ''' + + with self._lock: + self._clean_item(key) + + def clear_expired(self) -> None: + ''' + Removes all expired items (thread-safe). + ''' + + with self._lock: + self._clear_expired() + + def clear(self) -> None: + ''' + Removes all items (thread-safe). + ''' + + with self._lock: + self._clear() + + def __getitem__(self, key: str) -> Any: + return self.get(key) + + def __setitem__(self, key: str, value: Any) -> None: + self.set(key, value) + + def __delitem__(self, key: str) -> None: + self.delete(key) \ No newline at end of file