Cache Framework

OpenViper provides a simple, robust caching abstraction that can be used directly or within your routing and background task layers. The core cache interacts asynchronously with the underlying store, ensuring non-blocking operations.

Getting Started

The simplest way to use the cache is to import the get_cache factory. It will automatically load the backend configured in your settings.

from openviper.cache import get_cache

async def my_view(request):
    cache = get_cache()

    # Check if we have cached data
    if await cache.has_key("my_expensive_data"):
        return await cache.get("my_expensive_data")

    # Compute and cache
    data = await compute_expensive_data()
    await cache.set("my_expensive_data", data, ttl=300) # cache for 5 minutes

    return data

Configuration

OpenViper uses a CACHES dictionary in settings.py OPTIONS:

CACHES = {
    "default": {
        "BACKEND": "openviper.cache.InMemoryCache",
        "OPTIONS": {"ttl": 300},
    },
    "redis": {
        "BACKEND": "openviper.cache.RedisCache",
        "OPTIONS": {"host": "localhost", "port": 6379, "db": 0},
    },
    "memcached": {
        "BACKEND": "openviper.cache.MemcachedCache",
        "OPTIONS": {"host": "localhost", "port": 11211},
    },
    "file": {
        "BACKEND": "openviper.cache.FileCache",
        "OPTIONS": {"cache_dir": ".cache/openviper"},
    },
    "dragonfly": {
        "BACKEND": "openviper.cache.DragonflyCache",
        "OPTIONS": {"host": "localhost", "port": 6379, "db": 0},
    },
}

The ttl option in OPTIONS sets the default time-to-live in seconds for backends that support it (InMemoryCache uses this as its default TTL).

Thread-safe singleton access is guaranteed via an internal lock.

Custom backends can be registered by dotted path:

CACHES = {
    "custom": {
        "BACKEND": "myapp.cache.MyCustomCache",
        "OPTIONS": {"endpoint": "custom-host:1234"},
    },
}

Built-in Backends

OpenViper ships with six cache backends covering local development through high-concurrency production deployments.

InMemoryCache

The default cache. Stores data in a Python dictionary. Suitable for local development or single-process deployments without critical cache persistence needs.

RedisCache

A production-ready asynchronous Redis backend.

Requirements: You must install the redis library (e.g., pip install redis).

CACHES = {
    "default": {
        "BACKEND": "openviper.cache.RedisCache",
        "OPTIONS": {"host": "localhost", "port": 6379, "db": 0},
    },
}

All keys are prefixed with ov:cache: by default. The clear() method uses SCAN and UNLINK to delete only matching keys - it never calls FLUSHDB.

MemcachedCache

An asynchronous Memcached backend using the aiomcache library.

Requirements: You must install the aiomcache library (e.g., pip install aiomcache).

CACHES = {
    "default": {
        "BACKEND": "openviper.cache.MemcachedCache",
        "OPTIONS": {"host": "localhost", "port": 11211},
    },
}

All keys are prefixed with ov:cache: by default. Note that Memcached does not support prefix-based key iteration, so clear() calls flush_all which clears the entire Memcached instance. Use separate Memcached instances or key prefixes to isolate data.

FileCache

A file-system-backed cache that stores each entry as a separate file under a configurable directory. Values are serialized with orjson and expired entries are lazily removed on access.

CACHES = {
    "default": {
        "BACKEND": "openviper.cache.FileCache",
        "OPTIONS": {"cache_dir": ".cache/openviper"},
    },
}

Suitable for single-server deployments or development environments where Redis/Memcached are not available. Not recommended for multi-server setups because the cache directory is local to each node. Keys are hex-encoded to prevent directory traversal attacks.

DatabaseCache

A database-backed cache using the OpenViper ORM. Values are serialized with orjson and stored in the openviper_cache_entries table.

CACHES = {
    "db": {
        "BACKEND": "openviper.cache.DatabaseCache",
    },
}

Supports PostgreSQL (INSERT ... ON CONFLICT upsert), SQLite (INSERT OR REPLACE), and a fallback ORM-based upsert for other dialects. Expired entries are lazily cleaned up on access.

DragonflyCache

A Dragonfly backend that inherits from RedisCache and uses the redis.asyncio client library. Dragonfly is a modern in-memory data store that speaks the Redis protocol and offers significantly higher multi-threaded throughput.

Requirements: You must install the redis library (e.g., pip install redis).

CACHES = {
    "default": {
        "BACKEND": "openviper.cache.DragonflyCache",
        "OPTIONS": {"host": "localhost", "port": 6379, "db": 0},
    },
}

All keys are prefixed with ov:df: by default to isolate them from Redis data in the same instance. The clear() method uses SCAN and UNLINK for safe, non-blocking removal.

Creating Custom Backends

If you need to store your cache in a different system (like AWS ElastiCache or a custom distributed store), you can build a custom backend.

  1. Create a class that inherits from openviper.cache.base.BaseCache.

  2. Implement the required async methods: get, set, delete, has_key, and clear.

# myapp/cache.py
from typing import Any
from openviper.cache.base import BaseCache

class ElasticacheBackend(BaseCache):
    def __init__(self, *, endpoint: str, port: int = 6379):
        # ... initialize connection
        pass

    async def get(self, key: str, default: Any = None) -> Any:
        # ... read from ElastiCache
        pass

    async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
        # ... write to ElastiCache
        pass

    async def delete(self, key: str) -> None:
        pass

    async def has_key(self, key: str) -> bool:
        pass

    async def clear(self) -> None:
        pass
  1. Point your settings to the custom backend using a dotted module path:

# settings.py
CACHES = {
    "default": {
        "BACKEND": "myapp.cache.ElasticacheBackend",
        "OPTIONS": {"endpoint": "my-cluster.xxxxxx.use1.cache.amazonaws.com"},
    },
}

