hishel 0.0.29__tar.gz → 0.0.30__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.
- {hishel-0.0.29 → hishel-0.0.30}/CHANGELOG.md +15 -6
- {hishel-0.0.29 → hishel-0.0.30}/PKG-INFO +16 -7
- {hishel-0.0.29 → hishel-0.0.30}/hishel/__init__.py +1 -1
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_pool.py +13 -6
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_storages.py +87 -5
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_transports.py +14 -1
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_controller.py +8 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_s3.py +7 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_pool.py +13 -6
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_storages.py +87 -5
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_transports.py +14 -1
- {hishel-0.0.29 → hishel-0.0.30}/.gitignore +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/LICENSE +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/README.md +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/__init__.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_client.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_mock.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_exceptions.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_files.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_headers.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_lfu_cache.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_serializers.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/__init__.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_client.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_mock.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_synchronization.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/_utils.py +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/hishel/py.typed +0 -0
- {hishel-0.0.29 → hishel-0.0.30}/pyproject.toml +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.30 (12th July, 2024)
|
|
4
|
+
|
|
5
|
+
- Fix cache update on revalidation response with content (rfc9111 section 4.3.3) (#239)
|
|
6
|
+
- Fix request extensions that were not passed into revalidation request for transport-based implementation (but were
|
|
7
|
+
passed for the pool-based impl) (#247).
|
|
8
|
+
- Add `cache_private` property to the controller to support acting as shared cache. (#224)
|
|
9
|
+
- Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls. (#252)
|
|
10
|
+
- Add `remove` support for storages (#241)
|
|
11
|
+
|
|
3
12
|
## 0.0.29 (23th June, 2024)
|
|
4
13
|
|
|
5
14
|
- Documentation hotfix. (#244)
|
|
@@ -61,11 +70,11 @@
|
|
|
61
70
|
- Add `cache_disabled` extension to temporarily disable the cache (#109)
|
|
62
71
|
- Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111)
|
|
63
72
|
|
|
64
|
-
## 0.0.17 (6th November, 2023)
|
|
73
|
+
## 0.0.17 (6th November, 2023)
|
|
65
74
|
|
|
66
75
|
- Fix `Last-Modified` validation.
|
|
67
76
|
|
|
68
|
-
## 0.0.16 (25th October, 2023)
|
|
77
|
+
## 0.0.16 (25th October, 2023)
|
|
69
78
|
|
|
70
79
|
- Add `install_cache` function. (#95)
|
|
71
80
|
- Add sqlite support. (#92)
|
|
@@ -85,19 +94,19 @@
|
|
|
85
94
|
|
|
86
95
|
- Add metadata into the response extensions. (#56)
|
|
87
96
|
|
|
88
|
-
## 0.0.11 (15th August, 2023)
|
|
97
|
+
## 0.0.11 (15th August, 2023)
|
|
89
98
|
|
|
90
99
|
- Add support for request `cache-control` directives. (#42)
|
|
91
100
|
- Drop httpcore dependency. (#40)
|
|
92
101
|
- Support HTTP methods only if they are defined as cacheable. (#37)
|
|
93
102
|
|
|
94
|
-
## 0.0.10 (7th August, 2023)
|
|
103
|
+
## 0.0.10 (7th August, 2023)
|
|
95
104
|
|
|
96
105
|
- Add Response metadata. (#33)
|
|
97
106
|
- Add API Reference documentation. (#30)
|
|
98
107
|
- Use stale responses only if the client is disconnected. (#28)
|
|
99
108
|
|
|
100
|
-
## 0.0.9 (1st August, 2023)
|
|
109
|
+
## 0.0.9 (1st August, 2023)
|
|
101
110
|
|
|
102
111
|
- Expose Controller API. (#23)
|
|
103
112
|
|
|
@@ -116,7 +125,7 @@
|
|
|
116
125
|
## 0.0.6 (29th July, 2023)
|
|
117
126
|
|
|
118
127
|
- Fix `Vary` header validation. (#8)
|
|
119
|
-
- Dump original requests with the responses. (#7)
|
|
128
|
+
- Dump original requests with the responses. (#7)
|
|
120
129
|
|
|
121
130
|
## 0.0.5 (29th July, 2023)
|
|
122
131
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
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,15 @@ Help us grow and continue developing good software for you ❤️
|
|
|
175
175
|
|
|
176
176
|
# Changelog
|
|
177
177
|
|
|
178
|
+
## 0.0.30 (12th July, 2024)
|
|
179
|
+
|
|
180
|
+
- Fix cache update on revalidation response with content (rfc9111 section 4.3.3) (#239)
|
|
181
|
+
- Fix request extensions that were not passed into revalidation request for transport-based implementation (but were
|
|
182
|
+
passed for the pool-based impl) (#247).
|
|
183
|
+
- Add `cache_private` property to the controller to support acting as shared cache. (#224)
|
|
184
|
+
- Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls. (#252)
|
|
185
|
+
- Add `remove` support for storages (#241)
|
|
186
|
+
|
|
178
187
|
## 0.0.29 (23th June, 2024)
|
|
179
188
|
|
|
180
189
|
- Documentation hotfix. (#244)
|
|
@@ -236,11 +245,11 @@ Help us grow and continue developing good software for you ❤️
|
|
|
236
245
|
- Add `cache_disabled` extension to temporarily disable the cache (#109)
|
|
237
246
|
- Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111)
|
|
238
247
|
|
|
239
|
-
## 0.0.17 (6th November, 2023)
|
|
248
|
+
## 0.0.17 (6th November, 2023)
|
|
240
249
|
|
|
241
250
|
- Fix `Last-Modified` validation.
|
|
242
251
|
|
|
243
|
-
## 0.0.16 (25th October, 2023)
|
|
252
|
+
## 0.0.16 (25th October, 2023)
|
|
244
253
|
|
|
245
254
|
- Add `install_cache` function. (#95)
|
|
246
255
|
- Add sqlite support. (#92)
|
|
@@ -260,19 +269,19 @@ Help us grow and continue developing good software for you ❤️
|
|
|
260
269
|
|
|
261
270
|
- Add metadata into the response extensions. (#56)
|
|
262
271
|
|
|
263
|
-
## 0.0.11 (15th August, 2023)
|
|
272
|
+
## 0.0.11 (15th August, 2023)
|
|
264
273
|
|
|
265
274
|
- Add support for request `cache-control` directives. (#42)
|
|
266
275
|
- Drop httpcore dependency. (#40)
|
|
267
276
|
- Support HTTP methods only if they are defined as cacheable. (#37)
|
|
268
277
|
|
|
269
|
-
## 0.0.10 (7th August, 2023)
|
|
278
|
+
## 0.0.10 (7th August, 2023)
|
|
270
279
|
|
|
271
280
|
- Add Response metadata. (#33)
|
|
272
281
|
- Add API Reference documentation. (#30)
|
|
273
282
|
- Use stale responses only if the client is disconnected. (#28)
|
|
274
283
|
|
|
275
|
-
## 0.0.9 (1st August, 2023)
|
|
284
|
+
## 0.0.9 (1st August, 2023)
|
|
276
285
|
|
|
277
286
|
- Expose Controller API. (#23)
|
|
278
287
|
|
|
@@ -291,7 +300,7 @@ Help us grow and continue developing good software for you ❤️
|
|
|
291
300
|
## 0.0.6 (29th July, 2023)
|
|
292
301
|
|
|
293
302
|
- Fix `Vary` header validation. (#8)
|
|
294
|
-
- Dump original requests with the responses. (#7)
|
|
303
|
+
- Dump original requests with the responses. (#7)
|
|
295
304
|
|
|
296
305
|
## 0.0.5 (29th July, 2023)
|
|
297
306
|
|
|
@@ -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
|
|
@@ -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,12 @@ 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
|
+
if entry.is_file():
|
|
231
|
+
age = time.time() - entry.stat().st_mtime
|
|
232
|
+
if age > self._ttl:
|
|
233
|
+
os.unlink(entry.path)
|
|
211
234
|
|
|
212
235
|
|
|
213
236
|
class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
@@ -282,6 +305,24 @@ class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
|
282
305
|
await self._connection.commit()
|
|
283
306
|
await self._remove_expired_caches()
|
|
284
307
|
|
|
308
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Removes the response from the cache.
|
|
311
|
+
|
|
312
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
313
|
+
:type key: Union[str, Response]
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
await self._setup()
|
|
317
|
+
assert self._connection
|
|
318
|
+
|
|
319
|
+
if isinstance(key, Response): # pragma: no cover
|
|
320
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
321
|
+
|
|
322
|
+
async with self._lock:
|
|
323
|
+
await self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
|
|
324
|
+
await self._connection.commit()
|
|
325
|
+
|
|
285
326
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
286
327
|
"""
|
|
287
328
|
Updates the metadata of the stored response.
|
|
@@ -404,6 +445,19 @@ class AsyncRedisStorage(AsyncBaseStorage):
|
|
|
404
445
|
key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px
|
|
405
446
|
)
|
|
406
447
|
|
|
448
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
449
|
+
"""
|
|
450
|
+
Removes the response from the cache.
|
|
451
|
+
|
|
452
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
453
|
+
:type key: Union[str, Response]
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
if isinstance(key, Response): # pragma: no cover
|
|
457
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
458
|
+
|
|
459
|
+
await self._client.delete(key)
|
|
460
|
+
|
|
407
461
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
408
462
|
"""
|
|
409
463
|
Updates the metadata of the stored response.
|
|
@@ -504,6 +558,20 @@ class AsyncInMemoryStorage(AsyncBaseStorage):
|
|
|
504
558
|
self._cache.put(key, (stored_response, time.monotonic()))
|
|
505
559
|
await self._remove_expired_caches()
|
|
506
560
|
|
|
561
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
562
|
+
"""
|
|
563
|
+
Removes the response from the cache.
|
|
564
|
+
|
|
565
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
566
|
+
:type key: Union[str, Response]
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
if isinstance(key, Response): # pragma: no cover
|
|
570
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
571
|
+
|
|
572
|
+
async with self._lock:
|
|
573
|
+
self._cache.remove_key(key)
|
|
574
|
+
|
|
507
575
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
508
576
|
"""
|
|
509
577
|
Updates the metadata of the stored response.
|
|
@@ -634,6 +702,20 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
634
702
|
|
|
635
703
|
await self._remove_expired_caches(key)
|
|
636
704
|
|
|
705
|
+
async def remove(self, key: RemoveTypes) -> None:
|
|
706
|
+
"""
|
|
707
|
+
Removes the response from the cache.
|
|
708
|
+
|
|
709
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
710
|
+
:type key: Union[str, Response]
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
if isinstance(key, Response): # pragma: no cover
|
|
714
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
715
|
+
|
|
716
|
+
async with self._lock:
|
|
717
|
+
await self._s3_manager.remove_entry(key)
|
|
718
|
+
|
|
637
719
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
638
720
|
"""
|
|
639
721
|
Updates the metadata of the stored response.
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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,12 @@ 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
|
+
if entry.is_file():
|
|
231
|
+
age = time.time() - entry.stat().st_mtime
|
|
232
|
+
if age > self._ttl:
|
|
233
|
+
os.unlink(entry.path)
|
|
211
234
|
|
|
212
235
|
|
|
213
236
|
class SQLiteStorage(BaseStorage):
|
|
@@ -282,6 +305,24 @@ class SQLiteStorage(BaseStorage):
|
|
|
282
305
|
self._connection.commit()
|
|
283
306
|
self._remove_expired_caches()
|
|
284
307
|
|
|
308
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Removes the response from the cache.
|
|
311
|
+
|
|
312
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
313
|
+
:type key: Union[str, Response]
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
self._setup()
|
|
317
|
+
assert self._connection
|
|
318
|
+
|
|
319
|
+
if isinstance(key, Response): # pragma: no cover
|
|
320
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
321
|
+
|
|
322
|
+
with self._lock:
|
|
323
|
+
self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
|
|
324
|
+
self._connection.commit()
|
|
325
|
+
|
|
285
326
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
286
327
|
"""
|
|
287
328
|
Updates the metadata of the stored response.
|
|
@@ -404,6 +445,19 @@ class RedisStorage(BaseStorage):
|
|
|
404
445
|
key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px
|
|
405
446
|
)
|
|
406
447
|
|
|
448
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
449
|
+
"""
|
|
450
|
+
Removes the response from the cache.
|
|
451
|
+
|
|
452
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
453
|
+
:type key: Union[str, Response]
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
if isinstance(key, Response): # pragma: no cover
|
|
457
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
458
|
+
|
|
459
|
+
self._client.delete(key)
|
|
460
|
+
|
|
407
461
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
408
462
|
"""
|
|
409
463
|
Updates the metadata of the stored response.
|
|
@@ -504,6 +558,20 @@ class InMemoryStorage(BaseStorage):
|
|
|
504
558
|
self._cache.put(key, (stored_response, time.monotonic()))
|
|
505
559
|
self._remove_expired_caches()
|
|
506
560
|
|
|
561
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
562
|
+
"""
|
|
563
|
+
Removes the response from the cache.
|
|
564
|
+
|
|
565
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
566
|
+
:type key: Union[str, Response]
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
if isinstance(key, Response): # pragma: no cover
|
|
570
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
571
|
+
|
|
572
|
+
with self._lock:
|
|
573
|
+
self._cache.remove_key(key)
|
|
574
|
+
|
|
507
575
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
508
576
|
"""
|
|
509
577
|
Updates the metadata of the stored response.
|
|
@@ -634,6 +702,20 @@ class S3Storage(BaseStorage): # pragma: no cover
|
|
|
634
702
|
|
|
635
703
|
self._remove_expired_caches(key)
|
|
636
704
|
|
|
705
|
+
def remove(self, key: RemoveTypes) -> None:
|
|
706
|
+
"""
|
|
707
|
+
Removes the response from the cache.
|
|
708
|
+
|
|
709
|
+
:param key: Hashed value of concatenated HTTP method and URI or an HTTP response
|
|
710
|
+
:type key: Union[str, Response]
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
if isinstance(key, Response): # pragma: no cover
|
|
714
|
+
key = t.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
715
|
+
|
|
716
|
+
with self._lock:
|
|
717
|
+
self._s3_manager.remove_entry(key)
|
|
718
|
+
|
|
637
719
|
def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
638
720
|
"""
|
|
639
721
|
Updates the metadata of the stored response.
|
|
@@ -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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|