-
Notifications
You must be signed in to change notification settings - Fork 72
Add asyncio/coroutine support for caching async functions #319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
5498214
Initial plan
Copilot c4fb65f
Add asyncio support for caching coroutines
Copilot ad4e27c
Fix async concurrent call handling and update tests/examples
Copilot 1626b92
Fix linting issues in async implementation
Copilot 90a0dd3
Add comment explaining next_time mark/unmark pattern
Copilot 4094e94
add pytest-asyncio
Borda daab0dd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] a41c5fe
Merge branch 'master' into copilot/add-async-support-for-caching
Borda f1bca87
Update examples/async_example.py
Borda 23125e3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 496cb20
Merge branch 'master' into copilot/add-async-support-for-caching
Borda a72adb6
Address PR review feedback: fix test assertions, add allow_none test,…
Copilot 6393d95
Refactor async caching tests: introduce test classes and improve orga…
Borda 07328a6
Merge branch 'master' into copilot/add-async-support-for-caching
Borda daa5d1a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 2481527
Fix redundant test assertions and add edge case tests for async funct…
Copilot 1d706fe
Simplify line breaks in async_example.py for 120 char line length
Copilot 21e8474
linting
Borda e9d4142
linting
Borda e6a9938
Update src/cachier/core.py
Borda 8d99753
Merge branch 'master' into copilot/add-async-support-for-caching
Borda 1cf9d27
Fix redundant test assertion in test_uses_cache_before_expiry
Copilot 5fc456d
Add comprehensive tests for missing async code coverage
Copilot 91a26b3
Apply suggestions from code review
Borda a2cd7a6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 06c3ee8
Add tests for missing code coverage: exception handling and stale pro…
Copilot c61a1bf
linting
Borda 935ea40
Add tests to cover remaining uncovered async code paths
Copilot 40fd702
Remove line number references from test docstrings
Copilot 6be5c10
Remove duplicate tests and consolidate stale entry processing tests
Copilot c81f4a9
Remove unreachable async code path for stale entry processing
Copilot 093dbd3
Move common pytest marks to class level for cleaner test code
Copilot 71cb19c
linting
Borda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| """Example demonstrating async/coroutine support in Cachier. | ||
|
|
||
| This example shows how to use the @cachier decorator with async functions to cache the results of HTTP requests or other | ||
| async operations. | ||
|
|
||
| """ | ||
|
|
||
| import asyncio | ||
| import time | ||
| from datetime import timedelta | ||
|
|
||
| from cachier import cachier | ||
|
|
||
|
|
||
| # Example 1: Basic async function caching | ||
| @cachier(backend="pickle", stale_after=timedelta(hours=1)) | ||
| async def fetch_user_data(user_id: int) -> dict: | ||
| """Simulate fetching user data from an API.""" | ||
| print(f" Fetching user {user_id} from API...") | ||
| await asyncio.sleep(1) # Simulate network delay | ||
| return {"id": user_id, "name": f"User{user_id}", "email": f"user{user_id}@example.com"} | ||
|
|
||
|
|
||
| # Example 2: Async function with memory backend (faster, but not persistent) | ||
| @cachier(backend="memory") | ||
| async def calculate_complex_result(x: int, y: int) -> int: | ||
| """Simulate a complex calculation.""" | ||
| print(f" Computing {x} ** {y}...") | ||
| await asyncio.sleep(0.5) # Simulate computation time | ||
| return x**y | ||
|
|
||
|
|
||
| # Example 3: Async function with stale_after (without next_time for simplicity) | ||
| @cachier(backend="memory", stale_after=timedelta(seconds=3), next_time=False) | ||
| async def get_weather_data(city: str) -> dict: | ||
| """Simulate fetching weather data with automatic refresh when stale.""" | ||
| print(f" Fetching weather for {city}...") | ||
| await asyncio.sleep(0.5) | ||
| return {"city": city, "temp": 72, "condition": "sunny", "timestamp": time.time()} | ||
|
|
||
|
|
||
| # Example 4: Real-world HTTP request caching (requires httpx) | ||
| async def demo_http_caching(): | ||
| """Demonstrate caching actual HTTP requests.""" | ||
| print("\n=== HTTP Request Caching Example ===") | ||
| try: | ||
| import httpx | ||
|
|
||
| @cachier(backend="pickle", stale_after=timedelta(minutes=5)) | ||
| async def fetch_github_user(username: str) -> dict: | ||
| """Fetch GitHub user data with caching.""" | ||
| print(f" Making API request for {username}...") | ||
| async with httpx.AsyncClient() as client: | ||
| response = await client.get(f"https://api.github.com/users/{username}") | ||
| return response.json() | ||
|
|
||
| # First call - makes actual HTTP request | ||
| start = time.time() | ||
| user1 = await fetch_github_user("torvalds") | ||
| duration1 = time.time() - start | ||
| print(f" First call took {duration1:.2f}s") | ||
| user_name = user1.get("name", "N/A") | ||
| user_repos = user1.get("public_repos", "N/A") | ||
| print(f" User: {user_name}, Repos: {user_repos}") | ||
|
|
||
| # Second call - uses cache (much faster) | ||
| start = time.time() | ||
| await fetch_github_user("torvalds") | ||
| duration2 = time.time() - start | ||
| print(f" Second call took {duration2:.2f}s (from cache)") | ||
| if duration2 > 0: | ||
| print(f" Cache speedup: {duration1 / duration2:.1f}x") | ||
| else: | ||
| print(" Cache speedup: instantaneous (duration too small to measure)") | ||
|
|
||
| except ImportError: | ||
| print(" (Skipping - httpx not installed. Install with: pip install httpx)") | ||
|
|
||
|
|
||
| async def main(): | ||
| """Run all async caching examples.""" | ||
| print("=" * 60) | ||
| print("Cachier Async/Coroutine Support Examples") | ||
| print("=" * 60) | ||
Borda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Example 1: Basic async caching | ||
| print("\n=== Example 1: Basic Async Caching ===") | ||
| start = time.time() | ||
| user = await fetch_user_data(42) | ||
| duration1 = time.time() - start | ||
| print(f"First call: {user} (took {duration1:.2f}s)") | ||
|
|
||
| start = time.time() | ||
| user = await fetch_user_data(42) | ||
| duration2 = time.time() - start | ||
| print(f"Second call: {user} (took {duration2:.2f}s)") | ||
| if duration2 > 0: | ||
| print(f"Speedup: {duration1 / duration2:.1f}x faster!") | ||
| else: | ||
| print("Speedup: instantaneous (duration too small to measure)") | ||
|
|
||
| # Example 2: Memory backend | ||
| print("\n=== Example 2: Memory Backend (Fast, Non-Persistent) ===") | ||
| start = time.time() | ||
| result = await calculate_complex_result(2, 20) | ||
| duration1 = time.time() - start | ||
| print(f"First call: 2^20 = {result} (took {duration1:.2f}s)") | ||
|
|
||
| start = time.time() | ||
| result = await calculate_complex_result(2, 20) | ||
| duration2 = time.time() - start | ||
| print(f"Second call: 2^20 = {result} (took {duration2:.2f}s)") | ||
|
|
||
| # Example 3: Stale-after | ||
| print("\n=== Example 3: Stale-After ===") | ||
| weather = await get_weather_data("San Francisco") | ||
| print(f"First call: {weather}") | ||
|
|
||
| weather = await get_weather_data("San Francisco") | ||
| print(f"Second call (cached): {weather}") | ||
|
|
||
| print("Waiting 4 seconds for cache to become stale...") | ||
| await asyncio.sleep(4) | ||
|
|
||
| weather = await get_weather_data("San Francisco") | ||
| print(f"Third call (recalculates because stale): {weather}") | ||
|
|
||
| # Example 4: Concurrent requests | ||
| print("\n=== Example 4: Concurrent Async Requests ===") | ||
| print("Making 5 concurrent requests...") | ||
| print("(First 3 are unique and will execute, last 2 are duplicates)") | ||
| start = time.time() | ||
| await asyncio.gather( | ||
| fetch_user_data(1), | ||
| fetch_user_data(2), | ||
| fetch_user_data(3), | ||
| fetch_user_data(1), # Duplicate - will execute in parallel with first | ||
| fetch_user_data(2), # Duplicate - will execute in parallel with second | ||
| ) | ||
| duration = time.time() - start | ||
| print(f"All requests completed in {duration:.2f}s") | ||
|
|
||
| # Now test that subsequent calls use cache | ||
| print("\nMaking the same requests again (should use cache):") | ||
| start = time.time() | ||
| await asyncio.gather( | ||
| fetch_user_data(1), | ||
| fetch_user_data(2), | ||
| fetch_user_data(3), | ||
| ) | ||
| duration2 = time.time() - start | ||
| print(f"Completed in {duration2:.2f}s - much faster!") | ||
|
|
||
| # Example 5: HTTP caching (if httpx is available) | ||
| await demo_http_caching() | ||
|
|
||
| # Clean up | ||
| print("\n=== Cleanup ===") | ||
| fetch_user_data.clear_cache() | ||
| calculate_complex_result.clear_cache() | ||
| get_weather_data.clear_cache() | ||
| print("All caches cleared!") | ||
|
|
||
| print("\n" + "=" * 60) | ||
| print("Key Features Demonstrated:") | ||
| print(" - Async function caching with @cachier decorator") | ||
| print(" - Multiple backends (pickle, memory)") | ||
| print(" - Automatic cache invalidation (stale_after)") | ||
| print(" - Concurrent request handling") | ||
| print(" - Significant performance improvements") | ||
| print("\nNote: For async functions, concurrent calls with the same") | ||
| print("arguments will execute in parallel initially. Subsequent calls") | ||
| print("will use the cached result for significant speedup.") | ||
| print("=" * 60) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.