hishel 1.0.0.dev1__py3-none-any.whl → 1.0.0.dev3__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,10 +16,11 @@ 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:
22
- from hishel import CompletePair, Request, Response
23
+ from hishel import Entry, Request, Response
23
24
 
24
25
 
25
26
  TState = TypeVar("TState", bound="State")
@@ -144,7 +145,7 @@ class State(ABC):
144
145
 
145
146
  def vary_headers_match(
146
147
  original_request: Request,
147
- associated_pair: CompletePair,
148
+ associated_entry: Entry,
148
149
  ) -> bool:
149
150
  """
150
151
  Determines if request headers match the Vary requirements of a cached response.
@@ -160,8 +161,8 @@ def vary_headers_match(
160
161
  ----------
161
162
  original_request : Request
162
163
  The new incoming request that we're trying to satisfy
163
- associated_pair : CompletePair
164
- A cached request-response pair that might match the new request
164
+ associated_entry : Entry
165
+ A cached request-response entry that might match the new request
165
166
 
166
167
  Returns:
167
168
  -------
@@ -194,31 +195,31 @@ def vary_headers_match(
194
195
  >>> # No Vary header - always matches
195
196
  >>> request = Request(headers=Headers({"accept": "application/json"}))
196
197
  >>> response = Response(headers=Headers({})) # No Vary
197
- >>> pair = CompletePair(request=request, response=response)
198
- >>> vary_headers_match(request, pair)
198
+ >>> entry = Entry(request=request, response=response)
199
+ >>> vary_headers_match(request, entry)
199
200
  True
200
201
 
201
202
  >>> # Vary: Accept with matching Accept header
202
203
  >>> request1 = Request(headers=Headers({"accept": "application/json"}))
203
204
  >>> response = Response(headers=Headers({"vary": "Accept"}))
204
- >>> pair = CompletePair(request=request1, response=response)
205
+ >>> entry = Entry(request=request1, response=response)
205
206
  >>> request2 = Request(headers=Headers({"accept": "application/json"}))
206
- >>> vary_headers_match(request2, pair)
207
+ >>> vary_headers_match(request2, entry)
207
208
  True
208
209
 
209
210
  >>> # Vary: Accept with non-matching Accept header
210
211
  >>> request2 = Request(headers=Headers({"accept": "application/xml"}))
211
- >>> vary_headers_match(request2, pair)
212
+ >>> vary_headers_match(request2, entry)
212
213
  False
213
214
 
214
215
  >>> # Vary: * always fails
215
216
  >>> response = Response(headers=Headers({"vary": "*"}))
216
- >>> pair = CompletePair(request=request1, response=response)
217
- >>> vary_headers_match(request2, pair)
217
+ >>> entry = Entry(request=request1, response=response)
218
+ >>> vary_headers_match(request2, entry)
218
219
  False
219
220
  """
220
221
  # Extract the Vary header from the cached response
221
- vary_header = associated_pair.response.headers.get("vary")
222
+ vary_header = associated_entry.response.headers.get("vary")
222
223
 
223
224
  # If no Vary header exists, any request matches
224
225
  # The response doesn't vary based on request headers
@@ -241,7 +242,7 @@ def vary_headers_match(
241
242
 
242
243
  # Compare the specific header value between original and new request
243
244
  # Both headers must have the same value (or both be absent)
244
- if original_request.headers.get(vary_header) != associated_pair.request.headers.get(vary_header):
245
+ if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header):
245
246
  return False
246
247
 
247
248
  # All Vary headers matched
@@ -1120,7 +1121,7 @@ AnyState = Union[
1120
1121
  "NeedToBeUpdated",
1121
1122
  "NeedRevalidation",
1122
1123
  "IdleClient",
1123
- "InvalidatePairs",
1124
+ "InvalidateEntries",
1124
1125
  ]
1125
1126
 
1126
1127
  # Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods
@@ -1166,7 +1167,7 @@ class IdleClient(State):
1166
1167
  """
1167
1168
 
1168
1169
  def next(
1169
- self, request: Request, associated_pairs: list[CompletePair]
1170
+ self, request: Request, associated_entries: list[Entry]
1170
1171
  ) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]:
1171
1172
  """
