Metadata-Version: 2.3
Name: cachebox
Version: 2.2.3
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Typing :: Typed
License-File: LICENSE
Summary: The fastest memoizing and caching Python library written in Rust
Keywords: caching,cached,cachetools,cachebox,cache,in-memory-caching,memoizing
Author: awolverp
Author-email: awolverp@gmail.com
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/awolverp/cachebox

# Cachebox
![Downloads](https://static.pepy.tech/badge/cachebox) ![Downloads](https://static.pepy.tech/badge/cachebox/week)

[**v2 Changelog**](https://github.com/awolverp/cachebox/blob/main/CHANGELOG.md#200---2024-03-09) | [**Releases**](https://github.com/awolverp/cachebox/releases)

The fastest caching library with different implementations, written in Rust.

- 🚀 3-21x faster than other libraries (like cachetools and cacheout)
- 📊 Very very low memory usage (1/3 of dictionary)
- **(R)** written in Rust
- 🤝 Support Python 3.8 and above
- 📦 Over 7 cache algorithms are supported
- 🧶 Completely thread-safe

**(@)** decorator example:
```python
from cachebox import cached, cachedmethod, TTLCache, LRUCache

# Keep coin price for no longer than a minute
@cached(TTLCache(maxsize=126, ttl=60))
def get_coin_price(coin_name):
    return web3_client.get_price(coin_name)

# Async functions are supported
@cached(LRUCache(maxsize=126))
async def get_coin_price(coin_name):
    return await async_web3_client.get_price(coin_name)

# You can pass `capacity` parameter.
# If `capacity` specified, the cache will be able to hold at
# least capacity elements without reallocating.
@cached(LRUCache(maxsize=126, capacity=100))
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

# methods are supported
class APIResource:
    @cachedmethod(
        TTLCache(126, ttl=10),
        # You can detemine how caching is done using `key_maker` parameter.
        key_maker=lambda args, kwds: args[0].client_ip
    )
    def get_information(self, request):
        ...
```

## Page Contents
- ⁉️ [When i need caching?](#when-i-need-caching)
- 🤷‍♂️ [Why `cachebox`?](#why-cachebox)
- 🎯 [Features](#features)
- 🛠️ [Installation](#installation)
- 🎓 [Usage](#API)
- 🚀 [Performance table](#performance-table)
- ⁉️ [Frequently Asked Questions](#frequently-asked-questions)
- 🆕 [*CHANGELOG*](CHANGELOG.md)
- ⏱️ [*BENCHMARK*](https://github.com/awolverp/cachebox-benchmark)

## When i need caching?
1. **Sometimes you have functions that take a long time to execute, and you need to call them each time.**

```python
@cached(LRUCache(260))
def function(np_array):
    # big operations
    ...
```

2. **Sometimes you need to temporarily store data in memory for a short period.**

3. **When dealing with remote APIs, Instead of making frequent API calls, store the responses in a cache.**

```python
@cached(TTLCache(0, ttl=10))
def api_call(key):
    return api.call(key)
```

4. **Caching query results from databases can enhance performance.**

```python
@cached(TTLCache(0, ttl=1))
def select_user(id):
    return db.execute("SELECT * FROM users WHERE id=?", (id,))
```

and ...

## Why `cachebox`?
**`cachebox`** library uses *Rust* language to has high-performance.

**Low memory usage** - It uses very low memory usage; let's have a simple compare to dictionary:
```python
>>> import sys, cachebox
>>> sys.getsizeof(cachebox.Cache(0, {i:i for i in range(100000)}))
1835032
>>> sys.getsizeof({i:i for i in range(100000)})
5242960
```

**High-speed** - Is speed important for you? It's is here for you; see [**here**](https://github.com/awolverp/cachebox-benchmark).

**Zero-dependecy** - As we said, `cachebox` written in Rust so you have not to install any other dependecies.

**Thread-safe** - It's completely thread-safe and uses read-writer locks to prevent problems.

## Installation
You can install **cachebox** from PyPi:
```sh
pip3 install -U cachebox
```

To verify that the library is installed correctly, run the following command:
```sh
python -c "import cachebox; print(cachebox.__version__)"
```

## API
All the implementations are support **mutable-mapping** methods (e.g `__setitem__`, `get`, `popitem`),
and there are some new methods for each implemetation.

These methods are available for all classes:
- `insert(key, value)`: an aliases for `__setitem__`

```python
>>> cache.insert(1, 1) # it equals to cache[1] = 1
```

- `capacity()`: Returns the number of elements the cache can hold without reallocating.

```python
>>> cache.update((i, i) for i in range(1000))
>>> cache.capacity()
1432
```

- `drain(n)`: According to cache algorithm, deletes and returns how many items removed from cache.

```python
>>> cache = LFUCache(10, {i:i for i in range(10)})
>>> cache.drain(8)
8
>>> len(cache)
2
>>> cache.drain(10)
2
>>> len(cache)
0
```

- `shrink_to_fit()`: Shrinks the capacity of the cache as much as possible.

```python
>>> cache = LRUCache(0, {i:i for i in range(10)})
>>> cache.capacity()
27
>>> cache.shrink_to_fit()
>>> cache.capacity()
11
```

### Cache
Fixed-size (or can be not) cache implementation without any policy,
So only can be fixed-size, or unlimited size cache.

```python
>>> from cachebox import Cache
>>> cache = Cache(100) # fixed-size cache
>>> cache = Cache(0) # unlimited-size cache
>>> cache = Cache(100, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
>>> cache = Cache(2, {i:i for i in range(10)})
...
OverflowError: maximum size limit reached
```

**There're no new methods for this class.**

### FIFOCache
FIFO Cache implementation (First-In First-Out policy, very useful).

In simple terms, the FIFO cache will remove the element that has been in the cache the longest;
It behaves like a Python dictionary.

```python
>>> from cachebox import FIFOCache
>>> cache = FIFOCache(100) # fixed-size cache
>>> cache = FIFOCache(0) # unlimited-size cache
>>> cache = FIFOCache(100, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
```

**There're new methods:**
- `first`: returns the first inserted key (the oldest)
- `last`: returns the last inserted key (the newest)


### LFUCache
LFU Cache implementation (Least frequantly used policy).

In simple terms, the LFU cache will remove the element in the cache that has been accessed the least,
regardless of time.

```python
>>> from cachebox import LFUCache
>>> cache = LFUCache(100) # fixed-size cache
>>> cache = LFUCache(0) # unlimited-size cache
>>> cache = LFUCache(100, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
```

**There's a new method:**
- `least_frequently_used`: returns the key that has been accessed the least.


### RRCache
RRCache implementation (Random Replacement policy).

In simple terms, the RR cache will choice randomly element to remove it to make space when necessary.

```python
>>> from cachebox import RRCache
>>> cache = RRCache(100) # fixed-size cache
>>> cache = RRCache(0) # unlimited-size cache
>>> cache = RRCache(100, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
```

**There're no new methods for this class.**


### LRUCache
LRU Cache implementation (Least recently used policy).

In simple terms, the LRU cache will remove the element in the cache that has not been accessed in the longest time.

```python
>>> from cachebox import LRUCache
>>> cache = LRUCache(100) # fixed-size cache
>>> cache = LRUCache(0) # unlimited-size cache
>>> cache = LRUCache(100, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
```

**There're new methods:**
- `least_recently_used`: returns the key that has not been accessed in the longest time.
- `most_recently_used`: returns the key that has been accessed in the longest time.


### TTLCache
TTL Cache implementation (Time-to-live policy).

In simple terms, The TTL cache is one that evicts items that are older than a time-to-live.

```python
>>> from cachebox import TTLCache
>>> cache = TTLCache(100, 2) # fixed-size cache, 2 ttl value
>>> cache = TTLCache(0, 10) # unlimited-size cache, 10 ttl value
>>> cache = TTLCache(100, 5, {"key1": "value1", "key2": "value2"}) # initialize from dict or any iterable object
```
**There're new methods:**
- `get_with_expire`: Works like `.get()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2})
>>> cache.get_with_expire(1)
(1, 1.23445675)
>>> cache.get_with_expire("no-exists")
(None, 0.0)
```

- `pop_with_expire`: Works like `.pop()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2})
>>> cache.pop_with_expire(1)
(1, 1.23445675)
>>> cache.pop_with_expire(1)
(None, 0.0)
```

- `popitem_with_expire`: Works like `.popitem()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2})
>>> cache.popitem_with_expire()
(1, 1, 1.23445675)
>>> cache.popitem_with_expire()
(2, 2, 1.94389545)
>>> cache.popitem_with_expire()
...
KeyError
```

### VTTLCache
VTTL Cache implementation (Time-to-live per-key policy)

Works like TTLCache, with this different that each key has own time-to-live value.

```python
>>> cache = VTTLCache(100) # fixed-size cache
>>> cache = VTTLCache(0) # unlimited-size cache

# initialize from dict or any iterable object;
# also these items will expire after 5 seconds
>>> cache = VTTLCache(100, {"key1": "value1", "key2": "value2"}, 5)

# initialize from dict or any iterable object;
# but these items never expire, because we pass None as them ttl value
>>> cache = VTTLCache(100, {"key1": "value1", "key2": "value2"}, None)
```

**There're new methods:**
- `insert(key, value, ttl)`: is different here. if you use `cache[key] = value` way, you cannot set ttl value for those item, but here you can.

```python
>>> cache.insert("key", "value", 10) # this item will expire after 10 seconds
>>> cache.insert("key", "value", None) # but this item never expire.
```

- `setdefault(key, default, ttl)`: Returns the value of the specified key.
If the key does not exist, insert the key, with the specified value.

- `update(iterable, ttl)`: inserts the specified items to the cache. The `iterable` can be a dictionary,
or an iterable object with key-value pairs.

```python
>>> cache = VTTLCache(20)
>>> cache.insert("key", "value", 10)
>>> cache.update({i:i for i in range(12)}, 2)
>>> len(cache)
13
>>> time.sleep(2)
>>> len(cache)
1
```

- `get_with_expire`: Works like `.get()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2}, 2)
>>> cache.get_with_expire(1)
(1, 1.9934)
>>> cache.get_with_expire("no-exists")
(None, 0.0)
```

- `pop_with_expire`: Works like `.pop()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2}, 2)
>>> cache.pop_with_expire(1)
(1, 1.99954)
>>> cache.pop_with_expire(1)
(None, 0.0)
```

- `popitem_with_expire`: Works like `.popitem()`, but also returns the remaining expiration.

```python
>>> cache.update({1: 1, 2: 2}, 2)
>>> cache.popitem_with_expire()
(1, 1, 1.9786564)
>>> cache.popitem_with_expire()
(2, 2, 1.97389545)
>>> cache.popitem_with_expire()
...
KeyError
```

## Performance table

> [!NOTE]\
> Operations which have an amortized cost are suffixed with a `*`. Operations with an expected cost are suffixed with a `~`.

|              | get(i) | insert(i)       | delete(i)      | update(m)        | popitem |
| ------------ | ------ | --------------- | -------------- | ---------------- | ------- |
| Cache        | O(1)~  | O(1)~*          | O(1)~          | O(m)~            | N/A     |
| FIFOCache    | O(1)~  | O(min(i, n-i))* | O(min(i, n-i)) | O(m*min(i, n-i)) | O(1)    |
| LFUCache     | O(1)~  | O(n)~*          | O(1)~          | O(m*n)~          | O(n)~*  |
| RRCache      | O(1)~  | O(1)~*          | O(1)~          | O(m)~            | O(1)~   |
| LRUCache     | O(1)~  | ?               | O(1)~          | ?                | O(1)    |
| TTLCache     | O(1)~  | O(min(i, n-i))* | O(min(i, n-i)) | O(m*min(i, n-i)) | O(1)    |
| VTTLCache    | O(1)~  | ?               | O(1)~          | ?                | O(1)~   |

## Frequently asked questions
#### What is the difference between TTLCache and VTTLCache?
In `TTLCache`, you set an expiration time for all items, but in `VTTLCache`,
you can set a unique expiration time for each item.

|              | TTL         | Speed   |
| ------------ | ----------- | ------- |
| TTLCache     | One ttl for all items       | TTLCache is very faster than VTTLCache |
| VTTLCache    | Each item has unique expiration time | VTTLCache is slow in inserting |


#### Can we set maxsize to zero?
Yes, if you pass zero to maxsize, means there's no limit for items.

#### Migrate from cachetools to cachebox
*cachebox* syntax is very similar to *cachetools*.
Just change these:
```python
# If you pass infinity to a cache implementation, change it to zero.
cachetools.Cache(math.inf) -> cachebox.Cache(0)
# If you use `isinstance` for cachetools classes, change those.
isinstance(cache, cachetools.Cache) -> isinstance(cache, cachebox.BaseCacheImpl)
```

## License
Copyright (c) 2024 aWolverP - **MIT License**

