gcache 1.2.1__tar.gz → 2.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gcache
3
- Version: 1.2.1
3
+ Version: 2.0.2
4
4
  Summary: Fine grained caching.
5
5
  License: MIT
6
6
  Author: Galileo Technologies Inc.
@@ -35,6 +35,7 @@ Description-Content-Type: text/markdown
35
35
  [![PyPI version](https://badge.fury.io/py/gcache.svg)](https://badge.fury.io/py/gcache)
36
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
37
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
+ [![codecov](https://codecov.io/gh/rungalileo/gcache/graph/badge.svg)](https://codecov.io/gh/rungalileo/gcache)
38
39
 
39
40
  A caching library built for moving fast without breaking things. GCache lets you rapidly add new caching use cases while maintaining structure and runtime control guardrails—so you can ramp up gradually, kill a bad cache instantly, and have full observability into what's cached across your system.
40
41
 
@@ -67,6 +68,7 @@ gcache = GCache(GCacheConfig())
67
68
  @gcache.cached(
68
69
  key_type="user_id",
69
70
  id_arg="user_id",
71
+ use_case="GetUser",
70
72
  default_config=GCacheKeyConfig(
71
73
  ttl_sec={CacheLayer.LOCAL: 60, CacheLayer.REMOTE: 300},
72
74
  ramp={CacheLayer.LOCAL: 100, CacheLayer.REMOTE: 100},
@@ -77,7 +79,7 @@ async def get_user(user_id: str) -> dict:
77
79
 
78
80
  # Use it — caching only happens inside enable() blocks
79
81
  with gcache.enable():
80
- user = await get_user("123") # Cached!
82
+ user = await get_user("123") # Cache key: urn:gcache:user_id:123#GetUser
81
83
  ```
82
84
 
83
85
  That's it. The function works normally outside `enable()` blocks, and caches results inside them.
@@ -136,6 +138,24 @@ Caching doesn't happen automatically—you control when it's active:
136
138
 
137
139
  - **Dynamic config** — The config provider runs on each request, so you can adjust TTLs or ramp percentages without redeploying.
138
140
 
141
+ ### Why Explicit `enable()`?
142
+
143
+ GCache requires you to explicitly enable caching with `with gcache.enable():`. This is intentional.
144
+
145
+ Caching in write paths can cause subtle bugs—a stale read might get cached right before a write, leading to inconsistent data. By requiring explicit opt-in, GCache forces you to consciously decide where caching is safe:
146
+
147
+ ```python
148
+ # Read path — caching is safe
149
+ with gcache.enable():
150
+ user = await get_user(user_id)
151
+
152
+ # Write path — no caching, function runs normally
153
+ await update_user(user_id, new_data)
154
+ await gcache.ainvalidate("user_id", user_id)
155
+ ```
156
+
157
+ This design prevents accidental caching in dangerous places.
158
+
139
159
  ## Runtime Configuration
140
160
 
141
161
  For dynamic control, provide a config provider when creating GCache. This lets you adjust caching behavior without redeploying:
@@ -183,17 +203,49 @@ async def get_user_profile(user_id: str) -> dict:
183
203
 
184
204
  ### Working with Complex Arguments
185
205
 
186
- Real functions have complex arguments. Use `id_arg` tuples and `arg_adapters` to handle them:
206
+ Options for mapping function arguments to cache keys.
207
+
208
+ #### `id_arg` (required)
209
+
210
+ Specifies which argument contains the entity ID for the cache key.
211
+
212
+ **String form** — use when the argument itself is the ID:
213
+ ```python
214
+ id_arg="user_id" # user_id argument is the ID
215
+ ```
216
+
217
+ **Tuple form** — use when the ID needs to be extracted from an object:
218
+ ```python
219
+ id_arg=("user", lambda u: u.id) # Extract ID from User object
220
+ ```
221
+
222
+ #### `arg_adapters`
223
+
224
+ Converts complex arguments to strings for the cache key. Only needed for non-primitive types.
225
+
226
+ ```python
227
+ arg_adapters={
228
+ "filters": lambda f: f.to_cache_key(), # Complex object
229
+ "page": str, # Simple conversion
230
+ }
231
+ ```
232
+
233
+ #### `ignore_args`
234
+
235
+ Excludes arguments that don't affect the cached result.
236
+
237
+ ```python
238
+ ignore_args=["db_session", "logger"]
239
+ ```
240
+
241
+ #### Example
187
242
 
188
243
  ```python
189
244
  @gcache.cached(
190
245
  key_type="user_id",
191
- id_arg=("user", lambda u: u.id), # Extract ID from User object
192
- arg_adapters={
193
- "filters": lambda f: f.to_cache_key(), # Convert complex objects
194
- "page": str, # Simple conversion
195
- },
196
- ignore_args=["db_session", "logger"], # Don't include these in cache key
246
+ id_arg=("user", lambda u: u.id),
247
+ arg_adapters={"filters": lambda f: f.to_cache_key()},
248
+ ignore_args=["db_session", "logger"],
197
249
  )
198
250
  async def search_user_posts(
199
251
  user: User,
@@ -203,8 +255,12 @@ async def search_user_posts(
203
255
  logger: Logger,
204
256
  ) -> list[Post]:
205
257
  ...
258
+
259
+ # Cache key: urn:gcache:user_id:123?filters=active&page=2#SearchUserPosts
206
260
  ```
207
261
 
262
+ The `id_arg` becomes `:123`, `arg_adapters` produce `?filters=active&page=2`, and `ignore_args` are excluded.
263
+
208
264
  ### Sync Functions Work Too
209
265
 
210
266
  ```python
@@ -230,7 +286,6 @@ from gcache import RedisConfig
230
286
 
231
287
  gcache = GCache(
232
288
  GCacheConfig(
233
- cache_config_provider=config_provider,
234
289
  redis_config=RedisConfig(
235
290
  host="redis.example.com",
236
291
  port=6379,
@@ -261,7 +316,6 @@ def make_redis_factory():
261
316
 
262
317
  gcache = GCache(
263
318
  GCacheConfig(
264
- cache_config_provider=config_provider,
265
319
  redis_client_factory=make_redis_factory(),
266
320
  )
267
321
  )
@@ -337,7 +391,6 @@ You can add a prefix to avoid collisions:
337
391
 
338
392
  ```python
339
393
  GCacheConfig(
340
- cache_config_provider=config_provider,
341
394
  metrics_prefix="myapp_", # Metrics become myapp_gcache_request_counter, etc.
342
395
  )
343
396
  ```
@@ -3,6 +3,7 @@
3
3
  [![PyPI version](https://badge.fury.io/py/gcache.svg)](https://badge.fury.io/py/gcache)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
+ [![codecov](https://codecov.io/gh/rungalileo/gcache/graph/badge.svg)](https://codecov.io/gh/rungalileo/gcache)
6
7
 
7
8
  A caching library built for moving fast without breaking things. GCache lets you rapidly add new caching use cases while maintaining structure and runtime control guardrails—so you can ramp up gradually, kill a bad cache instantly, and have full observability into what's cached across your system.
8
9
 
@@ -35,6 +36,7 @@ gcache = GCache(GCacheConfig())
35
36
  @gcache.cached(
36
37
  key_type="user_id",
37
38
  id_arg="user_id",
39
+ use_case="GetUser",
38
40
  default_config=GCacheKeyConfig(
39
41
  ttl_sec={CacheLayer.LOCAL: 60, CacheLayer.REMOTE: 300},
40
42
  ramp={CacheLayer.LOCAL: 100, CacheLayer.REMOTE: 100},
@@ -45,7 +47,7 @@ async def get_user(user_id: str) -> dict:
45
47
 
46
48
  # Use it — caching only happens inside enable() blocks
47
49
  with gcache.enable():
48
- user = await get_user("123") # Cached!
50
+ user = await get_user("123") # Cache key: urn:gcache:user_id:123#GetUser
49
51
  ```
50
52
 
51
53
  That's it. The function works normally outside `enable()` blocks, and caches results inside them.
@@ -104,6 +106,24 @@ Caching doesn't happen automatically—you control when it's active:
104
106
 
105
107
  - **Dynamic config** — The config provider runs on each request, so you can adjust TTLs or ramp percentages without redeploying.
106
108
 
109
+ ### Why Explicit `enable()`?
110
+
111
+ GCache requires you to explicitly enable caching with `with gcache.enable():`. This is intentional.
112
+
113
+ Caching in write paths can cause subtle bugs—a stale read might get cached right before a write, leading to inconsistent data. By requiring explicit opt-in, GCache forces you to consciously decide where caching is safe:
114
+
115
+ ```python
116
+ # Read path — caching is safe
117
+ with gcache.enable():
118
+ user = await get_user(user_id)
119
+
120
+ # Write path — no caching, function runs normally
121
+ await update_user(user_id, new_data)
122
+ await gcache.ainvalidate("user_id", user_id)
123
+ ```
124
+
125
+ This design prevents accidental caching in dangerous places.
126
+
107
127
  ## Runtime Configuration
108
128
 
109
129
  For dynamic control, provide a config provider when creating GCache. This lets you adjust caching behavior without redeploying:
@@ -151,17 +171,49 @@ async def get_user_profile(user_id: str) -> dict:
151
171
 
152
172
  ### Working with Complex Arguments
153
173
 
154
- Real functions have complex arguments. Use `id_arg` tuples and `arg_adapters` to handle them:
174
+ Options for mapping function arguments to cache keys.
175
+
176
+ #### `id_arg` (required)
177
+
178
+ Specifies which argument contains the entity ID for the cache key.
179
+
180
+ **String form** — use when the argument itself is the ID:
181
+ ```python
182
+ id_arg="user_id" # user_id argument is the ID
183
+ ```
184
+
185
+ **Tuple form** — use when the ID needs to be extracted from an object:
186
+ ```python
187
+ id_arg=("user", lambda u: u.id) # Extract ID from User object
188
+ ```
189
+
190
+ #### `arg_adapters`
191
+
192
+ Converts complex arguments to strings for the cache key. Only needed for non-primitive types.
193
+
194
+ ```python
195
+ arg_adapters={
196
+ "filters": lambda f: f.to_cache_key(), # Complex object
197
+ "page": str, # Simple conversion
198
+ }
199
+ ```
200
+
201
+ #### `ignore_args`
202
+
203
+ Excludes arguments that don't affect the cached result.
204
+
205
+ ```python
206
+ ignore_args=["db_session", "logger"]
207
+ ```
208
+
209
+ #### Example
155
210
 
156
211
  ```python
157
212
  @gcache.cached(
158
213
  key_type="user_id",
159
- id_arg=("user", lambda u: u.id), # Extract ID from User object
160
- arg_adapters={
161
- "filters": lambda f: f.to_cache_key(), # Convert complex objects
162
- "page": str, # Simple conversion
163
- },
164
- ignore_args=["db_session", "logger"], # Don't include these in cache key
214
+ id_arg=("user", lambda u: u.id),
215
+ arg_adapters={"filters": lambda f: f.to_cache_key()},
216
+ ignore_args=["db_session", "logger"],
165
217
  )
166
218
  async def search_user_posts(
167
219
  user: User,
@@ -171,8 +223,12 @@ async def search_user_posts(
171
223
  logger: Logger,
172
224
  ) -> list[Post]:
173
225
  ...
226
+
227
+ # Cache key: urn:gcache:user_id:123?filters=active&page=2#SearchUserPosts
174
228
  ```
175
229
 
230
+ The `id_arg` becomes `:123`, `arg_adapters` produce `?filters=active&page=2`, and `ignore_args` are excluded.
231
+
176
232
  ### Sync Functions Work Too
177
233
 
178
234
  ```python
@@ -198,7 +254,6 @@ from gcache import RedisConfig
198
254
 
199
255
  gcache = GCache(
200
256
  GCacheConfig(
201
- cache_config_provider=config_provider,
202
257
  redis_config=RedisConfig(
203
258
  host="redis.example.com",
204
259
  port=6379,
@@ -229,7 +284,6 @@ def make_redis_factory():
229
284
 
230
285
  gcache = GCache(
231
286
  GCacheConfig(
232
- cache_config_provider=config_provider,
233
287
  redis_client_factory=make_redis_factory(),
234
288
  )
235
289
  )
@@ -305,7 +359,6 @@ You can add a prefix to avoid collisions:
305
359
 
306
360
  ```python
307
361
  GCacheConfig(
308
- cache_config_provider=config_provider,
309
362
  metrics_prefix="myapp_", # Metrics become myapp_gcache_request_counter, etc.
310
363
  )
311
364
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gcache"
3
- version = "1.2.1"
3
+ version = "2.0.2"
4
4
  description = "Fine grained caching."
5
5
  authors = [{ name = "Galileo Technologies Inc.", email = "team@rungalileo.io" }]
6
6
  readme = "README.md"
@@ -1,3 +1,4 @@
1
- from .base import CacheConfigProvider, CacheLayer, GCache, GCacheConfig, GCacheKey, GCacheKeyConfig, RedisConfig
1
+ from gcache.config import CacheConfigProvider, CacheLayer, GCacheConfig, GCacheKey, GCacheKeyConfig, RedisConfig
2
+ from gcache.gcache import GCache
2
3
 
3
4
  __all__ = ["CacheConfigProvider", "CacheLayer", "GCache", "GCacheConfig", "GCacheKey", "GCacheKeyConfig", "RedisConfig"]
@@ -0,0 +1 @@
1
+ # Internal implementation modules - not part of public API
@@ -0,0 +1,69 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Awaitable, Callable
3
+ from typing import Any
4
+
5
+ from gcache.config import CacheConfigProvider, CacheLayer, GCacheKey, GCacheKeyConfig
6
+
7
+ #: Async callable that fetches the actual value on cache miss.
8
+ #: Invoked by cache implementations when the requested key is not found or is stale.
9
+ Fallback = Callable[..., Awaitable[Any]]
10
+
11
+
12
+ class CacheInterface(ABC):
13
+ def __init__(self, cache_config_provider: CacheConfigProvider):
14
+ self.config_provider = cache_config_provider
15
+
16
+ async def _resolve_config(self, key: GCacheKey) -> GCacheKeyConfig | None:
17
+ """
18
+ Resolve the cache config for a key.
19
+
20
+ First tries the config provider, then falls back to the key's default_config.
21
+ Returns None if neither provides a config.
22
+ """
23
+ config = await self.config_provider(key)
24
+ if config is None:
25
+ config = key.default_config
26
+ return config
27
+
28
+ @abstractmethod
29
+ async def get(self, key: GCacheKey, fallback: Fallback) -> Any:
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def put(self, key: GCacheKey, value: Any) -> None:
34
+ pass
35
+
36
+ @abstractmethod
37
+ async def delete(self, key: GCacheKey) -> bool:
38
+ pass
39
+
40
+ async def invalidate(self, key_type: str, id: str, future_buffer_ms: int) -> None:
41
+ """
42
+ Invalidate all cache entries matching key_type and id.
43
+
44
+ Sets a watermark timestamp so that any cached value created before
45
+ (now + future_buffer_ms) is considered stale on subsequent reads.
46
+
47
+ :param key_type: The entity type (e.g., 'user', 'project') matching the
48
+ key_type used in @cached decorators.
49
+ :param id: The entity identifier to invalidate.
50
+ :param future_buffer_ms: Extends invalidation window into the future.
51
+ Useful to handle race conditions where a read starts before a write
52
+ completes but finishes after, preventing caching of stale data.
53
+ """
54
+ pass
55
+
56
+ @abstractmethod
57
+ def layer(self) -> CacheLayer:
58
+ pass
59
+
60
+ async def flushall(self) -> None:
61
+ """
62
+ Remove all entries from this cache layer.
63
+
64
+ Used primarily for testing to reset cache state between tests.
65
+ Default implementation is a no-op; subclasses should override if
66
+ they support flushing (e.g., LocalCache clears its TTLCache dict,
67
+ RedisCache calls FLUSHALL on Redis).
68
+ """
69
+ pass
@@ -0,0 +1,18 @@
1
+ # Cache sizes
2
+ # Default max entries per use case to prevent unbounded memory growth.
3
+ LOCAL_CACHE_MAX_SIZE = 10_000
4
+
5
+ # Thresholds
6
+ # Threshold above which pickling runs in a thread to avoid blocking the event loop.
7
+ ASYNC_PICKLE_THRESHOLD_BYTES = 50_000
8
+
9
+ # TTLs (seconds)
10
+ # Watermark TTL must be longer than any invalidatable cache's TTL to ensure
11
+ # invalidation works correctly. 4 hours is a heuristic that covers most use cases.
12
+ # If your cache TTLs exceed 4 hours, consider making this configurable.
13
+ WATERMARK_TTL_SECONDS = 3600 * 4 # 4 hours
14
+
15
+ # Thread pool
16
+ # Default thread pool size for running async operations from sync code.
17
+ # Balances concurrency for I/O-bound Redis operations without excessive resource usage.
18
+ EVENT_LOOP_THREAD_POOL_SIZE = 16
@@ -7,7 +7,21 @@ from concurrent.futures import Future
7
7
  from logging import getLogger
8
8
  from typing import Any
9
9
 
10
- import uvloop
10
+ from gcache._internal.constants import EVENT_LOOP_THREAD_POOL_SIZE
11
+
12
+ # uvloop is optional - provides better performance on Linux/macOS but
13
+ # doesn't work on Windows or PyPy. Fall back to standard asyncio if unavailable.
14
+ try:
15
+ import uvloop
16
+
17
+ def _new_event_loop() -> asyncio.AbstractEventLoop:
18
+ return uvloop.new_event_loop()
19
+
20
+ except ImportError:
21
+
22
+ def _new_event_loop() -> asyncio.AbstractEventLoop:
23
+ return asyncio.new_event_loop()
24
+
11
25
 
12
26
  logger = getLogger(__name__)
13
27
 
@@ -31,7 +45,7 @@ class EventLoopThread(EventLoopThreadInterface, threading.Thread):
31
45
  def __init__(self, name: str = "EventLoopThread", daemon: bool = True) -> None:
32
46
  super().__init__(name=name)
33
47
  self.daemon = daemon
34
- self.loop = uvloop.new_event_loop()
48
+ self.loop = _new_event_loop()
35
49
 
36
50
  def run(self) -> None:
37
51
  # Set the event loop for this thread.
@@ -68,12 +82,12 @@ class EventLoopThread(EventLoopThreadInterface, threading.Thread):
68
82
 
69
83
  class EventLoopThreadPool(EventLoopThreadInterface):
70
84
  """
71
- Manage collection of EventLoopThread instances and also initailize them lazily.
85
+ Manage collection of EventLoopThread instances and also initialize them lazily.
72
86
 
73
87
  Lazy initialization is important when running in forked processes.
74
88
  """
75
89
 
76
- def __init__(self, name: str = "EventLoopThreadPool", num_threads: int = 16) -> None:
90
+ def __init__(self, name: str = "EventLoopThreadPool", num_threads: int = EVENT_LOOP_THREAD_POOL_SIZE) -> None:
77
91
  self.name = name
78
92
  self.num_threads = num_threads
79
93
  self.threads: list[EventLoopThread] | None = None
@@ -0,0 +1,71 @@
1
+ import asyncio
2
+ from typing import Any
3
+
4
+ from cachetools import TTLCache
5
+
6
+ from gcache._internal.cache_interface import CacheInterface, Fallback
7
+ from gcache._internal.constants import LOCAL_CACHE_MAX_SIZE
8
+ from gcache._internal.state import _GLOBAL_GCACHE_STATE
9
+ from gcache.config import CacheConfigProvider, CacheLayer, GCacheKey
10
+ from gcache.exceptions import MissingKeyConfig
11
+
12
+
13
+ class LocalCache(CacheInterface):
14
+ """
15
+ In-memory cache layer using TTLCache from cachetools.
16
+
17
+ Maintains a separate TTLCache instance per use_case, each with a configurable
18
+ TTL and a max size of LOCAL_CACHE_MAX_SIZE entries. This is the first layer
19
+ in the cache chain, checked before Redis.
20
+
21
+ Note: LocalCache does not support invalidation (watermarks). If you need
22
+ invalidation support, rely on the Redis layer with track_for_invalidation=True.
23
+ """
24
+
25
+ def __init__(self, cache_config_provider: CacheConfigProvider):
26
+ super().__init__(cache_config_provider)
27
+ self.caches: dict[str, TTLCache] = {} # use_case -> TTLCache instance
28
+ self.lock = asyncio.Lock() # Protects cache creation
29
+
30
+ async def _get_ttl_cache(self, key: GCacheKey) -> TTLCache:
31
+ cache = self.caches.get(key.use_case, None)
32
+ if cache is None:
33
+ config = await self._resolve_config(key)
34
+ if config is None:
35
+ raise MissingKeyConfig(key.use_case)
36
+
37
+ async with self.lock:
38
+ # See if cache was already created by another worker.
39
+ cache = self.caches.get(key.use_case, None)
40
+ if cache is None:
41
+ self.caches[key.use_case] = cache = TTLCache(
42
+ maxsize=LOCAL_CACHE_MAX_SIZE, ttl=config.ttl_sec[self.layer()]
43
+ )
44
+
45
+ return cache
46
+
47
+ async def get(self, key: GCacheKey, fallback: Fallback) -> Any:
48
+ _GLOBAL_GCACHE_STATE.logger.debug("Calling local cache")
49
+ cache = await self._get_ttl_cache(key)
50
+
51
+ if key not in cache:
52
+ await self.put(key, await fallback())
53
+
54
+ return cache[key]
55
+
56
+ async def put(self, key: GCacheKey, value: Any) -> None:
57
+ (await self._get_ttl_cache(key))[key] = value
58
+
59
+ async def delete(self, key: GCacheKey) -> bool:
60
+ try:
61
+ (await self._get_ttl_cache(key)).pop(key)
62
+ except KeyError:
63
+ return False
64
+ return True
65
+
66
+ def layer(self) -> CacheLayer:
67
+ return CacheLayer.LOCAL
68
+
69
+ async def flushall(self) -> None:
70
+ async with self.lock:
71
+ self.caches.clear()
@@ -0,0 +1,86 @@
1
+ from prometheus_client import Counter, Histogram
2
+
3
+
4
+ class GCacheMetrics:
5
+ """Centralized Prometheus metrics for GCache."""
6
+
7
+ _initialized: bool = False
8
+
9
+ # Counters
10
+ DISABLED_COUNTER: Counter
11
+ MISS_COUNTER: Counter
12
+ REQUEST_COUNTER: Counter
13
+ ERROR_COUNTER: Counter
14
+ INVALIDATION_COUNTER: Counter
15
+
16
+ # Histograms
17
+ GET_TIMER: Histogram
18
+ FALLBACK_TIMER: Histogram
19
+ SERIALIZATION_TIMER: Histogram
20
+ SIZE_HISTOGRAM: Histogram
21
+
22
+ @classmethod
23
+ def initialize(cls, prefix: str = "") -> None:
24
+ """Initialize all metrics with the given prefix. Only initializes once."""
25
+ if cls._initialized:
26
+ return
27
+
28
+ cls.DISABLED_COUNTER = Counter(
29
+ name=prefix + "gcache_disabled_counter",
30
+ labelnames=["use_case", "key_type", "layer", "reason"],
31
+ documentation="Cache disabled counter",
32
+ )
33
+
34
+ cls.MISS_COUNTER = Counter(
35
+ name=prefix + "gcache_miss_counter",
36
+ labelnames=["use_case", "key_type", "layer"],
37
+ documentation="Cache miss counter",
38
+ )
39
+
40
+ cls.REQUEST_COUNTER = Counter(
41
+ name=prefix + "gcache_request_counter",
42
+ labelnames=["use_case", "key_type", "layer"],
43
+ documentation="Cache request counter",
44
+ )
45
+
46
+ cls.ERROR_COUNTER = Counter(
47
+ name=prefix + "gcache_error_counter",
48
+ labelnames=["use_case", "key_type", "layer", "error", "in_fallback"],
49
+ documentation="Cache error counter",
50
+ )
51
+
52
+ cls.INVALIDATION_COUNTER = Counter(
53
+ name=prefix + "gcache_invalidation_counter",
54
+ labelnames=["key_type", "layer"],
55
+ documentation="Cache invalidation counter",
56
+ )
57
+
58
+ cls.GET_TIMER = Histogram(
59
+ name=prefix + "gcache_get_timer",
60
+ labelnames=["use_case", "key_type", "layer"],
61
+ documentation="Cache get timer",
62
+ buckets=[0.001] + list(Histogram.DEFAULT_BUCKETS),
63
+ )
64
+
65
+ cls.FALLBACK_TIMER = Histogram(
66
+ name=prefix + "gcache_fallback_timer",
67
+ labelnames=["use_case", "key_type", "layer"],
68
+ documentation="Fallback timer",
69
+ buckets=[0.001] + list(Histogram.DEFAULT_BUCKETS),
70
+ )
71
+
72
+ cls.SERIALIZATION_TIMER = Histogram(
73
+ name=prefix + "gcache_serialization_timer",
74
+ labelnames=["use_case", "key_type", "layer", "operation"],
75
+ documentation="Cache serialization timer",
76
+ buckets=[0.001] + list(Histogram.DEFAULT_BUCKETS),
77
+ )
78
+
79
+ cls.SIZE_HISTOGRAM = Histogram(
80
+ name=prefix + "gcache_size_histogram",
81
+ labelnames=["use_case", "key_type", "layer"],
82
+ documentation="Cache size histogram",
83
+ buckets=[100, 1000, 10_000, 100_000, 1_000_000, 10_000_000],
84
+ )
85
+
86
+ cls._initialized = True
@@ -0,0 +1,22 @@
1
+ from typing import Any
2
+
3
+ from gcache._internal.cache_interface import CacheInterface, Fallback
4
+ from gcache.config import CacheLayer, GCacheKey
5
+
6
+
7
+ class NoopCache(CacheInterface):
8
+ """
9
+ NOOP Cache that does nothing but invoke fallback on get.
10
+ """
11
+
12
+ async def get(self, key: GCacheKey, fallback: Fallback) -> Any:
13
+ return await fallback()
14
+
15
+ async def put(self, key: GCacheKey, value: Any) -> None:
16
+ pass
17
+
18
+ async def delete(self, key: GCacheKey) -> bool:
19
+ return False
20
+
21
+ def layer(self) -> CacheLayer:
22
+ return CacheLayer.NOOP