hishel 1.0.0.dev2__py3-none-any.whl → 1.1.0__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
@@ -9,7 +9,6 @@ from typing import (
9
9
  TYPE_CHECKING,
10
10
  Any,
11
11
  Dict,
12
- Literal,
13
12
  Optional,
14
13
  TypeVar,
15
14
  Union,
@@ -20,7 +19,7 @@ from hishel._core.models import ResponseMetadata
20
19
  from hishel._utils import parse_date, partition
21
20
 
22
21
  if TYPE_CHECKING:
23
- from hishel import CompletePair, Request, Response
22
+ from hishel import Entry, Request, Response
24
23
 
25
24
 
26
25
  TState = TypeVar("TState", bound="State")
@@ -145,7 +144,7 @@ class State(ABC):
145
144
 
146
145
  def vary_headers_match(
147
146
  original_request: Request,
148
- associated_pair: CompletePair,
147
+ associated_entry: Entry,
149
148
  ) -> bool:
150
149
  """
151
150
  Determines if request headers match the Vary requirements of a cached response.
@@ -161,8 +160,8 @@ def vary_headers_match(
161
160
  ----------
162
161
  original_request : Request
163
162
  The new incoming request that we're trying to satisfy
164
- associated_pair : CompletePair
165
- A cached request-response pair that might match the new request
163
+ associated_entry : Entry
164
+ A cached request-response entry that might match the new request
166
165
 
167
166
  Returns:
168
167
  -------
@@ -195,31 +194,31 @@ def vary_headers_match(
195
194
  >>> # No Vary header - always matches
196
195
  >>> request = Request(headers=Headers({"accept": "application/json"}))
197
196
  >>> response = Response(headers=Headers({})) # No Vary
198
- >>> pair = CompletePair(request=request, response=response)
199
- >>> vary_headers_match(request, pair)
197
+ >>> entry = Entry(request=request, response=response)
198
+ >>> vary_headers_match(request, entry)
200
199
  True
201
200
 
202
201
  >>> # Vary: Accept with matching Accept header
203
202
  >>> request1 = Request(headers=Headers({"accept": "application/json"}))
204
203
  >>> response = Response(headers=Headers({"vary": "Accept"}))
205
- >>> pair = CompletePair(request=request1, response=response)
204
+ >>> entry = Entry(request=request1, response=response)
206
205
  >>> request2 = Request(headers=Headers({"accept": "application/json"}))
207
- >>> vary_headers_match(request2, pair)
206
+ >>> vary_headers_match(request2, entry)
208
207
  True
209
208
 
210
209
  >>> # Vary: Accept with non-matching Accept header
211
210
  >>> request2 = Request(headers=Headers({"accept": "application/xml"}))
212
- >>> vary_headers_match(request2, pair)
211
+ >>> vary_headers_match(request2, entry)
213
212
  False
214
213
 
215
214
  >>> # Vary: * always fails
216
215
  >>> response = Response(headers=Headers({"vary": "*"}))
217
- >>> pair = CompletePair(request=request1, response=response)
218
- >>> vary_headers_match(request2, pair)
216
+ >>> entry = Entry(request=request1, response=response)
217
+ >>> vary_headers_match(request2, entry)
219
218
  False
220
219
  """
221
220
  # Extract the Vary header from the cached response
222
- vary_header = associated_pair.response.headers.get("vary")
221
+ vary_header = associated_entry.response.headers.get("vary")
223
222
 
224
223
  # If no Vary header exists, any request matches
225
224
  # The response doesn't vary based on request headers
@@ -242,7 +241,7 @@ def vary_headers_match(
242
241
 
243
242
  # Compare the specific header value between original and new request
244
243
  # Both headers must have the same value (or both be absent)
245
- if original_request.headers.get(vary_header) != associated_pair.request.headers.get(vary_header):
244
+ if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header):
246
245
  return False
247
246
 
248
247
  # All Vary headers matched
@@ -1121,19 +1120,13 @@ AnyState = Union[
1121
1120
  "NeedToBeUpdated",
1122
1121
  "NeedRevalidation",
1123
1122
  "IdleClient",
1124
- "InvalidatePairs",
1123
+ "InvalidateEntries",
1125
1124
  ]
1126
1125
 
1127
1126
  # Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods
1128
1127
  SAFE_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
1129
1128
 
1130
1129
 
1131
- def create_idle_state(role: Literal["client", "server"], options: Optional[CacheOptions] = None) -> IdleClient:
1132
- if role == "server":
1133
- raise NotImplementedError("Server role is not implemented yet.")
1134
- return IdleClient(options=options or CacheOptions())
1135
-
1136
-
1137
1130
  @dataclass
1138
1131
  class IdleClient(State):
1139
1132
  """
@@ -1167,7 +1160,7 @@ class IdleClient(State):
1167
1160
  """
1168
1161
 
1169
1162
  def next(
1170
- self, request: Request, associated_pairs: list[CompletePair]
1163
+ self, request: Request, associated_entries: list[Entry]
1171
1164
  ) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]:
1172
1165
  """
1173
1166
  Determines the next state transition based on the request and available cached responses.
@@ -1180,9 +1173,9 @@ class IdleClient(State):
1180
1173
  ----------
1181
1174
  request : Request
1182
1175
  The incoming HTTP request from the client
1183
- associated_pairs : list[CompletePair]
1184
- List of request-response pairs previously stored in the cache that may match
1185
- this request. These pairs are pre-filtered by cache key (typically URI).
1176
+ associated_entries : list[Entry]
1177
+ List of request-response entries previously stored in the cache that may match
1178
+ this request. These entries are pre-filtered by cache key (typically URI).
1186
1179
 
1187
1180
  Returns:
1188
1181
  -------
@@ -1317,7 +1310,7 @@ class IdleClient(State):
1317
1310
  #
1318
1311
  # If a cached response has Cache-Control: no-cache, it cannot be reused without
1319
1312
  # validation, regardless of its freshness.
1320
- def no_cache_missing(pair: CompletePair) -> bool:
1313
+ def no_cache_missing(pair: Entry) -> bool:
1321
1314
  """Check if the cached response lacks the no-cache directive."""
1322
1315
  return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False
1323
1316
 
@@ -1332,7 +1325,7 @@ class IdleClient(State):
1332
1325
  #
1333
1326
  # Note: Condition 5.3 (successfully validated) is handled in the
1334
1327
  # NeedRevalidation state, not here.
1335
- def fresh_or_allowed_stale(pair: CompletePair) -> bool:
1328
+ def fresh_or_allowed_stale(pair: Entry) -> bool:
1336
1329
  """
1337
1330
  Determine if a cached response is fresh or allowed to be served stale.
1338
1331
 
@@ -1361,7 +1354,7 @@ class IdleClient(State):
1361
1354
  # "ready to use" and "needs revalidation" groups.
1362
1355
  filtered_pairs = [
1363
1356
  pair
1364
- for pair in associated_pairs
1357
+ for pair in associated_entries
1365
1358
  if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call]
1366
1359
  ]
1367
1360
 
@@ -1419,7 +1412,7 @@ class IdleClient(State):
1419
1412
  # Calculate current age and update the Age header
1420
1413
  current_age = get_age(selected_pair.response)
