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
hishel/_serializers.py
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
import pickle
|
|
4
|
-
import typing as tp
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
from httpcore import Request, Response
|
|
8
|
-
|
|
9
|
-
from ._utils import normalized_url
|
|
10
|
-
|
|
11
|
-
try:
|
|
12
|
-
import yaml
|
|
13
|
-
except ImportError: # pragma: no cover
|
|
14
|
-
yaml = None # type: ignore
|
|
15
|
-
|
|
16
|
-
HEADERS_ENCODING = "iso-8859-1"
|
|
17
|
-
KNOWN_RESPONSE_EXTENSIONS = ("http_version", "reason_phrase")
|
|
18
|
-
KNOWN_REQUEST_EXTENSIONS = ("timeout", "sni_hostname")
|
|
19
|
-
|
|
20
|
-
__all__ = ("BaseSerializer", "JSONSerializer", "Metadata", "PickleSerializer", "YAMLSerializer", "clone_model")
|
|
21
|
-
|
|
22
|
-
T = tp.TypeVar("T", Request, Response)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def clone_model(model: T) -> T:
|
|
26
|
-
if isinstance(model, Response):
|
|
27
|
-
return Response(
|
|
28
|
-
status=model.status,
|
|
29
|
-
headers=model.headers,
|
|
30
|
-
content=model.content,
|
|
31
|
-
extensions={key: value for key, value in model.extensions.items() if key in KNOWN_RESPONSE_EXTENSIONS},
|
|
32
|
-
) # type: ignore
|
|
33
|
-
else:
|
|
34
|
-
return Request(
|
|
35
|
-
method=model.method,
|
|
36
|
-
url=normalized_url(model.url),
|
|
37
|
-
headers=model.headers,
|
|
38
|
-
extensions={key: value for key, value in model.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS},
|
|
39
|
-
) # type: ignore
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class Metadata(tp.TypedDict):
|
|
43
|
-
number_of_uses: int
|
|
44
|
-
created_at: datetime
|
|
45
|
-
cache_key: str
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class BaseSerializer:
|
|
49
|
-
def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]:
|
|
50
|
-
raise NotImplementedError()
|
|
51
|
-
|
|
52
|
-
def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]:
|
|
53
|
-
raise NotImplementedError()
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def is_binary(self) -> bool:
|
|
57
|
-
raise NotImplementedError()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class PickleSerializer(BaseSerializer):
|
|
61
|
-
"""
|
|
62
|
-
A simple pickle-based serializer.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]:
|
|
66
|
-
"""
|
|
67
|
-
Dumps the HTTP response and its HTTP request.
|
|
68
|
-
|
|
69
|
-
:param response: An HTTP response
|
|
70
|
-
:type response: Response
|
|
71
|
-
:param request: An HTTP request
|
|
72
|
-
:type request: Request
|
|
73
|
-
:param metadata: Additional information about the stored response
|
|
74
|
-
:type metadata: Metadata
|
|
75
|
-
:return: Serialized response
|
|
76
|
-
:rtype: tp.Union[str, bytes]
|
|
77
|
-
"""
|
|
78
|
-
clone_response = clone_model(response)
|
|
79
|
-
clone_request = clone_model(request)
|
|
80
|
-
return pickle.dumps((clone_response, clone_request, metadata))
|
|
81
|
-
|
|
82
|
-
def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]:
|
|
83
|
-
"""
|
|
84
|
-
Loads the HTTP response and its HTTP request from serialized data.
|
|
85
|
-
|
|
86
|
-
:param data: Serialized data
|
|
87
|
-
:type data: tp.Union[str, bytes]
|
|
88
|
-
:return: HTTP response and its HTTP request
|
|
89
|
-
:rtype: tp.Tuple[Response, Request, Metadata]
|
|
90
|
-
"""
|
|
91
|
-
assert isinstance(data, bytes)
|
|
92
|
-
return tp.cast(tp.Tuple[Response, Request, Metadata], pickle.loads(data))
|
|
93
|
-
|
|
94
|
-
@property
|
|
95
|
-
def is_binary(self) -> bool: # pragma: no cover
|
|
96
|
-
return True
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class JSONSerializer(BaseSerializer):
|
|
100
|
-
"""A simple json-based serializer."""
|
|
101
|
-
|
|
102
|
-
def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]:
|
|
103
|
-
"""
|
|
104
|
-
Dumps the HTTP response and its HTTP request.
|
|
105
|
-
|
|
106
|
-
:param response: An HTTP response
|
|
107
|
-
:type response: Response
|
|
108
|
-
:param request: An HTTP request
|
|
109
|
-
:type request: Request
|
|
110
|
-
:param metadata: Additional information about the stored response
|
|
111
|
-
:type metadata: Metadata
|
|
112
|
-
:return: Serialized response
|
|
113
|
-
:rtype: tp.Union[str, bytes]
|
|
114
|
-
"""
|
|
115
|
-
response_dict = {
|
|
116
|
-
"status": response.status,
|
|
117
|
-
"headers": [
|
|
118
|
-
(key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in response.headers
|
|
119
|
-
],
|
|
120
|
-
"content": base64.b64encode(response.content).decode("ascii"),
|
|
121
|
-
"extensions": {
|
|
122
|
-
key: value.decode("ascii")
|
|
123
|
-
for key, value in response.extensions.items()
|
|
124
|
-
if key in KNOWN_RESPONSE_EXTENSIONS
|
|
125
|
-
},
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
request_dict = {
|
|
129
|
-
"method": request.method.decode("ascii"),
|
|
130
|
-
"url": normalized_url(request.url),
|
|
131
|
-
"headers": [
|
|
132
|
-
(key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in request.headers
|
|
133
|
-
],
|
|
134
|
-
"extensions": {key: value for key, value in request.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS},
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
metadata_dict = {
|
|
138
|
-
"cache_key": metadata["cache_key"],
|
|
139
|
-
"number_of_uses": metadata["number_of_uses"],
|
|
140
|
-
"created_at": metadata["created_at"].strftime("%a, %d %b %Y %H:%M:%S GMT"),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
full_json = {
|
|
144
|
-
"response": response_dict,
|
|
145
|
-
"request": request_dict,
|
|
146
|
-
"metadata": metadata_dict,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return json.dumps(full_json, indent=4)
|
|
150
|
-
|
|
151
|
-
def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]:
|
|
152
|
-
"""
|
|
153
|
-
Loads the HTTP response and its HTTP request from serialized data.
|
|
154
|
-
|
|
155
|
-
:param data: Serialized data
|
|
156
|
-
:type data: tp.Union[str, bytes]
|
|
157
|
-
:return: HTTP response and its HTTP request
|
|
158
|
-
:rtype: tp.Tuple[Response, Request, Metadata]
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
full_json = json.loads(data)
|
|
162
|
-
|
|
163
|
-
response_dict = full_json["response"]
|
|
164
|
-
request_dict = full_json["request"]
|
|
165
|
-
metadata_dict = full_json["metadata"]
|
|
166
|
-
metadata_dict["created_at"] = datetime.strptime(
|
|
167
|
-
metadata_dict["created_at"],
|
|
168
|
-
"%a, %d %b %Y %H:%M:%S GMT",
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
response = Response(
|
|
172
|
-
status=response_dict["status"],
|
|
173
|
-
headers=[
|
|
174
|
-
(key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING))
|
|
175
|
-
for key, value in response_dict["headers"]
|
|
176
|
-
],
|
|
177
|
-
content=base64.b64decode(response_dict["content"].encode("ascii")),
|
|
178
|
-
extensions={
|
|
179
|
-
key: value.encode("ascii")
|
|
180
|
-
for key, value in response_dict["extensions"].items()
|
|
181
|
-
if key in KNOWN_RESPONSE_EXTENSIONS
|
|
182
|
-
},
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
request = Request(
|
|
186
|
-
method=request_dict["method"],
|
|
187
|
-
url=request_dict["url"],
|
|
188
|
-
headers=[
|
|
189
|
-
(key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in request_dict["headers"]
|
|
190
|
-
],
|
|
191
|
-
extensions={
|
|
192
|
-
key: value for key, value in request_dict["extensions"].items() if key in KNOWN_REQUEST_EXTENSIONS
|
|
193
|
-
},
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
metadata = Metadata(
|
|
197
|
-
cache_key=metadata_dict["cache_key"],
|
|
198
|
-
created_at=metadata_dict["created_at"],
|
|
199
|
-
number_of_uses=metadata_dict["number_of_uses"],
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
return response, request, metadata
|
|
203
|
-
|
|
204
|
-
@property
|
|
205
|
-
def is_binary(self) -> bool:
|
|
206
|
-
return False
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
class YAMLSerializer(BaseSerializer):
|
|
210
|
-
"""A simple yaml-based serializer."""
|
|
211
|
-
|
|
212
|
-
def dumps(self, response: Response, request: Request, metadata: Metadata) -> tp.Union[str, bytes]:
|
|
213
|
-
"""
|
|
214
|
-
Dumps the HTTP response and its HTTP request.
|
|
215
|
-
|
|
216
|
-
:param response: An HTTP response
|
|
217
|
-
:type response: Response
|
|
218
|
-
:param request: An HTTP request
|
|
219
|
-
:type request: Request
|
|
220
|
-
:param metadata: Additional information about the stored response
|
|
221
|
-
:type metadata: Metadata
|
|
222
|
-
:return: Serialized response
|
|
223
|
-
:rtype: tp.Union[str, bytes]
|
|
224
|
-
"""
|
|
225
|
-
if yaml is None: # pragma: no cover
|
|
226
|
-
raise RuntimeError(
|
|
227
|
-
f"The `{type(self).__name__}` was used, but the required packages were not found. "
|
|
228
|
-
"Check that you have `Hishel` installed with the `yaml` extension as shown.\n"
|
|
229
|
-
"```pip install hishel[yaml]```"
|
|
230
|
-
)
|
|
231
|
-
response_dict = {
|
|
232
|
-
"status": response.status,
|
|
233
|
-
"headers": [
|
|
234
|
-
(key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in response.headers
|
|
235
|
-
],
|
|
236
|
-
"content": base64.b64encode(response.content).decode("ascii"),
|
|
237
|
-
"extensions": {
|
|
238
|
-
key: value.decode("ascii")
|
|
239
|
-
for key, value in response.extensions.items()
|
|
240
|
-
if key in KNOWN_RESPONSE_EXTENSIONS
|
|
241
|
-
},
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
request_dict = {
|
|
245
|
-
"method": request.method.decode("ascii"),
|
|
246
|
-
"url": normalized_url(request.url),
|
|
247
|
-
"headers": [
|
|
248
|
-
(key.decode(HEADERS_ENCODING), value.decode(HEADERS_ENCODING)) for key, value in request.headers
|
|
249
|
-
],
|
|
250
|
-
"extensions": {key: value for key, value in request.extensions.items() if key in KNOWN_REQUEST_EXTENSIONS},
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
metadata_dict = {
|
|
254
|
-
"cache_key": metadata["cache_key"],
|
|
255
|
-
"number_of_uses": metadata["number_of_uses"],
|
|
256
|
-
"created_at": metadata["created_at"].strftime("%a, %d %b %Y %H:%M:%S GMT"),
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
full_json = {
|
|
260
|
-
"response": response_dict,
|
|
261
|
-
"request": request_dict,
|
|
262
|
-
"metadata": metadata_dict,
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return yaml.safe_dump(full_json, sort_keys=False)
|
|
266
|
-
|
|
267
|
-
def loads(self, data: tp.Union[str, bytes]) -> tp.Tuple[Response, Request, Metadata]:
|
|
268
|
-
"""
|
|
269
|
-
Loads the HTTP response and its HTTP request from serialized data.
|
|
270
|
-
|
|
271
|
-
:param data: Serialized data
|
|
272
|
-
:type data: tp.Union[str, bytes]
|
|
273
|
-
:raises RuntimeError: When used without the `yaml` extension installed
|
|
274
|
-
:return: HTTP response and its HTTP request
|
|
275
|
-
:rtype: tp.Tuple[Response, Request, Metadata]
|
|
276
|
-
"""
|
|
277
|
-
if yaml is None: # pragma: no cover
|
|
278
|
-
raise RuntimeError(
|
|
279
|
-
f"The `{type(self).__name__}` was used, but the required packages were not found. "
|
|
280
|
-
"Check that you have `Hishel` installed with the `yaml` extension as shown.\n"
|
|
281
|
-
"```pip install hishel[yaml]```"
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
full_json = yaml.safe_load(data)
|
|
285
|
-
|
|
286
|
-
response_dict = full_json["response"]
|
|
287
|
-
request_dict = full_json["request"]
|
|
288
|
-
metadata_dict = full_json["metadata"]
|
|
289
|
-
metadata_dict["created_at"] = datetime.strptime(
|
|
290
|
-
metadata_dict["created_at"],
|
|
291
|
-
"%a, %d %b %Y %H:%M:%S GMT",
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
response = Response(
|
|
295
|
-
status=response_dict["status"],
|
|
296
|
-
headers=[
|
|
297
|
-
(key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING))
|
|
298
|
-
for key, value in response_dict["headers"]
|
|
299
|
-
],
|
|
300
|
-
content=base64.b64decode(response_dict["content"].encode("ascii")),
|
|
301
|
-
extensions={
|
|
302
|
-
key: value.encode("ascii")
|
|
303
|
-
for key, value in response_dict["extensions"].items()
|
|
304
|
-
if key in KNOWN_RESPONSE_EXTENSIONS
|
|
305
|
-
},
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
request = Request(
|
|
309
|
-
method=request_dict["method"],
|
|
310
|
-
url=request_dict["url"],
|
|
311
|
-
headers=[
|
|
312
|
-
(key.encode(HEADERS_ENCODING), value.encode(HEADERS_ENCODING)) for key, value in request_dict["headers"]
|
|
313
|
-
],
|
|
314
|
-
extensions={
|
|
315
|
-
key: value for key, value in request_dict["extensions"].items() if key in KNOWN_REQUEST_EXTENSIONS
|
|
316
|
-
},
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
metadata = Metadata(
|
|
320
|
-
cache_key=metadata_dict["cache_key"],
|
|
321
|
-
created_at=metadata_dict["created_at"],
|
|
322
|
-
number_of_uses=metadata_dict["number_of_uses"],
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
return response, request, metadata
|
|
326
|
-
|
|
327
|
-
@property
|
|
328
|
-
def is_binary(self) -> bool: # pragma: no cover
|
|
329
|
-
return False
|
hishel/_sync/__init__.py
DELETED
hishel/_sync/_client.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
|
|
5
|
-
from ._transports import CacheTransport
|
|
6
|
-
|
|
7
|
-
__all__ = ("CacheClient",)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CacheClient(httpx.Client):
|
|
11
|
-
def __init__(self, *args: tp.Any, **kwargs: tp.Any):
|
|
12
|
-
self._storage = kwargs.pop("storage") if "storage" in kwargs else None
|
|
13
|
-
self._controller = kwargs.pop("controller") if "controller" in kwargs else None
|
|
14
|
-
super().__init__(*args, **kwargs)
|
|
15
|
-
|
|
16
|
-
def _init_transport(self, *args, **kwargs) -> CacheTransport: # type: ignore
|
|
17
|
-
_transport = super()._init_transport(*args, **kwargs)
|
|
18
|
-
return CacheTransport(
|
|
19
|
-
transport=_transport,
|
|
20
|
-
storage=self._storage,
|
|
21
|
-
controller=self._controller,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
def _init_proxy_transport(self, *args, **kwargs) -> CacheTransport: # type: ignore
|
|
25
|
-
_transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover
|
|
26
|
-
return CacheTransport( # pragma: no cover
|
|
27
|
-
transport=_transport,
|
|
28
|
-
storage=self._storage,
|
|
29
|
-
controller=self._controller,
|
|
30
|
-
)
|
hishel/_sync/_mock.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
from types import TracebackType
|
|
3
|
-
|
|
4
|
-
import httpcore
|
|
5
|
-
import httpx
|
|
6
|
-
from httpcore._sync.interfaces import RequestInterface
|
|
7
|
-
|
|
8
|
-
if tp.TYPE_CHECKING: # pragma: no cover
|
|
9
|
-
from typing_extensions import Self
|
|
10
|
-
|
|
11
|
-
__all__ = ("MockConnectionPool", "MockTransport")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class MockConnectionPool(RequestInterface):
|
|
15
|
-
def handle_request(self, request: httpcore.Request) -> httpcore.Response:
|
|
16
|
-
assert isinstance(request.stream, tp.Iterable)
|
|
17
|
-
data = b"".join([chunk for chunk in request.stream]) # noqa: F841
|
|
18
|
-
return self.mocked_responses.pop(0)
|
|
19
|
-
|
|
20
|
-
def add_responses(self, responses: tp.List[httpcore.Response]) -> None:
|
|
21
|
-
if not hasattr(self, "mocked_responses"):
|
|
22
|
-
self.mocked_responses = []
|
|
23
|
-
self.mocked_responses.extend(responses)
|
|
24
|
-
|
|
25
|
-
def __enter__(self) -> "Self":
|
|
26
|
-
return self
|
|
27
|
-
|
|
28
|
-
def __exit__(
|
|
29
|
-
self,
|
|
30
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
31
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
32
|
-
traceback: tp.Optional[TracebackType] = None,
|
|
33
|
-
) -> None: ...
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MockTransport(httpx.BaseTransport):
|
|
37
|
-
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
38
|
-
return self.mocked_responses.pop(0)
|
|
39
|
-
|
|
40
|
-
def add_responses(self, responses: tp.List[httpx.Response]) -> None:
|
|
41
|
-
if not hasattr(self, "mocked_responses"):
|
|
42
|
-
self.mocked_responses = []
|
|
43
|
-
self.mocked_responses.extend(responses)
|
hishel/_sync/_pool.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
import typing as tp
|
|
5
|
-
|
|
6
|
-
from httpcore._sync.interfaces import RequestInterface
|
|
7
|
-
from httpcore._exceptions import ConnectError
|
|
8
|
-
from httpcore._models import Request, Response
|
|
9
|
-
|
|
10
|
-
from .._controller import Controller, allowed_stale
|
|
11
|
-
from .._headers import parse_cache_control
|
|
12
|
-
from .._serializers import JSONSerializer, Metadata
|
|
13
|
-
from .._utils import extract_header_values_decoded
|
|
14
|
-
from ._storages import BaseStorage, FileStorage
|
|
15
|
-
|
|
16
|
-
T = tp.TypeVar("T")
|
|
17
|
-
|
|
18
|
-
__all__ = ("CacheConnectionPool",)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def fake_stream(content: bytes) -> tp.Iterable[bytes]:
|
|
22
|
-
yield content
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def generate_504() -> Response:
|
|
26
|
-
return Response(status=504)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CacheConnectionPool(RequestInterface):
|
|
30
|
-
"""An HTTP Core Connection Pool that supports HTTP caching.
|
|
31
|
-
|
|
32
|
-
:param pool: `Connection Pool` that our class wraps in order to add an HTTP Cache layer on top of
|
|
33
|
-
:type pool: RequestInterface
|
|
34
|
-
:param storage: Storage that handles how the responses should be saved., defaults to None
|
|
35
|
-
:type storage: tp.Optional[BaseStorage], optional
|
|
36
|
-
:param controller: Controller that manages the cache behavior at the specification level, defaults to None
|
|
37
|
-
:type controller: tp.Optional[Controller], optional
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
pool: RequestInterface,
|
|
43
|
-
storage: tp.Optional[BaseStorage] = None,
|
|
44
|
-
controller: tp.Optional[Controller] = None,
|
|
45
|
-
) -> None:
|
|
46
|
-
self._pool = pool
|
|
47
|
-
|
|
48
|
-
self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer())
|
|
49
|
-
|
|
50
|
-
if not isinstance(self._storage, BaseStorage): # pragma: no cover
|
|
51
|
-
raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`")
|
|
52
|
-
|
|
53
|
-
self._controller = controller if controller is not None else Controller()
|
|
54
|
-
|
|
55
|
-
def handle_request(self, request: Request) -> Response:
|
|
56
|
-
"""
|
|
57
|
-
Handles HTTP requests while also implementing HTTP caching.
|
|
58
|
-
|
|
59
|
-
:param request: An HTTP request
|
|
60
|
-
:type request: httpcore.Request
|
|
61
|
-
:return: An HTTP response
|
|
62
|
-
:rtype: httpcore.Response
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
if request.extensions.get("cache_disabled", False):
|
|
66
|
-
request.headers.extend([(b"cache-control", b"no-cache"), (b"cache-control", b"max-age=0")])
|
|
67
|
-
|
|
68
|
-
if request.method.upper() not in [b"GET", b"HEAD"]:
|
|
69
|
-
# If the HTTP method is, for example, POST,
|
|
70
|
-
# we must also use the request data to generate the hash.
|
|
71
|
-
assert isinstance(request.stream, tp.Iterable)
|
|
72
|
-
body_for_key = b"".join([chunk for chunk in request.stream])
|
|
73
|
-
request.stream = fake_stream(body_for_key)
|
|
74
|
-
else:
|
|
75
|
-
body_for_key = b""
|
|
76
|
-
|
|
77
|
-
key = self._controller._key_generator(request, body_for_key)
|
|
78
|
-
stored_data = self._storage.retrieve(key)
|
|
79
|
-
|
|
80
|
-
request_cache_control = parse_cache_control(extract_header_values_decoded(request.headers, b"Cache-Control"))
|
|
81
|
-
|
|
82
|
-
if request_cache_control.only_if_cached and not stored_data:
|
|
83
|
-
return generate_504()
|
|
84
|
-
|
|
85
|
-
if stored_data:
|
|
86
|
-
# Try using the stored response if it was discovered.
|
|
87
|
-
|
|
88
|
-
stored_response, stored_request, metadata = stored_data
|
|
89
|
-
|
|
90
|
-
# Immediately read the stored response to avoid issues when trying to access the response body.
|
|
91
|
-
stored_response.read()
|
|
92
|
-
|
|
93
|
-
res = self._controller.construct_response_from_cache(
|
|
94
|
-
request=request,
|
|
95
|
-
response=stored_response,
|
|
96
|
-
original_request=stored_request,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
if isinstance(res, Response):
|
|
100
|
-
# Simply use the response if the controller determines it is ready for use.
|
|
101
|
-
return self._create_hishel_response(
|
|
102
|
-
key=key,
|
|
103
|
-
response=stored_response,
|
|
104
|
-
request=request,
|
|
105
|
-
metadata=metadata,
|
|
106
|
-
cached=True,
|
|
107
|
-
revalidated=False,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if request_cache_control.only_if_cached:
|
|
111
|
-
return generate_504()
|
|
112
|
-
|
|
113
|
-
if isinstance(res, Request):
|
|
114
|
-
# Controller has determined that the response needs to be re-validated.
|
|
115
|
-
|
|
116
|
-
try:
|
|
117
|
-
revalidation_response = self._pool.handle_request(res)
|
|
118
|
-
except ConnectError:
|
|
119
|
-
# If there is a connection error, we can use the stale response if allowed.
|
|
120
|
-
if self._controller._allow_stale and allowed_stale(response=stored_response):
|
|
121
|
-
return self._create_hishel_response(
|
|
122
|
-
key=key,
|
|
123
|
-
response=stored_response,
|
|
124
|
-
request=request,
|
|
125
|
-
metadata=metadata,
|
|
126
|
-
cached=True,
|
|
127
|
-
revalidated=False,
|
|
128
|
-
)
|
|
129
|
-
raise # pragma: no cover
|
|
130
|
-
# Merge headers with the stale response.
|
|
131
|
-
final_response = self._controller.handle_validation_response(
|
|
132
|
-
old_response=stored_response, new_response=revalidation_response
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
final_response.read()
|
|
136
|
-
|
|
137
|
-
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
138
|
-
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
139
|
-
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
140
|
-
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
141
|
-
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
142
|
-
if revalidation_response.status != 304 and self._controller.is_cachable(
|
|
143
|
-
request=request, response=final_response
|
|
144
|
-
):
|
|
145
|
-
self._storage.store(key, response=final_response, request=request)
|
|
146
|
-
|
|
147
|
-
return self._create_hishel_response(
|
|
148
|
-
key=key,
|
|
149
|
-
response=final_response,
|
|
150
|
-
request=request,
|
|
151
|
-
cached=revalidation_response.status == 304,
|
|
152
|
-
revalidated=True,
|
|
153
|
-
metadata=metadata,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
regular_response = self._pool.handle_request(request)
|
|
157
|
-
regular_response.read()
|
|
158
|
-
|
|
159
|
-
if self._controller.is_cachable(request=request, response=regular_response):
|
|
160
|
-
self._storage.store(key, response=regular_response, request=request)
|
|
161
|
-
|
|
162
|
-
return self._create_hishel_response(
|
|
163
|
-
key=key, response=regular_response, request=request, cached=False, revalidated=False
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
def _create_hishel_response(
|
|
167
|
-
self,
|
|
168
|
-
key: str,
|
|
169
|
-
response: Response,
|
|
170
|
-
request: Request,
|
|
171
|
-
cached: bool,
|
|
172
|
-
revalidated: bool,
|
|
173
|
-
metadata: Metadata | None = None,
|
|
174
|
-
) -> Response:
|
|
175
|
-
if cached:
|
|
176
|
-
assert metadata
|
|
177
|
-
metadata["number_of_uses"] += 1
|
|
178
|
-
self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
|
|
179
|
-
response.extensions["from_cache"] = True # type: ignore[index]
|
|
180
|
-
response.extensions["cache_metadata"] = metadata # type: ignore[index]
|
|
181
|
-
else:
|
|
182
|
-
response.extensions["from_cache"] = False # type: ignore[index]
|
|
183
|
-
response.extensions["revalidated"] = revalidated # type: ignore[index]
|
|
184
|
-
return response
|
|
185
|
-
|
|
186
|
-
def close(self) -> None:
|
|
187
|
-
self._storage.close()
|
|
188
|
-
|
|
189
|
-
if hasattr(self._pool, "close"): # pragma: no cover
|
|
190
|
-
self._pool.close()
|
|
191
|
-
|
|
192
|
-
def __enter__(self: T) -> T:
|
|
193
|
-
return self
|
|
194
|
-
|
|
195
|
-
def __exit__(
|
|
196
|
-
self,
|
|
197
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
198
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
199
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
200
|
-
) -> None:
|
|
201
|
-
self.close()
|