cachu 0.1.3__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {cachu-0.1.3/src/cachu.egg-info → cachu-0.2.0}/PKG-INFO +71 -20
  2. cachu-0.1.3/PKG-INFO → cachu-0.2.0/README.md +63 -35
  3. {cachu-0.1.3 → cachu-0.2.0}/pyproject.toml +8 -4
  4. {cachu-0.1.3 → cachu-0.2.0}/setup.cfg +1 -1
  5. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/__init__.py +18 -1
  6. cachu-0.2.0/src/cachu/async_decorator.py +261 -0
  7. cachu-0.2.0/src/cachu/async_operations.py +178 -0
  8. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/backends/__init__.py +6 -1
  9. cachu-0.2.0/src/cachu/backends/async_base.py +50 -0
  10. cachu-0.2.0/src/cachu/backends/async_memory.py +111 -0
  11. cachu-0.2.0/src/cachu/backends/async_redis.py +141 -0
  12. cachu-0.2.0/src/cachu/backends/async_sqlite.py +244 -0
  13. cachu-0.2.0/src/cachu/backends/file.py +10 -0
  14. cachu-0.2.0/src/cachu/backends/sqlite.py +240 -0
  15. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/decorator.py +3 -3
  16. cachu-0.1.3/README.md → cachu-0.2.0/src/cachu.egg-info/PKG-INFO +86 -16
  17. {cachu-0.1.3 → cachu-0.2.0}/src/cachu.egg-info/SOURCES.txt +12 -1
  18. {cachu-0.1.3 → cachu-0.2.0}/src/cachu.egg-info/requires.txt +7 -2
  19. cachu-0.2.0/tests/test_async_memory.py +157 -0
  20. cachu-0.2.0/tests/test_async_redis.py +146 -0
  21. cachu-0.2.0/tests/test_async_sqlite.py +164 -0
  22. cachu-0.2.0/tests/test_sqlite_backend.py +163 -0
  23. cachu-0.1.3/src/cachu/backends/file.py +0 -158
  24. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/backends/memory.py +0 -0
  25. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/backends/redis.py +0 -0
  26. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/config.py +0 -0
  27. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/keys.py +0 -0
  28. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/operations.py +0 -0
  29. {cachu-0.1.3 → cachu-0.2.0}/src/cachu/types.py +0 -0
  30. {cachu-0.1.3 → cachu-0.2.0}/src/cachu.egg-info/dependency_links.txt +0 -0
  31. {cachu-0.1.3 → cachu-0.2.0}/src/cachu.egg-info/top_level.txt +0 -0
  32. {cachu-0.1.3 → cachu-0.2.0}/tests/test_clearing.py +0 -0
  33. {cachu-0.1.3 → cachu-0.2.0}/tests/test_config.py +0 -0
  34. {cachu-0.1.3 → cachu-0.2.0}/tests/test_defaultcache.py +0 -0
  35. {cachu-0.1.3 → cachu-0.2.0}/tests/test_delete_keys.py +0 -0
  36. {cachu-0.1.3 → cachu-0.2.0}/tests/test_disable.py +0 -0
  37. {cachu-0.1.3 → cachu-0.2.0}/tests/test_exclude_params.py +0 -0
  38. {cachu-0.1.3 → cachu-0.2.0}/tests/test_file_cache.py +0 -0
  39. {cachu-0.1.3 → cachu-0.2.0}/tests/test_integration.py +0 -0
  40. {cachu-0.1.3 → cachu-0.2.0}/tests/test_memory_cache.py +0 -0
  41. {cachu-0.1.3 → cachu-0.2.0}/tests/test_namespace.py +0 -0
  42. {cachu-0.1.3 → cachu-0.2.0}/tests/test_namespace_isolation.py +0 -0
  43. {cachu-0.1.3 → cachu-0.2.0}/tests/test_redis_cache.py +0 -0
  44. {cachu-0.1.3 → cachu-0.2.0}/tests/test_set_keys.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cachu
3
- Version: 0.1.3
4
- Summary: Flexible caching library built on dogpile.cache
3
+ Version: 0.2.0
4
+ Summary: Flexible caching library with sync and async support for memory, file (SQLite), and Redis backends
5
5
  Author: bissli
