hishel 0.0.29__py3-none-any.whl → 0.0.31__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 +1 -1
- hishel/_async/_pool.py +13 -6
- hishel/_async/_storages.py +92 -7
- hishel/_async/_transports.py +14 -1
- hishel/_controller.py +11 -3
- hishel/_s3.py +7 -0
- hishel/_sync/_pool.py +13 -6
- hishel/_sync/_storages.py +92 -7
- hishel/_sync/_transports.py +14 -1
- {hishel-0.0.29.dist-info → hishel-0.0.31.dist-info}/METADATA +22 -7
- {hishel-0.0.29.dist-info → hishel-0.0.31.dist-info}/RECORD +13 -13
- {hishel-0.0.29.dist-info → hishel-0.0.31.dist-info}/WHEEL +0 -0
- {hishel-0.0.29.dist-info → hishel-0.0.31.dist-info}/licenses/LICENSE +0 -0
hishel/__init__.py
CHANGED
hishel/_async/_pool.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import datetime
|
|
4
3
|
import types
|
|
5
4
|
import typing as tp
|
|
6
5
|
|
|
@@ -134,23 +133,31 @@ class AsyncCacheConnectionPool(AsyncRequestInterface):
|
|
|
134
133
|
)
|
|
135
134
|
|
|
136
135
|
await final_response.aread()
|
|
136
|
+
|
|
137
|
+
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
138
|
+
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
139
|
+
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
140
|
+
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
141
|
+
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
142
|
+
if revalidation_response.status != 304 and self._controller.is_cachable(
|
|
143
|
+
request=request, response=final_response
|
|
144
|
+
):
|
|
145
|
+
await self._storage.store(key, response=final_response, request=request)
|
|
146
|
+
|
|
137
147
|
return await self._create_hishel_response(
|
|
138
148
|
key=key,
|
|
139
149
|
response=final_response,
|
|
140
150
|
request=request,
|
|
141
|
-
metadata=metadata,
|
|
142
151
|
cached=revalidation_response.status == 304,
|
|
143
152
|
revalidated=True,
|
|
153
|
+
metadata=metadata,
|
|
144
154
|
)
|
|
145
155
|
|
|
146
156
|
regular_response = await self._pool.handle_async_request(request)
|
|
147
157
|
await regular_response.aread()
|
|
148
158
|
|
|
149
159
|
if self._controller.is_cachable(request=request, response=regular_response):
|
|
150
|
-
|
|
151
|
-
cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
|
|
152
|
-
)
|
|
153
|
-
await self._storage.store(key, response=regular_response, request=request, metadata=metadata)
|
|
160
|
+
await self._storage.store(key, response=regular_response, request=request)
|
|
154
161
|
|
|
155
162
|
return await self._create_hishel_response(
|
|
156
163
|
key=key, response=regular_response, request=request, cached=False, revalidated=False
|
hishel/_async/_storages.py
CHANGED
|
@@ -4,6 +4,7 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
import typing as t
|
|
7
8
|
import typing as tp
|
|
8
9
|
import warnings
|
|
9
10
|
from copy import deepcopy
|
|
@@ -43,6 +44,7 @@ __all__ = (
|
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata]
|
|
47
|
+
RemoveTypes = tp.Union[str, Response]
|
|
46
48
|
|
|
47
49
|
try:
|
|
48
50
|
import redis.asyncio as redis
|
|
@@ -62,6 +64,9 @@ class AsyncBaseStorage:
|
|
|
62
64
|
async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
|
|
63
65
|
raise NotImplementedError()
|
|
64
66
|
|
|
67
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
68
|
+
raise NotImplementedError()
|
|
69
|
+
|
|
65
70
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
66
71
|
raise NotImplementedError()
|
|
67
72
|
|
|
@@ -137,6 +142,23 @@ class AsyncFileStorage(AsyncBaseStorage):
|
|
|
137
142
|
)
|
|
138
143
|
await self._remove_expired_caches(response_path)
|
|
139
144
|
|
|
145
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Removes the response from the cache.
|
|
148
|
+
|
|
149
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
150
|
+
:type key: Union[str, Response]
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if isinstance(key, Response): # pragma: no cover
|
|
154
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
155
|
+
|
|
156
|
+
response_path = self._base_path / key
|
|
157
|
+
|
|
158
|
+
async with self._lock:
|
|
159
|
+
if response_path.exists():
|
|
160
|
+
response_path.unlink()
|
|
161
|
+
|
|
140
162
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
141
163
|
"""
|
|
142
164
|
Updates the metadata of the stored response.
|
|
@@ -203,11 +225,15 @@ class AsyncFileStorage(AsyncBaseStorage):
|
|
|
203
225
|
|
|
204
226
|
self._last_cleaned = time.monotonic()
|
|
205
227
|
async with self._lock:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
with os.scandir(self._base_path) as entries:
|
|
229
|
+
for entry in entries:
|
|
230
|
+
try:
|
|
231
|
+
if entry.is_file():
|
|
232
|
+
age = time.time() - entry.stat().st_mtime
|
|
233
|
+
if age > self._ttl:
|
|
234
|
+
os.unlink(entry.path)
|
|
235
|
+
except FileNotFoundError: # pragma: no cover
|
|
236
|
+
pass
|
|
211
237
|
|
|
212
238
|
|
|
213
239
|
class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
@@ -282,6 +308,24 @@ class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
|
282
308
|
await self._connection.commit()
|
|
283
309
|
await self._remove_expired_caches()
|
|
284
310
|
|
|
311
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Removes the response from the cache.
|
|
314
|
+
|
|
315
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
316
|
+
:type key: Union[str, Response]
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
await self._setup()
|
|
320
|
+
assert self._connection
|
|
321
|
+
|
|
322
|
+
if isinstance(key, Response): # pragma: no cover
|
|
323
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
324
|
+
|
|
325
|
+
async with self._lock:
|
|
326
|
+
await self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
|
|
327
|
+
await self._connection.commit()
|
|
328
|
+
|
|
285
329
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
286
330
|
"""
|
|
287
331
|
Updates the metadata of the stored response.
|
|
@@ -333,8 +377,8 @@ class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
|
333
377
|
return self._serializer.loads(cached_response)
|
|
334
378
|
|
|
335
379
|
async def aclose(self) -> None: # pragma: no cover
|
|
336
|
-
|
|
337
|
-
|
|
380
|
+
if self._connection is not None:
|
|
381
|
+
await self._connection.close()
|
|
338
382
|
|
|
339
383
|
async def _remove_expired_caches(self) -> None:
|
|
340
384
|
assert self._connection
|
|
@@ -404,6 +448,19 @@ class AsyncRedisStorage(AsyncBaseStorage):
|
|
|
404
448
|
key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px
|
|
405
449
|
)
|
|
406
450
|
|
|
451
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Removes the response from the cache.
|
|
454
|
+
|
|
455
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
456
|
+
:type key: Union[str, Response]
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
if isinstance(key, Response): # pragma: no cover
|
|
460
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
461
|
+
|
|
462
|
+
await self._client.delete(key)
|
|
463
|
+
|
|
407
464
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
408
465
|
"""
|
|
409
466
|
Updates the metadata of the stored response.
|
|
@@ -504,6 +561,20 @@ class AsyncInMemoryStorage(AsyncBaseStorage):
|
|
|
504
561
|
self._cache.put(key, (stored_response, time.monotonic()))
|
|
505
562
|
await self._remove_expired_caches()
|
|
506
563
|
|
|
564
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Removes the response from the cache.
|
|
567
|
+
|
|
568
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
569
|
+
:type key: Union[str, Response]
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
if isinstance(key, Response): # pragma: no cover
|
|
573
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
574
|
+
|
|
575
|
+
async with self._lock:
|
|
576
|
+
self._cache.remove_key(key)
|
|
577
|
+
|
|
507
578
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
508
579
|
"""
|
|
509
580
|
Updates the metadata of the stored response.
|
|
@@ -634,6 +705,20 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
634
705
|
|
|
635
706
|
await self._remove_expired_caches(key)
|
|
636
707
|
|
|
708
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
709
|
+
"""
|
|
710
|
+
Removes the response from the cache.
|
|
711
|
+
|
|
712
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
713
|
+
:type key: Union[str, Response]
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
if isinstance(key, Response): # pragma: no cover
|
|
717
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
718
|
+
|
|
719
|
+
async with self._lock:
|
|
720
|
+
await self._s3_manager.remove_entry(key)
|
|
721
|
+
|
|
637
722
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
638
723
|
"""
|
|
639
724
|
Updates the metadata of the stored response.
|
hishel/_async/_transports.py
CHANGED
|
@@ -156,6 +156,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
156
156
|
url=normalized_url(res.url),
|
|
157
157
|
headers=res.headers,
|
|
158
158
|
stream=AsyncCacheStream(res.stream),
|
|
159
|
+
extensions=res.extensions,
|
|
159
160
|
)
|
|
160
161
|
try:
|
|
161
162
|
revalidation_response = await self._transport.handle_async_request(revalidation_request)
|
|
@@ -181,13 +182,25 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
181
182
|
|
|
182
183
|
# Merge headers with the stale response.
|
|
183
184
|
final_httpcore_response = self._controller.handle_validation_response(
|
|
184
|
-
old_response=stored_response,
|
|
185
|
+
old_response=stored_response,
|
|
186
|
+
new_response=httpcore_revalidation_response,
|
|
185
187
|
)
|
|
186
188
|
|
|
187
189
|
await final_httpcore_response.aread()
|
|
188
190
|
await revalidation_response.aclose()
|
|
189
191
|
|
|
190
192
|
assert isinstance(final_httpcore_response.stream, tp.AsyncIterable)
|
|
193
|
+
|
|
194
|
+
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
195
|
+
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
196
|
+
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
197
|
+
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
198
|
+
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
199
|
+
if revalidation_response.status_code != 304 and self._controller.is_cachable(
|
|
200
|
+
request=httpcore_request, response=final_httpcore_response
|
|
201
|
+
):
|
|
202
|
+
await self._storage.store(key, response=final_httpcore_response, request=httpcore_request)
|
|
203
|
+
|
|
191
204
|
return await self._create_hishel_response(
|
|
192
205
|
key=key,
|
|
193
206
|
response=final_httpcore_response,
|
hishel/_controller.py
CHANGED
|
@@ -107,6 +107,7 @@ class Controller:
|
|
|
107
107
|
self,
|
|
108
108
|
cacheable_methods: tp.Optional[tp.List[str]] = None,
|
|
109
109
|
cacheable_status_codes: tp.Optional[tp.List[int]] = None,
|
|
110
|
+
cache_private: bool = True,
|
|
110
111
|
allow_heuristics: bool = False,
|
|
111
112
|
clock: tp.Optional[BaseClock] = None,
|
|
112
113
|
allow_stale: bool = False,
|
|
@@ -128,6 +129,7 @@ class Controller:
|
|
|
128
129
|
self._cacheable_methods.append(method.upper())
|
|
129
130
|
|
|
130
131
|
self._cacheable_status_codes = cacheable_status_codes if cacheable_status_codes else [200, 301, 308]
|
|
132
|
+
self._cache_private = cache_private
|
|
131
133
|
self._clock = clock if clock else Clock()
|
|
132
134
|
self._allow_heuristics = allow_heuristics
|
|
133
135
|
self._allow_stale = allow_stale
|
|
@@ -147,9 +149,6 @@ class Controller:
|
|
|
147
149
|
method = request.method.decode("ascii")
|
|
148
150
|
force_cache = request.extensions.get("force_cache", None)
|
|
149
151
|
|
|
150
|
-
if force_cache if force_cache is not None else self._force_cache:
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
152
|
if response.status not in self._cacheable_status_codes:
|
|
154
153
|
return False
|
|
155
154
|
|
|
@@ -160,6 +159,9 @@ class Controller:
|
|
|
160
159
|
if method not in self._cacheable_methods:
|
|
161
160
|
return False
|
|
162
161
|
|
|
162
|
+
if force_cache if force_cache is not None else self._force_cache:
|
|
163
|
+
return True
|
|
164
|
+
|
|
163
165
|
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control"))
|
|
164
166
|
request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"cache-control"))
|
|
165
167
|
|
|
@@ -176,6 +178,12 @@ class Controller:
|
|
|
176
178
|
if response_cache_control.no_store and not response_cache_control.must_understand:
|
|
177
179
|
return False
|
|
178
180
|
|
|
181
|
+
# a shared cache must not store a response with private directive
|
|
182
|
+
# Note that we do not implement special handling for the qualified form,
|
|
183
|
+
# which would only forbid storing specified headers.
|
|
184
|
+
if not self._cache_private and response_cache_control.private:
|
|
185
|
+
return False
|
|
186
|
+
|
|
179
187
|
expires_presents = header_presents(response.headers, b"expires")
|
|
180
188
|
# the response contains at least one of the following:
|
|
181
189
|
# - a public response directive (see Section 5.2.2.9);
|
hishel/_s3.py
CHANGED
|
@@ -78,6 +78,10 @@ class S3Manager:
|
|
|
78
78
|
if get_timestamp_in_ms() - float(obj["Metadata"]["created_at"]) > ttl:
|
|
79
79
|
self._client.delete_object(Bucket=self._bucket_name, Key=obj["Key"])
|
|
80
80
|
|
|
81
|
+
def remove_entry(self, key: str) -> None:
|
|
82
|
+
path = "hishel-" + key
|
|
83
|
+
self._client.delete_object(Bucket=self._bucket_name, Key=path)
|
|
84
|
+
|
|
81
85
|
|
|
82
86
|
class AsyncS3Manager: # pragma: no cover
|
|
83
87
|
def __init__(
|
|
@@ -93,3 +97,6 @@ class AsyncS3Manager: # pragma: no cover
|
|
|
93
97
|
|
|
94
98
|
async def remove_expired(self, ttl: int, key: str) -> None:
|
|
95
99
|
return await to_thread.run_sync(self._sync_manager.remove_expired, ttl, key)
|
|
100
|
+
|
|
101
|
+
async def remove_entry(self, key: str) -> None:
|
|
102
|
+
return await to_thread.run_sync(self._sync_manager.remove_entry, key)
|
hishel/_sync/_pool.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import datetime
|
|
4
3
|
import types
|
|
5
4
|
import typing as tp
|
|
6
5
|
|
|
@@ -134,23 +133,31 @@ class CacheConnectionPool(RequestInterface):
|
|
|
134
133
|
)
|
|
135
134
|
|
|
136
135
|
final_response.read()
|
|
136
|
+
|
|
137
|
+
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
138
|
+
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
139
|
+
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
140
|
+
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
141
|
+
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
142
|
+
if revalidation_response.status != 304 and self._controller.is_cachable(
|
|
143
|
+
request=request, response=final_response
|
|
144
|
+
):
|
|
145
|
+
self._storage.store(key, response=final_response, request=request)
|
|
146
|
+
|
|
137
147
|
return self._create_hishel_response(
|
|
138
148
|
key=key,
|
|
139
149
|
response=final_response,
|
|
140
150
|
request=request,
|
|
141
|
-
metadata=metadata,
|
|
142
151
|
cached=revalidation_response.status == 304,
|
|
143
152
|
revalidated=True,
|
|
153
|
+
metadata=metadata,
|
|
144
154
|
)
|
|
145
155
|
|
|
146
156
|
regular_response = self._pool.handle_request(request)
|
|
147
157
|
regular_response.read()
|
|
148
158
|
|
|
149
159
|
if self._controller.is_cachable(request=request, response=regular_response):
|
|
150
|
-
|
|
151
|
-
cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
|
|
152
|
-
)
|
|
153
|
-
self._storage.store(key, response=regular_response, request=request, metadata=metadata)
|
|
160
|
+
self._storage.store(key, response=regular_response, request=request)
|
|
154
161
|
|
|
155
162
|
return self._create_hishel_response(
|
|
156
163
|
key=key, response=regular_response, request=request, cached=False, revalidated=False
|
hishel/_sync/_storages.py
CHANGED
|
@@ -4,6 +4,7 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
import typing as t
|
|
7
8
|
import typing as tp
|
|
8
9
|
import warnings
|
|
9
10
|
from copy import deepcopy
|
|
@@ -43,6 +44,7 @@ __all__ = (
|
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata]
|
|
47
|
+
RemoveTypes = tp.Union[str, Response]
|
|
46
48
|
|
|
47
49
|
try:
|
|
48
50
|
import redis
|
|
@@ -62,6 +64,9 @@ class BaseStorage:
|
|
|
62
64
|
def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
|
|
63
65
|
raise NotImplementedError()
|
|
64
66
|
|
|
67
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
68
|
+
raise NotImplementedError()
|
|
69
|
+
|
|
65
70
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
66
71
|
raise NotImplementedError()
|
|
67
72
|
|
|
@@ -137,6 +142,23 @@ class FileStorage(BaseStorage):
|
|
|
137
142
|
)
|
|
138
143
|
self._remove_expired_caches(response_path)
|
|
139
144
|
|
|
145
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Removes the response from the cache.
|
|
148
|
+
|
|
149
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
150
|
+
:type key: Union[str, Response]
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if isinstance(key, Response): # pragma: no cover
|
|
154
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
155
|
+
|
|
156
|
+
response_path = self._base_path / key
|
|
157
|
+
|
|
158
|
+
with self._lock:
|
|
159
|
+
if response_path.exists():
|
|
160
|
+
response_path.unlink()
|
|
161
|
+
|
|
140
162
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
141
163
|
"""
|
|
142
164
|
Updates the metadata of the stored response.
|
|
@@ -203,11 +225,15 @@ class FileStorage(BaseStorage):
|
|
|
203
225
|
|
|
204
226
|
self._last_cleaned = time.monotonic()
|
|
205
227
|
with self._lock:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
with os.scandir(self._base_path) as entries:
|
|
229
|
+
for entry in entries:
|
|
230
|
+
try:
|
|
231
|
+
if entry.is_file():
|
|
232
|
+
age = time.time() - entry.stat().st_mtime
|
|
233
|
+
if age > self._ttl:
|
|
234
|
+
os.unlink(entry.path)
|
|
235
|
+
except FileNotFoundError: # pragma: no cover
|
|
236
|
+
pass
|
|
211
237
|
|
|
212
238
|
|
|
213
239
|
class SQLiteStorage(BaseStorage):
|
|
@@ -282,6 +308,24 @@ class SQLiteStorage(BaseStorage):
|
|
|
282
308
|
self._connection.commit()
|
|
283
309
|
self._remove_expired_caches()
|
|
284
310
|
|
|
311
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Removes the response from the cache.
|
|
314
|
+
|
|
315
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
316
|
+
:type key: Union[str, Response]
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
self._setup()
|
|
320
|
+
assert self._connection
|
|
321
|
+
|
|
322
|
+
if isinstance(key, Response): # pragma: no cover
|
|
323
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
324
|
+
|
|
325
|
+
with self._lock:
|
|
326
|
+
self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
|
|
327
|
+
self._connection.commit()
|
|
328
|
+
|
|
285
329
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
286
330
|
"""
|
|
287
331
|
Updates the metadata of the stored response.
|
|
@@ -333,8 +377,8 @@ class SQLiteStorage(BaseStorage):
|
|
|
333
377
|
return self._serializer.loads(cached_response)
|
|
334
378
|
|
|
335
379
|
def close(self) -> None: # pragma: no cover
|
|
336
|
-
|
|
337
|
-
|
|
380
|
+
if self._connection is not None:
|
|
381
|
+
self._connection.close()
|
|
338
382
|
|
|
339
383
|
def _remove_expired_caches(self) -> None:
|
|
340
384
|
assert self._connection
|
|
@@ -404,6 +448,19 @@ class RedisStorage(BaseStorage):
|
|
|
404
448
|
key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px
|
|
405
449
|
)
|
|
406
450
|
|
|
451
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
452
|
+
"""
|
|
453
|
+
Removes the response from the cache.
|
|
454
|
+
|
|
455
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
456
|
+
:type key: Union[str, Response]
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
if isinstance(key, Response): # pragma: no cover
|
|
460
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
461
|
+
|
|
462
|
+
self._client.delete(key)
|
|
463
|
+
|
|
407
464
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
408
465
|
"""
|
|
409
466
|
Updates the metadata of the stored response.
|
|
@@ -504,6 +561,20 @@ class InMemoryStorage(BaseStorage):
|
|
|
504
561
|
self._cache.put(key, (stored_response, time.monotonic()))
|
|
505
562
|
self._remove_expired_caches()
|
|
506
563
|
|
|
564
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Removes the response from the cache.
|
|
567
|
+
|
|
568
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
569
|
+
:type key: Union[str, Response]
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
if isinstance(key, Response): # pragma: no cover
|
|
573
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
574
|
+
|
|
575
|
+
with self._lock:
|
|
576
|
+
self._cache.remove_key(key)
|
|
577
|
+
|
|
507
578
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
508
579
|
"""
|
|
509
580
|
Updates the metadata of the stored response.
|
|
@@ -634,6 +705,20 @@ class S3Storage(BaseStorage): # pragma: no cover
|
|
|
634
705
|
|
|
635
706
|
self._remove_expired_caches(key)
|
|
636
707
|
|
|
708
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
709
|
+
"""
|
|
710
|
+
Removes the response from the cache.
|
|
711
|
+
|
|
712
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
713
|
+
:type key: Union[str, Response]
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
if isinstance(key, Response): # pragma: no cover
|
|
717
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
718
|
+
|
|
719
|
+
with self._lock:
|
|
720
|
+
self._s3_manager.remove_entry(key)
|
|
721
|
+
|
|
637
722
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
638
723
|
"""
|
|
639
724
|
Updates the metadata of the stored response.
|
hishel/_sync/_transports.py
CHANGED
|
@@ -156,6 +156,7 @@ class CacheTransport(httpx.BaseTransport):
|
|
|
156
156
|
url=normalized_url(res.url),
|
|
157
157
|
headers=res.headers,
|
|
158
158
|
stream=CacheStream(res.stream),
|
|
159
|
+
extensions=res.extensions,
|
|
159
160
|
)
|
|
160
161
|
try:
|
|
161
162
|
revalidation_response = self._transport.handle_request(revalidation_request)
|
|
@@ -181,13 +182,25 @@ class CacheTransport(httpx.BaseTransport):
|
|
|
181
182
|
|
|
182
183
|
# Merge headers with the stale response.
|
|
183
184
|
final_httpcore_response = self._controller.handle_validation_response(
|
|
184
|
-
old_response=stored_response,
|
|
185
|
+
old_response=stored_response,
|
|
186
|
+
new_response=httpcore_revalidation_response,
|
|
185
187
|
)
|
|
186
188
|
|
|
187
189
|
final_httpcore_response.read()
|
|
188
190
|
revalidation_response.close()
|
|
189
191
|
|
|
190
192
|
assert isinstance(final_httpcore_response.stream, tp.Iterable)
|
|
193
|
+
|
|
194
|
+
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
195
|
+
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
196
|
+
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
197
|
+
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
198
|
+
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
199
|
+
if revalidation_response.status_code != 304 and self._controller.is_cachable(
|
|
200
|
+
request=httpcore_request, response=final_httpcore_response
|
|
201
|
+
):
|
|
202
|
+
self._storage.store(key, response=final_httpcore_response, request=httpcore_request)
|
|
203
|
+
|
|
191
204
|
return self._create_hishel_response(
|
|
192
205
|
key=key,
|
|
193
206
|
response=final_httpcore_response,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.31
|
|
4
4
|
Summary: Persistent cache implementation for httpx and httpcore
|
|
5
5
|
Project-URL: Homepage, https://hishel.com
|
|
6
6
|
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
@@ -175,6 +175,21 @@ Help us grow and continue developing good software for you ❤️
|
|
|
175
175
|
|
|
176
176
|
# Changelog
|
|
177
177
|
|
|
178
|
+
## 0.0.31 (22nd Sep, 2024)
|
|
179
|
+
|
|
180
|
+
- Ignore file not found error when cleaning up a file storage. (#264)
|
|
181
|
+
- Fix `AssertionError` on `client.close()` when use SQLiteStorage. (#269)
|
|
182
|
+
- Fix ignored flags when use `force_cache`. (#271)
|
|
183
|
+
|
|
184
|
+
## 0.0.30 (12th July, 2024)
|
|
185
|
+
|
|
186
|
+
- Fix cache update on revalidation response with content (rfc9111 section 4.3.3) (#239)
|
|
187
|
+
- Fix request extensions that were not passed into revalidation request for transport-based implementation (but were
|
|
188
|
+
passed for the pool-based impl) (#247).
|
|
189
|
+
- Add `cache_private` property to the controller to support acting as shared cache. (#224)
|
|
190
|
+
- Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls. (#252)
|
|
191
|
+
- Add `remove` support for storages (#241)
|
|
192
|
+
|
|
178
193
|
## 0.0.29 (23th June, 2024)
|
|
179
194
|
|
|
180
195
|
- Documentation hotfix. (#244)
|
|
@@ -236,11 +251,11 @@ Help us grow and continue developing good software for you ❤️
|
|
|
236
251
|
- Add `cache_disabled` extension to temporarily disable the cache (#109)
|
|
237
252
|
- Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111)
|
|
238
253
|
|
|
239
|
-
## 0.0.17 (6th November, 2023)
|
|
254
|
+
## 0.0.17 (6th November, 2023)
|
|
240
255
|
|
|
241
256
|
- Fix `Last-Modified` validation.
|
|
242
257
|
|
|
243
|
-
## 0.0.16 (25th October, 2023)
|
|
258
|
+
## 0.0.16 (25th October, 2023)
|
|
244
259
|
|
|
245
260
|
- Add `install_cache` function. (#95)
|
|
246
261
|
- Add sqlite support. (#92)
|
|
@@ -260,19 +275,19 @@ Help us grow and continue developing good software for you ❤️
|
|
|
260
275
|
|
|
261
276
|
- Add metadata into the response extensions. (#56)
|
|
262
277
|
|
|
263
|
-
## 0.0.11 (15th August, 2023)
|
|
278
|
+
## 0.0.11 (15th August, 2023)
|
|
264
279
|
|
|
265
280
|
- Add support for request `cache-control` directives. (#42)
|
|
266
281
|
- Drop httpcore dependency. (#40)
|
|
267
282
|
- Support HTTP methods only if they are defined as cacheable. (#37)
|
|
268
283
|
|
|
269
|
-
## 0.0.10 (7th August, 2023)
|
|
284
|
+
## 0.0.10 (7th August, 2023)
|
|
270
285
|
|
|
271
286
|
- Add Response metadata. (#33)
|
|
272
287
|
- Add API Reference documentation. (#30)
|
|
273
288
|
- Use stale responses only if the client is disconnected. (#28)
|
|
274
289
|
|
|
275
|
-
## 0.0.9 (1st August, 2023)
|
|
290
|
+
## 0.0.9 (1st August, 2023)
|
|
276
291
|
|
|
277
292
|
- Expose Controller API. (#23)
|
|
278
293
|
|
|
@@ -291,7 +306,7 @@ Help us grow and continue developing good software for you ❤️
|
|
|
291
306
|
## 0.0.6 (29th July, 2023)
|
|
292
307
|
|
|
293
308
|
- Fix `Vary` header validation. (#8)
|
|
294
|
-
- Dump original requests with the responses. (#7)
|
|
309
|
+
- Dump original requests with the responses. (#7)
|
|
295
310
|
|
|
296
311
|
## 0.0.5 (29th July, 2023)
|
|
297
312
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
hishel/__init__.py,sha256=
|
|
2
|
-
hishel/_controller.py,sha256=
|
|
1
|
+
hishel/__init__.py,sha256=XNbMtsi4tkqZZ6AgwVBDu6BumB6WkK3alKCqk-JVDWs,369
|
|
2
|
+
hishel/_controller.py,sha256=kfWAAtgP4RCYNPH33XeDJRJ-zajqS_w0ZbM0M0CsKiY,15875
|
|
3
3
|
hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
|
|
4
4
|
hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
|
|
5
5
|
hishel/_headers.py,sha256=TWuHi7sRoeS2xxdNGujKmqWtgncUqfhNGCgHKYpRU-I,7329
|
|
6
6
|
hishel/_lfu_cache.py,sha256=GBxToQI8u_a9TzYnLlZMLhgZ8Lb83boPHzTvIgqV6pA,2707
|
|
7
|
-
hishel/_s3.py,sha256=
|
|
7
|
+
hishel/_s3.py,sha256=JqRlygITK5uAryviC15HZKQlKY7etUOPWcazTJeYKBI,3736
|
|
8
8
|
hishel/_serializers.py,sha256=gepVb8JC4aBkGw9kLcbAsyo-1XgK_lzTssLr_8av4SQ,11640
|
|
9
9
|
hishel/_synchronization.py,sha256=xOmU9_8KAWTAv3r8EpqPISrtSF3slyh1J0Sc7ZQO1rg,897
|
|
10
10
|
hishel/_utils.py,sha256=cgLGjBI7H-T_DkYBXiHiEEf8SGjGeZ6Cc2IHbLAZybU,2501
|
|
@@ -12,16 +12,16 @@ hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
12
12
|
hishel/_async/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
|
|
13
13
|
hishel/_async/_client.py,sha256=AkVSSbNTTHmK0gX6PRYVQ-3aDbuCX2Im4VKbLkwLiBU,1101
|
|
14
14
|
hishel/_async/_mock.py,sha256=995v9p5xiw3svGSOJATkLMqwodlhZhcwmGygLHM2VFw,1515
|
|
15
|
-
hishel/_async/_pool.py,sha256=
|
|
16
|
-
hishel/_async/_storages.py,sha256=
|
|
17
|
-
hishel/_async/_transports.py,sha256=
|
|
15
|
+
hishel/_async/_pool.py,sha256=li-921qyGzrV7SVUOUlMI0KE7IRsupSkE5iApzxmgqk,8175
|
|
16
|
+
hishel/_async/_storages.py,sha256=SPWifKGSGAFP2wMFoxZf5weZFbuVgHFciyglY6Hb6fc,28699
|
|
17
|
+
hishel/_async/_transports.py,sha256=moeH-eQLJHkMp83NnScgsQTSQntDCR1_4A1ByZ_fXjk,11174
|
|
18
18
|
hishel/_sync/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
|
|
19
19
|
hishel/_sync/_client.py,sha256=O-gwm9DsveKtSFUfqdbBB-3I1FmXr5rE-uQ7X5frwDA,1060
|
|
20
20
|
hishel/_sync/_mock.py,sha256=im88tZr-XhP9BpzvIt3uOjndAlNcJvFP7Puv3H-6lKU,1430
|
|
21
|
-
hishel/_sync/_pool.py,sha256=
|
|
22
|
-
hishel/_sync/_storages.py,sha256=
|
|
23
|
-
hishel/_sync/_transports.py,sha256=
|
|
24
|
-
hishel-0.0.
|
|
25
|
-
hishel-0.0.
|
|
26
|
-
hishel-0.0.
|
|
27
|
-
hishel-0.0.
|
|
21
|
+
hishel/_sync/_pool.py,sha256=VcAknzyAL2i4-zcyE2fOTmTjfBZ2wkBVNYTvSw0OjVQ,7940
|
|
22
|
+
hishel/_sync/_storages.py,sha256=RYzYXqnv0o2JO3RoEmlEUp0yOg_ungXfz4dLN7UTpIQ,27909
|
|
23
|
+
hishel/_sync/_transports.py,sha256=G3_8SdPwlnrHZRvE1gqFLE4oZadVqNgg5mvxghDMih0,10838
|
|
24
|
+
hishel-0.0.31.dist-info/METADATA,sha256=-hIw1fiLfsgB3RmeHqw4odYGvdE4iezkjqZijkD6JvM,11478
|
|
25
|
+
hishel-0.0.31.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
26
|
+
hishel-0.0.31.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
27
|
+
hishel-0.0.31.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|