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/__init__.py +15 -14
- hishel/_async_cache.py +42 -36
- hishel/_async_httpx.py +243 -0
- hishel/_core/_headers.py +11 -1
- hishel/_core/_spec.py +88 -84
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/{_async/_storages/_sqlite.py → _storages/_async_sqlite.py} +95 -132
- 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} +95 -132
- hishel/_core/models.py +6 -22
- hishel/_sync_cache.py +42 -36
- hishel/_sync_httpx.py +243 -0
- hishel/_utils.py +49 -2
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +3 -326
- hishel/requests.py +25 -17
- {hishel-1.0.0.dev2.dist-info → hishel-1.0.0.dev3.dist-info}/METADATA +100 -6
- hishel-1.0.0.dev3.dist-info/RECORD +23 -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.0.0.dev3.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev2.dist-info → hishel-1.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
A cached request-response
|
|
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
|
-
>>>
|
|
199
|
-
>>> vary_headers_match(request,
|
|
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
|
-
>>>
|
|
205
|
+
>>> entry = Entry(request=request1, response=response)
|
|
206
206
|
>>> request2 = Request(headers=Headers({"accept": "application/json"}))
|
|
207
|
-
>>> vary_headers_match(request2,
|
|
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,
|
|
212
|
+
>>> vary_headers_match(request2, entry)
|
|
213
213
|
False
|
|
214
214
|
|
|
215
215
|
>>> # Vary: * always fails
|
|
216
216
|
>>> response = Response(headers=Headers({"vary": "*"}))
|
|
217
|
-
>>>
|
|
218
|
-
>>> vary_headers_match(request2,
|
|
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 =
|
|
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) !=
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
1184
|
-
List of request-response
|
|
1185
|
-
this request. These
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
-
|
|
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
|
-
|
|
1881
|
-
The cached request-response
|
|
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
|
-
|
|
1896
|
+
revalidating_entries: list[Entry]
|
|
1896
1897
|
"""
|
|
1897
|
-
The stored
|
|
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", "
|
|
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,
|
|
1923
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
1923
1924
|
- NeedToBeUpdated: When 304 response allows cached responses to be freshened
|
|
1924
|
-
-
|
|
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
|
-
...
|
|
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,
|
|
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
|
|
2010
|
-
# The last
|
|
2011
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
|
2051
|
+
return InvalidateEntries(
|
|
2049
2052
|
options=self.options,
|
|
2050
|
-
|
|
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(
|
|
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.
|
|
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" | "
|
|
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,
|
|
2112
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
2108
2113
|
- NeedToBeUpdated: When matching responses are found and updated
|
|
2109
|
-
-
|
|
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[
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2299
|
+
return InvalidateEntries(
|
|
2289
2300
|
options=self.options,
|
|
2290
|
-
|
|
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,
|
|
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,
|
|
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
|
|
2393
|
+
class InvalidateEntries(State):
|
|
2395
2394
|
"""
|
|
2396
|
-
The state that represents the deletion of cache
|
|
2395
|
+
The state that represents the deletion of cache entries.
|
|
2397
2396
|
"""
|
|
2398
2397
|
|
|
2399
|
-
|
|
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__(
|
|
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
|
-
|
|
2431
|
+
updating_entries: list[Entry]
|
|
2428
2432
|
original_request: Request
|
|
2429
2433
|
|
|
2430
2434
|
def next(self) -> FromCache:
|
|
2431
|
-
return FromCache(pair=self.
|
|
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
|