hishel 1.0.0.dev1__py3-none-any.whl → 1.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hishel/__init__.py +15 -14
- hishel/_async_cache.py +50 -37
- hishel/_async_httpx.py +243 -0
- hishel/_core/_headers.py +11 -1
- hishel/_core/_spec.py +184 -127
- 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 +13 -26
- hishel/_sync_cache.py +50 -37
- hishel/_sync_httpx.py +243 -0
- hishel/_utils.py +48 -137
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +3 -326
- hishel/requests.py +25 -17
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev3.dist-info}/METADATA +139 -27
- 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.dev1.dist-info/RECORD +0 -19
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev3.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
hishel/_core/_spec.py
CHANGED
|
@@ -16,10 +16,11 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from hishel._core._headers import Headers, Range, Vary, parse_cache_control
|
|
19
|
+
from hishel._core.models import ResponseMetadata
|
|
19
20
|
from hishel._utils import parse_date, partition
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
|
-
from hishel import
|
|
23
|
+
from hishel import Entry, Request, Response
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
TState = TypeVar("TState", bound="State")
|
|
@@ -144,7 +145,7 @@ class State(ABC):
|
|
|
144
145
|
|
|
145
146
|
def vary_headers_match(
|
|
146
147
|
original_request: Request,
|
|
147
|
-
|
|
148
|
+
associated_entry: Entry,
|
|
148
149
|
) -> bool:
|
|
149
150
|
"""
|
|
150
151
|
Determines if request headers match the Vary requirements of a cached response.
|
|
@@ -160,8 +161,8 @@ def vary_headers_match(
|
|
|
160
161
|
----------
|
|
161
162
|
original_request : Request
|
|
162
163
|
The new incoming request that we're trying to satisfy
|
|
163
|
-
|
|
164
|
-
A cached request-response
|
|
164
|
+
associated_entry : Entry
|
|
165
|
+
A cached request-response entry that might match the new request
|
|
165
166
|
|
|
166
167
|
Returns:
|
|
167
168
|
-------
|
|
@@ -194,31 +195,31 @@ def vary_headers_match(
|
|
|
194
195
|
>>> # No Vary header - always matches
|
|
195
196
|
>>> request = Request(headers=Headers({"accept": "application/json"}))
|
|
196
197
|
>>> response = Response(headers=Headers({})) # No Vary
|
|
197
|
-
>>>
|
|
198
|
-
>>> vary_headers_match(request,
|
|
198
|
+
>>> entry = Entry(request=request, response=response)
|
|
199
|
+
>>> vary_headers_match(request, entry)
|
|
199
200
|
True
|
|
200
201
|
|
|
201
202
|
>>> # Vary: Accept with matching Accept header
|
|
202
203
|
>>> request1 = Request(headers=Headers({"accept": "application/json"}))
|
|
203
204
|
>>> response = Response(headers=Headers({"vary": "Accept"}))
|
|
204
|
-
>>>
|
|
205
|
+
>>> entry = Entry(request=request1, response=response)
|
|
205
206
|
>>> request2 = Request(headers=Headers({"accept": "application/json"}))
|
|
206
|
-
>>> vary_headers_match(request2,
|
|
207
|
+
>>> vary_headers_match(request2, entry)
|
|
207
208
|
True
|
|
208
209
|
|
|
209
210
|
>>> # Vary: Accept with non-matching Accept header
|
|
210
211
|
>>> request2 = Request(headers=Headers({"accept": "application/xml"}))
|
|
211
|
-
>>> vary_headers_match(request2,
|
|
212
|
+
>>> vary_headers_match(request2, entry)
|
|
212
213
|
False
|
|
213
214
|
|
|
214
215
|
>>> # Vary: * always fails
|
|
215
216
|
>>> response = Response(headers=Headers({"vary": "*"}))
|
|
216
|
-
>>>
|
|
217
|
-
>>> vary_headers_match(request2,
|
|
217
|
+
>>> entry = Entry(request=request1, response=response)
|
|
218
|
+
>>> vary_headers_match(request2, entry)
|
|
218
219
|
False
|
|
219
220
|
"""
|
|
220
221
|
# Extract the Vary header from the cached response
|
|
221
|
-
vary_header =
|
|
222
|
+
vary_header = associated_entry.response.headers.get("vary")
|
|
222
223
|
|
|
223
224
|
# If no Vary header exists, any request matches
|
|
224
225
|
# The response doesn't vary based on request headers
|
|
@@ -241,7 +242,7 @@ def vary_headers_match(
|
|
|
241
242
|
|
|
242
243
|
# Compare the specific header value between original and new request
|
|
243
244
|
# Both headers must have the same value (or both be absent)
|
|
244
|
-
if original_request.headers.get(vary_header) !=
|
|
245
|
+
if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header):
|
|
245
246
|
return False
|
|
246
247
|
|
|
247
248
|
# All Vary headers matched
|
|
@@ -1120,7 +1121,7 @@ AnyState = Union[
|
|
|
1120
1121
|
"NeedToBeUpdated",
|
|
1121
1122
|
"NeedRevalidation",
|
|
1122
1123
|
"IdleClient",
|
|
1123
|
-
"
|
|
1124
|
+
"InvalidateEntries",
|
|
1124
1125
|
]
|
|
1125
1126
|
|
|
1126
1127
|
# Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods
|
|
@@ -1166,7 +1167,7 @@ class IdleClient(State):
|
|
|
1166
1167
|
"""
|
|
1167
1168
|
|
|
1168
1169
|
def next(
|
|
1169
|
-
self, request: Request,
|
|
1170
|
+
self, request: Request, associated_entries: list[Entry]
|
|
1170
1171
|
) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]:
|
|
1171
1172
|
"""
|
|
1172
1173
|
Determines the next state transition based on the request and available cached responses.
|
|
@@ -1179,9 +1180,9 @@ class IdleClient(State):
|
|
|
1179
1180
|
----------
|
|
1180
1181
|
request : Request
|
|
1181
1182
|
The incoming HTTP request from the client
|
|
1182
|
-
|
|
1183
|
-
List of request-response
|
|
1184
|
-
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).
|
|
1185
1186
|
|
|
1186
1187
|
Returns:
|
|
1187
1188
|
-------
|
|
@@ -1316,7 +1317,7 @@ class IdleClient(State):
|
|
|
1316
1317
|
#
|
|
1317
1318
|
# If a cached response has Cache-Control: no-cache, it cannot be reused without
|
|
1318
1319
|
# validation, regardless of its freshness.
|
|
1319
|
-
def no_cache_missing(pair:
|
|
1320
|
+
def no_cache_missing(pair: Entry) -> bool:
|
|
1320
1321
|
"""Check if the cached response lacks the no-cache directive."""
|
|
1321
1322
|
return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False
|
|
1322
1323
|
|
|
@@ -1331,7 +1332,7 @@ class IdleClient(State):
|
|
|
1331
1332
|
#
|
|
1332
1333
|
# Note: Condition 5.3 (successfully validated) is handled in the
|
|
1333
1334
|
# NeedRevalidation state, not here.
|
|
1334
|
-
def fresh_or_allowed_stale(pair:
|
|
1335
|
+
def fresh_or_allowed_stale(pair: Entry) -> bool:
|
|
1335
1336
|
"""
|
|
1336
1337
|
Determine if a cached response is fresh or allowed to be served stale.
|
|
1337
1338
|
|
|
@@ -1360,7 +1361,7 @@ class IdleClient(State):
|
|
|
1360
1361
|
# "ready to use" and "needs revalidation" groups.
|
|
1361
1362
|
filtered_pairs = [
|
|
1362
1363
|
pair
|
|
1363
|
-
for pair in
|
|
1364
|
+
for pair in associated_entries
|
|
1364
1365
|
if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call]
|
|
1365
1366
|
]
|
|
1366
1367
|
|
|
@@ -1412,16 +1413,11 @@ class IdleClient(State):
|
|
|
1412
1413
|
#
|
|
1413
1414
|
# The Age header informs the client how old the cached response is.
|
|
1414
1415
|
|
|
1415
|
-
# Mark all ready-to-use responses with metadata (for observability)
|
|
1416
|
-
for pair in ready_to_use:
|
|
1417
|
-
pair.response.metadata["hishel_from_cache"] = True # type: ignore
|
|
1418
|
-
|
|
1419
1416
|
# Use the most recent response (first in sorted list)
|
|
1420
1417
|
selected_pair = ready_to_use[0]
|
|
1421
1418
|
|
|
1422
1419
|
# Calculate current age and update the Age header
|
|
1423
1420
|
current_age = get_age(selected_pair.response)
|
|
1424
|
-
|
|
1425
1421
|
return FromCache(
|
|
1426
1422
|
pair=replace(
|
|
1427
1423
|
selected_pair,
|
|
@@ -1459,7 +1455,7 @@ class IdleClient(State):
|
|
|
1459
1455
|
# (ETag, Last-Modified) from the cached response.
|
|
1460
1456
|
return NeedRevalidation(
|
|
1461
1457
|
request=make_conditional_request(request, need_revalidation[-1].response),
|
|
1462
|
-
|
|
1458
|
+
revalidating_entries=need_revalidation,
|
|
1463
1459
|
options=self.options,
|
|
1464
1460
|
original_request=request,
|
|
1465
1461
|
)
|
|
@@ -1529,7 +1525,7 @@ class CacheMiss(State):
|
|
|
1529
1525
|
Indicates whether the cache miss occurred after a revalidation attempt.
|
|
1530
1526
|
"""
|
|
1531
1527
|
|
|
1532
|
-
def next(self, response: Response
|
|
1528
|
+
def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]:
|
|
1533
1529
|
"""
|
|
1534
1530
|
Evaluates whether a response can be stored in the cache.
|
|
1535
1531
|
|
|
@@ -1578,20 +1574,6 @@ class CacheMiss(State):
|
|
|
1578
1574
|
* an s-maxage response directive (if cache is shared)
|
|
1579
1575
|
* a status code that is defined as heuristically cacheable"
|
|
1580
1576
|
|
|
1581
|
-
Side Effects:
|
|
1582
|
-
------------
|
|
1583
|
-
Sets metadata flags on the response object:
|
|
1584
|
-
- hishel_spec_ignored: False (caching spec is being followed)
|
|
1585
|
-
- hishel_from_cache: False (response is from origin, not cache)
|
|
1586
|
-
- hishel_revalidated: True (if after_revalidation is True)
|
|
1587
|
-
- hishel_stored: True/False (whether response was stored)
|
|
1588
|
-
|
|
1589
|
-
Logging:
|
|
1590
|
-
-------
|
|
1591
|
-
When a response cannot be stored, detailed debug logs are emitted explaining
|
|
1592
|
-
which specific RFC requirement failed, with direct links to the relevant
|
|
1593
|
-
RFC sections.
|
|
1594
|
-
|
|
1595
1577
|
Examples:
|
|
1596
1578
|
--------
|
|
1597
1579
|
>>> # Cacheable response
|
|
@@ -1600,7 +1582,7 @@ class CacheMiss(State):
|
|
|
1600
1582
|
... status_code=200,
|
|
1601
1583
|
... headers=Headers({"cache-control": "max-age=3600"})
|
|
1602
1584
|
... )
|
|
1603
|
-
>>> next_state = cache_miss.next(response
|
|
1585
|
+
>>> next_state = cache_miss.next(response)
|
|
1604
1586
|
>>> isinstance(next_state, StoreAndUse)
|
|
1605
1587
|
True
|
|
1606
1588
|
|
|
@@ -1609,26 +1591,11 @@ class CacheMiss(State):
|
|
|
1609
1591
|
... status_code=200,
|
|
1610
1592
|
... headers=Headers({"cache-control": "no-store"})
|
|
1611
1593
|
... )
|
|
1612
|
-
>>> next_state = cache_miss.next(response
|
|
1594
|
+
>>> next_state = cache_miss.next(response)
|
|
1613
1595
|
>>> isinstance(next_state, CouldNotBeStored)
|
|
1614
1596
|
True
|
|
1615
1597
|
"""
|
|
1616
1598
|
|
|
1617
|
-
# ============================================================================
|
|
1618
|
-
# STEP 1: Set Response Metadata
|
|
1619
|
-
# ============================================================================
|
|
1620
|
-
# Initialize metadata flags to track the response lifecycle
|
|
1621
|
-
|
|
1622
|
-
response.metadata["hishel_spec_ignored"] = False # type: ignore
|
|
1623
|
-
# We are following the caching specification
|
|
1624
|
-
|
|
1625
|
-
response.metadata["hishel_from_cache"] = False # type: ignore
|
|
1626
|
-
# This response came from origin server, not cache
|
|
1627
|
-
|
|
1628
|
-
if self.after_revalidation:
|
|
1629
|
-
response.metadata["hishel_revalidated"] = True # type: ignore
|
|
1630
|
-
# Mark that this response is the result of a revalidation
|
|
1631
|
-
|
|
1632
1599
|
# ============================================================================
|
|
1633
1600
|
# STEP 2: Parse Cache-Control Directive
|
|
1634
1601
|
# ============================================================================
|
|
@@ -1723,11 +1690,14 @@ class CacheMiss(State):
|
|
|
1723
1690
|
#
|
|
1724
1691
|
# Requests with Authorization headers often contain user-specific data.
|
|
1725
1692
|
# Shared caches must be careful not to serve one user's data to another.
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1693
|
+
has_explicit_directive = (
|
|
1694
|
+
response_cache_control.public
|
|
1695
|
+
or response_cache_control.s_maxage is not None
|
|
1696
|
+
or response_cache_control.must_revalidate
|
|
1697
|
+
)
|
|
1698
|
+
can_cache_auth_request = (
|
|
1699
|
+
not self.options.shared or "authorization" not in request.headers or has_explicit_directive
|
|
1700
|
+
)
|
|
1731
1701
|
|
|
1732
1702
|
# CONDITION 7: Response Contains Required Caching Information
|
|
1733
1703
|
# RFC 9111 Section 3, paragraph 2.7:
|
|
@@ -1797,7 +1767,7 @@ class CacheMiss(State):
|
|
|
1797
1767
|
or not understands_how_to_cache
|
|
1798
1768
|
or not no_store_is_not_present
|
|
1799
1769
|
or not private_directive_allows_storing
|
|
1800
|
-
or not
|
|
1770
|
+
or not can_cache_auth_request
|
|
1801
1771
|
or not contains_required_component
|
|
1802
1772
|
):
|
|
1803
1773
|
# --------------------------------------------------------------------
|
|
@@ -1833,10 +1803,11 @@ class CacheMiss(State):
|
|
|
1833
1803
|
"Cannot store the response because the `private` response directive does not "
|
|
1834
1804
|
"allow shared caches to store it. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.5.1"
|
|
1835
1805
|
)
|
|
1836
|
-
elif not
|
|
1806
|
+
elif not can_cache_auth_request:
|
|
1837
1807
|
logger.debug(
|
|
1838
|
-
"Cannot store the response because the
|
|
1839
|
-
"
|
|
1808
|
+
"Cannot store the response because the request contained an Authorization header "
|
|
1809
|
+
"and there was no explicit directive allowing shared caching. "
|
|
1810
|
+
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-5"
|
|
1840
1811
|
)
|
|
1841
1812
|
elif not contains_required_component:
|
|
1842
1813
|
logger.debug(
|
|
@@ -1844,10 +1815,11 @@ class CacheMiss(State):
|
|
|
1844
1815
|
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1"
|
|
1845
1816
|
)
|
|
1846
1817
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1818
|
+
return CouldNotBeStored(
|
|
1819
|
+
response=response,
|
|
1820
|
+
options=self.options,
|
|
1821
|
+
after_revalidation=self.after_revalidation,
|
|
1822
|
+
)
|
|
1851
1823
|
|
|
1852
1824
|
# --------------------------------------------------------------------
|
|
1853
1825
|
# Transition to: StoreAndUse
|
|
@@ -1856,9 +1828,6 @@ class CacheMiss(State):
|
|
|
1856
1828
|
|
|
1857
1829
|
logger.debug("Storing response in cache")
|
|
1858
1830
|
|
|
1859
|
-
# Mark response as stored
|
|
1860
|
-
response.metadata["hishel_stored"] = True # type: ignore
|
|
1861
|
-
|
|
1862
1831
|
# Remove headers that should not be stored
|
|
1863
1832
|
# RFC 9111 Section 3.1: Storing Header and Trailer Fields
|
|
1864
1833
|
# https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1
|
|
@@ -1866,9 +1835,9 @@ class CacheMiss(State):
|
|
|
1866
1835
|
cleaned_response = exclude_unstorable_headers(response, self.options.shared)
|
|
1867
1836
|
|
|
1868
1837
|
return StoreAndUse(
|
|
1869
|
-
pair_id=pair_id,
|
|
1870
1838
|
response=cleaned_response,
|
|
1871
1839
|
options=self.options,
|
|
1840
|
+
after_revalidation=self.after_revalidation,
|
|
1872
1841
|
)
|
|
1873
1842
|
|
|
1874
1843
|
|
|
@@ -1888,7 +1857,7 @@ class NeedRevalidation(State):
|
|
|
1888
1857
|
State Transitions:
|
|
1889
1858
|
-----------------
|
|
1890
1859
|
- NeedToBeUpdated: 304 response received, cached responses can be freshened
|
|
1891
|
-
-
|
|
1860
|
+
- InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached
|
|
1892
1861
|
- CacheMiss: No matching responses found during freshening
|
|
1893
1862
|
|
|
1894
1863
|
RFC 9111 References:
|
|
@@ -1909,8 +1878,8 @@ class NeedRevalidation(State):
|
|
|
1909
1878
|
original_request : Request
|
|
1910
1879
|
The original client request (without conditional headers) that initiated
|
|
1911
1880
|
this revalidation. This is used when creating new cache entries.
|
|
1912
|
-
|
|
1913
|
-
The cached request-response
|
|
1881
|
+
revalidating_entries : list[Entry]
|
|
1882
|
+
The cached request-response entries that are being revalidated. These are
|
|
1914
1883
|
stale responses that might still be usable if the server confirms they
|
|
1915
1884
|
haven't changed (304 response).
|
|
1916
1885
|
options : CacheOptions
|
|
@@ -1924,12 +1893,14 @@ class NeedRevalidation(State):
|
|
|
1924
1893
|
|
|
1925
1894
|
original_request: Request
|
|
1926
1895
|
|
|
1927
|
-
|
|
1896
|
+
revalidating_entries: list[Entry]
|
|
1928
1897
|
"""
|
|
1929
|
-
The stored
|
|
1898
|
+
The stored entries that the request was sent for revalidation.
|
|
1930
1899
|
"""
|
|
1931
1900
|
|
|
1932
|
-
def next(
|
|
1901
|
+
def next(
|
|
1902
|
+
self, revalidation_response: Response
|
|
1903
|
+
) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache"]:
|
|
1933
1904
|
"""
|
|
1934
1905
|
Handles the response to a conditional request and determines the next state.
|
|
1935
1906
|
|
|
@@ -1949,9 +1920,9 @@ class NeedRevalidation(State):
|
|
|
1949
1920
|
|
|
1950
1921
|
Returns:
|
|
1951
1922
|
-------
|
|
1952
|
-
Union[NeedToBeUpdated,
|
|
1923
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
1953
1924
|
- NeedToBeUpdated: When 304 response allows cached responses to be freshened
|
|
1954
|
-
-
|
|
1925
|
+
- InvalidateEntries: When old responses must be invalidated (wraps next state)
|
|
1955
1926
|
- CacheMiss: When no matching responses found or storing new response
|
|
1956
1927
|
|
|
1957
1928
|
RFC 9111 Compliance:
|
|
@@ -1986,7 +1957,7 @@ class NeedRevalidation(State):
|
|
|
1986
1957
|
>>> need_revalidation = NeedRevalidation(
|
|
1987
1958
|
... request=conditional_request,
|
|
1988
1959
|
... original_request=original_request,
|
|
1989
|
-
...
|
|
1960
|
+
... revalidating_entries=[cached_entry],
|
|
1990
1961
|
... options=default_options
|
|
1991
1962
|
... )
|
|
1992
1963
|
>>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'}))
|
|
@@ -1997,7 +1968,7 @@ class NeedRevalidation(State):
|
|
|
1997
1968
|
>>> # 200 OK - use new response
|
|
1998
1969
|
>>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"}))
|
|
1999
1970
|
>>> next_state = need_revalidation.next(response_200)
|
|
2000
|
-
>>> isinstance(next_state,
|
|
1971
|
+
>>> isinstance(next_state, InvalidateEntries)
|
|
2001
1972
|
True
|
|
2002
1973
|
"""
|
|
2003
1974
|
|
|
@@ -2036,17 +2007,19 @@ class NeedRevalidation(State):
|
|
|
2036
2007
|
# 2. Store the new response (if cacheable)
|
|
2037
2008
|
# 3. Use the new response to satisfy the request
|
|
2038
2009
|
elif revalidation_response.status_code // 100 == 2:
|
|
2039
|
-
# Invalidate all old
|
|
2040
|
-
# The last
|
|
2041
|
-
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(
|
|
2042
2013
|
options=self.options,
|
|
2043
|
-
|
|
2014
|
+
entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
|
|
2044
2015
|
# After invalidation, attempt to cache the new response
|
|
2045
2016
|
next_state=CacheMiss(
|
|
2046
2017
|
request=self.original_request,
|
|
2047
2018
|
options=self.options,
|
|
2048
2019
|
after_revalidation=True, # Mark that this occurred during revalidation
|
|
2049
|
-
).next(
|
|
2020
|
+
).next(
|
|
2021
|
+
revalidation_response,
|
|
2022
|
+
),
|
|
2050
2023
|
)
|
|
2051
2024
|
|
|
2052
2025
|
# ============================================================================
|
|
@@ -2075,14 +2048,25 @@ class NeedRevalidation(State):
|
|
|
2075
2048
|
elif revalidation_response.status_code // 100 == 5:
|
|
2076
2049
|
# Same as 2xx: invalidate old responses and store the error response
|
|
2077
2050
|
# This ensures clients see the error rather than potentially stale data
|
|
2078
|
-
return
|
|
2051
|
+
return InvalidateEntries(
|
|
2079
2052
|
options=self.options,
|
|
2080
|
-
|
|
2053
|
+
entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
|
|
2081
2054
|
next_state=CacheMiss(
|
|
2082
2055
|
request=self.original_request,
|
|
2083
2056
|
options=self.options,
|
|
2084
2057
|
after_revalidation=True,
|
|
2085
|
-
).next(
|
|
2058
|
+
).next(
|
|
2059
|
+
revalidation_response,
|
|
2060
|
+
),
|
|
2061
|
+
)
|
|
2062
|
+
elif revalidation_response.status_code // 100 == 3:
|
|
2063
|
+
# 3xx Redirects should have been followed by the HTTP client
|
|
2064
|
+
return FromCache(
|
|
2065
|
+
pair=replace(
|
|
2066
|
+
self.revalidating_entries[-1],
|
|
2067
|
+
response=revalidation_response,
|
|
2068
|
+
),
|
|
2069
|
+
options=self.options,
|
|
2086
2070
|
)
|
|
2087
2071
|
|
|
2088
2072
|
# ============================================================================
|
|
@@ -2102,7 +2086,7 @@ class NeedRevalidation(State):
|
|
|
2102
2086
|
|
|
2103
2087
|
def freshening_stored_responses(
|
|
2104
2088
|
self, revalidation_response: Response
|
|
2105
|
-
) -> "NeedToBeUpdated" | "
|
|
2089
|
+
) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss":
|
|
2106
2090
|
"""
|
|
2107
2091
|
Freshens cached responses after receiving a 304 Not Modified response.
|
|
2108
2092
|
|
|
@@ -2125,9 +2109,9 @@ class NeedRevalidation(State):
|
|
|
2125
2109
|
|
|
2126
2110
|
Returns:
|
|
2127
2111
|
-------
|
|
2128
|
-
Union[NeedToBeUpdated,
|
|
2112
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
2129
2113
|
- NeedToBeUpdated: When matching responses are found and updated
|
|
2130
|
-
-
|
|
2114
|
+
- InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist
|
|
2131
2115
|
- CacheMiss: When no matching responses are found
|
|
2132
2116
|
|
|
2133
2117
|
RFC 9111 Compliance:
|
|
@@ -2188,7 +2172,7 @@ class NeedRevalidation(State):
|
|
|
2188
2172
|
# Priority 2: Last-Modified timestamp
|
|
2189
2173
|
# Priority 3: Single response assumption
|
|
2190
2174
|
|
|
2191
|
-
identified_for_revalidation: list[
|
|
2175
|
+
identified_for_revalidation: list[Entry]
|
|
2192
2176
|
|
|
2193
2177
|
# MATCHING STRATEGY 1: Strong ETag
|
|
2194
2178
|
# RFC 9110 Section 8.8.3: ETag
|
|
@@ -2208,7 +2192,7 @@ class NeedRevalidation(State):
|
|
|
2208
2192
|
# Found a strong ETag in the 304 response
|
|
2209
2193
|
# Partition cached responses: matching vs non-matching ETags
|
|
2210
2194
|
identified_for_revalidation, need_to_be_invalidated = partition(
|
|
2211
|
-
self.
|
|
2195
|
+
self.revalidating_entries,
|
|
2212
2196
|
lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call]
|
|
2213
2197
|
)
|
|
2214
2198
|
|
|
@@ -2226,7 +2210,7 @@ class NeedRevalidation(State):
|
|
|
2226
2210
|
# Found Last-Modified in the 304 response
|
|
2227
2211
|
# Partition cached responses: matching vs non-matching timestamps
|
|
2228
2212
|
identified_for_revalidation, need_to_be_invalidated = partition(
|
|
2229
|
-
self.
|
|
2213
|
+
self.revalidating_entries,
|
|
2230
2214
|
lambda pair: pair.response.headers.get("last-modified")
|
|
2231
2215
|
== revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call]
|
|
2232
2216
|
)
|
|
@@ -2240,14 +2224,20 @@ class NeedRevalidation(State):
|
|
|
2240
2224
|
# we can safely assume that single response is the one being confirmed.
|
|
2241
2225
|
# This handles cases where the server doesn't return validators in the 304.
|
|
2242
2226
|
else:
|
|
2243
|
-
if len(self.
|
|
2227
|
+
if len(self.revalidating_entries) == 1:
|
|
2244
2228
|
# Only one cached response - it must be the matching one
|
|
2245
|
-
identified_for_revalidation, need_to_be_invalidated =
|
|
2229
|
+
identified_for_revalidation, need_to_be_invalidated = (
|
|
2230
|
+
[self.revalidating_entries[0]],
|
|
2231
|
+
[],
|
|
2232
|
+
)
|
|
2246
2233
|
else:
|
|
2247
2234
|
# Multiple cached responses but no validators to match them
|
|
2248
2235
|
# We cannot determine which (if any) are valid
|
|
2249
2236
|
# Conservative approach: invalidate all of them
|
|
2250
|
-
identified_for_revalidation, need_to_be_invalidated =
|
|
2237
|
+
identified_for_revalidation, need_to_be_invalidated = (
|
|
2238
|
+
[],
|
|
2239
|
+
self.revalidating_entries,
|
|
2240
|
+
)
|
|
2251
2241
|
|
|
2252
2242
|
# ============================================================================
|
|
2253
2243
|
# STEP 2: Update Matching Responses or Create Cache Miss
|
|
@@ -2272,7 +2262,7 @@ class NeedRevalidation(State):
|
|
|
2272
2262
|
# while excluding certain headers that shouldn't be updated
|
|
2273
2263
|
# (Content-Encoding, Content-Type, Content-Range).
|
|
2274
2264
|
next_state = NeedToBeUpdated(
|
|
2275
|
-
|
|
2265
|
+
updating_entries=[
|
|
2276
2266
|
replace(
|
|
2277
2267
|
pair,
|
|
2278
2268
|
response=refresh_response_headers(pair.response, revalidation_response),
|
|
@@ -2306,9 +2296,9 @@ class NeedRevalidation(State):
|
|
|
2306
2296
|
|
|
2307
2297
|
if need_to_be_invalidated:
|
|
2308
2298
|
# Wrap the next state in an invalidation operation
|
|
2309
|
-
return
|
|
2299
|
+
return InvalidateEntries(
|
|
2310
2300
|
options=self.options,
|
|
2311
|
-
|
|
2301
|
+
entry_ids=[entry.id for entry in need_to_be_invalidated],
|
|
2312
2302
|
next_state=next_state,
|
|
2313
2303
|
)
|
|
2314
2304
|
|
|
@@ -2316,41 +2306,96 @@ class NeedRevalidation(State):
|
|
|
2316
2306
|
return next_state
|
|
2317
2307
|
|
|
2318
2308
|
|
|
2319
|
-
@dataclass
|
|
2320
2309
|
class StoreAndUse(State):
|
|
2321
2310
|
"""
|
|
2322
2311
|
The state that indicates that the response can be stored in the cache and used.
|
|
2323
|
-
"""
|
|
2324
2312
|
|
|
2325
|
-
|
|
2313
|
+
Attributes:
|
|
2314
|
+
----------
|
|
2315
|
+
response : Response
|
|
2316
|
+
The HTTP response to be stored in the cache.
|
|
2317
|
+
after_revalidation : bool
|
|
2318
|
+
Indicates if the storage is occurring after a revalidation process.
|
|
2319
|
+
"""
|
|
2326
2320
|
|
|
2327
|
-
|
|
2321
|
+
def __init__(
|
|
2322
|
+
self,
|
|
2323
|
+
response: Response,
|
|
2324
|
+
options: CacheOptions,
|
|
2325
|
+
after_revalidation: bool = False,
|
|
2326
|
+
) -> None:
|
|
2327
|
+
super().__init__(options)
|
|
2328
|
+
self.response = response
|
|
2329
|
+
self.after_revalidation = after_revalidation
|
|
2330
|
+
response_meta = ResponseMetadata(
|
|
2331
|
+
hishel_created_at=time.time(),
|
|
2332
|
+
hishel_from_cache=False,
|
|
2333
|
+
hishel_spec_ignored=False,
|
|
2334
|
+
hishel_revalidated=after_revalidation,
|
|
2335
|
+
hishel_stored=True,
|
|
2336
|
+
)
|
|
2337
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2328
2338
|
|
|
2329
2339
|
def next(self) -> None:
|
|
2330
|
-
return None
|
|
2340
|
+
return None
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
# @dataclass
|
|
2344
|
+
# class CouldNotBeStored(State):
|
|
2345
|
+
# """
|
|
2346
|
+
# The state that indicates that the response could not be stored in the cache.
|
|
2347
|
+
# """
|
|
2348
|
+
|
|
2349
|
+
# response: Response
|
|
2350
|
+
|
|
2351
|
+
# pair_id: uuid.UUID
|
|
2352
|
+
|
|
2353
|
+
# def next(self) -> None:
|
|
2354
|
+
# return None # pragma: nocover
|
|
2331
2355
|
|
|
2332
2356
|
|
|
2333
|
-
@dataclass
|
|
2334
2357
|
class CouldNotBeStored(State):
|
|
2335
2358
|
"""
|
|
2336
2359
|
The state that indicates that the response could not be stored in the cache.
|
|
2337
|
-
"""
|
|
2338
2360
|
|
|
2339
|
-
|
|
2361
|
+
Attributes:
|
|
2362
|
+
----------
|
|
2363
|
+
response : Response
|
|
2364
|
+
The HTTP response that could not be stored.
|
|
2365
|
+
pair_id : uuid.UUID
|
|
2366
|
+
The unique identifier for the cache pair.
|
|
2367
|
+
after_revalidation : bool
|
|
2368
|
+
Indicates if the storage attempt occurred after a revalidation process.
|
|
2369
|
+
"""
|
|
2340
2370
|
|
|
2341
|
-
|
|
2371
|
+
def __init__(
|
|
2372
|
+
self,
|
|
2373
|
+
response: Response,
|
|
2374
|
+
options: CacheOptions,
|
|
2375
|
+
after_revalidation: bool = False,
|
|
2376
|
+
) -> None:
|
|
2377
|
+
super().__init__(options)
|
|
2378
|
+
self.response = response
|
|
2379
|
+
response_meta = ResponseMetadata(
|
|
2380
|
+
hishel_created_at=time.time(),
|
|
2381
|
+
hishel_from_cache=False,
|
|
2382
|
+
hishel_spec_ignored=False,
|
|
2383
|
+
hishel_revalidated=after_revalidation,
|
|
2384
|
+
hishel_stored=False,
|
|
2385
|
+
)
|
|
2386
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2342
2387
|
|
|
2343
2388
|
def next(self) -> None:
|
|
2344
|
-
return None
|
|
2389
|
+
return None
|
|
2345
2390
|
|
|
2346
2391
|
|
|
2347
2392
|
@dataclass
|
|
2348
|
-
class
|
|
2393
|
+
class InvalidateEntries(State):
|
|
2349
2394
|
"""
|
|
2350
|
-
The state that represents the deletion of cache
|
|
2395
|
+
The state that represents the deletion of cache entries.
|
|
2351
2396
|
"""
|
|
2352
2397
|
|
|
2353
|
-
|
|
2398
|
+
entry_ids: list[uuid.UUID]
|
|
2354
2399
|
|
|
2355
2400
|
next_state: AnyState
|
|
2356
2401
|
|
|
@@ -2358,21 +2403,33 @@ class InvalidatePairs(State):
|
|
|
2358
2403
|
return self.next_state
|
|
2359
2404
|
|
|
2360
2405
|
|
|
2361
|
-
@dataclass
|
|
2362
2406
|
class FromCache(State):
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2407
|
+
def __init__(
|
|
2408
|
+
self,
|
|
2409
|
+
pair: Entry,
|
|
2410
|
+
options: CacheOptions,
|
|
2411
|
+
after_revalidation: bool = False,
|
|
2412
|
+
) -> None:
|
|
2413
|
+
super().__init__(options)
|
|
2414
|
+
self.pair = pair
|
|
2415
|
+
self.after_revalidation = after_revalidation
|
|
2416
|
+
response_meta = ResponseMetadata(
|
|
2417
|
+
hishel_created_at=pair.meta.created_at,
|
|
2418
|
+
hishel_from_cache=True,
|
|
2419
|
+
hishel_spec_ignored=False,
|
|
2420
|
+
hishel_revalidated=after_revalidation,
|
|
2421
|
+
hishel_stored=False,
|
|
2422
|
+
)
|
|
2423
|
+
self.pair.response.metadata.update(response_meta) # type: ignore
|
|
2367
2424
|
|
|
2368
2425
|
def next(self) -> None:
|
|
2369
|
-
return None
|
|
2426
|
+
return None
|
|
2370
2427
|
|
|
2371
2428
|
|
|
2372
2429
|
@dataclass
|
|
2373
2430
|
class NeedToBeUpdated(State):
|
|
2374
|
-
|
|
2431
|
+
updating_entries: list[Entry]
|
|
2375
2432
|
original_request: Request
|
|
2376
2433
|
|
|
2377
2434
|
def next(self) -> FromCache:
|
|
2378
|
-
return FromCache(pair=self.
|
|
2435
|
+
return FromCache(pair=self.updating_entries[-1], options=self.options) # pragma: nocover
|