1172
1173
  Determines the next state transition based on the request and available cached responses.
@@ -1179,9 +1180,9 @@ class IdleClient(State):
1179
1180
  ----------
1180
1181
  request : Request
1181
1182
  The incoming HTTP request from the client
1182
- associated_pairs : list[CompletePair]
1183
- List of request-response pairs previously stored in the cache that may match
1184
- this request. These pairs are pre-filtered by cache key (typically URI).
1183
+ associated_entries : list[Entry]
1184
+ List of request-response entries previously stored in the cache that may match
1185
+ this request. These entries are pre-filtered by cache key (typically URI).
1185
1186
 
1186
1187
  Returns:
1187
1188
  -------
@@ -1316,7 +1317,7 @@ class IdleClient(State):
1316
1317
  #
1317
1318
  # If a cached response has Cache-Control: no-cache, it cannot be reused without
1318
1319
  # validation, regardless of its freshness.
1319
- def no_cache_missing(pair: CompletePair) -> bool:
1320
+ def no_cache_missing(pair: Entry) -> bool:
1320
1321
  """Check if the cached response lacks the no-cache directive."""
1321
1322
  return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False
1322
1323
 
@@ -1331,7 +1332,7 @@ class IdleClient(State):
1331
1332
  #
1332
1333
  # Note: Condition 5.3 (successfully validated) is handled in the
1333
1334
  # NeedRevalidation state, not here.
1334
- def fresh_or_allowed_stale(pair: CompletePair) -> bool:
1335
+ def fresh_or_allowed_stale(pair: Entry) -> bool:
1335
1336
  """
1336
1337
  Determine if a cached response is fresh or allowed to be served stale.
1337
1338
 
@@ -1360,7 +1361,7 @@ class IdleClient(State):
1360
1361
  # "ready to use" and "needs revalidation" groups.
1361
1362
  filtered_pairs = [
1362
1363
  pair
1363
- for pair in associated_pairs
1364
+ for pair in associated_entries
1364
1365
  if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call]
1365
1366
  ]
1366
1367
 
@@ -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,
@@ -1459,7 +1455,7 @@ class IdleClient(State):
1459
1455
  # (ETag, Last-Modified) from the cached response.
1460
1456
  return NeedRevalidation(
1461
1457
  request=make_conditional_request(request, need_revalidation[-1].response),
1462
- revalidating_pairs=need_revalidation,
1458
+ revalidating_entries=need_revalidation,
1463
1459
  options=self.options,
1464
1460
  original_request=request,
1465
1461
  )
@@ -1529,7 +1525,7 @@ class CacheMiss(State):
1529
1525
  Indicates whether the cache miss occurred after a revalidation attempt.
1530
1526
  """
1531
1527
 
