hishel 1.0.0.dev3__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hishel/__init__.py +11 -3
- hishel/_async_cache.py +70 -37
- hishel/_async_httpx.py +8 -15
- hishel/_core/_spec.py +17 -40
- hishel/_core/_storages/_async_sqlite.py +5 -2
- hishel/_core/_storages/_sync_sqlite.py +5 -2
- hishel/_core/models.py +87 -11
- hishel/_policies.py +49 -0
- hishel/_sync_cache.py +70 -37
- hishel/_sync_httpx.py +8 -15
- hishel/_utils.py +2 -2
- hishel/asgi.py +16 -16
- hishel/requests.py +3 -5
- {hishel-1.0.0.dev3.dist-info → hishel-1.1.0.dist-info}/METADATA +129 -16
- hishel-1.1.0.dist-info/RECORD +24 -0
- hishel-1.0.0.dev3.dist-info/RECORD +0 -23
- {hishel-1.0.0.dev3.dist-info → hishel-1.1.0.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev3.dist-info → hishel-1.1.0.dist-info}/licenses/LICENSE +0 -0
hishel/__init__.py
CHANGED
|
@@ -15,17 +15,20 @@ from hishel._core._spec import (
|
|
|
15
15
|
NeedToBeUpdated as NeedToBeUpdated,
|
|
16
16
|
State as State,
|
|
17
17
|
StoreAndUse as StoreAndUse,
|
|
18
|
-
create_idle_state as create_idle_state,
|
|
19
18
|
)
|
|
20
19
|
from hishel._core.models import (
|
|
21
20
|
Entry as Entry,
|
|
22
21
|
EntryMeta as EntryMeta,
|
|
23
22
|
Request as Request,
|
|
24
|
-
Response,
|
|
23
|
+
Response as Response,
|
|
24
|
+
ResponseMetadata as ResponseMetadata,
|
|
25
|
+
RequestMetadata as RequestMetadata,
|
|
25
26
|
)
|
|
26
27
|
from hishel._async_cache import AsyncCacheProxy as AsyncCacheProxy
|
|
27
28
|
from hishel._sync_cache import SyncCacheProxy as SyncCacheProxy
|
|
28
29
|
|
|
30
|
+
from hishel._policies import SpecificationPolicy, FilterPolicy, CachePolicy
|
|
31
|
+
|
|
29
32
|
__all__ = (
|
|
30
33
|
# New API
|
|
31
34
|
## States
|
|
@@ -41,12 +44,13 @@ __all__ = (
|
|
|
41
44
|
"StoreAndUse",
|
|
42
45
|
"CouldNotBeStored",
|
|
43
46
|
"InvalidateEntries",
|
|
44
|
-
"create_idle_state",
|
|
45
47
|
## Models
|
|
46
48
|
"Request",
|
|
47
49
|
"Response",
|
|
48
50
|
"Entry",
|
|
49
51
|
"EntryMeta",
|
|
52
|
+
"RequestMetadata",
|
|
53
|
+
"ResponseMetadata",
|
|
50
54
|
## Headers
|
|
51
55
|
"Headers",
|
|
52
56
|
## Storages
|
|
@@ -57,4 +61,8 @@ __all__ = (
|
|
|
57
61
|
# Proxy
|
|
58
62
|
"AsyncCacheProxy",
|
|
59
63
|
"SyncCacheProxy",
|
|
64
|
+
# Policies
|
|
65
|
+
"CachePolicy",
|
|
66
|
+
"SpecificationPolicy",
|
|
67
|
+
"FilterPolicy",
|
|
60
68
|
)
|
hishel/_async_cache.py
CHANGED
|
@@ -13,7 +13,6 @@ from hishel import (
|
|
|
13
13
|
AsyncBaseStorage,
|
|
14
14
|
AsyncSqliteStorage,
|
|
15
15
|
CacheMiss,
|
|
16
|
-
CacheOptions,
|
|
17
16
|
CouldNotBeStored,
|
|
18
17
|
FromCache,
|
|
19
18
|
IdleClient,
|
|
@@ -22,10 +21,10 @@ from hishel import (
|
|
|
22
21
|
Request,
|
|
23
22
|
Response,
|
|
24
23
|
StoreAndUse,
|
|
25
|
-
create_idle_state,
|
|
26
24
|
)
|
|
27
25
|
from hishel._core._spec import InvalidateEntries, vary_headers_match
|
|
28
26
|
from hishel._core.models import Entry, ResponseMetadata
|
|
27
|
+
from hishel._policies import CachePolicy, FilterPolicy, SpecificationPolicy
|
|
29
28
|
from hishel._utils import make_async_iterator
|
|
30
29
|
|
|
31
30
|
logger = logging.getLogger("hishel.integrations.clients")
|
|
@@ -37,87 +36,121 @@ class AsyncCacheProxy:
|
|
|
37
36
|
|
|
38
37
|
This class is independent of any specific HTTP library and works only with internal models.
|
|
39
38
|
It delegates request execution to a user-provided callable, making it compatible with any
|
|
40
|
-
HTTP client. Caching behavior
|
|
41
|
-
|
|
39
|
+
HTTP client. Caching behavior is determined by the policy object.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
request_sender: Callable that sends HTTP requests and returns responses.
|
|
43
|
+
storage: Storage backend for cache entries. Defaults to AsyncSqliteStorage.
|
|
44
|
+
policy: Caching policy to use. Can be SpecificationPolicy (respects RFC 9111) or
|
|
45
|
+
FilterPolicy (user-defined filtering). Defaults to SpecificationPolicy().
|
|
42
46
|
"""
|
|
43
47
|
|
|
44
48
|
def __init__(
|
|
45
49
|
self,
|
|
46
50
|
request_sender: Callable[[Request], Awaitable[Response]],
|
|
47
51
|
storage: AsyncBaseStorage | None = None,
|
|
48
|
-
|
|
49
|
-
ignore_specification: bool = False,
|
|
52
|
+
policy: CachePolicy | None = None,
|
|
50
53
|
) -> None:
|
|
51
54
|
self.send_request = request_sender
|
|
52
55
|
self.storage = storage if storage is not None else AsyncSqliteStorage()
|
|
53
|
-
self.
|
|
54
|
-
self.ignore_specification = ignore_specification
|
|
56
|
+
self.policy = policy if policy is not None else SpecificationPolicy()
|
|
55
57
|
|
|
56
58
|
async def handle_request(self, request: Request) -> Response:
|
|
57
|
-
if self.
|
|
58
|
-
return await self.
|
|
59
|
+
if isinstance(self.policy, FilterPolicy):
|
|
60
|
+
return await self._handle_request_with_filters(request)
|
|
59
61
|
return await self._handle_request_respecting_spec(request)
|
|
60
62
|
|
|
61
63
|
async def _get_key_for_request(self, request: Request) -> str:
|
|
62
|
-
if request.metadata.get("hishel_body_key"):
|
|
64
|
+
if self.policy.use_body_key or request.metadata.get("hishel_body_key"):
|
|
63
65
|
assert isinstance(request.stream, (AsyncIterator, AsyncIterable))
|
|
64
66
|
collected = b"".join([chunk async for chunk in request.stream])
|
|
65
67
|
hash_ = hashlib.sha256(collected).hexdigest()
|
|
66
68
|
request.stream = make_async_iterator([collected])
|
|
67
|
-
return
|
|
69
|
+
return hash_
|
|
68
70
|
return hashlib.sha256(str(request.url).encode("utf-8")).hexdigest()
|
|
69
71
|
|
|
70
|
-
async def
|
|
71
|
-
if
|
|
72
|
+
async def _maybe_refresh_entry_ttl(self, entry: Entry) -> None:
|
|
73
|
+
if entry.request.metadata.get("hishel_refresh_ttl_on_access"):
|
|
72
74
|
await self.storage.update_entry(
|
|
73
|
-
|
|
74
|
-
lambda
|
|
75
|
-
|
|
76
|
-
meta=replace(
|
|
75
|
+
entry.id,
|
|
76
|
+
lambda current_entry: replace(
|
|
77
|
+
current_entry,
|
|
78
|
+
meta=replace(current_entry.meta, created_at=time.time()),
|
|
77
79
|
),
|
|
78
80
|
)
|
|
79
81
|
|
|
80
|
-
async def
|
|
82
|
+
async def _handle_request_with_filters(self, request: Request) -> Response:
|
|
83
|
+
assert isinstance(self.policy, FilterPolicy)
|
|
84
|
+
|
|
85
|
+
for request_filter in self.policy.request_filters:
|
|
86
|
+
if request_filter.needs_body():
|
|
87
|
+
body = await request.aread()
|
|
88
|
+
if not request_filter.apply(request, body):
|
|
89
|
+
logger.debug("Request filtered out by request filter")
|
|
90
|
+
return await self.send_request(request)
|
|
91
|
+
else:
|
|
92
|
+
if not request_filter.apply(request, None):
|
|
93
|
+
logger.debug("Request filtered out by request filter")
|
|
94
|
+
return await self.send_request(request)
|
|
95
|
+
|
|
81
96
|
logger.debug("Trying to get cached response ignoring specification")
|
|
82
|
-
|
|
97
|
+
cache_key = await self._get_key_for_request(request)
|
|
98
|
+
entries = await self.storage.get_entries(cache_key)
|
|
83
99
|
|
|
84
100
|
logger.debug(f"Found {len(entries)} cached entries for the request")
|
|
85
101
|
|
|
86
|
-
for
|
|
102
|
+
for entry in entries:
|
|
87
103
|
if (
|
|
88
|
-
str(
|
|
89
|
-
and
|
|
104
|
+
str(entry.request.url) == str(request.url)
|
|
105
|
+
and entry.request.method == request.method
|
|
90
106
|
and vary_headers_match(
|
|
91
107
|
request,
|
|
92
|
-
|
|
108
|
+
entry,
|
|
93
109
|
)
|
|
94
110
|
):
|
|
95
111
|
logger.debug(
|
|
96
112
|
"Found matching cached response for the request",
|
|
97
113
|
)
|
|
98
114
|
response_meta = ResponseMetadata(
|
|
99
|
-
hishel_spec_ignored=True,
|
|
100
115
|
hishel_from_cache=True,
|
|
101
|
-
hishel_created_at=
|
|
116
|
+
hishel_created_at=entry.meta.created_at,
|
|
102
117
|
hishel_revalidated=False,
|
|
103
118
|
hishel_stored=False,
|
|
104
119
|
)
|
|
105
|
-
|
|
106
|
-
await self.
|
|
107
|
-
return
|
|
120
|
+
entry.response.metadata.update(response_meta) # type: ignore
|
|
121
|
+
await self._maybe_refresh_entry_ttl(entry)
|
|
122
|
+
return entry.response
|
|
108
123
|
|
|
109
124
|
response = await self.send_request(request)
|
|
125
|
+
for response_filter in self.policy.response_filters:
|
|
126
|
+
if response_filter.needs_body():
|
|
127
|
+
body = await response.aread()
|
|
128
|
+
if not response_filter.apply(response, body):
|
|
129
|
+
logger.debug("Response filtered out by response filter")
|
|
130
|
+
return response
|
|
131
|
+
else:
|
|
132
|
+
if not response_filter.apply(response, None):
|
|
133
|
+
logger.debug("Response filtered out by response filter")
|
|
134
|
+
return response
|
|
135
|
+
response_meta = ResponseMetadata(
|
|
136
|
+
hishel_from_cache=False,
|
|
137
|
+
hishel_created_at=time.time(),
|
|
138
|
+
hishel_revalidated=False,
|
|
139
|
+
hishel_stored=True,
|
|
140
|
+
)
|
|
141
|
+
response.metadata.update(response_meta) # type: ignore
|
|
110
142
|
|
|
111
143
|
logger.debug("Storing response in cache ignoring specification")
|
|
112
144
|
entry = await self.storage.create_entry(
|
|
113
145
|
request,
|
|
114
146
|
response,
|
|
115
|
-
|
|
147
|
+
cache_key,
|
|
116
148
|
)
|
|
117
149
|
return entry.response
|
|
118
150
|
|
|
119
151
|
async def _handle_request_respecting_spec(self, request: Request) -> Response:
|
|
120
|
-
|
|
152
|
+
assert isinstance(self.policy, SpecificationPolicy)
|
|
153
|
+
state: AnyState = IdleClient(options=self.policy.cache_options)
|
|
121
154
|
|
|
122
155
|
while state:
|
|
123
156
|
logger.debug(f"Handling state: {state.__class__.__name__}")
|
|
@@ -132,8 +165,8 @@ class AsyncCacheProxy:
|
|
|
132
165
|
elif isinstance(state, NeedRevalidation):
|
|
133
166
|
state = await self._handle_revalidation(state)
|
|
134
167
|
elif isinstance(state, FromCache):
|
|
135
|
-
await self.
|
|
136
|
-
return state.
|
|
168
|
+
await self._maybe_refresh_entry_ttl(state.entry)
|
|
169
|
+
return state.entry.response
|
|
137
170
|
elif isinstance(state, NeedToBeUpdated):
|
|
138
171
|
state = await self._handle_update(state)
|
|
139
172
|
elif isinstance(state, InvalidateEntries):
|
|
@@ -152,12 +185,12 @@ class AsyncCacheProxy:
|
|
|
152
185
|
return state.next(response)
|
|
153
186
|
|
|
154
187
|
async def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response:
|
|
155
|
-
|
|
188
|
+
entry = await self.storage.create_entry(
|
|
156
189
|
request,
|
|
157
190
|
state.response,
|
|
158
191
|
await self._get_key_for_request(request),
|
|
159
192
|
)
|
|
160
|
-
return
|
|
193
|
+
return entry.response
|
|
161
194
|
|
|
162
195
|
async def _handle_revalidation(self, state: NeedRevalidation) -> AnyState:
|
|
163
196
|
revalidation_response = await self.send_request(state.request)
|
|
@@ -167,8 +200,8 @@ class AsyncCacheProxy:
|
|
|
167
200
|
for entry in state.updating_entries:
|
|
168
201
|
await self.storage.update_entry(
|
|
169
202
|
entry.id,
|
|
170
|
-
lambda
|
|
171
|
-
|
|
203
|
+
lambda entry: replace(
|
|
204
|
+
entry,
|
|
172
205
|
response=replace(entry.response, headers=entry.response.headers),
|
|
173
206
|
),
|
|
174
207
|
)
|
hishel/_async_httpx.py
CHANGED
|
@@ -14,11 +14,9 @@ from typing import (
|
|
|
14
14
|
from httpx import RequestNotRead
|
|
15
15
|
|
|
16
16
|
from hishel import AsyncCacheProxy, Headers, Request, Response
|
|
17
|
-
from hishel._core._spec import (
|
|
18
|
-
CacheOptions,
|
|
19
|
-
)
|
|
20
17
|
from hishel._core._storages._async_base import AsyncBaseStorage
|
|
21
18
|
from hishel._core.models import RequestMetadata, extract_metadata_from_headers
|
|
19
|
+
from hishel._policies import CachePolicy
|
|
22
20
|
from hishel._utils import (
|
|
23
21
|
filter_mapping,
|
|
24
22
|
make_async_iterator,
|
|
@@ -62,14 +60,14 @@ def _internal_to_httpx(
|
|
|
62
60
|
method=value.method,
|
|
63
61
|
url=value.url,
|
|
64
62
|
headers=value.headers,
|
|
65
|
-
stream=_IteratorStream(value.
|
|
63
|
+
stream=_IteratorStream(value._aiter_stream()),
|
|
66
64
|
extensions=value.metadata,
|
|
67
65
|
)
|
|
68
66
|
elif isinstance(value, Response):
|
|
69
67
|
return httpx.Response(
|
|
70
68
|
status_code=value.status_code,
|
|
71
69
|
headers=value.headers,
|
|
72
|
-
stream=_IteratorStream(value.
|
|
70
|
+
stream=_IteratorStream(value._aiter_stream()),
|
|
73
71
|
extensions=value.metadata,
|
|
74
72
|
)
|
|
75
73
|
|
|
@@ -149,15 +147,13 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
149
147
|
self,
|
|
150
148
|
next_transport: httpx.AsyncBaseTransport,
|
|
151
149
|
storage: AsyncBaseStorage | None = None,
|
|
152
|
-
|
|
153
|
-
ignore_specification: bool = False,
|
|
150
|
+
policy: CachePolicy | None = None,
|
|
154
151
|
) -> None:
|
|
155
152
|
self.next_transport = next_transport
|
|
156
153
|
self._cache_proxy: AsyncCacheProxy = AsyncCacheProxy(
|
|
157
154
|
request_sender=self.request_sender,
|
|
158
155
|
storage=storage,
|
|
159
|
-
|
|
160
|
-
ignore_specification=ignore_specification,
|
|
156
|
+
policy=policy,
|
|
161
157
|
)
|
|
162
158
|
self.storage = self._cache_proxy.storage
|
|
163
159
|
|
|
@@ -184,8 +180,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
184
180
|
class AsyncCacheClient(httpx.AsyncClient):
|
|
185
181
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
186
182
|
self.storage: AsyncBaseStorage | None = kwargs.pop("storage", None)
|
|
187
|
-
self.
|
|
188
|
-
self.ignore_specification: bool = kwargs.pop("ignore_specification", False)
|
|
183
|
+
self.policy: CachePolicy | None = kwargs.pop("policy", None)
|
|
189
184
|
super().__init__(*args, **kwargs)
|
|
190
185
|
|
|
191
186
|
def _init_transport(
|
|
@@ -212,8 +207,7 @@ class AsyncCacheClient(httpx.AsyncClient):
|
|
|
212
207
|
limits=limits,
|
|
213
208
|
),
|
|
214
209
|
storage=self.storage,
|
|
215
|
-
|
|
216
|
-
ignore_specification=False,
|
|
210
|
+
policy=self.policy,
|
|
217
211
|
)
|
|
218
212
|
|
|
219
213
|
def _init_proxy_transport(
|
|
@@ -238,6 +232,5 @@ class AsyncCacheClient(httpx.AsyncClient):
|
|
|
238
232
|
proxy=proxy,
|
|
239
233
|
),
|
|
240
234
|
storage=self.storage,
|
|
241
|
-
|
|
242
|
-
ignore_specification=self.ignore_specification,
|
|
235
|
+
policy=self.policy,
|
|
243
236
|
)
|
hishel/_core/_spec.py
CHANGED
|
@@ -9,7 +9,6 @@ from typing import (
|
|
|
9
9
|
TYPE_CHECKING,
|
|
10
10
|
Any,
|
|
11
11
|
Dict,
|
|
12
|
-
Literal,
|
|
13
12
|
Optional,
|
|
14
13
|
TypeVar,
|
|
15
14
|
Union,
|
|
@@ -1128,12 +1127,6 @@ AnyState = Union[
|
|
|
1128
1127
|
SAFE_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
|
|
1129
1128
|
|
|
1130
1129
|
|
|
1131
|
-
def create_idle_state(role: Literal["client", "server"], options: Optional[CacheOptions] = None) -> IdleClient:
|
|
1132
|
-
if role == "server":
|
|
1133
|
-
raise NotImplementedError("Server role is not implemented yet.")
|
|
1134
|
-
return IdleClient(options=options or CacheOptions())
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
1130
|
@dataclass
|
|
1138
1131
|
class IdleClient(State):
|
|
1139
1132
|
"""
|
|
@@ -1419,7 +1412,7 @@ class IdleClient(State):
|
|
|
1419
1412
|
# Calculate current age and update the Age header
|
|
1420
1413
|
current_age = get_age(selected_pair.response)
|
|
1421
1414
|
return FromCache(
|
|
1422
|
-
|
|
1415
|
+
entry=replace(
|
|
1423
1416
|
selected_pair,
|
|
1424
1417
|
response=replace(
|
|
1425
1418
|
selected_pair.response,
|
|
@@ -1900,7 +1893,7 @@ class NeedRevalidation(State):
|
|
|
1900
1893
|
|
|
1901
1894
|
def next(
|
|
1902
1895
|
self, revalidation_response: Response
|
|
1903
|
-
) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache"]:
|
|
1896
|
+
) -> Union["NeedToBeUpdated", "InvalidateEntries", "CacheMiss", "FromCache", "StoreAndUse", "CouldNotBeStored"]:
|
|
1904
1897
|
"""
|
|
1905
1898
|
Handles the response to a conditional request and determines the next state.
|
|
1906
1899
|
|
|
@@ -2059,30 +2052,17 @@ class NeedRevalidation(State):
|
|
|
2059
2052
|
revalidation_response,
|
|
2060
2053
|
),
|
|
2061
2054
|
)
|
|
2062
|
-
|
|
2063
|
-
#
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2055
|
+
else:
|
|
2056
|
+
# ============================================================================
|
|
2057
|
+
# STEP 4: Handle Unexpected Status Codes
|
|
2058
|
+
# ============================================================================
|
|
2059
|
+
# RFC 9111 does not define behavior for other status codes in this context.
|
|
2060
|
+
# In practice, we need to forward any unexpected responses to the client.
|
|
2061
|
+
return CacheMiss(
|
|
2062
|
+
request=self.revalidating_entries[-1].request,
|
|
2069
2063
|
options=self.options,
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
# ============================================================================
|
|
2073
|
-
# STEP 4: Handle Unexpected Status Codes
|
|
2074
|
-
# ============================================================================
|
|
2075
|
-
# This should not happen in normal operation. Valid revalidation responses are:
|
|
2076
|
-
# - 304 Not Modified
|
|
2077
|
-
# - 2xx Success (typically 200 OK)
|
|
2078
|
-
# - 5xx Server Error
|
|
2079
|
-
#
|
|
2080
|
-
# Other status codes (1xx, 3xx, 4xx) are unexpected during revalidation.
|
|
2081
|
-
# 3xx redirects should have been followed by the HTTP client.
|
|
2082
|
-
# 4xx errors (except 404) are unusual during revalidation.
|
|
2083
|
-
raise RuntimeError(
|
|
2084
|
-
f"Unexpected response status code during revalidation: {revalidation_response.status_code}"
|
|
2085
|
-
) # pragma: nocover
|
|
2064
|
+
after_revalidation=True,
|
|
2065
|
+
).next(revalidation_response)
|
|
2086
2066
|
|
|
2087
2067
|
def freshening_stored_responses(
|
|
2088
2068
|
self, revalidation_response: Response
|
|
@@ -2330,7 +2310,6 @@ class StoreAndUse(State):
|
|
|
2330
2310
|
response_meta = ResponseMetadata(
|
|
2331
2311
|
hishel_created_at=time.time(),
|
|
2332
2312
|
hishel_from_cache=False,
|
|
2333
|
-
hishel_spec_ignored=False,
|
|
2334
2313
|
hishel_revalidated=after_revalidation,
|
|
2335
2314
|
hishel_stored=True,
|
|
2336
2315
|
)
|
|
@@ -2379,7 +2358,6 @@ class CouldNotBeStored(State):
|
|
|
2379
2358
|
response_meta = ResponseMetadata(
|
|
2380
2359
|
hishel_created_at=time.time(),
|
|
2381
2360
|
hishel_from_cache=False,
|
|
2382
|
-
hishel_spec_ignored=False,
|
|
2383
2361
|
hishel_revalidated=after_revalidation,
|
|
2384
2362
|
hishel_stored=False,
|
|
2385
2363
|
)
|
|
@@ -2406,21 +2384,20 @@ class InvalidateEntries(State):
|
|
|
2406
2384
|
class FromCache(State):
|
|
2407
2385
|
def __init__(
|
|
2408
2386
|
self,
|
|
2409
|
-
|
|
2387
|
+
entry: Entry,
|
|
2410
2388
|
options: CacheOptions,
|
|
2411
2389
|
after_revalidation: bool = False,
|
|
2412
2390
|
) -> None:
|
|
2413
2391
|
super().__init__(options)
|
|
2414
|
-
self.
|
|
2392
|
+
self.entry = entry
|
|
2415
2393
|
self.after_revalidation = after_revalidation
|
|
2416
2394
|
response_meta = ResponseMetadata(
|
|
2417
|
-
hishel_created_at=
|
|
2395
|
+
hishel_created_at=entry.meta.created_at,
|
|
2418
2396
|
hishel_from_cache=True,
|
|
2419
|
-
hishel_spec_ignored=False,
|
|
2420
2397
|
hishel_revalidated=after_revalidation,
|
|
2421
2398
|
hishel_stored=False,
|
|
2422
2399
|
)
|
|
2423
|
-
self.
|
|
2400
|
+
self.entry.response.metadata.update(response_meta) # type: ignore
|
|
2424
2401
|
|
|
2425
2402
|
def next(self) -> None:
|
|
2426
2403
|
return None
|
|
@@ -2432,4 +2409,4 @@ class NeedToBeUpdated(State):
|
|
|
2432
2409
|
original_request: Request
|
|
2433
2410
|
|
|
2434
2411
|
def next(self) -> FromCache:
|
|
2435
|
-
return FromCache(
|
|
2412
|
+
return FromCache(entry=self.updating_entries[-1], options=self.options)
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
5
|
from dataclasses import replace
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
9
|
AsyncIterable,
|
|
@@ -46,10 +47,12 @@ try:
|
|
|
46
47
|
default_ttl: Optional[float] = None,
|
|
47
48
|
refresh_ttl_on_access: bool = True,
|
|
48
49
|
) -> None:
|
|
49
|
-
|
|
50
|
+
db_path = Path(database_path)
|
|
50
51
|
|
|
51
52
|
self.connection = connection
|
|
52
|
-
self.database_path =
|
|
53
|
+
self.database_path = (
|
|
54
|
+
ensure_cache_dict(db_path.parent if db_path.parent != Path(".") else None) / db_path.name
|
|
55
|
+
)
|
|
53
56
|
self.default_ttl = default_ttl
|
|
54
57
|
self.refresh_ttl_on_access = refresh_ttl_on_access
|
|
55
58
|
self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
5
|
from dataclasses import replace
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
9
|
Iterable,
|
|
@@ -46,10 +47,12 @@ try:
|
|
|
46
47
|
default_ttl: Optional[float] = None,
|
|
47
48
|
refresh_ttl_on_access: bool = True,
|
|
48
49
|
) -> None:
|
|
49
|
-
|
|
50
|
+
db_path = Path(database_path)
|
|
50
51
|
|
|
51
52
|
self.connection = connection
|
|
52
|
-
self.database_path =
|
|
53
|
+
self.database_path = (
|
|
54
|
+
ensure_cache_dict(db_path.parent if db_path.parent != Path(".") else None) / db_path.name
|
|
55
|
+
)
|
|
53
56
|
self.default_ttl = default_ttl
|
|
54
57
|
self.refresh_ttl_on_access = refresh_ttl_on_access
|
|
55
58
|
self.last_cleanup = time.time() - BATCH_CLEANUP_INTERVAL + BATCH_CLEANUP_START_DELAY
|
hishel/_core/models.py
CHANGED
|
@@ -5,14 +5,18 @@ import uuid
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import (
|
|
7
7
|
Any,
|
|
8
|
+
AsyncIterable,
|
|
8
9
|
AsyncIterator,
|
|
10
|
+
Iterable,
|
|
9
11
|
Iterator,
|
|
10
12
|
Mapping,
|
|
11
13
|
Optional,
|
|
12
14
|
TypedDict,
|
|
15
|
+
cast,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
from hishel._core._headers import Headers
|
|
19
|
+
from hishel._utils import make_async_iterator, make_sync_iterator
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
class AnyIterable:
|
|
@@ -96,18 +100,56 @@ class Request:
|
|
|
96
100
|
stream: Iterator[bytes] | AsyncIterator[bytes] = field(default_factory=lambda: iter(AnyIterable()))
|
|
97
101
|
metadata: RequestMetadata | Mapping[str, Any] = field(default_factory=dict)
|
|
98
102
|
|
|
99
|
-
def
|
|
100
|
-
if
|
|
101
|
-
|
|
103
|
+
def _iter_stream(self) -> Iterator[bytes]:
|
|
104
|
+
if hasattr(self, "collected_body"):
|
|
105
|
+
yield getattr(self, "collected_body")
|
|
106
|
+
return
|
|
107
|
+
if isinstance(self.stream, (Iterator, Iterable)):
|
|
108
|
+
yield from self.stream
|
|
109
|
+
return
|
|
102
110
|
raise TypeError("Request stream is not an Iterator")
|
|
103
111
|
|
|
104
|
-
async def
|
|
105
|
-
if
|
|
112
|
+
async def _aiter_stream(self) -> AsyncIterator[bytes]:
|
|
113
|
+
if hasattr(self, "collected_body"):
|
|
114
|
+
yield getattr(self, "collected_body")
|
|
115
|
+
return
|
|
116
|
+
if isinstance(self.stream, (AsyncIterator, AsyncIterable)):
|
|
106
117
|
async for chunk in self.stream:
|
|
107
118
|
yield chunk
|
|
119
|
+
return
|
|
108
120
|
else:
|
|
109
121
|
raise TypeError("Request stream is not an AsyncIterator")
|
|
110
122
|
|
|
123
|
+
def read(self) -> bytes:
|
|
124
|
+
"""
|
|
125
|
+
Synchronously reads the entire request body without consuming the stream.
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(self.stream, Iterator):
|
|
128
|
+
raise TypeError("Request stream is not an Iterator")
|
|
129
|
+
|
|
130
|
+
if hasattr(self, "collected_body"):
|
|
131
|
+
return cast(bytes, getattr(self, "collected_body"))
|
|
132
|
+
|
|
133
|
+
collected = b"".join([chunk for chunk in self.stream])
|
|
134
|
+
setattr(self, "collected_body", collected)
|
|
135
|
+
self.stream = make_sync_iterator([collected])
|
|
136
|
+
return collected
|
|
137
|
+
|
|
138
|
+
async def aread(self) -> bytes:
|
|
139
|
+
"""
|
|
140
|
+
Asynchronously reads the entire request body without consuming the stream.
|
|
141
|
+
"""
|
|
142
|
+
if not isinstance(self.stream, AsyncIterator):
|
|
143
|
+
raise TypeError("Request stream is not an AsyncIterator")
|
|
144
|
+
|
|
145
|
+
if hasattr(self, "collected_body"):
|
|
146
|
+
return cast(bytes, getattr(self, "collected_body"))
|
|
147
|
+
|
|
148
|
+
collected = b"".join([chunk async for chunk in self.stream])
|
|
149
|
+
setattr(self, "collected_body", collected)
|
|
150
|
+
self.stream = make_async_iterator([collected])
|
|
151
|
+
return collected
|
|
152
|
+
|
|
111
153
|
|
|
112
154
|
class ResponseMetadata(TypedDict, total=False):
|
|
113
155
|
# All the names here should be prefixed with "hishel_" to avoid collisions with user data
|
|
@@ -117,9 +159,6 @@ class ResponseMetadata(TypedDict, total=False):
|
|
|
117
159
|
hishel_revalidated: bool
|
|
118
160
|
"""Indicates whether the response was revalidated with the origin server."""
|
|
119
161
|
|
|
120
|
-
hishel_spec_ignored: bool
|
|
121
|
-
"""Indicates whether the caching specification was ignored for this response."""
|
|
122
|
-
|
|
123
162
|
hishel_stored: bool
|
|
124
163
|
"""Indicates whether the response was stored in cache."""
|
|
125
164
|
|
|
@@ -134,18 +173,55 @@ class Response:
|
|
|
134
173
|
stream: Iterator[bytes] | AsyncIterator[bytes] = field(default_factory=lambda: iter(AnyIterable()))
|
|
135
174
|
metadata: ResponseMetadata | Mapping[str, Any] = field(default_factory=dict)
|
|
136
175
|
|
|
137
|
-
def
|
|
176
|
+
def _iter_stream(self) -> Iterator[bytes]:
|
|
177
|
+
if hasattr(self, "collected_body"):
|
|
178
|
+
yield getattr(self, "collected_body")
|
|
179
|
+
return
|
|
138
180
|
if isinstance(self.stream, Iterator):
|
|
139
|
-
|
|
181
|
+
yield from self.stream
|
|
182
|
+
return
|
|
140
183
|
raise TypeError("Response stream is not an Iterator")
|
|
141
184
|
|
|
142
|
-
async def
|
|
185
|
+
async def _aiter_stream(self) -> AsyncIterator[bytes]:
|
|
186
|
+
if hasattr(self, "collected_body"):
|
|
187
|
+
yield getattr(self, "collected_body")
|
|
188
|
+
return
|
|
143
189
|
if isinstance(self.stream, AsyncIterator):
|
|
144
190
|
async for chunk in self.stream:
|
|
145
191
|
yield chunk
|
|
146
192
|
else:
|
|
147
193
|
raise TypeError("Response stream is not an AsyncIterator")
|
|
148
194
|
|
|
195
|
+
def read(self) -> bytes:
|
|
196
|
+
"""
|
|
197
|
+
Synchronously reads the entire request body without consuming the stream.
|
|
198
|
+
"""
|
|
199
|
+
if not isinstance(self.stream, Iterator):
|
|
200
|
+
raise TypeError("Request stream is not an Iterator")
|
|
201
|
+
|
|
202
|
+
if hasattr(self, "collected_body"):
|
|
203
|
+
return cast(bytes, getattr(self, "collected_body"))
|
|
204
|
+
|
|
205
|
+
collected = b"".join([chunk for chunk in self.stream])
|
|
206
|
+
setattr(self, "collected_body", collected)
|
|
207
|
+
self.stream = make_sync_iterator([collected])
|
|
208
|
+
return collected
|
|
209
|
+
|
|
210
|
+
async def aread(self) -> bytes:
|
|
211
|
+
"""
|
|
212
|
+
Asynchronously reads the entire request body without consuming the stream.
|
|
213
|
+
"""
|
|
214
|
+
if not isinstance(self.stream, AsyncIterator):
|
|
215
|
+
raise TypeError("Request stream is not an AsyncIterator")
|
|
216
|
+
|
|
217
|
+
if hasattr(self, "collected_body"):
|
|
218
|
+
return cast(bytes, getattr(self, "collected_body"))
|
|
219
|
+
|
|
220
|
+
collected = b"".join([chunk async for chunk in self.stream])
|
|
221
|
+
setattr(self, "collected_body", collected)
|
|
222
|
+
self.stream = make_async_iterator([collected])
|
|
223
|
+
return collected
|
|
224
|
+
|
|
149
225
|
|
|
150
226
|
@dataclass
|
|
151
227
|
class EntryMeta:
|