6
6
  License-Expression: 0BSD
7
7
  Project-URL: Repository, https://github.com/bissli/cachu.git
@@ -9,13 +9,17 @@ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: dogpile.cache
11
11
  Requires-Dist: func-timeout
12
+ Provides-Extra: async
13
+ Requires-Dist: aiosqlite; extra == "async"
12
14
  Provides-Extra: redis
13
- Requires-Dist: redis; extra == "redis"
15
+ Requires-Dist: redis>=4.2.0; extra == "redis"
14
16
  Provides-Extra: test
15
17
  Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-asyncio; extra == "test"
16
19
  Requires-Dist: pytest-mock; extra == "test"
17
- Requires-Dist: redis; extra == "test"
20
+ Requires-Dist: redis>=4.2.0; extra == "test"
18
21
  Requires-Dist: testcontainers[redis]; extra == "test"
22
+ Requires-Dist: aiosqlite; extra == "test"
19
23
 
20
24
  # cachu
21
25
 
@@ -71,13 +75,13 @@ cachu.configure(
71
75
 
72
76
  ### Configuration Options
73
77
 
74
- | Option | Default | Description |
75
- |--------|---------|-------------|
76
- | `backend` | `'memory'` | Default backend type |
77
- | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
78
- | `file_dir` | `'/tmp'` | Directory for file-based caches |
79
- | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
80
- | `redis_distributed` | `False` | Enable distributed locks for Redis |
78
+ | Option | Default | Description |
79
+ | ------------------- | ---------------------------- | ------------------------------------------------- |
80
+ | `backend` | `'memory'` | Default backend type |
81
+ | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
82
+ | `file_dir` | `'/tmp'` | Directory for file-based caches |
83
+ | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
84
+ | `redis_distributed` | `False` | Enable distributed locks for Redis |
81
85
 
82
86
  ### Package Isolation
83
87
 
@@ -300,12 +304,12 @@ cache_clear()
300
304
 
301
305
  **Clearing behavior:**
302
306
 
303
- | `ttl` | `tag` | `backend` | Behavior |
304
- |-------|-------|-----------|----------|
305
- | `300` | `None` | `'memory'` | All keys in 300s memory region |
306
- | `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
307
- | `None` | `None` | `'memory'` | All memory regions |
308
- | `None` | `'users'` | `None` | "users" tag across all backends |
307
+ | `ttl` | `tag` | `backend` | Behavior |
308
+ | ------ | --------- | ---------- | -------------------------------------- |
309
+ | `300` | `None` | `'memory'` | All keys in 300s memory region |
310
+ | `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
311
+ | `None` | `None` | `'memory'` | All memory regions |
312
+ | `None` | `'users'` | `None` | "users" tag across all backends |
309
313
 
310
314
  ### Cross-Module Clearing
311
315
 
@@ -363,6 +367,38 @@ if cachu.is_disabled():
363
367
  print("Caching is disabled")
364
368
  ```
365
369
 
370
+ ## Async Support
371
+
372
+ The library provides full async/await support with matching APIs:
373
+
374
+ ```python
375
+ from cachu import async_cache, async_cache_get, async_cache_set, async_cache_delete
376
+ from cachu import async_cache_clear, async_cache_info
377
+
378
+ @async_cache(ttl=300, backend='memory')
379
+ async def get_user(user_id: int) -> dict:
380
+ return await fetch_from_database(user_id)
381
+
382
+ # Usage
383
+ user = await get_user(123) # Cache miss
384
+ user = await get_user(123) # Cache hit
385
+
386
+ # Per-call control works the same way
387
+ user = await get_user(123, _skip_cache=True)
388
+ user = await get_user(123, _overwrite_cache=True)
389
+
390
+ # CRUD operations
391
+ cached = await async_cache_get(get_user, user_id=123)
392
+ await async_cache_set(get_user, {'id': 123, 'name': 'Test'}, user_id=123)
393
+ await async_cache_delete(get_user, user_id=123)
394
+ await async_cache_clear(backend='memory', ttl=300)
395
+
396
+ # Statistics
397
+ info = await async_cache_info(get_user)
398
+ ```
399
+
400
+ All decorator options (`ttl`, `backend`, `tag`, `exclude`, `cache_if`, `validate`, `package`) work identically to the sync version.
401
+
366
402
  ## Advanced
367
403
 
368
404
  ### Direct Backend Access
@@ -397,25 +433,40 @@ from cachu import (
397
433
  enable,
398
434
  is_disabled,
399
435
 
400
- # Decorator
436
+ # Sync Decorator
401
437
  cache,
402
438
 
403
- # CRUD Operations
439
+ # Sync CRUD Operations
404
440
  cache_get,
405
441
  cache_set,
406
442
  cache_delete,
407
443
  cache_clear,
408
444
  cache_info,
409
445
 
446
+ # Async Decorator
447
+ async_cache,
448
+
449
+ # Async CRUD Operations
450
+ async_cache_get,
451
+ async_cache_set,
452
+ async_cache_delete,
453
+ async_cache_clear,
454
+ async_cache_info,
455
+
410
456
  # Advanced
411
457
  get_backend,
458
+ get_async_backend,
412
459
  get_redis_client,
460
+ Backend,
461
+ AsyncBackend,
462
+ clear_async_backends,
413
463
  )
414
464
  ```
415
465
 
416
466
  ## Features
417
467
 
418
- - **Multiple backends**: Memory, file (DBM), and Redis
468
+ - **Multiple backends**: Memory, file (SQLite), and Redis
469
+ - **Async support**: Full async/await API with `@async_cache` decorator
419
470
  - **Flexible TTL**: Configure different TTLs for different use cases
420
471
  - **Tags**: Organize and selectively clear cache entries
421
472
  - **Package isolation**: Each package gets isolated configuration
@@ -1,22 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: cachu
3
- Version: 0.1.3
4
- Summary: Flexible caching library built on dogpile.cache
5
- Author: bissli
6
- License-Expression: 0BSD
7
- Project-URL: Repository, https://github.com/bissli/cachu.git
8
- Requires-Python: >=3.10
9
- Description-Content-Type: text/markdown
10
- Requires-Dist: dogpile.cache
11
- Requires-Dist: func-timeout
12
- Provides-Extra: redis
13
- Requires-Dist: redis; extra == "redis"
14
- Provides-Extra: test
15
- Requires-Dist: pytest; extra == "test"
16
- Requires-Dist: pytest-mock; extra == "test"
17
- Requires-Dist: redis; extra == "test"
18
- Requires-Dist: testcontainers[redis]; extra == "test"
19
-
20
1
  # cachu
21
2
 
22
3
  Flexible caching library with support for memory, file, and Redis backends.
@@ -71,13 +52,13 @@ cachu.configure(
71
52
 
72
53
  ### Configuration Options
73
54
 
74
- | Option | Default | Description |
75
- |--------|---------|-------------|
76
- | `backend` | `'memory'` | Default backend type |
77
- | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
78
- | `file_dir` | `'/tmp'` | Directory for file-based caches |
79
- | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
80
- | `redis_distributed` | `False` | Enable distributed locks for Redis |
55
+ | Option | Default | Description |
56
+ | ------------------- | ---------------------------- | ------------------------------------------------- |
57
+ | `backend` | `'memory'` | Default backend type |
58
+ | `key_prefix` | `''` | Prefix for all cache keys (useful for versioning) |
59
+ | `file_dir` | `'/tmp'` | Directory for file-based caches |
60
+ | `redis_url` | `'redis://localhost:6379/0'` | Redis connection URL |
61
+ | `redis_distributed` | `False` | Enable distributed locks for Redis |
81
62
 
82
63
  ### Package Isolation
83
64
 
@@ -300,12 +281,12 @@ cache_clear()
300
281
 
301
282
  **Clearing behavior:**
302
283
 
303
- | `ttl` | `tag` | `backend` | Behavior |
304
- |-------|-------|-----------|----------|
305
- | `300` | `None` | `'memory'` | All keys in 300s memory region |
306
- | `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
307
- | `None` | `None` | `'memory'` | All memory regions |
308
- | `None` | `'users'` | `None` | "users" tag across all backends |
284
+ | `ttl` | `tag` | `backend` | Behavior |
285
+ | ------ | --------- | ---------- | -------------------------------------- |
286
+ | `300` | `None` | `'memory'` | All keys in 300s memory region |
287
+ | `300` | `'users'` | `'memory'` | Only "users" tag in 300s memory region |
288
+ | `None` | `None` | `'memory'` | All memory regions |
289
+ | `None` | `'users'` | `None` | "users" tag across all backends |
309
290
 
310
291
  ### Cross-Module Clearing
311
292
 
@@ -363,6 +344,38 @@ if cachu.is_disabled():
363
344
  print("Caching is disabled")
364
345
  ```
365
346
 
347
+ ## Async Support
348
+
349
+ The library provides full async/await support with matching APIs:
350
+
351
+ ```python
352
+ from cachu import async_cache, async_cache_get, async_cache_set, async_cache_delete
353
+ from cachu import async_cache_clear, async_cache_info
354
+
355
+ @async_cache(ttl=300, backend='memory')
356
+ async def get_user(user_id: int) -> dict:
357
+ return await fetch_from_database(user_id)
358
+
359
+ # Usage
360
+ user = await get_user(123) # Cache miss
361
+ user = await get_user(123) # Cache hit
362
+
363
+ # Per-call control works the same way
364
+ user = await get_user(123, _skip_cache=True)
365
+ user = await get_user(123, _overwrite_cache=True)
366
+
367
+ # CRUD operations
368
+ cached = await async_cache_get(get_user, user_id=123)
369
+ await async_cache_set(get_user, {'id': 123, 'name': 'Test'}, user_id=123)
370
+ await async_cache_delete(get_user, user_id=123)
371
+ await async_cache_clear(backend='memory', ttl=300)
372
+
373
+ # Statistics
374
+ info = await async_cache_info(get_user)
375
+ ```
376
+
377
+ All decorator options (`ttl`, `backend`, `tag`, `exclude`, `cache_if`, `validate`, `package`) work identically to the sync version.
378
+
366
379
  ## Advanced
367
380
 
368
381
  ### Direct Backend Access
@@ -397,25 +410,40 @@ from cachu import (
397
410
  enable,
398
411
  is_disabled,
399
412
 
400
- # Decorator
413
+ # Sync Decorator
401
414
  cache,
402
415
 
403
- # CRUD Operations
416
+ # Sync CRUD Operations
404
417
  cache_get,
405
418
  cache_set,
406
419
  cache_delete,
407
420
  cache_clear,
408
421
  cache_info,
409
422
 
423
+ # Async Decorator
424
+ async_cache,
425
+
426
+ # Async CRUD Operations
427
+ async_cache_get,
428
+ async_cache_set,
429
+ async_cache_delete,
430
+ async_cache_clear,
431
+ async_cache_info,
432
+
410
433
  # Advanced
411
434
  get_backend,
435
+ get_async_backend,
412
436
  get_redis_client,
437
+ Backend,
438
+ AsyncBackend,
439
+ clear_async_backends,
413
440
  )
414
441
  ```
415
442
 
416
443
  ## Features
417
444
 
418
- - **Multiple backends**: Memory, file (DBM), and Redis
445
+ - **Multiple backends**: Memory, file (SQLite), and Redis
446
+ - **Async support**: Full async/await API with `@async_cache` decorator
419
447
  - **Flexible TTL**: Configure different TTLs for different use cases
420
448
  - **Tags**: Organize and selectively clear cache entries
421
449
  - **Package isolation**: Each package gets isolated configuration
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "cachu"
3
- version = "0.1.3"
4
- description = "Flexible caching library built on dogpile.cache"
3
+ version = "0.2.0"
4
+ description = "Flexible caching library with sync and async support for memory, file (SQLite), and Redis backends"
5
5
  readme = "README.md"
6
6
  license = "0BSD"
7
7
  authors = [{ name = "bissli" }]
@@ -12,12 +12,15 @@ dependencies = [
12
12
  ]
13
13
 
14
14
  [project.optional-dependencies]
15
- redis = ["redis"]
15
+ async = ["aiosqlite"]
16
+ redis = ["redis>=4.2.0"]
16
17
  test = [
17
18
  "pytest",
19
+ "pytest-asyncio",
18
20
  "pytest-mock",
19
- "redis",
21
+ "redis>=4.2.0",
20
22
  "testcontainers[redis]",
23
+ "aiosqlite",
21
24
  ]
22
25
 
23
26
  [project.urls]
@@ -31,6 +34,7 @@ testpaths = ["tests"]
31
34
  python_files = ["test_*.py"]
32
35
  python_classes = ["Test*"]
33
36
  python_functions = ["test_*"]
37
+ asyncio_mode = "auto"
34
38
  markers = [
35
39
  "redis: marks tests requiring Redis (deselect with '-m \"not redis\"')",
36
40
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.3
2
+ current_version = 0.2.0
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -1,7 +1,13 @@
1
1
  """Flexible caching library with support for memory, file, and Redis backends.
2
2
  """
3
- __version__ = '0.1.3'
3
+ __version__ = '0.2.0'
4
4
 
5
+ from .async_decorator import async_cache, clear_async_backends
6
+ from .async_decorator import get_async_backend, get_async_cache_info
7
+ from .async_operations import async_cache_clear, async_cache_delete
8
+ from .async_operations import async_cache_get, async_cache_info
9
+ from .async_operations import async_cache_set
10
+ from .backends import AsyncBackend, Backend
5
11
  from .backends.redis import get_redis_client
6
12
  from .config import configure, disable, enable, get_all_configs, get_config
7
13
  from .config import is_disabled
@@ -24,4 +30,15 @@ __all__ = [
24
30
  'cache_info',
25
31
  'get_backend',
26
32
  'get_redis_client',
33
+ 'Backend',
34
+ 'AsyncBackend',
35
+ 'async_cache',
36
+ 'async_cache_get',
37
+ 'async_cache_set',
38
+ 'async_cache_delete',
39
+ 'async_cache_clear',
40
+ 'async_cache_info',
41
+ 'get_async_backend',
42
+ 'get_async_cache_info',
43
+ 'clear_async_backends',
27
44
  ]
@@ -0,0 +1,261 @@
1
+ """Async cache decorator implementation.
2
+ """
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import time
7
+ from collections.abc import Awaitable, Callable
8
+ from functools import wraps
9
+ from typing import Any
10
+
11
+ from .backends import NO_VALUE
12
+ from .backends.async_base import AsyncBackend
13
+ from .backends.async_memory import AsyncMemoryBackend
14
+ from .config import _get_caller_package, get_config, is_disabled
15
+ from .keys import make_key_generator, mangle_key
16
+ from .types import CacheEntry, CacheInfo, CacheMeta
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _async_backends: dict[tuple[str | None, str, int], AsyncBackend] = {}
21
+ _async_backends_lock = asyncio.Lock()
22
+
23
+ _async_stats: dict[int, tuple[int, int]] = {}
24
+ _async_stats_lock = asyncio.Lock()
25
+
26
+
27
+ async def _get_async_backend(package: str | None, backend_type: str, ttl: int) -> AsyncBackend:
28
+ """Get or create an async backend instance.
29
+ """
30
+ key = (package, backend_type, ttl)
31
+
32
+ async with _async_backends_lock:
33
+ if key in _async_backends:
34
+ return _async_backends[key]
35
+
36
+ cfg = get_config(package)
37
+
38
+ if backend_type == 'memory':
39
+ backend: AsyncBackend = AsyncMemoryBackend()
40
+ elif backend_type == 'file':
41
+ from .backends.async_sqlite import AsyncSqliteBackend
42
+
43
+ if ttl < 60:
44
+ filename = f'cache{ttl}sec.db'
45
+ elif ttl < 3600:
46
+ filename = f'cache{ttl // 60}min.db'
47
+ else:
48
+ filename = f'cache{ttl // 3600}hour.db'
49
+
50
+ if package:
51
+ filename = f'{package}_{filename}'
52
+
53
+ filepath = os.path.join(cfg.file_dir, filename)
54
+ backend = AsyncSqliteBackend(filepath)
55
+ elif backend_type == 'redis':
56
+ from .backends.async_redis import AsyncRedisBackend
57
+ backend = AsyncRedisBackend(cfg.redis_url, cfg.redis_distributed)
58
+ else:
59
+ raise ValueError(f'Unknown backend type: {backend_type}')
60
+
61
+ _async_backends[key] = backend
62
+ logger.debug(f"Created async {backend_type} backend for package '{package}', {ttl}s TTL")
63
+ return backend
64
+
65
+
66
+ async def get_async_backend(
67
+ backend_type: str | None = None,
68
+ package: str | None = None,
69
+ ttl: int = 300,
70
+ ) -> AsyncBackend:
71
+ """Get an async backend instance.
72
+
73
+ Args:
74
+ backend_type: 'memory', 'file', or 'redis'. Uses config default if None.
75
+ package: Package name. Auto-detected if None.
76
+ ttl: TTL in seconds (used for backend separation).
77
+ """
78
+ if package is None:
79
+ package = _get_caller_package()
80
+
81
+ if backend_type is None:
82
+ cfg = get_config(package)
83
+ backend_type = cfg.backend
84
+
85
+ return await _get_async_backend(package, backend_type, ttl)
86
+
87
+
88
+ def async_cache(
89
+ ttl: int = 300,
90
+ backend: str | None = None,
91
+ tag: str = '',
92
+ exclude: set[str] | None = None,
93
+ cache_if: Callable[[Any], bool] | None = None,
94
+ validate: Callable[[CacheEntry], bool] | None = None,
95
+ package: str | None = None,
96
+ ) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
97
+ """Async cache decorator with configurable backend and behavior.
98
+
99
+ Args:
100
+ ttl: Time-to-live in seconds (default: 300)
101
+ backend: Backend type ('memory', 'file', 'redis'). Uses config default if None.
102
+ tag: Tag for grouping related cache entries
103
+ exclude: Parameter names to exclude from cache key
104
+ cache_if: Function to determine if result should be cached.
105
+ Called with result value, caches if returns True.
106
+ validate: Function to validate cached entries before returning.
107
+ Called with CacheEntry, returns False to recompute.
108
+ package: Package name for config isolation. Auto-detected if None.
109
+
110
+ Per-call control via reserved kwargs (not passed to function):
111
+ _skip_cache: If True, bypass cache completely for this call
112
+ _overwrite_cache: If True, execute function and overwrite cached value
113
+
114
+ Example:
115
+ @async_cache(ttl=300, tag='users')
116
+ async def get_user(user_id: int) -> dict:
117
+ return await fetch_user(user_id)
118
+
119
+ # Normal call
120
+ user = await get_user(123)
121
+
122
+ # Skip cache
123
+ user = await get_user(123, _skip_cache=True)
124
+
125
+ # Force refresh
126
+ user = await get_user(123, _overwrite_cache=True)
127
+ """
128
+ resolved_package = package if package is not None else _get_caller_package()
129
+
130
+ if backend is None:
131
+ cfg = get_config(resolved_package)
132
+ resolved_backend = cfg.backend
133
+ else:
134
+ resolved_backend = backend
135
+
136
+ def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
137
+ key_generator = make_key_generator(fn, tag, exclude)
138
+
139
+ meta = CacheMeta(
140
+ ttl=ttl,
141
+ backend=resolved_backend,
142
+ tag=tag,
143
+ exclude=exclude or set(),
144
+ cache_if=cache_if,
145
+ validate=validate,
146
+ package=resolved_package,
147
+ key_generator=key_generator,
148
+ )
149
+
150
+ @wraps(fn)
151
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
152
+ skip_cache = kwargs.pop('_skip_cache', False)
153
+ overwrite_cache = kwargs.pop('_overwrite_cache', False)
154
+
155
+ if is_disabled() or skip_cache:
156
+ return await fn(*args, **kwargs)
157
+
158
+ backend_instance = await _get_async_backend(resolved_package, resolved_backend, ttl)
159
+ cfg = get_config(resolved_package)
160
+
161
+ base_key = key_generator(*args, **kwargs)
162
+ cache_key = mangle_key(base_key, cfg.key_prefix, ttl)
163
+
164
+ if not overwrite_cache:
165
+ value, created_at = await backend_instance.get_with_metadata(cache_key)
166
+
167
+ if value is not NO_VALUE:
168
+ if validate is not None and created_at is not None:
169
+ entry = CacheEntry(
170
+ value=value,
171
+ created_at=created_at,
172
+ age=time.time() - created_at,
173
+ )
174
+ if not validate(entry):
175
+ logger.debug(f'Cache validation failed for {fn.__name__}')
176
+ else:
177
+ await _record_async_hit(wrapper)
178
+ return value
179
+ else:
180
+ await _record_async_hit(wrapper)
181
+ return value
182
+
183
+ await _record_async_miss(wrapper)
184
+ result = await fn(*args, **kwargs)
185
+
186
+ should_cache = cache_if is None or cache_if(result)
187
+
188
+ if should_cache:
189
+ await backend_instance.set(cache_key, result, ttl)
190
+ logger.debug(f'Cached {fn.__name__} with key {cache_key}')
191
+
192
+ return result
193
+
194
+ wrapper._cache_meta = meta # type: ignore
195
+ wrapper._cache_key_generator = key_generator # type: ignore
196
+
197
+ return wrapper
198
+
199
+ return decorator
200
+
201
+
202
+ async def _record_async_hit(fn: Callable[..., Any]) -> None:
203
+ """Record a cache hit for the async function.
204
+ """
205
+ fn_id = id(fn)
206
+ async with _async_stats_lock:
207
+ hits, misses = _async_stats.get(fn_id, (0, 0))
208
+ _async_stats[fn_id] = (hits + 1, misses)
209
+
210
+
211
+ async def _record_async_miss(fn: Callable[..., Any]) -> None:
212
+ """Record a cache miss for the async function.
213
+ """
214
+ fn_id = id(fn)
215
+ async with _async_stats_lock:
216
+ hits, misses = _async_stats.get(fn_id, (0, 0))
217
+ _async_stats[fn_id] = (hits, misses + 1)
218
+
219
+
220
+ async def get_async_cache_info(fn: Callable[..., Any]) -> CacheInfo:
221
+ """Get cache statistics for an async decorated function.
222
+
223
+ Args:
224
+ fn: A function decorated with @async_cache
225
+
226
+ Returns
227
+ CacheInfo with hits, misses, and currsize
228
+ """
229
+ fn_id = id(fn)
230
+
231
+ async with _async_stats_lock:
232
+ hits, misses = _async_stats.get(fn_id, (0, 0))
233
+
234
+ meta = getattr(fn, '_cache_meta', None)
235
+ if meta is None:
236
+ return CacheInfo(hits=hits, misses=misses, currsize=0)
237
+
238
+ backend_instance = await _get_async_backend(meta.package, meta.backend, meta.ttl)
239
+ cfg = get_config(meta.package)
240
+
241
+ fn_name = getattr(fn, '__wrapped__', fn).__name__
242
+ pattern = f'*:{cfg.key_prefix}{fn_name}|*'
243
+
244
+ currsize = await backend_instance.count(pattern)
245
+
246
+ return CacheInfo(hits=hits, misses=misses, currsize=currsize)
247
+
248
+
249
+ async def clear_async_backends(package: str | None = None) -> None:
250
+ """Clear all async backend instances for a package. Primarily for testing.
251
+ """
252
+ async with _async_backends_lock:
253
+ if package is None:
254
+ for backend in _async_backends.values():
255
+ await backend.close()
256
+ _async_backends.clear()
257
+ else:
258
+ keys_to_delete = [k for k in _async_backends if k[0] == package]
259
+ for key in keys_to_delete:
260
+ await _async_backends[key].close()
261
+ del _async_backends[key]