1532
- def next(self, response: Response, pair_id: uuid.UUID) -> Union["StoreAndUse", "CouldNotBeStored"]:
1528
+ def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]:
1533
1529
  """
1534
1530
  Evaluates whether a response can be stored in the cache.
1535
1531
 
@@ -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
@@ -1600,7 +1582,7 @@ class CacheMiss(State):
1600
1582
  ... status_code=200,
1601
1583
  ... headers=Headers({"cache-control": "max-age=3600"})
1602
1584
  ... )
1603
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1585
+ >>> next_state = cache_miss.next(response)
1604
1586
  >>> isinstance(next_state, StoreAndUse)
1605
1587
  True
1606
1588
 
@@ -1609,26 +1591,11 @@ class CacheMiss(State):
1609
1591
  ... status_code=200,
1610
1592
  ... headers=Headers({"cache-control": "no-store"})
1611
1593
  ... )
1612
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1594
+ >>> next_state = cache_miss.next(response)
1613
1595
  >>> isinstance(next_state, CouldNotBeStored)
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,11 @@ 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,
1820
+ options=self.options,
1821
+ after_revalidation=self.after_revalidation,
1822
+ )
1851
1823
 
1852
1824
  # --------------------------------------------------------------------
1853
1825
  # Transition to: StoreAndUse
@@ -1856,9 +1828,6 @@ class CacheMiss(State):
1856
1828
 
1857
1829
  logger.debug("Storing response in cache")
1858
1830
 
1859
- # Mark response as stored
1860
- response.metadata["hishel_stored"] = True # type: ignore
1861
-
1862
1831
  # Remove headers that should not be stored
1863
1832
  # RFC 9111 Section 3.1: Storing Header and Trailer Fields
1864
1833
  # https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1
@@ -1866,9 +1835,9 @@ class CacheMiss(State):
1866
1835
  cleaned_response = exclude_unstorable_headers(response, self.options.shared)
1867
1836
 
1868
1837
  return StoreAndUse(
1869
- pair_id=pair_id,
1870
1838
  response=cleaned_response,
1871
1839
  options=self.options,
1840
+ after_revalidation=self.after_revalidation,
1872
1841
  )
1873
1842
 
1874
1843
 
@@ -1888,7 +1857,7 @@ class NeedRevalidation(State):
1888
1857
  State Transitions:
1889
1858
  -----------------
1890
1859
  - NeedToBeUpdated: 304 response received, cached responses can be freshened
1891
- - InvalidatePairs + CacheMiss: 2xx/5xx response received, new response must be cached
1860
+ - InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached
1892
1861
  - CacheMiss: No matching responses found during freshening
1893
1862
 
1894
1863
  RFC 9111 References:
@@ -1909,8 +1878,8 @@ class NeedRevalidation(State):
1909
1878
  original_request : Request
1910
1879
  The original client request (without conditional headers) that initiated
1911
1880
  this revalidation. This is used when creating new cache entries.
1912
- revalidating_pairs : list[CompletePair]
1913
- The cached request-response pairs that are being revalidated. These are
1881
+ revalidating_entries : list[Entry]
1882
+ The cached request-response entries that are being revalidated. These are
1914
1883
  stale responses that might still be usable if the server confirms they
1915
1884
  haven't changed (304 response).
1916
1885
  options : CacheOptions
@@ -1924,12 +1893,14 @@ class NeedRevalidation(State):
1924
1893
 
1925
1894
  original_request: Request
1926
1895
 
1927
- revalidating_pairs: list[CompletePair]
1896
+ revalidating_entries: list[Entry]
1928
1897
  """
1929
- The stored pairs that the request was sent for revalidation.
1898
+ The stored entries that the request was sent for revalidation.
1930
1899
  """
1931
1900
 
1932
- def next(self, revalidation_response: Response) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss"]:
1901
+ def next(
1902
+ self, revalidation_response: Response
1903
+ ) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache"]:
1933
1904
  """
1934
1905
  Handles the response to a conditional request and determines the next state.
1935
1906
 
@@ -1949,9 +1920,9 @@ class NeedRevalidation(State):
1949
1920
 
1950
1921
  Returns:
1951
1922
  -------
1952
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
1923
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
1953
1924
  - NeedToBeUpdated: When 304 response allows cached responses to be freshened
1954
- - InvalidatePairs: When old responses must be invalidated (wraps next state)
1925
+ - InvalidateEntries: When old responses must be invalidated (wraps next state)
1955
1926
  - CacheMiss: When no matching responses found or storing new response
1956
1927
 
1957
1928
  RFC 9111 Compliance:
@@ -1986,7 +1957,7 @@ class NeedRevalidation(State):
1986
1957
  >>> need_revalidation = NeedRevalidation(
1987
1958
  ... request=conditional_request,
1988
1959
  ... original_request=original_request,
1989
- ... revalidating_pairs=[cached_pair],
1960
+ ... revalidating_entries=[cached_entry],
1990
1961
  ... options=default_options
1991
1962
  ... )
1992
1963
  >>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'}))
@@ -1997,7 +1968,7 @@ class NeedRevalidation(State):
1997
1968
  >>> # 200 OK - use new response
1998
1969
  >>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"}))
1999
1970
  >>> next_state = need_revalidation.next(response_200)
2000
- >>> isinstance(next_state, InvalidatePairs)
1971
+ >>> isinstance(next_state, InvalidateEntries)
2001
1972
  True
