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/_controller.py
DELETED
|
@@ -1,581 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import typing as tp
|
|
3
|
-
|
|
4
|
-
from httpcore import Request, Response
|
|
5
|
-
|
|
6
|
-
from ._headers import Vary, parse_cache_control
|
|
7
|
-
from ._utils import (
|
|
8
|
-
BaseClock,
|
|
9
|
-
Clock,
|
|
10
|
-
extract_header_values,
|
|
11
|
-
extract_header_values_decoded,
|
|
12
|
-
generate_key,
|
|
13
|
-
get_safe_url,
|
|
14
|
-
header_presents,
|
|
15
|
-
parse_date,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger("hishel.controller")
|
|
19
|
-
|
|
20
|
-
HEURISTICALLY_CACHEABLE_STATUS_CODES = (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501)
|
|
21
|
-
HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]
|
|
22
|
-
|
|
23
|
-
__all__ = ("HEURISTICALLY_CACHEABLE_STATUS_CODES", "Controller")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_updated_headers(
|
|
27
|
-
stored_response_headers: tp.List[tp.Tuple[bytes, bytes]],
|
|
28
|
-
new_response_headers: tp.List[tp.Tuple[bytes, bytes]],
|
|
29
|
-
) -> tp.List[tp.Tuple[bytes, bytes]]:
|
|
30
|
-
updated_headers = []
|
|
31
|
-
|
|
32
|
-
checked = set()
|
|
33
|
-
|
|
34
|
-
for key, value in stored_response_headers:
|
|
35
|
-
if key not in checked and key.lower() != b"content-length":
|
|
36
|
-
checked.add(key)
|
|
37
|
-
values = extract_header_values(new_response_headers, key)
|
|
38
|
-
|
|
39
|
-
if values:
|
|
40
|
-
updated_headers.extend([(key, value) for value in values])
|
|
41
|
-
else:
|
|
42
|
-
values = extract_header_values(stored_response_headers, key)
|
|
43
|
-
updated_headers.extend([(key, value) for value in values])
|
|
44
|
-
|
|
45
|
-
for key, value in new_response_headers:
|
|
46
|
-
if key not in checked and key.lower() != b"content-length":
|
|
47
|
-
values = extract_header_values(new_response_headers, key)
|
|
48
|
-
updated_headers.extend([(key, value) for value in values])
|
|
49
|
-
|
|
50
|
-
return updated_headers
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_freshness_lifetime(response: Response) -> tp.Optional[int]:
|
|
54
|
-
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
|
|
55
|
-
|
|
56
|
-
if response_cache_control.max_age is not None:
|
|
57
|
-
return response_cache_control.max_age
|
|
58
|
-
|
|
59
|
-
if header_presents(response.headers, b"expires"):
|
|
60
|
-
expires = extract_header_values_decoded(response.headers, b"expires", single=True)[0]
|
|
61
|
-
expires_timestamp = parse_date(expires)
|
|
62
|
-
if expires_timestamp is None:
|
|
63
|
-
return None
|
|
64
|
-
date = extract_header_values_decoded(response.headers, b"date", single=True)[0]
|
|
65
|
-
date_timestamp = parse_date(date)
|
|
66
|
-
if date_timestamp is None:
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
return expires_timestamp - date_timestamp
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
|
|
74
|
-
last_modified = extract_header_values_decoded(response.headers, b"last-modified", single=True)
|
|
75
|
-
|
|
76
|
-
if last_modified:
|
|
77
|
-
last_modified_timestamp = parse_date(last_modified[0])
|
|
78
|
-
if last_modified_timestamp is not None:
|
|
79
|
-
now = clock.now()
|
|
80
|
-
|
|
81
|
-
ONE_WEEK = 604_800
|
|
82
|
-
|
|
83
|
-
return min(ONE_WEEK, int((now - last_modified_timestamp) * 0.1))
|
|
84
|
-
|
|
85
|
-
ONE_DAY = 86_400
|
|
86
|
-
return ONE_DAY
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_age(response: Response, clock: "BaseClock") -> int:
|
|
90
|
-
if not header_presents(response.headers, b"date"):
|
|
91
|
-
# If the response does not have a date header, then it is impossible to calculate the age.
|
|
92
|
-
# Instead of raising an exception, we return infinity to be sure that the response is not considered fresh.
|
|
93
|
-
return float("inf") # type: ignore
|
|
94
|
-
|
|
95
|
-
date = parse_date(extract_header_values_decoded(response.headers, b"date")[0])
|
|
96
|
-
if date is None:
|
|
97
|
-
return float("inf") # type: ignore
|
|
98
|
-
|
|
99
|
-
now = clock.now()
|
|
100
|
-
|
|
101
|
-
apparent_age = max(0, now - date)
|
|
102
|
-
return int(apparent_age)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def allowed_stale(response: Response) -> bool:
|
|
106
|
-
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
|
|
107
|
-
|
|
108
|
-
if response_cache_control.no_cache:
|
|
109
|
-
return False
|
|
110
|
-
|
|
111
|
-
if response_cache_control.must_revalidate:
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
return True
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class Controller:
|
|
118
|
-
def __init__(
|
|
119
|
-
self,
|
|
120
|
-
cacheable_methods: tp.Optional[tp.List[str]] = None,
|
|
121
|
-
cacheable_status_codes: tp.Optional[tp.List[int]] = None,
|
|
122
|
-
cache_private: bool = True,
|
|
123
|
-
allow_heuristics: bool = False,
|
|
124
|
-
clock: tp.Optional[BaseClock] = None,
|
|
125
|
-
allow_stale: bool = False,
|
|
126
|
-
always_revalidate: bool = False,
|
|
127
|
-
force_cache: bool = False,
|
|
128
|
-
key_generator: tp.Optional[tp.Callable[[Request, tp.Optional[bytes]], str]] = None,
|
|
129
|
-
):
|
|
130
|
-
self._cacheable_methods = []
|
|
131
|
-
|
|
132
|
-
if cacheable_methods is None:
|
|
133
|
-
self._cacheable_methods.append("GET")
|
|
134
|
-
else:
|
|
135
|
-
for method in cacheable_methods:
|
|
136
|
-
if method.upper() not in HTTP_METHODS:
|
|
137
|
-
raise RuntimeError(
|
|
138
|
-
f"Hishel does not support the HTTP method `{method}`.\n"
|
|
139
|
-
f"Please use the methods from this list: {HTTP_METHODS}"
|
|
140
|
-
)
|
|
141
|
-
self._cacheable_methods.append(method.upper())
|
|
142
|
-
|
|
143
|
-
self._cacheable_status_codes = cacheable_status_codes if cacheable_status_codes else [200, 301, 308]
|
|
144
|
-
self._cache_private = cache_private
|
|
145
|
-
self._clock = clock if clock else Clock()
|
|
146
|
-
self._allow_heuristics = allow_heuristics
|
|
147
|
-
self._allow_stale = allow_stale
|
|
148
|
-
self._always_revalidate = always_revalidate
|
|
149
|
-
self._force_cache = force_cache
|
|
150
|
-
self._key_generator = key_generator or generate_key
|
|
151
|
-
|
|
152
|
-
def is_cachable(self, request: Request, response: Response) -> bool:
|
|
153
|
-
"""
|
|
154
|
-
Determines whether the response may be cached.
|
|
155
|
-
|
|
156
|
-
The only thing this method does is determine whether the
|
|
157
|
-
response associated with this request can be cached for later use.
|
|
158
|
-
`https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-in-caches`
|
|
159
|
-
lists the steps that this method simply follows.
|
|
160
|
-
"""
|
|
161
|
-
method = request.method.decode("ascii")
|
|
162
|
-
force_cache = request.extensions.get("force_cache", None)
|
|
163
|
-
|
|
164
|
-
if response.status not in self._cacheable_status_codes:
|
|
165
|
-
logger.debug(
|
|
166
|
-
(
|
|
167
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
168
|
-
f"as not cachable since its status code ({response.status})"
|
|
169
|
-
" is not in the list of cacheable status codes."
|
|
170
|
-
)
|
|
171
|
-
)
|
|
172
|
-
return False
|
|
173
|
-
|
|
174
|
-
if response.status in (301, 308):
|
|
175
|
-
logger.debug(
|
|
176
|
-
(
|
|
177
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
178
|
-
"as cachable since its status code is a permanent redirect."
|
|
179
|
-
)
|
|
180
|
-
)
|
|
181
|
-
return True
|
|
182
|
-
|
|
183
|
-
# the request method is understood by the cache
|
|
184
|
-
if method not in self._cacheable_methods:
|
|
185
|
-
logger.debug(
|
|
186
|
-
(
|
|
187
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
188
|
-
f"as not cachable since the request method ({method}) is not in the list of cacheable methods."
|
|
189
|
-
)
|
|
190
|
-
)
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
if force_cache if force_cache is not None else self._force_cache:
|
|
194
|
-
logger.debug(
|
|
195
|
-
(
|
|
196
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
197
|
-
"as cachable since the request is forced to use the cache."
|
|
198
|
-
)
|
|
199
|
-
)
|
|
200
|
-
return True
|
|
201
|
-
|
|
202
|
-
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control"))
|
|
203
|
-
request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"cache-control"))
|
|
204
|
-
|
|
205
|
-
# the response status code is final
|
|
206
|
-
if response.status // 100 == 1:
|
|
207
|
-
logger.debug(
|
|
208
|
-
(
|
|
209
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
210
|
-
"as not cachable since its status code is informational."
|
|
211
|
-
)
|
|
212
|
-
)
|
|
213
|
-
return False
|
|
214
|
-
|
|
215
|
-
# the no-store cache directive is not present (see Section 5.2.2.5)
|
|
216
|
-
if request_cache_control.no_store:
|
|
217
|
-
logger.debug(
|
|
218
|
-
(
|
|
219
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
220
|
-
"as not cachable since the request contains the no-store directive."
|
|
221
|
-
)
|
|
222
|
-
)
|
|
223
|
-
return False
|
|
224
|
-
|
|
225
|
-
# note that the must-understand cache directive overrides
|
|
226
|
-
# no-store in certain circumstances; see Section 5.2.2.3.
|
|
227
|
-
if response_cache_control.no_store:
|
|
228
|
-
if response_cache_control.must_understand:
|
|
229
|
-
logger.debug(
|
|
230
|
-
(
|
|
231
|
-
f"Skipping the no-store directive for the resource located at {get_safe_url(request.url)} "
|
|
232
|
-
"since the response contains the must-understand directive."
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
else:
|
|
236
|
-
logger.debug(
|
|
237
|
-
(
|
|
238
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
239
|
-
"as not cachable since the response contains the no-store directive."
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
return False
|
|
243
|
-
|
|
244
|
-
# a shared cache must not store a response with private directive
|
|
245
|
-
# Note that we do not implement special handling for the qualified form,
|
|
246
|
-
# which would only forbid storing specified headers.
|
|
247
|
-
if not self._cache_private and response_cache_control.private:
|
|
248
|
-
logger.debug(
|
|
249
|
-
(
|
|
250
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
251
|
-
"as not cachable since the response contains the private directive."
|
|
252
|
-
)
|
|
253
|
-
)
|
|
254
|
-
return False
|
|
255
|
-
|
|
256
|
-
expires_presents = header_presents(response.headers, b"expires")
|
|
257
|
-
# the response contains at least one of the following:
|
|
258
|
-
# - a public response directive (see Section 5.2.2.9);
|
|
259
|
-
# - a private response directive, if the cache is not shared (see Section 5.2.2.7);
|
|
260
|
-
# - an Expires header field (see Section 5.3);
|
|
261
|
-
# - a max-age response directive (see Section 5.2.2.1);
|
|
262
|
-
# - if the cache is shared: an s-maxage response directive (see Section 5.2.2.10);
|
|
263
|
-
# - a cache extension that allows it to be cached (see Section 5.2.3); or
|
|
264
|
-
# - a status code that is defined as heuristically cacheable (see Section 4.2.2).
|
|
265
|
-
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
|
|
266
|
-
logger.debug(
|
|
267
|
-
(
|
|
268
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
269
|
-
"as cachable since its status code is heuristically cacheable."
|
|
270
|
-
)
|
|
271
|
-
)
|
|
272
|
-
return True
|
|
273
|
-
|
|
274
|
-
if not any(
|
|
275
|
-
[
|
|
276
|
-
response_cache_control.public,
|
|
277
|
-
response_cache_control.private,
|
|
278
|
-
expires_presents,
|
|
279
|
-
response_cache_control.max_age is not None,
|
|
280
|
-
]
|
|
281
|
-
):
|
|
282
|
-
logger.debug(
|
|
283
|
-
(
|
|
284
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
285
|
-
"as not cachable since it does not contain any of the required cache directives."
|
|
286
|
-
)
|
|
287
|
-
)
|
|
288
|
-
return False
|
|
289
|
-
|
|
290
|
-
logger.debug(
|
|
291
|
-
(
|
|
292
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
293
|
-
"as cachable since it meets the criteria for being stored in the cache."
|
|
294
|
-
)
|
|
295
|
-
)
|
|
296
|
-
# response is a cachable!
|
|
297
|
-
return True
|
|
298
|
-
|
|
299
|
-
def _make_request_conditional(self, request: Request, response: Response) -> None:
|
|
300
|
-
"""
|
|
301
|
-
Adds the precondition headers needed for response validation.
|
|
302
|
-
|
|
303
|
-
This method will use the "Last-Modified" or "Etag" headers
|
|
304
|
-
if they are provided in order to create precondition headers.
|
|
305
|
-
|
|
306
|
-
See also (https://www.rfc-editor.org/rfc/rfc9111.html#name-sending-a-validation-reques)
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
if header_presents(response.headers, b"last-modified"):
|
|
310
|
-
last_modified = extract_header_values(response.headers, b"last-modified", single=True)[0]
|
|
311
|
-
logger.debug(
|
|
312
|
-
(
|
|
313
|
-
f"Adding the 'If-Modified-Since' header with the value of '{last_modified.decode('ascii')}' "
|
|
314
|
-
f"to the request for the resource located at {get_safe_url(request.url)}."
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
else:
|
|
318
|
-
last_modified = None
|
|
319
|
-
|
|
320
|
-
if header_presents(response.headers, b"etag"):
|
|
321
|
-
etag = extract_header_values(response.headers, b"etag", single=True)[0]
|
|
322
|
-
logger.debug(
|
|
323
|
-
(
|
|
324
|
-
f"Adding the 'If-None-Match' header with the value of '{etag.decode('ascii')}' "
|
|
325
|
-
f"to the request for the resource located at {get_safe_url(request.url)}."
|
|
326
|
-
)
|
|
327
|
-
)
|
|
328
|
-
else:
|
|
329
|
-
etag = None
|
|
330
|
-
|
|
331
|
-
precondition_headers: tp.List[tp.Tuple[bytes, bytes]] = []
|
|
332
|
-
if last_modified:
|
|
333
|
-
precondition_headers.append((b"If-Modified-Since", last_modified))
|
|
334
|
-
if etag:
|
|
335
|
-
precondition_headers.append((b"If-None-Match", etag))
|
|
336
|
-
|
|
337
|
-
request.headers.extend(precondition_headers)
|
|
338
|
-
|
|
339
|
-
def _validate_vary(self, request: Request, response: Response, original_request: Request) -> bool:
|
|
340
|
-
"""
|
|
341
|
-
Determines whether the "vary" headers in the request and response headers are identical.
|
|
342
|
-
|
|
343
|
-
See also (https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with).
|
|
344
|
-
"""
|
|
345
|
-
|
|
346
|
-
vary_headers = extract_header_values_decoded(response.headers, b"vary")
|
|
347
|
-
vary = Vary.from_value(vary_values=vary_headers)
|
|
348
|
-
for vary_header in vary._values:
|
|
349
|
-
if vary_header == "*":
|
|
350
|
-
return False # pragma: no cover
|
|
351
|
-
|
|
352
|
-
if extract_header_values(request.headers, vary_header) != extract_header_values(
|
|
353
|
-
original_request.headers, vary_header
|
|
354
|
-
):
|
|
355
|
-
return False
|
|
356
|
-
|
|
357
|
-
return True
|
|
358
|
-
|
|
359
|
-
def construct_response_from_cache(
|
|
360
|
-
self, request: Request, response: Response, original_request: Request
|
|
361
|
-
) -> tp.Union[Response, Request, None]:
|
|
362
|
-
"""
|
|
363
|
-
Specifies whether the response should be used, skipped, or validated by the cache.
|
|
364
|
-
|
|
365
|
-
This method makes a decision regarding what to do with
|
|
366
|
-
the stored response when it is retrieved from storage.
|
|
367
|
-
It might be ready for use or it might need to be revalidated.
|
|
368
|
-
This method mirrors the relevant section from RFC 9111,
|
|
369
|
-
see (https://www.rfc-editor.org/rfc/rfc9111.html#name-constructing-responses-from).
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
Response: This response is applicable to the request.
|
|
373
|
-
Request: This response can be used for this request, but it must first be revalidated.
|
|
374
|
-
None: It is not possible to use this response for this request.
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
# Use of responses with status codes 301 and 308 is always
|
|
378
|
-
# legal as long as they don't adhere to any caching rules.
|
|
379
|
-
if response.status in (301, 308):
|
|
380
|
-
logger.debug(
|
|
381
|
-
(
|
|
382
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
383
|
-
"as valid for cache use since its status code is a permanent redirect."
|
|
384
|
-
)
|
|
385
|
-
)
|
|
386
|
-
return response
|
|
387
|
-
|
|
388
|
-
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
|
|
389
|
-
request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control"))
|
|
390
|
-
|
|
391
|
-
# request header fields nominated by the stored
|
|
392
|
-
# response (if any) match those presented (see Section 4.1)
|
|
393
|
-
if not self._validate_vary(request=request, response=response, original_request=original_request):
|
|
394
|
-
# If the vary headers does not match, then do not use the response
|
|
395
|
-
logger.debug(
|
|
396
|
-
(
|
|
397
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
398
|
-
"as invalid for cache use since the vary headers do not match."
|
|
399
|
-
)
|
|
400
|
-
)
|
|
401
|
-
return None # pragma: no cover
|
|
402
|
-
|
|
403
|
-
# !!! this should be after the "vary" header validation.
|
|
404
|
-
force_cache = request.extensions.get("force_cache", None)
|
|
405
|
-
if force_cache if force_cache is not None else self._force_cache:
|
|
406
|
-
logger.debug(
|
|
407
|
-
(
|
|
408
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
409
|
-
"as valid for cache use since the request is forced to use the cache."
|
|
410
|
-
)
|
|
411
|
-
)
|
|
412
|
-
return response
|
|
413
|
-
|
|
414
|
-
# the stored response does not contain the
|
|
415
|
-
# no-cache directive (Section 5.2.2.4), unless
|
|
416
|
-
# it is successfully validated (Section 4.3)
|
|
417
|
-
if (
|
|
418
|
-
self._always_revalidate
|
|
419
|
-
or response_cache_control.no_cache
|
|
420
|
-
or response_cache_control.must_revalidate
|
|
421
|
-
or request_cache_control.no_cache
|
|
422
|
-
):
|
|
423
|
-
if self._always_revalidate:
|
|
424
|
-
log_text = (
|
|
425
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
426
|
-
"as needing revalidation since the cache is set to always revalidate."
|
|
427
|
-
)
|
|
428
|
-
elif response_cache_control.no_cache:
|
|
429
|
-
log_text = (
|
|
430
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
431
|
-
"as needing revalidation since the response contains the no-cache directive."
|
|
432
|
-
)
|
|
433
|
-
elif response_cache_control.must_revalidate:
|
|
434
|
-
log_text = (
|
|
435
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
436
|
-
"as needing revalidation since the response contains the must-revalidate directive."
|
|
437
|
-
)
|
|
438
|
-
elif request_cache_control.no_cache:
|
|
439
|
-
log_text = (
|
|
440
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
441
|
-
"as needing revalidation since the request contains the no-cache directive."
|
|
442
|
-
)
|
|
443
|
-
else:
|
|
444
|
-
assert False, "Unreachable code " # pragma: no cover
|
|
445
|
-
logger.debug(log_text)
|
|
446
|
-
self._make_request_conditional(request=request, response=response)
|
|
447
|
-
return request
|
|
448
|
-
|
|
449
|
-
freshness_lifetime = get_freshness_lifetime(response)
|
|
450
|
-
|
|
451
|
-
if freshness_lifetime is None:
|
|
452
|
-
logger.debug(
|
|
453
|
-
(
|
|
454
|
-
"Could not determine the freshness lifetime of "
|
|
455
|
-
f"the resource located at {get_safe_url(request.url)}, "
|
|
456
|
-
"trying to use heuristics to calculate it."
|
|
457
|
-
)
|
|
458
|
-
)
|
|
459
|
-
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
|
|
460
|
-
freshness_lifetime = get_heuristic_freshness(response=response, clock=self._clock)
|
|
461
|
-
logger.debug(
|
|
462
|
-
(
|
|
463
|
-
f"Successfully calculated the freshness lifetime of the resource located at "
|
|
464
|
-
f"{get_safe_url(request.url)} using heuristics."
|
|
465
|
-
)
|
|
466
|
-
)
|
|
467
|
-
else:
|
|
468
|
-
logger.debug(
|
|
469
|
-
(
|
|
470
|
-
"Could not calculate the freshness lifetime of "
|
|
471
|
-
f"the resource located at {get_safe_url(request.url)}. "
|
|
472
|
-
"Making a conditional request to revalidate the response."
|
|
473
|
-
)
|
|
474
|
-
)
|
|
475
|
-
# If Freshness cannot be calculated, then send the request
|
|
476
|
-
self._make_request_conditional(request=request, response=response)
|
|
477
|
-
return request
|
|
478
|
-
|
|
479
|
-
age = get_age(response, self._clock)
|
|
480
|
-
is_fresh = freshness_lifetime > age
|
|
481
|
-
|
|
482
|
-
# The min-fresh request directive indicates that the client
|
|
483
|
-
# prefers a response whose freshness lifetime is no less than
|
|
484
|
-
# its current age plus the specified time in seconds.
|
|
485
|
-
# That is, the client wants a response that will still
|
|
486
|
-
# be fresh for at least the specified number of seconds.
|
|
487
|
-
if request_cache_control.min_fresh is not None:
|
|
488
|
-
if freshness_lifetime < (age + request_cache_control.min_fresh):
|
|
489
|
-
logger.debug(
|
|
490
|
-
(
|
|
491
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
492
|
-
"as invalid for cache use since the time left for "
|
|
493
|
-
"freshness is less than the min-fresh directive."
|
|
494
|
-
)
|
|
495
|
-
)
|
|
496
|
-
return None
|
|
497
|
-
|
|
498
|
-
# The max-stale request directive indicates that the
|
|
499
|
-
# client will accept a response that has exceeded its freshness lifetime.
|
|
500
|
-
# If a value is present, then the client is willing to accept a response
|
|
501
|
-
# that has exceeded its freshness lifetime by no more than the specified
|
|
502
|
-
# number of seconds. If no value is assigned to max-stale, then
|
|
503
|
-
# the client will accept a stale response of any age.
|
|
504
|
-
if not is_fresh and request_cache_control.max_stale is not None:
|
|
505
|
-
exceeded_freshness_lifetime = age - freshness_lifetime
|
|
506
|
-
|
|
507
|
-
if request_cache_control.max_stale < exceeded_freshness_lifetime:
|
|
508
|
-
logger.debug(
|
|
509
|
-
(
|
|
510
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
511
|
-
"as invalid for cache use since the freshness lifetime has been exceeded more than max-stale."
|
|
512
|
-
)
|
|
513
|
-
)
|
|
514
|
-
return None
|
|
515
|
-
else:
|
|
516
|
-
logger.debug(
|
|
517
|
-
(
|
|
518
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
519
|
-
"as valid for cache use since the freshness lifetime has been exceeded less than max-stale."
|
|
520
|
-
)
|
|
521
|
-
)
|
|
522
|
-
return response
|
|
523
|
-
|
|
524
|
-
# The max-age request directive indicates that
|
|
525
|
-
# the client prefers a response whose age is
|
|
526
|
-
# less than or equal to the specified number of seconds.
|
|
527
|
-
# Unless the max-stale request directive is also present,
|
|
528
|
-
# the client does not wish to receive a stale response.
|
|
529
|
-
if request_cache_control.max_age is not None:
|
|
530
|
-
if request_cache_control.max_age < age:
|
|
531
|
-
logger.debug(
|
|
532
|
-
(
|
|
533
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
534
|
-
"as invalid for cache use since the age of the response exceeds the max-age directive."
|
|
535
|
-
)
|
|
536
|
-
)
|
|
537
|
-
return None
|
|
538
|
-
|
|
539
|
-
# the stored response is one of the following:
|
|
540
|
-
# fresh (see Section 4.2), or
|
|
541
|
-
# allowed to be served stale (see Section 4.2.4), or
|
|
542
|
-
# successfully validated (see Section 4.3).
|
|
543
|
-
if is_fresh:
|
|
544
|
-
logger.debug(
|
|
545
|
-
(
|
|
546
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
547
|
-
"as valid for cache use since it is fresh."
|
|
548
|
-
)
|
|
549
|
-
)
|
|
550
|
-
return response
|
|
551
|
-
else:
|
|
552
|
-
logger.debug(
|
|
553
|
-
(
|
|
554
|
-
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
555
|
-
"as needing revalidation since it is not fresh."
|
|
556
|
-
)
|
|
557
|
-
)
|
|
558
|
-
# Otherwise, make a conditional request
|
|
559
|
-
self._make_request_conditional(request=request, response=response)
|
|
560
|
-
return request
|
|
561
|
-
|
|
562
|
-
def handle_validation_response(self, old_response: Response, new_response: Response) -> Response:
|
|
563
|
-
"""
|
|
564
|
-
Handles incoming validation response.
|
|
565
|
-
|
|
566
|
-
This method takes care of what to do with the incoming
|
|
567
|
-
validation response; if it is a 304 response, it updates
|
|
568
|
-
the headers with the new response and returns it.
|
|
569
|
-
|
|
570
|
-
This method mirrors the relevant section from RFC 9111,
|
|
571
|
-
see (https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo).
|
|
572
|
-
"""
|
|
573
|
-
if new_response.status == 304:
|
|
574
|
-
headers = get_updated_headers(
|
|
575
|
-
stored_response_headers=old_response.headers,
|
|
576
|
-
new_response_headers=new_response.headers,
|
|
577
|
-
)
|
|
578
|
-
old_response.headers = headers
|
|
579
|
-
return old_response
|
|
580
|
-
else:
|
|
581
|
-
return new_response
|
hishel/_exceptions.py
DELETED
hishel/_files.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
|
|
3
|
-
import anyio
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class AsyncBaseFileManager:
|
|
7
|
-
def __init__(self, is_binary: bool) -> None:
|
|
8
|
-
self.is_binary = is_binary
|
|
9
|
-
|
|
10
|
-
async def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None:
|
|
11
|
-
raise NotImplementedError()
|
|
12
|
-
|
|
13
|
-
async def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]:
|
|
14
|
-
raise NotImplementedError()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AsyncFileManager(AsyncBaseFileManager):
|
|
18
|
-
async def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None:
|
|
19
|
-
is_binary = self.is_binary if is_binary is None else is_binary
|
|
20
|
-
mode = "wb" if is_binary else "wt"
|
|
21
|
-
async with await anyio.open_file(path, mode) as f: # type: ignore[call-overload]
|
|
22
|
-
await f.write(data)
|
|
23
|
-
|
|
24
|
-
async def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]:
|
|
25
|
-
is_binary = self.is_binary if is_binary is None else is_binary
|
|
26
|
-
mode = "rb" if is_binary else "rt"
|
|
27
|
-
|
|
28
|
-
async with await anyio.open_file(path, mode) as f: # type: ignore[call-overload]
|
|
29
|
-
return tp.cast(tp.Union[bytes, str], await f.read())
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class BaseFileManager:
|
|
33
|
-
def __init__(self, is_binary: bool) -> None:
|
|
34
|
-
self.is_binary = is_binary
|
|
35
|
-
|
|
36
|
-
def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None:
|
|
37
|
-
raise NotImplementedError()
|
|
38
|
-
|
|
39
|
-
def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]:
|
|
40
|
-
raise NotImplementedError()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class FileManager(BaseFileManager):
|
|
44
|
-
def write_to(self, path: str, data: tp.Union[bytes, str], is_binary: tp.Optional[bool] = None) -> None:
|
|
45
|
-
is_binary = self.is_binary if is_binary is None else is_binary
|
|
46
|
-
mode = "wb" if is_binary else "wt"
|
|
47
|
-
with open(path, mode) as f:
|
|
48
|
-
f.write(data)
|
|
49
|
-
|
|
50
|
-
def read_from(self, path: str, is_binary: tp.Optional[bool] = None) -> tp.Union[bytes, str]:
|
|
51
|
-
is_binary = self.is_binary if is_binary is None else is_binary
|
|
52
|
-
mode = "rb" if is_binary else "rt"
|
|
53
|
-
with open(path, mode) as f:
|
|
54
|
-
return tp.cast(tp.Union[bytes, str], f.read())
|