1421
1414
  return FromCache(
1422
- pair=replace(
1415
+ entry=replace(
1423
1416
  selected_pair,
1424
1417
  response=replace(
1425
1418
  selected_pair.response,
@@ -1455,7 +1448,7 @@ class IdleClient(State):
1455
1448
  # (ETag, Last-Modified) from the cached response.
1456
1449
  return NeedRevalidation(
1457
1450
  request=make_conditional_request(request, need_revalidation[-1].response),
1458
- revalidating_pairs=need_revalidation,
1451
+ revalidating_entries=need_revalidation,
1459
1452
  options=self.options,
1460
1453
  original_request=request,
1461
1454
  )
@@ -1525,7 +1518,7 @@ class CacheMiss(State):
1525
1518
  Indicates whether the cache miss occurred after a revalidation attempt.
1526
1519
  """
1527
1520
 
1528
- def next(self, response: Response, pair_id: uuid.UUID) -> Union["StoreAndUse", "CouldNotBeStored"]:
1521
+ def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]:
1529
1522
  """
1530
1523
  Evaluates whether a response can be stored in the cache.
1531
1524
 
@@ -1582,7 +1575,7 @@ class CacheMiss(State):
1582
1575
  ... status_code=200,
1583
1576
  ... headers=Headers({"cache-control": "max-age=3600"})
1584
1577
  ... )
1585
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1578
+ >>> next_state = cache_miss.next(response)
1586
1579
  >>> isinstance(next_state, StoreAndUse)
1587
1580
  True
1588
1581
 
@@ -1591,7 +1584,7 @@ class CacheMiss(State):
1591
1584
  ... status_code=200,
1592
1585
  ... headers=Headers({"cache-control": "no-store"})
1593
1586
  ... )
1594
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1587
+ >>> next_state = cache_miss.next(response)
1595
1588
  >>> isinstance(next_state, CouldNotBeStored)
1596
1589
  True
1597
1590
  """
@@ -1816,7 +1809,9 @@ class CacheMiss(State):
1816
1809
  )
1817
1810
 
1818
1811
  return CouldNotBeStored(
1819
- response=response, pair_id=pair_id, options=self.options, after_revalidation=self.after_revalidation
1812
+ response=response,
1813
+ options=self.options,
1814
+ after_revalidation=self.after_revalidation,
1820
1815
  )
1821
1816
 
1822
1817
  # --------------------------------------------------------------------
@@ -1833,7 +1828,6 @@ class CacheMiss(State):
1833
1828
  cleaned_response = exclude_unstorable_headers(response, self.options.shared)
1834
1829
 
1835
1830
  return StoreAndUse(
1836
- pair_id=pair_id,
1837
1831
  response=cleaned_response,
1838
1832
  options=self.options,
1839
1833
  after_revalidation=self.after_revalidation,
@@ -1856,7 +1850,7 @@ class NeedRevalidation(State):
1856
1850
  State Transitions:
1857
1851
  -----------------
1858
1852
  - NeedToBeUpdated: 304 response received, cached responses can be freshened
1859
- - InvalidatePairs + CacheMiss: 2xx/5xx response received, new response must be cached
1853
+ - InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached
1860
1854
  - CacheMiss: No matching responses found during freshening
1861
1855
 
1862
1856
  RFC 9111 References:
@@ -1877,8 +1871,8 @@ class NeedRevalidation(State):
1877
1871
  original_request : Request
1878
1872
  The original client request (without conditional headers) that initiated
1879
1873
  this revalidation. This is used when creating new cache entries.
1880
- revalidating_pairs : list[CompletePair]
1881
- The cached request-response pairs that are being revalidated. These are
1874
+ revalidating_entries : list[Entry]
1875
+ The cached request-response entries that are being revalidated. These are
1882
1876
  stale responses that might still be usable if the server confirms they
1883
1877
  haven't changed (304 response).
1884
1878
  options : CacheOptions
@@ -1892,14 +1886,14 @@ class NeedRevalidation(State):
1892
1886
 
1893
1887
  original_request: Request
1894
1888
 
1895
- revalidating_pairs: list[CompletePair]
1889
+ revalidating_entries: list[Entry]
1896
1890
  """
1897
- The stored pairs that the request was sent for revalidation.
1891
+ The stored entries that the request was sent for revalidation.
1898
1892
  """
1899
1893
 
1900
1894
  def next(
1901
1895
  self, revalidation_response: Response
1902
- ) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss", "FromCache"]:
1896
+ ) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache", "StoreAndUse", "CouldNotBeStored"]:
1903
1897
  """
1904
1898
  Handles the response to a conditional request and determines the next state.
1905
1899
 
@@ -1919,9 +1913,9 @@ class NeedRevalidation(State):
1919
1913
 
1920
1914
  Returns:
1921
1915
  -------
1922
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
1916
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
1923
1917
  - NeedToBeUpdated: When 304 response allows cached responses to be freshened
1924
- - InvalidatePairs: When old responses must be invalidated (wraps next state)
1918
+ - InvalidateEntries: When old responses must be invalidated (wraps next state)
1925
1919
  - CacheMiss: When no matching responses found or storing new response
1926
1920
 
1927
1921
  RFC 9111 Compliance:
@@ -1956,7 +1950,7 @@ class NeedRevalidation(State):
1956
1950
  >>> need_revalidation = NeedRevalidation(
1957
1951
  ... request=conditional_request,
1958
1952
  ... original_request=original_request,
1959
- ... revalidating_pairs=[cached_pair],
1953
+ ... revalidating_entries=[cached_entry],
1960
1954
  ... options=default_options
1961
1955
  ... )
1962
1956
  >>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'}))
@@ -1967,7 +1961,7 @@ class NeedRevalidation(State):
1967
1961
  >>> # 200 OK - use new response
1968
1962
  >>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"}))
1969
1963
  >>> next_state = need_revalidation.next(response_200)
1970
- >>> isinstance(next_state, InvalidatePairs)
1964
+ >>> isinstance(next_state, InvalidateEntries)
1971
1965
  True
1972
1966
  """
1973
1967
 
@@ -2006,17 +2000,19 @@ class NeedRevalidation(State):
2006
2000
  # 2. Store the new response (if cacheable)
2007
2001
  # 3. Use the new response to satisfy the request
2008
2002
  elif revalidation_response.status_code // 100 == 2:
2009
- # Invalidate all old pairs except the last one
2010
- # The last pair's ID will be reused for the new response
2011
- return InvalidatePairs(
2003
+ # Invalidate all old entries except the last one
2004
+ # The last entry's ID will be reused for the new response
2005
+ return InvalidateEntries(
2012
2006
  options=self.options,
2013
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2007
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2014
2008
  # After invalidation, attempt to cache the new response
2015
2009
  next_state=CacheMiss(
2016
2010
  request=self.original_request,
2017
2011
  options=self.options,
2018
2012
  after_revalidation=True, # Mark that this occurred during revalidation
2019
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2013
+ ).next(
2014
+ revalidation_response,
2015
+ ),
2020
2016
  )
2021
2017
 
2022
2018
  # ============================================================================
@@ -2045,43 +2041,32 @@ class NeedRevalidation(State):
2045
2041
  elif revalidation_response.status_code // 100 == 5:
2046
2042
  # Same as 2xx: invalidate old responses and store the error response
2047
2043
  # This ensures clients see the error rather than potentially stale data
2048
- return InvalidatePairs(
2044
+ return InvalidateEntries(
2049
2045
  options=self.options,
2050
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2046
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2051
2047
  next_state=CacheMiss(
2052
2048
  request=self.original_request,
2053
2049
  options=self.options,
2054
2050
  after_revalidation=True,
2055
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
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,
2051
+ ).next(
2052
+ revalidation_response,
2063
2053
  ),
2064
- options=self.options,
2065
2054
  )
2066
-
2067
- # ============================================================================
2068
- # STEP 4: Handle Unexpected Status Codes
2069
- # ============================================================================
2070
- # This should not happen in normal operation. Valid revalidation responses are:
2071
- # - 304 Not Modified
2072
- # - 2xx Success (typically 200 OK)
2073
- # - 5xx Server Error
2074
- #
2075
- # Other status codes (1xx, 3xx, 4xx) are unexpected during revalidation.
2076
- # 3xx redirects should have been followed by the HTTP client.
2077
- # 4xx errors (except 404) are unusual during revalidation.
2078
- raise RuntimeError(
2079
- f"Unexpected response status code during revalidation: {revalidation_response.status_code}"
2080
- ) # pragma: nocover
2055
+ else:
2056
+ # ============================================================================
2057
+ # STEP 4: Handle Unexpected Status Codes
2058
+ # ============================================================================
2059
+ # RFC 9111 does not define behavior for other status codes in this context.
2060
+ # In practice, we need to forward any unexpected responses to the client.
2061
+ return CacheMiss(
2062
+ request=self.revalidating_entries[-1].request,
2063
+ options=self.options,
2064
+ after_revalidation=True,
2065
+ ).next(revalidation_response)
2081
2066
 
2082
2067
  def freshening_stored_responses(
2083
2068
  self, revalidation_response: Response
2084
- ) -> "NeedToBeUpdated" | "InvalidatePairs" | "CacheMiss":
2069
+ ) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss":
2085
2070
  """
2086
2071
  Freshens cached responses after receiving a 304 Not Modified response.
2087
2072
 
@@ -2104,9 +2089,9 @@ class NeedRevalidation(State):
2104
2089
 
2105
2090
  Returns:
2106
2091
  -------
2107
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
2092
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
2108
2093
  - NeedToBeUpdated: When matching responses are found and updated
2109
- - InvalidatePairs: Wraps NeedToBeUpdated if non-matching responses exist
2094
+ - InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist
2110
2095
  - CacheMiss: When no matching responses are found
2111
2096
 
2112
2097
  RFC 9111 Compliance:
@@ -2167,7 +2152,7 @@ class NeedRevalidation(State):
2167
2152
  # Priority 2: Last-Modified timestamp
2168
2153
  # Priority 3: Single response assumption
2169
2154
 
2170
- identified_for_revalidation: list[CompletePair]
2155
+ identified_for_revalidation: list[Entry]
2171
2156
 
2172
2157
  # MATCHING STRATEGY 1: Strong ETag
2173
2158
  # RFC 9110 Section 8.8.3: ETag
@@ -2187,7 +2172,7 @@ class NeedRevalidation(State):
2187
2172
  # Found a strong ETag in the 304 response
2188
2173
  # Partition cached responses: matching vs non-matching ETags
2189
2174
  identified_for_revalidation, need_to_be_invalidated = partition(
2190
- self.revalidating_pairs,
2175
+ self.revalidating_entries,
2191
2176
  lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call]
2192
2177
  )
2193
2178
 
@@ -2205,7 +2190,7 @@ class NeedRevalidation(State):
2205
2190
  # Found Last-Modified in the 304 response
2206
2191
  # Partition cached responses: matching vs non-matching timestamps
2207
2192
  identified_for_revalidation, need_to_be_invalidated = partition(
2208
- self.revalidating_pairs,
2193
+ self.revalidating_entries,
2209
2194
  lambda pair: pair.response.headers.get("last-modified")
2210
2195
  == revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call]
2211
2196
  )
