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.
- hishel/__init__.py +55 -53
- hishel/{beta/_async_cache.py → _async_cache.py} +3 -3
- hishel/{beta → _core}/__init__.py +6 -6
- hishel/{beta/_core → _core}/_async/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/_base/_storages/_base.py +1 -1
- hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
- hishel/{beta/_core → _core}/_spec.py +89 -2
- hishel/{beta/_core → _core}/_sync/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/models.py +1 -1
- hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
- hishel/{beta/httpx.py → httpx.py} +6 -6
- hishel/{beta/requests.py → requests.py} +5 -5
- hishel-1.0.0.dev0.dist-info/METADATA +321 -0
- hishel-1.0.0.dev0.dist-info/RECORD +19 -0
- hishel/_async/__init__.py +0 -5
- hishel/_async/_client.py +0 -30
- hishel/_async/_mock.py +0 -43
- hishel/_async/_pool.py +0 -201
- hishel/_async/_storages.py +0 -768
- hishel/_async/_transports.py +0 -282
- hishel/_controller.py +0 -581
- hishel/_exceptions.py +0 -10
- hishel/_files.py +0 -54
- hishel/_headers.py +0 -215
- hishel/_lfu_cache.py +0 -71
- hishel/_lmdb_types_.pyi +0 -53
- hishel/_s3.py +0 -122
- hishel/_serializers.py +0 -329
- hishel/_sync/__init__.py +0 -5
- hishel/_sync/_client.py +0 -30
- hishel/_sync/_mock.py +0 -43
- hishel/_sync/_pool.py +0 -201
- hishel/_sync/_storages.py +0 -768
- hishel/_sync/_transports.py +0 -282
- hishel/_synchronization.py +0 -37
- hishel/beta/_core/__init__.py +0 -0
- hishel-0.1.5.dist-info/METADATA +0 -258
- hishel-0.1.5.dist-info/RECORD +0 -41
- /hishel/{beta/_core → _core}/_headers.py +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/WHEEL +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
hishel/_async/_transports.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
import typing as tp
|
|
5
|
-
|
|
6
|
-
import httpcore
|
|
7
|
-
import httpx
|
|
8
|
-
from httpx import AsyncByteStream, Request, Response
|
|
9
|
-
from httpx._exceptions import ConnectError
|
|
10
|
-
|
|
11
|
-
from .._controller import Controller, allowed_stale
|
|
12
|
-
from .._headers import parse_cache_control
|
|
13
|
-
from .._serializers import JSONSerializer, Metadata
|
|
14
|
-
from .._utils import extract_header_values_decoded, normalized_url
|
|
15
|
-
from ._storages import AsyncBaseStorage, AsyncFileStorage
|
|
16
|
-
|
|
17
|
-
if tp.TYPE_CHECKING: # pragma: no cover
|
|
18
|
-
from typing_extensions import Self
|
|
19
|
-
|
|
20
|
-
__all__ = ("AsyncCacheTransport",)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async def fake_stream(content: bytes) -> tp.AsyncIterable[bytes]:
|
|
24
|
-
yield content
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def generate_504() -> Response:
|
|
28
|
-
return Response(status_code=504)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class AsyncCacheStream(AsyncByteStream):
|
|
32
|
-
def __init__(self, httpcore_stream: tp.AsyncIterable[bytes]):
|
|
33
|
-
self._httpcore_stream = httpcore_stream
|
|
34
|
-
|
|
35
|
-
async def __aiter__(self) -> tp.AsyncIterator[bytes]:
|
|
36
|
-
async for part in self._httpcore_stream:
|
|
37
|
-
yield part
|
|
38
|
-
|
|
39
|
-
async def aclose(self) -> None:
|
|
40
|
-
if hasattr(self._httpcore_stream, "aclose"):
|
|
41
|
-
await self._httpcore_stream.aclose()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
45
|
-
"""
|
|
46
|
-
An HTTPX Transport that supports HTTP caching.
|
|
47
|
-
|
|
48
|
-
:param transport: `Transport` that our class wraps in order to add an HTTP Cache layer on top of
|
|
49
|
-
:type transport: httpx.AsyncBaseTransport
|
|
50
|
-
:param storage: Storage that handles how the responses should be saved., defaults to None
|
|
51
|
-
:type storage: tp.Optional[AsyncBaseStorage], optional
|
|
52
|
-
:param controller: Controller that manages the cache behavior at the specification level, defaults to None
|
|
53
|
-
:type controller: tp.Optional[Controller], optional
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(
|
|
57
|
-
self,
|
|
58
|
-
transport: httpx.AsyncBaseTransport,
|
|
59
|
-
storage: tp.Optional[AsyncBaseStorage] = None,
|
|
60
|
-
controller: tp.Optional[Controller] = None,
|
|
61
|
-
) -> None:
|
|
62
|
-
self._transport = transport
|
|
63
|
-
|
|
64
|
-
self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer())
|
|
65
|
-
|
|
66
|
-
if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover
|
|
67
|
-
raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`")
|
|
68
|
-
|
|
69
|
-
self._controller = controller if controller is not None else Controller()
|
|
70
|
-
|
|
71
|
-
async def handle_async_request(self, request: Request) -> Response:
|
|
72
|
-
"""
|
|
73
|
-
Handles HTTP requests while also implementing HTTP caching.
|
|
74
|
-
|
|
75
|
-
:param request: An HTTP request
|
|
76
|
-
:type request: httpx.Request
|
|
77
|
-
:return: An HTTP response
|
|
78
|
-
:rtype: httpx.Response
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
if request.extensions.get("cache_disabled", False):
|
|
82
|
-
request.headers.update(
|
|
83
|
-
[
|
|
84
|
-
("Cache-Control", "no-store"),
|
|
85
|
-
("Cache-Control", "no-cache"),
|
|
86
|
-
*[("cache-control", value) for value in request.headers.get_list("cache-control")],
|
|
87
|
-
]
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if request.method not in ["GET", "HEAD"]:
|
|
91
|
-
# If the HTTP method is, for example, POST,
|
|
92
|
-
# we must also use the request data to generate the hash.
|
|
93
|
-
body_for_key = await request.aread()
|
|
94
|
-
request.stream = AsyncCacheStream(fake_stream(body_for_key))
|
|
95
|
-
else:
|
|
96
|
-
body_for_key = b""
|
|
97
|
-
|
|
98
|
-
# Construct the HTTPCore request because Controllers and Storages work with HTTPCore requests.
|
|
99
|
-
httpcore_request = httpcore.Request(
|
|
100
|
-
method=request.method,
|
|
101
|
-
url=httpcore.URL(
|
|
102
|
-
scheme=request.url.raw_scheme,
|
|
103
|
-
host=request.url.raw_host,
|
|
104
|
-
port=request.url.port,
|
|
105
|
-
target=request.url.raw_path,
|
|
106
|
-
),
|
|
107
|
-
headers=request.headers.raw,
|
|
108
|
-
content=request.stream,
|
|
109
|
-
extensions=request.extensions,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
key = self._controller._key_generator(httpcore_request, body_for_key)
|
|
113
|
-
stored_data = await self._storage.retrieve(key)
|
|
114
|
-
|
|
115
|
-
request_cache_control = parse_cache_control(
|
|
116
|
-
extract_header_values_decoded(request.headers.raw, b"Cache-Control")
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
if request_cache_control.only_if_cached and not stored_data:
|
|
120
|
-
return generate_504()
|
|
121
|
-
|
|
122
|
-
if stored_data:
|
|
123
|
-
# Try using the stored response if it was discovered.
|
|
124
|
-
|
|
125
|
-
stored_response, stored_request, metadata = stored_data
|
|
126
|
-
|
|
127
|
-
# Immediately read the stored response to avoid issues when trying to access the response body.
|
|
128
|
-
stored_response.read()
|
|
129
|
-
|
|
130
|
-
res = self._controller.construct_response_from_cache(
|
|
131
|
-
request=httpcore_request,
|
|
132
|
-
response=stored_response,
|
|
133
|
-
original_request=stored_request,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if isinstance(res, httpcore.Response):
|
|
137
|
-
# Simply use the response if the controller determines it is ready for use.
|
|
138
|
-
return await self._create_hishel_response(
|
|
139
|
-
key=key,
|
|
140
|
-
response=res,
|
|
141
|
-
request=httpcore_request,
|
|
142
|
-
cached=True,
|
|
143
|
-
revalidated=False,
|
|
144
|
-
metadata=metadata,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
if request_cache_control.only_if_cached:
|
|
148
|
-
return generate_504()
|
|
149
|
-
|
|
150
|
-
if isinstance(res, httpcore.Request):
|
|
151
|
-
# Controller has determined that the response needs to be re-validated.
|
|
152
|
-
assert isinstance(res.stream, tp.AsyncIterable)
|
|
153
|
-
revalidation_request = Request(
|
|
154
|
-
method=res.method.decode(),
|
|
155
|
-
url=normalized_url(res.url),
|
|
156
|
-
headers=res.headers,
|
|
157
|
-
stream=AsyncCacheStream(res.stream),
|
|
158
|
-
extensions=res.extensions,
|
|
159
|
-
)
|
|
160
|
-
try:
|
|
161
|
-
revalidation_response = await self._transport.handle_async_request(revalidation_request)
|
|
162
|
-
except ConnectError:
|
|
163
|
-
# If there is a connection error, we can use the stale response if allowed.
|
|
164
|
-
if self._controller._allow_stale and allowed_stale(response=stored_response):
|
|
165
|
-
return await self._create_hishel_response(
|
|
166
|
-
key=key,
|
|
167
|
-
response=stored_response,
|
|
168
|
-
request=httpcore_request,
|
|
169
|
-
cached=True,
|
|
170
|
-
revalidated=False,
|
|
171
|
-
metadata=metadata,
|
|
172
|
-
)
|
|
173
|
-
raise # pragma: no cover
|
|
174
|
-
assert isinstance(revalidation_response.stream, tp.AsyncIterable)
|
|
175
|
-
httpcore_revalidation_response = httpcore.Response(
|
|
176
|
-
status=revalidation_response.status_code,
|
|
177
|
-
headers=revalidation_response.headers.raw,
|
|
178
|
-
content=AsyncCacheStream(revalidation_response.stream),
|
|
179
|
-
extensions=revalidation_response.extensions,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Merge headers with the stale response.
|
|
183
|
-
final_httpcore_response = self._controller.handle_validation_response(
|
|
184
|
-
old_response=stored_response,
|
|
185
|
-
new_response=httpcore_revalidation_response,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
await final_httpcore_response.aread()
|
|
189
|
-
await revalidation_response.aclose()
|
|
190
|
-
|
|
191
|
-
assert isinstance(final_httpcore_response.stream, tp.AsyncIterable)
|
|
192
|
-
|
|
193
|
-
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
194
|
-
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
195
|
-
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
196
|
-
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
197
|
-
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
198
|
-
if revalidation_response.status_code != 304 and self._controller.is_cachable(
|
|
199
|
-
request=httpcore_request, response=final_httpcore_response
|
|
200
|
-
):
|
|
201
|
-
await self._storage.store(key, response=final_httpcore_response, request=httpcore_request)
|
|
202
|
-
|
|
203
|
-
return await self._create_hishel_response(
|
|
204
|
-
key=key,
|
|
205
|
-
response=final_httpcore_response,
|
|
206
|
-
request=httpcore_request,
|
|
207
|
-
cached=revalidation_response.status_code == 304,
|
|
208
|
-
revalidated=True,
|
|
209
|
-
metadata=metadata,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
regular_response = await self._transport.handle_async_request(request)
|
|
213
|
-
try:
|
|
214
|
-
# Prefer already-read content, if available
|
|
215
|
-
stream = fake_stream(regular_response.content)
|
|
216
|
-
except httpx.ResponseNotRead:
|
|
217
|
-
# Fall back to stream if not yet read
|
|
218
|
-
assert isinstance(regular_response.stream, tp.AsyncIterable)
|
|
219
|
-
stream = regular_response.stream
|
|
220
|
-
httpcore_regular_response = httpcore.Response(
|
|
221
|
-
status=regular_response.status_code,
|
|
222
|
-
headers=regular_response.headers.raw,
|
|
223
|
-
content=AsyncCacheStream(stream),
|
|
224
|
-
extensions=regular_response.extensions,
|
|
225
|
-
)
|
|
226
|
-
await httpcore_regular_response.aread()
|
|
227
|
-
await httpcore_regular_response.aclose()
|
|
228
|
-
|
|
229
|
-
if self._controller.is_cachable(request=httpcore_request, response=httpcore_regular_response):
|
|
230
|
-
await self._storage.store(
|
|
231
|
-
key,
|
|
232
|
-
response=httpcore_regular_response,
|
|
233
|
-
request=httpcore_request,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
return await self._create_hishel_response(
|
|
237
|
-
key=key,
|
|
238
|
-
response=httpcore_regular_response,
|
|
239
|
-
request=httpcore_request,
|
|
240
|
-
cached=False,
|
|
241
|
-
revalidated=False,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
async def _create_hishel_response(
|
|
245
|
-
self,
|
|
246
|
-
key: str,
|
|
247
|
-
response: httpcore.Response,
|
|
248
|
-
request: httpcore.Request,
|
|
249
|
-
cached: bool,
|
|
250
|
-
revalidated: bool,
|
|
251
|
-
metadata: Metadata | None = None,
|
|
252
|
-
) -> Response:
|
|
253
|
-
if cached:
|
|
254
|
-
assert metadata
|
|
255
|
-
metadata["number_of_uses"] += 1
|
|
256
|
-
await self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
|
|
257
|
-
response.extensions["from_cache"] = True # type: ignore[index]
|
|
258
|
-
response.extensions["cache_metadata"] = metadata # type: ignore[index]
|
|
259
|
-
else:
|
|
260
|
-
response.extensions["from_cache"] = False # type: ignore[index]
|
|
261
|
-
response.extensions["revalidated"] = revalidated # type: ignore[index]
|
|
262
|
-
return Response(
|
|
263
|
-
status_code=response.status,
|
|
264
|
-
headers=response.headers,
|
|
265
|
-
stream=AsyncCacheStream(fake_stream(response.content)),
|
|
266
|
-
extensions=response.extensions,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
async def aclose(self) -> None:
|
|
270
|
-
await self._storage.aclose()
|
|
271
|
-
await self._transport.aclose()
|
|
272
|
-
|
|
273
|
-
async def __aenter__(self) -> Self:
|
|
274
|
-
return self
|
|
275
|
-
|
|
276
|
-
async def __aexit__(
|
|
277
|
-
self,
|
|
278
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
279
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
280
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
281
|
-
) -> None:
|
|
282
|
-
await self.aclose()
|