hishel 1.0.0.dev2__py3-none-any.whl → 1.1.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hishel
3
- Version: 1.0.0.dev2
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
@@ -29,6 +29,8 @@ Requires-Dist: typing-extensions>=4.14.1
29
29
  Provides-Extra: async
30
30
  Requires-Dist: anyio>=4.9.0; extra == 'async'
31
31
  Requires-Dist: anysqlite>=0.0.5; extra == 'async'
32
+ Provides-Extra: fastapi
33
+ Requires-Dist: fastapi>=0.119.1; extra == 'fastapi'
32
34
  Provides-Extra: httpx
33
35
  Requires-Dist: anyio>=4.9.0; extra == 'httpx'
34
36
  Requires-Dist: anysqlite>=0.0.5; extra == 'httpx'
@@ -37,6 +39,7 @@ Provides-Extra: requests
37
39
  Requires-Dist: requests>=2.32.5; extra == 'requests'
38
40
  Description-Content-Type: text/markdown
39
41
 
42
+
40
43
  <p align="center">
41
44
  <img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_yellow.png#gh-dark-mode-only">
42
45
  <img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_black.png#gh-light-mode-only">
@@ -73,14 +76,16 @@ Description-Content-Type: text/markdown
73
76
  ## ✨ Features
74
77
 
75
78
  - 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification
76
- - 🔌 **Easy Integration** - Drop-in support for HTTPX and Requests
79
+ - 🔌 **Easy Integration** - Drop-in support for HTTPX, Requests, ASGI, FastAPI, and BlackSheep
77
80
  - 💾 **Flexible Storage** - SQLite backend with more coming soon
78
81
  - ⚡ **High Performance** - Efficient caching with minimal overhead
79
82
  - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
80
83
  - 🎨 **Type Safe** - Fully typed with comprehensive type hints
81
84
  - 🧪 **Well Tested** - Extensive test coverage and battle-tested
82
- - 🎛️ **Configurable** - Fine-grained control over caching behavior
83
- - 🌐 **Future Ready** - Designed for easy integration with any HTTP client/server
85
+ - 🎛️ **Configurable** - Fine-grained control over caching behavior with flexible policies
86
+ - 💨 **Memory Efficient** - Streaming support prevents loading large payloads into memory
87
+ - 🌐 **Universal** - Works with any ASGI application (Starlette, Litestar, BlackSheep, etc.)
88
+ - 🎯 **GraphQL Support** - Cache GraphQL queries with body-sensitive content caching
84
89
 
85
90
  ## 📦 Installation
86
91
 
@@ -90,19 +95,23 @@ pip install hishel
90
95
 
91
96
  ### Optional Dependencies
92
97
 
93
- Install with specific HTTP client support:
98
+ Install with specific integration support:
94
99
 
95
100
  ```bash
96
101
  pip install hishel[httpx] # For HTTPX support
97
102
  pip install hishel[requests] # For Requests support
103
+ pip install hishel[fastapi] # For FastAPI support (includes ASGI)
98
104
  ```
99
105
 
100
- Or install both:
106
+ Or install multiple:
101
107
 
102
108
  ```bash
103
- pip install hishel[httpx,requests]
109
+ pip install hishel[httpx,requests,fastapi]
104
110
  ```
105
111
 
112
+ > [!NOTE]
113
+ > ASGI middleware has no extra dependencies - it's included in the base installation.
114
+
106
115
  ## 🚀 Quick Start
107
116
 
108
117
  ### With HTTPX
@@ -156,23 +165,112 @@ response = session.get("https://api.example.com/data")
156
165
  print(response.headers.get("X-Hishel-From-Cache")) # "True"
