Metadata-Version: 2.4
Name: rust-py-cache
Version: 0.2.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Database
Classifier: Topic :: System :: Distributed Computing
Summary: An ultra-fast local cache for Python, powered by Rust.
Keywords: cache,rust,pyo3,ttl,performance
Author-email: Roberto Lima <robertolima.izphera@gmail.com>
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/robertolima/rust-py-cache

# rust-py-cache

> **An ultra-fast local cache for Python, powered by Rust.**

A local, in-memory, thread-safe cache with TTL, lazy expiration, and metrics. The
core is written in Rust (PyO3 + maturin) on top of a concurrent `DashMap`; the
Python API is minimal. Think of it as a "mini Redis" living **inside** your Python
process.

[![PyPI](https://img.shields.io/pypi/v/rust-py-cache)](https://pypi.org/project/rust-py-cache/)
[![Python](https://img.shields.io/pypi/pyversions/rust-py-cache)](https://pypi.org/project/rust-py-cache/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](./LICENSE)

🌐 **Website:** [rust-py-cache.vercel.app](https://rust-py-cache.vercel.app/)

## Installation

```bash
pip install rust-py-cache
```

To work on it locally (requires Rust + maturin):

```bash
python -m venv .venv && source .venv/bin/activate
pip install maturin pytest
maturin develop          # compiles the Rust core and installs into the venv
pytest                   # runs the tests
```

## Usage

```python
from rust_py_cache import Cache

cache = Cache()

cache.set("user:1", {"name": "Roberto"}, ttl=60)   # ttl in seconds
user = cache.get("user:1")                          # {"name": "Roberto"}
cache.get("missing", default=0)                     # 0

cache.exists("user:1")        # True (honors TTL)
cache.delete("user:1")        # True if removed, False if absent
cache.len()                   # approximate size
cache.keys()                  # list of keys
cache.cleanup_expired()       # remove expired entries; returns the count
cache.clear()                 # remove everything (keeps counters)
cache.stats()                 # {'hits','misses','sets','deletes','expired','evicted','size'}
```

### Bounded cache with LRU eviction

```python
# Cap the number of keys. When full and a new key arrives, evict the
# least-recently-used entry instead of rejecting the write.
cache = Cache(max_size=1000, eviction_policy="lru")
cache.eviction_policy            # "lru"

# Default policy is "reject": set() returns False when full (and the key is new).
cache = Cache(max_size=1000)     # eviction_policy="reject"
cache.set("a", 1)                # True / False
```

### Background expiration

```python
# A background thread reclaims expired entries every N seconds, so you don't
# have to call cleanup_expired() yourself. It stops when the cache is collected.
cache = Cache(cleanup_interval=30)   # seconds (int/float)
```

### Memoization decorator

```python
@cache.cached(ttl=60)
def add(a, b):
    return a + b

add(2, 3)   # runs and caches
add(2, 3)   # served from cache

# custom key (fixed string or callable):
@cache.cached(ttl=300, key=lambda user_id: f"user:{user_id}")
def load_user(user_id):
    ...
```

See full examples under [`examples/`](./examples) (FastAPI and Django).

## API

Constructor: `Cache(max_size=None, eviction_policy="reject", cleanup_interval=None)`.
`eviction_policy` must be `"reject"` or `"lru"` (any other value raises
`ValueError`). `cleanup_interval` (seconds, `> 0`) enables the background sweeper.

| Method | Description |
|---|---|
| `set(key, value, ttl=None)` | Store a value. `ttl` in seconds (`int`/`float`); `None` = no expiration; `ttl <= 0` → `ValueError`. Overwrites. Returns `True`, or `False` when full under `eviction_policy="reject"` and the key is new. |
| `get(key, default=None)` | The value, or `default` if missing/expired (expired entries are removed). |
| `delete(key)` | `True` if removed, `False` if it didn't exist. |
| `exists(key)` | `True`/`False`, honoring TTL. |
| `keys()` | List of keys (may include expired-but-not-yet-collected ones). |
| `len()` / `len(cache)` | Approximate size. |
| `clear()` | Remove everything (does not reset counters). |
| `cleanup_expired()` | Remove expired entries; returns how many. |
| `eviction_policy` (property) | The active policy: `"reject"` or `"lru"`. |
| `stats()` | `dict` with `hits, misses, sets, deletes, expired, evicted, size`. |
| `@cache.cached(ttl=None, key=None)` | Memoization decorator. |

## How it works

- **Serialization:** in the MVP, values are serialized with `pickle` (on the Python
  side, via PyO3) and stored as opaque bytes (`Vec<u8>`) in the Rust core.
- **Concurrency:** `DashMap` (a HashMap with per-shard locks) plus `AtomicU64`
  counters, with no global lock on the hot path. Thread-safe, no busy loop.
- **TTL:** expiration is **lazy** by default — an expired key is removed when
  accessed (`get`/`exists`) or via `cleanup_expired()`. Pass `cleanup_interval` to
  also run a **background** sweeper thread that reclaims expired keys on its own.
- **Eviction:** with `max_size` + `eviction_policy="lru"`, a full cache evicts the
  least-recently-used entry (recency updated on every `get` hit) to admit a new key.

## Limitations

- The cache is **process-local**: multiple workers = multiple independent caches.
- It does **not** replace Redis for distributed caching.
- Data is **lost** when the process restarts.
- `pickle` must **not** be used to deserialize untrusted data.
- Lazy TTL by default: without `cleanup_interval`, expired items may linger until
  accessed or until `cleanup_expired()` runs.

## Development

```bash
cargo test          # Rust core tests
maturin develop     # rebuild and install
pytest              # Python tests
```

> If `maturin develop` complains about both `VIRTUAL_ENV` and `CONDA_PREFIX` being
> set, run `conda deactivate` first, or use `env -u CONDA_PREFIX maturin develop`.

## Roadmap

Stages and next steps (LRU/LFU eviction, background expiration, configurable
serializer, namespaces, etc.) are in [ROADMAP.md](./ROADMAP.md).

## License

MIT

