hishel 0.1.4__py3-none-any.whl → 1.0.0.dev0__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 +55 -53
- hishel/{beta/_async_cache.py → _async_cache.py} +3 -3
- hishel/{beta → _core}/__init__.py +6 -6
- hishel/{beta/_core → _core}/_async/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/_base/_storages/_base.py +13 -1
- hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
- hishel/_core/_headers.py +636 -0
- hishel/{beta/_core → _core}/_spec.py +89 -2
- hishel/{beta/_core → _core}/_sync/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/models.py +1 -1
- hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
- hishel/{beta/httpx.py → httpx.py} +18 -7
- hishel/{beta/requests.py → requests.py} +15 -10
- hishel-1.0.0.dev0.dist-info/METADATA +321 -0
- hishel-1.0.0.dev0.dist-info/RECORD +19 -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/_core/__init__.py +0 -0
- hishel/beta/_core/_headers.py +0 -301
- 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.0.dev0.dist-info}/WHEEL +0 -0
- {hishel-0.1.4.dist-info → hishel-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
hishel/_sync/_transports.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
import typing as tp
|
|
5
|
-
|
|
6
|
-
import httpcore
|
|
7
|
-
import httpx
|
|
8
|
-
from httpx import SyncByteStream, Request, Response
|
|
9
|
-
from httpx._exceptions import ConnectError
|
|
10
|
-
|
|
11
|
-
from .._controller import Controller, allowed_stale
|
|
12
|
-
from .._headers import parse_cache_control
|
|
13
|
-
from .._serializers import JSONSerializer, Metadata
|
|
14
|
-
from .._utils import extract_header_values_decoded, normalized_url
|
|
15
|
-
from ._storages import BaseStorage, FileStorage
|
|
16
|
-
|
|
17
|
-
if tp.TYPE_CHECKING: # pragma: no cover
|
|
18
|
-
from typing_extensions import Self
|
|
19
|
-
|
|
20
|
-
__all__ = ("CacheTransport",)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def fake_stream(content: bytes) -> tp.Iterable[bytes]:
|
|
24
|
-
yield content
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def generate_504() -> Response:
|
|
28
|
-
return Response(status_code=504)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class CacheStream(SyncByteStream):
|
|
32
|
-
def __init__(self, httpcore_stream: tp.Iterable[bytes]):
|
|
33
|
-
self._httpcore_stream = httpcore_stream
|
|
34
|
-
|
|
35
|
-
def __iter__(self) -> tp.Iterator[bytes]:
|
|
36
|
-
for part in self._httpcore_stream:
|
|
37
|
-
yield part
|
|
38
|
-
|
|
39
|
-
def close(self) -> None:
|
|
40
|
-
if hasattr(self._httpcore_stream, "close"):
|
|
41
|
-
self._httpcore_stream.close()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class CacheTransport(httpx.BaseTransport):
|
|
45
|
-
"""
|
|
46
|
-
An HTTPX Transport that supports HTTP caching.
|
|
47
|
-
|
|
48
|
-
:param transport: `Transport` that our class wraps in order to add an HTTP Cache layer on top of
|
|
49
|
-
:type transport: httpx.BaseTransport
|
|
50
|
-
:param storage: Storage that handles how the responses should be saved., defaults to None
|
|
51
|
-
:type storage: tp.Optional[BaseStorage], optional
|
|
52
|
-
:param controller: Controller that manages the cache behavior at the specification level, defaults to None
|
|
53
|
-
:type controller: tp.Optional[Controller], optional
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(
|
|
57
|
-
self,
|
|
58
|
-
transport: httpx.BaseTransport,
|
|
59
|
-
storage: tp.Optional[BaseStorage] = None,
|
|
60
|
-
controller: tp.Optional[Controller] = None,
|
|
61
|
-
) -> None:
|
|
62
|
-
self._transport = transport
|
|
63
|
-
|
|
64
|
-
self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer())
|
|
65
|
-
|
|
66
|
-
if not isinstance(self._storage, BaseStorage): # pragma: no cover
|
|
67
|
-
raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`")
|
|
68
|
-
|
|
69
|
-
self._controller = controller if controller is not None else Controller()
|
|
70
|
-
|
|
71
|
-
def handle_request(self, request: Request) -> Response:
|
|
72
|
-
"""
|
|
73
|
-
Handles HTTP requests while also implementing HTTP caching.
|
|
74
|
-
|
|
75
|
-
:param request: An HTTP request
|
|
76
|
-
:type request: httpx.Request
|
|
77
|
-
:return: An HTTP response
|
|
78
|
-
:rtype: httpx.Response
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
if request.extensions.get("cache_disabled", False):
|
|
82
|
-
request.headers.update(
|
|
83
|
-
[
|
|
84
|
-
("Cache-Control", "no-store"),
|
|
85
|
-
("Cache-Control", "no-cache"),
|
|
86
|
-
*[("cache-control", value) for value in request.headers.get_list("cache-control")],
|
|
87
|
-
]
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if request.method not in ["GET", "HEAD"]:
|
|
91
|
-
# If the HTTP method is, for example, POST,
|
|
92
|
-
# we must also use the request data to generate the hash.
|
|
93
|
-
body_for_key = request.read()
|
|
94
|
-
request.stream = CacheStream(fake_stream(body_for_key))
|
|
95
|
-
else:
|
|
96
|
-
body_for_key = b""
|
|
97
|
-
|
|
98
|
-
# Construct the HTTPCore request because Controllers and Storages work with HTTPCore requests.
|
|
99
|
-
httpcore_request = httpcore.Request(
|
|
100
|
-
method=request.method,
|
|
101
|
-
url=httpcore.URL(
|
|
102
|
-
scheme=request.url.raw_scheme,
|
|
103
|
-
host=request.url.raw_host,
|
|
104
|
-
port=request.url.port,
|
|
105
|
-
target=request.url.raw_path,
|
|
106
|
-
),
|
|
107
|
-
headers=request.headers.raw,
|
|
108
|
-
content=request.stream,
|
|
109
|
-
extensions=request.extensions,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
key = self._controller._key_generator(httpcore_request, body_for_key)
|
|
113
|
-
stored_data = self._storage.retrieve(key)
|
|
114
|
-
|
|
115
|
-
request_cache_control = parse_cache_control(
|
|
116
|
-
extract_header_values_decoded(request.headers.raw, b"Cache-Control")
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
if request_cache_control.only_if_cached and not stored_data:
|
|
120
|
-
return generate_504()
|
|
121
|
-
|
|
122
|
-
if stored_data:
|
|
123
|
-
# Try using the stored response if it was discovered.
|
|
124
|
-
|
|
125
|
-
stored_response, stored_request, metadata = stored_data
|
|
126
|
-
|
|
127
|
-
# Immediately read the stored response to avoid issues when trying to access the response body.
|
|
128
|
-
stored_response.read()
|
|
129
|
-
|
|
130
|
-
res = self._controller.construct_response_from_cache(
|
|
131
|
-
request=httpcore_request,
|
|
132
|
-
response=stored_response,
|
|
133
|
-
original_request=stored_request,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if isinstance(res, httpcore.Response):
|
|
137
|
-
# Simply use the response if the controller determines it is ready for use.
|
|
138
|
-
return self._create_hishel_response(
|
|
139
|
-
key=key,
|
|
140
|
-
response=res,
|
|
141
|
-
request=httpcore_request,
|
|
142
|
-
cached=True,
|
|
143
|
-
revalidated=False,
|
|
144
|
-
metadata=metadata,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
if request_cache_control.only_if_cached:
|
|
148
|
-
return generate_504()
|
|
149
|
-
|
|
150
|
-
if isinstance(res, httpcore.Request):
|
|
151
|
-
# Controller has determined that the response needs to be re-validated.
|
|
152
|
-
assert isinstance(res.stream, tp.Iterable)
|
|
153
|
-
revalidation_request = Request(
|
|
154
|
-
method=res.method.decode(),
|
|
155
|
-
url=normalized_url(res.url),
|
|
156
|
-
headers=res.headers,
|
|
157
|
-
stream=CacheStream(res.stream),
|
|
158
|
-
extensions=res.extensions,
|
|
159
|
-
)
|
|
160
|
-
try:
|
|
161
|
-
revalidation_response = self._transport.handle_request(revalidation_request)
|
|
162
|
-
except ConnectError:
|
|
163
|
-
# If there is a connection error, we can use the stale response if allowed.
|
|
164
|
-
if self._controller._allow_stale and allowed_stale(response=stored_response):
|
|
165
|
-
return self._create_hishel_response(
|
|
166
|
-
key=key,
|
|
167
|
-
response=stored_response,
|
|
168
|
-
request=httpcore_request,
|
|
169
|
-
cached=True,
|
|
170
|
-
revalidated=False,
|
|
171
|
-
metadata=metadata,
|
|
172
|
-
)
|
|
173
|
-
raise # pragma: no cover
|
|
174
|
-
assert isinstance(revalidation_response.stream, tp.Iterable)
|
|
175
|
-
httpcore_revalidation_response = httpcore.Response(
|
|
176
|
-
status=revalidation_response.status_code,
|
|
177
|
-
headers=revalidation_response.headers.raw,
|
|
178
|
-
content=CacheStream(revalidation_response.stream),
|
|
179
|
-
extensions=revalidation_response.extensions,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Merge headers with the stale response.
|
|
183
|
-
final_httpcore_response = self._controller.handle_validation_response(
|
|
184
|
-
old_response=stored_response,
|
|
185
|
-
new_response=httpcore_revalidation_response,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
final_httpcore_response.read()
|
|
189
|
-
revalidation_response.close()
|
|
190
|
-
|
|
191
|
-
assert isinstance(final_httpcore_response.stream, tp.Iterable)
|
|
192
|
-
|
|
193
|
-
# RFC 9111: 4.3.3. Handling a Validation Response
|
|
194
|
-
# A 304 (Not Modified) response status code indicates that the stored response can be updated and
|
|
195
|
-
# reused. A full response (i.e., one containing content) indicates that none of the stored responses
|
|
196
|
-
# nominated in the conditional request are suitable. Instead, the cache MUST use the full response to
|
|
197
|
-
# satisfy the request. The cache MAY store such a full response, subject to its constraints.
|
|
198
|
-
if revalidation_response.status_code != 304 and self._controller.is_cachable(
|
|
199
|
-
request=httpcore_request, response=final_httpcore_response
|
|
200
|
-
):
|
|
201
|
-
self._storage.store(key, response=final_httpcore_response, request=httpcore_request)
|
|
202
|
-
|
|
203
|
-
return self._create_hishel_response(
|
|
204
|
-
key=key,
|
|
205
|
-
response=final_httpcore_response,
|
|
206
|
-
request=httpcore_request,
|
|
207
|
-
cached=revalidation_response.status_code == 304,
|
|
208
|
-
revalidated=True,
|
|
209
|
-
metadata=metadata,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
regular_response = self._transport.handle_request(request)
|
|
213
|
-
try:
|
|
214
|
-
# Prefer already-read content, if available
|
|
215
|
-
stream = fake_stream(regular_response.content)
|
|
216
|
-
except httpx.ResponseNotRead:
|
|
217
|
-
# Fall back to stream if not yet read
|
|
218
|
-
assert isinstance(regular_response.stream, tp.Iterable)
|
|
219
|
-
stream = regular_response.stream
|
|
220
|
-
httpcore_regular_response = httpcore.Response(
|
|
221
|
-
status=regular_response.status_code,
|
|
222
|
-
headers=regular_response.headers.raw,
|
|
223
|
-
content=CacheStream(stream),
|
|
224
|
-
extensions=regular_response.extensions,
|
|
225
|
-
)
|
|
226
|
-
httpcore_regular_response.read()
|
|
227
|
-
httpcore_regular_response.close()
|
|
228
|
-
|
|
229
|
-
if self._controller.is_cachable(request=httpcore_request, response=httpcore_regular_response):
|
|
230
|
-
self._storage.store(
|
|
231
|
-
key,
|
|
232
|
-
response=httpcore_regular_response,
|
|
233
|
-
request=httpcore_request,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
return self._create_hishel_response(
|
|
237
|
-
key=key,
|
|
238
|
-
response=httpcore_regular_response,
|
|
239
|
-
request=httpcore_request,
|
|
240
|
-
cached=False,
|
|
241
|
-
revalidated=False,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
def _create_hishel_response(
|
|
245
|
-
self,
|
|
246
|
-
key: str,
|
|
247
|
-
response: httpcore.Response,
|
|
248
|
-
request: httpcore.Request,
|
|
249
|
-
cached: bool,
|
|
250
|
-
revalidated: bool,
|
|
251
|
-
metadata: Metadata | None = None,
|
|
252
|
-
) -> Response:
|
|
253
|
-
if cached:
|
|
254
|
-
assert metadata
|
|
255
|
-
metadata["number_of_uses"] += 1
|
|
256
|
-
self._storage.update_metadata(key=key, request=request, response=response, metadata=metadata)
|
|
257
|
-
response.extensions["from_cache"] = True # type: ignore[index]
|
|
258
|
-
response.extensions["cache_metadata"] = metadata # type: ignore[index]
|
|
259
|
-
else:
|
|
260
|
-
response.extensions["from_cache"] = False # type: ignore[index]
|
|
261
|
-
response.extensions["revalidated"] = revalidated # type: ignore[index]
|
|
262
|
-
return Response(
|
|
263
|
-
status_code=response.status,
|
|
264
|
-
headers=response.headers,
|
|
265
|
-
stream=CacheStream(fake_stream(response.content)),
|
|
266
|
-
extensions=response.extensions,
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
def close(self) -> None:
|
|
270
|
-
self._storage.close()
|
|
271
|
-
self._transport.close()
|
|
272
|
-
|
|
273
|
-
def __enter__(self) -> Self:
|
|
274
|
-
return self
|
|
275
|
-
|
|
276
|
-
def __exit__(
|
|
277
|
-
self,
|
|
278
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
279
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
280
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
281
|
-
) -> None:
|
|
282
|
-
self.close()
|
hishel/_synchronization.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import types
|
|
2
|
-
import typing as tp
|
|
3
|
-
from threading import Lock as T_LOCK
|
|
4
|
-
|
|
5
|
-
import anyio
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AsyncLock:
|
|
9
|
-
def __init__(self) -> None:
|
|
10
|
-
self._lock = anyio.Lock()
|
|
11
|
-
|
|
12
|
-
async def __aenter__(self) -> None:
|
|
13
|
-
await self._lock.acquire()
|
|
14
|
-
|
|
15
|
-
async def __aexit__(
|
|
16
|
-
self,
|
|
17
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
18
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
19
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
20
|
-
) -> None:
|
|
21
|
-
self._lock.release()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Lock:
|
|
25
|
-
def __init__(self) -> None:
|
|
26
|
-
self._lock = T_LOCK()
|
|
27
|
-
|
|
28
|
-
def __enter__(self) -> None:
|
|
29
|
-
self._lock.acquire()
|
|
30
|
-
|
|
31
|
-
def __exit__(
|
|
32
|
-
self,
|
|
33
|
-
exc_type: tp.Optional[tp.Type[BaseException]] = None,
|
|
34
|
-
exc_value: tp.Optional[BaseException] = None,
|
|
35
|
-
traceback: tp.Optional[types.TracebackType] = None,
|
|
36
|
-
) -> None:
|
|
37
|
-
self._lock.release()
|
hishel/beta/_core/__init__.py
DELETED
|
File without changes
|
hishel/beta/_core/_headers.py
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import string
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, Dict, Iterator, List, Literal, Mapping, MutableMapping, Optional, Union, cast
|
|
6
|
-
|
|
7
|
-
from hishel._exceptions import ParseError, ValidationError
|
|
8
|
-
|
|
9
|
-
## Grammar
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
HTAB = "\t"
|
|
13
|
-
SP = " "
|
|
14
|
-
obs_text = "".join(chr(i) for i in range(0x80, 0xFF + 1)) # 0x80-0xFF
|
|
15
|
-
|
|
16
|
-
tchar = "!#$%&'*+-.^_`|~0123456789" + string.ascii_letters
|
|
17
|
-
qdtext = "".join(
|
|
18
|
-
[
|
|
19
|
-
HTAB,
|
|
20
|
-
SP,
|
|
21
|
-
"\x21",
|
|
22
|
-
"".join(chr(i) for i in range(0x23, 0x5B + 1)), # 0x23-0x5b
|
|
23
|
-
"".join(chr(i) for i in range(0x5D, 0x7E + 1)), # 0x5D-0x7E
|
|
24
|
-
obs_text,
|
|
25
|
-
]
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
TIME_FIELDS = [
|
|
29
|
-
"max_age",
|
|
30
|
-
"max_stale",
|
|
31
|
-
"min_fresh",
|
|
32
|
-
"s_maxage",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
BOOLEAN_FIELDS = [
|
|
36
|
-
"immutable",
|
|
37
|
-
"must_revalidate",
|
|
38
|
-
"must_understand",
|
|
39
|
-
"no_store",
|
|
40
|
-
"no_transform",
|
|
41
|
-
"only_if_cached",
|
|
42
|
-
"public",
|
|
43
|
-
"proxy_revalidate",
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
LIST_FIELDS = ["no_cache", "private"]
|
|
47
|
-
|
|
48
|
-
__all__ = (
|
|
49
|
-
"CacheControl",
|
|
50
|
-
"Vary",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def strip_ows_around(text: str) -> str:
|
|
55
|
-
return text.strip(" ").strip("\t")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def normalize_directive(text: str) -> str:
|
|
59
|
-
return text.replace("-", "_")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def parse_cache_control(cache_control_value: Optional[str]) -> "CacheControl":
|
|
63
|
-
if cache_control_value is None:
|
|
64
|
-
return CacheControl()
|
|
65
|
-
directives = {}
|
|
66
|
-
|
|
67
|
-
if "no-cache=" in cache_control_value or "private=" in cache_control_value:
|
|
68
|
-
cache_control_splited = [cache_control_value]
|
|
69
|
-
else:
|
|
70
|
-
cache_control_splited = cache_control_value.split(",")
|
|
71
|
-
|
|
72
|
-
for directive in cache_control_splited:
|
|
73
|
-
key: str = ""
|
|
74
|
-
value: Optional[str] = None
|
|
75
|
-
dquote = False
|
|
76
|
-
|
|
77
|
-
if not directive:
|
|
78
|
-
raise ParseError("The directive should not be left blank.")
|
|
79
|
-
|
|
80
|
-
directive = strip_ows_around(directive)
|
|
81
|
-
|
|
82
|
-
if not directive:
|
|
83
|
-
raise ParseError("The directive should not contain only whitespaces.")
|
|
84
|
-
|
|
85
|
-
for i, key_char in enumerate(directive):
|
|
86
|
-
if key_char == "=":
|
|
87
|
-
value = directive[i + 1 :]
|
|
88
|
-
|
|
89
|
-
if not value:
|
|
90
|
-
raise ParseError("The directive value cannot be left blank.")
|
|
91
|
-
|
|
92
|
-
if value[0] == '"':
|
|
93
|
-
dquote = True
|
|
94
|
-
if dquote and value[-1] != '"':
|
|
95
|
-
raise ParseError("Invalid quotes around the value.")
|
|
96
|
-
|
|
97
|
-
if not dquote:
|
|
98
|
-
for value_char in value:
|
|
99
|
-
if value_char not in tchar:
|
|
100
|
-
raise ParseError(
|
|
101
|
-
f"The character '{value_char!r}' is not permitted for the unquoted values."
|
|
102
|
-
)
|
|
103
|
-
else:
|
|
104
|
-
for value_char in value[1:-1]:
|
|
105
|
-
if value_char not in qdtext:
|
|
106
|
-
raise ParseError(f"The character '{value_char!r}' is not permitted for the quoted values.")
|
|
107
|
-
break
|
|
108
|
-
|
|
109
|
-
if key_char not in tchar:
|
|
110
|
-
raise ParseError(f"The character '{key_char!r}' is not permitted in the directive name.")
|
|
111
|
-
key += key_char
|
|
112
|
-
directives[key] = value
|
|
113
|
-
validated_data = CacheControl.validate(directives)
|
|
114
|
-
return CacheControl(**validated_data)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class Vary:
|
|
118
|
-
def __init__(self, values: List[str]) -> None:
|
|
119
|
-
self.values = values
|
|
120
|
-
|
|
121
|
-
@classmethod
|
|
122
|
-
def from_value(cls, vary_value: str) -> "Vary":
|
|
123
|
-
values = []
|
|
124
|
-
|
|
125
|
-
for field_name in vary_value.split(","):
|
|
126
|
-
field_name = field_name.strip()
|
|
127
|
-
values.append(field_name)
|
|
128
|
-
return Vary(values)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@dataclass
|
|
132
|
-
class ContentRange:
|
|
133
|
-
unit: Literal["bytes"]
|
|
134
|
-
range: tuple[int, int] | None
|
|
135
|
-
size: int | None
|
|
136
|
-
|
|
137
|
-
@classmethod
|
|
138
|
-
def from_str(cls, content_range: str) -> "ContentRange":
|
|
139
|
-
words = [word for word in content_range.split(" ") if word != ""]
|
|
140
|
-
|
|
141
|
-
unit = words[0]
|
|
142
|
-
range, size = words[1].split("/")
|
|
143
|
-
|
|
144
|
-
splited_range = range.split("-")
|
|
145
|
-
|
|
146
|
-
return cls(
|
|
147
|
-
unit=cast(Literal["bytes"], unit),
|
|
148
|
-
range=None if range == "*" else (int(splited_range[0]), int(splited_range[1])),
|
|
149
|
-
size=None if size == "*" else int(size),
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@dataclass
|
|
154
|
-
class Range:
|
|
155
|
-
unit: Literal["bytes"]
|
|
156
|
-
range: tuple[int | None, int | None]
|
|
157
|
-
|
|
158
|
-
@classmethod
|
|
159
|
-
def try_from_str(cls, range_header: str) -> "Range" | None:
|
|
160
|
-
# Example: "bytes=0-99,200-299,-500,100-"
|
|
161
|
-
unit, values = range_header.split("=")
|
|
162
|
-
unit = unit.strip()
|
|
163
|
-
parts = [p.strip() for p in values.split(",")]
|
|
164
|
-
|
|
165
|
-
parsed: list[tuple[int | None, int | None]] = []
|
|
166
|
-
for part in parts:
|
|
167
|
-
if "-" not in part:
|
|
168
|
-
raise ValueError(f"Invalid range part: {part}")
|
|
169
|
-
start_str, end_str = part.split("-", 1)
|
|
170
|
-
start = int(start_str) if start_str else None
|
|
171
|
-
end = int(end_str) if end_str else None
|
|
172
|
-
parsed.append((start, end))
|
|
173
|
-
|
|
174
|
-
if len(parsed) != 1:
|
|
175
|
-
# we don't support multiple ranges
|
|
176
|
-
return None
|
|
177
|
-
|
|
178
|
-
return cls(
|
|
179
|
-
unit=cast(Literal["bytes"], unit),
|
|
180
|
-
range=parsed[0],
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
class CacheControl:
|
|
185
|
-
def __init__(
|
|
186
|
-
self,
|
|
187
|
-
immutable: bool = False, # [RFC8246]
|
|
188
|
-
max_age: Optional[int] = None, # [RFC9111, Section 5.2.1.1, 5.2.2.1]
|
|
189
|
-
max_stale: Optional[int] = None, # [RFC9111, Section 5.2.1.2]
|
|
190
|
-
min_fresh: Optional[int] = None, # [RFC9111, Section 5.2.1.3]
|
|
191
|
-
must_revalidate: bool = False, # [RFC9111, Section 5.2.2.2]
|
|
192
|
-
must_understand: bool = False, # [RFC9111, Section 5.2.2.3]
|
|
193
|
-
no_cache: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.1.4, 5.2.2.4]
|
|
194
|
-
no_store: bool = False, # [RFC9111, Section 5.2.1.5, 5.2.2.5]
|
|
195
|
-
no_transform: bool = False, # [RFC9111, Section 5.2.1.6, 5.2.2.6]
|
|
196
|
-
only_if_cached: bool = False, # [RFC9111, Section 5.2.1.7]
|
|
197
|
-
private: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.2.7]
|
|
198
|
-
proxy_revalidate: bool = False, # [RFC9111, Section 5.2.2.8]
|
|
199
|
-
public: bool = False, # [RFC9111, Section 5.2.2.9]
|
|
200
|
-
s_maxage: Optional[int] = None, # [RFC9111, Section 5.2.2.10]
|
|
201
|
-
) -> None:
|
|
202
|
-
self.immutable = immutable
|
|
203
|
-
self.max_age = max_age
|
|
204
|
-
self.max_stale = max_stale
|
|
205
|
-
self.min_fresh = min_fresh
|
|
206
|
-
self.must_revalidate = must_revalidate
|
|
207
|
-
self.must_understand = must_understand
|
|
208
|
-
self.no_cache = no_cache
|
|
209
|
-
self.no_store = no_store
|
|
210
|
-
self.no_transform = no_transform
|
|
211
|
-
self.only_if_cached = only_if_cached
|
|
212
|
-
self.private = private
|
|
213
|
-
self.proxy_revalidate = proxy_revalidate
|
|
214
|
-
self.public = public
|
|
215
|
-
self.s_maxage = s_maxage
|
|
216
|
-
|
|
217
|
-
@classmethod
|
|
218
|
-
def validate(cls, directives: Dict[str, Any]) -> Dict[str, Any]:
|
|
219
|
-
validated_data: Dict[str, Any] = {}
|
|
220
|
-
|
|
221
|
-
for key, value in directives.items():
|
|
222
|
-
key = normalize_directive(key)
|
|
223
|
-
if key in TIME_FIELDS:
|
|
224
|
-
if value is None:
|
|
225
|
-
raise ValidationError(f"The directive '{key}' necessitates a value.")
|
|
226
|
-
|
|
227
|
-
if value[0] == '"' or value[-1] == '"':
|
|
228
|
-
raise ValidationError(f"The argument '{key}' should be an integer, but a quote was found.")
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
validated_data[key] = int(value)
|
|
232
|
-
except Exception:
|
|
233
|
-
raise ValidationError(f"The argument '{key}' should be an integer, but got '{value!r}'.")
|
|
234
|
-
elif key in BOOLEAN_FIELDS:
|
|
235
|
-
if value is not None:
|
|
236
|
-
raise ValidationError(f"The directive '{key}' should have no value, but it does.")
|
|
237
|
-
validated_data[key] = True
|
|
238
|
-
elif key in LIST_FIELDS:
|
|
239
|
-
if value is None:
|
|
240
|
-
validated_data[key] = True
|
|
241
|
-
else:
|
|
242
|
-
values = []
|
|
243
|
-
for list_value in value[1:-1].split(","):
|
|
244
|
-
if not list_value:
|
|
245
|
-
raise ValidationError("The list value must not be empty.")
|
|
246
|
-
list_value = strip_ows_around(list_value)
|
|
247
|
-
values.append(list_value)
|
|
248
|
-
validated_data[key] = values
|
|
249
|
-
|
|
250
|
-
return validated_data
|
|
251
|
-
|
|
252
|
-
def __repr__(self) -> str:
|
|
253
|
-
fields = ""
|
|
254
|
-
|
|
255
|
-
for key in TIME_FIELDS:
|
|
256
|
-
key = key.replace("-", "_")
|
|
257
|
-
value = getattr(self, key)
|
|
258
|
-
if value:
|
|
259
|
-
fields += f"{key}={value}, "
|
|
260
|
-
|
|
261
|
-
for key in BOOLEAN_FIELDS:
|
|
262
|
-
key = key.replace("-", "_")
|
|
263
|
-
value = getattr(self, key)
|
|
264
|
-
if value:
|
|
265
|
-
fields += f"{key}, "
|
|
266
|
-
|
|
267
|
-
fields = fields[:-2]
|
|
268
|
-
|
|
269
|
-
return f"<{type(self).__name__} {fields}>"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
class Headers(MutableMapping[str, str]):
|
|
273
|
-
def __init__(self, headers: Mapping[str, Union[str, List[str]]]) -> None:
|
|
274
|
-
self._headers = {k.lower(): ([v] if isinstance(v, str) else v[:]) for k, v in headers.items()}
|
|
275
|
-
|
|
276
|
-
def get_list(self, key: str) -> Optional[List[str]]:
|
|
277
|
-
return self._headers.get(key.lower(), None)
|
|
278
|
-
|
|
279
|
-
def __getitem__(self, key: str) -> str:
|
|
280
|
-
return ", ".join(self._headers[key.lower()])
|
|
281
|
-
|
|
282
|
-
def __setitem__(self, key: str, value: str) -> None:
|
|
283
|
-
self._headers.setdefault(key.lower(), []).append(value)
|
|
284
|
-
|
|
285
|
-
def __delitem__(self, key: str) -> None:
|
|
286
|
-
del self._headers[key.lower()]
|
|
287
|
-
|
|
288
|
-
def __iter__(self) -> Iterator[str]:
|
|
289
|
-
return iter(self._headers)
|
|
290
|
-
|
|
291
|
-
def __len__(self) -> int:
|
|
292
|
-
return len(self._headers)
|
|
293
|
-
|
|
294
|
-
def __repr__(self) -> str:
|
|
295
|
-
return repr(self._headers)
|
|
296
|
-
|
|
297
|
-
def __str__(self) -> str:
|
|
298
|
-
return str(self._headers)
|
|
299
|
-
|
|
300
|
-
def __eq__(self, other_headers: Any) -> bool:
|
|
301
|
-
return isinstance(other_headers, Headers) and self._headers == other_headers._headers # type: ignore
|