157
166
  ```
158
167
 
168
+ ### With ASGI Applications
169
+
170
+ Add caching middleware to any ASGI application:
171
+
172
+ ```python
173
+ from hishel.asgi import ASGICacheMiddleware
174
+
175
+ # Wrap your ASGI app
176
+ app = ASGICacheMiddleware(app)
177
+
178
+ # Or configure with options
179
+ from hishel import AsyncSqliteStorage, CacheOptions, SpecificationPolicy
180
+
181
+ app = ASGICacheMiddleware(
182
+ app,
183
+ storage=AsyncSqliteStorage(),
184
+ policy=SpecificationPolicy(
185
+ cache_options=CacheOptions(shared=True)
186
+ ),
187
+ )
188
+ ```
189
+
190
+ ### With FastAPI
191
+
192
+ Add Cache-Control headers using the `cache()` dependency:
193
+
194
+ ```python
195
+ from fastapi import FastAPI
196
+ from hishel.fastapi import cache
197
+
198
+ app = FastAPI()
199
+
200
+ @app.get("/api/data", dependencies=[cache(max_age=300, public=True)])
201
+ async def get_data():
202
+ # Cache-Control: public, max-age=300
203
+ return {"data": "cached for 5 minutes"}
204
+
205
+ # Optionally wrap with ASGI middleware for local caching according to specified rules
206
+ from hishel.asgi import ASGICacheMiddleware
207
+ from hishel import AsyncSqliteStorage
208
+
209
+ app = ASGICacheMiddleware(app, storage=AsyncSqliteStorage())
210
+ ```
211
+
212
+ ### With BlackSheep
213
+
214
+ Use BlackSheep's native `cache_control` decorator with Hishel's ASGI middleware:
215
+
216
+ ```python
217
+ from blacksheep import Application, get
218
+ from blacksheep.server.headers.cache import cache_control
219
+
220
+ app = Application()
221
+
222
+ @get("/api/data")
223
+ @cache_control(max_age=300, public=True)
224
+ async def get_data():
225
+ # Cache-Control: public, max-age=300
226
+ return {"data": "cached for 5 minutes"}
227
+ ```
228
+
159
229
  ## 🎛️ Advanced Configuration
160
230
 
161
- ### Custom Cache Options
231
+ ### Caching Policies
232
+
233
+ Hishel supports two types of caching policies:
234
+
235
+ **SpecificationPolicy** - RFC 9111 compliant HTTP caching (default):
162
236
 
163
237
  ```python
164
- from hishel import CacheOptions
238
+ from hishel import CacheOptions, SpecificationPolicy
165
239
  from hishel.httpx import SyncCacheClient
166
240
 
167
241
  client = SyncCacheClient(
168
- cache_options=CacheOptions(
169
- shared=False, # Use as private cache (browser-like)
170
- supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
171
- 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
+ )
248
+ )
249
+ )
250
+ ```
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()]
172
268
  )
173
269
  )
174
270
  ```
175
271
 
