longcache 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.
- longcache/__init__.py +11 -0
- longcache/backends/__init__.py +21 -0
- longcache/backends/async_redis.py +274 -0
- longcache/backends/base.py +167 -0
- longcache/backends/memory.py +243 -0
- longcache/backends/redis.py +374 -0
- longcache/backends/sqlite.py +521 -0
- longcache/cli.py +216 -0
- longcache/config.py +127 -0
- longcache/core/__init__.py +8 -0
- longcache/core/base_embedder.py +43 -0
- longcache/core/cache.py +872 -0
- longcache/core/embedder.py +155 -0
- longcache/core/entry.py +122 -0
- longcache/core/index.py +272 -0
- longcache/integrations/__init__.py +32 -0
- longcache/integrations/langchain.py +316 -0
- longcache/integrations/langgraph.py +150 -0
- longcache/integrations/llamaindex.py +103 -0
- longcache/integrations/openai.py +146 -0
- longcache/middleware.py +366 -0
- longcache-0.1.0.dist-info/METADATA +942 -0
- longcache-0.1.0.dist-info/RECORD +26 -0
- longcache-0.1.0.dist-info/WHEEL +4 -0
- longcache-0.1.0.dist-info/entry_points.txt +2 -0
- longcache-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI integration for LongCache.
|
|
3
|
+
|
|
4
|
+
Provides CachedOpenAI that wraps the OpenAI Python client
|
|
5
|
+
with semantic caching, supporting both sync and async calls
|
|
6
|
+
as well as streaming.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from longcache import LongCache, CacheConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CachedOpenAI:
|
|
17
|
+
"""
|
|
18
|
+
Wraps the OpenAI Python client with semantic caching.
|
|
19
|
+
|
|
20
|
+
Intercepts chat completion calls before they reach the OpenAI API.
|
|
21
|
+
On cache HIT, returns the cached response instantly without an API call.
|
|
22
|
+
|
|
23
|
+
Requires the 'openai' extra: uv pip install longcache[openai]
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
from openai import OpenAI
|
|
27
|
+
from longcache.integrations.openai import CachedOpenAI
|
|
28
|
+
|
|
29
|
+
client = CachedOpenAI(OpenAI(api_key="..."), cache=LongCache())
|
|
30
|
+
response = client.chat.completions.create(
|
|
31
|
+
model="gpt-4o",
|
|
32
|
+
messages=[{"role": "user", "content": "What is RAG?"}]
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
client: Any,
|
|
39
|
+
cache: Optional[LongCache] = None,
|
|
40
|
+
config: Optional[CacheConfig] = None,
|
|
41
|
+
namespace: str = "openai",
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize CachedOpenAI.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client: An OpenAI client instance (sync or async).
|
|
48
|
+
cache: Existing LongCache instance. Creates a new one if not provided.
|
|
49
|
+
config: CacheConfig for the new cache (ignored if cache is provided).
|
|
50
|
+
namespace: Default namespace for cache entries.
|
|
51
|
+
"""
|
|
52
|
+
self._client = client
|
|
53
|
+
self.cache = cache or LongCache(config or CacheConfig(namespace=namespace))
|
|
54
|
+
self._namespace = namespace
|
|
55
|
+
|
|
56
|
+
# Wrap chat.completions with caching
|
|
57
|
+
self.chat = CachedChatCompletions(self)
|
|
58
|
+
|
|
59
|
+
def _extract_query(self, messages: list[dict]) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Extract the user query from a messages list.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
messages: List of message dicts with 'role' and 'content' keys.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The content of the last user message.
|
|
68
|
+
"""
|
|
69
|
+
for msg in reversed(messages):
|
|
70
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
71
|
+
return msg.get("content", str(msg))
|
|
72
|
+
# Fallback: stringify all messages
|
|
73
|
+
return str(messages)
|
|
74
|
+
|
|
75
|
+
def __getattr__(self, name: str) -> Any:
|
|
76
|
+
"""Proxy all other attributes to the underlying client."""
|
|
77
|
+
return getattr(self._client, name)
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
return f"CachedOpenAI(client={self._client!r}, cache={self.cache!r})"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CachedChatCompletions:
|
|
84
|
+
"""
|
|
85
|
+
Cached wrapper for the chat.completions endpoint.
|
|
86
|
+
|
|
87
|
+
Provides create() methods that check the cache before making API calls.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, parent: CachedOpenAI):
|
|
91
|
+
"""
|
|
92
|
+
Initialize CachedChatCompletions.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
parent: The parent CachedOpenAI instance.
|
|
96
|
+
"""
|
|
97
|
+
self._parent = parent
|
|
98
|
+
|
|
99
|
+
def create(self, **kwargs) -> Any:
|
|
100
|
+
"""
|
|
101
|
+
Create a chat completion with caching.
|
|
102
|
+
|
|
103
|
+
Extracts the user query from messages, checks the cache,
|
|
104
|
+
and only calls the API on a cache MISS.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
**kwargs: Arguments passed to openai.chat.completions.create().
|
|
108
|
+
Must include 'messages' with at least one user message.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The chat completion response (from cache or API).
|
|
112
|
+
"""
|
|
113
|
+
messages = kwargs.get("messages", [])
|
|
114
|
+
query = self._parent._extract_query(messages)
|
|
115
|
+
|
|
116
|
+
cached = self._parent.cache.get(query, namespace=self._parent._namespace)
|
|
117
|
+
if cached is not None:
|
|
118
|
+
return cached
|
|
119
|
+
|
|
120
|
+
result = self._parent._client.chat.completions.create(**kwargs)
|
|
121
|
+
self._parent.cache.set(query, result, namespace=self._parent._namespace)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
async def acreate(self, **kwargs) -> Any:
|
|
125
|
+
"""
|
|
126
|
+
Async create a chat completion with caching.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
**kwargs: Arguments passed to the async OpenAI client.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The chat completion response (from cache or API).
|
|
133
|
+
"""
|
|
134
|
+
messages = kwargs.get("messages", [])
|
|
135
|
+
query = self._parent._extract_query(messages)
|
|
136
|
+
|
|
137
|
+
cached = self._parent.cache.get(query, namespace=self._parent._namespace)
|
|
138
|
+
if cached is not None:
|
|
139
|
+
return cached
|
|
140
|
+
|
|
141
|
+
result = await self._parent._client.chat.completions.acreate(**kwargs)
|
|
142
|
+
self._parent.cache.set(query, result, namespace=self._parent._namespace)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
return f"CachedChatCompletions(parent={self._parent!r})"
|
longcache/middleware.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI/Starlette ASGI middleware for LongCache.
|
|
3
|
+
|
|
4
|
+
Intercepts HTTP requests to chat completion endpoints, checks the
|
|
5
|
+
semantic cache before forwarding to the upstream LLM API, and
|
|
6
|
+
auto-caches responses. Drop-in middleware for any Starlette app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, Optional, Set
|
|
14
|
+
|
|
15
|
+
from longcache import LongCache, CacheConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LongCacheMiddleware:
|
|
19
|
+
"""
|
|
20
|
+
ASGI middleware that adds semantic caching to LLM API endpoints.
|
|
21
|
+
|
|
22
|
+
Intercepts POST requests to configured routes (default: /v1/chat/completions),
|
|
23
|
+
extracts the user query, checks the semantic cache, and either returns
|
|
24
|
+
the cached response or forwards the request and caches the result.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
from fastapi import FastAPI
|
|
28
|
+
from longcache.middleware import LongCacheMiddleware
|
|
29
|
+
|
|
30
|
+
app = FastAPI()
|
|
31
|
+
app.add_middleware(LongCacheMiddleware, cache=LongCache())
|
|
32
|
+
|
|
33
|
+
Or with Starlette:
|
|
34
|
+
from starlette.applications import Starlette
|
|
35
|
+
from longcache.middleware import LongCacheMiddleware
|
|
36
|
+
|
|
37
|
+
app = Starlette(middleware=[
|
|
38
|
+
(LongCacheMiddleware, {"cache": LongCache()})
|
|
39
|
+
])
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
app: Callable,
|
|
45
|
+
cache: Optional[LongCache] = None,
|
|
46
|
+
config: Optional[CacheConfig] = None,
|
|
47
|
+
namespace: str = "http",
|
|
48
|
+
cached_routes: Optional[Set[str]] = None,
|
|
49
|
+
exclude_namespaces: Optional[Set[str]] = None,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize the LongCache ASGI middleware.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
app: The ASGI application to wrap.
|
|
56
|
+
cache: Existing LongCache instance. Creates a new one if not provided.
|
|
57
|
+
config: CacheConfig for the new cache (ignored if cache is provided).
|
|
58
|
+
namespace: Default namespace for cache entries.
|
|
59
|
+
cached_routes: Set of URL paths that should be cached.
|
|
60
|
+
Defaults to {"/v1/chat/completions", "/chat/completions"}.
|
|
61
|
+
exclude_namespaces: Set of namespaces to exclude from caching.
|
|
62
|
+
"""
|
|
63
|
+
self.app = app
|
|
64
|
+
self.cache = cache or LongCache(config or CacheConfig(namespace=namespace))
|
|
65
|
+
self._namespace = namespace
|
|
66
|
+
self.cached_routes = cached_routes or {
|
|
67
|
+
"/v1/chat/completions",
|
|
68
|
+
"/chat/completions",
|
|
69
|
+
}
|
|
70
|
+
self.exclude_namespaces = exclude_namespaces or set()
|
|
71
|
+
|
|
72
|
+
async def __call__(self, scope: dict, receive: Callable, send: Callable):
|
|
73
|
+
"""
|
|
74
|
+
ASGI middleware entry point.
|
|
75
|
+
|
|
76
|
+
Intercepts request/response flow for configured routes.
|
|
77
|
+
"""
|
|
78
|
+
if scope["type"] != "http":
|
|
79
|
+
await self.app(scope, receive, send)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
path = scope.get("path", "")
|
|
83
|
+
method = scope.get("method", "")
|
|
84
|
+
|
|
85
|
+
# Only intercept POST requests to cached routes
|
|
86
|
+
if method != "POST" or path not in self.cached_routes:
|
|
87
|
+
await self.app(scope, receive, send)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Read the request body
|
|
91
|
+
body = await self._read_body(receive)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
request_data = json.loads(body) if body else {}
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
# Not JSON, pass through without caching
|
|
97
|
+
await self._send_passthrough(scope, body, send)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Extract the user query from messages
|
|
101
|
+
query = self._extract_query(request_data)
|
|
102
|
+
if not query:
|
|
103
|
+
await self._send_passthrough(scope, body, send)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Check cache
|
|
107
|
+
cached = self.cache.get(query, namespace=self._namespace)
|
|
108
|
+
if cached is not None:
|
|
109
|
+
await self._send_cached_response(cached, send)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Cache MISS — forward request and capture response
|
|
113
|
+
response_data = await self._forward_and_capture(scope, body, send)
|
|
114
|
+
|
|
115
|
+
# Cache the response
|
|
116
|
+
if response_data is not None:
|
|
117
|
+
self.cache.set(query, response_data, namespace=self._namespace)
|
|
118
|
+
|
|
119
|
+
def _extract_query(self, request_data: dict) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Extract the user query from a chat completion request.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
request_data: Parsed JSON request body.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The extracted query string, or empty string if not found.
|
|
128
|
+
"""
|
|
129
|
+
messages = request_data.get("messages", [])
|
|
130
|
+
for msg in reversed(messages):
|
|
131
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
132
|
+
return msg.get("content", "")
|
|
133
|
+
return ""
|
|
134
|
+
|
|
135
|
+
async def _read_body(self, receive: Callable) -> bytes:
|
|
136
|
+
"""
|
|
137
|
+
Read the full request body from the ASGI receive callable.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
receive: The ASGI receive callable.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The complete request body as bytes.
|
|
144
|
+
"""
|
|
145
|
+
body = b""
|
|
146
|
+
while True:
|
|
147
|
+
message = await receive()
|
|
148
|
+
if message["type"] == "http.request":
|
|
149
|
+
body += message.get("body", b"")
|
|
150
|
+
if not message.get("more_body", False):
|
|
151
|
+
break
|
|
152
|
+
elif message["type"] == "http.disconnect":
|
|
153
|
+
break
|
|
154
|
+
return body
|
|
155
|
+
|
|
156
|
+
async def _send_passthrough(self, scope: dict, body: bytes, send: Callable):
|
|
157
|
+
"""
|
|
158
|
+
Forward the original request without caching.
|
|
159
|
+
|
|
160
|
+
Reconstructs the ASGI receive callable with the captured body
|
|
161
|
+
and forwards to the upstream app.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
async def receive_with_body():
|
|
165
|
+
return {
|
|
166
|
+
"type": "http.request",
|
|
167
|
+
"body": body,
|
|
168
|
+
"more_body": False,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await self.app(scope, receive_with_body, send)
|
|
172
|
+
|
|
173
|
+
async def _send_cached_response(self, cached_response: Any, send: Callable):
|
|
174
|
+
"""
|
|
175
|
+
Send a cached response as an HTTP response.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
cached_response: The cached response data.
|
|
179
|
+
send: The ASGI send callable.
|
|
180
|
+
"""
|
|
181
|
+
response_body = json.dumps(cached_response, default=str).encode("utf-8")
|
|
182
|
+
|
|
183
|
+
await send(
|
|
184
|
+
{
|
|
185
|
+
"type": "http.response.start",
|
|
186
|
+
"status": 200,
|
|
187
|
+
"headers": [
|
|
188
|
+
[b"content-type", b"application/json"],
|
|
189
|
+
[b"x-longcache", b"HIT"],
|
|
190
|
+
],
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
await send(
|
|
194
|
+
{
|
|
195
|
+
"type": "http.response.body",
|
|
196
|
+
"body": response_body,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def _forward_and_capture(self, scope: dict, body: bytes, send: Callable) -> Optional[Any]:
|
|
201
|
+
"""
|
|
202
|
+
Forward the request to the upstream app and capture the response.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
scope: The ASGI scope.
|
|
206
|
+
body: The request body.
|
|
207
|
+
send: The original ASGI send callable.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The parsed response data if successful, None otherwise.
|
|
211
|
+
"""
|
|
212
|
+
response_parts = []
|
|
213
|
+
response_started = False
|
|
214
|
+
response_status = 200
|
|
215
|
+
|
|
216
|
+
async def capturing_send(message: dict):
|
|
217
|
+
nonlocal response_started, response_status
|
|
218
|
+
|
|
219
|
+
if message["type"] == "http.response.start":
|
|
220
|
+
response_started = True
|
|
221
|
+
response_status = message.get("status", 200)
|
|
222
|
+
# Only capture headers, pass through to real send
|
|
223
|
+
await send(message)
|
|
224
|
+
elif message["type"] == "http.response.body":
|
|
225
|
+
response_parts.append(message.get("body", b""))
|
|
226
|
+
await send(message)
|
|
227
|
+
|
|
228
|
+
async def receive_with_body():
|
|
229
|
+
return {
|
|
230
|
+
"type": "http.request",
|
|
231
|
+
"body": body,
|
|
232
|
+
"more_body": False,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
await self.app(scope, receive_with_body, capturing_send)
|
|
237
|
+
except Exception:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
if response_status != 200:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Parse the captured response
|
|
244
|
+
full_response = b"".join(response_parts)
|
|
245
|
+
try:
|
|
246
|
+
return json.loads(full_response)
|
|
247
|
+
except (json.JSONDecodeError, TypeError):
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def __repr__(self) -> str:
|
|
251
|
+
return f"LongCacheMiddleware(routes={self.cached_routes}, cache={self.cache!r})"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class StreamingCacheAdapter:
|
|
255
|
+
"""
|
|
256
|
+
Adapter for caching streaming LLM responses.
|
|
257
|
+
|
|
258
|
+
Collects streaming chunks into a complete response for caching,
|
|
259
|
+
and replays cached responses token-by-token to maintain the
|
|
260
|
+
streaming UX for end users.
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
from longcache.middleware import StreamingCacheAdapter
|
|
264
|
+
|
|
265
|
+
def my_stream_func(query):
|
|
266
|
+
for chunk in llm.stream(query):
|
|
267
|
+
yield chunk
|
|
268
|
+
|
|
269
|
+
adapter = StreamingCacheAdapter(cache=LongCache(), stream_func=my_stream_func)
|
|
270
|
+
for token in adapter.stream("What is RAG?"):
|
|
271
|
+
print(token, end="")
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(
|
|
275
|
+
self,
|
|
276
|
+
cache: LongCache,
|
|
277
|
+
stream_func: Callable,
|
|
278
|
+
namespace: str = "streaming",
|
|
279
|
+
replay_delay: float = 0.01,
|
|
280
|
+
):
|
|
281
|
+
"""
|
|
282
|
+
Initialize the streaming cache adapter.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
cache: LongCache instance for storage.
|
|
286
|
+
stream_func: Callable that yields response chunks for a query.
|
|
287
|
+
namespace: Cache namespace for streaming entries.
|
|
288
|
+
replay_delay: Delay in seconds between replayed tokens
|
|
289
|
+
to simulate streaming pacing.
|
|
290
|
+
"""
|
|
291
|
+
self.cache = cache
|
|
292
|
+
self.stream_func = stream_func
|
|
293
|
+
self._namespace = namespace
|
|
294
|
+
self._replay_delay = replay_delay
|
|
295
|
+
|
|
296
|
+
def stream(self, query: str):
|
|
297
|
+
"""
|
|
298
|
+
Stream a response with caching.
|
|
299
|
+
|
|
300
|
+
On cache HIT, replays the cached response token-by-token.
|
|
301
|
+
On cache MISS, streams from the source, collects chunks,
|
|
302
|
+
and caches the assembled response.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
query: The query string.
|
|
306
|
+
|
|
307
|
+
Yields:
|
|
308
|
+
Response tokens/chunks.
|
|
309
|
+
"""
|
|
310
|
+
cached = self.cache.get(query, namespace=self._namespace)
|
|
311
|
+
if cached is not None:
|
|
312
|
+
# Replay cached response with realistic pacing
|
|
313
|
+
import time as _time
|
|
314
|
+
import re
|
|
315
|
+
|
|
316
|
+
cached_str = str(cached)
|
|
317
|
+
tokens = re.findall(r"\S+\s*", cached_str)
|
|
318
|
+
for token in tokens:
|
|
319
|
+
yield token
|
|
320
|
+
if self._replay_delay > 0:
|
|
321
|
+
_time.sleep(self._replay_delay)
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Stream from source and collect
|
|
325
|
+
collected = []
|
|
326
|
+
for chunk in self.stream_func(query):
|
|
327
|
+
collected.append(str(chunk))
|
|
328
|
+
yield chunk
|
|
329
|
+
|
|
330
|
+
# Cache the assembled response
|
|
331
|
+
assembled = "".join(collected)
|
|
332
|
+
self.cache.set(query, assembled, namespace=self._namespace)
|
|
333
|
+
|
|
334
|
+
async def astream(self, query: str):
|
|
335
|
+
"""
|
|
336
|
+
Async stream a response with caching.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
query: The query string.
|
|
340
|
+
|
|
341
|
+
Yields:
|
|
342
|
+
Response tokens/chunks.
|
|
343
|
+
"""
|
|
344
|
+
cached = self.cache.get(query, namespace=self._namespace)
|
|
345
|
+
if cached is not None:
|
|
346
|
+
import asyncio
|
|
347
|
+
import re
|
|
348
|
+
|
|
349
|
+
cached_str = str(cached)
|
|
350
|
+
tokens = re.findall(r"\S+\s*", cached_str)
|
|
351
|
+
for token in tokens:
|
|
352
|
+
yield token
|
|
353
|
+
if self._replay_delay > 0:
|
|
354
|
+
await asyncio.sleep(self._replay_delay)
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
collected = []
|
|
358
|
+
async for chunk in self.stream_func(query):
|
|
359
|
+
collected.append(str(chunk))
|
|
360
|
+
yield chunk
|
|
361
|
+
|
|
362
|
+
assembled = "".join(collected)
|
|
363
|
+
self.cache.set(query, assembled, namespace=self._namespace)
|
|
364
|
+
|
|
365
|
+
def __repr__(self) -> str:
|
|
366
|
+
return f"StreamingCacheAdapter(cache={self.cache!r})"
|