spakky-cache 6.3.1__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.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.3
2
+ Name: spakky-cache
3
+ Version: 6.3.1
4
+ Summary: Backend-neutral cache contracts and in-memory backend for Spakky Framework
5
+ Author: Spakky
6
+ Author-email: Spakky <sejong418@icloud.com>
7
+ License: MIT
8
+ Requires-Dist: spakky>=6.3.1
9
+ Requires-Dist: typing-extensions>=4.15.0
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Spakky Cache
14
+
15
+ Backend-neutral application data cache contracts for [Spakky Framework](https://github.com/E5presso/spakky-framework).
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install spakky-cache
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - **Typed cache results**: `CacheHit[T]` and `CacheMiss` represent hit and miss outcomes without backend-specific exceptions.
26
+ - **Sync and async contracts**: `ICache[T]` defines `get`, `set`, `delete`, `clear` and async equivalents.
27
+ - **TTL semantics**: Positive TTL values expire entries deterministically; missing and expired entries are misses.
28
+ - **In-memory backend**: `InMemoryCache[T]` is suitable for local development, tests, and single-process usage.
29
+ - **AOP method caching**: `@cacheable()` and `@cache_evict()` apply cache hit/miss and eviction behavior without manual plumbing.
30
+ - **Application data scope**: This package does not expose or mutate `ApplicationContext` internal caches.
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from datetime import timedelta
36
+
37
+ from spakky.cache import CacheHit, InMemoryCache
38
+
39
+ cache = InMemoryCache[str]()
40
+ cache.set("profile:42", "Ada", ttl=timedelta(minutes=5))
41
+
42
+ result = cache.get("profile:42")
43
+ if isinstance(result, CacheHit):
44
+ print(result.value)
45
+ ```
46
+
47
+ ## Annotation Usage
48
+
49
+ Load the `spakky-cache` plugin, then annotate service methods. The plugin registers `InMemoryCache`, `CacheAspect`, and `AsyncCacheAspect` so sync and async methods are handled through Spakky AOP.
50
+
51
+ ```python
52
+ from datetime import timedelta
53
+
54
+ from spakky.cache import cache_evict, cacheable
55
+ from spakky.core.stereotype.usecase import UseCase
56
+
57
+
58
+ @UseCase()
59
+ class ProfileService:
60
+ def __init__(self) -> None:
61
+ self.calls = 0
62
+
63
+ @cacheable(key="profile:{0}", ttl=timedelta(minutes=5))
64
+ def load_profile(self, user_id: str) -> str:
65
+ self.calls += 1
66
+ return f"profile:{user_id}"
67
+
68
+ @cache_evict(key="profile:{0}")
69
+ def refresh_profile(self, user_id: str) -> None:
70
+ ...
71
+ ```
72
+
73
+ The default key is derived from the method module, qualified name, positional arguments, and sorted keyword arguments. Explicit `key` values are Python format strings evaluated against method call arguments. Invalid key formatting raises `CacheKeyGenerationError`. Backend failures are not swallowed; cache errors propagate loudly.
74
+
75
+ `@cache_evict()` deletes the matching entry only after the annotated method succeeds. Failed method calls leave existing entries untouched so a failed refresh does not erase the last known cached value.
76
+
77
+ ## Async Usage
78
+
79
+ ```python
80
+ from spakky.cache import CacheHit, InMemoryCache
81
+
82
+ cache = InMemoryCache[int]()
83
+ await cache.set_async("answer", 42)
84
+
85
+ result = await cache.get_async("answer")
86
+ if isinstance(result, CacheHit):
87
+ assert result.value == 42
88
+ ```
89
+
90
+ ## TTL Rules
91
+
92
+ `ttl=None` stores an entry until explicit deletion or clear. Positive `float`, `int`, or `datetime.timedelta` values expire entries after that duration. Zero or negative TTL values raise `InvalidCacheTTLError`.
93
+
94
+ The in-memory backend deletes expired entries when they are observed. It is deterministic for one process and does not provide distributed invalidation, stampede protection, or tag-based eviction.
95
+
96
+ Cache eviction annotations remove a single matching entry only after the annotated method succeeds.
97
+
98
+ ## Scope
99
+
100
+ `spakky-cache` is an application data cache abstraction. It is separate from `ApplicationContext` internal type, singleton, and context caches. Distributed locks, cache stampede protection, tag invalidation, write-through/write-behind policies, and metrics exporters are outside the current contract.
101
+
102
+ ## License
103
+
104
+ MIT License
@@ -0,0 +1,92 @@
1
+ # Spakky Cache
2
+
3
+ Backend-neutral application data cache contracts for [Spakky Framework](https://github.com/E5presso/spakky-framework).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install spakky-cache
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Typed cache results**: `CacheHit[T]` and `CacheMiss` represent hit and miss outcomes without backend-specific exceptions.
14
+ - **Sync and async contracts**: `ICache[T]` defines `get`, `set`, `delete`, `clear` and async equivalents.
15
+ - **TTL semantics**: Positive TTL values expire entries deterministically; missing and expired entries are misses.
16
+ - **In-memory backend**: `InMemoryCache[T]` is suitable for local development, tests, and single-process usage.
17
+ - **AOP method caching**: `@cacheable()` and `@cache_evict()` apply cache hit/miss and eviction behavior without manual plumbing.
18
+ - **Application data scope**: This package does not expose or mutate `ApplicationContext` internal caches.
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from datetime import timedelta
24
+
25
+ from spakky.cache import CacheHit, InMemoryCache
26
+
27
+ cache = InMemoryCache[str]()
28
+ cache.set("profile:42", "Ada", ttl=timedelta(minutes=5))
29
+
30
+ result = cache.get("profile:42")
31
+ if isinstance(result, CacheHit):
32
+ print(result.value)
33
+ ```
34
+
35
+ ## Annotation Usage
36
+
37
+ Load the `spakky-cache` plugin, then annotate service methods. The plugin registers `InMemoryCache`, `CacheAspect`, and `AsyncCacheAspect` so sync and async methods are handled through Spakky AOP.
38
+
39
+ ```python
40
+ from datetime import timedelta
41
+
42
+ from spakky.cache import cache_evict, cacheable
43
+ from spakky.core.stereotype.usecase import UseCase
44
+
45
+
46
+ @UseCase()
47
+ class ProfileService:
48
+ def __init__(self) -> None:
49
+ self.calls = 0
50
+
51
+ @cacheable(key="profile:{0}", ttl=timedelta(minutes=5))
52
+ def load_profile(self, user_id: str) -> str:
53
+ self.calls += 1
54
+ return f"profile:{user_id}"
55
+
56
+ @cache_evict(key="profile:{0}")
57
+ def refresh_profile(self, user_id: str) -> None:
58
+ ...
59
+ ```
60
+
61
+ The default key is derived from the method module, qualified name, positional arguments, and sorted keyword arguments. Explicit `key` values are Python format strings evaluated against method call arguments. Invalid key formatting raises `CacheKeyGenerationError`. Backend failures are not swallowed; cache errors propagate loudly.
62
+
63
+ `@cache_evict()` deletes the matching entry only after the annotated method succeeds. Failed method calls leave existing entries untouched so a failed refresh does not erase the last known cached value.
64
+
65
+ ## Async Usage
66
+
67
+ ```python
68
+ from spakky.cache import CacheHit, InMemoryCache
69
+
70
+ cache = InMemoryCache[int]()
71
+ await cache.set_async("answer", 42)
72
+
73
+ result = await cache.get_async("answer")
74
+ if isinstance(result, CacheHit):
75
+ assert result.value == 42
76
+ ```
77
+
78
+ ## TTL Rules
79
+
80
+ `ttl=None` stores an entry until explicit deletion or clear. Positive `float`, `int`, or `datetime.timedelta` values expire entries after that duration. Zero or negative TTL values raise `InvalidCacheTTLError`.
81
+
82
+ The in-memory backend deletes expired entries when they are observed. It is deterministic for one process and does not provide distributed invalidation, stampede protection, or tag-based eviction.
83
+
84
+ Cache eviction annotations remove a single matching entry only after the annotated method succeeds.
85
+
86
+ ## Scope
87
+
88
+ `spakky-cache` is an application data cache abstraction. It is separate from `ApplicationContext` internal type, singleton, and context caches. Distributed locks, cache stampede protection, tag invalidation, write-through/write-behind policies, and metrics exporters are outside the current contract.
89
+
90
+ ## License
91
+
92
+ MIT License
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "spakky-cache"
3
+ version = "6.3.1"
4
+ description = "Backend-neutral cache contracts and in-memory backend for Spakky Framework"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Spakky", email = "sejong418@icloud.com" }]
9
+ dependencies = ["spakky>=6.3.1", "typing-extensions>=4.15.0"]
10
+
11
+ [project.entry-points."spakky.plugins"]
12
+ spakky-cache = "spakky.cache.main:initialize"
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.10.10,<0.11.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [tool.uv.build-backend]
19
+ module-root = "src"
20
+ module-name = "spakky.cache"
21
+
22
+ [tool.pyrefly]
23
+ python-version = "3.11"
24
+ search_path = ["src", ".", "../spakky/src"]
25
+ project_excludes = ["**/__pycache__", "**/*.pyc"]
26
+
27
+ [tool.ruff]
28
+ builtins = ["_"]
29
+ cache-dir = "~/.cache/ruff"
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src", "../spakky/src"]
33
+ testpaths = "tests"
34
+ python_files = ["test_*.py"]
35
+ asyncio_mode = "auto"
36
+ addopts = """
37
+ --cov
38
+ --cov-report=term
39
+ --cov-report=xml
40
+ --no-cov-on-fail
41
+ --strict-markers
42
+ --dist=load
43
+ -p no:warnings
44
+ -n auto
45
+ --spec
46
+ """
47
+ spec_test_format = "{result} {docstring_summary}"
48
+
49
+ [tool.coverage.run]
50
+ include = ["src/spakky/cache/**/*.py"]
51
+ branch = true
52
+
53
+ [tool.coverage.report]
54
+ show_missing = true
55
+ precision = 2
56
+ fail_under = 90
57
+ skip_empty = true
58
+ exclude_lines = [
59
+ "pragma: no cover",
60
+ "def __repr__",
61
+ "raise AssertionError",
62
+ "raise NotImplementedError",
63
+ "@(abc\\.)?abstractmethod",
64
+ "@(typing\\.)?overload",
65
+ "\\.\\.\\.",
66
+ "pass",
67
+ ]
68
+
69
+ [tool.uv.sources]
70
+ spakky = { workspace = true }
@@ -0,0 +1,36 @@
1
+ """Backend-neutral application data cache contracts."""
2
+
3
+ from spakky.core.application.plugin import Plugin
4
+
5
+ from spakky.cache.annotation import CacheEvict, Cacheable, cache_evict, cacheable
6
+ from spakky.cache.aspects.cache_aspect import AsyncCacheAspect, CacheAspect
7
+ from spakky.cache.backends.memory import InMemoryCache
8
+ from spakky.cache.error import (
9
+ AbstractSpakkyCacheError,
10
+ CacheKeyGenerationError,
11
+ InvalidCacheTTLError,
12
+ )
13
+ from spakky.cache.interfaces.cache import ICache, CacheTTL
14
+ from spakky.cache.result import CacheHit, CacheMiss, CacheResult
15
+
16
+ PLUGIN_NAME = Plugin(name="spakky-cache")
17
+ """Plugin identifier for the Spakky Cache package."""
18
+
19
+ __all__ = [
20
+ "ICache",
21
+ "AbstractSpakkyCacheError",
22
+ "AsyncCacheAspect",
23
+ "CacheEvict",
24
+ "CacheHit",
25
+ "CacheKeyGenerationError",
26
+ "CacheMiss",
27
+ "CacheResult",
28
+ "CacheTTL",
29
+ "CacheAspect",
30
+ "Cacheable",
31
+ "InMemoryCache",
32
+ "InvalidCacheTTLError",
33
+ "PLUGIN_NAME",
34
+ "cache_evict",
35
+ "cacheable",
36
+ ]
@@ -0,0 +1,54 @@
1
+ """Cache method annotations."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, ParamSpec, TypeVar
5
+
6
+ from spakky.core.common.annotation import FunctionAnnotation
7
+
8
+ from spakky.cache.interfaces.cache import CacheTTL
9
+
10
+ P = ParamSpec("P")
11
+ R = TypeVar("R")
12
+
13
+
14
+ @dataclass
15
+ class Cacheable(FunctionAnnotation):
16
+ """Annotation for caching method return values."""
17
+
18
+ key: str | None = None
19
+ """Optional format string used as the cache key."""
20
+
21
+ ttl: CacheTTL = None
22
+ """Optional cache entry TTL."""
23
+
24
+
25
+ @dataclass
26
+ class CacheEvict(FunctionAnnotation):
27
+ """Annotation for evicting a cache entry after successful method execution."""
28
+
29
+ key: str | None = None
30
+ """Optional format string used as the cache key."""
31
+
32
+
33
+ def cacheable(
34
+ key: str | None = None,
35
+ *,
36
+ ttl: CacheTTL = None,
37
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
38
+ """Decorate a method so its return value is cached by AOP."""
39
+
40
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
41
+ return Cacheable(key=key, ttl=ttl)(func)
42
+
43
+ return decorator
44
+
45
+
46
+ def cache_evict(
47
+ key: str | None = None,
48
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
49
+ """Decorate a method so its cache entry is evicted after success."""
50
+
51
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
52
+ return CacheEvict(key=key)(func)
53
+
54
+ return decorator
@@ -0,0 +1,5 @@
1
+ """Cache AOP aspects."""
2
+
3
+ from spakky.cache.aspects.cache_aspect import AsyncCacheAspect, CacheAspect
4
+
5
+ __all__ = ["AsyncCacheAspect", "CacheAspect"]
@@ -0,0 +1,114 @@
1
+ """AOP aspects for cache annotations."""
2
+
3
+ from inspect import iscoroutinefunction
4
+
5
+ from spakky.core.aop.aspect import Aspect, AsyncAspect
6
+ from spakky.core.aop.interfaces.aspect import IAspect, IAsyncAspect
7
+ from spakky.core.aop.pointcut import Around
8
+ from spakky.core.common.types import AsyncFunc, Func
9
+ from spakky.core.pod.annotations.order import Order
10
+
11
+ from spakky.cache.annotation import CacheEvict, Cacheable
12
+ from spakky.cache.error import CacheKeyGenerationError
13
+ from spakky.cache.interfaces.cache import ICache
14
+ from spakky.cache.result import CacheHit
15
+
16
+
17
+ def _matches_sync(method: Func) -> bool:
18
+ return (
19
+ Cacheable.exists(method) or CacheEvict.exists(method)
20
+ ) and not iscoroutinefunction(method)
21
+
22
+
23
+ def _matches_async(method: Func) -> bool:
24
+ return (
25
+ Cacheable.exists(method) or CacheEvict.exists(method)
26
+ ) and iscoroutinefunction(method)
27
+
28
+
29
+ def _key_from_call(
30
+ joinpoint: Func,
31
+ configured_key: str | None,
32
+ args: tuple[object, ...],
33
+ kwargs: dict[str, object],
34
+ ) -> str:
35
+ try:
36
+ if configured_key is not None:
37
+ return configured_key.format(*args, **kwargs)
38
+ ordered_kwargs = tuple(sorted(kwargs.items(), key=lambda item: item[0]))
39
+ return (
40
+ f"{joinpoint.__module__}.{joinpoint.__qualname__}:"
41
+ f"args={args!r}:kwargs={ordered_kwargs!r}"
42
+ )
43
+ except Exception as error:
44
+ raise CacheKeyGenerationError() from error
45
+
46
+
47
+ @Order(0)
48
+ @Aspect()
49
+ class CacheAspect(IAspect):
50
+ """Aspect that applies cacheable and cache eviction annotations."""
51
+
52
+ _cache: ICache[object]
53
+
54
+ def __init__(self, cache: ICache[object]) -> None:
55
+ self._cache = cache
56
+
57
+ @Around(_matches_sync)
58
+ def around(
59
+ self,
60
+ joinpoint: Func,
61
+ *args: object,
62
+ **kwargs: object,
63
+ ) -> object:
64
+ evict = CacheEvict.get_or_none(joinpoint)
65
+ if evict is not None:
66
+ result = joinpoint(*args, **kwargs)
67
+ self._cache.delete(_key_from_call(joinpoint, evict.key, args, kwargs))
68
+ return result
69
+
70
+ cacheable = Cacheable.get(joinpoint)
71
+ key = _key_from_call(joinpoint, cacheable.key, args, kwargs)
72
+ cached = self._cache.get(key)
73
+ if isinstance(cached, CacheHit):
74
+ return cached.value
75
+
76
+ result = joinpoint(*args, **kwargs)
77
+ self._cache.set(key, result, ttl=cacheable.ttl)
78
+ return result
79
+
80
+
81
+ @Order(0)
82
+ @AsyncAspect()
83
+ class AsyncCacheAspect(IAsyncAspect):
84
+ """Async aspect that applies cacheable and cache eviction annotations."""
85
+
86
+ _cache: ICache[object]
87
+
88
+ def __init__(self, cache: ICache[object]) -> None:
89
+ self._cache = cache
90
+
91
+ @Around(_matches_async)
92
+ async def around_async(
93
+ self,
94
+ joinpoint: AsyncFunc,
95
+ *args: object,
96
+ **kwargs: object,
97
+ ) -> object:
98
+ evict = CacheEvict.get_or_none(joinpoint)
99
+ if evict is not None:
100
+ result = await joinpoint(*args, **kwargs)
101
+ await self._cache.delete_async(
102
+ _key_from_call(joinpoint, evict.key, args, kwargs)
103
+ )
104
+ return result
105
+
106
+ cacheable = Cacheable.get(joinpoint)
107
+ key = _key_from_call(joinpoint, cacheable.key, args, kwargs)
108
+ cached = await self._cache.get_async(key)
109
+ if isinstance(cached, CacheHit):
110
+ return cached.value
111
+
112
+ result = await joinpoint(*args, **kwargs)
113
+ await self._cache.set_async(key, result, ttl=cacheable.ttl)
114
+ return result
@@ -0,0 +1,5 @@
1
+ """Cache backend implementations."""
2
+
3
+ from spakky.cache.backends.memory import InMemoryCache
4
+
5
+ __all__ = ["InMemoryCache"]
@@ -0,0 +1,86 @@
1
+ """In-memory cache backend."""
2
+
3
+ from collections.abc import Callable
4
+ from datetime import timedelta
5
+ from time import monotonic
6
+ from typing import Generic, TypeVar
7
+
8
+ from typing_extensions import override
9
+
10
+ from spakky.cache.error import InvalidCacheTTLError
11
+ from spakky.cache.interfaces.cache import ICache, CacheTTL
12
+ from spakky.cache.result import CacheHit, CacheMiss, CacheResult
13
+ from spakky.core.common.mutability import immutable
14
+ from spakky.core.pod.annotations.pod import Pod
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ @immutable
20
+ class _CacheEntry(Generic[T]):
21
+ value: T
22
+ expires_at: float | None
23
+
24
+
25
+ @Pod()
26
+ class InMemoryCache(ICache[T]):
27
+ """Deterministic in-memory cache backend for one process."""
28
+
29
+ def __init__(self, *, clock: Callable[[], float] = monotonic) -> None:
30
+ self._clock = clock
31
+ self._entries: dict[str, _CacheEntry[T]] = {}
32
+
33
+ @override
34
+ def get(self, key: str) -> CacheResult[T]:
35
+ entry = self._entries.get(key)
36
+ if entry is None:
37
+ return CacheMiss()
38
+ if self._is_expired(entry):
39
+ self._entries.pop(key)
40
+ return CacheMiss()
41
+ return CacheHit(value=entry.value)
42
+
43
+ @override
44
+ def set(self, key: str, value: T, *, ttl: CacheTTL = None) -> None:
45
+ expires_at = self._compute_expires_at(ttl)
46
+ self._entries[key] = _CacheEntry(value=value, expires_at=expires_at)
47
+
48
+ @override
49
+ def delete(self, key: str) -> bool:
50
+ return self._entries.pop(key, None) is not None
51
+
52
+ @override
53
+ def clear(self) -> None:
54
+ self._entries.clear()
55
+
56
+ @override
57
+ async def get_async(self, key: str) -> CacheResult[T]:
58
+ return self.get(key)
59
+
60
+ @override
61
+ async def set_async(self, key: str, value: T, *, ttl: CacheTTL = None) -> None:
62
+ self.set(key, value, ttl=ttl)
63
+
64
+ @override
65
+ async def delete_async(self, key: str) -> bool:
66
+ return self.delete(key)
67
+
68
+ @override
69
+ async def clear_async(self) -> None:
70
+ self.clear()
71
+
72
+ def _compute_expires_at(self, ttl: CacheTTL) -> float | None:
73
+ if ttl is None:
74
+ return None
75
+ if isinstance(ttl, timedelta):
76
+ ttl_seconds = ttl.total_seconds()
77
+ else:
78
+ ttl_seconds = float(ttl)
79
+ if ttl_seconds <= 0:
80
+ raise InvalidCacheTTLError()
81
+ return self._clock() + ttl_seconds
82
+
83
+ def _is_expired(self, entry: _CacheEntry[T]) -> bool:
84
+ if entry.expires_at is None:
85
+ return False
86
+ return self._clock() >= entry.expires_at
@@ -0,0 +1,23 @@
1
+ """Error classes for the spakky-cache package."""
2
+
3
+ from abc import ABC
4
+
5
+ from spakky.core.common.error import AbstractSpakkyFrameworkError
6
+
7
+
8
+ class AbstractSpakkyCacheError(AbstractSpakkyFrameworkError, ABC):
9
+ """Base class for cache-related errors."""
10
+
11
+ ...
12
+
13
+
14
+ class InvalidCacheTTLError(AbstractSpakkyCacheError):
15
+ """Raised when a cache entry is written with an invalid TTL."""
16
+
17
+ message = "Cache TTL must be positive"
18
+
19
+
20
+ class CacheKeyGenerationError(AbstractSpakkyCacheError):
21
+ """Raised when a cache annotation cannot produce a deterministic key."""
22
+
23
+ message = "Cache key generation failed"
@@ -0,0 +1,5 @@
1
+ """Cache interface contracts."""
2
+
3
+ from spakky.cache.interfaces.cache import ICache
4
+
5
+ __all__ = ["ICache"]
@@ -0,0 +1,56 @@
1
+ """Backend-neutral cache contract."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from datetime import timedelta
5
+ from typing import Generic, TypeAlias, TypeVar
6
+
7
+ from spakky.cache.result import CacheResult
8
+
9
+ CacheTTL: TypeAlias = float | int | timedelta | None
10
+ """Cache entry lifetime. None means no automatic expiry."""
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class ICache(ABC, Generic[T]):
16
+ """Backend-neutral application data cache contract."""
17
+
18
+ @abstractmethod
19
+ def get(self, key: str) -> CacheResult[T]:
20
+ """Return a typed hit or miss result for a cache key."""
21
+ ...
22
+
23
+ @abstractmethod
24
+ def set(self, key: str, value: T, *, ttl: CacheTTL = None) -> None:
25
+ """Store a cache value with an optional TTL."""
26
+ ...
27
+
28
+ @abstractmethod
29
+ def delete(self, key: str) -> bool:
30
+ """Remove one cache key and return whether an entry existed."""
31
+ ...
32
+
33
+ @abstractmethod
34
+ def clear(self) -> None:
35
+ """Remove all cache entries from this backend instance."""
36
+ ...
37
+
38
+ @abstractmethod
39
+ async def get_async(self, key: str) -> CacheResult[T]:
40
+ """Async variant of get."""
41
+ ...
42
+
43
+ @abstractmethod
44
+ async def set_async(self, key: str, value: T, *, ttl: CacheTTL = None) -> None:
45
+ """Async variant of set."""
46
+ ...
47
+
48
+ @abstractmethod
49
+ async def delete_async(self, key: str) -> bool:
50
+ """Async variant of delete."""
51
+ ...
52
+
53
+ @abstractmethod
54
+ async def clear_async(self) -> None:
55
+ """Async variant of clear."""
56
+ ...
@@ -0,0 +1,17 @@
1
+ """Plugin initialization entry point."""
2
+
3
+ from spakky.core.application.application import SpakkyApplication
4
+
5
+ from spakky.cache.aspects.cache_aspect import AsyncCacheAspect, CacheAspect
6
+ from spakky.cache.backends.memory import InMemoryCache
7
+
8
+
9
+ def initialize(app: SpakkyApplication) -> None:
10
+ """Initialize the spakky-cache plugin.
11
+
12
+ Args:
13
+ app: The SpakkyApplication instance.
14
+ """
15
+ app.add(InMemoryCache)
16
+ app.add(CacheAspect)
17
+ app.add(AsyncCacheAspect)
@@ -0,0 +1,23 @@
1
+ """Typed cache result contracts."""
2
+
3
+ from typing import Generic, TypeAlias, TypeVar
4
+
5
+ from spakky.core.common.mutability import immutable
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ @immutable
11
+ class CacheHit(Generic[T]):
12
+ """Cache lookup result for an existing entry."""
13
+
14
+ value: T
15
+
16
+
17
+ @immutable
18
+ class CacheMiss:
19
+ """Cache lookup result for a missing or expired entry."""
20
+
21
+
22
+ CacheResult: TypeAlias = CacheHit[T] | CacheMiss
23
+ """Cache lookup result type."""