hishel 0.1.2__py3-none-any.whl → 0.1.4__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 +41 -1
- hishel/_async/_client.py +1 -1
- hishel/_async/_storages.py +17 -17
- hishel/_async/_transports.py +9 -4
- hishel/_controller.py +2 -3
- hishel/_lmdb_types_.pyi +53 -0
- hishel/_s3.py +19 -8
- hishel/_serializers.py +2 -2
- hishel/_sync/_client.py +1 -1
- hishel/_sync/_storages.py +16 -16
- hishel/_sync/_transports.py +9 -4
- hishel/_utils.py +340 -0
- hishel/beta/__init__.py +59 -0
- hishel/beta/_async_cache.py +167 -0
- hishel/beta/_core/__init__.py +0 -0
- hishel/beta/_core/_async/_storages/_sqlite.py +411 -0
- hishel/beta/_core/_base/_storages/_base.py +260 -0
- hishel/beta/_core/_base/_storages/_packing.py +165 -0
- hishel/beta/_core/_headers.py +301 -0
- hishel/beta/_core/_spec.py +2291 -0
- hishel/beta/_core/_sync/_storages/_sqlite.py +411 -0
- hishel/beta/_core/models.py +176 -0
- hishel/beta/_sync_cache.py +167 -0
- hishel/beta/httpx.py +317 -0
- hishel/beta/requests.py +193 -0
- {hishel-0.1.2.dist-info → hishel-0.1.4.dist-info}/METADATA +50 -6
- hishel-0.1.4.dist-info/RECORD +41 -0
- hishel-0.1.2.dist-info/RECORD +0 -27
- {hishel-0.1.2.dist-info → hishel-0.1.4.dist-info}/WHEEL +0 -0
- {hishel-0.1.2.dist-info → hishel-0.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from typing import Iterator, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from hishel.beta import (
|
|
12
|
+
AnyState,
|
|
13
|
+
SyncBaseStorage,
|
|
14
|
+
SyncSqliteStorage,
|
|
15
|
+
CacheMiss,
|
|
16
|
+
CacheOptions,
|
|
17
|
+
CouldNotBeStored,
|
|
18
|
+
FromCache,
|
|
19
|
+
IdleClient,
|
|
20
|
+
NeedRevalidation,
|
|
21
|
+
NeedToBeUpdated,
|
|
22
|
+
Request,
|
|
23
|
+
Response,
|
|
24
|
+
StoreAndUse,
|
|
25
|
+
create_idle_state,
|
|
26
|
+
)
|
|
27
|
+
from hishel.beta._core._spec import InvalidatePairs, vary_headers_match
|
|
28
|
+
from hishel.beta._core.models import CompletePair
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("hishel.integrations.clients")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SyncCacheProxy:
|
|
34
|
+
"""
|
|
35
|
+
A proxy for HTTP caching in clients.
|
|
36
|
+
|
|
37
|
+
This class is independent of any specific HTTP library and works only with internal models.
|
|
38
|
+
It delegates request execution to a user-provided callable, making it compatible with any
|
|
39
|
+
HTTP client. Caching behavior can be configured to either fully respect HTTP
|
|
40
|
+
caching rules or bypass them entirely.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
send_request: Callable[[Request], Response],
|
|
46
|
+
storage: SyncBaseStorage | None = None,
|
|
47
|
+
cache_options: CacheOptions | None = None,
|
|
48
|
+
ignore_specification: bool = False,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.send_request = send_request
|
|
51
|
+
self.storage = storage if storage is not None else SyncSqliteStorage()
|
|
52
|
+
self.cache_options = cache_options if cache_options is not None else CacheOptions()
|
|
53
|
+
self.ignore_specification = ignore_specification
|
|
54
|
+
|
|
55
|
+
def handle_request(self, request: Request) -> Response:
|
|
56
|
+
if self.ignore_specification or request.metadata.get("hishel_spec_ignore"):
|
|
57
|
+
return self._handle_request_ignoring_spec(request)
|
|
58
|
+
return self._handle_request_respecting_spec(request)
|
|
59
|
+
|
|
60
|
+
def _get_key_for_request(self, request: Request) -> str:
|
|
61
|
+
if request.metadata.get("hishel_body_key"):
|
|
62
|
+
assert isinstance(request.stream, Iterator)
|
|
63
|
+
collected = b"".join([chunk for chunk in request.stream])
|
|
64
|
+
hash_ = hashlib.sha256(collected).hexdigest()
|
|
65
|
+
return f"{str(request.url)}-{hash_}"
|
|
66
|
+
return str(request.url)
|
|
67
|
+
|
|
68
|
+
def _maybe_refresh_pair_ttl(self, pair: CompletePair) -> None:
|
|
69
|
+
if pair.request.metadata.get("hishel_refresh_ttl_on_access"):
|
|
70
|
+
self.storage.update_pair(
|
|
71
|
+
pair.id,
|
|
72
|
+
lambda complete_pair: replace(complete_pair, meta=replace(complete_pair.meta, created_at=time.time())),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _handle_request_ignoring_spec(self, request: Request) -> Response:
|
|
76
|
+
logger.debug("Trying to get cached response ignoring specification")
|
|
77
|
+
pairs = self.storage.get_pairs(self._get_key_for_request(request))
|
|
78
|
+
|
|
79
|
+
logger.debug(f"Found {len(pairs)} cached pairs for the request")
|
|
80
|
+
|
|
81
|
+
for pair in pairs:
|
|
82
|
+
if (
|
|
83
|
+
str(pair.request.url) == str(request.url)
|
|
84
|
+
and pair.request.method == request.method
|
|
85
|
+
and vary_headers_match(
|
|
86
|
+
request,
|
|
87
|
+
pair,
|
|
88
|
+
)
|
|
89
|
+
):
|
|
90
|
+
logger.debug(
|
|
91
|
+
"Found matching cached response for the request",
|
|
92
|
+
)
|
|
93
|
+
pair.response.metadata["hishel_from_cache"] = True # type: ignore
|
|
94
|
+
self._maybe_refresh_pair_ttl(pair)
|
|
95
|
+
return pair.response
|
|
96
|
+
|
|
97
|
+
incomplete_pair = self.storage.create_pair(
|
|
98
|
+
request,
|
|
99
|
+
)
|
|
100
|
+
response = self.send_request(incomplete_pair.request)
|
|
101
|
+
|
|
102
|
+
logger.debug("Storing response in cache ignoring specification")
|
|
103
|
+
complete_pair = self.storage.add_response(
|
|
104
|
+
incomplete_pair.id, response, self._get_key_for_request(request)
|
|
105
|
+
)
|
|
106
|
+
return complete_pair.response
|
|
107
|
+
|
|
108
|
+
def _handle_request_respecting_spec(self, request: Request) -> Response:
|
|
109
|
+
state: AnyState = create_idle_state("client", self.cache_options)
|
|
110
|
+
|
|
111
|
+
while state:
|
|
112
|
+
logger.debug(f"Handling state: {state.__class__.__name__}")
|
|
113
|
+
if isinstance(state, IdleClient):
|
|
114
|
+
state = self._handle_idle_state(state, request)
|
|
115
|
+
elif isinstance(state, CacheMiss):
|
|
116
|
+
state = self._handle_cache_miss(state)
|
|
117
|
+
elif isinstance(state, StoreAndUse):
|
|
118
|
+
return self._handle_store_and_use(state, request)
|
|
119
|
+
elif isinstance(state, CouldNotBeStored):
|
|
120
|
+
return state.response
|
|
121
|
+
elif isinstance(state, NeedRevalidation):
|
|
122
|
+
state = self._handle_revalidation(state)
|
|
123
|
+
elif isinstance(state, FromCache):
|
|
124
|
+
self._maybe_refresh_pair_ttl(state.pair)
|
|
125
|
+
return state.pair.response
|
|
126
|
+
elif isinstance(state, NeedToBeUpdated):
|
|
127
|
+
state = self._handle_update(state)
|
|
128
|
+
elif isinstance(state, InvalidatePairs):
|
|
129
|
+
state = self._handle_invalidate_pairs(state)
|
|
130
|
+
else:
|
|
131
|
+
assert_never(state)
|
|
132
|
+
|
|
133
|
+
raise RuntimeError("Unreachable")
|
|
134
|
+
|
|
135
|
+
def _handle_idle_state(self, state: IdleClient, request: Request) -> AnyState:
|
|
136
|
+
stored_pairs = self.storage.get_pairs(self._get_key_for_request(request))
|
|
137
|
+
return state.next(request, stored_pairs)
|
|
138
|
+
|
|
139
|
+
def _handle_cache_miss(self, state: CacheMiss) -> AnyState:
|
|
140
|
+
incomplete_pair = self.storage.create_pair(state.request)
|
|
141
|
+
response = self.send_request(incomplete_pair.request)
|
|
142
|
+
return state.next(response, incomplete_pair.id)
|
|
143
|
+
|
|
144
|
+
def _handle_store_and_use(self, state: StoreAndUse, request: Request) -> Response:
|
|
145
|
+
complete_pair = self.storage.add_response(
|
|
146
|
+
state.pair_id, state.response, self._get_key_for_request(request)
|
|
147
|
+
)
|
|
148
|
+
return complete_pair.response
|
|
149
|
+
|
|
150
|
+
def _handle_revalidation(self, state: NeedRevalidation) -> AnyState:
|
|
151
|
+
revalidation_response = self.send_request(state.request)
|
|
152
|
+
return state.next(revalidation_response)
|
|
153
|
+
|
|
154
|
+
def _handle_update(self, state: NeedToBeUpdated) -> AnyState:
|
|
155
|
+
for pair in state.updating_pairs:
|
|
156
|
+
self.storage.update_pair(
|
|
157
|
+
pair.id,
|
|
158
|
+
lambda complete_pair: replace(
|
|
159
|
+
complete_pair, response=replace(pair.response, headers=pair.response.headers)
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
return state.next()
|
|
163
|
+
|
|
164
|
+
def _handle_invalidate_pairs(self, state: InvalidatePairs) -> AnyState:
|
|
165
|
+
for pair_id in state.pair_ids:
|
|
166
|
+
self.storage.remove(pair_id)
|
|
167
|
+
return state.next()
|
hishel/beta/httpx.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ssl
|
|
4
|
+
import typing as t
|
|
5
|
+
from typing import AsyncIterator, Iterable, Iterator, Union, overload
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from hishel.beta import Headers, Request, Response
|
|
10
|
+
from hishel.beta._async_cache import AsyncCacheProxy
|
|
11
|
+
from hishel.beta._core._base._storages._base import AsyncBaseStorage, SyncBaseStorage
|
|
12
|
+
from hishel.beta._core._spec import (
|
|
13
|
+
CacheOptions,
|
|
14
|
+
)
|
|
15
|
+
from hishel.beta._core.models import AnyIterable
|
|
16
|
+
from hishel.beta._sync_cache import SyncCacheProxy
|
|
17
|
+
|
|
18
|
+
SOCKET_OPTION = t.Union[
|
|
19
|
+
t.Tuple[int, int, int],
|
|
20
|
+
t.Tuple[int, int, t.Union[bytes, bytearray]],
|
|
21
|
+
t.Tuple[int, int, None, int],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IteratorStream(httpx.SyncByteStream, httpx.AsyncByteStream):
|
|
26
|
+
def __init__(self, iterator: Iterator[bytes] | AsyncIterator[bytes]) -> None:
|
|
27
|
+
self.iterator = iterator
|
|
28
|
+
|
|
29
|
+
def __iter__(self) -> Iterator[bytes]:
|
|
30
|
+
assert isinstance(self.iterator, (Iterator))
|
|
31
|
+
yield from self.iterator
|
|
32
|
+
|
|
33
|
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
|
34
|
+
assert isinstance(self.iterator, (AsyncIterator))
|
|
35
|
+
async for chunk in self.iterator:
|
|
36
|
+
yield chunk
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def internal_to_httpx(
|
|
41
|
+
value: Request,
|
|
42
|
+
) -> httpx.Request: ...
|
|
43
|
+
@overload
|
|
44
|
+
def internal_to_httpx(
|
|
45
|
+
value: Response,
|
|
46
|
+
) -> httpx.Response: ...
|
|
47
|
+
def internal_to_httpx(
|
|
48
|
+
value: Union[Request, Response],
|
|
49
|
+
) -> Union[httpx.Request, httpx.Response]:
|
|
50
|
+
"""
|
|
51
|
+
Convert internal Request/Response to httpx.Request/httpx.Response.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, Request):
|
|
54
|
+
return httpx.Request(
|
|
55
|
+
method=value.method,
|
|
56
|
+
url=value.url,
|
|
57
|
+
headers=value.headers,
|
|
58
|
+
stream=IteratorStream(value.stream),
|
|
59
|
+
extensions=value.metadata,
|
|
60
|
+
)
|
|
61
|
+
elif isinstance(value, Response):
|
|
62
|
+
return httpx.Response(
|
|
63
|
+
status_code=value.status_code,
|
|
64
|
+
headers=value.headers,
|
|
65
|
+
stream=IteratorStream(value.stream),
|
|
66
|
+
extensions=value.metadata,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def httpx_to_internal(
|
|
72
|
+
value: httpx.Request,
|
|
73
|
+
) -> Request: ...
|
|
74
|
+
@overload
|
|
75
|
+
def httpx_to_internal(
|
|
76
|
+
value: httpx.Response,
|
|
77
|
+
) -> Response: ...
|
|
78
|
+
def httpx_to_internal(
|
|
79
|
+
value: Union[httpx.Request, httpx.Response],
|
|
80
|
+
) -> Union[Request, Response]:
|
|
81
|
+
"""
|
|
82
|
+
Convert httpx.Request/httpx.Response to internal Request/Response.
|
|
83
|
+
"""
|
|
84
|
+
stream: Union[Iterator[bytes], AsyncIterator[bytes]]
|
|
85
|
+
try:
|
|
86
|
+
stream = AnyIterable(value.content)
|
|
87
|
+
except (httpx.RequestNotRead, httpx.ResponseNotRead):
|
|
88
|
+
if isinstance(value, httpx.Response):
|
|
89
|
+
stream = value.iter_raw() if isinstance(value.stream, Iterable) else value.aiter_raw()
|
|
90
|
+
else:
|
|
91
|
+
stream = value.stream # type: ignore
|
|
92
|
+
if isinstance(value, httpx.Request):
|
|
93
|
+
return Request(
|
|
94
|
+
method=value.method,
|
|
95
|
+
url=str(value.url),
|
|
96
|
+
headers=Headers({key: value for key, value in value.headers.items()}),
|
|
97
|
+
stream=stream,
|
|
98
|
+
metadata={
|
|
99
|
+
"hishel_refresh_ttl_on_access": value.extensions.get("hishel_refresh_ttl_on_access"),
|
|
100
|
+
"hishel_ttl": value.extensions.get("hishel_ttl"),
|
|
101
|
+
"hishel_spec_ignore": value.extensions.get("hishel_spec_ignore"),
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
elif isinstance(value, httpx.Response):
|
|
105
|
+
return Response(
|
|
106
|
+
status_code=value.status_code,
|
|
107
|
+
headers=Headers({key: value for key, value in value.headers.items()}),
|
|
108
|
+
stream=stream,
|
|
109
|
+
metadata={},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SyncCacheTransport(httpx.BaseTransport):
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
next_transport: httpx.BaseTransport,
|
|
117
|
+
storage: SyncBaseStorage | None = None,
|
|
118
|
+
cache_options: CacheOptions | None = None,
|
|
119
|
+
ignore_specification: bool = False,
|
|
120
|
+
) -> None:
|
|
121
|
+
self.next_transport = next_transport
|
|
122
|
+
self._cache_proxy: SyncCacheProxy = SyncCacheProxy(
|
|
123
|
+
send_request=self.sync_send_request,
|
|
124
|
+
storage=storage,
|
|
125
|
+
cache_options=cache_options,
|
|
126
|
+
ignore_specification=ignore_specification,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def handle_request(
|
|
130
|
+
self,
|
|
131
|
+
request: httpx.Request,
|
|
132
|
+
) -> httpx.Response:
|
|
133
|
+
internal_request = httpx_to_internal(request)
|
|
134
|
+
internal_response = self._cache_proxy.handle_request(internal_request)
|
|
135
|
+
response = internal_to_httpx(internal_response)
|
|
136
|
+
return response
|
|
137
|
+
|
|
138
|
+
def close(self) -> None:
|
|
139
|
+
self.next_transport.close()
|
|
140
|
+
super().close()
|
|
141
|
+
|
|
142
|
+
def sync_send_request(self, request: Request) -> Response:
|
|
143
|
+
httpx_request = internal_to_httpx(request)
|
|
144
|
+
httpx_response = self.next_transport.handle_request(httpx_request)
|
|
145
|
+
return httpx_to_internal(httpx_response)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SyncCacheClient(httpx.Client):
|
|
149
|
+
@overload
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
storage: SyncBaseStorage | None = None,
|
|
154
|
+
cache_options: CacheOptions | None = None,
|
|
155
|
+
**kwargs: t.Any,
|
|
156
|
+
) -> None: ...
|
|
157
|
+
@overload
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
*args: t.Any,
|
|
161
|
+
**kwargs: t.Any,
|
|
162
|
+
) -> None: ...
|
|
163
|
+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
164
|
+
self.storage: SyncBaseStorage | None = kwargs.pop("storage", None)
|
|
165
|
+
self.cache_options: CacheOptions | None = kwargs.pop("cache_options", None)
|
|
166
|
+
super().__init__(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
def _init_transport(
|
|
169
|
+
self,
|
|
170
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
171
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
172
|
+
trust_env: bool = True,
|
|
173
|
+
http1: bool = True,
|
|
174
|
+
http2: bool = False,
|
|
175
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
176
|
+
transport: httpx.BaseTransport | None = None,
|
|
177
|
+
**kwargs: t.Any,
|
|
178
|
+
) -> httpx.BaseTransport:
|
|
179
|
+
if transport is not None:
|
|
180
|
+
return transport
|
|
181
|
+
|
|
182
|
+
return SyncCacheTransport(
|
|
183
|
+
next_transport=httpx.HTTPTransport(
|
|
184
|
+
verify=verify,
|
|
185
|
+
cert=cert,
|
|
186
|
+
trust_env=trust_env,
|
|
187
|
+
http1=http1,
|
|
188
|
+
http2=http2,
|
|
189
|
+
limits=limits,
|
|
190
|
+
),
|
|
191
|
+
storage=self.storage,
|
|
192
|
+
cache_options=self.cache_options,
|
|
193
|
+
ignore_specification=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _init_proxy_transport(
|
|
197
|
+
self,
|
|
198
|
+
proxy: httpx.Proxy,
|
|
199
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
200
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
201
|
+
trust_env: bool = True,
|
|
202
|
+
http1: bool = True,
|
|
203
|
+
http2: bool = False,
|
|
204
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
205
|
+
**kwargs: t.Any,
|
|
206
|
+
) -> httpx.BaseTransport:
|
|
207
|
+
return SyncCacheTransport(
|
|
208
|
+
next_transport=httpx.HTTPTransport(
|
|
209
|
+
verify=verify,
|
|
210
|
+
cert=cert,
|
|
211
|
+
trust_env=trust_env,
|
|
212
|
+
http1=http1,
|
|
213
|
+
http2=http2,
|
|
214
|
+
limits=limits,
|
|
215
|
+
proxy=proxy,
|
|
216
|
+
),
|
|
217
|
+
storage=self.storage,
|
|
218
|
+
cache_options=self.cache_options,
|
|
219
|
+
ignore_specification=False,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
224
|
+
def __init__(
|
|
225
|
+
self,
|
|
226
|
+
next_transport: httpx.AsyncBaseTransport,
|
|
227
|
+
storage: AsyncBaseStorage | None = None,
|
|
228
|
+
cache_options: CacheOptions | None = None,
|
|
229
|
+
ignore_specification: bool = False,
|
|
230
|
+
) -> None:
|
|
231
|
+
self.next_transport = next_transport
|
|
232
|
+
self._cache_proxy: AsyncCacheProxy = AsyncCacheProxy(
|
|
233
|
+
send_request=self.async_send_request,
|
|
234
|
+
storage=storage,
|
|
235
|
+
cache_options=cache_options,
|
|
236
|
+
ignore_specification=ignore_specification,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
async def handle_async_request(
|
|
240
|
+
self,
|
|
241
|
+
request: httpx.Request,
|
|
242
|
+
) -> httpx.Response:
|
|
243
|
+
internal_request = httpx_to_internal(request)
|
|
244
|
+
internal_response = await self._cache_proxy.handle_request(internal_request)
|
|
245
|
+
response = internal_to_httpx(internal_response)
|
|
246
|
+
return response
|
|
247
|
+
|
|
248
|
+
async def aclose(self) -> None:
|
|
249
|
+
await self.next_transport.aclose()
|
|
250
|
+
await super().aclose()
|
|
251
|
+
|
|
252
|
+
async def async_send_request(self, request: Request) -> Response:
|
|
253
|
+
httpx_request = internal_to_httpx(request)
|
|
254
|
+
httpx_response = await self.next_transport.handle_async_request(httpx_request)
|
|
255
|
+
return httpx_to_internal(httpx_response)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class AsyncCacheClient(httpx.AsyncClient):
|
|
259
|
+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
260
|
+
self.storage: AsyncBaseStorage | None = kwargs.pop("storage", None)
|
|
261
|
+
self.cache_options: CacheOptions | None = kwargs.pop("cache_options", None)
|
|
262
|
+
self.ignore_specification: bool = kwargs.pop("ignore_specification", False)
|
|
263
|
+
super().__init__(*args, **kwargs)
|
|
264
|
+
|
|
265
|
+
def _init_transport(
|
|
266
|
+
self,
|
|
267
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
268
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
269
|
+
trust_env: bool = True,
|
|
270
|
+
http1: bool = True,
|
|
271
|
+
http2: bool = False,
|
|
272
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
273
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
274
|
+
**kwargs: t.Any,
|
|
275
|
+
) -> httpx.AsyncBaseTransport:
|
|
276
|
+
if transport is not None:
|
|
277
|
+
return transport
|
|
278
|
+
|
|
279
|
+
return AsyncCacheTransport(
|
|
280
|
+
next_transport=httpx.AsyncHTTPTransport(
|
|
281
|
+
verify=verify,
|
|
282
|
+
cert=cert,
|
|
283
|
+
trust_env=trust_env,
|
|
284
|
+
http1=http1,
|
|
285
|
+
http2=http2,
|
|
286
|
+
limits=limits,
|
|
287
|
+
),
|
|
288
|
+
storage=self.storage,
|
|
289
|
+
cache_options=self.cache_options,
|
|
290
|
+
ignore_specification=False,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _init_proxy_transport(
|
|
294
|
+
self,
|
|
295
|
+
proxy: httpx.Proxy,
|
|
296
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
297
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
298
|
+
trust_env: bool = True,
|
|
299
|
+
http1: bool = True,
|
|
300
|
+
http2: bool = False,
|
|
301
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
302
|
+
**kwargs: t.Any,
|
|
303
|
+
) -> httpx.AsyncBaseTransport:
|
|
304
|
+
return AsyncCacheTransport(
|
|
305
|
+
next_transport=httpx.AsyncHTTPTransport(
|
|
306
|
+
verify=verify,
|
|
307
|
+
cert=cert,
|
|
308
|
+
trust_env=trust_env,
|
|
309
|
+
http1=http1,
|
|
310
|
+
http2=http2,
|
|
311
|
+
limits=limits,
|
|
312
|
+
proxy=proxy,
|
|
313
|
+
),
|
|
314
|
+
storage=self.storage,
|
|
315
|
+
cache_options=self.cache_options,
|
|
316
|
+
ignore_specification=self.ignore_specification,
|
|
317
|
+
)
|
hishel/beta/requests.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import RawIOBase
|
|
4
|
+
from typing import Iterator, Mapping, Optional, overload
|
|
5
|
+
|
|
6
|
+
from typing_extensions import assert_never
|
|
7
|
+
|
|
8
|
+
from hishel._utils import snake_to_header
|
|
9
|
+
from hishel.beta import Headers, Request, Response as Response
|
|
10
|
+
from hishel.beta._core._base._storages._base import SyncBaseStorage
|
|
11
|
+
from hishel.beta._core._spec import CacheOptions
|
|
12
|
+
from hishel.beta._core.models import extract_metadata_from_headers
|
|
13
|
+
from hishel.beta._sync_cache import SyncCacheProxy
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import requests
|
|
17
|
+
from requests.adapters import HTTPAdapter
|
|
18
|
+
from urllib3 import HTTPResponse
|
|
19
|
+
from urllib3.util.retry import Retry as Retry
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"The 'requests' library is required to use the requests integration. "
|
|
23
|
+
"Install hishel with 'pip install hishel[requests]'."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IteratorStream(RawIOBase):
|
|
28
|
+
def __init__(self, iterator: Iterator[bytes]):
|
|
29
|
+
self.iterator = iterator
|
|
30
|
+
self.leftover = b""
|
|
31
|
+
|
|
32
|
+
def readable(self) -> bool:
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
def readinto(self, b: bytearray) -> Optional[int]: # type: ignore
|
|
36
|
+
chunk = self.read(len(b))
|
|
37
|
+
if not chunk:
|
|
38
|
+
return 0
|
|
39
|
+
n = len(chunk)
|
|
40
|
+
b[:n] = chunk
|
|
41
|
+
return n
|
|
42
|
+
|
|
43
|
+
def read(self, size: int = -1) -> bytes:
|
|
44
|
+
if size is None or size < 0:
|
|
45
|
+
result = self.leftover + b"".join(self.iterator)
|
|
46
|
+
self.leftover = b""
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
while len(self.leftover) < size:
|
|
50
|
+
try:
|
|
51
|
+
self.leftover += next(self.iterator)
|
|
52
|
+
except StopIteration:
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
result = self.leftover[:size]
|
|
56
|
+
self.leftover = self.leftover[size:]
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def requests_to_internal(
|
|
62
|
+
model: requests.models.PreparedRequest,
|
|
63
|
+
) -> Request: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def requests_to_internal(
|
|
68
|
+
model: requests.models.Response,
|
|
69
|
+
) -> Response: ...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def requests_to_internal(
|
|
73
|
+
model: requests.models.PreparedRequest | requests.models.Response,
|
|
74
|
+
) -> Request | Response:
|
|
75
|
+
if isinstance(model, requests.models.PreparedRequest):
|
|
76
|
+
body: bytes
|
|
77
|
+
if isinstance(model.body, str):
|
|
78
|
+
body = model.body.encode("utf-8")
|
|
79
|
+
elif isinstance(model.body, bytes):
|
|
80
|
+
body = model.body
|
|
81
|
+
else:
|
|
82
|
+
body = b""
|
|
83
|
+
assert model.method
|
|
84
|
+
return Request(
|
|
85
|
+
method=model.method,
|
|
86
|
+
url=str(model.url),
|
|
87
|
+
headers=Headers(model.headers),
|
|
88
|
+
stream=iter([body]),
|
|
89
|
+
metadata=extract_metadata_from_headers(model.headers),
|
|
90
|
+
)
|
|
91
|
+
elif isinstance(model, requests.models.Response):
|
|
92
|
+
try:
|
|
93
|
+
stream = model.raw.stream(amt=8192)
|
|
94
|
+
except requests.exceptions.StreamConsumedError:
|
|
95
|
+
stream = iter([model.content])
|
|
96
|
+
|
|
97
|
+
return Response(
|
|
98
|
+
status_code=model.status_code,
|
|
99
|
+
headers=Headers(model.headers),
|
|
100
|
+
stream=stream,
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
assert_never(model)
|
|
104
|
+
raise RuntimeError("This line should never be reached, but is here to satisfy type checkers.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@overload
|
|
108
|
+
def internal_to_requests(model: Request) -> requests.models.PreparedRequest: ...
|
|
109
|
+
@overload
|
|
110
|
+
def internal_to_requests(model: Response) -> requests.models.Response: ...
|
|
111
|
+
def internal_to_requests(model: Request | Response) -> requests.models.Response | requests.models.PreparedRequest:
|
|
112
|
+
if isinstance(model, Response):
|
|
113
|
+
response = requests.models.Response()
|
|
114
|
+
|
|
115
|
+
assert isinstance(model.stream, Iterator)
|
|
116
|
+
# Collect all chunks from the internal stream
|
|
117
|
+
stream = IteratorStream(model.stream)
|
|
118
|
+
|
|
119
|
+
urllib_response = HTTPResponse(
|
|
120
|
+
body=stream,
|
|
121
|
+
headers={**model.headers, **{snake_to_header(k): str(v) for k, v in model.metadata.items()}},
|
|
122
|
+
status=model.status_code,
|
|
123
|
+
preload_content=False,
|
|
124
|
+
decode_content=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Set up the response object
|
|
128
|
+
response.raw = urllib_response
|
|
129
|
+
response.status_code = model.status_code
|
|
130
|
+
response.headers.update(model.headers)
|
|
131
|
+
response.headers.update({snake_to_header(k): str(v) for k, v in model.metadata.items()})
|
|
132
|
+
response.url = "" # Will be set by requests
|
|
133
|
+
response.encoding = response.apparent_encoding
|
|
134
|
+
|
|
135
|
+
return response
|
|
136
|
+
else:
|
|
137
|
+
assert isinstance(model.stream, Iterator)
|
|
138
|
+
request = requests.Request(
|
|
139
|
+
method=model.method,
|
|
140
|
+
url=model.url,
|
|
141
|
+
headers=model.headers,
|
|
142
|
+
data=b"".join(model.stream) if model.stream else None,
|
|
143
|
+
)
|
|
144
|
+
return request.prepare()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CacheAdapter(HTTPAdapter):
|
|
148
|
+
"""
|
|
149
|
+
A custom HTTPAdapter that can be used with requests to capture HTTP interactions
|
|
150
|
+
for snapshot testing.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
pool_connections: int = 10,
|
|
156
|
+
pool_maxsize: int = 10,
|
|
157
|
+
max_retries: int = 0,
|
|
158
|
+
pool_block: bool = False,
|
|
159
|
+
storage: SyncBaseStorage | None = None,
|
|
160
|
+
cache_options: CacheOptions | None = None,
|
|
161
|
+
ignore_specification: bool = False,
|
|
162
|
+
):
|
|
163
|
+
super().__init__(pool_connections, pool_maxsize, max_retries, pool_block)
|
|
164
|
+
self._cache_proxy = SyncCacheProxy(
|
|
165
|
+
send_request=self.send_request,
|
|
166
|
+
storage=storage,
|
|
167
|
+
cache_options=cache_options,
|
|
168
|
+
ignore_specification=ignore_specification,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def send(
|
|
172
|
+
self,
|
|
173
|
+
request: requests.models.PreparedRequest,
|
|
174
|
+
stream: bool = False,
|
|
175
|
+
timeout: None | float | tuple[float, float] | tuple[float, None] = None,
|
|
176
|
+
verify: bool | str = True,
|
|
177
|
+
cert: None | bytes | str | tuple[bytes | str, bytes | str] = None,
|
|
178
|
+
proxies: Mapping[str, str] | None = None,
|
|
179
|
+
) -> requests.models.Response:
|
|
180
|
+
internal_request = requests_to_internal(request)
|
|
181
|
+
internal_response = self._cache_proxy.handle_request(internal_request)
|
|
182
|
+
response = internal_to_requests(internal_response)
|
|
183
|
+
|
|
184
|
+
# Set the original request on the response
|
|
185
|
+
response.request = request
|
|
186
|
+
response.connection = self # type: ignore
|
|
187
|
+
|
|
188
|
+
return response
|
|
189
|
+
|
|
190
|
+
def send_request(self, request: Request) -> Response:
|
|
191
|
+
requests_request = internal_to_requests(request)
|
|
192
|
+
response = super().send(requests_request, stream=True)
|
|
193
|
+
return requests_to_internal(response)
|