hishel 1.0.0.dev2__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
@@ -20,7 +20,7 @@ from hishel._core.models import ResponseMetadata
20
20
  from hishel._utils import parse_date, partition
21
21
 
22
22
  if TYPE_CHECKING:
23
- from hishel import CompletePair, Request, Response
23
+ from hishel import Entry, Request, Response
24
24
 
25
25
 
26
26
  TState = TypeVar("TState", bound="State")
@@ -145,7 +145,7 @@ class State(ABC):
145
145
 
146
146
  def vary_headers_match(
147
147
  original_request: Request,
148
- associated_pair: CompletePair,
148
+ associated_entry: Entry,
149
149
  ) -> bool:
150
150
  """
151
151
  Determines if request headers match the Vary requirements of a cached response.
@@ -161,8 +161,8 @@ def vary_headers_match(
161
161
  ----------
162
162
  original_request : Request
163
163
  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
164
+ associated_entry : Entry
165
+ A cached request-response entry that might match the new request
166
166
 
167
167
  Returns:
168
168
  -------
@@ -195,31 +195,31 @@ def vary_headers_match(
195
195
  >>> # No Vary header - always matches
196
196
  >>> request = Request(headers=Headers({"accept": "application/json"}))
197
197
  >>> response = Response(headers=Headers({})) # No Vary
198
- >>> pair = CompletePair(request=request, response=response)
199
- >>> vary_headers_match(request, pair)
198
+ >>> entry = Entry(request=request, response=response)
199
+ >>> vary_headers_match(request, entry)
200
200
  True
201
201
 
202
202
  >>> # Vary: Accept with matching Accept header
203
203
  >>> request1 = Request(headers=Headers({"accept": "application/json"}))
204
204
  >>> response = Response(headers=Headers({"vary": "Accept"}))
205
- >>> pair = CompletePair(request=request1, response=response)
205
+ >>> entry = Entry(request=request1, response=response)
206
206
  >>> request2 = Request(headers=Headers({"accept": "application/json"}))
207
- >>> vary_headers_match(request2, pair)
207
+ >>> vary_headers_match(request2, entry)
208
208
  True
209
209
 
210
210
  >>> # Vary: Accept with non-matching Accept header
211
211
  >>> request2 = Request(headers=Headers({"accept": "application/xml"}))
212
- >>> vary_headers_match(request2, pair)
212
+ >>> vary_headers_match(request2, entry)
213
213
  False
214
214
 
215
215
  >>> # Vary: * always fails
216
216
  >>> response = Response(headers=Headers({"vary": "*"}))
217
- >>> pair = CompletePair(request=request1, response=response)
218
- >>> vary_headers_match(request2, pair)
217
+ >>> entry = Entry(request=request1, response=response)
218
+ >>> vary_headers_match(request2, entry)
219
219
  False
220
220
  """
221
221
  # Extract the Vary header from the cached response
222
- vary_header = associated_pair.response.headers.get("vary")
222
+ vary_header = associated_entry.response.headers.get("vary")
223
223
 
224
224
  # If no Vary header exists, any request matches
225
225
  # The response doesn't vary based on request headers
@@ -242,7 +242,7 @@ def vary_headers_match(
242
242
 
243
243
  # Compare the specific header value between original and new request
244
244
  # 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):
245
+ if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header):
246
246
  return False
247
247
 
248
248
  # All Vary headers matched
@@ -1121,7 +1121,7 @@ AnyState = Union[
1121
1121
  "NeedToBeUpdated",
1122
1122
  "NeedRevalidation",
1123
1123
  "IdleClient",
1124
- "InvalidatePairs",
1124
+ "InvalidateEntries",
1125
1125
  ]
1126
1126
 
1127
1127
  # Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods
@@ -1167,7 +1167,7 @@ class IdleClient(State):
1167
1167
  """
1168
1168
 
1169
1169
  def next(
1170
- self, request: Request, associated_pairs: list[CompletePair]
1170
+ self, request: Request, associated_entries: list[Entry]
1171
1171
  ) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]:
1172
1172
  """
1173
1173
  Determines the next state transition based on the request and available cached responses.
@@ -1180,9 +1180,9 @@ class IdleClient(State):
1180
1180
  ----------
1181
1181
  request : Request
1182
1182
  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).
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).
1186
1186
 
