hishel 1.0.0.dev2__py3-none-any.whl → 1.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hishel/__init__.py +15 -14
- hishel/_async_cache.py +42 -36
- hishel/_async_httpx.py +243 -0
- hishel/_core/_headers.py +11 -1
- hishel/_core/_spec.py +88 -84
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/{_async/_storages/_sqlite.py → _storages/_async_sqlite.py} +95 -132
- hishel/_core/_storages/_packing.py +144 -0
- hishel/_core/_storages/_sync_base.py +71 -0
- hishel/_core/{_sync/_storages/_sqlite.py → _storages/_sync_sqlite.py} +95 -132
- hishel/_core/models.py +6 -22
- hishel/_sync_cache.py +42 -36
- hishel/_sync_httpx.py +243 -0
- hishel/_utils.py +49 -2
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +3 -326
- hishel/requests.py +25 -17
- {hishel-1.0.0.dev2.dist-info → hishel-1.0.0.dev3.dist-info}/METADATA +100 -6
- hishel-1.0.0.dev3.dist-info/RECORD +23 -0
- hishel/_core/__init__.py +0 -59
- hishel/_core/_base/_storages/_base.py +0 -272
- hishel/_core/_base/_storages/_packing.py +0 -165
- hishel-1.0.0.dev2.dist-info/RECORD +0 -19
- {hishel-1.0.0.dev2.dist-info → hishel-1.0.0.dev3.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev2.dist-info → hishel-1.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
hishel/_sync_httpx.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ssl
|
|
4
|
+
import typing as t
|
|
5
|
+
from typing import (
|
|
6
|
+
Iterable,
|
|
7
|
+
Iterator,
|
|
8
|
+
Iterator,
|
|
9
|
+
Union,
|
|
10
|
+
cast,
|
|
11
|
+
overload,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from httpx import RequestNotRead
|
|
15
|
+
|
|
16
|
+
from hishel import SyncCacheProxy, Headers, Request, Response
|
|
17
|
+
from hishel._core._spec import (
|
|
18
|
+
CacheOptions,
|
|
19
|
+
)
|
|
20
|
+
from hishel._core._storages._sync_base import SyncBaseStorage
|
|
21
|
+
from hishel._core.models import RequestMetadata, extract_metadata_from_headers
|
|
22
|
+
from hishel._utils import (
|
|
23
|
+
filter_mapping,
|
|
24
|
+
make_sync_iterator,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import httpx
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"httpx is required to use hishel.httpx module. "
|
|
32
|
+
"Please install hishel with the 'httpx' extra, "
|
|
33
|
+
"e.g., 'pip install hishel[httpx]'."
|
|
34
|
+
) from e
|
|
35
|
+
|
|
36
|
+
SOCKET_OPTION = t.Union[
|
|
37
|
+
t.Tuple[int, int, int],
|
|
38
|
+
t.Tuple[int, int, t.Union[bytes, bytearray]],
|
|
39
|
+
t.Tuple[int, int, None, int],
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# 128 KB
|
|
43
|
+
CHUNK_SIZE = 131072
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def _internal_to_httpx(
|
|
48
|
+
value: Request,
|
|
49
|
+
) -> httpx.Request: ...
|
|
50
|
+
@overload
|
|
51
|
+
def _internal_to_httpx(
|
|
52
|
+
value: Response,
|
|
53
|
+
) -> httpx.Response: ...
|
|
54
|
+
def _internal_to_httpx(
|
|
55
|
+
value: Union[Request, Response],
|
|
56
|
+
) -> Union[httpx.Request, httpx.Response]:
|
|
57
|
+
"""
|
|
58
|
+
Convert internal Request/Response to httpx.Request/httpx.Response.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(value, Request):
|
|
61
|
+
return httpx.Request(
|
|
62
|
+
method=value.method,
|
|
63
|
+
url=value.url,
|
|
64
|
+
headers=value.headers,
|
|
65
|
+
stream=_IteratorStream(value.iter_stream()),
|
|
66
|
+
extensions=value.metadata,
|
|
67
|
+
)
|
|
68
|
+
elif isinstance(value, Response):
|
|
69
|
+
return httpx.Response(
|
|
70
|
+
status_code=value.status_code,
|
|
71
|
+
headers=value.headers,
|
|
72
|
+
stream=_IteratorStream(value.iter_stream()),
|
|
73
|
+
extensions=value.metadata,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def _httpx_to_internal(
|
|
79
|
+
value: httpx.Request,
|
|
80
|
+
) -> Request: ...
|
|
81
|
+
@overload
|
|
82
|
+
def _httpx_to_internal(
|
|
83
|
+
value: httpx.Response,
|
|
84
|
+
) -> Response: ...
|
|
85
|
+
def _httpx_to_internal(
|
|
86
|
+
value: Union[httpx.Request, httpx.Response],
|
|
87
|
+
) -> Union[Request, Response]:
|
|
88
|
+
"""
|
|
89
|
+
Convert httpx.Request/httpx.Response to internal Request/Response.
|
|
90
|
+
"""
|
|
91
|
+
headers = Headers(
|
|
92
|
+
filter_mapping(
|
|
93
|
+
Headers({key: value for key, value in value.headers.items()}),
|
|
94
|
+
["Transfer-Encoding"],
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
if isinstance(value, httpx.Request):
|
|
98
|
+
extension_metadata = RequestMetadata(
|
|
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
|
+
hishel_body_key=value.extensions.get("hishel_body_key"),
|
|
103
|
+
)
|
|
104
|
+
headers_metadata = extract_metadata_from_headers(value.headers)
|
|
105
|
+
|
|
106
|
+
for key, val in extension_metadata.items():
|
|
107
|
+
if key in value.extensions:
|
|
108
|
+
headers_metadata[key] = val # type: ignore
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
stream = make_sync_iterator([value.content])
|
|
112
|
+
except RequestNotRead:
|
|
113
|
+
stream = cast(Iterator[bytes], value.stream)
|
|
114
|
+
|
|
115
|
+
return Request(
|
|
116
|
+
method=value.method,
|
|
117
|
+
url=str(value.url),
|
|
118
|
+
headers=headers,
|
|
119
|
+
stream=stream,
|
|
120
|
+
metadata=headers_metadata,
|
|
121
|
+
)
|
|
122
|
+
elif isinstance(value, httpx.Response):
|
|
123
|
+
if value.is_stream_consumed and "content-encoding" in value.headers:
|
|
124
|
+
raise RuntimeError("Can't get the raw stream of a response with `Content-Encoding` header.")
|
|
125
|
+
stream = (
|
|
126
|
+
make_sync_iterator([value.content]) if value.is_stream_consumed else value.iter_raw(chunk_size=CHUNK_SIZE)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return Response(
|
|
130
|
+
status_code=value.status_code,
|
|
131
|
+
headers=headers,
|
|
132
|
+
stream=stream,
|
|
133
|
+
metadata={},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class _IteratorStream(httpx.SyncByteStream, httpx.AsyncByteStream):
|
|
138
|
+
def __init__(self, iterator: Iterator[bytes] | Iterator[bytes]) -> None:
|
|
139
|
+
self.iterator = iterator
|
|
140
|
+
|
|
141
|
+
def __iter__(self) -> Iterator[bytes]:
|
|
142
|
+
assert isinstance(self.iterator, (Iterator, Iterable))
|
|
143
|
+
for chunk in self.iterator:
|
|
144
|
+
yield chunk
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SyncCacheTransport(httpx.BaseTransport):
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
next_transport: httpx.BaseTransport,
|
|
151
|
+
storage: SyncBaseStorage | None = None,
|
|
152
|
+
cache_options: CacheOptions | None = None,
|
|
153
|
+
ignore_specification: bool = False,
|
|
154
|
+
) -> None:
|
|
155
|
+
self.next_transport = next_transport
|
|
156
|
+
self._cache_proxy: SyncCacheProxy = SyncCacheProxy(
|
|
157
|
+
request_sender=self.request_sender,
|
|
158
|
+
storage=storage,
|
|
159
|
+
cache_options=cache_options,
|
|
160
|
+
ignore_specification=ignore_specification,
|
|
161
|
+
)
|
|
162
|
+
self.storage = self._cache_proxy.storage
|
|
163
|
+
|
|
164
|
+
def handle_request(
|
|
165
|
+
self,
|
|
166
|
+
request: httpx.Request,
|
|
167
|
+
) -> httpx.Response:
|
|
168
|
+
internal_request = _httpx_to_internal(request)
|
|
169
|
+
internal_response = self._cache_proxy.handle_request(internal_request)
|
|
170
|
+
response = _internal_to_httpx(internal_response)
|
|
171
|
+
return response
|
|
172
|
+
|
|
173
|
+
def close(self) -> None:
|
|
174
|
+
self.next_transport.close()
|
|
175
|
+
self.storage.close()
|
|
176
|
+
super().close()
|
|
177
|
+
|
|
178
|
+
def request_sender(self, request: Request) -> Response:
|
|
179
|
+
httpx_request = _internal_to_httpx(request)
|
|
180
|
+
httpx_response = self.next_transport.handle_request(httpx_request)
|
|
181
|
+
return _httpx_to_internal(httpx_response)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SyncCacheClient(httpx.Client):
|
|
185
|
+
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
|
186
|
+
self.storage: SyncBaseStorage | None = kwargs.pop("storage", None)
|
|
187
|
+
self.cache_options: CacheOptions | None = kwargs.pop("cache_options", None)
|
|
188
|
+
self.ignore_specification: bool = kwargs.pop("ignore_specification", False)
|
|
189
|
+
super().__init__(*args, **kwargs)
|
|
190
|
+
|
|
191
|
+
def _init_transport(
|
|
192
|
+
self,
|
|
193
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
194
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
195
|
+
trust_env: bool = True,
|
|
196
|
+
http1: bool = True,
|
|
197
|
+
http2: bool = False,
|
|
198
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
199
|
+
transport: httpx.BaseTransport | None = None,
|
|
200
|
+
**kwargs: t.Any,
|
|
201
|
+
) -> httpx.BaseTransport:
|
|
202
|
+
if transport is not None:
|
|
203
|
+
return transport
|
|
204
|
+
|
|
205
|
+
return SyncCacheTransport(
|
|
206
|
+
next_transport=httpx.HTTPTransport(
|
|
207
|
+
verify=verify,
|
|
208
|
+
cert=cert,
|
|
209
|
+
trust_env=trust_env,
|
|
210
|
+
http1=http1,
|
|
211
|
+
http2=http2,
|
|
212
|
+
limits=limits,
|
|
213
|
+
),
|
|
214
|
+
storage=self.storage,
|
|
215
|
+
cache_options=self.cache_options,
|
|
216
|
+
ignore_specification=False,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _init_proxy_transport(
|
|
220
|
+
self,
|
|
221
|
+
proxy: httpx.Proxy,
|
|
222
|
+
verify: ssl.SSLContext | str | bool = True,
|
|
223
|
+
cert: t.Union[str, t.Tuple[str, str], t.Tuple[str, str, str], None] = None,
|
|
224
|
+
trust_env: bool = True,
|
|
225
|
+
http1: bool = True,
|
|
226
|
+
http2: bool = False,
|
|
227
|
+
limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
228
|
+
**kwargs: t.Any,
|
|
229
|
+
) -> httpx.BaseTransport:
|
|
230
|
+
return SyncCacheTransport(
|
|
231
|
+
next_transport=httpx.HTTPTransport(
|
|
232
|
+
verify=verify,
|
|
233
|
+
cert=cert,
|
|
234
|
+
trust_env=trust_env,
|
|
235
|
+
http1=http1,
|
|
236
|
+
http2=http2,
|
|
237
|
+
limits=limits,
|
|
238
|
+
proxy=proxy,
|
|
239
|
+
),
|
|
240
|
+
storage=self.storage,
|
|
241
|
+
cache_options=self.cache_options,
|
|
242
|
+
ignore_specification=self.ignore_specification,
|
|
243
|
+
)
|
hishel/_utils.py
CHANGED
|
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import calendar
|
|
4
4
|
import time
|
|
5
5
|
import typing as tp
|
|
6
|
-
from email.utils import parsedate_tz
|
|
6
|
+
from email.utils import formatdate, parsedate_tz
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import AsyncIterator, Iterable, Iterator
|
|
8
9
|
|
|
9
10
|
HEADERS_ENCODING = "iso-8859-1"
|
|
@@ -50,11 +51,35 @@ def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp
|
|
|
50
51
|
return matching, non_matching
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
async def make_async_iterator(
|
|
54
|
+
async def make_async_iterator(
|
|
55
|
+
iterable: Iterable[bytes],
|
|
56
|
+
) -> AsyncIterator[bytes]:
|
|
54
57
|
for item in iterable:
|
|
55
58
|
yield item
|
|
56
59
|
|
|
57
60
|
|
|
61
|
+
def filter_mapping(mapping: tp.Mapping[str, T], keys_to_exclude: tp.Iterable[str]) -> tp.Dict[str, T]:
|
|
62
|
+
"""
|
|
63
|
+
Filter out specified keys from a string-keyed mapping using case-insensitive comparison.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
mapping: The input mapping with string keys to filter.
|
|
67
|
+
keys_to_exclude: An iterable of string keys to exclude (case-insensitive).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A new dictionary with the specified keys excluded.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
```python
|
|
74
|
+
original = {'a': 1, 'B': 2, 'c': 3}
|
|
75
|
+
filtered = filter_mapping(original, ['b'])
|
|
76
|
+
# filtered will be {'a': 1, 'c': 3}
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
exclude_set = {k.lower() for k in keys_to_exclude}
|
|
80
|
+
return {k: v for k, v in mapping.items() if k.lower() not in exclude_set}
|
|
81
|
+
|
|
82
|
+
|
|
58
83
|
def make_sync_iterator(iterable: Iterable[bytes]) -> Iterator[bytes]:
|
|
59
84
|
for item in iterable:
|
|
60
85
|
yield item
|
|
@@ -80,3 +105,25 @@ def snake_to_header(text: str) -> str:
|
|
|
80
105
|
"""
|
|
81
106
|
# Split by underscore, capitalize each word, join with dash, add X- prefix
|
|
82
107
|
return "X-" + "-".join(word.capitalize() for word in text.split("_"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ensure_cache_dict(base_path: str | None = None) -> Path:
|
|
111
|
+
_base_path = Path(base_path) if base_path is not None else Path(".cache/hishel")
|
|
112
|
+
_gitignore_file = _base_path / ".gitignore"
|
|
113
|
+
|
|
114
|
+
_base_path.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
if not _gitignore_file.is_file():
|
|
117
|
+
with open(_gitignore_file, "w", encoding="utf-8") as f:
|
|
118
|
+
f.write("# Automatically created by Hishel\n*")
|
|
119
|
+
return _base_path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_http_date() -> str:
|
|
123
|
+
"""
|
|
124
|
+
Generate a Date header value for HTTP responses.
|
|
125
|
+
Returns date in RFC 1123 format (required by HTTP/1.1).
|
|
126
|
+
|
|
127
|
+
Example output: 'Sun, 26 Oct 2025 12:34:56 GMT'
|
|
128
|
+
"""
|
|
129
|
+
return formatdate(timeval=None, localtime=False, usegmt=True)
|