hishel 0.0.31__py3-none-any.whl → 0.0.33__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 CHANGED
@@ -14,4 +14,4 @@ def install_cache() -> None: # pragma: no cover
14
14
  httpx.Client = CacheClient # type: ignore
15
15
 
16
16
 
17
- __version__ = "0.0.31"
17
+ __version__ = "0.0.33"
hishel/_controller.py CHANGED
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import typing as tp
2
3
 
3
4
  from httpcore import Request, Response
@@ -10,10 +11,13 @@ from ._utils import (
10
11
  extract_header_values,
11
12
  extract_header_values_decoded,
12
13
  generate_key,
14
+ get_safe_url,
13
15
  header_presents,
14
16
  parse_date,
15
17
  )
16
18
 
19
+ logger = logging.getLogger("hishel.controller")
20
+
17
21
  HEURISTICALLY_CACHEABLE_STATUS_CODES = (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501)
18
22
  HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]
19
23
 
@@ -79,8 +83,10 @@ def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
79
83
 
80
84
 
81
85
  def get_age(response: Response, clock: "BaseClock") -> int:
82
- if not header_presents(response.headers, b"date"): # pragma: no cover
83
- raise RuntimeError("The `Date` header is missing in the response.")
86
+ if not header_presents(response.headers, b"date"):
87
+ # If the response does not have a date header, then it is impossible to calculate the age.
88
+ # Instead of raising an exception, we return infinity to be sure that the response is not considered fresh.
89
+ return float("inf") # type: ignore
84
90
 
85
91
  date = parse_date(extract_header_values_decoded(response.headers, b"date")[0])
86
92
 
@@ -150,16 +156,41 @@ class Controller:
150
156
  force_cache = request.extensions.get("force_cache", None)
151
157
 
152
158
  if response.status not in self._cacheable_status_codes:
159
+ logger.debug(
160
+ (
161
+ f"Considering the resource located at {get_safe_url(request.url)} "
162
+ f"as not cachable since its status code ({response.status})"
163
+ " is not in the list of cacheable status codes."
164
+ )
165
+ )
153
166
  return False
154
167
 
155
168
  if response.status in (301, 308):
169
+ logger.debug(
170
+ (
171
+ f"Considering the resource located at {get_safe_url(request.url)} "
172
+ "as cachable since its status code is a permanent redirect."
173
+ )
174
+ )
156
175
  return True
157
176
 
158
177
  # the request method is understood by the cache
159
178
  if method not in self._cacheable_methods:
179
+ logger.debug(
180
+ (
181
+ f"Considering the resource located at {get_safe_url(request.url)} "
182
+ f"as not cachable since the request method ({method}) is not in the list of cacheable methods."
183
+ )
184
+ )
160
185
  return False
161
186
 
162
187
  if force_cache if force_cache is not None else self._force_cache:
188
+ logger.debug(
189
+ (
190
+ f"Considering the resource located at {get_safe_url(request.url)} "
191
+ "as cachable since the request is forced to use the cache."
192
+ )
193
+ )
163
194
  return True
164
195
 
165
196
  response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control"))
@@ -167,21 +198,53 @@ class Controller:
167
198
 
168
199
  # the response status code is final
169
200
  if response.status // 100 == 1:
201
+ logger.debug(
202
+ (
203
+ f"Considering the resource located at {get_safe_url(request.url)} "
204
+ "as not cachable since its status code is informational."
205
+ )
206
+ )
170
207
  return False
171
208
 
172
209
  # the no-store cache directive is not present (see Section 5.2.2.5)
173
210
  if request_cache_control.no_store:
211
+ logger.debug(
212
+ (
213
+ f"Considering the resource located at {get_safe_url(request.url)} "
214
+ "as not cachable since the request contains the no-store directive."
215
+ )
216
+ )
174
217
  return False
175
218
 
176
219
  # note that the must-understand cache directive overrides
177
220
  # no-store in certain circumstances; see Section 5.2.2.3.
178
- if response_cache_control.no_store and not response_cache_control.must_understand:
179
- return False
221
+ if response_cache_control.no_store:
222
+ if response_cache_control.must_understand:
223
+ logger.debug(
224
+ (
225
+ f"Skipping the no-store directive for the resource located at {get_safe_url(request.url)} "
226
+ "since the response contains the must-understand directive."
227
+ )
228
+ )
229
+ else:
230
+ logger.debug(
231
+ (
232
+ f"Considering the resource located at {get_safe_url(request.url)} "
233
+ "as not cachable since the response contains the no-store directive."
234
+ )
235
+ )
236
+ return False
180
237
 
181
238
  # a shared cache must not store a response with private directive
182
239
  # Note that we do not implement special handling for the qualified form,
183
240
  # which would only forbid storing specified headers.
184
241
  if not self._cache_private and response_cache_control.private:
242
+ logger.debug(
243
+ (
244
+ f"Considering the resource located at {get_safe_url(request.url)} "
245
+ "as not cachable since the response contains the private directive."
246
+ )
247
+ )
185
248
  return False
186
249
 
187
250
  expires_presents = header_presents(response.headers, b"expires")
@@ -194,6 +257,12 @@ class Controller:
194
257
  # - a cache extension that allows it to be cached (see Section 5.2.3); or
195
258
  # - a status code that is defined as heuristically cacheable (see Section 4.2.2).
196
259
  if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
260
+ logger.debug(
261
+ (
262
+ f"Considering the resource located at {get_safe_url(request.url)} "
263
+ "as cachable since its status code is heuristically cacheable."
264
+ )
265
+ )
197
266
  return True
198
267
 
199
268
  if not any(
@@ -204,8 +273,20 @@ class Controller:
204
273
  response_cache_control.max_age is not None,
205
274
  ]
206
275
  ):
276
+ logger.debug(
277
+ (
278
+ f"Considering the resource located at {get_safe_url(request.url)} "
279
+ "as not cachable since it does not contain any of the required cache directives."
280
+ )
281
+ )
207
282
  return False
208
283
 
284
+ logger.debug(
285
+ (
286
+ f"Considering the resource located at {get_safe_url(request.url)} "
287
+ "as cachable since it meets the criteria for being stored in the cache."
288
+ )
289
+ )
209
290
  # response is a cachable!
210
291
  return True
211
292
 
@@ -221,11 +302,23 @@ class Controller:
221
302
 
222
303
  if header_presents(response.headers, b"last-modified"):
223
304
  last_modified = extract_header_values(response.headers, b"last-modified", single=True)[0]
305
+ logger.debug(
306
+ (
307
+ f"Adding the 'If-Modified-Since' header with the value of '{last_modified.decode('ascii')}' "
308
+ f"to the request for the resource located at {get_safe_url(request.url)}."
309
+ )
310
+ )
224
311
  else:
225
312
  last_modified = None
226
313
 
227
314
  if header_presents(response.headers, b"etag"):
228
315
  etag = extract_header_values(response.headers, b"etag", single=True)[0]
316
+ logger.debug(
317
+ (
318
+ f"Adding the 'If-None-Match' header with the value of '{etag.decode('ascii')}' "
319
+ f"to the request for the resource located at {get_safe_url(request.url)}."
320
+ )
321
+ )
229
322
  else:
230
323
  etag = None
231
324
 
@@ -278,6 +371,12 @@ class Controller:
278
371
  # Use of responses with status codes 301 and 308 is always
279
372
  # legal as long as they don't adhere to any caching rules.
280
373
  if response.status in (301, 308):
374
+ logger.debug(
375
+ (
376
+ f"Considering the resource located at {get_safe_url(request.url)} "
377
+ "as valid for cache use since its status code is a permanent redirect."
378
+ )
379
+ )
281
380
  return response
282
381
 
283
382
  response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
@@ -287,11 +386,23 @@ class Controller:
287
386
  # response (if any) match those presented (see Section 4.1)
288
387
  if not self._validate_vary(request=request, response=response, original_request=original_request):
289
388
  # If the vary headers does not match, then do not use the response
389
+ logger.debug(
390
+ (
391
+ f"Considering the resource located at {get_safe_url(request.url)} "
392
+ "as invalid for cache use since the vary headers do not match."
393
+ )
394
+ )
290
395
  return None # pragma: no cover
291
396
 
292
397
  # !!! this should be after the "vary" header validation.
293
398
  force_cache = request.extensions.get("force_cache", None)
294
399
  if force_cache if force_cache is not None else self._force_cache:
400
+ logger.debug(
401
+ (
402
+ f"Considering the resource located at {get_safe_url(request.url)} "
403
+ "as valid for cache use since the request is forced to use the cache."
404
+ )
405
+ )
295
406
  return response
296
407
 
297
408
  # the stored response does not contain the
@@ -303,15 +414,58 @@ class Controller:
303
414
  or response_cache_control.must_revalidate
304
415
  or request_cache_control.no_cache
305
416
  ):
417
+ if self._always_revalidate:
418
+ log_text = (
419
+ f"Considering the resource located at {get_safe_url(request.url)} "
420
+ "as needing revalidation since the cache is set to always revalidate."
421
+ )
422
+ elif response_cache_control.no_cache:
423
+ log_text = (
424
+ f"Considering the resource located at {get_safe_url(request.url)} "
425
+ "as needing revalidation since the response contains the no-cache directive."
426
+ )
427
+ elif response_cache_control.must_revalidate:
428
+ log_text = (
429
+ f"Considering the resource located at {get_safe_url(request.url)} "
430
+ "as needing revalidation since the response contains the must-revalidate directive."
431
+ )
432
+ elif request_cache_control.no_cache:
433
+ log_text = (
434
+ f"Considering the resource located at {get_safe_url(request.url)} "
435
+ "as needing revalidation since the request contains the no-cache directive."
436
+ )
437
+ else:
438
+ assert False, "Unreachable code " # pragma: no cover
439
+ logger.debug(log_text)
306
440
  self._make_request_conditional(request=request, response=response)
