hishel 0.1.5__py3-none-any.whl → 1.0.0.dev0__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.
Files changed (41) hide show
  1. hishel/__init__.py +55 -53
  2. hishel/{beta/_async_cache.py → _async_cache.py} +3 -3
  3. hishel/{beta → _core}/__init__.py +6 -6
  4. hishel/{beta/_core → _core}/_async/_storages/_sqlite.py +3 -3
  5. hishel/{beta/_core → _core}/_base/_storages/_base.py +1 -1
  6. hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
  7. hishel/{beta/_core → _core}/_spec.py +89 -2
  8. hishel/{beta/_core → _core}/_sync/_storages/_sqlite.py +3 -3
  9. hishel/{beta/_core → _core}/models.py +1 -1
  10. hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
  11. hishel/{beta/httpx.py → httpx.py} +6 -6
  12. hishel/{beta/requests.py → requests.py} +5 -5
  13. hishel-1.0.0.dev0.dist-info/METADATA +321 -0
  14. hishel-1.0.0.dev0.dist-info/RECORD +19 -0
  15. hishel/_async/__init__.py +0 -5
  16. hishel/_async/_client.py +0 -30
  17. hishel/_async/_mock.py +0 -43
  18. hishel/_async/_pool.py +0 -201
  19. hishel/_async/_storages.py +0 -768
  20. hishel/_async/_transports.py +0 -282
  21. hishel/_controller.py +0 -581
  22. hishel/_exceptions.py +0 -10
  23. hishel/_files.py +0 -54
  24. hishel/_headers.py +0 -215
  25. hishel/_lfu_cache.py +0 -71
  26. hishel/_lmdb_types_.pyi +0 -53
  27. hishel/_s3.py +0 -122
  28. hishel/_serializers.py +0 -329
  29. hishel/_sync/__init__.py +0 -5
  30. hishel/_sync/_client.py +0 -30
  31. hishel/_sync/_mock.py +0 -43
  32. hishel/_sync/_pool.py +0 -201
  33. hishel/_sync/_storages.py +0 -768
  34. hishel/_sync/_transports.py +0 -282
  35. hishel/_synchronization.py +0 -37
  36. hishel/beta/_core/__init__.py +0 -0
  37. hishel-0.1.5.dist-info/METADATA +0 -258
  38. hishel-0.1.5.dist-info/RECORD +0 -41
  39. /hishel/{beta/_core → _core}/_headers.py +0 -0
  40. {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/WHEEL +0 -0
  41. {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,321 @@
1
+ Metadata-Version: 2.4
2
+ Name: hishel
3
+ Version: 1.0.0.dev0
4
+ Summary: Elegant HTTP Caching for Python
5
+ Project-URL: Homepage, https://hishel.com
6
+ Project-URL: Source, https://github.com/karpetrosyan/hishel
7
+ Author-email: Kar Petrosyan <kar.petrosyanpy@gmail.com>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Framework :: Trio
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: anyio>=4.9.0
28
+ Requires-Dist: anysqlite>=0.0.5
29
+ Requires-Dist: httpx>=0.28.0
30
+ Requires-Dist: msgpack>=1.1.2
31
+ Requires-Dist: typing-extensions>=4.14.1
32
+ Provides-Extra: httpx
33
+ Requires-Dist: httpx>=0.28.1; extra == 'httpx'
34
+ Provides-Extra: requests
35
+ Requires-Dist: requests>=2.32.5; extra == 'requests'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <p align="center">
39
+ <img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_yellow.png#gh-dark-mode-only">
40
+ <img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_black.png#gh-light-mode-only">
41
+ </p>
42
+
43
+ <h1 align="center">Hishel</h1>
44
+
45
+ <p align="center">
46
+ <strong>Elegant HTTP Caching for Python</strong>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/hishel">
51
+ <img src="https://img.shields.io/pypi/v/hishel.svg" alt="PyPI version">
52
+ </a>
53
+ <a href="https://pypi.org/project/hishel">
54
+ <img src="https://img.shields.io/pypi/pyversions/hishel.svg" alt="Python versions">
55
+ </a>
56
+ <a href="https://github.com/karpetrosyan/hishel/blob/master/LICENSE">
57
+ <img src="https://img.shields.io/pypi/l/hishel" alt="License">
58
+ </a>
59
+ <a href="https://coveralls.io/github/karpetrosyan/hishel">
60
+ <img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="Coverage">
61
+ </a>
62
+ <a href="https://static.pepy.tech/badge/hishel/month">
63
+ <img src="https://static.pepy.tech/badge/hishel/month" alt="Downloads">
64
+ </a>
65
+ </p>
66
+
67
+ ---
68
+
69
+ **Hishel** (հիշել, *to remember* in Armenian) is a modern HTTP caching library for Python that implements [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) specifications. It provides seamless caching integration for popular HTTP clients with minimal code changes.
70
+
71
+ ## ✨ Features
72
+
73
+ - 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification
74
+ - 🔌 **Easy Integration** - Drop-in support for HTTPX and Requests
75
+ - 💾 **Flexible Storage** - SQLite backend with more coming soon
76
+ - ⚡ **High Performance** - Efficient caching with minimal overhead
77
+ - 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
78
+ - 🎨 **Type Safe** - Fully typed with comprehensive type hints
79
+ - 🧪 **Well Tested** - Extensive test coverage and battle-tested
80
+ - 🎛️ **Configurable** - Fine-grained control over caching behavior
81
+ - 🌐 **Future Ready** - Designed for easy integration with any HTTP client/server
82
+
83
+ ## 📦 Installation
84
+
85
+ ```bash
86
+ pip install hishel
87
+ ```
88
+
89
+ ### Optional Dependencies
90
+
91
+ Install with specific HTTP client support:
92
+
93
+ ```bash
94
+ pip install hishel[httpx] # For HTTPX support
95
+ pip install hishel[requests] # For Requests support
96
+ ```
97
+
98
+ Or install both:
99
+
100
+ ```bash
101
+ pip install hishel[httpx,requests]
102
+ ```
103
+
104
+ ## 🚀 Quick Start
105
+
106
+ ### With HTTPX
107
+
108
+ **Synchronous:**
109
+
110
+ ```python
111
+ from hishel.httpx import SyncCacheClient
112
+
113
+ client = SyncCacheClient()
114
+
115
+ # First request - fetches from origin
116
+ response = client.get("https://api.example.com/data")
117
+ print(response.extensions["hishel_from_cache"]) # False
118
+
119
+ # Second request - served from cache
120
+ response = client.get("https://api.example.com/data")
121
+ print(response.extensions["hishel_from_cache"]) # True
122
+ ```
123
+
124
+ **Asynchronous:**
125
+
126
+ ```python
127
+ from hishel.httpx import AsyncCacheClient
128
+
129
+ async with AsyncCacheClient() as client:
130
+ # First request - fetches from origin
131
+ response = await client.get("https://api.example.com/data")
132
+ print(response.extensions["hishel_from_cache"]) # False
133
+
134
+ # Second request - served from cache
135
+ response = await client.get("https://api.example.com/data")
136
+ print(response.extensions["hishel_from_cache"]) # True
137
+ ```
138
+
139
+ ### With Requests
140
+
141
+ ```python
142
+ import requests
143
+ from hishel.requests import CacheAdapter
144
+
145
+ session = requests.Session()
146
+ session.mount("https://", CacheAdapter())
147
+ session.mount("http://", CacheAdapter())
148
+
149
+ # First request - fetches from origin
150
+ response = session.get("https://api.example.com/data")
151
+
152
+ # Second request - served from cache
153
+ response = session.get("https://api.example.com/data")
154
+ print(response.headers.get("X-Hishel-From-Cache")) # "True"
155
+ ```
156
+
157
+ ## 🎛️ Advanced Configuration
158
+
159
+ ### Custom Cache Options
160
+
161
+ ```python
162
+ from hishel import CacheOptions
163
+ from hishel.httpx import SyncCacheClient
164
+
165
+ client = SyncCacheClient(
166
+ cache_options=CacheOptions(
167
+ shared=False, # Use as private cache (browser-like)
168
+ supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
169
+ allow_stale=True # Allow serving stale responses
170
+ )
171
+ )
172
+ ```
173
+
174
+ ### Custom Storage Backend
175
+
176
+ ```python
177
+ from hishel import SyncSqliteStorage
178
+ from hishel.httpx import SyncCacheClient
179
+
180
+ storage = SyncSqliteStorage(
181
+ database_path="my_cache.db",
182
+ default_ttl=7200.0, # Cache entries expire after 2 hours
183
+ refresh_ttl_on_access=True # Reset TTL when accessing cached entries
184
+ )
185
+
186
+ client = SyncCacheClient(storage=storage)
187
+ ```
188
+
189
+ ## 🏗️ Architecture
190
+
191
+ Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
192
+
193
+ - ✅ **Correct** - Fully RFC 9111 compliant
194
+ - ✅ **Testable** - Easy to test without network dependencies
195
+ - ✅ **Flexible** - Works with any HTTP client or server
196
+ - ✅ **Type Safe** - Clear state transitions with full type hints
197
+
198
+ ## 🔮 Roadmap
199
+
200
+ While Hishel currently supports HTTPX and Requests, we're actively working on:
201
+
202
+ - 🎯 Additional HTTP client integrations
203
+ - 🎯 Server-side caching support
204
+ - 🎯 More storage backends
205
+ - 🎯 Advanced caching strategies
206
+ - 🎯 Performance optimizations
207
+
208
+ ## 📚 Documentation
209
+
210
+ Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev)
211
+
212
+ - [Getting Started](https://hishel.com)
213
+ - [HTTPX Integration](https://hishel.com/dev/integrations/httpx)
214
+ - [Requests Integration](https://hishel.com/dev/integrations/requests)
215
+ - [Storage Backends](https://hishel.com/dev/storages)
216
+ - [RFC 9111 Specification](https://hishel.com/dev/specification)
217
+
218
+ ## 🤝 Contributing
219
+
220
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
221
+
222
+ See our [Contributing Guide](https://hishel.com/dev/contributing) for more details.
223
+
224
+ ## 📄 License
225
+
226
+ This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
227
+
228
+ ## 💖 Support
229
+
230
+ If you find Hishel useful, please consider:
231
+
232
+ - ⭐ Starring the repository
233
+ - 🐛 Reporting bugs and issues
234
+ - 💡 Suggesting new features
235
+ - 📖 Improving documentation
236
+ - ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan)
237
+
238
+ ## 🙏 Acknowledgments
239
+
240
+ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly:
241
+
242
+ - [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python
243
+ - [Requests](https://github.com/psf/requests) - The classic HTTP library for Python
244
+ - [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification
245
+
246
+ ---
247
+
248
+ <p align="center">
249
+ <strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
250
+ </p>
251
+
252
+ ## [1.0.0dev0] - 2025-10-19
253
+
254
+ ### ⚙️ Miscellaneous Tasks
255
+
256
+ - *(docs)* Use mike powered versioning
257
+ - *(docs)* Improve docs versioning, deploy dev doc on ci
258
+ ## [0.1.5] - 2025-10-18
259
+
260
+ ### 🚀 Features
261
+
262
+ - *(perf)* Set chunk size to 128KB for httpx to reduce SQLite read/writes
263
+ - Better cache-control parsing
264
+ - Add close method to storages API (#384)
265
+ - *(perf)* Increase requests buffer size to 128KB, disable charset detection
266
+
267
+ ### 🐛 Bug Fixes
268
+
269
+ - *(docs)* Fix some line breaks
270
+
271
+ ### ⚙️ Miscellaneous Tasks
272
+
273
+ - Remove some redundant files from repo
274
+ ## [0.1.4] - 2025-10-14
275
+
276
+ ### 🚀 Features
277
+
278
+ - Add support for a sans-IO API (#366)
279
+ - Allow already consumed streams with `CacheTransport` (#377)
280
+ - Add sqlite storage for beta storages
281
+ - Get rid of some locks from sqlite storage
282
+ - Better async implemetation for sqlite storage
283
+
284
+ ### 🐛 Bug Fixes
285
+
286
+ - Create an sqlite file in a cache folder
287
+ - Fix beta imports
288
+
289
+ ### ⚙️ Miscellaneous Tasks
290
+
291
+ - Improve CI (#369)
292
+ - *(internal)* Remove src folder (#373)
293
+ - *(internal)* Temporary remove python3.14 from CI
294
+ - *(tests)* Add sqlite tests for new storage
295
+ - *(tests)* Move some tests to beta
296
+ ## [0.1.3] - 2025-07-06
297
+
298
+ ### 🚀 Features
299
+
300
+ - Support providing a path prefix to S3 storage (#342)
301
+
302
+ ### 📚 Documentation
303
+
304
+ - Update link to httpx transports page (#337)
305
+ ## [0.1.2] - 2025-04-04
306
+
307
+ ### 🐛 Bug Fixes
308
+
309
+ - Requirements.txt to reduce vulnerabilities (#263)
310
+ ## [0.0.30] - 2024-07-12
311
+
312
+ ### 🐛 Bug Fixes
313
+
314
+ - Requirements.txt to reduce vulnerabilities (#245)
315
+ - Requirements.txt to reduce vulnerabilities (#255)
316
+ ## [0.0.27] - 2024-05-31
317
+
318
+ ### 🐛 Bug Fixes
319
+
320
+ - *(redis)* Do not update metadata with negative ttl (#231)
321
+ ## [0.0.1] - 2023-07-22
@@ -0,0 +1,19 @@
1
+ hishel/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
2
+ hishel/_async_cache.py,sha256=gE5CygC7FG9htBMfxul7carRRNph8zcMlSoOcB_LNTY,6792
3
+ hishel/_sync_cache.py,sha256=lfkWHJFK527peESMaufjKSbXBriidc09tOwBwub2t34,6538
4
+ hishel/_utils.py,sha256=uO8PcY_E1sHSgBGzZ2GNB4kpKqAlzmnzPCc3s-yDd44,13826
5
+ hishel/httpx.py,sha256=HcJ5iO9PgkEOp92ti8013N6m1IotLajwd9M_DLsmrX0,10997
6
+ hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ hishel/requests.py,sha256=eiWcwCId04DucnquCsU12tj9cDZcn-cjZ9MYniVuNeo,6429
8
+ hishel/_core/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
9
+ hishel/_core/_headers.py,sha256=ii4x2L6GoQFpqpgg28OtFh7p2DoM9mhE4q6CjW6xUWc,17473
10
+ hishel/_core/_spec.py,sha256=d2ZnTXttyT4zuVq9xHAO86VGJxAEBxD2a8WMyEgOuAo,102702
11
+ hishel/_core/models.py,sha256=5qwo1WifrDeZdXag7M5rh0hJuVsm1N-sF3UagQ5LcLc,5519
12
+ hishel/_core/_async/_storages/_sqlite.py,sha256=wIO0UaFzal9qoVqDVczzcsW0kGUjBQD-ikauc_MN414,14704
13
+ hishel/_core/_base/_storages/_base.py,sha256=xLJGTBlFK8DVrQMgRMtGXJnYRUmNB-iYkk7S-BtMx8s,8516
14
+ hishel/_core/_base/_storages/_packing.py,sha256=NFMpSvYYTDBNkzwpjj5l4w-JOPLc19oAEDqDEQJ7VZI,4873
15
+ hishel/_core/_sync/_storages/_sqlite.py,sha256=TDm9jXIWtd54m4_8AiVApxZVmbBoeFVi3E6s-vGzDjs,14138
16
+ hishel-1.0.0.dev0.dist-info/METADATA,sha256=EpqEHRIGfzVXqMiRefCa_NZ9AlbjzVToXfnK-GBrs9o,9993
17
+ hishel-1.0.0.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ hishel-1.0.0.dev0.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
19
+ hishel-1.0.0.dev0.dist-info/RECORD,,
hishel/_async/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- from ._client import * # noqa: F403
2
- from ._mock import * # noqa: F403
3
- from ._pool import * # noqa: F403
4
- from ._storages import * # noqa: F403
5
- from ._transports import * # noqa: F403
hishel/_async/_client.py DELETED
@@ -1,30 +0,0 @@
1
- import typing as tp
2
-
3
- import httpx
4
-
5
- from ._transports import AsyncCacheTransport
6
-
7
- __all__ = ("AsyncCacheClient",)
8
-
9
-
10
- class AsyncCacheClient(httpx.AsyncClient):
11
- def __init__(self, *args: tp.Any, **kwargs: tp.Any):
12
- self._storage = kwargs.pop("storage") if "storage" in kwargs else None
13
- self._controller = kwargs.pop("controller") if "controller" in kwargs else None
14
- super().__init__(*args, **kwargs)
15
-
16
- def _init_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
17
- _transport = super()._init_transport(*args, **kwargs)
18
- return AsyncCacheTransport(
19
- transport=_transport,
20
- storage=self._storage,
21
- controller=self._controller,
22
- )
23
-
24
- def _init_proxy_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
25
- _transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover
26
- return AsyncCacheTransport( # pragma: no cover
27
- transport=_transport,
28
- storage=self._storage,
29
- controller=self._controller,
30
- )
hishel/_async/_mock.py DELETED
@@ -1,43 +0,0 @@
1
- import typing as tp
2
- from types import TracebackType
3
-
4
- import httpcore
5
- import httpx
6
- from httpcore._async.interfaces import AsyncRequestInterface
7
-
8
- if tp.TYPE_CHECKING: # pragma: no cover
9
- from typing_extensions import Self
10
-
11
- __all__ = ("MockAsyncConnectionPool", "MockAsyncTransport")
12
-
13
-
14
- class MockAsyncConnectionPool(AsyncRequestInterface):
15
- async def handle_async_request(self, request: httpcore.Request) -> httpcore.Response:
16
- assert isinstance(request.stream, tp.AsyncIterable)
17
- data = b"".join([chunk async for chunk in request.stream]) # noqa: F841
18
- return self.mocked_responses.pop(0)
19
-
20
- def add_responses(self, responses: tp.List[httpcore.Response]) -> None:
21
- if not hasattr(self, "mocked_responses"):
22
- self.mocked_responses = []
23
- self.mocked_responses.extend(responses)
24
-
25
- async def __aenter__(self) -> "Self":
26
- return self
27
-
28
- async def __aexit__(
29
- self,
30
- exc_type: tp.Optional[tp.Type[BaseException]] = None,
31
- exc_value: tp.Optional[BaseException] = None,
32
- traceback: tp.Optional[TracebackType] = None,
33
- ) -> None: ...
34
-
35
-
36
- class MockAsyncTransport(httpx.AsyncBaseTransport):
37
- async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
38
- return self.mocked_responses.pop(0)
39
-
40
- def add_responses(self, responses: tp.List[httpx.Response]) -> None:
41
- if not hasattr(self, "mocked_responses"):
42
- self.mocked_responses = []
43
- self.mocked_responses.extend(responses)
hishel/_async/_pool.py DELETED
@@ -1,201 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import types
4
- import typing as tp
5
-
6
- from httpcore._async.interfaces import AsyncRequestInterface
7
- from httpcore._exceptions import ConnectError
8
- from httpcore._models import Request, Response
9
-
10
- from .._controller import Controller, allowed_stale
11
- from .._headers import parse_cache_control
12
- from .._serializers import JSONSerializer, Metadata
13
- from .._utils import extract_header_values_decoded
14
- from ._storages import AsyncBaseStorage, AsyncFileStorage
15
-
16
- T = tp.TypeVar("T")
17
-
18
- __all__ = ("AsyncCacheConnectionPool",)
19
-
20
-
21
- async def fake_stream(content: bytes) -> tp.AsyncIterable[bytes]:
22
- yield content
23
-
24
-
25
- def generate_504() -> Response:
26
- return Response(status=504)
27
-
28
-
29
- class AsyncCacheConnectionPool(AsyncRequestInterface):
30
- """An HTTP Core Connection Pool that supports HTTP caching.
31
-
32
- :param pool: `Connection Pool` that our class wraps in order to add an HTTP Cache layer on top of
33
- :type pool: AsyncRequestInterface
34
- :param storage: Storage that handles how the responses should be saved., defaults to None
35
- :type storage: tp.Optional[AsyncBaseStorage], optional
36
- :param controller: Controller that manages the cache behavior at the specification level, defaults to None
37
- :type controller: tp.Optional[Controller], optional
38
- """
39
-
40
- def __init__(
41
- self,
42
- pool: AsyncRequestInterface,
43
- storage: tp.Optional[AsyncBaseStorage] = None,
44
- controller: tp.Optional[Controller] = None,
45
- ) -> None:
46
- self._pool = pool
47
-
48
- self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer())
49
-
50
- if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover
51
- raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`")
52
-
53
- self._controller = controller if controller is not None else Controller()
54
-
55
- async def handle_async_request(self, request: Request) -> Response:
56
- """
57
- Handles HTTP requests while also implementing HTTP caching.
58
-
59
- :param request: An HTTP request
60
- :type request: httpcore.Request
61
- :return: An HTTP response
62
- :rtype: httpcore.Response
63
- """
64
-
65
- if request.extensions.get("cache_disabled", False):
66
- request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")])
67
-
68
- if request.method.upper() not in [b"GET", b"HEAD"]:
69
- # If the HTTP method is, for example, POST,
70
- # we must also use the request data to generate the hash.
71
- assert isinstance(request.stream, tp.AsyncIterable)
72
- body_for_key = b"".join([chunk async for chunk in request.stream])
73
- request.stream = fake_stream(body_for_key)
74
- else:
75
- body_for_key = b""
76
-
77
- key = self._controller._key_generator(request, body_for_key)
78
- stored_data = await self._storage.retrieve(key)
79
-
80
- request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control"))
81
-
82
- if request_cache_control.only_if_cached and not stored_data:
83
- return generate_504()
84
-
85
- if stored_data:
86
- # Try using the stored response if it was discovered.
87
-
88
- stored_response, stored_request, metadata = stored_data
89
-
90
- # Immediately read the stored response to avoid issues when trying to access the response body.
91
- stored_response.read()
92
-
93
- res = self._controller.construct_response_from_cache(
94
- request=request,
95
- response=stored_response,
96
- original_request=stored_request,
97
- )
98
-
99
- if isinstance(res, Response):
100
- # Simply use the response if the controller determines it is ready for use.
101
- return await self._create_hishel_response(
102
- key=key,
103
- response=stored_response,
104
- request=request,
105
- metadata=metadata,
106
- cached=True,
107
- revalidated=False,
108
- )
109
-
110
- if request_cache_control.only_if_cached:
111
- return generate_504()
112
-
113
- if isinstance(res, Request):
114
- # Controller has determined that the response needs to be re-validated.
115
-
116
- try:
117
- revalidation_response = await self._pool.handle_async_request(res)
118
- except ConnectError:
119
- # If there is a connection error, we can use the stale response if allowed.
120
- if self._controller._allow_stale and allowed_stale(response=stored_response):
121
- return await self._create_hishel_response(
122
- key=key,
123
- response=stored_response,
124
- request=request,
125
- metadata=metadata,
126
- cached=True,
127
- revalidated=False,
128
- )
129
- raise # pragma: no cover
130
- # Merge headers with the stale response.
131
- final_response = self._controller.handle_validation_response(
132
- old_response=stored_response, new_response=revalidation_response
133
- )
134
-
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
-
147
- return await self._create_hishel_response(
148
- key=key,
149
- response=final_response,
150
- request=request,
151
- cached=revalidation_response.status == 304,
152
- revalidated=True,
153
- metadata=metadata,
154
- )
155
-
156
- regular_response = await self._pool.handle_async_request(request)
157
- await regular_response.aread()
158
-
159
- if self._controller.is_cachable(request=request, response=regular_response):
160
- await self._storage.store(key, response=regular_response, request=request)
161
-
162
- return await self._create_hishel_response(
163
- key=key, response=regular_response, request=request, cached=False, revalidated=False
164
- )
165
-
166
- async def _create_hishel_response(
167
- self,
168
- key: str,
169
- response: Response,
170
- request: Request,
171
- cached: bool,
172
- revalidated: bool,
173
- metadata: Metadata | None = None,
174
- ) -> Response:
175
- if cached:
176
- assert metadata
177
- metadata["number_of_uses"] += 1
178
- await self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
179
- response.extensions["from_cache"] = True # type: ignore[index]
180
- response.extensions["cache_metadata"] = metadata # type: ignore[index]
181
- else:
182
- response.extensions["from_cache"] = False # type: ignore[index]
183
- response.extensions["revalidated"] = revalidated # type: ignore[index]
184
- return response
185
-
186
- async def aclose(self) -> None:
187
- await self._storage.aclose()
188
-
189
- if hasattr(self._pool, "aclose"): # pragma: no cover
190
- await self._pool.aclose()
191
-
192
- async def __aenter__(self: T) -> T:
193
- return self
194
-
195
- async def __aexit__(
196
- self,
197
- exc_type: tp.Optional[tp.Type[BaseException]] = None,
198
- exc_value: tp.Optional[BaseException] = None,
199
- traceback: tp.Optional[types.TracebackType] = None,
200
- ) -> None:
201
- await self.aclose()