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/__init__.py +26 -17
- hishel/_async_cache.py +104 -65
- hishel/_async_httpx.py +236 -0
- hishel/_core/_headers.py +11 -1
- hishel/_core/_spec.py +101 -120
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/{_async/_storages/_sqlite.py → _storages/_async_sqlite.py} +100 -134
- hishel/_core/_storages/_packing.py +144 -0
- hishel/_core/_storages/_sync_base.py +71 -0
- hishel/_core/{_sync/_storages/_sqlite.py → _storages/_sync_sqlite.py} +100 -134
- hishel/_core/models.py +93 -33
- hishel/_policies.py +49 -0
- hishel/_sync_cache.py +104 -65
- hishel/_sync_httpx.py +236 -0
- hishel/_utils.py +49 -2
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +3 -326
- hishel/requests.py +28 -22
- {hishel-1.0.0.dev2.dist-info → hishel-1.1.0.dist-info}/METADATA +225 -18
- hishel-1.1.0.dist-info/RECORD +24 -0
- hishel/_core/__init__.py +0 -59
- hishel/_core/_base/_storages/_base.py +0 -272
- hishel/_core/_base/_storages/_packing.py +0 -165
- hishel-1.0.0.dev2.dist-info/RECORD +0 -19
- {hishel-1.0.0.dev2.dist-info → hishel-1.1.0.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev2.dist-info → hishel-1.1.0.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
A cached request-response
|
|
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
|
-
>>>
|
|
199
|
-
>>> vary_headers_match(request,
|
|
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
|
-
>>>
|
|
204
|
+
>>> entry = Entry(request=request1, response=response)
|
|
206
205
|
>>> request2 = Request(headers=Headers({"accept": "application/json"}))
|
|
207
|
-
>>> vary_headers_match(request2,
|
|
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,
|
|
211
|
+
>>> vary_headers_match(request2, entry)
|
|
213
212
|
False
|
|
214
213
|
|
|
215
214
|
>>> # Vary: * always fails
|
|
216
215
|
>>> response = Response(headers=Headers({"vary": "*"}))
|
|
217
|
-
>>>
|
|
218
|
-
>>> vary_headers_match(request2,
|
|
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 =
|
|
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) !=
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
1184
|
-
List of request-response
|
|
1185
|
-
this request. These
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
-
|
|
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
|
-
|
|
1881
|
-
The cached request-response
|
|
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
|
-
|
|
1889
|
+
revalidating_entries: list[Entry]
|
|
1896
1890
|
"""
|
|
1897
|
-
The stored
|
|
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", "
|
|
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,
|
|
1916
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
1923
1917
|
- NeedToBeUpdated: When 304 response allows cached responses to be freshened
|
|
1924
|
-
-
|
|
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
|
-
...
|
|
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,
|
|
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
|
|
2010
|
-
# The last
|
|
2011
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
|
2044
|
+
return InvalidateEntries(
|
|
2049
2045
|
options=self.options,
|
|
2050
|
-
|
|
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(
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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" | "
|
|
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,
|
|
2092
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
2108
2093
|
- NeedToBeUpdated: When matching responses are found and updated
|
|
2109
|
-
-
|
|
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[
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2279
|
+
return InvalidateEntries(
|
|
2289
2280
|
options=self.options,
|
|
2290
|
-
|
|
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,
|
|
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,
|
|
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
|
|
2371
|
+
class InvalidateEntries(State):
|
|
2395
2372
|
"""
|
|
2396
|
-
The state that represents the deletion of cache
|
|
2373
|
+
The state that represents the deletion of cache entries.
|
|
2397
2374
|
"""
|
|
2398
2375
|
|
|
2399
|
-
|
|
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__(
|
|
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.
|
|
2392
|
+
self.entry = entry
|
|
2411
2393
|
self.after_revalidation = after_revalidation
|
|
2412
2394
|
response_meta = ResponseMetadata(
|
|
2413
|
-
hishel_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.
|
|
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
|
-
|
|
2408
|
+
updating_entries: list[Entry]
|
|
2428
2409
|
original_request: Request
|
|
2429
2410
|
|
|
2430
2411
|
def next(self) -> FromCache:
|
|
2431
|
-
return FromCache(
|
|
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
|