1187
1187
  Returns:
1188
1188
  -------
@@ -1317,7 +1317,7 @@ class IdleClient(State):
1317
1317
  #
1318
1318
  # If a cached response has Cache-Control: no-cache, it cannot be reused without
1319
1319
  # validation, regardless of its freshness.
1320
- def no_cache_missing(pair: CompletePair) -> bool:
1320
+ def no_cache_missing(pair: Entry) -> bool:
1321
1321
  """Check if the cached response lacks the no-cache directive."""
1322
1322
  return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False
1323
1323
 
@@ -1332,7 +1332,7 @@ class IdleClient(State):
1332
1332
  #
1333
1333
  # Note: Condition 5.3 (successfully validated) is handled in the
1334
1334
  # NeedRevalidation state, not here.
1335
- def fresh_or_allowed_stale(pair: CompletePair) -> bool:
1335
+ def fresh_or_allowed_stale(pair: Entry) -> bool:
1336
1336
  """
1337
1337
  Determine if a cached response is fresh or allowed to be served stale.
1338
1338
 
@@ -1361,7 +1361,7 @@ class IdleClient(State):
1361
1361
  # "ready to use" and "needs revalidation" groups.
1362
1362
  filtered_pairs = [
1363
1363
  pair
1364
- for pair in associated_pairs
1364
+ for pair in associated_entries
1365
1365
  if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call]
1366
1366
  ]
1367
1367
 
@@ -1455,7 +1455,7 @@ class IdleClient(State):
1455
1455
  # (ETag, Last-Modified) from the cached response.
1456
1456
  return NeedRevalidation(
1457
1457
  request=make_conditional_request(request, need_revalidation[-1].response),
1458
- revalidating_pairs=need_revalidation,
1458
+ revalidating_entries=need_revalidation,
1459
1459
  options=self.options,
1460
1460
  original_request=request,
1461
1461
  )
@@ -1525,7 +1525,7 @@ class CacheMiss(State):
1525
1525
  Indicates whether the cache miss occurred after a revalidation attempt.
1526
1526
  """
1527
1527
 
1528
- def next(self, response: Response, pair_id: uuid.UUID) -> Union["StoreAndUse", "CouldNotBeStored"]:
1528
+ def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]:
1529
1529
  """
1530
1530
  Evaluates whether a response can be stored in the cache.
1531
1531
 
@@ -1582,7 +1582,7 @@ class CacheMiss(State):
1582
1582
  ... status_code=200,
1583
1583
  ... headers=Headers({"cache-control": "max-age=3600"})
1584
1584
  ... )
1585
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1585
+ >>> next_state = cache_miss.next(response)
1586
1586
  >>> isinstance(next_state, StoreAndUse)
1587
1587
  True
1588
1588
 
@@ -1591,7 +1591,7 @@ class CacheMiss(State):
1591
1591
  ... status_code=200,
1592
1592
  ... headers=Headers({"cache-control": "no-store"})
1593
1593
  ... )
1594
- >>> next_state = cache_miss.next(response, uuid.uuid4())
1594
+ >>> next_state = cache_miss.next(response)
1595
1595
  >>> isinstance(next_state, CouldNotBeStored)
1596
1596
  True
1597
1597
  """
@@ -1816,7 +1816,9 @@ class CacheMiss(State):
1816
1816
  )
1817
1817
 
1818
1818
  return CouldNotBeStored(
1819
- response=response, pair_id=pair_id, options=self.options, after_revalidation=self.after_revalidation
1819
+ response=response,
1820
+ options=self.options,
1821
+ after_revalidation=self.after_revalidation,
1820
1822
  )
1821
1823
 
1822
1824
  # --------------------------------------------------------------------
@@ -1833,7 +1835,6 @@ class CacheMiss(State):
1833
1835
  cleaned_response = exclude_unstorable_headers(response, self.options.shared)
1834
1836
 