@@ -2219,14 +2204,20 @@ class NeedRevalidation(State):
2219
2204
  # we can safely assume that single response is the one being confirmed.
2220
2205
  # This handles cases where the server doesn't return validators in the 304.
2221
2206
  else:
2222
- if len(self.revalidating_pairs) == 1:
2207
+ if len(self.revalidating_entries) == 1:
2223
2208
  # Only one cached response - it must be the matching one
2224
- identified_for_revalidation, need_to_be_invalidated = [self.revalidating_pairs[0]], []
2209
+ identified_for_revalidation, need_to_be_invalidated = (
2210
+ [self.revalidating_entries[0]],
2211
+ [],
2212
+ )
2225
2213
  else:
2226
2214
  # Multiple cached responses but no validators to match them
2227
2215
  # We cannot determine which (if any) are valid
2228
2216
  # Conservative approach: invalidate all of them
2229
- identified_for_revalidation, need_to_be_invalidated = [], self.revalidating_pairs
2217
+ identified_for_revalidation, need_to_be_invalidated = (
2218
+ [],
2219
+ self.revalidating_entries,
2220
+ )
2230
2221
 
2231
2222
  # ============================================================================
2232
2223
  # STEP 2: Update Matching Responses or Create Cache Miss
@@ -2251,7 +2242,7 @@ class NeedRevalidation(State):
2251
2242
  # while excluding certain headers that shouldn't be updated
