hishel 1.0.0.dev0__py3-none-any.whl → 1.0.0.dev2__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/_core/_spec.py CHANGED
@@ -16,6 +16,7 @@ from typing import (
16
16
  )
17
17
 
18
18
  from hishel._core._headers import Headers, Range, Vary, parse_cache_control
19
+ from hishel._core.models import ResponseMetadata
19
20
  from hishel._utils import parse_date, partition
20
21
 
21
22
  if TYPE_CHECKING:
@@ -1412,16 +1413,11 @@ class IdleClient(State):
1412
1413
  #
1413
1414
  # The Age header informs the client how old the cached response is.
1414
1415
 
1415
- # Mark all ready-to-use responses with metadata (for observability)
1416
- for pair in ready_to_use:
1417
- pair.response.metadata["hishel_from_cache"] = True # type: ignore
1418
-
1419
1416
  # Use the most recent response (first in sorted list)
1420
1417
  selected_pair = ready_to_use[0]
1421
1418
 
1422
1419
  # Calculate current age and update the Age header
1423
1420
  current_age = get_age(selected_pair.response)
1424
-
1425
1421
  return FromCache(
1426
1422
  pair=replace(
1427
1423
  selected_pair,
@@ -1578,20 +1574,6 @@ class CacheMiss(State):
1578
1574
  * an s-maxage response directive (if cache is shared)
1579
1575
  * a status code that is defined as heuristically cacheable"
1580
1576
 
1581
- Side Effects:
1582
- ------------
1583
- Sets metadata flags on the response object:
1584
- - hishel_spec_ignored: False (caching spec is being followed)
1585
- - hishel_from_cache: False (response is from origin, not cache)
1586
- - hishel_revalidated: True (if after_revalidation is True)
1587
- - hishel_stored: True/False (whether response was stored)
1588
-
1589
- Logging:
1590
- -------
1591
- When a response cannot be stored, detailed debug logs are emitted explaining
1592
- which specific RFC requirement failed, with direct links to the relevant
1593
- RFC sections.
1594
-
1595
1577
  Examples:
1596
1578
  --------
1597
1579
  >>> # Cacheable response
@@ -1614,21 +1596,6 @@ class CacheMiss(State):
1614
1596
  True
1615
1597
  """
1616
1598
 
1617
- # ============================================================================
1618
- # STEP 1: Set Response Metadata
1619
- # ============================================================================
1620
- # Initialize metadata flags to track the response lifecycle
1621
-
1622
- response.metadata["hishel_spec_ignored"] = False # type: ignore
1623
- # We are following the caching specification
1624
-
1625
- response.metadata["hishel_from_cache"] = False # type: ignore
1626
- # This response came from origin server, not cache
1627
-
1628
- if self.after_revalidation:
1629
- response.metadata["hishel_revalidated"] = True # type: ignore
1630
- # Mark that this response is the result of a revalidation
1631
-
1632
1599
  # ============================================================================
1633
1600
  # STEP 2: Parse Cache-Control Directive
1634
1601
  # ============================================================================
@@ -1723,11 +1690,14 @@ class CacheMiss(State):
1723
1690
  #
1724
1691
  # Requests with Authorization headers often contain user-specific data.
1725
1692
  # Shared caches must be careful not to serve one user's data to another.
1726
- #
1727
- # This check is inverted in the current implementation and needs review:
1728
- # TODO: Fix logic - should be: (not shared) OR (no auth header) OR (has explicit directive)
1729
- # Current logic: (shared) AND (no auth header)
1730
- is_shared_and_authorized = not (self.options.shared and "authorization" in request.headers)
1693
+ has_explicit_directive = (
1694
+ response_cache_control.public
1695
+ or response_cache_control.s_maxage is not None
1696
+ or response_cache_control.must_revalidate
1697
+ )
1698
+ can_cache_auth_request = (
1699
+ not self.options.shared or "authorization" not in request.headers or has_explicit_directive
1700
+ )
1731
1701
 
1732
1702
  # CONDITION 7: Response Contains Required Caching Information
1733
1703
  # RFC 9111 Section 3, paragraph 2.7:
@@ -1797,7 +1767,7 @@ class CacheMiss(State):
1797
1767
  or not understands_how_to_cache
1798
1768
  or not no_store_is_not_present
1799
1769
  or not private_directive_allows_storing
1800
- or not is_shared_and_authorized
1770
+ or not can_cache_auth_request
1801
1771
  or not contains_required_component
1802
1772
  ):
1803
1773
  # --------------------------------------------------------------------
@@ -1833,10 +1803,11 @@ class CacheMiss(State):
1833
1803
  "Cannot store the response because the `private` response directive does not "
1834
1804
  "allow shared caches to store it. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.5.1"
1835
1805
  )
1836
- elif not is_shared_and_authorized:
1806
+ elif not can_cache_auth_request:
1837
1807
  logger.debug(
1838
- "Cannot store the response because the cache is shared and the request contains "
1839
- "an Authorization header field. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.6.1"
1808
+ "Cannot store the response because the request contained an Authorization header "
1809
+ "and there was no explicit directive allowing shared caching. "
1810
+ "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-5"
1840
1811
  )
1841
1812
  elif not contains_required_component:
1842
1813
  logger.debug(
@@ -1844,10 +1815,9 @@ class CacheMiss(State):
1844
1815
  "See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1"
1845
1816
  )
1846
1817
 
1847
- # Mark response as not stored
1848
- response.metadata["hishel_stored"] = False # type: ignore
1849
-
1850
- return CouldNotBeStored(response=response, pair_id=pair_id, options=self.options)
1818
+ return CouldNotBeStored(
1819
+ response=response, pair_id=pair_id, options=self.options, after_revalidation=self.after_revalidation
1820
+ )
1851
1821
 
1852
1822
  # --------------------------------------------------------------------
1853
1823
  # Transition to: StoreAndUse
@@ -1856,9 +1826,6 @@ class CacheMiss(State):
1856
1826
 
1857
1827
  logger.debug("Storing response in cache")
1858
1828
 
1859
- # Mark response as stored
1860
- response.metadata["hishel_stored"] = True # type: ignore
1861
-
1862
1829
  # Remove headers that should not be stored
1863
1830
  # RFC 9111 Section 3.1: Storing Header and Trailer Fields
1864
1831
  # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1
@@ -1869,6 +1836,7 @@ class CacheMiss(State):
1869
1836
  pair_id=pair_id,
1870
1837
  response=cleaned_response,
1871
1838
  options=self.options,
1839
+ after_revalidation=self.after_revalidation,
1872
1840
  )
1873
1841
 
1874
1842
 
@@ -1929,7 +1897,9 @@ class NeedRevalidation(State):
1929
1897
  The stored pairs that the request was sent for revalidation.
1930
1898
  """
1931
1899
 
1932
- def next(self, revalidation_response: Response) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss"]:
1900
+ def next(
1901
+ self, revalidation_response: Response
1902
+ ) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss", "FromCache"]:
1933
1903
  """
1934
1904
  Handles the response to a conditional request and determines the next state.
1935
1905
 
@@ -2084,6 +2054,15 @@ class NeedRevalidation(State):
2084
2054
  after_revalidation=True,
2085
2055
  ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2086
2056
  )
2057
+ elif revalidation_response.status_code // 100 == 3:
2058
+ # 3xx Redirects should have been followed by the HTTP client
2059
+ return FromCache(
2060
+ pair=replace(
2061
+ self.revalidating_pairs[-1],
2062
+ response=revalidation_response,
2063
+ ),
2064
+ options=self.options,
2065
+ )
2087
2066
 
2088
2067
  # ============================================================================
2089
2068
  # STEP 4: Handle Unexpected Status Codes
@@ -2316,32 +2295,99 @@ class NeedRevalidation(State):
2316
2295
  return next_state
2317
2296
 
2318
2297
 
2319
- @dataclass
2298
+ # @dataclass
2299
+ # class StoreAndUse(State):
2300
+ # """
2301
+ # The state that indicates that the response can be stored in the cache and used.
2302
+ # """
2303
+
2304
+ # pair_id: uuid.UUID
2305
+
2306
+ # response: Response
2307
+
2308
+ # def next(self) -> None:
2309
+ # return None # pragma: nocover
2310
+
2311
+
2320
2312
  class StoreAndUse(State):
2321
2313
  """
2322
2314
  The state that indicates that the response can be stored in the cache and used.
2323
- """
2324
2315
 
2325
- pair_id: uuid.UUID
2316
+ Attributes:
2317
+ ----------
2318
+ pair_id : uuid.UUID
2319
+ The unique identifier for the cache pair.
2320
+ response : Response
2321
+ The HTTP response to be stored in the cache.
2322
+ after_revalidation : bool
2323
+ Indicates if the storage is occurring after a revalidation process.
2324
+ """
2326
2325
 
2327
- response: Response
2326
+ def __init__(
2327
+ self, pair_id: uuid.UUID, response: Response, options: CacheOptions, after_revalidation: bool = False
2328
+ ) -> None:
2329
+ super().__init__(options)
2330
+ self.pair_id = pair_id
2331
+ self.response = response
2332
+ self.after_revalidation = after_revalidation
2333
+ response_meta = ResponseMetadata(
2334
+ hishel_created_at=time.time(),
2335
+ hishel_from_cache=False,
2336
+ hishel_spec_ignored=False,
2337
+ hishel_revalidated=after_revalidation,
2338
+ hishel_stored=True,
2339
+ )
2340
+ self.response.metadata.update(response_meta) # type: ignore
2328
2341
 
2329
2342
  def next(self) -> None:
2330
- return None # pragma: nocover
2343
+ return None
2344
+
2345
+
2346
+ # @dataclass
2347
+ # class CouldNotBeStored(State):
2348
+ # """
2349
+ # The state that indicates that the response could not be stored in the cache.
2350
+ # """
2351
+
2352
+ # response: Response
2353
+
2354
+ # pair_id: uuid.UUID
2355
+
2356
+ # def next(self) -> None:
2357
+ # return None # pragma: nocover
2331
2358
 
2332
2359
 
2333
- @dataclass
2334
2360
  class CouldNotBeStored(State):
2335
2361
  """
2336
2362
  The state that indicates that the response could not be stored in the cache.
2337
- """
2338
2363
 
2339
- response: Response
2364
+ Attributes:
2365
+ ----------
2366
+ response : Response
2367
+ The HTTP response that could not be stored.
2368
+ pair_id : uuid.UUID
2369
+ The unique identifier for the cache pair.
2370
+ after_revalidation : bool
2371
+ Indicates if the storage attempt occurred after a revalidation process.
2372
+ """
2340
2373
 
2341
- pair_id: uuid.UUID
2374
+ def __init__(
2375
+ self, response: Response, pair_id: uuid.UUID, options: CacheOptions, after_revalidation: bool = False
2376
+ ) -> None:
2377
+ super().__init__(options)
2378
+ self.response = response
2379
+ self.pair_id = pair_id
2380
+ response_meta = ResponseMetadata(
2381
+ hishel_created_at=time.time(),
2382
+ hishel_from_cache=False,
2383
+ hishel_spec_ignored=False,
2384
+ hishel_revalidated=after_revalidation,
2385
+ hishel_stored=False,
2386
+ )
2387
+ self.response.metadata.update(response_meta) # type: ignore
2342
2388
 
2343
2389
  def next(self) -> None:
2344
- return None # pragma: nocover
2390
+ return None
2345
2391
 
2346
2392
 
2347
2393
  @dataclass
@@ -2358,15 +2404,22 @@ class InvalidatePairs(State):
2358
2404
  return self.next_state
2359
2405
 
2360
2406
 
2361
- @dataclass
2362
2407
  class FromCache(State):
2363
- pair: CompletePair
2364
- """
2365
- List of pairs that can be used to satisfy the request.
2366
- """
2408
+ def __init__(self, pair: CompletePair, options: CacheOptions, after_revalidation: bool = False) -> None:
2409
+ super().__init__(options)
2410
+ self.pair = pair
2411
+ self.after_revalidation = after_revalidation
2412
+ response_meta = ResponseMetadata(
2413
+ hishel_created_at=pair.meta.created_at,
2414
+ hishel_from_cache=True,
2415
+ hishel_spec_ignored=False,
2416
+ hishel_revalidated=after_revalidation,
2417
+ hishel_stored=False,
2418
+ )
2419
+ self.pair.response.metadata.update(response_meta) # type: ignore
2367
2420
 
2368
2421
  def next(self) -> None:
2369
- return None # pragma: nocover
2422
+ return None
2370
2423
 
2371
2424
 
2372
2425
  @dataclass