2002
1973
  """
2003
1974
 
@@ -2036,17 +2007,19 @@ class NeedRevalidation(State):
2036
2007
  # 2. Store the new response (if cacheable)
2037
2008
  # 3. Use the new response to satisfy the request
2038
2009
  elif revalidation_response.status_code // 100 == 2:
2039
- # Invalidate all old pairs except the last one
2040
- # The last pair's ID will be reused for the new response
2041
- return InvalidatePairs(
2010
+ # Invalidate all old entries except the last one
2011
+ # The last entry's ID will be reused for the new response
2012
+ return InvalidateEntries(
2042
2013
  options=self.options,
2043
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2014
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2044
2015
  # After invalidation, attempt to cache the new response
2045
2016
  next_state=CacheMiss(
2046
2017
  request=self.original_request,
2047
2018
  options=self.options,
2048
2019
  after_revalidation=True, # Mark that this occurred during revalidation
2049
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2020
+ ).next(
2021
+ revalidation_response,
2022
+ ),
2050
2023
  )
2051
2024
 
2052
2025
  # ============================================================================
@@ -2075,14 +2048,25 @@ class NeedRevalidation(State):
2075
2048
  elif revalidation_response.status_code // 100 == 5:
2076
2049
  # Same as 2xx: invalidate old responses and store the error response
2077
2050
  # This ensures clients see the error rather than potentially stale data
2078
- return InvalidatePairs(
2051
+ return InvalidateEntries(
2079
2052
  options=self.options,
2080
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2053
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2081
2054
  next_state=CacheMiss(
2082
2055
  request=self.original_request,
2083
2056
  options=self.options,
2084
2057
  after_revalidation=True,
2085
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2058
+ ).next(
2059
+ revalidation_response,
2060
+ ),
2061
+ )
2062
+ elif revalidation_response.status_code // 100 == 3:
2063
+ # 3xx Redirects should have been followed by the HTTP client
2064
+ return FromCache(
2065
+ pair=replace(
2066
+ self.revalidating_entries[-1],
2067
+ response=revalidation_response,
2068
+ ),
2069
+ options=self.options,
2086
2070
  )
2087
2071
 
2088
2072
  # ============================================================================
@@ -2102,7 +2086,7 @@ class NeedRevalidation(State):
2102
2086
 
2103
2087
  def freshening_stored_responses(
2104
2088
  self, revalidation_response: Response
2105
- ) -> "NeedToBeUpdated" | "InvalidatePairs" | "CacheMiss":
2089
+ ) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss":
2106
2090
  """
2107
2091
  Freshens cached responses after receiving a 304 Not Modified response.
2108
2092
 
@@ -2125,9 +2109,9 @@ class NeedRevalidation(State):
2125
2109
 
2126
2110
  Returns:
2127
2111
  -------
2128
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
2112
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
2129
2113
  - NeedToBeUpdated: When matching responses are found and updated
2130
- - InvalidatePairs: Wraps NeedToBeUpdated if non-matching responses exist
2114
+ - InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist
2131
2115
  - CacheMiss: When no matching responses are found
2132
2116
 
2133
2117
  RFC 9111 Compliance:
@@ -2188,7 +2172,7 @@ class NeedRevalidation(State):
2188
2172
  # Priority 2: Last-Modified timestamp
2189
2173
  # Priority 3: Single response assumption
2190
2174
 
2191
- identified_for_revalidation: list[CompletePair]
2175
+ identified_for_revalidation: list[Entry]
2192
2176
 
2193
2177
  # MATCHING STRATEGY 1: Strong ETag
2194
2178
  # RFC 9110 Section 8.8.3: ETag
@@ -2208,7 +2192,7 @@ class NeedRevalidation(State):
2208
2192
  # Found a strong ETag in the 304 response
2209
2193
  # Partition cached responses: matching vs non-matching ETags
2210
2194
  identified_for_revalidation, need_to_be_invalidated = partition(
2211
- self.revalidating_pairs,
2195
+ self.revalidating_entries,
2212
2196
  lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call]
2213
2197
  )
2214
2198
 
