Skip to content

Commit 54c9275

Browse files
authored
release(sdk-py): 0.3.12 (langchain-ai#7224)
Release Note: Impllemnets alpha support for server-side SWR w/ the cache
1 parent f393a54 commit 54c9275

4 files changed

Lines changed: 365 additions & 190 deletions

File tree

libs/sdk-py/langgraph_sdk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
from langgraph_sdk.encryption import Encryption
44
from langgraph_sdk.encryption.types import EncryptionContext
55

6-
__version__ = "0.3.11"
6+
__version__ = "0.3.12"
77

88
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]

libs/sdk-py/langgraph_sdk/cache.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77

88
from __future__ import annotations
99

10+
from collections.abc import Awaitable, Callable
1011
from datetime import timedelta
11-
from typing import Any
12+
from typing import Any, Generic, Literal, TypeVar
13+
14+
T = TypeVar("T")
15+
16+
CacheStatus = Literal["miss", "fresh", "stale", "expired"]
1217

1318
try:
1419
from langgraph_api.cache import ( # type: ignore[unresolved-import]
@@ -22,9 +27,29 @@
2227
_cache_set = None
2328

2429

30+
try:
31+
from langgraph_api.cache import SWRResult # type: ignore[unresolved-import]
32+
from langgraph_api.cache import swr as _api_swr # type: ignore[unresolved-import]
33+
34+
except ImportError:
35+
_api_swr = None
36+
37+
class SWRResult(Generic[T]):
38+
"""Result wrapper returned by :func:`swr`."""
39+
40+
value: T
41+
status: CacheStatus
42+
43+
async def mutate(self, value: T = ...) -> T: # type: ignore[assignment]
44+
"""Update or revalidate the cached value."""
45+
...
46+
47+
2548
__all__ = [
49+
"SWRResult",
2650
"cache_get",
2751
"cache_set",
52+
"swr",
2853
]
2954

3055

@@ -60,3 +85,56 @@ async def cache_set(key: str, value: Any, *, ttl: timedelta | None = None) -> No
6085
"(https://docs.langchain.com/langsmith/deployments)."
6186
)
6287
await _cache_set(key, value, ttl)
88+
89+
90+
async def swr(
91+
key: str,
92+
loader: Callable[[], Awaitable[T]],
93+
*,
94+
fresh_for: timedelta | None = None,
95+
max_age: timedelta | None = None,
96+
model: type[T] | None = None,
97+
) -> SWRResult[T]:
98+
"""Load a cached value using stale-while-revalidate semantics.
99+
100+
This helper is server-side only and is intended for caching internal async
101+
dependencies such as auth or metadata lookups.
102+
103+
Args:
104+
key: Cache key.
105+
loader: Async callable that fetches the value on miss/revalidation.
106+
fresh_for: How long a cached value is considered fresh (no revalidation).
107+
Defaults to ``timedelta(0)`` so every access triggers a background
108+
revalidate while still returning the cached value instantly. Values
109+
above :data:`MAX_CACHE_TTL` are clamped to the backend maximum.
110+
max_age: Total lifetime of a cached entry. After this, the next access
111+
blocks on the loader. Defaults to :data:`MAX_CACHE_TTL` (24 h by
112+
default). Values above :data:`MAX_CACHE_TTL` are clamped to the
113+
backend maximum.
114+
model: Optional Pydantic model class. When provided, values are
115+
serialized via ``model_dump(mode="json")`` before storage and
116+
deserialized via ``model.model_validate()`` on read.
117+
118+
Returns:
119+
An :class:`SWRResult` with ``.value``, ``.status``, and an async
120+
``.mutate()`` method.
121+
122+
Semantics:
123+
- cache miss: await ``loader()``, store the value, return it
124+
- fresh hit (age < fresh_for): return the cached value
125+
- stale hit (fresh_for <= age < max_age): return the cached value
126+
immediately and trigger a best-effort background refresh
127+
- expired (age >= max_age): await ``loader()``, store the value, return it
128+
"""
129+
if _api_swr is None:
130+
raise RuntimeError(
131+
"Cache is only available server-side within the LangGraph Agent Server "
132+
"(https://docs.langchain.com/langsmith/deployments)."
133+
)
134+
if fresh_for is None:
135+
fresh_for = timedelta(0)
136+
if max_age is None:
137+
max_age = timedelta(days=1)
138+
return await _api_swr(
139+
key, loader, fresh_for=fresh_for, max_age=max_age, model=model
140+
)

libs/sdk-py/tests/test_cache.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from datetime import timedelta
2+
from unittest.mock import AsyncMock
3+
4+
import pytest
5+
6+
import langgraph_sdk.cache as cache_module
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_swr_proxies_to_server_impl(monkeypatch):
11+
loader = AsyncMock(return_value={"ok": True})
12+
forwarded = {}
13+
14+
async def fake_swr(key, inner_loader, *, fresh_for, max_age, model):
15+
forwarded["key"] = key
16+
forwarded["fresh_for"] = fresh_for
17+
forwarded["max_age"] = max_age
18+
forwarded["model"] = model
19+
return await inner_loader()
20+
21+
monkeypatch.setattr(cache_module, "_api_swr", fake_swr)
22+
23+
result = await cache_module.swr(
24+
"test-key",
25+
loader,
26+
fresh_for=timedelta(seconds=1),
27+
max_age=timedelta(seconds=10),
28+
)
29+
30+
assert result == {"ok": True}
31+
assert forwarded == {
32+
"key": "test-key",
33+
"fresh_for": timedelta(seconds=1),
34+
"max_age": timedelta(seconds=10),
35+
"model": None,
36+
}
37+
loader.assert_awaited_once()
38+
39+
40+
@pytest.mark.asyncio
41+
async def test_swr_requires_server_runtime(monkeypatch):
42+
monkeypatch.setattr(cache_module, "_api_swr", None)
43+
44+
with pytest.raises(RuntimeError, match="Cache is only available server-side"):
45+
await cache_module.swr(
46+
"test-key",
47+
AsyncMock(return_value="unused"),
48+
fresh_for=timedelta(seconds=1),
49+
max_age=timedelta(seconds=10),
50+
)
51+
52+
53+
@pytest.mark.asyncio
54+
async def test_swr_defaults(monkeypatch):
55+
"""fresh_for defaults to 0, max_age defaults to 1 day."""
56+
loader = AsyncMock(return_value="val")
57+
forwarded = {}
58+
59+
async def fake_swr(_key, inner_loader, *, fresh_for, max_age, model): # noqa: ARG001
60+
forwarded["fresh_for"] = fresh_for
61+
forwarded["max_age"] = max_age
62+
return await inner_loader()
63+
64+
monkeypatch.setattr(cache_module, "_api_swr", fake_swr)
65+
66+
await cache_module.swr("k", loader)
67+
68+
assert forwarded["fresh_for"] == timedelta(0)
69+
assert forwarded["max_age"] == timedelta(days=1)

0 commit comments

Comments
 (0)