307
441
  return request
308
442
 
309
443
  freshness_lifetime = get_freshness_lifetime(response)
310
444
 
311
445
  if freshness_lifetime is None:
446
+ logger.debug(
447
+ (
448
+ "Could not determine the freshness lifetime of "
449
+ f"the resource located at {get_safe_url(request.url)}, "
450
+ "trying to use heuristics to calculate it."
451
+ )
452
+ )
312
453
  if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
313
454
  freshness_lifetime = get_heuristic_freshness(response=response, clock=self._clock)
455
+ logger.debug(
456
+ (
457
+ f"Successfully calculated the freshness lifetime of the resource located at "
458
+ f"{get_safe_url(request.url)} using heuristics."
459
+ )
460
+ )
314
461
  else:
462
+ logger.debug(
463
+ (
464
+ "Could not calculate the freshness lifetime of "
465
+ f"the resource located at {get_safe_url(request.url)}. "
466
+ "Making a conditional request to revalidate the response."
467
+ )
468
+ )
315
469
  # If Freshness cannot be calculated, then send the request
316
470
  self._make_request_conditional(request=request, response=response)
317
471
  return request
@@ -326,6 +480,13 @@ class Controller:
326
480
  # be fresh for at least the specified number of seconds.
327
481
  if request_cache_control.min_fresh is not None:
328
482
  if freshness_lifetime < (age + request_cache_control.min_fresh):
483
+ logger.debug(
484
+ (
485
+ f"Considering the resource located at {get_safe_url(request.url)} "
486
+ "as invalid for cache use since the time left for "
487
+ "freshness is less than the min-fresh directive."
488
+ )
489
+ )
329
490
  return None
330
491
 
331
492
  # The max-stale request directive indicates that the
@@ -338,7 +499,21 @@ class Controller:
338
499
  exceeded_freshness_lifetime = age - freshness_lifetime
339
500
 
340
501
  if request_cache_control.max_stale < exceeded_freshness_lifetime:
502
+ logger.debug(
503
+ (
504
+ f"Considering the resource located at {get_safe_url(request.url)} "
505
+ "as invalid for cache use since the freshness lifetime has been exceeded more than max-stale."
506
+ )
507
+ )
341
508
  return None
509
+ else:
510
+ logger.debug(
511
+ (
512
+ f"Considering the resource located at {get_safe_url(request.url)} "
513
+ "as valid for cache use since the freshness lifetime has been exceeded less than max-stale."
514
+ )
515
+ )
516
+ return response
342
517
 
343
518
  # The max-age request directive indicates that
344
519
  # the client prefers a response whose age is
@@ -347,9 +522,12 @@ class Controller:
347
522
  # the client does not wish to receive a stale response.
348
523
  if request_cache_control.max_age is not None:
349
524
  if request_cache_control.max_age < age:
350
- return None
351
-
352
- if request_cache_control.max_stale is None and not is_fresh:
525
+ logger.debug(
526
+ (
527
+ f"Considering the resource located at {get_safe_url(request.url)} "
528
+ "as invalid for cache use since the age of the response exceeds the max-age directive."
529
+ )
530
+ )
353
531
  return None
354
532
 
355
533
  # the stored response is one of the following:
@@ -357,8 +535,20 @@ class Controller:
357
535
  # allowed to be served stale (see Section 4.2.4), or
358
536
  # successfully validated (see Section 4.3).
359
537
  if is_fresh:
538
+ logger.debug(
539
+ (
540
+ f"Considering the resource located at {get_safe_url(request.url)} "
541
+ "as valid for cache use since it is fresh."
542
+ )
543
+ )
360
544
  return response
361
545
  else:
546
+ logger.debug(
547
+ (
548
+ f"Considering the resource located at {get_safe_url(request.url)} "
549
+ "as needing revalidation since it is not fresh."
550
+ )
551
+ )
362
552
  # Otherwise, make a conditional request
363
553
  self._make_request_conditional(request=request, response=response)
364
554
  return request
hishel/_utils.py CHANGED
@@ -6,6 +6,7 @@ from hashlib import blake2b
6
6
 
7
7
  import anyio
8
8
  import httpcore
9
+ import httpx
9
10
 
10
11
  HEADERS_ENCODING = "iso-8859-1"
11
12
 
@@ -33,6 +34,16 @@ def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
33
34
  assert False, "Invalid type for `normalized_url`" # pragma: no cover
34
35
 
35
36
 
