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.
Files changed (29) hide show
  1. {hishel-0.0.29 → hishel-0.0.30}/CHANGELOG.md +15 -6
  2. {hishel-0.0.29 → hishel-0.0.30}/PKG-INFO +16 -7
  3. {hishel-0.0.29 → hishel-0.0.30}/hishel/__init__.py +1 -1
  4. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_pool.py +13 -6
  5. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_storages.py +87 -5
  6. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_transports.py +14 -1
  7. {hishel-0.0.29 → hishel-0.0.30}/hishel/_controller.py +8 -0
  8. {hishel-0.0.29 → hishel-0.0.30}/hishel/_s3.py +7 -0
  9. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_pool.py +13 -6
  10. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_storages.py +87 -5
  11. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_transports.py +14 -1
  12. {hishel-0.0.29 → hishel-0.0.30}/.gitignore +0 -0
  13. {hishel-0.0.29 → hishel-0.0.30}/LICENSE +0 -0
  14. {hishel-0.0.29 → hishel-0.0.30}/README.md +0 -0
  15. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/__init__.py +0 -0
  16. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_client.py +0 -0
  17. {hishel-0.0.29 → hishel-0.0.30}/hishel/_async/_mock.py +0 -0
  18. {hishel-0.0.29 → hishel-0.0.30}/hishel/_exceptions.py +0 -0
  19. {hishel-0.0.29 → hishel-0.0.30}/hishel/_files.py +0 -0
  20. {hishel-0.0.29 → hishel-0.0.30}/hishel/_headers.py +0 -0
  21. {hishel-0.0.29 → hishel-0.0.30}/hishel/_lfu_cache.py +0 -0
  22. {hishel-0.0.29 → hishel-0.0.30}/hishel/_serializers.py +0 -0
  23. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/__init__.py +0 -0
  24. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_client.py +0 -0
  25. {hishel-0.0.29 → hishel-0.0.30}/hishel/_sync/_mock.py +0 -0
  26. {hishel-0.0.29 → hishel-0.0.30}/hishel/_synchronization.py +0 -0
  27. {hishel-0.0.29 → hishel-0.0.30}/hishel/_utils.py +0 -0
  28. {hishel-0.0.29 → hishel-0.0.30}/hishel/py.typed +0 -0
  29. {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.29
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
 
@@ -14,4 +14,4 @@ def install_cache() -> None: # pragma: no cover
14
14
  httpx.Client = CacheClient # type: ignore
15
15
 
16
16
 
17
- __version__ = "0.0.29"
17
+ __version__ = "0.0.30"
@@ -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
- metadata = Metadata(
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
- for file in self._base_path.iterdir():
207
- if file.is_file():
208
- age = time.time() - file.stat().st_mtime
209
- if age > self._ttl:
210
- file.unlink()
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, new_response=httpcore_revalidation_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
- metadata = Metadata(
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
- for file in self._base_path.iterdir():
207
- if file.is_file():
208
- age = time.time() - file.stat().st_mtime
209
- if age > self._ttl:
210
- file.unlink()
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, new_response=httpcore_revalidation_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