hishel 0.1.5__py3-none-any.whl → 1.0.0b1__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.
- hishel/__init__.py +59 -52
- hishel/_async_cache.py +213 -0
- hishel/_async_httpx.py +236 -0
- hishel/{beta/_core → _core}/_headers.py +11 -1
- hishel/{beta/_core → _core}/_spec.py +270 -136
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/_storages/_async_sqlite.py +420 -0
- hishel/_core/_storages/_packing.py +144 -0
- hishel/_core/_storages/_sync_base.py +71 -0
- hishel/_core/_storages/_sync_sqlite.py +420 -0
- hishel/{beta/_core → _core}/models.py +100 -37
- hishel/_policies.py +49 -0
- hishel/_sync_cache.py +213 -0
- hishel/_sync_httpx.py +236 -0
- hishel/_utils.py +37 -366
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +12 -0
- hishel/{beta/requests.py → requests.py} +31 -25
- hishel-1.0.0b1.dist-info/METADATA +509 -0
- hishel-1.0.0b1.dist-info/RECORD +24 -0
- hishel/_async/__init__.py +0 -5
- hishel/_async/_client.py +0 -30
- hishel/_async/_mock.py +0 -43
- hishel/_async/_pool.py +0 -201
- hishel/_async/_storages.py +0 -768
- hishel/_async/_transports.py +0 -282
- hishel/_controller.py +0 -581
- hishel/_exceptions.py +0 -10
- hishel/_files.py +0 -54
- hishel/_headers.py +0 -215
- hishel/_lfu_cache.py +0 -71
- hishel/_lmdb_types_.pyi +0 -53
- hishel/_s3.py +0 -122
- hishel/_serializers.py +0 -329
- hishel/_sync/__init__.py +0 -5
- hishel/_sync/_client.py +0 -30
- hishel/_sync/_mock.py +0 -43
- hishel/_sync/_pool.py +0 -201
- hishel/_sync/_storages.py +0 -768
- hishel/_sync/_transports.py +0 -282
- hishel/_synchronization.py +0 -37
- hishel/beta/__init__.py +0 -59
- hishel/beta/_async_cache.py +0 -167
- hishel/beta/_core/__init__.py +0 -0
- hishel/beta/_core/_async/_storages/_sqlite.py +0 -411
- hishel/beta/_core/_base/_storages/_base.py +0 -272
- hishel/beta/_core/_base/_storages/_packing.py +0 -165
- hishel/beta/_core/_sync/_storages/_sqlite.py +0 -411
- hishel/beta/_sync_cache.py +0 -167
- hishel/beta/httpx.py +0 -328
- hishel-0.1.5.dist-info/METADATA +0 -258
- hishel-0.1.5.dist-info/RECORD +0 -41
- {hishel-0.1.5.dist-info → hishel-1.0.0b1.dist-info}/WHEEL +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
hishel/_sync/_transports.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
import typing as tp
|
|
5
|
-
|
|
6
|
-
import httpcore
|
|
7
|
-
import httpx
|
|
8
|
-
from httpx import SyncByteStream, Request, Response
|
|
9
|
-
from httpx._exceptions import ConnectError
|
|
10
|
-
|
|
11
|
-
from .._controller import Controller, allowed_stale
|
|
12
|
-
from .._headers import parse_cache_control
|
|
13
|
-
from .._serializers import JSONSerializer, Metadata
|
|
14
|
-
from .._utils import extract_header_values_decoded, normalized_url
|
|
15
|
-
from ._storages import BaseStorage, FileStorage
|
|
16
|
-
|
|
17
|
-
if tp.TYPE_CHECKING: # pragma: no cover
|
|
18
|
-
from typing_extensions import Self
|
|
19
|
-
|
|
20
|
-
__all__ = ("CacheTransport",)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def fake_stream(content: bytes) -> tp.Iterable[bytes]:
|
|
24
|
-
yield content
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def generate_504() -> Response:
|
|
28
|
-
return Response(status_code=504)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class CacheStream(SyncByteStream):
|
|
32
|
-
def __init__(self, httpcore_stream: tp.Iterable[bytes]):
|
|
33
|
-
self._httpcore_stream = httpcore_stream
|
|
34
|
-
|
|
35
|
-
def __iter__(self) -> tp.Iterator[bytes]:
|
|
36
|
-
for part in self._httpcore_stream:
|
|
37
|
-
yield part
|
|
38
|
-
|
|
39
|
-
def close(self) -> None:
|
|
40
|
-
if hasattr(self._httpcore_stream, "close"):
|
|
41
|
-
self._httpcore_stream.close()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class CacheTransport(httpx.BaseTransport):
|
|
45
|
-
"""
|
|
46
|
-
An HTTPX Transport that supports HTTP caching.
|
|
47
|
-
|
|
48
|
-
:param transport: `Transport` that our class wraps in order to add an HTTP Cache layer on top of
|
|
49
|
-
:type transport: httpx.BaseTransport
|
|
50
|
-
:param storage: Storage that handles how the responses should be saved., defaults to None
|
|
51
|
-
:type storage: tp.Optional[BaseStorage], optional
|
|
52
|
-
:param controller: Controller that manages the cache behavior at the specification level, defaults to None
|
|
53
|
-
:type controller: tp.Optional[Controller], optional
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(
|
|
57
|
-
self,
|
|
58
|
-
transport: httpx.BaseTransport,
|
|
59
|
-
storage: tp.Optional[BaseStorage] = None,
|
|
60
|
-
controller: tp.Optional[Controller] = None,
|
|
61
|
-
) -> None:
|
|
62
|
-
self._transport = transport
|
|
63
|
-
|
|
64
|
-
self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer())
|
|
65
|
-
|
|
66
|
-
if not isinstance(self._storage, BaseStorage): # pragma: no cover
|
|
67
|
-
raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`")
|
|
68
|
-
|
|
69
|
-
self._controller = controller if controller is not None else Controller()
|
|
70
|
-
|
|
71
|
-
def handle_request(self, request: Request) -> Response:
|
|
72
|
-
"""
|
|
73
|
-
Handles HTTP requests while also implementing HTTP caching.
|
|
74
|
-
|
|
75
|
-
:param request: An HTTP request
|
|
76
|
-
:type request: httpx.Request
|
|
77
|
-
:return: An HTTP response
|
|
78
|
-
:rtype: httpx.Response
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
if request.extensions.get("cache_disabled", False):
|
|
82
|
-
request.headers.update(
|
|
83
|
-
[
|
|
84
|
-
("Cache-Control", "no-store"),
|
|
85
|
-
("Cache-Control", "no-cache"),
|
|
86
|
-
*[("cache-control", value) for value in request.headers.get_list("cache-control")],
|
|
87
|
-
]
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if request.method not in ["GET", "HEAD"]:
|
|
91
|
-
# If the HTTP method is, for example, POST,
|
|
92
|
-
# we must also use the request data to generate the hash.
|
|
93
|
-
body_for_key = request.read()
|
|
94
|
-
request.stream = CacheStream(fake_stream(body_for_key))
|
|
95
|
-
else:
|
|
96
|
-
body_for_key = b""
|
|
97
|
-
|
|
98
|
-
# Construct the HTTPCore request because Controllers and Storages work with HTTPCore requests.
|
|
99
|
-
httpcore_request = httpcore.Request(
|
|
100
|
-
method=request.method,
|
|
101
|
-
url=httpcore.URL(
|
|
102
|
-
scheme=request.url.raw_scheme,
|
|
103
|
-
host=request.url.raw_host,
|
|
104
|
-
port=request.url.port,
|
|
105
|
-
target=request.url.raw_path,
|
|
106
|
-
),
|
|
107
|
-
headers=request.headers.raw,
|
|
108
|
-
content=request.stream,
|
|
109
|
-
extensions=request.extensions,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
key = self._controller._key_generator(httpcore_request, body_for_key)
|
|
113
|
-
stored_data = self._storage.retrieve(key)
|
|
114
|
-
|
|
115
|
-
request_cache_control = parse_cache_control(
|
|
116
|
-
extract_header_values_decoded(request.headers.raw, b"Cache-Control")
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
if request_cache_control.only_if_cached and not stored_data:
|
|
120
|
-
return generate_504()
|
|
121
|
-
|
|
122
|
-
if stored_data:
|
|
123
|
-
# Try using the stored response if it was discovered.
|
|
124
|
-
|
|
125
|
-
stored_response, stored_request, metadata = stored_data
|
|
126
|
-
|
|
127
|
-
# Immediately read the stored response to avoid issues when trying to access the response body.
|
|
128
|
-
stored_response.read()
|
|
129
|
-
|
|
130
|
-
res = self._controller.construct_response_from_cache(
|
|
131
|
-
request=httpcore_request,
|
|
132
|
-
response=stored_response,
|
|
133
|
-
original_request=stored_request,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if isinstance(res, httpcore.Response):
|
|
137
|
-
# Simply use the response if the controller determines it is ready for use.
|
|
138
|
-
return self._create_hishel_response(
|
|
139
|
-
key=key,
|
|
140
|
-
response=res,
|
|
141
|
-
request=httpcore_request,
|
|
142
|
-
cached=True,
|
|
143
|
-
revalidated=False,
|
|
144
|
-
metadata=metadata,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
if request_cache_control.only_if_cached:
|
|
148
|
-
return generate_504()
|
|
149
|
-
|
|
150
|
-
if isinstance(res, httpcore.Request):
|
|
151
|
-
# Controller has determined that the response needs to be re-validated.
|
|
152
|
-
assert isinstance(res.stream, tp.Iterable)
|
|
153
|
-
revalidation_request = Request(
|
|
154
|
-
method=res.method.decode(),
|
|
155
|
-
url=normalized_url(res.url),
|
|
156
|
-
headers=res.headers,
|
|
157
|
-
stream=CacheStream(res.stream),
|
|
158
|
-
extensions=res.extensions,
|
|
159
|
-
)
|
|
160
|
-
try:
|
|
161
|
-
revalidation_response = self._transport.handle_request(revalidation_request)
|
|
162
|
-
except ConnectError:
|
|
163
|
-
# If there is a connection error, we can use the stale response if allowed.
|
|
164
|
-
if self._controller._allow_stale and allowed_stale(response=stored_response):
|
|
165
|
-
return self._create_hishel_response(
|
|
166
|
-
key=key,
|
|
167
|
-
response=stored_response,
|
|
168
|
-
request=httpcore_request,
|
|
169
|
-
cached=True,
|
|
170
|
-
revalidated=False,
|
|
171
|
-
metadata=metadata,
|
|
172
|
-
)
|
|
173
|
-
raise # pragma: no cover
|
|
174
|
-
assert isinstance(revalidation_response.stream, tp.Iterable)
|
|
175
|
-
httpcore_revalidation_response = httpcore.Response(
|
|
176
|
-
status=revalidation_response.status_code,
|
|
177
|
-
headers=revalidation_response.headers.raw,
|
|
178
|
-
content=CacheStream(revalidation_response.stream),
|
|
179
|
-
extensions=revalidation_response.extensions,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Merge headers with the stale response.
|
|
183
|
-
final_httpcore_response = self._controller.handle_validation_response(
|
|
184
|
-
old_response=stored_response,
|
|
185
|
-
new_response=httpcore_revalidation_response,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
final_httpcore_response.read()
|
|
189
|
-
revalidation_response.close()
|
|
190
|
-
|
|
191
|
-
assert isinstance(final_httpcore_response.stream, tp.Iterable)
|
|
192
|
-
|
|
193
|
-
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
194
|
-
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
195
|
-
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
196
|
-
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
197
|
-
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
198
|
-
if revalidation_response.status_code != 304 and self._controller.is_cachable(
|
|
199
|
-
request=httpcore_request, response=final_httpcore_response
|
|
200
|
-
):
|
|
201
|
-
self._storage.store(key, response=final_httpcore_response, request=httpcore_request)
|
|
202
|
-
|
|
203
|
-
return self._create_hishel_response(
|
|
204
|
-
key=key,
|
|
205
|
-
response=final_httpcore_response,
|
|
206
|
-
request=httpcore_request,
|
|
207
|
-
cached=revalidation_response.status_code == 304,
|
|
208
|
-
revalidated=True,
|
|
209
|
-
metadata=metadata,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
regular_response = self._transport.handle_request(request)
|
|
213
|
-
try:
|
|
214
|
-
# Prefer already-read content, if available
|
|
215
|
-
stream = fake_stream(regular_response.content)
|
|
216
|
-
except httpx.ResponseNotRead:
|
|
217
|
-
# Fall back to stream if not yet read
|
|
218
|
-
assert isinstance(regular_response.stream, tp.Iterable)
|
|
219
|
-
stream = regular_response.stream
|
|
220
|
-
httpcore_regular_response = httpcore.Response(
|
|
221
|
-
status=regular_response.status_code,
|
|
222
|
-
headers=regular_response.headers.raw,
|
|
223
|
-
content=CacheStream(stream),
|
|
224
|
-
extensions=regular_response.extensions,
|
|
225
|
-
)
|
|
226
|
-
httpcore_regular_response.read()
|
|
227
|
-
httpcore_regular_response.close()
|
|
228
|
-
|
|
229
|
-
if self._controller.is_cachable(request=httpcore_request, response=httpcore_regular_response):
|
|
230
|
-
self._storage.store(
|
|
231
|
-
key,
|
|
232
|
-
response=httpcore_regular_response,
|
|
233
|
-
request=httpcore_request,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
return self._create_hishel_response(
|
|
237
|
-
key=key,
|
|
238
|
-
response=httpcore_regular_response,
|
|
239
|
-
request=httpcore_request,
|
|
240
|
-
cached=False,
|
|
241
|
-
revalidated=False,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
def _create_hishel_response(
|
|
245
|
-
self,
|
|
246
|
-
key: str,
|
|
247
|
-
response: httpcore.Response,
|
|
248
|
-
request: httpcore.Request,
|
|
249
|
-
cached: bool,
|
|
250
|
-
revalidated: bool,
|
|
251
|
-
metadata: Metadata | None = None,
|
|
252
|
-
) -> Response:
|
|
253
|
-
if cached:
|
|
254
|
-
assert metadata
|
|
255
|
-
metadata["number_of_uses"] += 1
|
|
256
|
-
self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
|
|
257
|
-
response.extensions["from_cache"] = True # type: ignore[index]
|
|
258
|
-
response.extensions["cache_metadata"] = metadata # type: ignore[index]
|
|
259
|
-
else:
|
|
260
|
-
response.extensions["from_cache"] = False # type: ignore[index]
|
|
261
|
-
response.extensions["revalidated"] = revalidated # type: ignore[index]
|
|
262
|
-
return Response(
|
|
263
|
-
status_code=response.status,
|
|
264
|
-
headers=response.headers,
|
|
265
|
-
stream=CacheStream(fake_stream(response.content)),
|
|
266
|
-
extensions=response.extensions,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
def close(self) -> None:
|
|
270
|
-
self._storage.close()
|
|
271
|
-
self._transport.close()
|
|
272
|
-
|
|
273
|
-
def __enter__(self) -> Self:
|
|
274
|
-
return self
|
|
275
|
-
|
|
276
|
-
def __exit__(
|
|
277
|
-
self,
|
|
278
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
279
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
280
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
281
|
-
) -> None:
|
|
282
|
-
self.close()
|
hishel/_synchronization.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import types
|
|
2
|
-
import typing as tp
|
|
3
|
-
from threading import Lock as T_LOCK
|
|
4
|
-
|
|
5
|
-
import anyio
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AsyncLock:
|
|
9
|
-
def __init__(self) -> None:
|
|
10
|
-
self._lock = anyio.Lock()
|
|
11
|
-
|
|
12
|
-
async def __aenter__(self) -> None:
|
|
13
|
-
await self._lock.acquire()
|
|
14
|
-
|
|
15
|
-
async def __aexit__(
|
|
16
|
-
self,
|
|
17
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
18
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
19
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
20
|
-
) -> None:
|
|
21
|
-
self._lock.release()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Lock:
|
|
25
|
-
def __init__(self) -> None:
|
|
26
|
-
self._lock = T_LOCK()
|
|
27
|
-
|
|
28
|
-
def __enter__(self) -> None:
|
|
29
|
-
self._lock.acquire()
|
|
30
|
-
|
|
31
|
-
def __exit__(
|
|
32
|
-
self,
|
|
33
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
34
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
35
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
36
|
-
) -> None:
|
|
37
|
-
self._lock.release()
|
hishel/beta/__init__.py
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from hishel.beta._core._async._storages._sqlite import AsyncSqliteStorage
|
|
2
|
-
from hishel.beta._core._base._storages._base import (
|
|
3
|
-
AsyncBaseStorage as AsyncBaseStorage,
|
|
4
|
-
SyncBaseStorage as SyncBaseStorage,
|
|
5
|
-
)
|
|
6
|
-
from hishel.beta._core._headers import Headers as Headers
|
|
7
|
-
from hishel.beta._core._spec import (
|
|
8
|
-
AnyState as AnyState,
|
|
9
|
-
CacheMiss as CacheMiss,
|
|
10
|
-
CacheOptions as CacheOptions,
|
|
11
|
-
CouldNotBeStored as CouldNotBeStored,
|
|
12
|
-
FromCache as FromCache,
|
|
13
|
-
IdleClient as IdleClient,
|
|
14
|
-
NeedRevalidation as NeedRevalidation,
|
|
15
|
-
NeedToBeUpdated as NeedToBeUpdated,
|
|
16
|
-
State as State,
|
|
17
|
-
StoreAndUse as StoreAndUse,
|
|
18
|
-
create_idle_state as create_idle_state,
|
|
19
|
-
)
|
|
20
|
-
from hishel.beta._core._sync._storages._sqlite import SyncSqliteStorage
|
|
21
|
-
from hishel.beta._core.models import (
|
|
22
|
-
CompletePair as CompletePair,
|
|
23
|
-
IncompletePair as IncompletePair,
|
|
24
|
-
Pair as Pair,
|
|
25
|
-
PairMeta as PairMeta,
|
|
26
|
-
Request as Request,
|
|
27
|
-
Response,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
__all__ = (
|
|
31
|
-
# New API
|
|
32
|
-
## States
|
|
33
|
-
"AnyState",
|
|
34
|
-
"IdleClient",
|
|
35
|
-
"CacheMiss",
|
|
36
|
-
"FromCache",
|
|
37
|
-
"NeedRevalidation",
|
|
38
|
-
"AnyState",
|
|
39
|
-
"CacheOptions",
|
|
40
|
-
"NeedToBeUpdated",
|
|
41
|
-
"State",
|
|
42
|
-
"StoreAndUse",
|
|
43
|
-
"CouldNotBeStored",
|
|
44
|
-
"create_idle_state",
|
|
45
|
-
## Models
|
|
46
|
-
"Request",
|
|
47
|
-
"Response",
|
|
48
|
-
"Pair",
|
|
49
|
-
"IncompletePair",
|
|
50
|
-
"CompletePair",
|
|
51
|
-
"PairMeta",
|
|
52
|
-
## Headers
|
|
53
|
-
"Headers",
|
|
54
|
-
## Storages
|
|
55
|
-
"SyncBaseStorage",
|
|
56
|
-
"AsyncBaseStorage",
|
|
57
|
-
"SyncSqliteStorage",
|
|
58
|
-
"AsyncSqliteStorage",
|
|
59
|
-
)
|
hishel/beta/_async_cache.py
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import logging
|
|
5
|
-
import time
|
|
6
|
-
from dataclasses import replace
|
|
7
|
-
from typing import AsyncIterator, Awaitable, Callable
|
|
8
|
-
|
|
9
|
-
from typing_extensions import assert_never
|
|
10
|
-
|
|
11
|
-
from hishel.beta import (
|
|
12
|
-
AnyState,
|
|
13
|
-
AsyncBaseStorage,
|
|
14
|
-
AsyncSqliteStorage,
|
|
15
|
-
CacheMiss,
|
|
16
|
-
CacheOptions,
|
|
17
|
-
CouldNotBeStored,
|
|
18
|
-
FromCache,
|
|
19
|
-
IdleClient,
|
|
20
|
-
NeedRevalidation,
|
|
21
|
-
NeedToBeUpdated,
|
|
22
|
-
Request,
|
|
23
|
-
Response,
|
|
24
|
-
StoreAndUse,
|
|
25
|
-
create_idle_state,
|
|
26
|
-
)
|
|
27
|
-
from hishel.beta._core._spec import InvalidatePairs, vary_headers_match
|
|
28
|
-
from hishel.beta._core.models import CompletePair
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger("hishel.integrations.clients")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class AsyncCacheProxy:
|
|
34
|
-
"""
|
|
35
|
-
A proxy for HTTP caching in clients.
|
|
36
|
-
|
|
37
|
-
This class is independent of any specific HTTP library and works only with internal models.
|
|
38
|
-
It delegates request execution to a user-provided callable, making it compatible with any
|
|
39
|
-
HTTP client. Caching behavior can be configured to either fully respect HTTP
|
|
40
|
-
caching rules or bypass them entirely.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
send_request: Callable[[Request], Awaitable[Response]],
|
|
46
|
-
storage: AsyncBaseStorage | None = None,
|
|
47
|
-
cache_options: CacheOptions | None = None,
|
|
48
|
-
ignore_specification: bool = False,
|
|
49
|
-
) -> None:
|
|
50
|
-
self.send_request = send_request
|
|
51
|
-
self.storage = storage if storage is not None else AsyncSqliteStorage()
|
|
52
|
-
self.cache_options = cache_options if cache_options is not None else CacheOptions()
|
|
53
|
-
self.ignore_specification = ignore_specification
|
|
54
|
-
|
|
55
|
-
async def handle_request(self, request: Request) -> Response:
|
|
56
|
-
if self.ignore_specification or request.metadata.get("hishel_spec_ignore"):
|
|
57
|
-
return await self._handle_request_ignoring_spec(request)
|
|
58
|
-
return await self._handle_request_respecting_spec(request)
|
|
59
|
-
|
|
60
|
-
async def _get_key_for_request(self, request: Request) -> str:
|
|
61
|
-
if request.metadata.get("hishel_body_key"):
|
|
62
|
-
assert isinstance(request.stream, AsyncIterator)
|
|
63
|
-
collected = b"".join([chunk async for chunk in request.stream])
|
|
64
|
-
hash_ = hashlib.sha256(collected).hexdigest()
|
|
65
|
-
return f"{str(request.url)}-{hash_}"
|
|
66
|
-
return str(request.url)
|
|
67
|
-
|
|
68
|
-
async def _maybe_refresh_pair_ttl(self, pair: CompletePair) -> None:
|
|
69
|
-
if pair.request.metadata.get("hishel_refresh_ttl_on_access"):
|
|
70
|
-
await self.storage.update_pair(
|
|
71
|
-
pair.id,
|
|
72
|
-
lambda complete_pair: replace(complete_pair, meta=replace(complete_pair.meta, created_at=time.time())),
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
async def _handle_request_ignoring_spec(self, request: Request) -> Response:
|
|
76
|
-
logger.debug("Trying to get cached response ignoring specification")
|
|
77
|
-
pairs = await self.storage.get_pairs(await self._get_key_for_request(request))
|
|
78
|
-
|
|
79
|
-
logger.debug(f"Found {len(pairs)} cached pairs for the request")
|
|
80
|
-
|
|
81
|
-
for pair in pairs:
|
|
82
|
-
if (
|
|
83
|
-
str(pair.request.url) == str(request.url)
|
|
84
|
-
and pair.request.method == request.method
|
|
85
|
-
and vary_headers_match(
|
|
86
|
-
request,
|
|
87
|
-
pair,
|
|
88
|
-
)
|
|
89
|
-
):
|
|
90
|
-
logger.debug(
|
|
91
|
-
"Found matching cached response for the request",
|
|
92
|
-
)
|
|
93
|
-
pair.response.metadata["hishel_from_cache"] = True # type: ignore
|
|
94
|
-
await self._maybe_refresh_pair_ttl(pair)
|
|
95
|
-
return pair.response
|
|
96
|
-
|
|
97
|
-
incomplete_pair = await self.storage.create_pair(
|
|
98
|
-
request,
|
|
99
|
-
)
|
|
100
|
-
response = await self.send_request(incomplete_pair.request)
|
|
101
|
-
|
|
102
|
-
logger.debug("Storing response in cache ignoring specification")
|
|
103
|
-
complete_pair = await self.storage.add_response(
|
|
104
|
-
incomplete_pair.id, response, await self._get_key_for_request(request)
|
|
105
|
-
)
|
|
106
|
-
return complete_pair.response
|
|
107
|
-
|
|
108
|
-
async def _handle_request_respecting_spec(self, request: Request) -> Response:
|
|
109
|
-
state: AnyState = create_idle_state("client", self.cache_options)
|
|
110
|
-
|
|
111
|
-
while state:
|
|
112
|
-
logger.debug(f"Handling state: {state.__class__.__name__}")
|
|
113
|
-
if isinstance(state, IdleClient):
|
|
114
|
-
state = await self._handle_idle_state(state, request)
|
|
115
|
-
elif isinstance(state, CacheMiss):
|
|
116
|
-
state = await self._handle_cache_miss(state)
|
|
117
|
-
elif isinstance(state, StoreAndUse):
|
|
118
|
-
return await self._handle_store_and_use(state, request)
|
|
119
|
-
elif isinstance(state, CouldNotBeStored):
|
|
120
|
-
return state.response
|
|
121
|
-
elif isinstance(state, NeedRevalidation):
|
|
122
|
-
state = await self._handle_revalidation(state)
|
|
123
|
-
elif isinstance(state, FromCache):
|
|
124
|
-
await self._maybe_refresh_pair_ttl(state.pair)
|
|
125
|
-
return state.pair.response
|
|
126
|
-
elif isinstance(state, NeedToBeUpdated):
|
|
127
|
-
state = await self._handle_update(state)
|
|
128
|
-
elif isinstance(state, InvalidatePairs):
|
|
129
|
-
state = await self._handle_invalidate_pairs(state)
|
|
130
|
-
else:
|
|
131
|
-
assert_never(state)
|
|
132
|
-
|
|
133
|
-
raise RuntimeError("Unreachable")
|
|
134
|
-
|
|
135
|
-
async def _handle_idle_state(self, state: IdleClient, request: Request) -> AnyState:
|
|
136
|
-
stored_pairs = await self.storage.get_pairs(await self._get_key_for_request(request))
|
|
137
|
-
return state.next(request, stored_pairs)
|
|
138
|
-
|
|
139
|
-
async def _handle_cache_miss(self, state: CacheMiss) -> AnyState:
|
|
140
|
-
incomplete_pair = await self.storage.create_pair(state.request)
|
|
141
|
-
response = await self.send_request(incomplete_pair.request)
|
|
142
|
-
return state.next(response, incomplete_pair.id)
|
|
143
|
-
|
|
144
|
-
async def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response:
|
|
145
|
-
complete_pair = await self.storage.add_response(
|
|
146
|
-
state.pair_id, state.response, await self._get_key_for_request(request)
|
|
147
|
-
)
|
|
148
|
-
return complete_pair.response
|
|
149
|
-
|
|
150
|
-
async def _handle_revalidation(self, state: NeedRevalidation) -> AnyState:
|
|
151
|
-
revalidation_response = await self.send_request(state.request)
|
|
152
|
-
return state.next(revalidation_response)
|
|
153
|
-
|
|
154
|
-
async def _handle_update(self, state: NeedToBeUpdated) -> AnyState:
|
|
155
|
-
for pair in state.updating_pairs:
|
|
156
|
-
await self.storage.update_pair(
|
|
157
|
-
pair.id,
|
|
158
|
-
lambda complete_pair: replace(
|
|
159
|
-
complete_pair, response=replace(pair.response, headers=pair.response.headers)
|
|
160
|
-
),
|
|
161
|
-
)
|
|
162
|
-
return state.next()
|
|
163
|
-
|
|
164
|
-
async def _handle_invalidate_pairs(self, state: InvalidatePairs) -> AnyState:
|
|
165
|
-
for pair_id in state.pair_ids:
|
|
166
|
-
await self.storage.remove(pair_id)
|
|
167
|
-
return state.next()
|
hishel/beta/_core/__init__.py
DELETED
|
File without changes
|