When get_cache() is called, OpenViper will dynamically import and instantiate your class.

API Reference

openviper.cache

get_cache(alias='default') BaseCache

Return the cache backend for the given alias, creating it on first access. Instances are stored in cache_instances and reused on subsequent calls. Thread-safe via cache_lock.

  • "default" alias with no CACHES setting returns an InMemoryCache.

  • Unknown non-default aliases raise ValueError.

cache_instances

Module-level dict[str, BaseCache] holding instantiated backends. Populated by get_cache().

cache_lock

threading.Lock guarding concurrent access to cache_instances.

openviper.cache.base

class BaseCache

Abstract base class for all cache backends. Concrete subclasses must call validate_cache_key(key) before any operation.

async get(key, default=None) -> Any

Fetch a value from the cache. Return default on miss.

async set(key, value, ttl=None) -> None

Store a value. ttl is time-to-live in seconds; None means no expiry.

async delete(key) -> None

Remove a value from the cache.

async clear() -> None

Remove all values from the cache.

async has_key(key) -> bool

Check whether a key exists. Default calls get() and checks for None; backends with a cheaper existence check should override.

async keys(prefix="") -> list[str]

Return all cache keys, optionally filtered by prefix. Default returns []; backends that can enumerate keys should override. The in-memory implementation performs a single-pass sweep: expired entries are evicted and matching keys are collected in one iteration over the data store.

async close() -> None

Release backend resources (connections, file handles). Backends that hold persistent connections (e.g. RedisCache, MemcachedCache) override this method to ensure clean shutdown. The default implementation is a no-op for backends that do not own connections.

openviper.cache.memory

class InMemoryCache(BaseCache)

In-memory cache backed by a dict. Thread-safe for concurrent async access via an asyncio.Lock. When no ttl is provided to set(), CACHES['default']['OPTIONS']['ttl'] is used as the default.

openviper.cache.redis

class RedisCache(BaseCache)

Redis-backed cache using redis.asyncio with orjson serialization. Requires the redis package. All keys are prefixed with key_prefix (default "ov:cache:").

__init__(*, key_prefix='ov:cache:', **kwargs)

Initialise the Redis client. Keyword arguments are forwarded to redis.asyncio.Redis().

openviper.cache.memcached

class MemcachedCache(BaseCache)

Memcached-backed cache using aiomcache with orjson serialization. Requires the aiomcache package. All keys are prefixed with key_prefix (default "ov:cache:").

__init__(*, key_prefix='ov:cache:', host='localhost', port=11211, **kwargs)

Initialise the aiomcache client. Keyword arguments are forwarded to aiomcache.Client().

Note

clear() calls flush_all which clears the entire Memcached instance. Use separate instances or key prefixes to isolate data.

openviper.cache.file

class FileCache(BaseCache)

File-system-backed cache using async I/O with orjson serialization. Each entry is stored as a separate file under cache_dir. Keys are hex-encoded to prevent directory traversal attacks. Expired entries are lazily removed on access.

__init__(*, cache_dir='.cache/openviper', key_prefix='ov:cache:', **kwargs)

Initialise the file cache with a directory path and optional prefix.

safe_filename(key: str) str

Convert a cache key into a safe hex-encoded filesystem path component.

openviper.cache.db_backend

class DatabaseCache(BaseCache)

Database-backed cache using the OpenViper ORM with orjson serialization. Supports PostgreSQL, SQLite, and fallback ORM-based upsert.

openviper.cache.dragonfly

class DragonflyCache(RedisCache)

Dragonfly-backed cache inheriting from RedisCache. Uses redis.asyncio with orjson serialization. Requires the redis package (Dragonfly speaks the Redis protocol). All keys are prefixed with key_prefix (default "ov:df:").

__init__(*, key_prefix='ov:df:', host='localhost', port=6379, db=0, **kwargs)

Initialise the Dragonfly cache. Delegates to RedisCache.__init__ with Dragonfly-specific defaults.

Note

clear() uses SCAN and UNLINK to delete only matching keys - it never calls FLUSHDB.

openviper.cache.redis

class RedisCache(BaseCache, *, key_prefix='ov:cache:', **kwargs)

Redis-backed cache using redis.asyncio with orjson serialization. Requires the redis package (pip install redis).

All keys are prefixed with key_prefix to isolate this cache from other data in the same Redis database. clear() only deletes keys matching the prefix via SCAN + UNLINK - it never calls FLUSHDB.

DEFAULT_KEY_PREFIX

Default Redis key prefix: "ov:cache:".

openviper.cache.db_backend

class DatabaseCache(BaseCache)

Database-backed cache using the OpenViper ORM with orjson serialization. Supports PostgreSQL (INSERT ... ON CONFLICT), SQLite (INSERT OR REPLACE), and a fallback ORM-based upsert for other dialects.

is_entry_expired(expires_at) bool

Return True when expires_at is in the past relative to now(). Handles timezone-aware/naive mismatches by converting to a common timezone before comparison.

validate_table_name(name) str

Validate that name is a safe SQL table identifier. Delegates to openviper.db.utils.validate_identifier() with description="table name". Raises ValueError on invalid input.

openviper.cache.db

class CacheEntry(Model)

ORM model for database-backed cache storage.

key

CharField(max_length=512, unique=True)

value

TextField

expires_at

DateTimeField(null=True)

openviper.cache.validation

validate_cache_key(key) str

Validate and return a cache key. Raises ValueError if the key is empty, exceeds CACHE_KEY_MAX_LEN characters, or contains whitespace.

CACHE_KEY_MAX_LEN

Maximum allowed cache key length: 250.

CACHE_KEY_RE

Compiled regex ^\\S+$ used to reject whitespace in cache keys.