hishel 1.0.0.dev3__tar.gz → 1.1.0__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 (26) hide show
  1. {hishel-1.0.0.dev3 → hishel-1.1.0}/CHANGELOG.md +29 -0
  2. {hishel-1.0.0.dev3 → hishel-1.1.0}/PKG-INFO +129 -16
  3. {hishel-1.0.0.dev3 → hishel-1.1.0}/README.md +99 -15
  4. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/__init__.py +11 -3
  5. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_async_cache.py +70 -37
  6. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_async_httpx.py +8 -15
  7. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_spec.py +17 -40
  8. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_storages/_async_sqlite.py +5 -2
  9. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_storages/_sync_sqlite.py +5 -2
  10. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/models.py +87 -11
  11. hishel-1.1.0/hishel/_policies.py +49 -0
  12. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_sync_cache.py +70 -37
  13. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_sync_httpx.py +8 -15
  14. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_utils.py +2 -2
  15. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/asgi.py +16 -16
  16. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/requests.py +3 -5
  17. {hishel-1.0.0.dev3 → hishel-1.1.0}/pyproject.toml +3 -1
  18. {hishel-1.0.0.dev3 → hishel-1.1.0}/.gitignore +0 -0
  19. {hishel-1.0.0.dev3 → hishel-1.1.0}/LICENSE +0 -0
  20. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_headers.py +0 -0
  21. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_storages/_async_base.py +0 -0
  22. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_storages/_packing.py +0 -0
  23. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/_core/_storages/_sync_base.py +0 -0
  24. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/fastapi.py +0 -0
  25. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/httpx.py +0 -0
  26. {hishel-1.0.0.dev3 → hishel-1.1.0}/hishel/py.typed +0 -0
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 1.1.0 - 2025-10-31
6
+ ### ⚙️ Miscellaneous Tasks
7
+ - Add in memory example
8
+
9
+ ### 🐛 Bug Fixes
10
+ - Pass any response with non-expected status code on revalidation to client
11
+
12
+ ### 🚀 Features
13
+ - Allow setting storage base with via `database_path` for sqlite storage
14
+
15
+ ## 1.0.0 - 2025-10-28
16
+ ### ⚙️ Miscellaneous Tasks
17
+ - Add examples, improve docs
18
+
19
+ ## 1.0.0b1 - 2025-10-28
20
+ ### ♻️ Refactoring
21
+ - Add policies
22
+
23
+ ### ⚙️ Miscellaneous Tasks
24
+ - Improve sans-io diagram colors
25
+ - Add graphql docs
26
+
27
+ ### 🐛 Bug Fixes
28
+ - Body-sensitive responses caching
29
+ - Filter out `Transfer-Encoding` header for asgi responses
30
+
31
+ ### 🚀 Features
32
+ - Add global `use_body_key` setting
33
+
5
34
  ## 1.0.0.dev3 - 2025-10-26
6
35
  ### ♻️ Refactoring
7
36
  - Replace pairs with entries, simplify storage API
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hishel
3
- Version: 1.0.0.dev3
3
+ Version: 1.1.0
4
4
  Summary: Elegant HTTP Caching for Python
5
5
  Project-URL: Homepage, https://hishel.com
6
6
  Project-URL: Source, https://github.com/karpetrosyan/hishel
@@ -82,9 +82,10 @@ Description-Content-Type: text/markdown
82
82
  - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
83
83
  - 🎨 **Type Safe** - Fully typed with comprehensive type hints
84
84
  - 🧪 **Well Tested** - Extensive test coverage and battle-tested
85
- - 🎛️ **Configurable** - Fine-grained control over caching behavior
86
- - **Memory Efficient** - Streaming support prevents loading large payloads into memory
85
+ - 🎛️ **Configurable** - Fine-grained control over caching behavior with flexible policies
86
+ - 💨 **Memory Efficient** - Streaming support prevents loading large payloads into memory
87
87
  - 🌐 **Universal** - Works with any ASGI application (Starlette, Litestar, BlackSheep, etc.)
88
+ - 🎯 **GraphQL Support** - Cache GraphQL queries with body-sensitive content caching
88
89
 
89
90
  ## 📦 Installation
90
91
 
@@ -175,12 +176,14 @@ from hishel.asgi import ASGICacheMiddleware
175
176
  app = ASGICacheMiddleware(app)
