Skip to main content

Lightweight, extensible Python caching library built on hexagonal architecture. Use built-in TTL, LRU, LFU policies or plug in your own — custom policies, storages, and metrics collectors supported out of the box. Zero dependencies.

Project description

⚡ Snaps

A snap — and your data is already there.

Snaps is a lightweight, extensible Python caching library built on clean hexagonal architecture. Plug in any policy, any storage, any metrics — or use the smart defaults and forget about it.


✨ Features

  • 🔌 Fully extensible — plug in custom policies, storages, and metrics collectors
  • 🧠 Built-in policies — TTL, LRU, LFU, or combine them
  • 🎯 Template keys — control your cache key format with key="user-{user_id}"
  • 📊 Stats built-in — hits, misses, evictions, hit rate out of the box
  • 🔒 Thread-safe — all built-in components use locks
  • 🏛️ Hexagonal architecture — core ports are fully decoupled from infrastructure
  • 🐍 Pure Python — zero dependencies, stdlib only

📦 Installation

pip install snaps-cacher

🚀 Quick Start

from cacher.presentations import snap

@snap()
def get_user(user_id: int) -> dict:
    return fetch_from_db(user_id)  # called only once per unique user_id

get_user(1)   # fetches from DB
get_user(1)   # returns from cache instantly

📖 Usage

Basic caching — no policy

Entries live forever until you clear manually.

@snap()
def slow_compute(x: int) -> int:
    time.sleep(1)  # heavy computation
    return x * x

slow_compute(5)   # ~1s
slow_compute(5)   # ~0.000001s — from cache

slow_compute.clear()   # clear when needed
slow_compute.stats()   # {'metrics': {...}, 'storage': {'size': 0}}

TTL — Time To Live

Entry expires after N seconds. Supports absolute and sliding modes.

# Absolute TTL — expires N seconds after creation
@snap(ttl=(60, False))
def get_exchange_rate(currency: str) -> float:
    return fetch_rate(currency)

# Sliding TTL — TTL resets on every access
@snap(ttl=(60, True))
def get_session(session_id: str) -> dict:
    return load_session(session_id)

LRU — Least Recently Used

Evicts the entry that hasn't been accessed for the longest time. Requires max_size and evictions_limit.

@snap(lru=True, max_size=1000, evictions_limit=10)
def load_product(product_id: int) -> dict:
    return db.fetch_product(product_id)

LFU — Least Frequently Used

Evicts the entry that has been accessed the fewest times. Requires max_size and evictions_limit.

@snap(lfu=True, max_size=500, evictions_limit=5)
def get_config(key: str) -> str:
    return config_service.get(key)

Combining policies

Policies work together — entry is invalid if any policy says so.

# TTL expires after 5 minutes AND LRU keeps only 1000 entries
@snap(ttl=(300, False), lru=True, max_size=1000, evictions_limit=10)
def get_post(post_id: int) -> dict:
    return db.fetch_post(post_id)

Template key

Control exactly how your cache key is formed. Inspired by the idea from EzyGang/py-cachify.

@snap(key="report:{year}-{month}:type-{report_type}")
def generate_report(year: int, month: int, report_type: str) -> dict:
    return heavy_report_generation(year, month, report_type)

generate_report(2025, 1, "sales")    # key: "report:2025-1:type-sales"
generate_report(2025, 1, "finance")  # key: "report:2025-1:type-finance"

Stats and clear

Every decorated function gets .stats() and .clear() attached automatically.

@snap(lru=True, max_size=100, evictions_limit=5)
def fetch(x: int) -> int:
    return x * 2

fetch(1)
fetch(2)
fetch(1)  # hit

print(fetch.stats())
# {
#   'metrics': {
#       'hits': 1,
#       'misses': 2,
#       'evictions': 0,
#       'hit_rate': 0.333
#   },
#   'storage': {'size': 2}
# }

fetch.clear()  # reset everything

🔌 Extensibility

Custom policy

from cacher.core import Policy, CacheEntry
from collections.abc import Hashable, Sequence

class MaxAccessPolicy(Policy):
    """Invalidates entry after N accesses."""

    requires_max_size: bool = False

    def __init__(self, max_accesses: int) -> None:
        self._max_accesses = max_accesses

    def is_valid(self, key: Hashable, entry: CacheEntry) -> bool:
        return entry.access_count <= self._max_accesses

    def on_add(self, key: Hashable, entry: CacheEntry) -> None: pass
    def on_access(self, key: Hashable, entry: CacheEntry) -> None: pass
    def on_remove(self, key: Hashable, entry: CacheEntry) -> None: pass
    def on_clear(self) -> None: pass
    def evict_candidates(self, limit: int) -> Sequence[Hashable]: return []