37
+ def get_safe_url(url: httpcore.URL) -> str:
38
+ httpx_url = httpx.URL(bytes(url).decode("ascii"))
39
+
40
+ schema = httpx_url.scheme
41
+ host = httpx_url.host
42
+ path = httpx_url.path
43
+
44
+ return f"{schema}://{host}{path}"
45
+
46
+
36
47
  def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
37
48
  encoded_url = normalized_url(request.url).encode("ascii")
38
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hishel
3
- Version: 0.0.31
3
+ Version: 0.0.33
4
4
  Summary: Persistent cache implementation for httpx and httpcore
5
5
  Project-URL: Homepage, https://hishel.com
6
6
  Project-URL: Source, https://github.com/karpetrosyan/hishel
@@ -175,6 +175,14 @@ Help us grow and continue developing good software for you ❤️
175
175
 
176
176
  # Changelog
177
177
 
178
+ ## 0.0.33 (4th Oct, 2024)
179
+
180
+ - Added a [Logging](https://hishel.com/advanced/logging/) section to the documentation.
181
+
182
+ ## 0.0.32 (27th Sep, 2024)
183
+
184
+ - Don't raise an exception if the `Date` header is not present. (#273)
185
+
178
186
  ## 0.0.31 (22nd Sep, 2024)
179
187
 
180
188
  - Ignore file not found error when cleaning up a file storage. (#264)
@@ -1,5 +1,5 @@
1
- hishel/__init__.py,sha256=XNbMtsi4tkqZZ6AgwVBDu6BumB6WkK3alKCqk-JVDWs,369
2
- hishel/_controller.py,sha256=kfWAAtgP4RCYNPH33XeDJRJ-zajqS_w0ZbM0M0CsKiY,15875
1
+ hishel/__init__.py,sha256=PqLQGkHTPzUnEDcEGVCMakiBjeU7jvbg4Pu4opBqbrs,369
2
+ hishel/_controller.py,sha256=F7hj1ePUvau2Wj5r6zzdYr8UXPq4osfmMxkmx_Ig2L8,24390
3
3
  hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
4
4
  hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
5
5
  hishel/_headers.py,sha256=TWuHi7sRoeS2xxdNGujKmqWtgncUqfhNGCgHKYpRU-I,7329
@@ -7,7 +7,7 @@ hishel/_lfu_cache.py,sha256=GBxToQI8u_a9TzYnLlZMLhgZ8Lb83boPHzTvIgqV6pA,2707
7
7
  hishel/_s3.py,sha256=JqRlygITK5uAryviC15HZKQlKY7etUOPWcazTJeYKBI,3736
8
8
  hishel/_serializers.py,sha256=gepVb8JC4aBkGw9kLcbAsyo-1XgK_lzTssLr_8av4SQ,11640
9
9
  hishel/_synchronization.py,sha256=xOmU9_8KAWTAv3r8EpqPISrtSF3slyh1J0Sc7ZQO1rg,897
10
- hishel/_utils.py,sha256=cgLGjBI7H-T_DkYBXiHiEEf8SGjGeZ6Cc2IHbLAZybU,2501
10
+ hishel/_utils.py,sha256=lnnFmDnKNDb_-OxlZncOJK5IEbtJeTywpITpYFf6WOk,2736
11
11
  hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  hishel/_async/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
13
13
  hishel/_async/_client.py,sha256=AkVSSbNTTHmK0gX6PRYVQ-3aDbuCX2Im4VKbLkwLiBU,1101
@@ -21,7 +21,7 @@ hishel/_sync/_mock.py,sha256=im88tZr-XhP9BpzvIt3uOjndAlNcJvFP7Puv3H-6lKU,1430
21
21
  hishel/_sync/_pool.py,sha256=VcAknzyAL2i4-zcyE2fOTmTjfBZ2wkBVNYTvSw0OjVQ,7940
22
22
  hishel/_sync/_storages.py,sha256=RYzYXqnv0o2JO3RoEmlEUp0yOg_ungXfz4dLN7UTpIQ,27909
23
23
  hishel/_sync/_transports.py,sha256=G3_8SdPwlnrHZRvE1gqFLE4oZadVqNgg5mvxghDMih0,10838
24
- hishel-0.0.31.dist-info/METADATA,sha256=-hIw1fiLfsgB3RmeHqw4odYGvdE4iezkjqZijkD6JvM,11478
25
- hishel-0.0.31.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
26
- hishel-0.0.31.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
27
- hishel-0.0.31.dist-info/RECORD,,
24
+ hishel-0.0.33.dist-info/METADATA,sha256=fVzp5e0XfMlqDkvEzZFfS3F_QfjrUA9wi-A696LNjkc,11694
25
+ hishel-0.0.33.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
26
+ hishel-0.0.33.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
27
+ hishel-0.0.33.dist-info/RECORD,,