hishel 0.1.4__py3-none-any.whl → 1.0.0b1__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 +59 -52
- hishel/_async_cache.py +213 -0
- hishel/_async_httpx.py +236 -0
- hishel/_core/_headers.py +646 -0
- hishel/{beta/_core → _core}/_spec.py +270 -136
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/_storages/_async_sqlite.py +420 -0
- hishel/_core/_storages/_packing.py +144 -0
- hishel/_core/_storages/_sync_base.py +71 -0
- hishel/_core/_storages/_sync_sqlite.py +420 -0
- hishel/{beta/_core → _core}/models.py +100 -37
- hishel/_policies.py +49 -0
- hishel/_sync_cache.py +213 -0
- hishel/_sync_httpx.py +236 -0
- hishel/_utils.py +37 -366
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +12 -0
- hishel/{beta/requests.py → requests.py} +41 -30
- hishel-1.0.0b1.dist-info/METADATA +509 -0
- hishel-1.0.0b1.dist-info/RECORD +24 -0
- hishel/_async/__init__.py +0 -5
- hishel/_async/_client.py +0 -30
- hishel/_async/_mock.py +0 -43
- hishel/_async/_pool.py +0 -201
- hishel/_async/_storages.py +0 -768
- hishel/_async/_transports.py +0 -282
- hishel/_controller.py +0 -581
- hishel/_exceptions.py +0 -10
- hishel/_files.py +0 -54
- hishel/_headers.py +0 -215
- hishel/_lfu_cache.py +0 -71
- hishel/_lmdb_types_.pyi +0 -53
- hishel/_s3.py +0 -122
- hishel/_serializers.py +0 -329
- hishel/_sync/__init__.py +0 -5
- hishel/_sync/_client.py +0 -30
- hishel/_sync/_mock.py +0 -43
- hishel/_sync/_pool.py +0 -201
- hishel/_sync/_storages.py +0 -768
- hishel/_sync/_transports.py +0 -282
- hishel/_synchronization.py +0 -37
- hishel/beta/__init__.py +0 -59
- hishel/beta/_async_cache.py +0 -167
- hishel/beta/_core/__init__.py +0 -0
- hishel/beta/_core/_async/_storages/_sqlite.py +0 -411
- hishel/beta/_core/_base/_storages/_base.py +0 -260
- hishel/beta/_core/_base/_storages/_packing.py +0 -165
- hishel/beta/_core/_headers.py +0 -301
- hishel/beta/_core/_sync/_storages/_sqlite.py +0 -411
- hishel/beta/_sync_cache.py +0 -167
- hishel/beta/httpx.py +0 -317
- hishel-0.1.4.dist-info/METADATA +0 -404
- hishel-0.1.4.dist-info/RECORD +0 -41
- {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/WHEEL +0 -0
- {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,17 +9,17 @@ from typing import (
|
|
|
9
9
|
TYPE_CHECKING,
|
|
10
10
|
Any,
|
|
11
11
|
Dict,
|
|
12
|
-
Literal,
|
|
13
12
|
Optional,
|
|
14
13
|
TypeVar,
|
|
15
14
|
Union,
|
|
16
15
|
)
|
|
17
16
|
|
|
17
|
+
from hishel._core._headers import Headers, Range, Vary, parse_cache_control
|
|
18
|
+
from hishel._core.models import ResponseMetadata
|
|
18
19
|
from hishel._utils import parse_date, partition
|
|
19
|
-
from hishel.beta._core._headers import Headers, Range, Vary, parse_cache_control
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from hishel
|
|
22
|
+
from hishel import Entry, Request, Response
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
TState = TypeVar("TState", bound="State")
|
|
@@ -41,9 +41,96 @@ logger = logging.getLogger("hishel.core.spec")
|
|
|
41
41
|
|
|
42
42
|
@dataclass
|
|
43
43
|
class CacheOptions:
|
|
44
|
+
"""
|
|
45
|
+
Configuration options for HTTP cache behavior.
|
|
46
|
+
|
|
47
|
+
These options control how the cache interprets and applies RFC 9111 caching rules.
|
|
48
|
+
All options have sensible defaults that follow the specification.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
----------
|
|
52
|
+
shared : bool
|
|
53
|
+
Determines whether the cache operates as a shared cache or private cache.
|
|
54
|
+
|
|
55
|
+
RFC 9111 Section 3.5: Authenticated Responses
|
|
56
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5
|
|
57
|
+
|
|
58
|
+
- Shared cache (True): Acts as a proxy, CDN, or gateway cache serving multiple users.
|
|
59
|
+
Must respect private directives and Authorization header restrictions.
|
|
60
|
+
Can use s-maxage directive instead of max-age for shared-specific freshness.
|
|
61
|
+
|
|
62
|
+
- Private cache (False): Acts as a browser or user-agent cache for a single user.
|
|
63
|
+
Can cache private responses and ignore s-maxage directives.
|
|
64
|
+
|
|
65
|
+
Default: True (shared cache)
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
--------
|
|
69
|
+
>>> # Shared cache (proxy/CDN)
|
|
70
|
+
>>> options = CacheOptions(shared=True)
|
|
71
|
+
|
|
72
|
+
>>> # Private cache (browser)
|
|
73
|
+
>>> options = CacheOptions(shared=False)
|
|
74
|
+
|
|
75
|
+
supported_methods : list[str]
|
|
76
|
+
HTTP methods that are allowed to be cached by this cache implementation.
|
|
77
|
+
|
|
78
|
+
RFC 9111 Section 3, paragraph 2.1:
|
|
79
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.1.1
|
|
80
|
+
|
|
81
|
+
"A cache MUST NOT store a response to a request unless:
|
|
82
|
+
- the request method is understood by the cache"
|
|
83
|
+
|
|
84
|
+
Default: ["GET", "HEAD"] (most commonly cached methods)
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
--------
|
|
88
|
+
>>> # Default: cache GET and HEAD only
|
|
89
|
+
>>> options = CacheOptions()
|
|
90
|
+
>>> options.supported_methods
|
|
91
|
+
['GET', 'HEAD']
|
|
92
|
+
|
|
93
|
+
>>> # Cache POST responses (advanced use case)
|
|
94
|
+
>>> options = CacheOptions(supported_methods=["GET", "HEAD", "POST"])
|
|
95
|
+
|
|
96
|
+
allow_stale : bool
|
|
97
|
+
Controls whether stale responses can be served without revalidation.
|
|
98
|
+
|
|
99
|
+
RFC 9111 Section 4.2.4: Serving Stale Responses
|
|
100
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4
|
|
101
|
+
|
|
102
|
+
"A cache MUST NOT generate a stale response unless it is disconnected or
|
|
103
|
+
doing so is explicitly permitted by the client or origin server (e.g., by
|
|
104
|
+
the max-stale request directive in Section 5.2.1, extension directives
|
|
105
|
+
such as those defined in [RFC5861], or configuration in accordance with
|
|
106
|
+
an out-of-band contract)."
|
|
107
|
+
|
|
108
|
+
Default: False (no stale responses)
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
--------
|
|
112
|
+
>>> # Conservative: never serve stale
|
|
113
|
+
>>> options = CacheOptions(allow_stale=False)
|
|
114
|
+
|
|
115
|
+
>>> # Permissive: serve stale when allowed
|
|
116
|
+
>>> options = CacheOptions(allow_stale=True)
|
|
117
|
+
|
|
118
|
+
>>> # Stale-while-revalidate pattern (RFC 5861)
|
|
119
|
+
>>> # Even with allow_stale=True, directives are respected
|
|
120
|
+
>>> options = CacheOptions(allow_stale=True)
|
|
121
|
+
"""
|
|
122
|
+
|
|
44
123
|
shared: bool = True
|
|
124
|
+
"""
|
|
125
|
+
When True, the cache operates as a shared cache (proxy/CDN).
|
|
126
|
+
When False, as a private cache (browser).
|
|
127
|
+
"""
|
|
128
|
+
|
|
45
129
|
supported_methods: list[str] = field(default_factory=lambda: ["GET", "HEAD"])
|
|
130
|
+
"""HTTP methods that are allowed to be cached."""
|
|
131
|
+
|
|
46
132
|
allow_stale: bool = False
|
|
133
|
+
"""When True, stale responses can be served without revalidation."""
|
|
47
134
|
|
|
48
135
|
|
|
49
136
|
@dataclass
|
|
@@ -57,7 +144,7 @@ class State(ABC):
|
|
|
57
144
|
|
|
58
145
|
def vary_headers_match(
|
|
59
146
|
original_request: Request,
|
|
60
|
-
|
|
147
|
+
associated_entry: Entry,
|
|
61
148
|
) -> bool:
|
|
62
149
|
"""
|
|
63
150
|
Determines if request headers match the Vary requirements of a cached response.
|
|
@@ -73,8 +160,8 @@ def vary_headers_match(
|
|
|
73
160
|
----------
|
|
74
161
|
original_request : Request
|
|
75
162
|
The new incoming request that we're trying to satisfy
|
|
76
|
-
|
|
77
|
-
A cached request-response
|
|
163
|
+
associated_entry : Entry
|
|
164
|
+
A cached request-response entry that might match the new request
|
|
78
165
|
|
|
79
166
|
Returns:
|
|
80
167
|
-------
|
|
@@ -107,31 +194,31 @@ def vary_headers_match(
|
|
|
107
194
|
>>> # No Vary header - always matches
|
|
108
195
|
>>> request = Request(headers=Headers({"accept": "application/json"}))
|
|
109
196
|
>>> response = Response(headers=Headers({})) # No Vary
|
|
110
|
-
>>>
|
|
111
|
-
>>> vary_headers_match(request,
|
|
197
|
+
>>> entry = Entry(request=request, response=response)
|
|
198
|
+
>>> vary_headers_match(request, entry)
|
|
112
199
|
True
|
|
113
200
|
|
|
114
201
|
>>> # Vary: Accept with matching Accept header
|
|
115
202
|
>>> request1 = Request(headers=Headers({"accept": "application/json"}))
|
|
116
203
|
>>> response = Response(headers=Headers({"vary": "Accept"}))
|
|
117
|
-
>>>
|
|
204
|
+
>>> entry = Entry(request=request1, response=response)
|
|
118
205
|
>>> request2 = Request(headers=Headers({"accept": "application/json"}))
|
|
119
|
-
>>> vary_headers_match(request2,
|
|
206
|
+
>>> vary_headers_match(request2, entry)
|
|
120
207
|
True
|
|
121
208
|
|
|
122
209
|
>>> # Vary: Accept with non-matching Accept header
|
|
123
210
|
>>> request2 = Request(headers=Headers({"accept": "application/xml"}))
|
|
124
|
-
>>> vary_headers_match(request2,
|
|
211
|
+
>>> vary_headers_match(request2, entry)
|
|
125
212
|
False
|
|
126
213
|
|
|
127
214
|
>>> # Vary: * always fails
|
|
128
215
|
>>> response = Response(headers=Headers({"vary": "*"}))
|
|
129
|
-
>>>
|
|
130
|
-
>>> vary_headers_match(request2,
|
|
216
|
+
>>> entry = Entry(request=request1, response=response)
|
|
217
|
+
>>> vary_headers_match(request2, entry)
|
|
131
218
|
False
|
|
132
219
|
"""
|
|
133
220
|
# Extract the Vary header from the cached response
|
|
134
|
-
vary_header =
|
|
221
|
+
vary_header = associated_entry.response.headers.get("vary")
|
|
135
222
|
|
|
136
223
|
# If no Vary header exists, any request matches
|
|
137
224
|
# The response doesn't vary based on request headers
|
|
@@ -154,7 +241,7 @@ def vary_headers_match(
|
|
|
154
241
|
|
|
155
242
|
# Compare the specific header value between original and new request
|
|
156
243
|
# Both headers must have the same value (or both be absent)
|
|
157
|
-
if original_request.headers.get(vary_header) !=
|
|
244
|
+
if original_request.headers.get(vary_header) != associated_entry.request.headers.get(vary_header):
|
|
158
245
|
return False
|
|
159
246
|
|
|
160
247
|
# All Vary headers matched
|
|
@@ -1033,19 +1120,13 @@ AnyState = Union[
|
|
|
1033
1120
|
"NeedToBeUpdated",
|
|
1034
1121
|
"NeedRevalidation",
|
|
1035
1122
|
"IdleClient",
|
|
1036
|
-
"
|
|
1123
|
+
"InvalidateEntries",
|
|
1037
1124
|
]
|
|
1038
1125
|
|
|
1039
1126
|
# Defined in https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods
|
|
1040
1127
|
SAFE_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
|
|
1041
1128
|
|
|
1042
1129
|
|
|
1043
|
-
def create_idle_state(role: Literal["client", "server"], options: Optional[CacheOptions] = None) -> IdleClient:
|
|
1044
|
-
if role == "server":
|
|
1045
|
-
raise NotImplementedError("Server role is not implemented yet.")
|
|
1046
|
-
return IdleClient(options=options or CacheOptions())
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
1130
|
@dataclass
|
|
1050
1131
|
class IdleClient(State):
|
|
1051
1132
|
"""
|
|
@@ -1079,7 +1160,7 @@ class IdleClient(State):
|
|
|
1079
1160
|
"""
|
|
1080
1161
|
|
|
1081
1162
|
def next(
|
|
1082
|
-
self, request: Request,
|
|
1163
|
+
self, request: Request, associated_entries: list[Entry]
|
|
1083
1164
|
) -> Union["CacheMiss", "FromCache", "NeedRevalidation"]:
|
|
1084
1165
|
"""
|
|
1085
1166
|
Determines the next state transition based on the request and available cached responses.
|
|
@@ -1092,9 +1173,9 @@ class IdleClient(State):
|
|
|
1092
1173
|
----------
|
|
1093
1174
|
request : Request
|
|
1094
1175
|
The incoming HTTP request from the client
|
|
1095
|
-
|
|
1096
|
-
List of request-response
|
|
1097
|
-
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).
|
|
1098
1179
|
|
|
1099
1180
|
Returns:
|
|
1100
1181
|
-------
|
|
@@ -1229,7 +1310,7 @@ class IdleClient(State):
|
|
|
1229
1310
|
#
|
|
1230
1311
|
# If a cached response has Cache-Control: no-cache, it cannot be reused without
|
|
1231
1312
|
# validation, regardless of its freshness.
|
|
1232
|
-
def no_cache_missing(pair:
|
|
1313
|
+
def no_cache_missing(pair: Entry) -> bool:
|
|
1233
1314
|
"""Check if the cached response lacks the no-cache directive."""
|
|
1234
1315
|
return parse_cache_control(pair.response.headers.get("cache-control")).no_cache is False
|
|
1235
1316
|
|
|
@@ -1244,7 +1325,7 @@ class IdleClient(State):
|
|
|
1244
1325
|
#
|
|
1245
1326
|
# Note: Condition 5.3 (successfully validated) is handled in the
|
|
1246
1327
|
# NeedRevalidation state, not here.
|
|
1247
|
-
def fresh_or_allowed_stale(pair:
|
|
1328
|
+
def fresh_or_allowed_stale(pair: Entry) -> bool:
|
|
1248
1329
|
"""
|
|
1249
1330
|
Determine if a cached response is fresh or allowed to be served stale.
|
|
1250
1331
|
|
|
@@ -1273,7 +1354,7 @@ class IdleClient(State):
|
|
|
1273
1354
|
# "ready to use" and "needs revalidation" groups.
|
|
1274
1355
|
filtered_pairs = [
|
|
1275
1356
|
pair
|
|
1276
|
-
for pair in
|
|
1357
|
+
for pair in associated_entries
|
|
1277
1358
|
if url_matches(pair) and method_matches(pair) and vary_headers_same(pair) and no_cache_missing(pair) # type: ignore[no-untyped-call]
|
|
1278
1359
|
]
|
|
1279
1360
|
|
|
@@ -1325,18 +1406,13 @@ class IdleClient(State):
|
|
|
1325
1406
|
#
|
|
1326
1407
|
# The Age header informs the client how old the cached response is.
|
|
1327
1408
|
|
|
1328
|
-
# Mark all ready-to-use responses with metadata (for observability)
|
|
1329
|
-
for pair in ready_to_use:
|
|
1330
|
-
pair.response.metadata["hishel_from_cache"] = True # type: ignore
|
|
1331
|
-
|
|
1332
1409
|
# Use the most recent response (first in sorted list)
|
|
1333
1410
|
selected_pair = ready_to_use[0]
|
|
1334
1411
|
|
|
1335
1412
|
# Calculate current age and update the Age header
|
|
1336
1413
|
current_age = get_age(selected_pair.response)
|
|
1337
|
-
|
|
1338
1414
|
return FromCache(
|
|
1339
|
-
|
|
1415
|
+
entry=replace(
|
|
1340
1416
|
selected_pair,
|
|
1341
1417
|
response=replace(
|
|
1342
1418
|
selected_pair.response,
|
|
@@ -1372,7 +1448,7 @@ class IdleClient(State):
|
|
|
1372
1448
|
# (ETag, Last-Modified) from the cached response.
|
|
1373
1449
|
return NeedRevalidation(
|
|
1374
1450
|
request=make_conditional_request(request, need_revalidation[-1].response),
|
|
1375
|
-
|
|
1451
|
+
revalidating_entries=need_revalidation,
|
|
1376
1452
|
options=self.options,
|
|
1377
1453
|
original_request=request,
|
|
1378
1454
|
)
|
|
@@ -1442,7 +1518,7 @@ class CacheMiss(State):
|
|
|
1442
1518
|
Indicates whether the cache miss occurred after a revalidation attempt.
|
|
1443
1519
|
"""
|
|
1444
1520
|
|
|
1445
|
-
def next(self, response: Response
|
|
1521
|
+
def next(self, response: Response) -> Union["StoreAndUse", "CouldNotBeStored"]:
|
|
1446
1522
|
"""
|
|
1447
1523
|
Evaluates whether a response can be stored in the cache.
|
|
1448
1524
|
|
|
@@ -1491,20 +1567,6 @@ class CacheMiss(State):
|
|
|
1491
1567
|
* an s-maxage response directive (if cache is shared)
|
|
1492
1568
|
* a status code that is defined as heuristically cacheable"
|
|
1493
1569
|
|
|
1494
|
-
Side Effects:
|
|
1495
|
-
------------
|
|
1496
|
-
Sets metadata flags on the response object:
|
|
1497
|
-
- hishel_spec_ignored: False (caching spec is being followed)
|
|
1498
|
-
- hishel_from_cache: False (response is from origin, not cache)
|
|
1499
|
-
- hishel_revalidated: True (if after_revalidation is True)
|
|
1500
|
-
- hishel_stored: True/False (whether response was stored)
|
|
1501
|
-
|
|
1502
|
-
Logging:
|
|
1503
|
-
-------
|
|
1504
|
-
When a response cannot be stored, detailed debug logs are emitted explaining
|
|
1505
|
-
which specific RFC requirement failed, with direct links to the relevant
|
|
1506
|
-
RFC sections.
|
|
1507
|
-
|
|
1508
1570
|
Examples:
|
|
1509
1571
|
--------
|
|
1510
1572
|
>>> # Cacheable response
|
|
@@ -1513,7 +1575,7 @@ class CacheMiss(State):
|
|
|
1513
1575
|
... status_code=200,
|
|
1514
1576
|
... headers=Headers({"cache-control": "max-age=3600"})
|
|
1515
1577
|
... )
|
|
1516
|
-
>>> next_state = cache_miss.next(response
|
|
1578
|
+
>>> next_state = cache_miss.next(response)
|
|
1517
1579
|
>>> isinstance(next_state, StoreAndUse)
|
|
1518
1580
|
True
|
|
1519
1581
|
|
|
@@ -1522,26 +1584,11 @@ class CacheMiss(State):
|
|
|
1522
1584
|
... status_code=200,
|
|
1523
1585
|
... headers=Headers({"cache-control": "no-store"})
|
|
1524
1586
|
... )
|
|
1525
|
-
>>> next_state = cache_miss.next(response
|
|
1587
|
+
>>> next_state = cache_miss.next(response)
|
|
1526
1588
|
>>> isinstance(next_state, CouldNotBeStored)
|
|
1527
1589
|
True
|
|
1528
1590
|
"""
|
|
1529
1591
|
|
|
1530
|
-
# ============================================================================
|
|
1531
|
-
# STEP 1: Set Response Metadata
|
|
1532
|
-
# ============================================================================
|
|
1533
|
-
# Initialize metadata flags to track the response lifecycle
|
|
1534
|
-
|
|
1535
|
-
response.metadata["hishel_spec_ignored"] = False # type: ignore
|
|
1536
|
-
# We are following the caching specification
|
|
1537
|
-
|
|
1538
|
-
response.metadata["hishel_from_cache"] = False # type: ignore
|
|
1539
|
-
# This response came from origin server, not cache
|
|
1540
|
-
|
|
1541
|
-
if self.after_revalidation:
|
|
1542
|
-
response.metadata["hishel_revalidated"] = True # type: ignore
|
|
1543
|
-
# Mark that this response is the result of a revalidation
|
|
1544
|
-
|
|
1545
1592
|
# ============================================================================
|
|
1546
1593
|
# STEP 2: Parse Cache-Control Directive
|
|
1547
1594
|
# ============================================================================
|
|
@@ -1636,11 +1683,14 @@ class CacheMiss(State):
|
|
|
1636
1683
|
#
|
|
1637
1684
|
# Requests with Authorization headers often contain user-specific data.
|
|
1638
1685
|
# Shared caches must be careful not to serve one user's data to another.
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1686
|
+
has_explicit_directive = (
|
|
1687
|
+
response_cache_control.public
|
|
1688
|
+
or response_cache_control.s_maxage is not None
|
|
1689
|
+
or response_cache_control.must_revalidate
|
|
1690
|
+
)
|
|
1691
|
+
can_cache_auth_request = (
|
|
1692
|
+
not self.options.shared or "authorization" not in request.headers or has_explicit_directive
|
|
1693
|
+
)
|
|
1644
1694
|
|
|
1645
1695
|
# CONDITION 7: Response Contains Required Caching Information
|
|
1646
1696
|
# RFC 9111 Section 3, paragraph 2.7:
|
|
@@ -1710,7 +1760,7 @@ class CacheMiss(State):
|
|
|
1710
1760
|
or not understands_how_to_cache
|
|
1711
1761
|
or not no_store_is_not_present
|
|
1712
1762
|
or not private_directive_allows_storing
|
|
1713
|
-
or not
|
|
1763
|
+
or not can_cache_auth_request
|
|
1714
1764
|
or not contains_required_component
|
|
1715
1765
|
):
|
|
1716
1766
|
# --------------------------------------------------------------------
|
|
@@ -1746,10 +1796,11 @@ class CacheMiss(State):
|
|
|
1746
1796
|
"Cannot store the response because the `private` response directive does not "
|
|
1747
1797
|
"allow shared caches to store it. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.5.1"
|
|
1748
1798
|
)
|
|
1749
|
-
elif not
|
|
1799
|
+
elif not can_cache_auth_request:
|
|
1750
1800
|
logger.debug(
|
|
1751
|
-
"Cannot store the response because the
|
|
1752
|
-
"
|
|
1801
|
+
"Cannot store the response because the request contained an Authorization header "
|
|
1802
|
+
"and there was no explicit directive allowing shared caching. "
|
|
1803
|
+
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-5"
|
|
1753
1804
|
)
|
|
1754
1805
|
elif not contains_required_component:
|
|
1755
1806
|
logger.debug(
|
|
@@ -1757,10 +1808,11 @@ class CacheMiss(State):
|
|
|
1757
1808
|
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1"
|
|
1758
1809
|
)
|
|
1759
1810
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1811
|
+
return CouldNotBeStored(
|
|
1812
|
+
response=response,
|
|
1813
|
+
options=self.options,
|
|
1814
|
+
after_revalidation=self.after_revalidation,
|
|
1815
|
+
)
|
|
1764
1816
|
|
|
1765
1817
|
# --------------------------------------------------------------------
|
|
1766
1818
|
# Transition to: StoreAndUse
|
|
@@ -1769,9 +1821,6 @@ class CacheMiss(State):
|
|
|
1769
1821
|
|
|
1770
1822
|
logger.debug("Storing response in cache")
|
|
1771
1823
|
|
|
1772
|
-
# Mark response as stored
|
|
1773
|
-
response.metadata["hishel_stored"] = True # type: ignore
|
|
1774
|
-
|
|
1775
1824
|
# Remove headers that should not be stored
|
|
1776
1825
|
# RFC 9111 Section 3.1: Storing Header and Trailer Fields
|
|
1777
1826
|
# https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1
|
|
@@ -1779,9 +1828,9 @@ class CacheMiss(State):
|
|
|
1779
1828
|
cleaned_response = exclude_unstorable_headers(response, self.options.shared)
|
|
1780
1829
|
|
|
1781
1830
|
return StoreAndUse(
|
|
1782
|
-
pair_id=pair_id,
|
|
1783
1831
|
response=cleaned_response,
|
|
1784
1832
|
options=self.options,
|
|
1833
|
+
after_revalidation=self.after_revalidation,
|
|
1785
1834
|
)
|
|
1786
1835
|
|
|
1787
1836
|
|
|
@@ -1801,7 +1850,7 @@ class NeedRevalidation(State):
|
|
|
1801
1850
|
State Transitions:
|
|
1802
1851
|
-----------------
|
|
1803
1852
|
- NeedToBeUpdated: 304 response received, cached responses can be freshened
|
|
1804
|
-
-
|
|
1853
|
+
- InvalidateEntries + CacheMiss: 2xx/5xx response received, new response must be cached
|
|
1805
1854
|
- CacheMiss: No matching responses found during freshening
|
|
1806
1855
|
|
|
1807
1856
|
RFC 9111 References:
|
|
@@ -1822,8 +1871,8 @@ class NeedRevalidation(State):
|
|
|
1822
1871
|
original_request : Request
|
|
1823
1872
|
The original client request (without conditional headers) that initiated
|
|
1824
1873
|
this revalidation. This is used when creating new cache entries.
|
|
1825
|
-
|
|
1826
|
-
The cached request-response
|
|
1874
|
+
revalidating_entries : list[Entry]
|
|
1875
|
+
The cached request-response entries that are being revalidated. These are
|
|
1827
1876
|
stale responses that might still be usable if the server confirms they
|
|
1828
1877
|
haven't changed (304 response).
|
|
1829
1878
|
options : CacheOptions
|
|
@@ -1837,12 +1886,14 @@ class NeedRevalidation(State):
|
|
|
1837
1886
|
|
|
1838
1887
|
original_request: Request
|
|
1839
1888
|
|
|
1840
|
-
|
|
1889
|
+
revalidating_entries: list[Entry]
|
|
1841
1890
|
"""
|
|
1842
|
-
The stored
|
|
1891
|
+
The stored entries that the request was sent for revalidation.
|
|
1843
1892
|
"""
|
|
1844
1893
|
|
|
1845
|
-
def next(
|
|
1894
|
+
def next(
|
|
1895
|
+
self, revalidation_response: Response
|
|
1896
|
+
) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache"]:
|
|
1846
1897
|
"""
|
|
1847
1898
|
Handles the response to a conditional request and determines the next state.
|
|
1848
1899
|
|
|
@@ -1862,9 +1913,9 @@ class NeedRevalidation(State):
|
|
|
1862
1913
|
|
|
1863
1914
|
Returns:
|
|
1864
1915
|
-------
|
|
1865
|
-
Union[NeedToBeUpdated,
|
|
1916
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
1866
1917
|
- NeedToBeUpdated: When 304 response allows cached responses to be freshened
|
|
1867
|
-
-
|
|
1918
|
+
- InvalidateEntries: When old responses must be invalidated (wraps next state)
|
|
1868
1919
|
- CacheMiss: When no matching responses found or storing new response
|
|
1869
1920
|
|
|
1870
1921
|
RFC 9111 Compliance:
|
|
@@ -1899,7 +1950,7 @@ class NeedRevalidation(State):
|
|
|
1899
1950
|
>>> need_revalidation = NeedRevalidation(
|
|
1900
1951
|
... request=conditional_request,
|
|
1901
1952
|
... original_request=original_request,
|
|
1902
|
-
...
|
|
1953
|
+
... revalidating_entries=[cached_entry],
|
|
1903
1954
|
... options=default_options
|
|
1904
1955
|
... )
|
|
1905
1956
|
>>> response_304 = Response(status_code=304, headers=Headers({"etag": '"abc123"'}))
|
|
@@ -1910,7 +1961,7 @@ class NeedRevalidation(State):
|
|
|
1910
1961
|
>>> # 200 OK - use new response
|
|
1911
1962
|
>>> response_200 = Response(status_code=200, headers=Headers({"cache-control": "max-age=3600"}))
|
|
1912
1963
|
>>> next_state = need_revalidation.next(response_200)
|
|
1913
|
-
>>> isinstance(next_state,
|
|
1964
|
+
>>> isinstance(next_state, InvalidateEntries)
|
|
1914
1965
|
True
|
|
1915
1966
|
"""
|
|
1916
1967
|
|
|
@@ -1949,17 +2000,19 @@ class NeedRevalidation(State):
|
|
|
1949
2000
|
# 2. Store the new response (if cacheable)
|
|
1950
2001
|
# 3. Use the new response to satisfy the request
|
|
1951
2002
|
elif revalidation_response.status_code // 100 == 2:
|
|
1952
|
-
# Invalidate all old
|
|
1953
|
-
# The last
|
|
1954
|
-
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(
|
|
1955
2006
|
options=self.options,
|
|
1956
|
-
|
|
2007
|
+
entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
|
|
1957
2008
|
# After invalidation, attempt to cache the new response
|
|
1958
2009
|
next_state=CacheMiss(
|
|
1959
2010
|
request=self.original_request,
|
|
1960
2011
|
options=self.options,
|
|
1961
2012
|
after_revalidation=True, # Mark that this occurred during revalidation
|
|
1962
|
-
).next(
|
|
2013
|
+
).next(
|
|
2014
|
+
revalidation_response,
|
|
2015
|
+
),
|
|
1963
2016
|
)
|
|
1964
2017
|
|
|
1965
2018
|
# ============================================================================
|
|
@@ -1988,14 +2041,25 @@ class NeedRevalidation(State):
|
|
|
1988
2041
|
elif revalidation_response.status_code // 100 == 5:
|
|
1989
2042
|
# Same as 2xx: invalidate old responses and store the error response
|
|
1990
2043
|
# This ensures clients see the error rather than potentially stale data
|
|
1991
|
-
return
|
|
2044
|
+
return InvalidateEntries(
|
|
1992
2045
|
options=self.options,
|
|
1993
|
-
|
|
2046
|
+
entry_ids=[entry.id for entry in self.revalidating_entries[:-1]],
|
|
1994
2047
|
next_state=CacheMiss(
|
|
1995
2048
|
request=self.original_request,
|
|
1996
2049
|
options=self.options,
|
|
1997
2050
|
after_revalidation=True,
|
|
1998
|
-
).next(
|
|
2051
|
+
).next(
|
|
2052
|
+
revalidation_response,
|
|
2053
|
+
),
|
|
2054
|
+
)
|
|
2055
|
+
elif revalidation_response.status_code // 100 == 3:
|
|
2056
|
+
# 3xx Redirects should have been followed by the HTTP client
|
|
2057
|
+
return FromCache(
|
|
2058
|
+
entry=replace(
|
|
2059
|
+
self.revalidating_entries[-1],
|
|
2060
|
+
response=revalidation_response,
|
|
2061
|
+
),
|
|
2062
|
+
options=self.options,
|
|
1999
2063
|
)
|
|
2000
2064
|
|
|
2001
2065
|
# ============================================================================
|
|
@@ -2015,7 +2079,7 @@ class NeedRevalidation(State):
|
|
|
2015
2079
|
|
|
2016
2080
|
def freshening_stored_responses(
|
|
2017
2081
|
self, revalidation_response: Response
|
|
2018
|
-
) -> "NeedToBeUpdated" | "
|
|
2082
|
+
) -> "NeedToBeUpdated" | "InvalidateEntries" | "CacheMiss":
|
|
2019
2083
|
"""
|
|
2020
2084
|
Freshens cached responses after receiving a 304 Not Modified response.
|
|
2021
2085
|
|
|
@@ -2038,9 +2102,9 @@ class NeedRevalidation(State):
|
|
|
2038
2102
|
|
|
2039
2103
|
Returns:
|
|
2040
2104
|
-------
|
|
2041
|
-
Union[NeedToBeUpdated,
|
|
2105
|
+
Union[NeedToBeUpdated, InvalidateEntries, CacheMiss]
|
|
2042
2106
|
- NeedToBeUpdated: When matching responses are found and updated
|
|
2043
|
-
-
|
|
2107
|
+
- InvalidateEntries: Wraps NeedToBeUpdated if non-matching responses exist
|
|
2044
2108
|
- CacheMiss: When no matching responses are found
|
|
2045
2109
|
|
|
2046
2110
|
RFC 9111 Compliance:
|
|
@@ -2101,7 +2165,7 @@ class NeedRevalidation(State):
|
|
|
2101
2165
|
# Priority 2: Last-Modified timestamp
|
|
2102
2166
|
# Priority 3: Single response assumption
|
|
2103
2167
|
|
|
2104
|
-
identified_for_revalidation: list[
|
|
2168
|
+
identified_for_revalidation: list[Entry]
|
|
2105
2169
|
|
|
2106
2170
|
# MATCHING STRATEGY 1: Strong ETag
|
|
2107
2171
|
# RFC 9110 Section 8.8.3: ETag
|
|
@@ -2121,7 +2185,7 @@ class NeedRevalidation(State):
|
|
|
2121
2185
|
# Found a strong ETag in the 304 response
|
|
2122
2186
|
# Partition cached responses: matching vs non-matching ETags
|
|
2123
2187
|
identified_for_revalidation, need_to_be_invalidated = partition(
|
|
2124
|
-
self.
|
|
2188
|
+
self.revalidating_entries,
|
|
2125
2189
|
lambda pair: pair.response.headers.get("etag") == revalidation_response.headers.get("etag"), # type: ignore[no-untyped-call]
|
|
2126
2190
|
)
|
|
2127
2191
|
|
|
@@ -2139,7 +2203,7 @@ class NeedRevalidation(State):
|
|
|
2139
2203
|
# Found Last-Modified in the 304 response
|
|
2140
2204
|
# Partition cached responses: matching vs non-matching timestamps
|
|
2141
2205
|
identified_for_revalidation, need_to_be_invalidated = partition(
|
|
2142
|
-
self.
|
|
2206
|
+
self.revalidating_entries,
|
|
2143
2207
|
lambda pair: pair.response.headers.get("last-modified")
|
|
2144
2208
|
== revalidation_response.headers.get("last-modified"), # type: ignore[no-untyped-call]
|
|
2145
2209
|
)
|
|
@@ -2153,14 +2217,20 @@ class NeedRevalidation(State):
|
|
|
2153
2217
|
# we can safely assume that single response is the one being confirmed.
|
|
2154
2218
|
# This handles cases where the server doesn't return validators in the 304.
|
|
2155
2219
|
else:
|
|
2156
|
-
if len(self.
|
|
2220
|
+
if len(self.revalidating_entries) == 1:
|
|
2157
2221
|
# Only one cached response - it must be the matching one
|
|
2158
|
-
identified_for_revalidation, need_to_be_invalidated =
|
|
2222
|
+
identified_for_revalidation, need_to_be_invalidated = (
|
|
2223
|
+
[self.revalidating_entries[0]],
|
|
2224
|
+
[],
|
|
2225
|
+
)
|
|
2159
2226
|
else:
|
|
2160
2227
|
# Multiple cached responses but no validators to match them
|
|
2161
2228
|
# We cannot determine which (if any) are valid
|
|
2162
2229
|
# Conservative approach: invalidate all of them
|
|
2163
|
-
identified_for_revalidation, need_to_be_invalidated =
|
|
2230
|
+
identified_for_revalidation, need_to_be_invalidated = (
|
|
2231
|
+
[],
|
|
2232
|
+
self.revalidating_entries,
|
|
2233
|
+
)
|
|
2164
2234
|
|
|
2165
2235
|
# ============================================================================
|
|
2166
2236
|
# STEP 2: Update Matching Responses or Create Cache Miss
|
|
@@ -2185,7 +2255,7 @@ class NeedRevalidation(State):
|
|
|
2185
2255
|
# while excluding certain headers that shouldn't be updated
|
|
2186
2256
|
# (Content-Encoding, Content-Type, Content-Range).
|
|
2187
2257
|
next_state = NeedToBeUpdated(
|
|
2188
|
-
|
|
2258
|
+
updating_entries=[
|
|
2189
2259
|
replace(
|
|
2190
2260
|
pair,
|
|
2191
2261
|
response=refresh_response_headers(pair.response, revalidation_response),
|
|
@@ -2219,9 +2289,9 @@ class NeedRevalidation(State):
|
|
|
2219
2289
|
|
|
2220
2290
|
if need_to_be_invalidated:
|
|
2221
2291
|
# Wrap the next state in an invalidation operation
|
|
2222
|
-
return
|
|
2292
|
+
return InvalidateEntries(
|
|
2223
2293
|
options=self.options,
|
|
2224
|
-
|
|
2294
|
+
entry_ids=[entry.id for entry in need_to_be_invalidated],
|
|
2225
2295
|
next_state=next_state,
|
|
2226
2296
|
)
|
|
2227
2297
|
|
|
@@ -2229,41 +2299,94 @@ class NeedRevalidation(State):
|
|
|
2229
2299
|
return next_state
|
|
2230
2300
|
|
|
2231
2301
|
|
|
2232
|
-
@dataclass
|
|
2233
2302
|
class StoreAndUse(State):
|
|
2234
2303
|
"""
|
|
2235
2304
|
The state that indicates that the response can be stored in the cache and used.
|
|
2236
|
-
"""
|
|
2237
2305
|
|
|
2238
|
-
|
|
2306
|
+
Attributes:
|
|
2307
|
+
----------
|
|
2308
|
+
response : Response
|
|
2309
|
+
The HTTP response to be stored in the cache.
|
|
2310
|
+
after_revalidation : bool
|
|
2311
|
+
Indicates if the storage is occurring after a revalidation process.
|
|
2312
|
+
"""
|
|
2239
2313
|
|
|
2240
|
-
|
|
2314
|
+
def __init__(
|
|
2315
|
+
self,
|
|
2316
|
+
response: Response,
|
|
2317
|
+
options: CacheOptions,
|
|
2318
|
+
after_revalidation: bool = False,
|
|
2319
|
+
) -> None:
|
|
2320
|
+
super().__init__(options)
|
|
2321
|
+
self.response = response
|
|
2322
|
+
self.after_revalidation = after_revalidation
|
|
2323
|
+
response_meta = ResponseMetadata(
|
|
2324
|
+
hishel_created_at=time.time(),
|
|
2325
|
+
hishel_from_cache=False,
|
|
2326
|
+
hishel_revalidated=after_revalidation,
|
|
2327
|
+
hishel_stored=True,
|
|
2328
|
+
)
|
|
2329
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2241
2330
|
|
|
2242
2331
|
def next(self) -> None:
|
|
2243
|
-
return None
|
|
2332
|
+
return None
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
# @dataclass
|
|
2336
|
+
# class CouldNotBeStored(State):
|
|
2337
|
+
# """
|
|
2338
|
+
# The state that indicates that the response could not be stored in the cache.
|
|
2339
|
+
# """
|
|
2340
|
+
|
|
2341
|
+
# response: Response
|
|
2342
|
+
|
|
2343
|
+
# pair_id: uuid.UUID
|
|
2344
|
+
|
|
2345
|
+
# def next(self) -> None:
|
|
2346
|
+
# return None # pragma: nocover
|
|
2244
2347
|
|
|
2245
2348
|
|
|
2246
|
-
@dataclass
|
|
2247
2349
|
class CouldNotBeStored(State):
|
|
2248
2350
|
"""
|
|
2249
2351
|
The state that indicates that the response could not be stored in the cache.
|
|
2250
|
-
"""
|
|
2251
2352
|
|
|
2252
|
-
|
|
2353
|
+
Attributes:
|
|
2354
|
+
----------
|
|
2355
|
+
response : Response
|
|
2356
|
+
The HTTP response that could not be stored.
|
|
2357
|
+
pair_id : uuid.UUID
|
|
2358
|
+
The unique identifier for the cache pair.
|
|
2359
|
+
after_revalidation : bool
|
|
2360
|
+
Indicates if the storage attempt occurred after a revalidation process.
|
|
2361
|
+
"""
|
|
2253
2362
|
|
|
2254
|
-
|
|
2363
|
+
def __init__(
|
|
2364
|
+
self,
|
|
2365
|
+
response: Response,
|
|
2366
|
+
options: CacheOptions,
|
|
2367
|
+
after_revalidation: bool = False,
|
|
2368
|
+
) -> None:
|
|
2369
|
+
super().__init__(options)
|
|
2370
|
+
self.response = response
|
|
2371
|
+
response_meta = ResponseMetadata(
|
|
2372
|
+
hishel_created_at=time.time(),
|
|
2373
|
+
hishel_from_cache=False,
|
|
2374
|
+
hishel_revalidated=after_revalidation,
|
|
2375
|
+
hishel_stored=False,
|
|
2376
|
+
)
|
|
2377
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2255
2378
|
|
|
2256
2379
|
def next(self) -> None:
|
|
2257
|
-
return None
|
|
2380
|
+
return None
|
|
2258
2381
|
|
|
2259
2382
|
|
|
2260
2383
|
@dataclass
|
|
2261
|
-
class
|
|
2384
|
+
class InvalidateEntries(State):
|
|
2262
2385
|
"""
|
|
2263
|
-
The state that represents the deletion of cache
|
|
2386
|
+
The state that represents the deletion of cache entries.
|
|
2264
2387
|
"""
|
|
2265
2388
|
|
|
2266
|
-
|
|
2389
|
+
entry_ids: list[uuid.UUID]
|
|
2267
2390
|
|
|
2268
2391
|
next_state: AnyState
|
|
2269
2392
|
|
|
@@ -2271,21 +2394,32 @@ class InvalidatePairs(State):
|
|
|
2271
2394
|
return self.next_state
|
|
2272
2395
|
|
|
2273
2396
|
|
|
2274
|
-
@dataclass
|
|
2275
2397
|
class FromCache(State):
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2398
|
+
def __init__(
|
|
2399
|
+
self,
|
|
2400
|
+
entry: Entry,
|
|
2401
|
+
options: CacheOptions,
|
|
2402
|
+
after_revalidation: bool = False,
|
|
2403
|
+
) -> None:
|
|
2404
|
+
super().__init__(options)
|
|
2405
|
+
self.entry = entry
|
|
2406
|
+
self.after_revalidation = after_revalidation
|
|
2407
|
+
response_meta = ResponseMetadata(
|
|
2408
|
+
hishel_created_at=entry.meta.created_at,
|
|
2409
|
+
hishel_from_cache=True,
|
|
2410
|
+
hishel_revalidated=after_revalidation,
|
|
2411
|
+
hishel_stored=False,
|
|
2412
|
+
)
|
|
2413
|
+
self.entry.response.metadata.update(response_meta) # type: ignore
|
|
2280
2414
|
|
|
2281
2415
|
def next(self) -> None:
|
|
2282
|
-
return None
|
|
2416
|
+
return None
|
|
2283
2417
|
|
|
2284
2418
|
|
|
2285
2419
|
@dataclass
|
|
2286
2420
|
class NeedToBeUpdated(State):
|
|
2287
|
-
|
|
2421
|
+
updating_entries: list[Entry]
|
|
2288
2422
|
original_request: Request
|
|
2289
2423
|
|
|
2290
2424
|
def next(self) -> FromCache:
|
|
2291
|
-
return FromCache(
|
|
2425
|
+
return FromCache(entry=self.updating_entries[-1], options=self.options)
|