@snap(policies=[MaxAccessPolicy(max_accesses=3)])
def get_token(user_id: int) -> str:
    return generate_token(user_id)

Custom storage

from cacher.core import Storage, CacheEntry
from collections.abc import Hashable, Sequence

class RedisStorage(Storage):
    """Example: Redis-backed storage."""

    def __init__(self, client) -> None:
        self._client = client

    def get(self, key: Hashable) -> CacheEntry | None:
        ...

    def put(self, key: Hashable, entry: CacheEntry) -> None:
        ...

    # implement: delete, contains, size, clear, keys


@snap(storage=RedisStorage(redis_client))
def get_user(user_id: int) -> dict:
    return db.fetch_user(user_id)

Custom metrics

from cacher.core import MetricsCollector
from collections.abc import Hashable, Mapping

class PrometheusMetrics(MetricsCollector):
    """Send metrics to Prometheus."""

    def hit(self, key: Hashable) -> None:
        cache_hits_total.inc()

    def miss(self, key: Hashable) -> None:
        cache_misses_total.inc()

    def evict(self, key: Hashable) -> None:
        cache_evictions_total.inc()

    def reset(self) -> None: ...
    def stats(self) -> Mapping: ...


@snap(metrics=PrometheusMetrics())
def get_data(key: str) -> dict:
    return fetch(key)

🏛️ Architecture

Snaps is built on hexagonal (ports & adapters) architecture. The core domain has zero knowledge of infrastructure.

snaps/cacher/
│
├── core/                    ← domain — no external dependencies
│   ├── entry.py             ← CacheEntry: value + metadata
│   └── ports/
│       ├── storage.py       ← Storage port (abstract)
│       ├── policy.py        ← Policy port (abstract)
│       ├── metrics.py       ← MetricsCollector port (abstract)
│       └── orchestrator.py  ← Orchestrator port (abstract)
│
├── storages/                ← infrastructure
│   └── memory.py            ← InMemoryStorage
│
├── policies/                ← plugins
│   ├── ttl.py               ← TTLPolicy
│   ├── lru.py               ← LRUPolicy
│   └── lfu.py               ← LFUPolicy
│
├── metrics/
│   └── memory.py            ← InMemoryMetrics
│
├── orchestrators/
│   ├── simple.py            ← SimpleOrchestrator (one policy)
│   └── composite.py         ← CompositeOrchestrator (many policies)
│
├── utils/
│   └── key_gen.py           ← auto key + template key generation
│
└── presentations/
    └── decorator.py         ← @snap — single entry point

🙏 Credits

  • Template key generation idea inspired by EzyGang/py-cachify — a production-ready caching library with distributed locks support worth checking out.

📄 License

MIT © Umidjon Khodjaev


⚙️ Changelog

See CHANGELOG.md for release history.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snaps_cacher-0.1.1.tar.gz (18.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

snaps_cacher-0.1.1-py3-none-any.whl (26.5 kB view details)

Uploaded Python 3

File details

Details for the file snaps_cacher-0.1.1.tar.gz.

File metadata

  • Download URL: snaps_cacher-0.1.1.tar.gz
  • Upload date:
  • Size: 18.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.4 CPython/3.12.3 Linux/6.17.0-22-generic

File hashes

Hashes for snaps_cacher-0.1.1.tar.gz
Algorithm Hash digest
SHA256 17d860d0078fb571acb33f43fae8dadd93178bb8a6459a3e5043d57ec5a0e1bc
MD5 0f5780127a529df23f691fa6c9fb7958
BLAKE2b-256 4ec7ab74465bea2782e1ba3131a6d99b610b0910c047c5999f3e1a5ba9d94eb5

See more details on using hashes here.

File details

Details for the file snaps_cacher-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: snaps_cacher-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 26.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.4 CPython/3.12.3 Linux/6.17.0-22-generic

File hashes

Hashes for snaps_cacher-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9d398d3a323f84033864f5d5b779027e8642887179b0db4ab9a4ae2040057ab6
MD5 2cff79dabbeacbefbf968740803113ec
BLAKE2b-256 c318caf69f55e4217d333b95d8a23685841175bb77325f23ad02df893a72880e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page