2252
2243
  # (Content-Encoding, Content-Type, Content-Range).
2253
2244
  next_state = NeedToBeUpdated(
2254
- updating_pairs=[
2245
+ updating_entries=[
2255
2246
  replace(
2256
2247
  pair,
2257
2248
  response=refresh_response_headers(pair.response, revalidation_response),
@@ -2285,9 +2276,9 @@ class NeedRevalidation(State):
2285
2276
 
2286
2277
  if need_to_be_invalidated:
2287
2278
  # Wrap the next state in an invalidation operation
2288
- return InvalidatePairs(
2279
+ return InvalidateEntries(
2289
2280
  options=self.options,
2290
- pair_ids=[pair.id for pair in need_to_be_invalidated],
2281
+ entry_ids=[entry.id for entry in need_to_be_invalidated],
2291
2282
  next_state=next_state,
2292
2283
  )
2293
2284
 
@@ -2295,28 +2286,12 @@ class NeedRevalidation(State):
2295
2286
  return next_state
2296
2287
 
2297
2288
 
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
-
2312
2289
  class StoreAndUse(State):
2313
2290
  """
2314
2291
  The state that indicates that the response can be stored in the cache and used.
2315
2292
 
2316
2293
  Attributes:
2317
2294
  ----------
2318
- pair_id : uuid.UUID
2319
- The unique identifier for the cache pair.
2320
2295
  response : Response
2321
2296
  The HTTP response to be stored in the cache.
2322
2297
  after_revalidation : bool
@@ -2324,16 +2299,17 @@ class StoreAndUse(State):
2324
2299
  """
2325
2300
 
2326
2301
  def __init__(
2327
- self, pair_id: uuid.UUID, response: Response, options: CacheOptions, after_revalidation: bool = False
2302
+ self,
2303
+ response: Response,
2304
+ options: CacheOptions,
2305
+ after_revalidation: bool = False,
2328
2306
  ) -> None:
2329
2307
  super().__init__(options)
2330
- self.pair_id = pair_id
2331
2308
  self.response = response
2332
2309
  self.after_revalidation = after_revalidation
2333
2310
  response_meta = ResponseMetadata(
2334
2311
  hishel_created_at=time.time(),
2335
2312
  hishel_from_cache=False,
2336
- hishel_spec_ignored=False,
2337
2313
  hishel_revalidated=after_revalidation,
2338
2314
  hishel_stored=True,
2339
2315
  )
@@ -2372,15 +2348,16 @@ class CouldNotBeStored(State):
2372
2348
  """
2373
2349
 
2374
2350
  def __init__(
2375
- self, response: Response, pair_id: uuid.UUID, options: CacheOptions, after_revalidation: bool = False
2351
+ self,
2352
+ response: Response,
2353
+ options: CacheOptions,
2354
+ after_revalidation: bool = False,
2376
2355
  ) -> None:
2377
2356
  super().__init__(options)
2378
2357
  self.response = response
2379
- self.pair_id = pair_id
2380
2358
  response_meta = ResponseMetadata(
2381
2359
  hishel_created_at=time.time(),
2382
2360
  hishel_from_cache=False,
2383
- hishel_spec_ignored=False,
2384
2361
  hishel_revalidated=after_revalidation,
2385
2362
  hishel_stored=False,
2386
2363
  )
@@ -2391,12 +2368,12 @@ class CouldNotBeStored(State):
2391
2368
 
2392
2369
 
2393
2370
  @dataclass
2394
- class InvalidatePairs(State):
2371
+ class InvalidateEntries(State):
2395
2372
  """
2396
- The state that represents the deletion of cache pairs.
2373
+ The state that represents the deletion of cache entries.
2397
2374
  """
2398
2375
 
2399
- pair_ids: list[uuid.UUID]
2376
+ entry_ids: list[uuid.UUID]
2400
2377
 
2401
2378
  next_state: AnyState
2402
2379
 
@@ -2405,18 +2382,22 @@ class InvalidatePairs(State):
2405
2382
 
2406
2383
 
2407
2384
  class FromCache(State):
2408
- def __init__(self, pair: CompletePair, options: CacheOptions, after_revalidation: bool = False) -> None:
2385
+ def __init__(
2386
+ self,
2387
+ entry: Entry,
2388
+ options: CacheOptions,
2389
+ after_revalidation: bool = False,
2390
+ ) -> None:
2409
2391
  super().__init__(options)
2410
- self.pair = pair
2392
+ self.entry = entry
2411
2393
  self.after_revalidation = after_revalidation
2412
2394
  response_meta = ResponseMetadata(
2413
- hishel_created_at=pair.meta.created_at,
2395
+ hishel_created_at=entry.meta.created_at,
2414
2396
  hishel_from_cache=True,
2415
- hishel_spec_ignored=False,
2416
2397
  hishel_revalidated=after_revalidation,
2417
2398
  hishel_stored=False,
2418
2399
  )
2419
- self.pair.response.metadata.update(response_meta) # type: ignore
2400
+ self.entry.response.metadata.update(response_meta) # type: ignore
2420
2401
 
2421
2402
  def next(self) -> None:
2422
2403
  return None
@@ -2424,8 +2405,8 @@ class FromCache(State):
2424
2405
 
2425
2406
  @dataclass
2426
2407
  class NeedToBeUpdated(State):
2427
- updating_pairs: list[CompletePair]
2408
+ updating_entries: list[Entry]
2428
2409
  original_request: Request
2429
2410
 
2430
2411
  def next(self) -> FromCache:
2431
- return FromCache(pair=self.updating_pairs[-1], options=self.options) # pragma: nocover
2412
+ return FromCache(entry=self.updating_entries[-1], options=self.options)
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import time
5
+ import typing as tp
6
+ import uuid
7
+
8
+ from ..models import Entry, Request, Response
9
+
10
+
11
+ class AsyncBaseStorage(abc.ABC):
12
+ @abc.abstractmethod
13
+ async def create_entry(self, request: Request, response: Response, key: str, id_: uuid.UUID | None = None) -> Entry:
14
+ raise NotImplementedError()
15
+
16
+ @abc.abstractmethod
17
+ async def get_entries(self, key: str) -> tp.List[Entry]:
18
+ raise NotImplementedError()
19
+
20
+ @abc.abstractmethod
21
+ async def update_entry(
22
+ self,
23
+ id: uuid.UUID,
24
+ new_entry: tp.Union[Entry, tp.Callable[[Entry], Entry]],
25
+ ) -> tp.Optional[Entry]:
26
+ raise NotImplementedError()
27
+
28
+ @abc.abstractmethod
29
+ async def remove_entry(self, id: uuid.UUID) -> None:
30
+ raise NotImplementedError()
31
+
32
+ async def close(self) -> None:
33
+ pass
34
+
35
+ def is_soft_deleted(self, pair: Entry) -> bool:
36
+ """
37
+ Check if a pair is soft deleted based on its metadata.
38
+
39
+ Args:
40
+ pair: The request pair to check.
41
+
42
+ Returns:
43
+ True if the pair is soft deleted, False otherwise.
44
+ """
45
+ return pair.meta.deleted_at is not None and pair.meta.deleted_at > 0
46
+
47
+ def is_safe_to_hard_delete(self, pair: Entry) -> bool:
48
+ """
49
+ Check if a pair is safe to hard delete based on its metadata.
50
+
51
+ If the pair has been soft deleted for more than 1 hour, it is considered safe to hard delete.
52
+
53
+ Args:
54
+ pair: The request pair to check.
55
+
56
+ Returns:
57
+ True if the pair is safe to hard delete, False otherwise.
58
+ """
59
+ return bool(pair.meta.deleted_at is not None and (pair.meta.deleted_at + 3600 < time.time()))
60
+
61
+ def mark_pair_as_deleted(self, pair: Entry) -> Entry:
62
+ """
63
+ Mark a pair as soft deleted by setting its deleted_at timestamp.
64
+
65
+ Args:
66
+ pair: The request pair to mark as deleted.
67
+ Returns:
68
+ The updated request pair with the deleted_at timestamp set.
69
+ """
70
+ pair.meta.deleted_at = time.time()
71
+ return pair