1835
1837
  return StoreAndUse(
1836
- pair_id=pair_id,
1837
1838
  response=cleaned_response,
1838
1839
  options=self.options,
1839
1840
  after_revalidation=self.after_revalidation,
@@ -1856,7 +1857,7 @@ class NeedRevalidation(State):
1856
1857
  State Transitions:
1857
1858
  -----------------
1858
1859
  - NeedToBeUpdated: 304 response received, cached responses can be freshened
1859
- - InvalidatePairs + CacheMiss: 2xx/5xx response received, new response must be cached
1860
+ - InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached
1860
1861
  - CacheMiss: No matching responses found during freshening
1861
1862
 
1862
1863
  RFC 9111 References:
@@ -1877,8 +1878,8 @@ class NeedRevalidation(State):
1877
1878
  original_request : Request
1878
1879
  The original client request (without conditional headers) that initiated
1879
1880
  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
1881
+ revalidating_entries : list[Entry]
1882
+ The cached request-response entries that are being revalidated. These are
1882
1883
  stale responses that might still be usable if the server confirms they
1883
1884
  haven't changed (304 response).
1884
1885
  options : CacheOptions
@@ -1892,14 +1893,14 @@ class NeedRevalidation(State):
1892
1893
 
1893
1894
  original_request: Request
1894
1895
 
1895
- revalidating_pairs: list[CompletePair]
1896
+ revalidating_entries: list[Entry]
1896
1897
  """
1897
- The stored pairs that the request was sent for revalidation.
1898
+ The stored entries that the request was sent for revalidation.
1898
1899
  """
1899
1900
 
1900
1901
  def next(
1901
1902
  self, revalidation_response: Response
1902
- ) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss", "FromCache"]:
1903
+ ) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache"]:
1903
1904
  """
1904
1905
  Handles the response to a conditional request and determines the next state.
1905
1906
 
@@ -1919,9 +1920,9 @@ class NeedRevalidation(State):
1919
1920
 
1920
1921
  Returns:
1921
1922
  -------
1922
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
1923
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
1923
1924
  - NeedToBeUpdated: When 304 response allows cached responses to be freshened
1924
- - InvalidatePairs: When old responses must be invalidated (wraps next state)
1925
+ - InvalidateEntries: When old responses must be invalidated (wraps next state)
1925
1926
  - CacheMiss: When no matching responses found or storing new response
1926
1927
 
1927
1928
  RFC 9111 Compliance:
@@ -1956,7 +1957,7 @@ class NeedRevalidation(State):
1956
1957
  >>> need_revalidation = NeedRevalidation(
1957
1958
  ... request=conditional_request,
1958
1959
  ... original_request=original_request,
1959
- ... revalidating_pairs=[cached_pair],
1960
+ ... revalidating_entries=[cached_entry],
1960
1961
  ... options=default_options
1961
1962
  ... )
1962
1963
  >>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'}))
@@ -1967,7 +1968,7 @@ class NeedRevalidation(State):
1967
1968
  >>> # 200 OK - use new response
1968
1969
  >>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"}))
1969
1970
  >>> next_state = need_revalidation.next(response_200)
1970
- >>> isinstance(next_state, InvalidatePairs)
1971
+ >>> isinstance(next_state, InvalidateEntries)
1971
1972
  True
1972
1973
  """
1973
1974
 
@@ -2006,17 +2007,19 @@ class NeedRevalidation(State):
2006
2007
  # 2. Store the new response (if cacheable)
2007
2008
  # 3. Use the new response to satisfy the request
