### Install Redis support Source: https://github.com/ezygang/py-cachify/blob/main/README.md Commands to install the Redis dependency for backend support. ```bash pip install redis ``` ```bash uv add redis ``` ```bash poetry add redis ``` -------------------------------- ### Install dependencies Source: https://github.com/ezygang/py-cachify/blob/main/AGENTS.md Install project dependencies using the uv package manager. ```bash uv sync ``` -------------------------------- ### Start documentation server Source: https://github.com/ezygang/py-cachify/blob/main/AGENTS.md Launch a local server to preview documentation. ```bash uv run task docs-dev ``` -------------------------------- ### Run py-cachify caching example Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/first-steps.md This bash script shows the command to execute the Python example that demonstrates py-cachify's caching functionality. It also includes the expected output, highlighting that the function is called only once. ```bash # Run our example $ python main.py # The ouput should be Called with 5 5 First call result: 10 Second call result: 10 ``` -------------------------------- ### Install py-cachify Source: https://github.com/ezygang/py-cachify/blob/main/README.md Commands to install the library using common Python package managers. ```bash pip install py-cachify ``` ```bash uv add py-cachify ``` ```bash poetry add py-cachify ``` -------------------------------- ### Quick Start with Caching Source: https://github.com/ezygang/py-cachify/blob/main/README.md Initialize the library and use the @cached decorator to cache function results. ```python from py_cachify import init_cachify, cached # Initialize once (uses in-memory cache by default) init_cachify() @cached(key='user-{user_id}', ttl=300) async def get_user(user_id: int) -> dict: return await fetch_from_db(user_id) # First call executes the function user = await get_user(42) # Subsequent calls return cached result instantly user = await get_user(42) ``` -------------------------------- ### Quick Start with Caching Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Initialize the library and use the @cached decorator to cache function results. ```python from py_cachify import init_cachify, cached # Initialize once (uses in-memory cache by default) init_cachify() @cached(key='user-{user_id}', ttl=300) async def get_user(user_id: int) -> dict: return await fetch_from_db(user_id) # First call executes the function user = await get_user(42) # Subsequent calls return cached result instantly user = await get_user(42) # Manually invalidate when needed await get_user.reset(user_id=42) ``` -------------------------------- ### Decorator with Dynamic Keys for Per-User Pools Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/pool.md Demonstrates creating dynamic pool keys using f-strings, enabling per-user or per-entity pools. This example shows how each `user_id` gets its own pool with a specified `max_size`. ```python from py_cachify import init_cachify, pooled init_cachify() @pooled(key='user-limit-{user_id}', max_size=5, raise_on_full=True) async def user_operation(user_id: str, data: str) -> dict: # Each user_id gets their own pool of size 5 return {'user_id': user_id, 'processed': data} async def main(): # Different pools: 'user-limit-1' and 'user-limit-2' await user_operation(user_id='1', data='a') await user_operation(user_id='2', data='b') ``` -------------------------------- ### Install py-cachify via poetry Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/initial-setup/install.md Use this command to add the package to a project managed by poetry. ```bash $ poetry add py-cachify ---> 100% Using version * for py-cachify Successfully installed py-cachify ``` -------------------------------- ### Install py-cachify via pip Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/initial-setup/install.md Use this command to install the package in environments managed by pip. ```bash $ pip install py-cachify ---> 100% Successfully installed py-cachify ``` -------------------------------- ### Load Shedding with Pool Size Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pool-methods.md An example demonstrating how to implement load shedding by checking the pool size before accepting new tasks. ```APIDOC ## Load Shedding Example This example shows how to use the `size()` or `asize()` method to implement load shedding, preventing new tasks from being added when the pool is nearing its capacity. ### Asynchronous Load Shedding ```python import asyncio from py_cachify import init_cachify, pool init_cachify() async def process_with_backpressure(task_id: str, worker_pool) -> None: current_size = await worker_pool.asize() max_size = worker_pool._max_size # Accessing internal attribute for threshold # Reject if pool is 80% full if current_size >= max_size * 0.8: print(f'Task {task_id}: Server busy, try again later') return async with worker_pool: print(f'Task {task_id}: Processing') await asyncio.sleep(0.5) async def main() -> None: worker_pool = pool(key='backpressure-pool', max_size=10) # Simulate many incoming tasks tasks = [process_with_backpressure(f'task-{i}', worker_pool) for i in range(15)] await asyncio.gather(*tasks) if __name__ == '__main__': asyncio.run(main()) ``` ### Explanation The `process_with_backpressure` function first checks the current pool size using `await worker_pool.asize()`. If the size exceeds 80% of the `max_size`, the task is rejected with a message. Otherwise, the task proceeds to be processed within the pool. ``` -------------------------------- ### Multi-Layer Caching Setup Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Configure two layers of caching: a global Redis layer with a 5-minute TTL and a local in-memory layer with a 5-second TTL. This provides a fast in-memory cache backed by a persistent Redis cache. ```python init_cachify(default_cache_ttl=300) # Redis layer # Local in-memory instance with shorter TTL local = init_cachify(is_global=False, prefix='L1-', default_cache_ttl=5) ``` -------------------------------- ### Initialize Py Cachify with Redis/DragonflyDB Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Initialize Py Cachify using a Redis or DragonflyDB client. This setup includes configuring the synchronous and asynchronous clients, a cache key prefix, and default expiration times for cache entries, locks, and pool slots. ```APIDOC ## Initialize Py Cachify with Redis/DragonflyDB ### Description Initializes the Py Cachify library with a Redis or DragonflyDB backend. This configuration specifies the connection details for both synchronous and asynchronous clients, a prefix for cache keys, and default time-to-live (TTL) values for cache entries, locks, and pool slots. It also sets the interval for polling when waiting for a lock. ### Code Example ```python from py_cachify import init_cachify from redis import from_url as redis_from_url from redis.asyncio import from_url as async_redis_from_url init_cachify( sync_client=redis_from_url('redis://localhost:6379/0'), async_client=async_redis_from_url('redis://localhost:6379/0'), prefix='APP-', default_cache_ttl=300, default_lock_expiration=30, default_pool_slot_expiration=600, # 10 min for pool slots lock_poll_interval=0.1, # Check lock every 100ms when waiting ) ``` ``` -------------------------------- ### Execute py-cachify Example Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md Command-line instructions to execute the Python script and observe the caching behavior in the terminal output. ```bash $ python main.py ``` -------------------------------- ### Demonstrate Nested Lock Acquisition Error in Py-Cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/simple-locks.md This example illustrates attempting to acquire a lock with the same key twice within a nested asynchronous context. It shows that Py-Cachify raises a `CachifyLockError` when a lock is already held, preventing re-acquisition and logging a warning. ```python import asyncio from py_cachify import init_cachify, lock # here we initializing a py-cachify to use an in-memory cache, as usual init_cachify() async def main() -> None: # and this is an async lock async with lock(key='cool-async-lock'): print('this code is locked and will be executed') async with lock(key='cool-async-lock'): print('we are attempting to acquire a new lock with the same key and will not make it to this print') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Initialize and Use Sync/Async Locks in Py-Cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/simple-locks.md This snippet demonstrates initializing Py-Cachify with an in-memory cache and then using both synchronous and asynchronous context managers for a single lock key. It shows that the `lock` object can be used in any environment without needing separate async/sync lock objects. ```python import asyncio from py_cachify import init_cachify, lock # here we initializing a py-cachify to use an in-memory cache, as usual init_cachify() async def main() -> None: # this is a sync lock with lock(key='cool-sync-lock'): print('this code is locked') # and this is an async lock async with lock(key='cool-async-lock'): print('this code is locked, but using async cache') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Bash Command Output for Lock Decorator Example Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/lock-as-decorator.md This bash output shows the result of running the Python example that utilizes the py-cachify lock decorator. It illustrates the lock status checks, lock release, and the exception raised when attempting to acquire an already held lock. ```bash $ python main.py # The output Sleep for is locked for argument 3: True Sleep for is locked for argument 4: False Sleep for is locked for argument 5: True Sleep for is locked for argument 5: False sleep_for_lock-1 is already locked! Exception: sleep_for_lock-1 is already locked! ``` -------------------------------- ### Handling Pool Full with Callbacks Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pooled-decorator.md Examples of using the on_full callback to implement fallback strategies like task rescheduling, returning cached data, or logging. ```python from py_cachify import init_cachify, pooled from celery import shared_task init_cachify() def reschedule_task(*args, **kwargs): # Reschedule this task to run later process_user_task.delay(*args, **kwargs) return {'status': 'rescheduled', 'user_id': kwargs.get('user_id')} @shared_task() @pooled(key='user-processing-pool-{user_id}', max_size=2, on_full=reschedule_task) def process_user_task(user_id: str) -> dict: # Process user data return {'status': 'completed', 'user_id': user_id} ``` ```python import asyncio from py_cachify import init_cachify, pooled, cached init_cachify() # Fallback returns stale cached data def return_cached(*args, **kwargs): user_id = kwargs.get('user_id') # Return a sensible default or fetch from cache return {'user_id': user_id, 'data': 'stale-cache', 'fresh': False} @pooled(key='expensive-query-pool', max_size=5, on_full=return_cached) @cached(key='expensive-query-{user_id}', ttl=60) async def expensive_query(user_id: str) -> dict: await asyncio.sleep(2) # Simulate slow query return {'user_id': user_id, 'data': 'fresh-data', 'fresh': True} async def main() -> None: result = await expensive_query(user_id='123') print(f'Result: {result}') if __name__ == '__main__': asyncio.run(main()) ``` ```python import asyncio import logging from py_cachify import init_cachify, pooled init_cachify() logger = logging.getLogger(__name__) def log_and_skip(*args, **kwargs): user_id = kwargs.get('user_id', 'unknown') logger.warning(f'Pool full - dropping task for user {user_id}') return None @pooled(key='notification-pool', max_size=10, on_full=log_and_skip) async def send_notification(user_id: str, message: str) -> dict: await asyncio.sleep(0.5) return {'sent': True, 'user_id': user_id} ``` -------------------------------- ### Resource Pool Context Manager Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Manage concurrent access to a resource using a context manager. This example limits concurrent API calls for a specific user (`user_id`) to a maximum of 5. ```python async with pool(key='api-pool-{user_id}', max_size=5): # Max 5 concurrent API calls per user await call_external_api(user_id) ``` -------------------------------- ### Py-Cachify Lock Execution Output Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/lock-parameters.md Example output from running the Py-Cachify lock acquisition script. It shows the successful execution message and subsequent messages indicating the lock is already held, demonstrating the lock's expiration and timeout behavior. ```bash $ python main.py # The output will be This code is executing under a lock with a timeout of 4 seconds and expiration set to 2 seconds example-lock is already locked! example-lock is already locked! example-lock is already locked! example-lock is already locked! This code is acquiring the same lock under the previous one. ``` -------------------------------- ### Implement Dynamic Cache Keys with @cached decorator Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/dynamic-cache-keys.md This example shows how to initialize py-cachify and apply the @cached decorator with a dynamic key string. The key uses named placeholders that correspond to function arguments, ensuring unique cache storage for different input combinations. ```python import asyncio from py_cachify import init_cachify, cached # Initialize py-cachify to use an in-memory cache init_cachify() # Use dynamic placeholders {a} and {b} in the cache key @cached(key='sum_two-{a}-{b}') async def sum_two(a: int, b: int) -> int: print(f'Called with {a} {b}') return a + b async def main() -> None: print(f'First call result: {await sum_two(5, 5)}') print(f'Second call result: {await sum_two(5, 5)}') print(f'Third call result: {await sum_two(5, 10)}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Initialize Py Cachify and Create Instance-Based Pools Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/pool.md Demonstrates how to initialize Py Cachify for global or instance-based usage. Instance-based initialization allows for independent pools with custom prefixes and configurations. ```python from py_cachify import init_cachify # Global initialization init_cachify() # Local instance with independent pools local_cachify = init_cachify(is_global=False, prefix='LOCAL-') # Instance-based pool local_pool = local_cachify.pool(key='local-worker', max_size=3) async with local_pool: # Uses local instance, not global pass # Instance-based decorator @local_cachify.pooled(key='local-task', max_size=2) async def local_task(): pass ``` -------------------------------- ### Global Initialization and Decorator Usage Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Configures the global cache client and demonstrates usage of cached, lock, once, and pooled decorators. ```python # Global initialization (returns a Cachify instance, but we don't need it here) init_cachify( sync_client=redis_from_url("redis://localhost:6379/0"), async_client=async_redis_from_url("redis://localhost:6379/1"), default_lock_expiration=60, default_cache_ttl=300, default_pool_slot_expiration=600, prefix='APP-', ) @cached(key='sum-{a}-{b}', ttl=30) def sum_two(a: int, b: int) -> int: return a + b @lock(key='critical-{id}', nowait=False, timeout=10) def critical_section(id: int) -> None: ... @once(key='run-once-{id}', raise_on_locked=True) def run_once(id: int) -> None: ... @pooled(key='worker-pool', max_size=5) async def worker_task(data: str) -> str: return f'processed-{data}' ``` -------------------------------- ### Create Pools with Default and Overridden Slot Expiration Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pool-parameters.md Demonstrates creating a standard pool that uses the default slot expiration and a specific pool ('quick-tasks') with a shorter TTL of 60 seconds. Also shows how to create a pool with no expiration ('infinite_pool') using 'slot_exp=None'. ```python # Uses the 300 second default standard_pool = pool(key='standard', max_size=10) # Override for a specific pool with shorter TTL short_pool = pool(key='quick-tasks', max_size=5, slot_exp=60) # 1 minute # Never expires (use with caution - orphaned slots persist until manual cleanup) infinite_pool = pool(key='long-tasks', max_size=3, slot_exp=None) ``` -------------------------------- ### Initialize a basic pool as a context manager Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/simple-pools.md Demonstrates using a pool to limit concurrent executions to a specified max_size. ```python import asyncio from py_cachify import init_cachify, pool # Initialize py-cachify init_cachify() async def main() -> None: # Create a pool that allows up to 2 concurrent executions worker_pool = pool(key='worker-pool', max_size=2) async with worker_pool: print('First worker executing') await asyncio.sleep(0.1) async with worker_pool: print('Second worker executing') await asyncio.sleep(0.1) print('All workers complete') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Initialization API Source: https://github.com/ezygang/py-cachify/blob/main/README.md Configures the caching backend for the application. Supports Redis/DragonflyDB or default in-memory storage. ```APIDOC ## init_cachify ### Description Initializes the global cachify configuration. This must be called before using any decorators or context managers. ### Parameters - **sync_client** (object) - Optional - A synchronous client instance (e.g., redis-py). - **async_client** (object) - Optional - An asynchronous client instance (e.g., redis-py async). - **prefix** (str) - Optional - A string prefix for all cache keys. - **default_cache_ttl** (int) - Optional - Default time-to-live for cached items in seconds. - **default_lock_expiration** (int) - Optional - Default expiration for locks in seconds. - **default_pool_slot_expiration** (int) - Optional - Default TTL for pool slots in seconds. - **lock_poll_interval** (float) - Optional - Interval in seconds to poll for lock availability. ``` -------------------------------- ### init_cachify Configuration Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Initializes the Cachify instance with specific client configurations and global/local scope settings. ```APIDOC ## init_cachify ### Description Initializes the library and returns a Cachify instance. This function configures the underlying cache clients and default behaviors for decorators. ### Parameters - **sync_client** (Optional[SyncClient]) - Optional - The synchronous client used for caching operations. If None, a new in-memory client is created. - **async_client** (Optional[AsyncClient]) - Optional - The asynchronous client used for caching operations. If None, a new async client is created around an in-memory cache. - **default_lock_expiration** (Optional[int]) - Optional - Default expiration time (in seconds) for locks. Defaults to 30. - **default_cache_ttl** (Optional[int]) - Optional - Default TTL (in seconds) for cached values when a decorator omits ttl. - **prefix** (str) - Optional - String prefix to prepend to all keys used in caching and locks. Defaults to 'PYC-'. - **lock_poll_interval** (float) - Optional - Interval in seconds between lock acquisition attempts when polling. Defaults to 0.1. - **default_pool_slot_expiration** (Optional[int]) - Optional - Default TTL (in seconds) for pool slots when a pool omits slot_exp. Defaults to 600. - **is_global** (bool) - Optional - Controls whether this call registers a global client. Defaults to True. ### Returns - **Cachify** (Object) - An instance object that exposes instance-scoped decorators: cached, lock, once, pool, and pooled. ``` -------------------------------- ### Initializing py-cachify with Custom Clients (Dedicated Instance) Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Create a dedicated py-cachify instance with custom synchronous and asynchronous clients, without affecting the global state. This allows for independent usage. ```python # Or create a dedicated instance without touching global state custom_cachify = init_cachify( sync_client=CustomSyncClient(), async_client=CustomAsyncClient(), is_global=False, ) ``` -------------------------------- ### Initialize global cachify backend Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Standard initialization pattern for applications using global decorators. ```python from redis import from_url as redis_from_url from redis.asyncio import from_url as async_redis_from_url from py_cachify import cached, init_cachify, lock, once, pooled ``` -------------------------------- ### Get Resource Pool Occupancy Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Retrieve the current number of active tasks within a resource pool managed by the `@pooled` decorator. This can be used to monitor pool usage. ```python occupancy = await process_task.size() ``` -------------------------------- ### Initialize Py Cachify with Custom Backend Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Provides guidance on implementing custom caching backends by adhering to the `SyncClient` or `AsyncClient` protocols. This allows integration with systems like Memcached, databases, or file-based storage. ```APIDOC ## Initialize Py Cachify with Custom Backend ### Description Allows users to implement their own caching backends by creating classes that conform to the `SyncClient` or `AsyncClient` protocols. This enables integration with various storage solutions such as Memcached, databases, or file systems for caching. ### Further Information Refer to the [Custom client guide](reference/init.md#custom-clients) for detailed instructions on implementing custom clients. ``` -------------------------------- ### Custom Synchronous Cache Client Interface Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Define a custom synchronous client by implementing get, set, and delete methods. The set method must support atomic set-if-not-exists semantics when nx=True. ```python from typing import Any, Optional, Awaitable class CustomSyncClient: def get(self, name: str) -> Optional[Any]: # Implementation for getting a value from the cache ... def set(self, name: str, value: Any, ex: Optional[int] = None, nx: bool = False) -> Any: # Implementation for setting a value in the cache. # When nx=True, this MUST act as an atomic "set-if-not-exists" and # return a truthy value on success and a falsy value on failure. ... def delete(self, *names: str) -> Any: # Implementation for deleting keys from the cache ... ``` -------------------------------- ### Custom Asynchronous Cache Client Interface Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Define a custom asynchronous client by implementing async get, set, and delete methods. The set method must support atomic set-if-not-exists semantics when nx=True. ```python class CustomAsyncClient: async def get(self, name: str) -> Optional[Any]: # Implementation for asynchronously getting a value from the cache ... async def set(self, name: str, value: Any, ex: Optional[int] = None, nx: bool = False) -> Awaitable[Any]: # Implementation for asynchronously setting a value in the cache. # When nx=True, this MUST act as an atomic "set-if-not-exists" and # return a truthy value on success and a falsy value on failure. ... async def delete(self, *names: str) -> Awaitable[Any]: # Implementation for asynchronously deleting keys from the cache ... ``` -------------------------------- ### Acquire Lock with Timeout and Expiration in Py-Cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/lock-parameters.md Demonstrates acquiring a lock with a specified timeout and expiration. The lock will wait up to 4 seconds to be acquired and will automatically expire after 2 seconds. This example shows nested lock acquisition. ```python import asyncio from py_cachify import init_cachify, lock # Initialize py-cachify to use in-memory cache init_cachify() async def main() -> None: example_lock = lock(key='example-lock', nowait=False, timeout=4, exp=2) async with example_lock: print('This code is executing under a lock with a timeout of 4 seconds and expiration set to 2 seconds') async with example_lock: print('This code is acquiring the same lock under the previous one.') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Using Dynamic Keys with @pooled() Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pooled-decorator.md Shows how to use format strings in the key parameter to create isolated pools based on function arguments. ```python from py_cachify import init_cachify, pooled init_cachify() @pooled(key='user-pool-{user_id}', max_size=2, on_full=lambda **kw: None) async def process_user(user_id: str) -> dict: return {'user_id': user_id} # Each user_id gets its own pool of size 2 async def main() -> None: # These use different pools (user-pool-1, user-pool-2) await process_user(user_id='1') await process_user(user_id='2') ``` -------------------------------- ### Implement synchronous pool usage Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/simple-pools.md Demonstrates using the pool context manager in a synchronous environment. ```python from py_cachify import init_cachify, pool init_cachify() with pool(key='sync-pool', max_size=3): print('Synchronous work in pool') ``` -------------------------------- ### Implement Asynchronous Caching with py-cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md Demonstrates how to apply the @cached decorator to an async function with custom encoding and decoding logic. The example shows how the first call executes the function while the second call retrieves and transforms the cached result. ```python @cached(key='sum_two-{a}-{b}', enc_dec=(encoder, decoder)) async def sum_two(a: int, b: int) -> int: print(f'Called with {a} {b}') return a + b async def main() -> None: print(f'First call result: {await sum_two(5, 5)}') print(f'Second call result: {await sum_two(5, 5)}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Run combined development tasks Source: https://github.com/ezygang/py-cachify/blob/main/AGENTS.md Execute formatting and linting tasks in a single command. ```bash uv run task format-and-lint # ruff + mypy ``` -------------------------------- ### Implement Layered Caching with py-cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/reset-attribute.md Demonstrates how to stack multiple @cached decorators to create a layered caching system. The example shows a local short-lived cache wrapping a global long-lived cache, and how to use the reset() method to clear both layers simultaneously. ```python import asyncio local_cachify = init_cachify(is_global=False, prefix='LOCAL-') @local_cachify.cached(key='local-sum_two-{a}-{b}', ttl=5) @cached(key='sum_two-{a}-{b}', ttl=60) async def sum_two(a: int, b: int) -> int: print(f'GLOBAL called with {a} {b}') return a + b async def main() -> None: print(f'First layered call: {await sum_two(2, 3)}') print(f'Second layered call: {await sum_two(2, 3)}') await sum_two.reset(a=2, b=3) print(f'Third layered call after reset: {await sum_two(2, 3)}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Pool Size Monitoring Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pool-methods.md Demonstrates how to use the size() and asize() methods to check the current number of occupied slots in a pool. ```APIDOC ## Pool Size Monitoring This section illustrates how to monitor the current number of occupied slots in a Py-Cachify pool using both asynchronous and synchronous methods. ### Synchronous Usage ```python from py_cachify import init_cachify, pool init_cachify() worker_pool = pool(key='sync-monitor', max_size=5) print(f'Pool size: {worker_pool.size()}') with worker_pool: print(f'Pool size during: {worker_pool.size()}') ``` ### Asynchronous Usage ```python import asyncio from py_cachify import init_cachify, pool init_cachify() async def main() -> None: worker_pool = pool(key='monitor-pool', max_size=3) print(f'Pool size before: {await worker_pool.asize()}') async with worker_pool: print(f'Pool size during: {await worker_pool.asize()}') print(f'Pool size after: {await worker_pool.asize()}') if __name__ == '__main__': asyncio.run(main()) ``` ### Method Reference | Method | Context | Returns | |-----------|--------------|---------------------------------------| | `size()` | Synchronous | `int`: current occupied slot count | | `asize()` | Asynchronous | `int`: current occupied slot count | Both methods return the count after cleaning up expired slots, so the number reflects actual available capacity. ``` -------------------------------- ### Initialize py-cachify with default global settings Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Call init_cachify without arguments to set up global decorators like `cached`, `lock`, `once`, and `pooled`. This is the default behavior and requires no explicit return value handling. ```python from py_cachify import init_cachify # Initialize globally (default behavior) init_cachify() ``` -------------------------------- ### Managing Multiple Isolated Instances Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Shows how to define multiple independent cache instances for different application subsystems. ```python from py_cachify import init_cachify user_cache = init_cachify(prefix='USER-', is_global=False) metrics_cache = init_cachify(prefix='METRICS-', is_global=False) @user_cache.cached(key='user-{user_id}') def get_user(user_id: int) -> dict: ... @metrics_cache.cached(key='metric-{name}') def compute_metric(name: str) -> float: ... ``` -------------------------------- ### Async Lock Decorator Usage with py-cachify Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/locks/lock-as-decorator.md This Python code demonstrates using the 'lock' decorator with an asynchronous function. It initializes py-cachify, defines a locked async function 'sleep_for', and then uses asyncio tasks to show lock status, release locks, and handle lock contention exceptions. ```python import asyncio from py_cachify import init_cachify, lock, CachifyLockError # Initialize py-cachify to use in-memory cache init_cachify() # Function that is wrapped in a lock and just sleeps for certain amount of time @lock(key='sleep_for_lock-{arg}', nowait=True) async def sleep_for(arg: int) -> None: await asyncio.sleep(arg) async def main() -> None: # Calling a function with an arg=3 _ = asyncio.create_task(sleep_for(3)) await asyncio.sleep(0.1) # Checking if the arg 3 call is locked (should be locked) print(f'Sleep for is locked for argument 3: {await sleep_for.is_locked(3)}') # Checking if the arg 4 call is locked (should not be locked) print(f'Sleep for is locked for argument 4: {await sleep_for.is_locked(4)}') task = asyncio.create_task(sleep_for(5)) await asyncio.sleep(0.1) # Checking if our call with arg=5 is locked print(f'Sleep for is locked for argument 5: {await sleep_for.is_locked(5)}') # Forcefully release a lock await sleep_for.release(5) # Doing a second check - shouldn't be locked now print(f'Sleep for is locked for argument 5: {await sleep_for.is_locked(5)}') await task # Trying to run 2 tasks with the same argument (and catching the exception) try: await asyncio.gather(sleep_for(1), sleep_for(1)) except CachifyLockError as e: print(f'Exception: {e}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Cache Function with Custom Encoder/Decoder in Python Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md Illustrates caching a function's result using custom encoder and decoder functions with Py-Cachify's `@cached` decorator. The encoder transforms the value before caching, and the decoder transforms it back when retrieved. This example defines an encoder that multiplies by 2 and a decoder that multiplies by 3, resulting in a net transformation of multiplying by 6. ```python import asyncio from py_cachify import init_cachify, cached # here we are initializing py-cachify to use an in-memory cache init_cachify() # our encoder will multiply the result by 2 def encoder(val: int) -> int: return val * 2 # and our decoder will do the multiplication by 3 def decoder(val: int) -> int: return val * 3 @cached(key='sum_two-{a}-{b}', enc_dec=(encoder, decoder)) async def sum_two(a: int, b: int) -> int: # Let's put print here to see what was the function called with print(f'Called with {a} {b}') return a + b async def main() -> None: # Call the function first time with (5, 5) print(f'First call result: {await sum_two(5, 5)}') # Call the function again to check if the cache is working print(f'Second call result: {await sum_two(5, 5)}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Initializing py-cachify with Custom Clients (Global) Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Initialize py-cachify to use custom synchronous and asynchronous clients for global caching operations. Ensure init_cachify is called with is_global=True at least once. ```python # Initialize a global Cachify client with custom clients init_cachify( sync_client=CustomSyncClient(), async_client=CustomAsyncClient(), ) ``` -------------------------------- ### Cache Function with TTL in Python Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md Demonstrates caching a function's result with a time-to-live (TTL) using the `@cached` decorator from Py-Cachify. The TTL overrides any default cache TTL configured during initialization. This example shows how a function call is cached for 1 second, and subsequent calls within that second return the cached result, while calls after expiration re-execute the function. ```python import asyncio from py_cachify import init_cachify, cached # here we are initializing py-cachify to use an in-memory cache # and setting a default_cache_ttl that will be used when ttl is omitted init_cachify(default_cache_ttl=10) # notice ttl, that will cache the result for one second and override default_cache_ttl @cached(key='sum_two-{a}-{b}', ttl=1) async def sum_two(a: int, b: int) -> int: # Let's put print here to see what was the function called with print(f'Called with {a} {b}') return a + b async def main() -> None: # Call the function first time with (5, 5) print(f'First call result: {await sum_two(5, 5)}') # Let's wait for 2 seconds await asyncio.sleep(2) # And we will call it again to check what will happen print(f'Second call result: {await sum_two(5, 5)}') if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Instance-Based Caching Initialization Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Initialize a global cachify instance for the main application and a separate, isolated instance for metrics. The metrics instance uses a different prefix and a default TTL of 60 seconds. ```python # Global for main app init_cachify(prefix='APP-') # Isolated instance for metrics (different prefix, different TTL) metrics = init_cachify(is_global=False, prefix='METRICS-', default_cache_ttl=60) ``` -------------------------------- ### init_cachify() Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Configures the core caching, locking, and pool management client. Can be used to set up global decorators or return an independent Cachify instance. ```APIDOC ## init_cachify() ### Description Configures the core caching, locking, and pool management client used by py-cachify. It can either register a global client for top-level decorators or return an independent Cachify instance. ### Parameters #### Arguments - **sync_client** (SyncClient) - Optional - The synchronous client instance. - **async_client** (AsyncClient) - Optional - The asynchronous client instance. - **default_lock_expiration** (int) - Optional - Default expiration for locks in seconds (default: 30). - **default_cache_ttl** (int) - Optional - Default time-to-live for cache entries. - **prefix** (str) - Optional - Prefix for keys (default: 'PYC-'). - **lock_poll_interval** (float) - Optional - Interval for lock polling in seconds (default: 0.1). - **default_pool_slot_expiration** (int) - Optional - Default expiration for pool slots in seconds (default: 600). - **is_global** (bool) - Optional - If True, registers as the global client; if False, returns an independent instance (default: True). ### Response - **Cachify** (object) - Returns a Cachify instance containing scoped decorators. ``` -------------------------------- ### Create isolated cache instances Source: https://github.com/ezygang/py-cachify/blob/main/README.md Use separate instances for different subsystems to avoid key collisions. ```python # Global for main app init_cachify(prefix='APP-') # Isolated instance for metrics (different prefix, different TTL) metrics = init_cachify(is_global=False, prefix='METRICS-', default_cache_ttl=60) @metrics.cached(key='metric-{name}') def compute_metric(name: str) -> float: return expensive_calculation(name) ``` -------------------------------- ### init_cachify() Configuration Source: https://github.com/ezygang/py-cachify/blob/main/docs/tutorial/pools/pool-parameters.md Configures global settings for the py-cachify library, including default slot expiration. ```APIDOC ## init_cachify(default_pool_slot_expiration) ### Description Sets global configuration for the library, specifically the default expiration time for pool slots. ### Parameters #### Request Body - **default_pool_slot_expiration** (int) - Optional - Default TTL for pool slots in seconds (default is 600 seconds). ``` -------------------------------- ### Initialize Py-Cachify Client Source: https://context7.com/ezygang/py-cachify/llms.txt Configures the global or local caching and locking client. Supports custom sync and async backends like Redis. ```python from py_cachify import init_cachify, cached, lock, once from redis import from_url from redis.asyncio import from_url as async_from_url # Basic in-memory initialization (default) init_cachify() # Redis initialization with configuration options init_cachify( sync_client=from_url("redis://localhost:6379/0"), async_client=async_from_url("redis://localhost:6379/0"), default_lock_expiration=30, default_cache_ttl=300, prefix='APP-', ) # Create a dedicated instance (does not touch global client) local_cache = init_cachify( is_global=False, prefix='LOCAL-', default_cache_ttl=60, ) @local_cache.cached(key='local-{x}') def compute_local(x: int) -> int: return x * 2 ``` -------------------------------- ### Initialize Py Cachify with Redis/DragonflyDB Source: https://github.com/ezygang/py-cachify/blob/main/README.md Configure cachify to use Redis or DragonflyDB as the caching backend. Set connection URLs, cache prefix, and default expiration times for cache entries, locks, and pool slots. Adjust lock polling interval for responsiveness. ```python from py_cachify import init_cachify from redis import from_url as redis_from_url from redis.asyncio import from_url as async_redis_from_url init_cachify( sync_client=redis_from_url('redis://localhost:6379/0'), async_client=async_redis_from_url('redis://localhost:6379/0'), prefix='APP-', default_cache_ttl=300, default_lock_expiration=30, default_pool_slot_expiration=600, # 10 min for pool slots lock_poll_interval=0.1, # Check lock every 100ms when waiting ) ``` -------------------------------- ### Creating Dedicated Isolated Instances Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Demonstrates how to create a local Cachify instance that operates independently of the global client. ```python from py_cachify import init_cachify # Global client, used by top-level decorators init_cachify( # e.g. some Redis or other backend sync_client=..., async_client=..., prefix='GLOBAL-', ) # Local instance: this does NOT modify the global client local_cachify = init_cachify( sync_client=None, # use in-memory sync cache async_client=None, # in-memory async wrapper prefix='LOCAL-', is_global=False, ) @local_cachify.cached(key='local-sum-{x}-{y}') def local_sum(x: int, y: int) -> int: return x + y @local_cachify.lock(key='local-lock-{name}') def local_locked(name: str) -> None: ... @local_cachify.once(key='local-once-{task_id}') def local_once(task_id: str) -> None: ... @local_cachify.pooled(key='local-pool', max_size=3) async def local_worker(data: str) -> str: ... ``` -------------------------------- ### Initialize Py Cachify with In-Memory Backend Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md Initialize Py Cachify using the default in-memory cache. This is suitable for development and testing environments. ```APIDOC ## Initialize Py Cachify with In-Memory Backend ### Description Initializes Py Cachify using its default in-memory caching mechanism. This is a simple and efficient option for development and testing purposes where external caching services are not required. ### Code Example ```python from py_cachify import init_cachify # Perfect for development and testing init_cachify() ``` ``` -------------------------------- ### Initialize Cachify with is_global=False Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md Use this mode when you need multiple, isolated caches or lock instances within the same process. The global client remains untouched, and the returned instance must be used explicitly for its decorators. ```python instance = init_cachify(is_global=False) @instance.cached(key='my-key-{x}') def f(x: int) -> int: ... ``` -------------------------------- ### Run development checks Source: https://github.com/ezygang/py-cachify/blob/main/AGENTS.md Execute individual development tasks for linting, type checking, and testing. ```bash uv run task ruff # Format & lint (includes unsafe fixes) uv run task mypy-lint # Type check uv run task tests # Unit tests (100% coverage enforced) ``` -------------------------------- ### Py Cachify API Quick Reference Source: https://github.com/ezygang/py-cachify/blob/main/docs/index.md A quick reference table for Py Cachify decorators and classes, outlining their purpose and key parameters for caching, distributed locking, and resource pooling. ```APIDOC ## API Quick Reference ### Description This table provides a concise overview of the primary decorators and classes available in Py Cachify, along with their intended use and essential parameters. It covers functionalities for caching function results, implementing distributed locks, and managing resource pools. ### Table | Decorator/Class | Purpose | Key Parameters | |-----------------|---------|----------------| | `@cached(key, ttl, enc_dec)` | Cache function results | `key`: template string, `ttl`: expiration in seconds | | `lock(key, nowait, timeout)` | Distributed lock context manager | `nowait`: fail fast, `timeout`: max wait time | | `@lock(key, nowait, timeout)` | Lock as decorator | Same as above | | `@once(key, raise_on_locked, return_on_locked)` | Prevent concurrent runs | `raise_on_locked`: exception vs skip | | `pool(key, max_size, slot_exp)` | Resource pool context manager *(v3.1.0)* | `max_size`: max concurrent, `slot_exp`: slot TTL | | `@pooled(key, max_size, on_full, raise_on_full)` | Pool as decorator *(v3.1.0)* | `on_full`: callback when full | ### Further Information For a comprehensive explanation of each API element, please consult the [Full API reference](reference/init.md). ``` -------------------------------- ### Context Manager with Pool Full Error Handling Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/pool.md Shows how to use a pool as a context manager with error handling for `CachifyPoolFullError`. This pattern is useful for gracefully skipping work when the pool is full. ```python import asyncio from py_cachify import init_cachify, pool, CachifyPoolFullError init_cachify() async def attempt_work(worker_pool) -> None: try: async with worker_pool: print('Acquired slot and working') await asyncio.sleep(1) except CachifyPoolFullError: print('Pool full - skipping work') async def main(): worker_pool = pool(key='work-pool', max_size=2) # Try to run 4 workers in a pool of 2 await asyncio.gather(*[attempt_work(worker_pool) for _ in range(4)]) if __name__ == '__main__': asyncio.run(main()) ``` -------------------------------- ### Full init_cachify function signature Source: https://github.com/ezygang/py-cachify/blob/main/docs/reference/init.md The `init_cachify` function configures caching, locking, and pool management clients. It accepts optional synchronous and asynchronous clients, default expiration times, a prefix, and poll intervals. The `is_global` parameter determines if the client is registered globally or returned as an instance. ```python from typing import Optional from py_cachify import init_cachify from py_cachify._backend._types._common import SyncClient, AsyncClient def init_cachify( sync_client: Optional[SyncClient] = None, async_client: Optional[AsyncClient] = None, default_lock_expiration: Optional[int] = 30, default_cache_ttl: Optional[int] = None, prefix: str = 'PYC-', lock_poll_interval: float = 0.1, default_pool_slot_expiration: Optional[int] = 600, *, is_global: bool = True, ) -> Cachify: # returns a Cachify instance ... ```