272
+ [Learn more about policies →](https://hishel.com/dev/policies/)
273
+
176
274
  ### Custom Storage Backend
177
275
 
178
276
  ```python
@@ -188,6 +286,60 @@ storage = SyncSqliteStorage(
188
286
  client = SyncCacheClient(storage=storage)
189
287
  ```
190
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
+
191
343
  ## 🏗️ Architecture
192
344
 
193
345
  Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
@@ -199,13 +351,11 @@ Hishel uses a **sans-I/O state machine** architecture that separates HTTP cachin
199
351
 
200
352
  ## 🔮 Roadmap
201
353
 
202
- While Hishel currently supports HTTPX and Requests, we're actively working on:
354
+ We're actively working on:
203
355
 
204
- - 🎯 Additional HTTP client integrations
205
- - 🎯 Server-side caching support
206
- - 🎯 More storage backends
207
- - 🎯 Advanced caching strategies
208
356
  - 🎯 Performance optimizations
357
+ - 🎯 More integrations
358
+ - 🎯 Partial responses support
209
359
 
210
360
  ## 📚 Documentation
211
361
 
@@ -214,7 +364,12 @@ Comprehensive documentation is available at [https://hishel.com/dev](https://his
214
364
  - [Getting Started](https://hishel.com)
215
365
  - [HTTPX Integration](https://hishel.com/dev/integrations/httpx)
216
366
  - [Requests Integration](https://hishel.com/dev/integrations/requests)
367
+ - [ASGI Integration](https://hishel.com/dev/asgi)
368
+ - [FastAPI Integration](https://hishel.com/dev/fastapi)
369
+ - [BlackSheep Integration](https://hishel.com/dev/integrations/blacksheep)
370
+ - [GraphQL Integration](https://hishel.com/dev/integrations/graphql)
217
371
  - [Storage Backends](https://hishel.com/dev/storages)
372
+ - [Request/Response Metadata](https://hishel.com/dev/metadata)
218
373
  - [RFC 9111 Specification](https://hishel.com/dev/specification)
219
374
 
220
375
  ## 🤝 Contributing
@@ -255,10 +410,62 @@ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecos
255
410
 
256
411
  All notable changes to this project will be documented in this file.
257
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
+
442
+ ## 1.0.0.dev3 - 2025-10-26
443
+ ### ♻️ Refactoring
444
+ - Replace pairs with entries, simplify storage API
445
+ - Automatically generate httpx sync integration from async
446
+
447
+ ### ⚙️ Miscellaneous Tasks
448
+ - Simplify metadata docs
449
+ - Add custom integrations docs
450
+ - More robust compressed response caching
451
+
452
+ ### 🐛 Bug Fixes
453
+ - Add missing permissions into `publish.yml`
454
+ - Raise on consumed httpx streams, which we can't store as is (it's already decoded)
455
+ - Fix compressed data caching for requests
456
+ - Handle httpx iterable usage instead of iterator correctly
457
+ - Add date header for proper age calculation
458
+
459
+ ### 🚀 Features
460
+ - Add integrations with fastapi and asgi
461
+ - Add blacksheep integration examples
462
+ - Add logging for asgi
463
+
258
464
  ## 1.0.0.dev2 - 2025-10-21
259
465
  ### ⚙️ Miscellaneous Tasks
260
466
  - Remove redundant utils and tests
261
467
  - Add import without extras check in ci
468
+ - Fix time travel date, explicitly specify the timezone
262
469
 
263
470
  ### 🐛 Bug Fixes
264
471
  - Fix check for storing auth requests
@@ -0,0 +1,24 @@
1
+ hishel/__init__.py,sha256=XYnAlT2Wkrg0Cw5u4DLJAsC3TjWDOQJr5kqQMuXxEJI,1796
2
+ hishel/_async_cache.py,sha256=4wm6YBL9ClNOg4WbEkALTJVCIXuzQU80MVem2C3hHrQ,8785
3
+ hishel/_async_httpx.py,sha256=WB35o7CseRDz89dpqoWNLXYEkDxqeSo709eR3oPXP7Q,7536
4
+ hishel/_policies.py,sha256=1ae_rmDF7oaG91-lQyOGVaTrRX8uI2GImmu5gN6WJa4,1135
5
+ hishel/_sync_cache.py,sha256=k0AN0M--yR4Jc6SiAreaxPUFiwEt5Dx7wi9jqW9sy50,8510
6
+ hishel/_sync_httpx.py,sha256=FbnJrdoLftPDVNUzYkAtz-JOTNtiBPJ2c1ZOcU0JnmE,7368
7
+ hishel/_utils.py,sha256=kR7RnhFqLzFRmB-YNnZteQVP0iDPUouCscA0_FHHFls,3837
8
+ hishel/asgi.py,sha256=ocXzqrrYGazeJxlKFcz1waoKvKGOqJ7YBEAmly4Towk,14998
9
+ hishel/fastapi.py,sha256=CVWCyXTxBPwG_XALo-Oldekv4lqMgH2-W-PPZ9rZjXg,10826
10
+ hishel/httpx.py,sha256=99a8X9COPiPHSgGW61O2uMWMZB7dY93Ty9DTCJ9C18Q,467
11
+ hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hishel/requests.py,sha256=FbOBMvgNSWkH0Bh0qZdr-dhCYG6siR5Lmxcu5eMay3s,6409
13
+ hishel/_core/_headers.py,sha256=hGaT6o1F-gs1pm5RpdGb0IMQL3uJYDH1xpwJLy28Cys,17514
14
+ hishel/_core/_spec.py,sha256=YfZfMYS4--o0v-AQZ37vVzHfc-Mh-SEHcl9SrBM9Y7Y,102963
15
+ hishel/_core/models.py,sha256=EabP2qnjYVzhPWhQer3QFmdDE6TDbqEBEqPHzv25VnA,7978
16
+ hishel/_core/_storages/_async_base.py,sha256=iZ6Mb30P0ho5h4UU5bgOrcsSMZ1427j9tht-tupZs68,2106
17
+ hishel/_core/_storages/_async_sqlite.py,sha256=QZEWroGZKGhCMf7CcYpZfKZdKd88R8elQCoPge0Khxc,15674
18
+ hishel/_core/_storages/_packing.py,sha256=mC8LMFQ5uPfFOgingKm2WKFO_DwcZ1OjTgI6xc0hfJI,3708
19
+ hishel/_core/_storages/_sync_base.py,sha256=qfOvcFY5qvrzSh4ztV2Trlxft-BF7An5SFsLlEb8EeE,2075
20
+ hishel/_core/_storages/_sync_sqlite.py,sha256=P3vIp636tTtM553-sBTZkam3-sNv0EEkh1rRSd1Qw90,15167
21
+ hishel-1.1.0.dist-info/METADATA,sha256=08BqqoUdS_tMtsQyo4JTS5mOuASXLW1cs5S_PrSAuGM,15759
22
+ hishel-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ hishel-1.1.0.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
24
+ hishel-1.1.0.dist-info/RECORD,,
hishel/_core/__init__.py DELETED
@@ -1,59 +0,0 @@
1
- from hishel._core._async._storages._sqlite import AsyncSqliteStorage
2
- from hishel._core._base._storages._base import (
3
- AsyncBaseStorage as AsyncBaseStorage,
4
- SyncBaseStorage as SyncBaseStorage,
5
- )
6
- from hishel._core._headers import Headers as Headers
7
- from hishel._core._spec import (
8
- AnyState as AnyState,
9
- CacheMiss as CacheMiss,
10
- CacheOptions as CacheOptions,
11
- CouldNotBeStored as CouldNotBeStored,
12
- FromCache as FromCache,
13
- IdleClient as IdleClient,
14
- NeedRevalidation as NeedRevalidation,
15
- NeedToBeUpdated as NeedToBeUpdated,
16
- State as State,
17
- StoreAndUse as StoreAndUse,
18
- create_idle_state as create_idle_state,
19
- )
20
- from hishel._core._sync._storages._sqlite import SyncSqliteStorage
21
- from hishel._core.models import (
22
- CompletePair as CompletePair,
23
- IncompletePair as IncompletePair,
24
- Pair as Pair,
25
- PairMeta as PairMeta,
26
- Request as Request,
27
- Response,
28
- )
29
-
30
- __all__ = (
31
- # New API
32
- ## States
33
- "AnyState",
34
- "IdleClient",
35
- "CacheMiss",
36
- "FromCache",
37
- "NeedRevalidation",
38
- "AnyState",
39
- "CacheOptions",
40
- "NeedToBeUpdated",
41
- "State",
42
- "StoreAndUse",
43
- "CouldNotBeStored",
44
- "create_idle_state",
45
- ## Models
46
- "Request",
47
- "Response",
48
- "Pair",
49
- "IncompletePair",
50
- "CompletePair",
51
- "PairMeta",
52
- ## Headers
53
- "Headers",
54
- ## Storages
55
- "SyncBaseStorage",
56
- "AsyncBaseStorage",
57
- "SyncSqliteStorage",
58
- "AsyncSqliteStorage",
59
- )
@@ -1,272 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import abc
4
- import time
5
- import typing as tp
6
- import uuid
7
- from abc import ABC
8
- from pathlib import Path
9
-
10
- from hishel._core.models import CompletePair, IncompletePair, Request, Response
11
-
12
-
13
- class SyncBaseStorage(ABC):
14
- @abc.abstractmethod
15
- def create_pair(
16
- self,
17
- request: Request,
18
- id: uuid.UUID | None = None,
19
- ) -> IncompletePair:
20
- """
21
- Store a request in the backend under the given key.
22
-
23
- Args:
24
- request: The request object to store.
25
-
26
- Returns:
27
- The created IncompletePair object representing the stored request.
28
-
29
- Raises:
30
- NotImplementedError: Must be implemented in subclasses.
31
- """
32
- raise NotImplementedError()
33
-
34
- @abc.abstractmethod
35
- def add_response(self, pair_id: uuid.UUID, response: Response, key: str | bytes) -> CompletePair:
36
- """
37
- Add a response to an existing request pair.
38
-
39
- Args:
40
- pair_id: The unique identifier of the request pair.
41
- response: The response object to add.
42
- key: The cache key associated with the request pair.
43
-
44
- Returns:
45
- The updated response object.
46
-
47
- Raises:
48
- NotImplementedError: Must be implemented in subclasses.
49
- """
50
- raise NotImplementedError()
51
-
52
- @abc.abstractmethod
53
- def get_pairs(self, key: str) -> tp.List[CompletePair]:
54
- """
55
- Retrieve all responses associated with a given key.
56
-
57
- Args:
58
- key: The unique identifier for the request pairs.
59
- complete_only: If True, only return pairs with responses. If False,
60
- only return pairs without responses. If None, return all pairs.
61
- """
62
- raise NotImplementedError()
63
-
64
- @abc.abstractmethod
65
- def update_pair(
66
- self,
67
- id: uuid.UUID,
68
- new_pair: tp.Union[CompletePair, tp.Callable[[CompletePair], CompletePair]],
69
- ) -> tp.Optional[CompletePair]:
70
- """
71
- Update an existing request pair.
72
-
73
- Args:
74
- id: The unique identifier of the request pair to update.
75
- new_pair: The new pair data or a callable that takes the current pair
76
- and returns the updated pair.
77
- """
78
- raise NotImplementedError()
79
-
80
- @abc.abstractmethod
81
- def remove(self, id: uuid.UUID) -> None:
82
- """
83
- Remove a request pair from the storage.
84
-
85
- Args:
86
- id: The unique identifier of the request pair to remove.
87
- """
88
- raise NotImplementedError()
89
-
90
- def close(self) -> None:
91
- """
92
- Close any resources held by the storage backend.
93
- """
94
- pass
95
-
96
- def is_soft_deleted(self, pair: IncompletePair | CompletePair) -> bool:
97
- """
98
- Check if a pair is soft deleted based on its metadata.
99
-
100
- Args:
101
- pair: The request pair to check.
102
-
103
- Returns:
104
- True if the pair is soft deleted, False otherwise.
105
- """
106
- return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0
107
-
108
- def is_safe_to_hard_delete(self, pair: IncompletePair | CompletePair) -> bool:
109
- """
110
- Check if a pair is safe to hard delete based on its metadata.
111
-
112
- If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete.
113
-
114
- Args:
115
- pair: The request pair to check.
116
-
117
- Returns:
118
- True if the pair is safe to hard delete, False otherwise.
119
- """
120
- return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time()))
121
-
122
- @tp.overload
123
- def mark_pair_as_deleted(self, pair: CompletePair) -> CompletePair: ...
124
- @tp.overload
125
- def mark_pair_as_deleted(self, pair: IncompletePair) -> IncompletePair: ...
126
- def mark_pair_as_deleted(self, pair: CompletePair | IncompletePair) -> CompletePair | IncompletePair:
127
- """
128
- Mark a pair as soft deleted by setting its deleted_at timestamp.
129
-
130
- Args:
131
- pair: The request pair to mark as deleted.
132
- Returns:
133
- The updated request pair with the deleted_at timestamp set.
134
- """
135
- pair.meta.deleted_at = time.time()
136
- return pair
137
-
138
-
139
- class AsyncBaseStorage(ABC):
140
- @abc.abstractmethod
141
- async def create_pair(
142
- self,
143
- request: Request,
144
- id: uuid.UUID | None = None,
145
- ) -> IncompletePair:
146
- """
147
- Store a request in the backend under the given key.
148
-
149
- Args:
150
- request: The request object to store.
151
-
152
- Returns:
153
- The created IncompletePair object representing the stored request.
154
-
155
- Raises:
156
- NotImplementedError: Must be implemented in subclasses.
157
- """
158
- raise NotImplementedError()
159
-
160
- @abc.abstractmethod
161
- async def add_response(self, pair_id: uuid.UUID, response: Response, key: str | bytes) -> CompletePair:
162
- """
163
- Add a response to an existing request pair.
164
-
165
- Args:
166
- pair_id: The unique identifier of the request pair.
167
- response: The response object to add.
168
- key: The cache key associated with the request pair.
169
-
170
- Returns:
171
- The updated response object.
172
-
173
- Raises:
174
- NotImplementedError: Must be implemented in subclasses.
175
- """
176
- raise NotImplementedError()
177
-
178
- @abc.abstractmethod
179
- async def get_pairs(self, key: str) -> tp.List[CompletePair]:
180
- """
181
- Retrieve all responses associated with a given key.
182
-
183
- Args:
184
- key: The unique identifier for the request pairs.
185
- """
186
- raise NotImplementedError()
187
-
188
- @abc.abstractmethod
189
- async def update_pair(
190
- self,
191
- id: uuid.UUID,
192
- new_pair: tp.Union[CompletePair, tp.Callable[[CompletePair], CompletePair]],
193
- ) -> tp.Optional[CompletePair]:
194
- """
195
- Update an existing request pair.
196
-
197
- Args:
198
- id: The unique identifier of the request pair to update.
199
- new_pair: The new pair data or a callable that takes the current pair
200
- and returns the updated pair.
201
- """
202
- raise NotImplementedError()
203
-
204
- @abc.abstractmethod
205
- async def remove(self, id: uuid.UUID) -> None:
206
- """
207
- Remove a request pair from the storage.
208
-
209
- Args:
210
- id: The unique identifier of the request pair to remove.
211
- """
212
- raise NotImplementedError()
213
-
214
- async def close(self) -> None:
215
- """
216
- Close any resources held by the storage backend.
217
- """
218
- pass
219
-
220
- def is_soft_deleted(self, pair: IncompletePair | CompletePair) -> bool:
221
- """
222
- Check if a pair is soft deleted based on its metadata.
223
-
224
- Args:
225
- pair: The request pair to check.
226
-
227
- Returns:
228
- True if the pair is soft deleted, False otherwise.
229
- """
230
- return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0
231
-
232
- def is_safe_to_hard_delete(self, pair: IncompletePair | CompletePair) -> bool:
233
- """
234
- Check if a pair is safe to hard delete based on its metadata.
235
-
236
- If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete.
237
-
238
- Args:
239
- pair: The request pair to check.
240
-
241
- Returns:
242
- True if the pair is safe to hard delete, False otherwise.
243
- """
244
- return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time()))
245
-
246
- @tp.overload
247
- def mark_pair_as_deleted(self, pair: CompletePair) -> CompletePair: ...
248
- @tp.overload
249
- def mark_pair_as_deleted(self, pair: IncompletePair) -> IncompletePair: ...
250
- def mark_pair_as_deleted(self, pair: CompletePair | IncompletePair) -> CompletePair | IncompletePair:
251
- """
252
- Mark a pair as soft deleted by setting its deleted_at timestamp.
253
-
254
- Args:
255
- pair: The request pair to mark as deleted.
256
- Returns:
257
- The updated request pair with the deleted_at timestamp set.
258
- """
259
- pair.meta.deleted_at = time.time()
260
- return pair
261
-
262
-
263
- def ensure_cache_dict(base_path: str | None = None) -> Path:
264
- _base_path = Path(base_path) if base_path is not None else Path(".cache/hishel")
265
- _gitignore_file = _base_path / ".gitignore"
266
-
267
- _base_path.mkdir(parents=True, exist_ok=True)
268
-
269
- if not _gitignore_file.is_file():
270
- with open(_gitignore_file, "w", encoding="utf-8") as f:
271
- f.write("# Automatically created by Hishel\n*")
272
- return _base_path