2008
2009
  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(
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(
2012
2013
  options=self.options,
2013
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2014
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2014
2015
  # After invalidation, attempt to cache the new response
2015
2016
  next_state=CacheMiss(
2016
2017
  request=self.original_request,
2017
2018
  options=self.options,
2018
2019
  after_revalidation=True, # Mark that this occurred during revalidation
2019
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2020
+ ).next(
2021
+ revalidation_response,
2022
+ ),
2020
2023
  )
2021
2024
 
2022
2025
  # ============================================================================
@@ -2045,20 +2048,22 @@ class NeedRevalidation(State):
2045
2048
  elif revalidation_response.status_code // 100 == 5:
2046
2049
  # Same as 2xx: invalidate old responses and store the error response
2047
2050
  # This ensures clients see the error rather than potentially stale data
2048
- return InvalidatePairs(
2051
+ return InvalidateEntries(
2049
2052
  options=self.options,
2050
- pair_ids=[pair.id for pair in self.revalidating_pairs[:-1]],
2053
+ entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
2051
2054
  next_state=CacheMiss(
2052
2055
  request=self.original_request,
2053
2056
  options=self.options,
2054
2057
  after_revalidation=True,
2055
- ).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
2058
+ ).next(
2059
+ revalidation_response,
2060
+ ),
2056
2061
  )
2057
2062
  elif revalidation_response.status_code // 100 == 3:
2058
2063
  # 3xx Redirects should have been followed by the HTTP client
2059
2064
  return FromCache(
2060
2065
  pair=replace(
2061
- self.revalidating_pairs[-1],
2066
+ self.revalidating_entries[-1],
2062
2067
  response=revalidation_response,
2063
2068
  ),
2064
2069
  options=self.options,
@@ -2081,7 +2086,7 @@ class NeedRevalidation(State):
2081
2086
 
2082
2087
  def freshening_stored_responses(
2083
2088
  self, revalidation_response: Response
2084
- ) -> "NeedToBeUpdated" | "InvalidatePairs" | "CacheMiss":
2089
+ ) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss":
2085
2090
  """
2086
2091
  Freshens cached responses after receiving a 304 Not Modified response.
2087
2092
 
@@ -2104,9 +2109,9 @@ class NeedRevalidation(State):
2104
2109
 
2105
2110
  Returns:
2106
2111
  -------
2107
- Union[NeedToBeUpdated, InvalidatePairs, CacheMiss]
2112
+ Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
2108
2113
  - NeedToBeUpdated: When matching responses are found and updated
2109
- - InvalidatePairs: Wraps NeedToBeUpdated if non-matching responses exist
2114
+ - InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist
2110
2115
  - CacheMiss: When no matching responses are found
2111
2116
 
2112
2117
  RFC 9111 Compliance:
@@ -2167,7 +2172,7 @@ class NeedRevalidation(State):
2167
2172
  # Priority 2: Last-Modified timestamp
2168
2173
  # Priority 3: Single response assumption
2169
2174
 
2170
- identified_for_revalidation: list[CompletePair]
2175
+ identified_for_revalidation: list[Entry]
2171
2176
 
2172
2177
  # MATCHING STRATEGY 1: Strong ETag
2173
2178
  # RFC 9110 Section 8.8.3: ETag
@@ -2187,7 +2192,7 @@ class NeedRevalidation(State):
2187
2192
  # Found a strong ETag in the 304 response
2188
2193
  # Partition cached responses: matching vs non-matching ETags
2189
2194
  identified_for_revalidation, need_to_be_invalidated = partition(
2190
- self.revalidating_pairs,
2195
+ self.revalidating_entries,
2191
2196
  lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call]
2192
2197
  )
2193
2198
 
@@ -2205,7 +2210,7 @@ class NeedRevalidation(State):
2205
2210
  # Found Last-Modified in the 304 response
2206
2211
  # Partition cached responses: matching vs non-matching timestamps
2207
2212
  identified_for_revalidation, need_to_be_invalidated = partition(
2208
- self.revalidating_pairs,
2213
+ self.revalidating_entries,
2209
2214
  lambda pair: pair.response.headers.get("last-modified")
2210
2215
  == revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call]
2211
2216
  )
@@ -2219,14 +2224,20 @@ class NeedRevalidation(State):
2219
2224
  # we can safely assume that single response is the one being confirmed.
2220
2225
  # This handles cases where the server doesn't return validators in the 304.
2221
2226
  else:
2222
- if len(self.revalidating_pairs) == 1:
2227
+ if len(self.revalidating_entries) == 1:
2223
2228
  # Only one cached response - it must be the matching one
2224
- 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
+ )
2225
2233
  else:
2226
2234
  # Multiple cached responses but no validators to match them
2227
2235
  # We cannot determine which (if any) are valid
2228
2236
  # Conservative approach: invalidate all of them
2229
- identified_for_revalidation, need_to_be_invalidated = [], self.revalidating_pairs
2237
+ identified_for_revalidation, need_to_be_invalidated = (
2238
+ [],
2239
+ self.revalidating_entries,
2240
+ )
2230
2241
 
2231
2242
  # ============================================================================
2232
2243
  # STEP 2: Update Matching Responses or Create Cache Miss
@@ -2251,7 +2262,7 @@ class NeedRevalidation(State):
2251
2262
  # while excluding certain headers that shouldn't be updated
2252
2263
  # (Content-Encoding, Content-Type, Content-Range).
2253
2264
  next_state = NeedToBeUpdated(
2254
- updating_pairs=[
2265
+ updating_entries=[
2255
2266
  replace(
2256
2267
  pair,
2257
2268
  response=refresh_response_headers(pair.response, revalidation_response),
@@ -2285,9 +2296,9 @@ class NeedRevalidation(State):
2285
2296
 
2286
2297
  if need_to_be_invalidated:
2287
2298
  # Wrap the next state in an invalidation operation
2288
- return InvalidatePairs(
2299
+ return InvalidateEntries(
2289
2300
  options=self.options,
2290
- pair_ids=[pair.id for pair in need_to_be_invalidated],
2301
+ entry_ids=[entry.id for entry in need_to_be_invalidated],
2291
2302
  next_state=next_state,
2292
2303
  )
2293
2304
 
@@ -2295,28 +2306,12 @@ class NeedRevalidation(State):
2295
2306
  return next_state
2296
2307
 
2297
2308
 
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
2309
  class StoreAndUse(State):
2313
2310
  """
2314
2311
  The state that indicates that the response can be stored in the cache and used.
2315
2312
 
2316
2313
  Attributes:
2317
2314
  ----------
2318
- pair_id : uuid.UUID
2319
- The unique identifier for the cache pair.
2320
2315
  response : Response
2321
2316
  The HTTP response to be stored in the cache.
2322
2317
  after_revalidation : bool
@@ -2324,10 +2319,12 @@ class StoreAndUse(State):
2324
2319
  """
2325
2320
 
2326
2321
  def __init__(
2327
- self, pair_id: uuid.UUID, response: Response, options: CacheOptions, after_revalidation: bool = False
2322
+ self,
2323
+ response: Response,
2324
+ options: CacheOptions,
2325
+ after_revalidation: bool = False,
2328
2326
  ) -> None:
2329
2327
  super().__init__(options)
2330
- self.pair_id = pair_id
2331
2328
  self.response = response
2332
2329
  self.after_revalidation = after_revalidation
2333
2330
  response_meta = ResponseMetadata(
@@ -2372,11 +2369,13 @@ class CouldNotBeStored(State):
2372
2369
  """
2373
2370
 
2374
2371
  def __init__(
2375
- self, response: Response, pair_id: uuid.UUID, options: CacheOptions, after_revalidation: bool = False
2372
+ self,
2373
+ response: Response,
2374
+ options: CacheOptions,
2375
+ after_revalidation: bool = False,
2376
2376
  ) -> None:
2377
2377
  super().__init__(options)
2378
2378
  self.response = response
2379
- self.pair_id = pair_id
2380
2379
  response_meta = ResponseMetadata(
2381
2380
  hishel_created_at=time.time(),
2382
2381
  hishel_from_cache=False,
@@ -2391,12 +2390,12 @@ class CouldNotBeStored(State):
2391
2390
 
2392
2391
 
2393
2392
  @dataclass
2394
- class InvalidatePairs(State):
2393
+ class InvalidateEntries(State):
2395
2394
  """
2396
- The state that represents the deletion of cache pairs.
2395
+ The state that represents the deletion of cache entries.
2397
2396
  """
2398
2397
 
2399
- pair_ids: list[uuid.UUID]
2398
+ entry_ids: list[uuid.UUID]
2400
2399
 
2401
2400
  next_state: AnyState
2402
2401
 
@@ -2405,7 +2404,12 @@ class InvalidatePairs(State):
2405
2404
 
2406
2405
 
2407
2406
  class FromCache(State):
2408
- def __init__(self, pair: CompletePair, options: CacheOptions, after_revalidation: bool = False) -> None:
2407
+ def __init__(
2408
+ self,
2409
+ pair: Entry,
2410
+ options: CacheOptions,
2411
+ after_revalidation: bool = False,
2412
+ ) -> None:
2409
2413
  super().__init__(options)
2410
2414
  self.pair = pair
2411
2415
  self.after_revalidation = after_revalidation
@@ -2424,8 +2428,8 @@ class FromCache(State):
2424
2428
 
2425
2429
  @dataclass
2426
2430
  class NeedToBeUpdated(State):
2427
- updating_pairs: list[CompletePair]
2431
+ updating_entries: list[Entry]
2428
2432
  original_request: Request
2429
2433
 
2430
2434
  def next(self) -> FromCache:
2431
- 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
@@ -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