176
177
 
177
178
  # Or configure with options
178
- from hishel import AsyncSqliteStorage, CacheOptions
179
+ from hishel import AsyncSqliteStorage, CacheOptions, SpecificationPolicy
179
180
 
180
181
  app = ASGICacheMiddleware(
181
182
  app,
182
183
  storage=AsyncSqliteStorage(),
183
- cache_options=CacheOptions(shared=True)
184
+ policy=SpecificationPolicy(
185
+ cache_options=CacheOptions(shared=True)
186
+ ),
184
187
  )
185
188
  ```
186
189
 
@@ -225,21 +228,49 @@ async def get_data():
225
228
 
226
229
  ## 🎛️ Advanced Configuration
227
230
 
228
- ### Custom Cache Options
231
+ ### Caching Policies
232
+
233
+ Hishel supports two types of caching policies:
234
+
235
+ **SpecificationPolicy** - RFC 9111 compliant HTTP caching (default):
229
236
 
230
237
  ```python
231
- from hishel import CacheOptions
238
+ from hishel import CacheOptions, SpecificationPolicy
232
239
  from hishel.httpx import SyncCacheClient
233
240
 
234
241
  client = SyncCacheClient(
235
- cache_options=CacheOptions(
236
- shared=False, # Use as private cache (browser-like)
237
- supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
238
- allow_stale=True # Allow serving stale responses
242
+ policy=SpecificationPolicy(
243
+ cache_options=CacheOptions(
244
+ shared=False, # Use as private cache (browser-like)
245
+ supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
246
+ allow_stale=True # Allow serving stale responses
247
+ )
239
248
  )
240
249
  )
241
250
  ```
242
251
 
252
+ **FilterPolicy** - Custom filtering logic for fine-grained control:
253
+
254
+ ```python
255
+ from hishel import FilterPolicy, BaseFilter, Request
256
+ from hishel.httpx import AsyncCacheClient
257
+
258
+ class CacheOnlyAPIRequests(BaseFilter[Request]):
259
+ def needs_body(self) -> bool:
260
+ return False
261
+
262
+ def apply(self, item: Request, body: bytes | None) -> bool:
263
+ return "/api/" in str(item.url)
264
+
265
+ client = AsyncCacheClient(
266
+ policy=FilterPolicy(
267
+ request_filters=[CacheOnlyAPIRequests()]
268
+ )
269
+ )
270
+ ```
271
+
272
+ [Learn more about policies →](https://hishel.com/dev/policies/)
273
+
243
274
  ### Custom Storage Backend
244
275
 
245
276
  ```python
@@ -255,6 +286,60 @@ storage = SyncSqliteStorage(
255
286
  client = SyncCacheClient(storage=storage)
256
287
  ```
257
288
 
289
+ ### GraphQL and Body-Sensitive Caching
290
+
291
+ Cache GraphQL queries and other POST requests by including the request body in the cache key.
292
+
293
+ **Using per-request header:**
294
+
295
+ ```python
296
+ from hishel import FilterPolicy
297
+ from hishel.httpx import SyncCacheClient
298
+
299
+ client = SyncCacheClient(
300
+ policy=FilterPolicy()
301
+ )
302
+
303
+ # Cache GraphQL queries - different queries get different cache entries
304
+ graphql_query = """
305
+ query GetUser($id: ID!) {
306
+ user(id: $id) {
307
+ name
308
+ email
309
+ }
310
+ }
311
+ """
312
+
313
+ response = client.post(
314
+ "https://api.example.com/graphql",
315
+ json={"query": graphql_query, "variables": {"id": "123"}},
316
+ headers={"X-Hishel-Body-Key": "true"} # Enable body-based caching
317
+ )
318
+
319
+ # Different query will be cached separately
320
+ response = client.post(
321
+ "https://api.example.com/graphql",
322
+ json={"query": graphql_query, "variables": {"id": "456"}},
323
+ headers={"X-Hishel-Body-Key": "true"}
324
+ )
325
+ ```
326
+
327
+ **Using global configuration:**
328
+
329
+ ```python
330
+ from hishel.httpx import SyncCacheClient
331
+ from hishel import FilterPolicy
332
+
333
+ # Enable body-based caching for all requests
334
+ client = SyncCacheClient(policy=FilterPolicy(use_body_key=True))
335
+
336
+ # All POST requests automatically include body in cache key
337
+ response = client.post(
338
+ "https://api.example.com/graphql",
339
+ json={"query": graphql_query, "variables": {"id": "123"}}
340
+ )
341
+ ```
342
+
258
343
  ## 🏗️ Architecture
259
344
 
260
345
  Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
@@ -266,13 +351,11 @@ Hishel uses a **sans-I/O state machine** architecture that separates HTTP cachin
266
351
 
267
352
  ## 🔮 Roadmap
268
353
 
269
- While Hishel currently supports HTTPX and Requests, we're actively working on:
354
+ We're actively working on:
270
355
 
271
- - 🎯 Additional HTTP client integrations
272
- - 🎯 Server-side caching support
273
- - 🎯 More storage backends
274
- - 🎯 Advanced caching strategies
275
356
  - 🎯 Performance optimizations
357
+ - 🎯 More integrations
358
+ - 🎯 Partial responses support
276
359
 
277
360
  ## 📚 Documentation
278
361
 
@@ -284,6 +367,7 @@ Comprehensive documentation is available at [https://hishel.com/dev](https://his
284
367
  - [ASGI Integration](https://hishel.com/dev/asgi)
285
368
  - [FastAPI Integration](https://hishel.com/dev/fastapi)
286
369
  - [BlackSheep Integration](https://hishel.com/dev/integrations/blacksheep)
370
+ - [GraphQL Integration](https://hishel.com/dev/integrations/graphql)
287
371
  - [Storage Backends](https://hishel.com/dev/storages)
288
372
  - [Request/Response Metadata](https://hishel.com/dev/metadata)
289
373
  - [RFC 9111 Specification](https://hishel.com/dev/specification)
@@ -326,6 +410,35 @@ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecos
326
410
 
327
411
  All notable changes to this project will be documented in this file.
328
412
 
413
+ ## 1.1.0 - 2025-10-31
414
+ ### ⚙️ Miscellaneous Tasks
415
+ - Add in memory example
416
+
417
+ ### 🐛 Bug Fixes
418
+ - Pass any response with non-expected status code on revalidation to client
419
+
420
+ ### 🚀 Features
421
+ - Allow setting storage base with via `database_path` for sqlite storage
422
+
423
+ ## 1.0.0 - 2025-10-28
424
+ ### ⚙️ Miscellaneous Tasks
425
+ - Add examples, improve docs
426
+
427
+ ## 1.0.0b1 - 2025-10-28
428
+ ### ♻️ Refactoring
429
+ - Add policies
430
+
431
+ ### ⚙️ Miscellaneous Tasks
432
+ - Improve sans-io diagram colors
433
+ - Add graphql docs
434
+
435
+ ### 🐛 Bug Fixes
436
+ - Body-sensitive responses caching
437
+ - Filter out `Transfer-Encoding` header for asgi responses
438
+
439
+ ### 🚀 Features
440
+ - Add global `use_body_key` setting
441
+
329
442
  ## 1.0.0.dev3 - 2025-10-26
330
443
  ### ♻️ Refactoring
331
444
  - Replace pairs with entries, simplify storage API
@@ -41,9 +41,10 @@
41
41
  - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
42
42
  - 🎨 **Type Safe** - Fully typed with comprehensive type hints
43
43
  - 🧪 **Well Tested** - Extensive test coverage and battle-tested
44
- - 🎛️ **Configurable** - Fine-grained control over caching behavior
45
- - **Memory Efficient** - Streaming support prevents loading large payloads into memory
44
+ - 🎛️ **Configurable** - Fine-grained control over caching behavior with flexible policies
45
+ - 💨 **Memory Efficient** - Streaming support prevents loading large payloads into memory
46
46
  - 🌐 **Universal** - Works with any ASGI application (Starlette, Litestar, BlackSheep, etc.)
47
+ - 🎯 **GraphQL Support** - Cache GraphQL queries with body-sensitive content caching
47
48
 
48
49
  ## 📦 Installation
49
50
 
@@ -134,12 +135,14 @@ from hishel.asgi import ASGICacheMiddleware
134
135
  app = ASGICacheMiddleware(app)
135
136
 
136
137
  # Or configure with options
137
- from hishel import AsyncSqliteStorage, CacheOptions
138
+ from hishel import AsyncSqliteStorage, CacheOptions, SpecificationPolicy
138
139
 
139
140
  app = ASGICacheMiddleware(
140
141
  app,
141
142
  storage=AsyncSqliteStorage(),
142
- cache_options=CacheOptions(shared=True)
143
+ policy=SpecificationPolicy(
144
+ cache_options=CacheOptions(shared=True)
145
+ ),
143
146
  )
144
147
  ```
145
148
 
@@ -184,21 +187,49 @@ async def get_data():
184
187
 
185
188
  ## 🎛️ Advanced Configuration
186
189
 
187
- ### Custom Cache Options
190
+ ### Caching Policies
191
+
192
+ Hishel supports two types of caching policies:
193
+
194
+ **SpecificationPolicy** - RFC 9111 compliant HTTP caching (default):
188
195
 
189
196
  ```python
190
- from hishel import CacheOptions
197
+ from hishel import CacheOptions, SpecificationPolicy
191
198
  from hishel.httpx import SyncCacheClient
192
199
 
193
200
  client = SyncCacheClient(
194
- cache_options=CacheOptions(
195
- shared=False, # Use as private cache (browser-like)
196
- supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
197
- allow_stale=True # Allow serving stale responses
201
+ policy=SpecificationPolicy(
202
+ cache_options=CacheOptions(
203
+ shared=False, # Use as private cache (browser-like)
204
+ supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
205
+ allow_stale=True # Allow serving stale responses
206
+ )
207
+ )
208
+ )
209
+ ```
210
+
211
+ **FilterPolicy** - Custom filtering logic for fine-grained control:
212
+
213
+ ```python
214
+ from hishel import FilterPolicy, BaseFilter, Request
215
+ from hishel.httpx import AsyncCacheClient
216
+
217
+ class CacheOnlyAPIRequests(BaseFilter[Request]):
218
+ def needs_body(self) -> bool:
219
+ return False
220
+
221
+ def apply(self, item: Request, body: bytes | None) -> bool:
222
+ return "/api/" in str(item.url)
223
+
224
+ client = AsyncCacheClient(
225
+ policy=FilterPolicy(
226
+ request_filters=[CacheOnlyAPIRequests()]
198
227
  )
199
228
  )
200
229
  ```
201
230
 
231
+ [Learn more about policies →](https://hishel.com/dev/policies/)
232
+
202
233
  ### Custom Storage Backend
203
234
 
204
235
  ```python
@@ -214,6 +245,60 @@ storage = SyncSqliteStorage(
214
245
  client = SyncCacheClient(storage=storage)
215
246
  ```
216
247
 
248
+ ### GraphQL and Body-Sensitive Caching
249
+
250
+ Cache GraphQL queries and other POST requests by including the request body in the cache key.
251
+
252
+ **Using per-request header:**
253
+
254
+ ```python
255
+ from hishel import FilterPolicy
256
+ from hishel.httpx import SyncCacheClient
257
+
258
+ client = SyncCacheClient(
259
+ policy=FilterPolicy()
260
+ )
261
+
262
+ # Cache GraphQL queries - different queries get different cache entries
263
+ graphql_query = """
264
+ query GetUser($id: ID!) {
265
+ user(id: $id) {
266
+ name
267
+ email
268
+ }
269
+ }
270
+ """
271
+
272
+ response = client.post(
273
+ "https://api.example.com/graphql",
274
+ json={"query": graphql_query, "variables": {"id": "123"}},
275
+ headers={"X-Hishel-Body-Key": "true"} # Enable body-based caching
276
+ )
277
+
278
+ # Different query will be cached separately
279
+ response = client.post(
280
+ "https://api.example.com/graphql",
281
+ json={"query": graphql_query, "variables": {"id": "456"}},
282
+ headers={"X-Hishel-Body-Key": "true"}
283
+ )
284
+ ```
285
+
286
+ **Using global configuration:**
287
+
288
+ ```python
289
+ from hishel.httpx import SyncCacheClient
290
+ from hishel import FilterPolicy
291
+
292
+ # Enable body-based caching for all requests
293
+ client = SyncCacheClient(policy=FilterPolicy(use_body_key=True))
294
+
295
+ # All POST requests automatically include body in cache key
296
+ response = client.post(
297
+ "https://api.example.com/graphql",
298
+ json={"query": graphql_query, "variables": {"id": "123"}}
299
+ )
300
+ ```
301
+
217
302
  ## 🏗️ Architecture
218
303
 
219
304
  Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
@@ -225,13 +310,11 @@ Hishel uses a **sans-I/O state machine** architecture that separates HTTP cachin
225
310
 
226
311
  ## 🔮 Roadmap
227
312
 
228
- While Hishel currently supports HTTPX and Requests, we're actively working on:
313
+ We're actively working on:
229
314
 
230
- - 🎯 Additional HTTP client integrations
231
- - 🎯 Server-side caching support
232
- - 🎯 More storage backends
233
- - 🎯 Advanced caching strategies
234
315
  - 🎯 Performance optimizations
316
+ - 🎯 More integrations
317
+ - 🎯 Partial responses support
235
318
 
236
319
  ## 📚 Documentation
237
320
 
@@ -243,6 +326,7 @@ Comprehensive documentation is available at [https://hishel.com/dev](https://his
243
326
  - [ASGI Integration](https://hishel.com/dev/asgi)
244
327
  - [FastAPI Integration](https://hishel.com/dev/fastapi)
245
328
  - [BlackSheep Integration](https://hishel.com/dev/integrations/blacksheep)
329
+ - [GraphQL Integration](https://hishel.com/dev/integrations/graphql)
246
330
  - [Storage Backends](https://hishel.com/dev/storages)
247
331
  - [Request/Response Metadata](https://hishel.com/dev/metadata)
248
332
  - [RFC 9111 Specification](https://hishel.com/dev/specification)
@@ -15,17 +15,20 @@ from hishel._core._spec import (
15
15
  NeedToBeUpdated as NeedToBeUpdated,
16
16
  State as State,
17
17
  StoreAndUse as StoreAndUse,
18
- create_idle_state as create_idle_state,
19
18
  )
20
19
  from hishel._core.models import (
21
20
  Entry as Entry,
22
21
  EntryMeta as EntryMeta,
23
22
  Request as Request,
24
- Response,
23
+ Response as Response,
24
+ ResponseMetadata as ResponseMetadata,
25
+ RequestMetadata as RequestMetadata,
25
26
  )
26
27
  from hishel._async_cache import AsyncCacheProxy as AsyncCacheProxy
27
28
  from hishel._sync_cache import SyncCacheProxy as SyncCacheProxy
28
29
 
30
+ from hishel._policies import SpecificationPolicy, FilterPolicy, CachePolicy
31
+
29
32
  __all__ = (
30
33
  # New API
31
34
  ## States
@@ -41,12 +44,13 @@ __all__ = (
41
44
  "StoreAndUse",
42
45
  "CouldNotBeStored",
43
46
  "InvalidateEntries",
44
- "create_idle_state",
45
47
  ## Models
46
48
  "Request",
47
49
  "Response",
48
50
  "Entry",
49
51
  "EntryMeta",
52
+ "RequestMetadata",
53
+ "ResponseMetadata",
50
54
  ## Headers
51
55
  "Headers",
52
56
  ## Storages
@@ -57,4 +61,8 @@ __all__ = (
57
61
  # Proxy
58
62
  "AsyncCacheProxy",
59
63
  "SyncCacheProxy",
64
+ # Policies
65
+ "CachePolicy",
66
+ "SpecificationPolicy",
67
+ "FilterPolicy",
60
68
  )
@@ -13,7 +13,6 @@ from hishel import (
13
13
  AsyncBaseStorage,
14
14
  AsyncSqliteStorage,
15
15
  CacheMiss,
16
- CacheOptions,
17
16
  CouldNotBeStored,
18
17
  FromCache,
19
18
  IdleClient,
@@ -22,10 +21,10 @@ from hishel import (
22
21
  Request,
23
22
  Response,
24
23
  StoreAndUse,
25
- create_idle_state,
26
24
  )
27
25
  from hishel._core._spec import InvalidateEntries, vary_headers_match
28
26
  from hishel._core.models import Entry, ResponseMetadata
27
+ from hishel._policies import CachePolicy, FilterPolicy, SpecificationPolicy
29
28
  from hishel._utils import make_async_iterator
30
29
 
31
30
  logger = logging.getLogger("hishel.integrations.clients")
@@ -37,87 +36,121 @@ class AsyncCacheProxy:
37
36
 
38
37
  This class is independent of any specific HTTP library and works only with internal models.
39
38
  It delegates request execution to a user-provided callable, making it compatible with any
40
- HTTP client. Caching behavior can be configured to either fully respect HTTP
41
- caching rules or bypass them entirely.
39
+ HTTP client. Caching behavior is determined by the policy object.
40
+
41
+ Args:
42
+ request_sender: Callable that sends HTTP requests and returns responses.
43
+ storage: Storage backend for cache entries. Defaults to AsyncSqliteStorage.
44
+ policy: Caching policy to use. Can be SpecificationPolicy (respects RFC 9111) or
45
+ FilterPolicy (user-defined filtering). Defaults to SpecificationPolicy().
42
46
  """
43
47
 
44
48
  def __init__(
45
49
  self,
46
50
  request_sender: Callable[[Request], Awaitable[Response]],
47
51
  storage: AsyncBaseStorage | None = None,
48
- cache_options: CacheOptions | None = None,
49
- ignore_specification: bool = False,
52
+ policy: CachePolicy | None = None,
50
53
  ) -> None:
51
54
  self.send_request = request_sender
52
55
  self.storage = storage if storage is not None else AsyncSqliteStorage()
53
- self.cache_options = cache_options if cache_options is not None else CacheOptions()
54
- self.ignore_specification = ignore_specification
56
+ self.policy = policy if policy is not None else SpecificationPolicy()
55
57
 
56
58
  async def handle_request(self, request: Request) -> Response:
57
- if self.ignore_specification or request.metadata.get("hishel_spec_ignore"):
58
- return await self._handle_request_ignoring_spec(request)
59
+ if isinstance(self.policy, FilterPolicy):
60
+ return await self._handle_request_with_filters(request)
59
61
  return await self._handle_request_respecting_spec(request)
60
62
 
61
63
  async def _get_key_for_request(self, request: Request) -> str:
62
- if request.metadata.get("hishel_body_key"):
64
+ if self.policy.use_body_key or request.metadata.get("hishel_body_key"):
63
65
  assert isinstance(request.stream, (AsyncIterator, AsyncIterable))
64
66
  collected = b"".join([chunk async for chunk in request.stream])
65
67
  hash_ = hashlib.sha256(collected).hexdigest()
66
68
  request.stream = make_async_iterator([collected])
67
- return f"{str(request.url)}-{hash_}"
69
+ return hash_
68
70
  return hashlib.sha256(str(request.url).encode("utf-8")).hexdigest()
69
71
 
70
- async def _maybe_refresh_pair_ttl(self, pair: Entry) -> None:
71
- if pair.request.metadata.get("hishel_refresh_ttl_on_access"):
72
+ async def _maybe_refresh_entry_ttl(self, entry: Entry) -> None:
73
+ if entry.request.metadata.get("hishel_refresh_ttl_on_access"):
72
74
  await self.storage.update_entry(
73
- pair.id,
74
- lambda complete_pair: replace(
75
- complete_pair,
76
- meta=replace(complete_pair.meta, created_at=time.time()),
75
+ entry.id,
76
+ lambda current_entry: replace(
77
+ current_entry,
78
+ meta=replace(current_entry.meta, created_at=time.time()),
77
79
  ),
78
80
  )
79
81
 
80
- async def _handle_request_ignoring_spec(self, request: Request) -> Response:
82
+ async def _handle_request_with_filters(self, request: Request) -> Response:
83
+ assert isinstance(self.policy, FilterPolicy)
84
+
85
+ for request_filter in self.policy.request_filters:
86
+ if request_filter.needs_body():
87
+ body = await request.aread()
88
+ if not request_filter.apply(request, body):
89
+ logger.debug("Request filtered out by request filter")
90
+ return await self.send_request(request)
91
+ else:
92
+ if not request_filter.apply(request, None):
93
+ logger.debug("Request filtered out by request filter")
94
+ return await self.send_request(request)
95
+
81
96
  logger.debug("Trying to get cached response ignoring specification")
82
- entries = await self.storage.get_entries(await self._get_key_for_request(request))
97
+ cache_key = await self._get_key_for_request(request)
98
+ entries = await self.storage.get_entries(cache_key)
83
99
 
84
100
  logger.debug(f"Found {len(entries)} cached entries for the request")
85
101
 
86
- for pair in entries:
102
+ for entry in entries:
87
103
  if (
88
- str(pair.request.url) == str(request.url)
89
- and pair.request.method == request.method
104
+ str(entry.request.url) == str(request.url)
105
+ and entry.request.method == request.method
90
106
  and vary_headers_match(
91
107
  request,
92
- pair,
108
+ entry,
93
109
  )
94
110
  ):
95
111
  logger.debug(
96
112
  "Found matching cached response for the request",
97
113
  )
98
114
  response_meta = ResponseMetadata(
99
- hishel_spec_ignored=True,
100
115
  hishel_from_cache=True,
101
- hishel_created_at=pair.meta.created_at,
116
+ hishel_created_at=entry.meta.created_at,
102
117
  hishel_revalidated=False,
103
118
  hishel_stored=False,
104
119
  )
105
- pair.response.metadata.update(response_meta) # type: ignore
106
- await self._maybe_refresh_pair_ttl(pair)
107
- return pair.response
120
+ entry.response.metadata.update(response_meta) # type: ignore
121
+ await self._maybe_refresh_entry_ttl(entry)
122
+ return entry.response
108
123
 
109
124
  response = await self.send_request(request)
125
+ for response_filter in self.policy.response_filters:
126
+ if response_filter.needs_body():
127
+ body = await response.aread()
128
+ if not response_filter.apply(response, body):
129
+ logger.debug("Response filtered out by response filter")
130
+ return response
131
+ else:
132
+ if not response_filter.apply(response, None):
133
+ logger.debug("Response filtered out by response filter")
134
+ return response
135
+ response_meta = ResponseMetadata(
136
+ hishel_from_cache=False,
137
+ hishel_created_at=time.time(),
138
+ hishel_revalidated=False,
139
+ hishel_stored=True,
140
+ )
141
+ response.metadata.update(response_meta) # type: ignore
110
142
 
111
143
  logger.debug("Storing response in cache ignoring specification")
112
144
  entry = await self.storage.create_entry(
113
145
  request,
114
146
  response,
115
- await self._get_key_for_request(request),
147
+ cache_key,
116
148
  )
117
149
  return entry.response
118
150
 
119
151
  async def _handle_request_respecting_spec(self, request: Request) -> Response:
120
- state: AnyState = create_idle_state("client", self.cache_options)
152
+ assert isinstance(self.policy, SpecificationPolicy)
153
+ state: AnyState = IdleClient(options=self.policy.cache_options)
121
154
 
122
155
  while state:
123
156
  logger.debug(f"Handling state: {state.__class__.__name__}")
@@ -132,8 +165,8 @@ class AsyncCacheProxy:
132
165
  elif isinstance(state, NeedRevalidation):
133
166
  state = await self._handle_revalidation(state)
134
167
  elif isinstance(state, FromCache):
135
- await self._maybe_refresh_pair_ttl(state.pair)
136
- return state.pair.response
168
+ await self._maybe_refresh_entry_ttl(state.entry)
169
+ return state.entry.response
137
170
  elif isinstance(state, NeedToBeUpdated):
138
171
  state = await self._handle_update(state)
139
172
  elif isinstance(state, InvalidateEntries):
@@ -152,12 +185,12 @@ class AsyncCacheProxy:
152
185
  return state.next(response)
153
186
 
154
187
  async def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response:
155
- complete_pair = await self.storage.create_entry(
188
+ entry = await self.storage.create_entry(
156
189
  request,
157
190
  state.response,
158
191
  await self._get_key_for_request(request),
159
192
  )
160
- return complete_pair.response
193
+ return entry.response
161
194
 
162
195
  async def _handle_revalidation(self, state: NeedRevalidation) -> AnyState:
163
196
  revalidation_response = await self.send_request(state.request)
@@ -167,8 +200,8 @@ class AsyncCacheProxy:
167
200
  for entry in state.updating_entries:
168
201
  await self.storage.update_entry(
169
202
  entry.id,
170
- lambda complete_pair: replace(
171
- complete_pair,
203
+ lambda entry: replace(
204
+ entry,
172
205
  response=replace(entry.response, headers=entry.response.headers),
173
206
  ),
174
207
  )