From 93f33daad96041b76ef02c8c5cdbcaf6db9888f7 Mon Sep 17 00:00:00 2001 From: Alexander Dmitriev Date: Tue, 26 May 2026 18:02:52 +0400 Subject: [PATCH 1/3] docs: update documentation to reflect project name change from py-cashier to cachium --- README.md | 4 +-- docs/api/builders.md | 7 +++-- docs/api/cache.md | 6 +++-- docs/api/serializers.md | 4 +++ docs/changelog.md | 2 +- docs/contributing.md | 8 +++--- docs/examples.md | 56 ++++++++++++++++++++++----------------- docs/guides/concepts.md | 15 ++++++----- docs/guides/quickstart.md | 14 +++++----- docs/guides/tutorials.md | 8 +++--- docs/index.md | 37 ++++++++++++++++++++------ docs/installation.md | 6 ++--- mkdocs.yml | 8 ++++++ pyproject.toml | 12 ++++----- 14 files changed, 117 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 628d84a..49678f1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ def expensive_calculation(x: int, y: int) -> int: return x + y # First call performs the calculation -result1 = expensive_calculation(1, 2) # Output: Calculating 1 + 2 +result1 = expensive_calculation(1, 2) print(result1) # Output: 3 # Second call uses cached result @@ -39,7 +39,7 @@ print(result2) # Output: 3 # Async function caching works too @cache(storage=TTLMapAsyncStorage.create_with(max_size=100, ttl=None)) async def async_io_operation(x: int, y: int) -> int: - await asyncio.sleep(1) # Simulate an I/O-bound operation + await asyncio.sleep(0.01) # Simulate an I/O-bound operation return x + y diff --git a/docs/api/builders.md b/docs/api/builders.md index 7adb8ba..95cd890 100644 --- a/docs/api/builders.md +++ b/docs/api/builders.md @@ -1,11 +1,14 @@ # Key Builders -Key builders construct the cache key for a given function call. The DefaultKeyBuilder uses the function path and selected arguments (by default all args/kwargs) serialized via a serializer (repr by default). Choose a custom builder when you need to exclude framework objects, add prefixes/versions, or change serialization. +Key builders construct the cache key for a given function call. The `DefaultKeyBuilder` uses the function file path, function name, and selected arguments (by default all args/kwargs) serialized via a serializer (`repr` by default). Choose a custom builder when you need to exclude framework objects, add prefixes/versions, or change serialization. + +The `cache` decorator accepts a key-builder factory: a callable that returns a `KeyBuilder` instance. Classes with no required constructor arguments work directly, for example `key_builder=MyKeyBuilder`. Common parameters (DefaultKeyBuilder): - func: the original function; used to extract signature and names. -- key_serializer: Serializer type used to turn values into stable strings (e.g., ReprSerializer, Md5Serializer). +- key_serializer: `Serializer` type used to turn values into stable strings (e.g., `ReprSerializer`, `Md5Serializer`). - prefix: optional string to namespace or version keys. +- delimiter: string used between argument key/value pairs. Related pages: Concepts (key model), Examples (custom builders), and Serializers. Full reference below. diff --git a/docs/api/cache.md b/docs/api/cache.md index f54b3ce..d3f751d 100644 --- a/docs/api/cache.md +++ b/docs/api/cache.md @@ -8,8 +8,10 @@ When to use: - You want to avoid thundering herds via per-key locks. Common parameters: -- storage: callable returning the storage instance (TTLMapStorage for sync, TTLMapAsyncStorage for async). -- key_builder: optional callable returning a KeyBuilder to customize how keys are built (which args participate, serialization, prefixes, etc.). +- storage: required callable returning the storage instance (`TTLMapStorage` for sync, `TTLMapAsyncStorage` for async). +- key_builder: optional callable returning a `KeyBuilder` to customize how keys are built (which args participate, serialization, prefixes, etc.). + +The storage factory is called once at decoration time, so each decorated function gets its own storage instance. Passing a sync storage to an async function, or an async storage to a sync function, raises `TypeError`. See also: Quickstart, Concepts, and Examples pages. Full reference below. diff --git a/docs/api/serializers.md b/docs/api/serializers.md index 748d52e..a73bd32 100644 --- a/docs/api/serializers.md +++ b/docs/api/serializers.md @@ -23,3 +23,7 @@ Related pages: Key Builders and Concepts. Full reference below. --- ::: cachium.serializers._std_hash.StdHashSerializer + +--- + +::: cachium.serializers._md5.Md5Serializer diff --git a/docs/changelog.md b/docs/changelog.md index a0a03f8..9ecf955 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,4 +2,4 @@ See the project changelog on GitHub: -- https://github.com/zoola969/py-cashier/blob/main/CHANGELOG.md +- https://github.com/zoola969/cachium/blob/main/CHANGELOG.md diff --git a/docs/contributing.md b/docs/contributing.md index 3656bf7..a77fa04 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -12,13 +12,13 @@ This page combines our previous Development Guide and contribution guidelines. Install the docs toolchain: ```bash -pip install mkdocs mkdocs-material "mkdocstrings[python]" +uv sync --group docs ``` Preview documentation locally: ```bash -mkdocs serve +uv run mkdocs serve ``` This will start a local server (usually http://127.0.0.1:8000/). @@ -26,7 +26,7 @@ This will start a local server (usually http://127.0.0.1:8000/). ## Running tests ```bash -pytest -q +uv run pytest ``` ## Linting and typing @@ -36,4 +36,4 @@ pytest -q ## Opening issues and PRs -Open issues and PRs on GitHub: https://github.com/zoola969/py-cashier +Open issues and PRs on GitHub: https://github.com/zoola969/cachium diff --git a/docs/examples.md b/docs/examples.md index 36823be..80145bb 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -10,7 +10,7 @@ This page provides examples of how to use cachium in various scenarios. from cachium import cache from cachium.storages.ttl_map import TTLMapStorage -@cache() +@cache(storage=TTLMapStorage.create_with()) def fibonacci(n: int) -> int: """Calculate the nth Fibonacci number.""" if n <= 1: @@ -33,12 +33,12 @@ import asyncio from cachium import cache from cachium.storages.ttl_map import TTLMapAsyncStorage -@cache() +@cache(storage=TTLMapAsyncStorage.create_with()) async def fetch_data(user_id: int) -> dict: """Simulate fetching user data from a database.""" print(f"Fetching data for user {user_id}...") # Simulate network delay - await asyncio.sleep(1) + await asyncio.sleep(0.01) return {"id": user_id, "name": f"User {user_id}"} async def main(): @@ -63,7 +63,7 @@ from cachium import cache from cachium.storages.ttl_map import TTLMapStorage import time -@cache(storage=TTLMapStorage.create_with(ttl=timedelta(seconds=5))) +@cache(storage=TTLMapStorage.create_with(ttl=timedelta(milliseconds=10))) def get_timestamp() -> float: """Return the current timestamp.""" return time.time() @@ -78,7 +78,7 @@ print(f"Timestamp 2: {ts2}") print(f"Same timestamp: {ts1 == ts2}") # Output: True # Wait for TTL to expire -time.sleep(6) +time.sleep(0.02) # Third call (after TTL expired) - returns new value ts3 = get_timestamp() @@ -118,15 +118,19 @@ process_data(2) # No output (cached) ```python from cachium import cache -from cachium.key_builders import DefaultKeyBuilder +from cachium.key_builders import KeyBuilder +from cachium.storages.ttl_map import TTLMapStorage -# Only consider the first argument for caching -key_builder = DefaultKeyBuilder( - func=lambda x, *args, **kwargs: x, - prefix="first_arg_only" -) +class FirstArgumentKeyBuilder(KeyBuilder): + """Build keys from only the first positional argument.""" -@cache(key_builder=key_builder) + def build_key(self, *args, **kwargs) -> str: + return f"first_arg_only:{args[0]}" + +@cache( + storage=TTLMapStorage.create_with(), + key_builder=FirstArgumentKeyBuilder, +) def add_numbers(a: int, b: int) -> int: print(f"Adding {a} + {b}") return a + b @@ -149,17 +153,21 @@ print(result3) # Output: 17 ```python from cachium import cache -from cachium.key_builders import DefaultKeyBuilder +from cachium.key_builders import KeyBuilder from cachium.serializers import Md5Serializer +from cachium.storages.ttl_map import TTLMapStorage -# Use MD5 serializer for consistent hashing across processes -key_builder = DefaultKeyBuilder( - func=lambda x, y: x + y, - key_serializer=Md5Serializer -) +class HashedArgsKeyBuilder(KeyBuilder): + """Build compact keys from args and kwargs.""" + + def build_key(self, *args, **kwargs) -> str: + return Md5Serializer.serialize((args, sorted(kwargs.items()))) -@cache(key_builder=key_builder) +@cache( + storage=TTLMapStorage.create_with(), + key_builder=HashedArgsKeyBuilder, +) def compute_value(x: int, y: int) -> int: print(f"Computing {x} * {y}") return x * y @@ -179,8 +187,8 @@ print(result_again) # Output: 50 ### Caching Database Queries ```python -from py_cashier import cache -from py_cashier._storages import TTLMapStorage +from cachium import cache +from cachium.storages.ttl_map import TTLMapStorage from datetime import timedelta import sqlite3 @@ -224,14 +232,14 @@ import asyncio import random from datetime import timedelta from cachium import cache -from cachium.storages.ttl_map import TTLMapStorage +from cachium.storages.ttl_map import TTLMapAsyncStorage -@cache(storage=TTLMapStorage.create_with(ttl=timedelta(minutes=5))) +@cache(storage=TTLMapAsyncStorage.create_with(ttl=timedelta(minutes=5))) async def fetch_weather(city: str) -> dict: """Simulate fetching weather data from an API.""" print(f"Fetching weather data for {city}...") # Simulate API request delay - await asyncio.sleep(1) + await asyncio.sleep(0.01) # Return simulated weather data return { "city": city, diff --git a/docs/guides/concepts.md b/docs/guides/concepts.md index c63ce84..407fbf6 100644 --- a/docs/guides/concepts.md +++ b/docs/guides/concepts.md @@ -1,19 +1,20 @@ # Concepts -This page explains the core concepts behind py-cashier so you can reason about cache hits, misses, and correctness in your application. +This page explains the core concepts behind cachium so you can reason about cache hits, misses, and correctness in your application. ## Cache key model -- What becomes the key: By default, keys are built from the fully qualified function name and its arguments. The default builder serializes each participating argument using a stable serializer (repr-based by default) to form a deterministic string key. -- Selective arguments: Use the CacheWith type annotation to mark which parameters should participate in the key. This helps avoid including non-deterministic or heavy framework objects (e.g., requests, DB sessions) that would explode key cardinality. -- Custom builders and serializers: Provide your own KeyBuilder to control which args/kwargs and in what order are included, and plug different serializers (e.g., MD5) when you need shorter or cross-process-stable keys. +- What becomes the key: By default, keys are built from the function file path, function name, and arguments. The default builder serializes each participating argument using `repr()` to form a deterministic string key. +- Selective arguments: Use the `CacheWith` type annotation to mark which parameters should participate in the key. If any parameter is marked, only marked parameters are included. This helps avoid non-deterministic or heavy framework objects such as requests or DB sessions. +- Custom builders and serializers: Provide your own `KeyBuilder` factory to control which args/kwargs are included and how they are serialized. Use serializers such as `Md5Serializer` when you need compact keys. ## Storage model -- In-memory TTL + LRU: The built-in TTLMapStorage/TTLMapAsyncStorage keep values in-memory with time-to-live expiration and a least-recently-used eviction policy when max_size is reached. -- Sizing: max_size bounds the number of cached entries. When the cache is full, the least recently used entries are evicted first. TTL expiration removes entries after their time window passes. +- In-memory TTL + LRU: The built-in `TTLMapStorage` and `TTLMapAsyncStorage` keep values in-memory with time-to-live expiration and a least-recently-used eviction policy when `max_size` is reached. +- Sizing: `max_size` bounds the number of cached entries. When the cache is full, the least recently used entries are evicted first. TTL expiration removes entries after their time window passes. Set `ttl=None` for entries that do not expire by age. +- Storage factories: The `cache` decorator expects a callable that creates storage, for example `TTLMapStorage.create_with(...)`. The factory is called once at decoration time, so each decorated function gets its own storage instance. - Fit for purpose: In-memory storages are ideal for function-level caching within a single process. For multi-process or cross-machine caches, use an external store (e.g., Redis) — planned for future releases. ## Concurrency model -- Per-key locking: py-cashier prevents dog-piling by ensuring only one caller computes a missing value per key. Others wait and reuse the result. This applies to both sync and async flows with appropriate lock types. +- Per-key locking: cachium prevents dog-piling by ensuring only one caller computes a missing value per key. Others wait and reuse the result. This applies to both sync and async flows with appropriate lock types. - Safety: The decorator verifies that sync functions use a sync storage and async functions use an async storage to avoid accidental cross-usage. - Granularity: Locks are per-key, so independent keys proceed in parallel. diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md index c3459a2..957e913 100644 --- a/docs/guides/quickstart.md +++ b/docs/guides/quickstart.md @@ -11,8 +11,8 @@ Notes: ```python from datetime import timedelta -from py_cashier import cache -from py_cashier.storages.ttl_map import TTLMapStorage +from cachium import cache +from cachium.storages.ttl_map import TTLMapStorage # Configure TTL and max size @cache(storage=TTLMapStorage.create_with(max_size=512, ttl=timedelta(seconds=30))) @@ -28,14 +28,14 @@ print(add(1, 2)) # cached ```python import asyncio from datetime import timedelta -from py_cashier import cache -from py_cashier.storages.ttl_map import TTLMapAsyncStorage +from cachium import cache +from cachium.storages.ttl_map import TTLMapAsyncStorage # Configure TTL and max size for async storage @cache(storage=TTLMapAsyncStorage.create_with(max_size=512, ttl=timedelta(seconds=30))) async def add_async(a: int, b: int) -> int: # Simulate I/O - await asyncio.sleep(0.1) + await asyncio.sleep(0.01) return a + b async def main(): @@ -49,8 +49,8 @@ asyncio.run(main()) ```python from typing import Annotated -from py_cashier import cache, CacheWith -from py_cashier.storages.ttl_map import TTLMapStorage +from cachium import cache, CacheWith +from cachium.storages.ttl_map import TTLMapStorage # Only `x` participates in the cache key; calls differing only by `y` share the cached result @cache(storage=TTLMapStorage.create_with()) diff --git a/docs/guides/tutorials.md b/docs/guides/tutorials.md index d9746d8..c670e64 100644 --- a/docs/guides/tutorials.md +++ b/docs/guides/tutorials.md @@ -6,8 +6,8 @@ This section contains step-by-step guides for common workflows. ```python from datetime import timedelta -from py_cashier import cache -from py_cashier.storages.ttl_map import TTLMapStorage +from cachium import cache +from cachium.storages.ttl_map import TTLMapStorage @cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(minutes=10))) def get_item(key: str) -> str: @@ -18,8 +18,8 @@ def get_item(key: str) -> str: ```python from typing import Annotated -from py_cashier import cache, CacheWith -from py_cashier.storages.ttl_map import TTLMapStorage +from cachium import cache, CacheWith +from cachium.storages.ttl_map import TTLMapStorage # Cache only by `x`, ignore `y` in the cache key @cache(storage=TTLMapStorage.create_with()) diff --git a/docs/index.md b/docs/index.md index 0ffe0bf..5404e9c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,22 +32,43 @@ from cachium.storages.ttl_map import TTLMapStorage def add(a: int, b: int) -> int: return a + b -# Simple function caching -@cache() -def expensive_calculation(x: int, y: int) -> int: - print(f"Calculating {x} + {y}") - return x + y +print(add(1, 2)) +print(add(1, 2)) # cached +``` Async: + ```python import asyncio from datetime import timedelta from cachium import cache from cachium.storages.ttl_map import TTLMapAsyncStorage -# Second call uses cached result -result2 = expensive_calculation(1, 2) # No calculation performed -print(result2) # Output: 3 +@cache(storage=TTLMapAsyncStorage.create_with(max_size=1024, ttl=timedelta(minutes=1))) +async def expensive_calculation(x: int, y: int) -> int: + await asyncio.sleep(0.01) + return x + y + +async def main(): + print(await expensive_calculation(1, 2)) + print(await expensive_calculation(1, 2)) # cached + +asyncio.run(main()) +``` + +Selective key arguments: + +```python +from typing import Annotated +from cachium import CacheWith, cache +from cachium.storages.ttl_map import TTLMapStorage + +@cache(storage=TTLMapStorage.create_with()) +def compute(x: Annotated[int, CacheWith()], y: int) -> int: + return x + y + +print(compute(1, 10)) +print(compute(1, 999)) # cached by x only ``` ## License diff --git a/docs/installation.md b/docs/installation.md index 54e9c85..40aee4b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -5,17 +5,17 @@ Supported Python versions: 3.10 – 3.14 Install from PyPI: ```bash -pip install py-cashier +pip install cachium ``` Upgrade to latest: ```bash -pip install -U py-cashier +pip install -U cachium ``` Verify installation: ```bash -python -c "import py_cashier; print(py_cashier.__version__)" +python -c "import cachium; print(cachium.__version__)" ``` diff --git a/mkdocs.yml b/mkdocs.yml index 9c0f459..e87b6ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,13 +24,21 @@ theme: nav: - Home: index.md + - Installation: installation.md + - Guides: + - Quickstart: guides/quickstart.md + - Concepts: guides/concepts.md + - Tutorials: guides/tutorials.md - API Reference: - Overview: api/index.md - Cache: api/cache.md - Builders: api/builders.md - Serializers: api/serializers.md + - Storages: api/storages.md - Utilities: api/utilities.md - Examples: examples.md + - Changelog: changelog.md + - Contributing: contributing.md markdown_extensions: - pymdownx.highlight: diff --git a/pyproject.toml b/pyproject.toml index ee5f555..27c2355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling", "uv-dynamic-versioning"] [dependency-groups] dev = [ - "black>=25.1,<27.0", + "black~=26.0", "pre-commit~=4.0", ] docs = [ @@ -14,9 +14,9 @@ docs = [ ] test = [ "coverage~=7.3", - "mypy>=1.17,<3.0", - "pytest-asyncio>=0.24,<2.0", - "pytest>=8.1,<10.0", + "mypy~=2.0", + "pytest-asyncio~=1.0", + "pytest~=9.0", ] [project] @@ -51,8 +51,8 @@ readme = "README.md" requires-python = ">=3.10" [project.urls] -Changelog = "https://github.com/zoola969/cachium/CHANGELOG.md" -Documentation = "https://cachium.readthedocs.io/en/latest/" +Changelog = "https://github.com/zoola969/cachium/blob/main/CHANGELOG.md" +Documentation = "https://zoola969.github.io/cachium/" Homepage = "https://github.com/zoola969/cachium" Issues = "https://github.com/zoola969/cachium/issues" Repository = "https://github.com/zoola969/cachium.git" From 355910ca1cf359d2af4182496b6da2bbb0b4d957 Mon Sep 17 00:00:00 2001 From: Alexander Dmitriev Date: Tue, 26 May 2026 18:05:40 +0400 Subject: [PATCH 2/3] deps: update package specifications for black, mypy, pytest, and pytest-asyncio --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 85ad35b..82d7934 100644 --- a/uv.lock +++ b/uv.lock @@ -155,7 +155,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=25.1,<27.0" }, + { name = "black", specifier = "~=26.0" }, { name = "pre-commit", specifier = "~=4.0" }, ] docs = [ @@ -165,9 +165,9 @@ docs = [ ] test = [ { name = "coverage", specifier = "~=7.3" }, - { name = "mypy", specifier = ">=1.17,<3.0" }, - { name = "pytest", specifier = ">=8.1,<10.0" }, - { name = "pytest-asyncio", specifier = ">=0.24,<2.0" }, + { name = "mypy", specifier = "~=2.0" }, + { name = "pytest", specifier = "~=9.0" }, + { name = "pytest-asyncio", specifier = "~=1.0" }, ] [[package]] From b3f466e8d56999063951395b15ab6e0718f28be9 Mon Sep 17 00:00:00 2001 From: Alexander Dmitriev Date: Tue, 26 May 2026 18:09:00 +0400 Subject: [PATCH 3/3] docs: add documentation for built-in storage options and usage examples --- docs/api/storages.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/api/storages.md diff --git a/docs/api/storages.md b/docs/api/storages.md new file mode 100644 index 0000000..e20b566 --- /dev/null +++ b/docs/api/storages.md @@ -0,0 +1,46 @@ +# Storages + +Storages keep cached values and provide the per-key locks used by the decorator to prevent dog-piling. + +Built-in storages: +- `TTLMapStorage`: synchronous in-memory storage with TTL expiration and LRU eviction. +- `TTLMapAsyncStorage`: asynchronous counterpart for `async def` functions. + +Usage: + +```python +from datetime import timedelta +from cachium import cache +from cachium.storages.ttl_map import TTLMapStorage + +@cache(storage=TTLMapStorage.create_with(max_size=1000, ttl=timedelta(minutes=5))) +def get_value(key: str) -> str: + return f"value:{key}" +``` + +Important details: +- `cache` expects a storage factory, not a storage instance. +- The factory is called once at decoration time, giving each decorated function its own storage. +- Use `TTLMapStorage` for regular functions and `TTLMapAsyncStorage` for async functions. +- `max_size` limits entry count; `ttl` controls age-based expiration. Use `ttl=None` to disable age-based expiration. +- Built-in storages are process-local. Use a custom storage backend for shared multi-process or cross-machine caching. + +Full reference below. + +::: cachium.storages._abc.Result + +--- + +::: cachium.storages._abc.BaseStorage + +--- + +::: cachium.storages._abc.BaseAsyncStorage + +--- + +::: cachium.storages.ttl_map.TTLMapStorage + +--- + +::: cachium.storages.ttl_map.TTLMapAsyncStorage