btrcache 0.1.0__py3-none-any.whl

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.
btrcache/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+
3
+ """Caching tools, collections and decorators.
4
+
5
+ This package provides in-memory cache implementations and memoizing
6
+ decorators for synchronous and asynchronous callables, cache collections
7
+ implementing multiple algorithms and other utilities for working with
8
+ caches.
9
+ """
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,12 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+
3
+ """Asyncio-based caching package.
4
+
5
+ Provides decorators for caching coroutines.
6
+ """
7
+
8
+ import typing
9
+
10
+ from src.btrcache.asyncio.decorators import async_cached, async_cachedmethod
11
+
12
+ __all__: typing.Final = ("async_cached", "async_cachedmethod")
@@ -0,0 +1,18 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+
3
+ """Type aliases for asynchronous cache implementations.
4
+
5
+ This module defines generic cache aliases used by the asynchronous
6
+ decorators.
7
+ """
8
+
9
+ import asyncio
10
+ import typing
11
+
12
+ from src.btrcache.cache import Cache
13
+
14
+ __all__: typing.Final = ("AsyncCache",)
15
+
16
+ # Each asynchronous cache entry stores the executing `Task`
17
+ # together with the shared `Future` returned
18
+ type AsyncCache[R] = Cache[tuple[asyncio.Task[R], asyncio.Future[R]]]
@@ -0,0 +1,395 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+ #
3
+ # Portions Copyright (C) 2026 James Ward
4
+ # Source: https://github.com/imnotjames/cachetools-async/blob/53d453441dac736b16ec549ee4dd50453c9669c9/src/cachetools_async/decorators.py
5
+ # License: MIT (See LICENSE file for details)
6
+
7
+ """Asynchronous caching utilities for coroutines and methods.
8
+
9
+ This module provides async-compatible decorator patterns similar to
10
+ `cachetools`, designed to store and reuse `Future` instances to prevent
11
+ duplicate in-flight executions (cache stampede) and cache completed
12
+ results.
13
+ """
14
+
15
+ import asyncio
16
+ import contextlib
17
+ import functools
18
+ import inspect
19
+ import typing
20
+
21
+ from src.btrcache.keys import HashedKey, KeyHashStrategy, KeyHashStrategyProtocol
22
+
23
+ if typing.TYPE_CHECKING:
24
+ import collections.abc
25
+
26
+ from src.btrcache.asyncio.cache import AsyncCache
27
+
28
+ __all__: typing.Final = ("async_cached", "async_cachedmethod")
29
+
30
+
31
+ def __async_cache_get[R](
32
+ *,
33
+ cache: AsyncCache[R],
34
+ key: HashedKey,
35
+ call_fn: collections.abc.Callable[[], collections.abc.Coroutine[typing.Any, typing.Any, R]],
36
+ result_predicate: collections.abc.Callable[[typing.Any], bool] = lambda _: True,
37
+ exception_predicate: collections.abc.Callable[[BaseException], bool] = lambda _: True,
38
+ ) -> tuple[asyncio.Task[R], asyncio.Future[R]]:
39
+ with contextlib.suppress(KeyError):
40
+ # If alrady exists a cached version, return it
41
+ return cache[key]
42
+
43
+ # Creates a new task for this call
44
+ loop = asyncio.get_event_loop()
45
+ task = loop.create_task(call_fn())
46
+ future: asyncio.Future[R] = loop.create_future()
47
+
48
+ # `done` callback for when future is concluded
49
+ def done(t: asyncio.Task[R]) -> None:
50
+ # Removes cancelled tasks from the cache
51
+ if t.cancelled():
52
+ cache.pop(key, None)
53
+ future.cancel()
54
+ return
55
+
56
+ # Treats thrown exceptions inside executed task
57
+ exception = t.exception()
58
+ if exception is not None:
59
+ # Validates if the thrown exception should invalidate caching
60
+ if not exception_predicate(exception):
61
+ cache.pop(key, None)
62
+
63
+ # Propagates the exception
64
+ future.set_exception(exception)
65
+ return
66
+
67
+ # Successful result
68
+ result = t.result()
69
+
70
+ # Validates if the result should invalidate caching
71
+ if not result_predicate(result):
72
+ cache.pop(key, None)
73
+
74
+ # Stores the successful result in the shared future
75
+ future.set_result(result)
76
+
77
+ # Adds `done` callback of future to task
78
+ task.add_done_callback(done)
79
+
80
+ # Returns the cached value
81
+ with contextlib.suppress(ValueError):
82
+ cache[key] = (task, future)
83
+
84
+ return task, future
85
+
86
+
87
+ class __AsyncCachedFunctionWrapperProtocol[**P, R](typing.Protocol):
88
+ """Protocol for cached asynchronous function wrappers."""
89
+
90
+ cache: AsyncCache[R] | None
91
+ cache_key: KeyHashStrategyProtocol
92
+ cache_lock: contextlib.AbstractContextManager[typing.Any] | None
93
+ cache_info: bool | None
94
+ cache_clear: collections.abc.Callable[[], None]
95
+
96
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> collections.abc.Awaitable[R]:
97
+ raise NotImplementedError
98
+
99
+
100
+ class __AsyncCachedMethodWrapperProtocol[**P, R](typing.Protocol):
101
+ """Protocol for cached asynchronous method wrappers."""
102
+
103
+ cache: collections.abc.Callable[[typing.Any], AsyncCache[R] | None]
104
+ cache_key: KeyHashStrategyProtocol
105
+ cache_lock: contextlib.AbstractContextManager[typing.Any] | None
106
+ cache_info: bool | None
107
+ cache_clear: collections.abc.Callable[[typing.Any], None]
108
+
109
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> collections.abc.Awaitable[R]:
110
+ raise NotImplementedError
111
+
112
+ def __get__(
113
+ self,
114
+ # IGNORE: Type `Any` was used as it must The descriptor
115
+ # is accessed from an instance of the class containing
116
+ # the decorated method which must be any arbitrary
117
+ # user-defined type.
118
+ instance: typing.Any, # noqa: ANN401
119
+ owner: type | None,
120
+ ) -> __AsyncCachedMethodWrapperProtocol[P, R]:
121
+ if instance is None:
122
+ return self
123
+
124
+ # Binds the method to the instance at runtime
125
+ bound = typing.cast(
126
+ "__AsyncCachedMethodWrapperProtocol[P, R]",
127
+ functools.partial(self.__call__, instance),
128
+ )
129
+
130
+ # Mirrors the cache attributes to the bound wrapper
131
+ bound.cache = self.cache
132
+ bound.cache_key = self.cache_key
133
+ bound.cache_lock = self.cache_lock
134
+ bound.cache_info = self.cache_info
135
+ bound.cache_clear = self.cache_clear
136
+
137
+ return bound
138
+
139
+
140
+ class __AsyncFunctionDecoratorProtocol(typing.Protocol):
141
+ """Protocol representing a decorator that wraps an async function."""
142
+
143
+ def __call__[**P, R](
144
+ self,
145
+ fn: collections.abc.Callable[P, collections.abc.Awaitable[R]],
146
+ ) -> __AsyncCachedFunctionWrapperProtocol[P, R]:
147
+ raise NotImplementedError
148
+
149
+
150
+ class __AsyncMethodDecoratorProtocol(typing.Protocol):
151
+ """Protocol representing a decorator that wraps an async instance method."""
152
+
153
+ def __call__[SelfT, **P, R](
154
+ self,
155
+ fn: collections.abc.Callable[typing.Concatenate[SelfT, P], collections.abc.Awaitable[R]],
156
+ ) -> __AsyncCachedMethodWrapperProtocol[P, R]:
157
+ raise NotImplementedError
158
+
159
+
160
+ # IGNORE: Function defines a public interface
161
+ # that necessitates many arguments
162
+ def async_cached( # noqa: PLR0913
163
+ cache: AsyncCache[typing.Any] | None,
164
+ *,
165
+ key_hash_strategy: KeyHashStrategyProtocol = KeyHashStrategy,
166
+ lock: contextlib.AbstractContextManager[typing.Any] | None = None,
167
+ info: bool = False,
168
+ result_predicate: collections.abc.Callable[[typing.Any], bool] = lambda _: True,
169
+ exception_predicate: collections.abc.Callable[[BaseException], bool] = lambda _: True,
170
+ ) -> __AsyncFunctionDecoratorProtocol:
171
+ """Decorate caching async function results using asyncio `Future`.
172
+
173
+ This decorator stores `Future` objects in a shared mapping keyed by
174
+ deterministic `HashedKey` objects generated from the supplied
175
+ `key_hash_strategy`. It ensures that concurrent callers awaiting the
176
+ same computation will reuse the same in-progress `Future`, preventing
177
+ duplicate execution (cache stampede protection).
178
+
179
+ Args:
180
+ cache (AsyncCache[Any] | None):
181
+ Cache used to store tasks and futures. If None, caching is disabled.
182
+
183
+ key_hash_strategy (KeyHashStrategyProtocol):
184
+ Strategy used to compute deterministic cache keys from the
185
+ positional and keyword arguments. Defaults to `KeyHashStrategy`.
186
+
187
+ lock (AbstractContextManager[Any] | None):
188
+ Optional lock context manager. Not supported in this implementation.
189
+
190
+ info (bool):
191
+ If True, raises NotImplementedError (not supported).
192
+
193
+ result_predicate (Callable[[Any], bool]):
194
+ A custom callable that receives the completed result of the
195
+ coroutine and returns True if it should remain cached. If it
196
+ returns False, the result is evicted from the cache immediately
197
+ upon completion. Defaults to a lambda that always returns True.
198
+
199
+ exception_predicate (Callable[[BaseException], bool]):
200
+ A custom callable that receives the raised exception if the
201
+ coroutine fails, returning True if the exception should be cached.
202
+ If it returns False, the failed future is evicted, allowing
203
+ subsequent calls to retry execution. Defaults to a lambda that
204
+ always returns True.
205
+
206
+ Returns:
207
+ __AsyncFunctionDecoratorProtocol:
208
+ A decorator that wraps an async function with caching behavior.
209
+
210
+ Raises:
211
+ NotImplementedError:
212
+ If `info` or `lock` are provided.
213
+
214
+ """
215
+ if info:
216
+ msg = "`info` is not supported"
217
+ raise NotImplementedError(msg)
218
+
219
+ if lock is not None:
220
+ msg = "`lock` is not supported"
221
+ raise NotImplementedError(msg)
222
+
223
+ def decorator[**P, R](
224
+ fn: collections.abc.Callable[P, collections.abc.Awaitable[R]],
225
+ ) -> __AsyncCachedFunctionWrapperProtocol[P, R]:
226
+ if not inspect.iscoroutinefunction(fn):
227
+ msg = f"Expected 'Coroutine' function, got {fn}"
228
+ raise TypeError(msg)
229
+
230
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
231
+ if cache is None:
232
+ return await fn(*args, **kwargs)
233
+
234
+ # Generates key
235
+ generated_key = HashedKey(args, kwargs, hash_strategy=key_hash_strategy)
236
+
237
+ task, future = __async_cache_get(
238
+ cache=cache,
239
+ key=generated_key,
240
+ call_fn=lambda: fn(*args, **kwargs),
241
+ result_predicate=result_predicate,
242
+ exception_predicate=exception_predicate,
243
+ )
244
+
245
+ try:
246
+ return await future
247
+ except asyncio.CancelledError:
248
+ # If an awaiting caller is cancelled, the corresponding cache entry is
249
+ # removed and the underlying task is cancelled when appropriate
250
+
251
+ # Evicts from cache instantly
252
+ cache.pop(generated_key, None)
253
+
254
+ # Directly terminates the underlying task loop execution
255
+ if not task.done():
256
+ task.cancel()
257
+
258
+ raise
259
+
260
+ wrapped = typing.cast(
261
+ "__AsyncCachedFunctionWrapperProtocol[P, R]",
262
+ functools.update_wrapper(wrapper, fn),
263
+ )
264
+
265
+ wrapped.cache = cache
266
+ wrapped.cache_key = key_hash_strategy
267
+ wrapped.cache_lock = None
268
+ wrapped.cache_info = None
269
+ wrapped.cache_clear = lambda: cache.clear() if cache is not None else None
270
+
271
+ return wrapped
272
+
273
+ return decorator
274
+
275
+
276
+ # IGNORE: Function defines a public interface
277
+ # that necessitates many arguments
278
+ def async_cachedmethod( # noqa: PLR0913
279
+ cache: collections.abc.Callable[[typing.Any], AsyncCache[typing.Any] | None],
280
+ *,
281
+ key_hash_strategy: KeyHashStrategyProtocol = KeyHashStrategy,
282
+ lock: collections.abc.Callable[[typing.Any], contextlib.AbstractContextManager[typing.Any]]
283
+ | None = None,
284
+ info: bool = False,
285
+ result_predicate: collections.abc.Callable[[typing.Any], bool] = lambda _: True,
286
+ exception_predicate: collections.abc.Callable[[BaseException], bool] = lambda _: True,
287
+ ) -> __AsyncMethodDecoratorProtocol:
288
+ """Decorate caching async instance/class methods.
289
+
290
+ This is similar to `cached`, but the cache is resolved dynamically
291
+ per instance (or class) via the provided cache function.
292
+
293
+ It is primarily used for memoizing async methods where cache storage
294
+ is attached to `self` or another runtime context.
295
+
296
+ Args:
297
+ cache (Callable[[Any], AsyncCache[Any] | None]):
298
+ Callable that returns an `AsyncCache` for the given instance,
299
+ or None to disable caching for that instance.
300
+
301
+ key_hash_strategy (KeyHashStrategyProtocol):
302
+ Strategy used to compute deterministic cache keys from the
303
+ positional and keyword arguments. Defaults to `KeyHashStrategy`.
304
+
305
+ lock (Callable[[Any], AbstractContextManager[Any]] | None):
306
+ Optional lock context manager factory. Not supported.
307
+
308
+ info (bool):
309
+ If True, raises NotImplementedError (not supported).
310
+
311
+ result_predicate (Callable[[Any], bool]):
312
+ A custom callable that receives the completed result of the
313
+ coroutine and returns True if it should remain cached. If it
314
+ returns False, the result is evicted from the cache immediately
315
+ upon completion. Defaults to a lambda that always returns True.
316
+
317
+ exception_predicate (Callable[[BaseException], bool]):
318
+ A custom callable that receives the raised exception if the
319
+ coroutine fails, returning True if the exception should be cached.
320
+ If it returns False, the failed future is evicted, allowing
321
+ subsequent calls to retry execution. Defaults to a lambda that
322
+ always returns True.
323
+
324
+ Returns:
325
+ __AsyncMethodDecoratorProtocol:
326
+ A decorator that wraps an async method with caching behavior.
327
+
328
+ Raises:
329
+ NotImplementedError:
330
+ If `info` or `lock` are provided.
331
+
332
+ """
333
+ if info:
334
+ msg = "`info` is not supported"
335
+ raise NotImplementedError(msg)
336
+
337
+ if lock is not None:
338
+ msg = "`lock` is not supported"
339
+ raise NotImplementedError(msg)
340
+
341
+ def decorator[SelfT, **P, R](
342
+ fn: collections.abc.Callable[typing.Concatenate[SelfT, P], collections.abc.Awaitable[R]],
343
+ ) -> __AsyncCachedMethodWrapperProtocol[P, R]:
344
+ if not inspect.iscoroutinefunction(fn):
345
+ msg = f"Expected 'Coroutine' function, got {fn}"
346
+ raise TypeError(msg)
347
+
348
+ async def wrapper(self_obj: SelfT, *args: P.args, **kwargs: P.kwargs) -> R:
349
+ self_cache = cache(self_obj)
350
+
351
+ if self_cache is None:
352
+ return await fn(self_obj, *args, **kwargs)
353
+
354
+ # Generates key
355
+ generated_key = HashedKey(args, kwargs, hash_strategy=key_hash_strategy)
356
+
357
+ task, future = __async_cache_get(
358
+ cache=self_cache,
359
+ key=generated_key,
360
+ call_fn=lambda: fn(self_obj, *args, **kwargs),
361
+ result_predicate=result_predicate,
362
+ exception_predicate=exception_predicate,
363
+ )
364
+
365
+ try:
366
+ return await future
367
+ except asyncio.CancelledError:
368
+ # If an awaiting caller is cancelled, the corresponding cache entry is
369
+ # removed and the underlying task is cancelled when appropriate
370
+
371
+ # Evicts from cache instantly
372
+ self_cache.pop(generated_key, None)
373
+
374
+ # Directly terminates the underlying task loop execution
375
+ if not task.done():
376
+ task.cancel()
377
+
378
+ raise
379
+
380
+ wrapped = typing.cast(
381
+ "__AsyncCachedMethodWrapperProtocol[P, R]",
382
+ functools.update_wrapper(wrapper, fn),
383
+ )
384
+
385
+ wrapped.cache = cache
386
+ wrapped.cache_key = key_hash_strategy
387
+ wrapped.cache_lock = None
388
+ wrapped.cache_info = None
389
+ wrapped.cache_clear = lambda self_obj: (
390
+ self_cache.clear() if (self_cache := cache(self_obj)) is not None else None
391
+ )
392
+
393
+ return wrapped
394
+
395
+ return decorator
btrcache/cache.py ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+
3
+ """Cache collections and related abstractions.
4
+
5
+ This module provides shared cache collection interfaces, generic type
6
+ aliases, and cache implementations.
7
+ """
8
+
9
+ import typing
10
+
11
+ from src.btrcache.keys import HashedKey
12
+
13
+ if typing.TYPE_CHECKING:
14
+ import collections.abc
15
+
16
+ __all__: typing.Final = ("Cache",)
17
+
18
+ type Cache[R] = collections.abc.MutableMapping[
19
+ HashedKey,
20
+ R,
21
+ ]
btrcache/keys.py ADDED
@@ -0,0 +1,213 @@
1
+ # Copyright (C) 2026 Lucas Dias
2
+ #
3
+ # Portions Copyright (C) 2026 Thomas Kemmer
4
+ # Source: https://github.com/tkem/cachetools/blob/48284d73d0a8834c9c50f8d41bb99e6f93b2dfed/src/cachetools/keys.py
5
+ # License: MIT (See LICENSE file for details)
6
+
7
+ """Utilities for constructing deterministic, hashable cache keys.
8
+
9
+ This module provides strategies for converting positional and keyword
10
+ arguments into immutable tuple representations suitable for hashing and
11
+ cache lookups, along with a lightweight wrapper that lazily caches the
12
+ computed hash value.
13
+ """
14
+
15
+ import typing
16
+
17
+ __all__: typing.Final = (
18
+ "HashedKey",
19
+ "KeyHashStrategy",
20
+ "KeyHashStrategyProtocol",
21
+ "KeyHashTypedStrategy",
22
+ )
23
+
24
+ # Immutable tuple representation used internally for cache keys.
25
+ type RawCacheKey = tuple[typing.Any, ...]
26
+
27
+
28
+ class KeyHashStrategyProtocol(typing.Protocol):
29
+ """Protocol implemented by cache key generation strategies."""
30
+
31
+ @staticmethod
32
+ def hashkey(args: tuple[object, ...], kwargs: dict[str, object]) -> RawCacheKey:
33
+ """Convert function arguments into a hashable cache key.
34
+
35
+ Args:
36
+ args (tuple[object, ...]):
37
+ Positional arguments.
38
+
39
+ kwargs (dict[str, object]):
40
+ Keyword arguments.
41
+
42
+ Returns:
43
+ RawCacheKey:
44
+ Immutable tuple representing the cache key.
45
+
46
+ """
47
+ raise NotImplementedError
48
+
49
+
50
+ class KeyHashStrategy:
51
+ """Generate cache keys using only argument values."""
52
+
53
+ @staticmethod
54
+ def hashkey(args: tuple[object, ...], kwargs: dict[str, object]) -> RawCacheKey:
55
+ """Construct a deterministic cache key.
56
+
57
+ Positional arguments are stored first. Keyword arguments are
58
+ sorted by name and appended after a sentinel marker to ensure
59
+ deterministic ordering and avoid ambiguity with positional
60
+ arguments.
61
+
62
+ Args:
63
+ args (tuple[object, ...]):
64
+ Positional arguments.
65
+
66
+ kwargs (dict[str, object]):
67
+ Keyword arguments.
68
+
69
+ Returns:
70
+ RawCacheKey:
71
+ Immutable tuple representing the cache key.
72
+
73
+ """
74
+ # No keyword arguments are present
75
+ if not kwargs:
76
+ return tuple(args)
77
+
78
+ # Appends a marker followed by sorted keyword argument pairs to ensure
79
+ # deterministic ordering and distinguish them from positional arguments
80
+ return args + HashedKey.KWARGS_MARKER + tuple(sorted(kwargs.items()))
81
+
82
+
83
+ class KeyHashTypedStrategy:
84
+ """Generate cache keys that also encode argument types."""
85
+
86
+ @staticmethod
87
+ def hashkey(args: tuple[object, ...], kwargs: dict[str, object]) -> RawCacheKey:
88
+ """Construct a type-aware cache key.
89
+
90
+ In addition to argument values, the runtime type of each argument
91
+ is appended to distinguish values that compare equal but have
92
+ different types.
93
+
94
+ Args:
95
+ args (tuple[object, ...]):
96
+ Positional arguments.
97
+
98
+ kwargs (dict[str, object]):
99
+ Keyword arguments.
100
+
101
+ Returns:
102
+ RawCacheKey:
103
+ Immutable tuple representing the cache key.
104
+
105
+ """
106
+ # Starts with the positional argument values and appends
107
+ # every value type to distinguish between them in the final
108
+ # hash
109
+ key = args + tuple(type(v) for v in args)
110
+
111
+ if kwargs:
112
+ # Sorts keyword arguments to produce a deterministic key
113
+ sorted_kwargs = tuple(sorted(kwargs.items()))
114
+
115
+ # Appends the sentinel marker and the keyword argument values
116
+ key += HashedKey.KWARGS_MARKER + sorted_kwargs
117
+
118
+ # Append the type of each keyword argument value
119
+ key += tuple(type(v) for _, v in sorted_kwargs)
120
+
121
+ return key
122
+
123
+
124
+ class HashedKey:
125
+ """Immutable tuple-like object that caches its computed hash value.
126
+
127
+ The hash is computed lazily on first use and cached for subsequent
128
+ calls to avoid repeated tuple hashing overhead in cache key generation.
129
+ """
130
+
131
+ # Sentinel marker used to separate positional arguments from keyword arguments
132
+ # when building cache keys.
133
+ #
134
+ # This prevents ambiguity between calls like:
135
+ # f(1, ("x", 2)) -> positional tuple argument
136
+ # f(1, x=2) -> keyword argument form
137
+ #
138
+ # Without a separator, both could normalize to the same tuple representation,
139
+ # causing cache key collisions.
140
+ #
141
+ # The value is a class object because it is guaranteed unique, immutable and hashable.
142
+ KWARGS_MARKER: typing.ClassVar[tuple[type]] = (tuple,)
143
+
144
+ def __init__(
145
+ self,
146
+ args: tuple[object, ...],
147
+ kwargs: dict[str, object],
148
+ *,
149
+ hash_strategy: KeyHashStrategyProtocol = KeyHashStrategy,
150
+ ) -> None:
151
+ """Initialize a hashable cache key from function arguments.
152
+
153
+ This converts positional and keyword arguments into a single immutable
154
+ tuple representation used for hashing and equality comparisons.
155
+
156
+ Args:
157
+ args (tuple[object, ...]):
158
+ Positional arguments.
159
+
160
+ kwargs (dict[str, object]):
161
+ Keyword arguments.
162
+
163
+ hash_strategy (KeyHashStrategyProtocol):
164
+ Strategy used to normalize the arguments into an immutable cache
165
+ key representation.
166
+
167
+ """
168
+ self.__hashvalue: int
169
+ self.__tuple: RawCacheKey = hash_strategy.hashkey(args, kwargs)
170
+
171
+ @typing.override
172
+ def __eq__(self, value: object, /) -> bool:
173
+ # Keys must share the same class
174
+ if not isinstance(value, HashedKey):
175
+ return NotImplemented
176
+
177
+ # If they share the same hash, must be the same key
178
+ return self.__hash__() == value.__hash__()
179
+
180
+ @typing.override
181
+ def __hash__(self) -> int:
182
+ # Try-except is used instead of `is None` for
183
+ # performance issues
184
+ try:
185
+ return self.__hashvalue
186
+ except AttributeError:
187
+ # It will raise error if tuple has unhashable element
188
+ self.__hashvalue = self.__tuple.__hash__()
189
+ return self.__hashvalue
190
+
191
+ @typing.override
192
+ def __getstate__(self) -> object:
193
+ # Only keeps the given arguments in pickle
194
+ # (keeping hash stored is not safe)
195
+ return self.__tuple
196
+
197
+ def __setstate__(self, state: RawCacheKey) -> None:
198
+ """Restore the object state during unpickling.
199
+
200
+ This method reconstructs the internal immutable tuple representation
201
+ from the serialized state. Any cached hash value is intentionally
202
+ not restored, ensuring that the hash is recomputed lazily after
203
+ unpickling if needed.
204
+
205
+ Args:
206
+ state (RawCacheKey):
207
+ The previously serialized tuple representing the cache key.
208
+ This contains only the semantic argument structure and does
209
+ not include any runtime-cached values such as the hash.
210
+
211
+ """
212
+ # Restores internal tuple only
213
+ self.__tuple = state
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: btrcache
3
+ Version: 0.1.0
4
+ Summary: High-performance caching tools, memoization decorators, and cache collections for Python.
5
+ Project-URL: Homepage, https://github.com/lucas-azdias/btrcache?tab=readme-ov-file
6
+ Project-URL: Source, https://github.com/lucas-azdias/btrcache
7
+ Project-URL: Issues, https://github.com/lucas-azdias/btrcache/issues
8
+ Author-email: Lucas Dias <lucas@azdias.com.br>
9
+ Maintainer-email: Lucas Dias <lucas@azdias.com.br>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.14
20
+ Description-Content-Type: text/markdown
21
+
22
+ # `btrcache`
23
+
24
+ [![License](https://img.shields.io/github/license/lucas-azdias/btrcache)](https://raw.github.com/lucas-azdias/btrcache/master/LICENSE)
25
+
26
+ [![Coverage](https://img.shields.io/codecov/c/github/lucas-azdias/btrcache/main.svg)](https://codecov.io/gh/lucas-azdias/btrcache)
27
+
28
+ High-performance caching tools, memoization decorators, and cache collections for Python.
29
+
30
+ ---
31
+
32
+ ## Overview
33
+
34
+ **`btrcache`** is a Python library that provides a flexible set of in-memory caching primitives, decorators, and cache collections designed for both synchronous and asynchronous workloads.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install btrcache
40
+ ```
41
+
42
+ <!--
43
+ ## Quick Start
44
+
45
+ ### Simple function memoization
46
+
47
+ ```python
48
+ from btrcache import cached
49
+
50
+ @cached()
51
+ def fib(n: int) -> int:
52
+ if n < 2:
53
+ return n
54
+ return fib(n - 1) + fib(n - 2)
55
+ ```
56
+
57
+ ### Custom cache size
58
+
59
+ ```python
60
+ from btrcache import cached, LRUCache
61
+
62
+ @cached(cache_factory=lambda: LRUCache(maxsize=1024))
63
+ def compute(x):
64
+ return x * x
65
+ ```
66
+ -->
67
+
68
+ ## Roadmap
69
+
70
+ * [X] Key generators
71
+ * [X] Asynchronous decorators
72
+ * [ ] Create tests
73
+ * [ ] Release in PyPi
74
+ * [ ] Synchronous decorators
75
+ * [ ] Cache collections
76
+ * [ ] Optimizations
77
+
78
+ ## Contributing
79
+
80
+ Contributions are welcome. Please ensure:
81
+
82
+ * Code is type-annotated;
83
+ * Tests cover edge cases.
84
+
85
+ ## Acknowledgements
86
+
87
+ This project builds upon ideas and implementations from the following open-source projects:
88
+
89
+ - [`cachetools`](https://github.com/tkem/cachetools/) - An extensible set of memoizing collections and decorators for Python. Developed and maintained by [Thomas Kemmer](https://github.com/tkem/).
90
+ - [`cachetools-async`](https://github.com/imnotjames/cachetools-async/) - Python library that extends `cachetools` with async decorators. Created by [James Ward](https://github.com/imnotjames/).
91
+
92
+ ## License
93
+
94
+ Copyright (C) 2026 Lucas Dias.
95
+
96
+ Licensed under the [MIT License](LICENSE).
@@ -0,0 +1,11 @@
1
+ btrcache/__init__.py,sha256=xV1uBiQrCqH7JOahvX3eUUwzfwMGf4vCjiIIb4uVy50,303
2
+ btrcache/__version__.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
3
+ btrcache/cache.py,sha256=-mTu0MFgI-SuYpNlKdKsk_B1sDj7WC7FxvlIPjGbmBY,409
4
+ btrcache/keys.py,sha256=T5jEYt9p7Dp8c10kmmQmOiifehg8k4mg33zsM7spMEU,7058
5
+ btrcache/asyncio/__init__.py,sha256=fErA-0szqAW0vAcEBuTbPyWSwTtMKXnVk3tSpKmT2xQ,273
6
+ btrcache/asyncio/cache.py,sha256=jzrsl5wWkB2FFxbBXlTYm3N5QNXpJK5qejBjxzCT6gQ,458
7
+ btrcache/asyncio/decorators.py,sha256=3T4zzQGq3SNxlXBeVwwuh-RW_IZRV5TvZZ45xrdOtII,14517
8
+ btrcache-0.1.0.dist-info/METADATA,sha256=d7HmKyT9uGd0qqExzoPKnIna_5IG6yXGu9IO16A3NEI,2811
9
+ btrcache-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ btrcache-0.1.0.dist-info/licenses/LICENSE,sha256=MdnrHR7Fd0qpOZbLajTEsB7ifgteTCTTiCcha0ASkWA,1067
11
+ btrcache-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.