@@ -2226,7 +2210,7 @@ class NeedRevalidation(State):
2226
2210
  # Found Last-Modified in the 304 response
2227
2211
  # Partition cached responses: matching vs non-matching timestamps
2228
2212
  identified_for_revalidation, need_to_be_invalidated = partition(
2229
- self.revalidating_pairs,
2213
+ self.revalidating_entries,
2230
2214
  lambda pair: pair.response.headers.get("last-modified")
2231
2215
  == revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call]
2232
2216
  )
@@ -2240,14 +2224,20 @@ class NeedRevalidation(State):
2240
2224
  # we can safely assume that single response is the one being confirmed.
2241
2225
  # This handles cases where the server doesn't return validators in the 304.
2242
2226
  else:
2243
- if len(self.revalidating_pairs) == 1:
2227
+ if len(self.revalidating_entries) == 1:
2244
2228
  # Only one cached response - it must be the matching one
2245
- identified_for_revalidation, need_to_be_invalidated = [self.revalidating_pairs[0]], []
2229
+ identified_for_revalidation, need_to_be_invalidated = (
2230
+ [self.revalidating_entries[0]],
2231
+ [],
2232
+ )
2246
2233
  else:
2247
2234
  # Multiple cached responses but no validators to match them
2248
2235
  # We cannot determine which (if any) are valid
2249
2236
  # Conservative approach: invalidate all of them
2250
- identified_for_revalidation, need_to_be_invalidated = [], self.revalidating_pairs
2237
+ identified_for_revalidation, need_to_be_invalidated = (
2238
+ [],
2239
+ self.revalidating_entries,
2240
+ )
2251
2241
 
2252
2242
  # ============================================================================
2253
2243
  # STEP 2: Update Matching Responses or Create Cache Miss
@@ -2272,7 +2262,7 @@ class NeedRevalidation(State):
2272
2262
  # while excluding certain headers that shouldn't be updated
2273
2263
  # (Content-Encoding, Content-Type, Content-Range).
2274
2264
  next_state = NeedToBeUpdated(
2275
- updating_pairs=[
2265
+ updating_entries=[
2276
2266
  replace(
2277
2267
  pair,
2278
2268
  response=refresh_response_headers(pair.response, revalidation_response),
@@ -2306,9 +2296,9 @@ class NeedRevalidation(State):
2306
2296
 
2307
2297
  if need_to_be_invalidated:
2308
2298
  # Wrap the next state in an invalidation operation
2309
- return InvalidatePairs(
2299
+ return InvalidateEntries(
2310
2300
  options=self.options,
2311
- pair_ids=[pair.id for pair in need_to_be_invalidated],
2301
+ entry_ids=[entry.id for entry in need_to_be_invalidated],
2312
2302
  next_state=next_state,
2313
2303
  )
2314
2304
 
@@ -2316,41 +2306,96 @@ class NeedRevalidation(State):
2316
2306
  return next_state
2317
2307
 
2318
2308
 
2319
- @dataclass
2320
2309
  class StoreAndUse(State):
2321
2310
  """
2322
2311
  The state that indicates that the response can be stored in the cache and used.
2323
- """
2324
2312
 
2325
- pair_id: uuid.UUID
2313
+ Attributes:
2314
+ ----------
2315
+ response : Response
2316
+ The HTTP response to be stored in the cache.
2317
+ after_revalidation : bool
2318
+ Indicates if the storage is occurring after a revalidation process.
2319
+ """
2326
2320
 
2327
- response: Response
2321
+ def __init__(
2322
+ self,
2323
+ response: Response,
2324
+ options: CacheOptions,
2325
+ after_revalidation: bool = False,
2326
+ ) -> None:
2327
+ super().__init__(options)
2328
+ self.response = response
2329
+ self.after_revalidation = after_revalidation
2330
+ response_meta = ResponseMetadata(
2331
+ hishel_created_at=time.time(),
2332
+ hishel_from_cache=False,
2333
+ hishel_spec_ignored=False,
2334
+ hishel_revalidated=after_revalidation,
2335
+ hishel_stored=True,
2336
+ )
2337
+ self.response.metadata.update(response_meta) # type: ignore
2328
2338
 
2329
2339
  def next(self) -> None:
2330
- return None # pragma: nocover
2340
+ return None
2341
+
2342
+
2343
+ # @dataclass
2344
+ # class CouldNotBeStored(State):
2345
+ # """
2346
+ # The state that indicates that the response could not be stored in the cache.
2347
+ # """
2348
+
2349
+ # response: Response
2350
+
2351
+ # pair_id: uuid.UUID
2352
+
2353
+ # def next(self) -> None:
2354
+ # return None # pragma: nocover
2331
2355
 
2332
2356
 
2333
- @dataclass
2334
2357
  class CouldNotBeStored(State):
2335
2358
  """
2336
2359
  The state that indicates that the response could not be stored in the cache.
2337
- """
2338
2360
 
2339
- response: Response
2361
+ Attributes:
2362
+ ----------
2363
+ response : Response
2364
+ The HTTP response that could not be stored.
2365
+ pair_id : uuid.UUID
2366
+ The unique identifier for the cache pair.
2367
+ after_revalidation : bool
2368
+ Indicates if the storage attempt occurred after a revalidation process.
2369
+ """
2340
2370
 
2341
- pair_id: uuid.UUID
2371
+ def __init__(
2372
+ self,
2373
+ response: Response,
2374
+ options: CacheOptions,
2375
+ after_revalidation: bool = False,
2376
+ ) -> None:
2377
+ super().__init__(options)
2378
+ self.response = response
2379
+ response_meta = ResponseMetadata(
2380
+ hishel_created_at=time.time(),
2381
+ hishel_from_cache=False,
2382
+ hishel_spec_ignored=False,
2383
+ hishel_revalidated=after_revalidation,
2384
+ hishel_stored=False,
2385
+ )
2386
+ self.response.metadata.update(response_meta) # type: ignore
2342
2387
 
2343
2388
  def next(self) -> None:
2344
- return None # pragma: nocover
2389
+ return None
2345
2390
 
2346
2391
 
2347
2392
  @dataclass
2348
- class InvalidatePairs(State):
2393
+ class InvalidateEntries(State):
2349
2394
  """
2350
- The state that represents the deletion of cache pairs.
2395
+ The state that represents the deletion of cache entries.
2351
2396
  """
2352
2397
 
2353
- pair_ids: list[uuid.UUID]
2398
+ entry_ids: list[uuid.UUID]
2354
2399
 
2355
2400
  next_state: AnyState
2356
2401
 
@@ -2358,21 +2403,33 @@ class InvalidatePairs(State):
2358
2403
  return self.next_state
2359
2404
 
2360
2405
 
2361
- @dataclass
2362
2406
  class FromCache(State):
2363
- pair: CompletePair
2364
- """
2365
- List of pairs that can be used to satisfy the request.
2366
- """
2407
+ def __init__(
2408
+ self,
2409
+ pair: Entry,
2410
+ options: CacheOptions,
2411
+ after_revalidation: bool = False,
2412
+ ) -> None:
2413
+ super().__init__(options)
2414
+ self.pair = pair
2415
+ self.after_revalidation = after_revalidation
2416
+ response_meta = ResponseMetadata(
2417
+ hishel_created_at=pair.meta.created_at,
2418
+ hishel_from_cache=True,
2419
+ hishel_spec_ignored=False,
2420
+ hishel_revalidated=after_revalidation,
2421
+ hishel_stored=False,
2422
+ )
2423
+ self.pair.response.metadata.update(response_meta) # type: ignore
2367
2424
 
2368
2425
  def next(self) -> None:
2369
- return None # pragma: nocover
2426
+ return None
2370
2427
 
2371
2428
 
2372
2429
  @dataclass
2373
2430
  class NeedToBeUpdated(State):
2374
- updating_pairs: list[CompletePair]
2431
+ updating_entries: list[Entry]
2375
2432
  original_request: Request
2376
2433
 
2377
2434
  def next(self) -> FromCache:
2378
- return FromCache(pair=self.updating_pairs[-1], options=self.options) # pragma: nocover
2435
+ return FromCache(pair=self.updating_entries[-1], options=self.options) # pragma: nocover