hishel 0.1.4__py3-none-any.whl → 0.1.5__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 +1 -1
- hishel/beta/_core/_base/_storages/_base.py +12 -0
- hishel/beta/_core/_headers.py +553 -218
- hishel/beta/httpx.py +12 -1
- hishel/beta/requests.py +10 -5
- {hishel-0.1.4.dist-info → hishel-0.1.5.dist-info}/METADATA +33 -179
- {hishel-0.1.4.dist-info → hishel-0.1.5.dist-info}/RECORD +9 -9
- {hishel-0.1.4.dist-info → hishel-0.1.5.dist-info}/WHEEL +0 -0
- {hishel-0.1.4.dist-info → hishel-0.1.5.dist-info}/licenses/LICENSE +0 -0
hishel/__init__.py
CHANGED
|
@@ -87,6 +87,12 @@ class SyncBaseStorage(ABC):
|
|
|
87
87
|
"""
|
|
88
88
|
raise NotImplementedError()
|
|
89
89
|
|
|
90
|
+
def close(self) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Close any resources held by the storage backend.
|
|
93
|
+
"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
90
96
|
def is_soft_deleted(self, pair: IncompletePair | CompletePair) -> bool:
|
|
91
97
|
"""
|
|
92
98
|
Check if a pair is soft deleted based on its metadata.
|
|
@@ -205,6 +211,12 @@ class AsyncBaseStorage(ABC):
|
|
|
205
211
|
"""
|
|
206
212
|
raise NotImplementedError()
|
|
207
213
|
|
|
214
|
+
async def close(self) -> None:
|
|
215
|
+
"""
|
|
216
|
+
Close any resources held by the storage backend.
|
|
217
|
+
"""
|
|
218
|
+
pass
|
|
219
|
+
|
|
208
220
|
def is_soft_deleted(self, pair: IncompletePair | CompletePair) -> bool:
|
|
209
221
|
"""
|
|
210
222
|
Check if a pair is soft deleted based on its metadata.
|
hishel/beta/_core/_headers.py
CHANGED
|
@@ -1,117 +1,286 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import string
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Iterator, List, Literal, Mapping, MutableMapping, Optional, Union, cast
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
HTTP token and quoted-string parsing utilities.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
These functions implement RFC 7230 parsing rules for HTTP/1.1 tokens
|
|
10
|
+
and quoted strings.
|
|
11
|
+
"""
|
|
8
12
|
|
|
9
|
-
## Grammar
|
|
10
13
|
|
|
14
|
+
def is_char(c: str) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Check if character is a valid ASCII character (0-127).
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
Per RFC 7230: CHAR = any US-ASCII character (octets 0 - 127)
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
c: Single character string
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if character is valid ASCII (0-127), False otherwise
|
|
25
|
+
"""
|
|
26
|
+
if not c:
|
|
27
|
+
return False
|
|
28
|
+
return ord(c) <= 127
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_ctl(c: str) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Check if character is a control character.
|
|
34
|
+
|
|
35
|
+
Per RFC 7230: CTL = control characters (0-31 and 127)
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
c: Single character string
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if character is a control character, False otherwise
|
|
42
|
+
"""
|
|
43
|
+
if not c:
|
|
44
|
+
return False
|
|
45
|
+
b = ord(c)
|
|
46
|
+
return b <= 31 or b == 127
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_separator(c: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Check if character is an HTTP separator.
|
|
52
|
+
|
|
53
|
+
Per RFC 2616 Section 2.2:
|
|
54
|
+
separators = "(" | ")" | "<" | ">" | "@"
|
|
55
|
+
| "," | ";" | ":" | "\" | <">
|
|
56
|
+
| "/" | "[" | "]" | "?" | "="
|
|
57
|
+
| "{" | "}" | SP | HT
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
c: Single character string
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if character is a separator, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
if not c:
|
|
66
|
+
return False
|
|
67
|
+
return c in '()<>@,;:\\"/[]?={} \t'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_token(c: str) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Check if character is valid in an HTTP token.
|
|
73
|
+
|
|
74
|
+
Per RFC 7230 Section 3.2.6:
|
|
75
|
+
token = 1*tchar
|
|
76
|
+
tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
77
|
+
/ "+" / "-" / "." / "0"-"9" / "A"-"Z"
|
|
78
|
+
/ "^" / "_" / "`" / "a"-"z" / "|" / "~"
|
|
79
|
+
|
|
80
|
+
Implementation: token chars are CHAR but not CTL or separators
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
c: Single character string
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if character is valid in a token, False otherwise
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
>>> is_token('a')
|
|
90
|
+
True
|
|
91
|
+
>>> is_token('Z')
|
|
92
|
+
True
|
|
93
|
+
>>> is_token('5')
|
|
94
|
+
True
|
|
95
|
+
>>> is_token('-')
|
|
96
|
+
True
|
|
97
|
+
>>> is_token('!')
|
|
98
|
+
True
|
|
99
|
+
>>> is_token(' ')
|
|
100
|
+
False
|
|
101
|
+
>>> is_token(',')
|
|
102
|
+
False
|
|
103
|
+
>>> is_token('=')
|
|
104
|
+
False
|
|
105
|
+
"""
|
|
106
|
+
return is_char(c) and not is_ctl(c) and not is_separator(c)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_qd_text(c: str) -> bool:
|
|
110
|
+
r"""
|
|
111
|
+
Check if character is valid in quoted-text.
|
|
112
|
+
|
|
113
|
+
Per RFC 7230 Section 3.2.6:
|
|
114
|
+
quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
|
115
|
+
qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
|
116
|
+
obs-text = %x80-FF
|
|
117
|
+
|
|
118
|
+
In other words:
|
|
119
|
+
- HTAB (0x09)
|
|
120
|
+
- SP (0x20)
|
|
121
|
+
- 0x21 (!)
|
|
122
|
+
- 0x23-0x5B (# to [, excluding " which is 0x22)
|
|
123
|
+
- 0x5D-0x7E (] to ~, excluding \ which is 0x5C)
|
|
124
|
+
- 0x80-0xFF (obs-text, extended ASCII)
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
c: Single character string
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if character is valid quoted-text, False otherwise
|
|
131
|
+
"""
|
|
132
|
+
if not c:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
b = ord(c)
|
|
136
|
+
return (
|
|
137
|
+
b == 0x09 # HTAB
|
|
138
|
+
or b == 0x20 # SP
|
|
139
|
+
or b == 0x21 # !
|
|
140
|
+
or (0x23 <= b <= 0x5B) # # to [ (skips " which is 0x22)
|
|
141
|
+
or (0x5D <= b <= 0x7E) # ] to ~ (skips \ which is 0x5C)
|
|
142
|
+
or b >= 0x80
|
|
143
|
+
) # obs-text
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def http_unquote_pair(c: str) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Unquote a single escaped character from a quoted-pair.
|
|
149
|
+
|
|
150
|
+
Per RFC 7230 Section 3.2.6:
|
|
151
|
+
quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
152
|
+
VCHAR = visible characters (0x21-0x7E)
|
|
153
|
+
|
|
154
|
+
Valid escaped characters:
|
|
155
|
+
- HTAB (0x09)
|
|
156
|
+
- SP (0x20)
|
|
157
|
+
- VCHAR (0x21-0x7E)
|
|
158
|
+
- obs-text (0x80-0xFF)
|
|
159
|
+
|
|
160
|
+
Invalid characters are replaced with '?'
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
c: Single character string (the character after the backslash)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The unquoted character, or '?' if invalid
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
>>> http_unquote_pair('"')
|
|
170
|
+
'"'
|
|
171
|
+
>>> http_unquote_pair('n')
|
|
172
|
+
'n'
|
|
173
|
+
>>> http_unquote_pair('\\')
|
|
174
|
+
'\\'
|
|
175
|
+
"""
|
|
176
|
+
if not c:
|
|
177
|
+
return "?"
|
|
178
|
+
|
|
179
|
+
b = ord(c)
|
|
180
|
+
# Valid characters that can be escaped
|
|
181
|
+
if b == 0x09 or b == 0x20 or (0x21 <= b <= 0x7E) or b >= 0x80:
|
|
182
|
+
return c
|
|
183
|
+
return "?"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def http_unquote(raw: str) -> tuple[int, str]:
|
|
187
|
+
"""
|
|
188
|
+
Unquote an HTTP quoted-string.
|
|
189
|
+
|
|
190
|
+
Per RFC 7230 Section 3.2.6:
|
|
191
|
+
quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
|
192
|
+
quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
193
|
+
|
|
194
|
+
The raw string must begin with a double quote ("). Only the first
|
|
195
|
+
quoted string is parsed. The function returns the number of characters
|
|
196
|
+
consumed and the unquoted result.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
raw: String that must start with a double quote
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Tuple of (eaten, result) where:
|
|
203
|
+
- eaten: number of characters consumed, or -1 on failure
|
|
204
|
+
- result: the unquoted string, or empty string on failure
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
>>> http_unquote('"hello"')
|
|
208
|
+
(7, 'hello')
|
|
209
|
+
>>> http_unquote('"hello world"')
|
|
210
|
+
(13, 'hello world')
|
|
211
|
+
>>> http_unquote('"hello\\"world"')
|
|
212
|
+
(14, 'hello"world')
|
|
213
|
+
>>> http_unquote('"test')
|
|
214
|
+
(-1, '')
|
|
215
|
+
>>> http_unquote('not quoted')
|
|
216
|
+
(-1, '')
|
|
217
|
+
"""
|
|
218
|
+
if not raw or raw[0] != '"':
|
|
219
|
+
return -1, ""
|
|
220
|
+
|
|
221
|
+
buf: list[str] = []
|
|
222
|
+
i = 1 # Start after opening quote
|
|
223
|
+
|
|
224
|
+
while i < len(raw):
|
|
225
|
+
b = raw[i]
|
|
226
|
+
|
|
227
|
+
if b == '"':
|
|
228
|
+
# Found closing quote - success
|
|
229
|
+
return i + 1, "".join(buf)
|
|
230
|
+
|
|
231
|
+
elif b == "\\":
|
|
232
|
+
# Escaped character (quoted-pair)
|
|
233
|
+
if i + 1 >= len(raw):
|
|
234
|
+
# Backslash at end of string - invalid
|
|
235
|
+
return -1, ""
|
|
236
|
+
|
|
237
|
+
# Unquote the next character
|
|
238
|
+
buf.append(http_unquote_pair(raw[i + 1]))
|
|
239
|
+
i += 2 # Skip both backslash and escaped char
|
|
240
|
+
|
|
241
|
+
else:
|
|
242
|
+
# Regular character
|
|
243
|
+
if is_qd_text(b):
|
|
244
|
+
buf.append(b)
|
|
245
|
+
else:
|
|
246
|
+
# Invalid character in quoted text
|
|
247
|
+
buf.append("?")
|
|
248
|
+
i += 1
|
|
249
|
+
|
|
250
|
+
# Reached end without finding closing quote - invalid
|
|
251
|
+
return -1, ""
|
|
15
252
|
|
|
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
253
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(",")
|
|
254
|
+
class Headers(MutableMapping[str, str]):
|
|
255
|
+
def __init__(self, headers: Mapping[str, Union[str, List[str]]]) -> None:
|
|
256
|
+
self._headers = {k.lower(): ([v] if isinstance(v, str) else v[:]) for k, v in headers.items()}
|
|
71
257
|
|
|
72
|
-
|
|
73
|
-
key
|
|
74
|
-
value: Optional[str] = None
|
|
75
|
-
dquote = False
|
|
258
|
+
def get_list(self, key: str) -> Optional[List[str]]:
|
|
259
|
+
return self._headers.get(key.lower(), None)
|
|
76
260
|
|
|
77
|
-
|
|
78
|
-
|
|
261
|
+
def __getitem__(self, key: str) -> str:
|
|
262
|
+
return ", ".join(self._headers[key.lower()])
|
|
79
263
|
|
|
80
|
-
|
|
264
|
+
def __setitem__(self, key: str, value: str) -> None:
|
|
265
|
+
self._headers.setdefault(key.lower(), []).append(value)
|
|
81
266
|
|
|
82
|
-
|
|
83
|
-
|
|
267
|
+
def __delitem__(self, key: str) -> None:
|
|
268
|
+
del self._headers[key.lower()]
|
|
84
269
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
value = directive[i + 1 :]
|
|
270
|
+
def __iter__(self) -> Iterator[str]:
|
|
271
|
+
return iter(self._headers)
|
|
88
272
|
|
|
89
|
-
|
|
90
|
-
|
|
273
|
+
def __len__(self) -> int:
|
|
274
|
+
return len(self._headers)
|
|
91
275
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if dquote and value[-1] != '"':
|
|
95
|
-
raise ParseError("Invalid quotes around the value.")
|
|
276
|
+
def __repr__(self) -> str:
|
|
277
|
+
return repr(self._headers)
|
|
96
278
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
279
|
+
def __str__(self) -> str:
|
|
280
|
+
return str(self._headers)
|
|
108
281
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
key += key_char
|
|
112
|
-
directives[key] = value
|
|
113
|
-
validated_data = CacheControl.validate(directives)
|
|
114
|
-
return CacheControl(**validated_data)
|
|
282
|
+
def __eq__(self, other_headers: Any) -> bool:
|
|
283
|
+
return isinstance(other_headers, Headers) and self._headers == other_headers._headers # type: ignore
|
|
115
284
|
|
|
116
285
|
|
|
117
286
|
class Vary:
|
|
@@ -128,28 +297,6 @@ class Vary:
|
|
|
128
297
|
return Vary(values)
|
|
129
298
|
|
|
130
299
|
|
|
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
300
|
@dataclass
|
|
154
301
|
class Range:
|
|
155
302
|
unit: Literal["bytes"]
|
|
@@ -182,120 +329,308 @@ class Range:
|
|
|
182
329
|
|
|
183
330
|
|
|
184
331
|
class CacheControl:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
332
|
+
"""
|
|
333
|
+
Unified Cache-Control directives for both requests and responses.
|
|
334
|
+
|
|
335
|
+
Supports all standard directives from RFC9111 and experimental directives.
|
|
336
|
+
Uses None for unset values instead of -1.
|
|
337
|
+
|
|
338
|
+
Supported Directives:
|
|
339
|
+
- immutable [RFC8246]
|
|
340
|
+
- max-age [RFC9111, Section 5.2.1.1, 5.2.2.1]
|
|
341
|
+
- max-stale [RFC9111, Section 5.2.1.2]
|
|
342
|
+
- min-fresh [RFC9111, Section 5.2.1.3]
|
|
343
|
+
- must-revalidate [RFC9111, Section 5.2.2.2]
|
|
344
|
+
- must-understand [RFC9111, Section 5.2.2.3]
|
|
345
|
+
- no-cache [RFC9111, Section 5.2.1.4, 5.2.2.4]
|
|
346
|
+
- no-store [RFC9111, Section 5.2.1.5, 5.2.2.5]
|
|
347
|
+
- no-transform [RFC9111, Section 5.2.1.6, 5.2.2.6]
|
|
348
|
+
- only-if-cached [RFC9111, Section 5.2.1.7]
|
|
349
|
+
- private [RFC9111, Section 5.2.2.7]
|
|
350
|
+
- proxy-revalidate [RFC9111, Section 5.2.2.8]
|
|
351
|
+
- public [RFC9111, Section 5.2.2.9]
|
|
352
|
+
- s-maxage [RFC9111, Section 5.2.2.10]
|
|
353
|
+
- stale-if-error [RFC5861, Section 4]
|
|
354
|
+
- stale-while-revalidate [RFC5861, Section 3]
|
|
355
|
+
|
|
356
|
+
no_cache and private can be:
|
|
357
|
+
- None: directive not present
|
|
358
|
+
- True: directive present without field names
|
|
359
|
+
- List[str]: directive present with specific field names
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(self) -> None:
|
|
363
|
+
# Common directives
|
|
364
|
+
self.max_age: Optional[int] = None
|
|
365
|
+
self.no_store: bool = False
|
|
366
|
+
self.no_transform: bool = False
|
|
367
|
+
|
|
368
|
+
# Request-specific
|
|
369
|
+
self.max_stale: Optional[int] = None
|
|
370
|
+
self.min_fresh: Optional[int] = None
|
|
371
|
+
self.only_if_cached: bool = False
|
|
372
|
+
|
|
373
|
+
# Response-specific
|
|
374
|
+
self.must_revalidate: bool = False
|
|
375
|
+
self.must_understand: bool = False
|
|
376
|
+
self.public: bool = False
|
|
377
|
+
self.proxy_revalidate: bool = False
|
|
378
|
+
self.s_maxage: Optional[int] = None
|
|
379
|
+
self.immutable: bool = False
|
|
380
|
+
|
|
381
|
+
# Can be boolean or contain field names
|
|
382
|
+
self.no_cache: Union[bool, List[str]] = False
|
|
383
|
+
self.private: Union[bool, List[str]] = False
|
|
384
|
+
|
|
385
|
+
# Experimental
|
|
386
|
+
self.stale_if_error: Optional[int] = None
|
|
387
|
+
self.stale_while_revalidate: Optional[int] = None
|
|
388
|
+
|
|
389
|
+
# Extensions (unrecognized directives)
|
|
390
|
+
self.extensions: List[str] = []
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def parse_int_value(value: str) -> Optional[int]:
|
|
394
|
+
"""Parse integer value, return None if invalid."""
|
|
395
|
+
try:
|
|
396
|
+
val = int(value)
|
|
397
|
+
# Cap at max int32 for compatibility
|
|
398
|
+
return min(val, 2147483647) if val >= 0 else None
|
|
399
|
+
except (ValueError, OverflowError):
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def parse_field_names(value: str) -> List[str]:
|
|
404
|
+
"""Parse comma-separated field names and canonicalize them."""
|
|
405
|
+
fields = []
|
|
406
|
+
for field in value.split(","):
|
|
407
|
+
field = field.strip()
|
|
408
|
+
if field:
|
|
409
|
+
# Convert to canonical header form (Title-Case)
|
|
410
|
+
canonical = "-".join(word.capitalize() for word in field.split("-"))
|
|
411
|
+
fields.append(canonical)
|
|
412
|
+
return fields
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def has_field_names(token: str) -> bool:
|
|
416
|
+
"""Check if token can have comma-separated field names."""
|
|
417
|
+
return token in ("no-cache", "private")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def parse(value: str) -> CacheControl:
|
|
421
|
+
"""
|
|
422
|
+
Parse a Cache-Control header value character by character.
|
|
423
|
+
|
|
424
|
+
This parser handles quoted values and field names correctly,
|
|
425
|
+
allowing commas within field name lists.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
value: The Cache-Control header value string
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
CacheControl object with parsed directives
|
|
432
|
+
"""
|
|
433
|
+
cc = CacheControl()
|
|
434
|
+
|
|
435
|
+
if not value:
|
|
436
|
+
return cc
|
|
437
|
+
|
|
438
|
+
i = 0
|
|
439
|
+
length = len(value)
|
|
440
|
+
|
|
441
|
+
while i < length:
|
|
442
|
+
# Skip leading whitespace and commas
|
|
443
|
+
while i < length and (value[i] in (" ", "\t", ",")):
|
|
444
|
+
i += 1
|
|
445
|
+
|
|
446
|
+
if i >= length:
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
# Find end of token
|
|
450
|
+
j = i
|
|
451
|
+
while j < length and is_token(value[j]):
|
|
452
|
+
j += 1
|
|
453
|
+
|
|
454
|
+
if j == i:
|
|
455
|
+
# No valid token found, skip this character
|
|
456
|
+
i += 1
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
token = value[i:j].lower()
|
|
460
|
+
token_has_fields = has_field_names(token)
|
|
461
|
+
|
|
462
|
+
# Skip whitespace after token
|
|
463
|
+
while j < length and value[j] in (" ", "\t"):
|
|
464
|
+
j += 1
|
|
465
|
+
|
|
466
|
+
# Check if token has a value (token=value)
|
|
467
|
+
if j < length and value[j] == "=":
|
|
468
|
+
k = j + 1
|
|
469
|
+
|
|
470
|
+
# Skip whitespace after equals sign
|
|
471
|
+
while k < length and value[k] in (" ", "\t"):
|
|
472
|
+
k += 1
|
|
473
|
+
|
|
474
|
+
if k >= length:
|
|
475
|
+
# Directive ends with '=' but no value
|
|
476
|
+
i = k
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
# Check for quoted value
|
|
480
|
+
if value[k] == '"':
|
|
481
|
+
eaten, result = http_unquote(value[k:])
|
|
482
|
+
if eaten == -1:
|
|
483
|
+
# Quote mismatch, skip to next directive
|
|
484
|
+
i = k + 1
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
i = k + eaten
|
|
488
|
+
handle_directive_with_value(cc, token, result)
|
|
489
|
+
else:
|
|
490
|
+
# Unquoted value
|
|
491
|
+
z = k
|
|
492
|
+
while z < length:
|
|
493
|
+
if token_has_fields:
|
|
494
|
+
# For directives with field names, stop only at whitespace
|
|
495
|
+
if value[z] in (" ", "\t"):
|
|
496
|
+
break
|
|
497
|
+
else:
|
|
498
|
+
# For other directives, stop at whitespace or comma
|
|
499
|
+
if value[z] in (" ", "\t", ","):
|
|
500
|
+
break
|
|
501
|
+
z += 1
|
|
502
|
+
|
|
503
|
+
result = value[k:z]
|
|
504
|
+
|
|
505
|
+
# Remove trailing comma if present
|
|
506
|
+
if result and result[-1] == ",":
|
|
507
|
+
result = result[:-1]
|
|
216
508
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
509
|
+
i = z
|
|
510
|
+
handle_directive_with_value(cc, token, result)
|
|
511
|
+
else:
|
|
512
|
+
# Token without value
|
|
513
|
+
handle_directive_without_value(cc, token)
|
|
514
|
+
i = j
|
|
515
|
+
|
|
516
|
+
return cc
|
|
251
517
|
|
|
252
|
-
def __repr__(self) -> str:
|
|
253
|
-
fields = ""
|
|
254
518
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
519
|
+
def handle_directive_with_value(cc: CacheControl, token: str, value: str) -> None:
|
|
520
|
+
"""Handle a directive that has a value."""
|
|
521
|
+
if token == "max-age":
|
|
522
|
+
cc.max_age = parse_int_value(value)
|
|
523
|
+
|
|
524
|
+
elif token == "s-maxage":
|
|
525
|
+
cc.s_maxage = parse_int_value(value)
|
|
526
|
+
|
|
527
|
+
elif token == "max-stale":
|
|
528
|
+
cc.max_stale = parse_int_value(value)
|
|
529
|
+
|
|
530
|
+
elif token == "min-fresh":
|
|
531
|
+
cc.min_fresh = parse_int_value(value)
|
|
532
|
+
|
|
533
|
+
elif token == "stale-if-error":
|
|
534
|
+
cc.stale_if_error = parse_int_value(value)
|
|
535
|
+
|
|
536
|
+
elif token == "stale-while-revalidate":
|
|
537
|
+
cc.stale_while_revalidate = parse_int_value(value)
|
|
538
|
+
|
|
539
|
+
elif token == "no-cache":
|
|
540
|
+
# no-cache with field names
|
|
541
|
+
cc.no_cache = parse_field_names(value)
|
|
542
|
+
|
|
543
|
+
elif token == "private":
|
|
544
|
+
# private with field names
|
|
545
|
+
cc.private = parse_field_names(value)
|
|
260
546
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if value:
|
|
265
|
-
fields += f"{key}, "
|
|
547
|
+
else:
|
|
548
|
+
# Unrecognized directive with value
|
|
549
|
+
cc.extensions.append(f"{token}={value}")
|
|
266
550
|
|
|
267
|
-
fields = fields[:-2]
|
|
268
551
|
|
|
269
|
-
|
|
552
|
+
def handle_directive_without_value(cc: CacheControl, token: str) -> None:
|
|
553
|
+
"""Handle a directive that doesn't have a value."""
|
|
554
|
+
if token == "max-stale":
|
|
555
|
+
# max-stale without value means accept any stale response
|
|
556
|
+
cc.max_stale = 2147483647 # max int32
|
|
270
557
|
|
|
558
|
+
elif token == "no-cache":
|
|
559
|
+
cc.no_cache = True
|
|
271
560
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
self._headers = {k.lower(): ([v] if isinstance(v, str) else v[:]) for k, v in headers.items()}
|
|
561
|
+
elif token == "private":
|
|
562
|
+
cc.private = True
|
|
275
563
|
|
|
276
|
-
|
|
277
|
-
|
|
564
|
+
elif token == "no-store":
|
|
565
|
+
cc.no_store = True
|
|
278
566
|
|
|
279
|
-
|
|
280
|
-
|
|
567
|
+
elif token == "no-transform":
|
|
568
|
+
cc.no_transform = True
|
|
281
569
|
|
|
282
|
-
|
|
283
|
-
|
|
570
|
+
elif token == "only-if-cached":
|
|
571
|
+
cc.only_if_cached = True
|
|
284
572
|
|
|
285
|
-
|
|
286
|
-
|
|
573
|
+
elif token == "must-revalidate":
|
|
574
|
+
cc.must_revalidate = True
|
|
287
575
|
|
|
288
|
-
|
|
289
|
-
|
|
576
|
+
elif token == "must-understand":
|
|
577
|
+
cc.must_understand = True
|
|
290
578
|
|
|
291
|
-
|
|
292
|
-
|
|
579
|
+
elif token == "public":
|
|
580
|
+
cc.public = True
|
|
293
581
|
|
|
294
|
-
|
|
295
|
-
|
|
582
|
+
elif token == "proxy-revalidate":
|
|
583
|
+
cc.proxy_revalidate = True
|
|
296
584
|
|
|
297
|
-
|
|
298
|
-
|
|
585
|
+
elif token == "immutable":
|
|
586
|
+
cc.immutable = True
|
|
299
587
|
|
|
300
|
-
|
|
301
|
-
|
|
588
|
+
else:
|
|
589
|
+
# Unrecognized directive without value
|
|
590
|
+
cc.extensions.append(token)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def parse_cache_control(value: str | None) -> CacheControl:
|
|
594
|
+
"""
|
|
595
|
+
Parse a Cache-Control header from either a request or response.
|
|
596
|
+
|
|
597
|
+
This is the main entry point for parsing.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
value: The Cache-Control header value
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
CacheControl object containing all parsed directives
|
|
604
|
+
|
|
605
|
+
Examples:
|
|
606
|
+
>>> # Response example
|
|
607
|
+
>>> cc = parse_cache_control("public, max-age=3600, must-revalidate")
|
|
608
|
+
>>> cc.public
|
|
609
|
+
True
|
|
610
|
+
>>> cc.max_age
|
|
611
|
+
3600
|
|
612
|
+
>>> cc.must_revalidate
|
|
613
|
+
True
|
|
614
|
+
|
|
615
|
+
>>> # Request example
|
|
616
|
+
>>> cc = parse_cache_control("max-age=0, no-cache")
|
|
617
|
+
>>> cc.max_age
|
|
618
|
+
0
|
|
619
|
+
>>> cc.no_cache
|
|
620
|
+
True
|
|
621
|
+
|
|
622
|
+
>>> # With field names
|
|
623
|
+
>>> cc = parse_cache_control('no-cache="Set-Cookie, Authorization"')
|
|
624
|
+
>>> cc.no_cache
|
|
625
|
+
['Set-Cookie', 'Authorization']
|
|
626
|
+
|
|
627
|
+
>>> # Experimental directives
|
|
628
|
+
>>> cc = parse_cache_control("immutable, stale-while-revalidate=86400")
|
|
629
|
+
>>> cc.immutable
|
|
630
|
+
True
|
|
631
|
+
>>> cc.stale_while_revalidate
|
|
632
|
+
86400
|
|
633
|
+
"""
|
|
634
|
+
if value is None:
|
|
635
|
+
return CacheControl()
|
|
636
|
+
return parse(value)
|
hishel/beta/httpx.py
CHANGED
|
@@ -21,6 +21,9 @@ SOCKET_OPTION = t.Union[
|
|
|
21
21
|
t.Tuple[int, int, None, int],
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
# 128 KB
|
|
25
|
+
CHUNK_SIZE = 131072
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
class IteratorStream(httpx.SyncByteStream, httpx.AsyncByteStream):
|
|
26
29
|
def __init__(self, iterator: Iterator[bytes] | AsyncIterator[bytes]) -> None:
|
|
@@ -86,7 +89,11 @@ def httpx_to_internal(
|
|
|
86
89
|
stream = AnyIterable(value.content)
|
|
87
90
|
except (httpx.RequestNotRead, httpx.ResponseNotRead):
|
|
88
91
|
if isinstance(value, httpx.Response):
|
|
89
|
-
stream =
|
|
92
|
+
stream = (
|
|
93
|
+
value.iter_raw(chunk_size=CHUNK_SIZE)
|
|
94
|
+
if isinstance(value.stream, Iterable)
|
|
95
|
+
else value.aiter_raw(chunk_size=CHUNK_SIZE)
|
|
96
|
+
)
|
|
90
97
|
else:
|
|
91
98
|
stream = value.stream # type: ignore
|
|
92
99
|
if isinstance(value, httpx.Request):
|
|
@@ -125,6 +132,7 @@ class SyncCacheTransport(httpx.BaseTransport):
|
|
|
125
132
|
cache_options=cache_options,
|
|
126
133
|
ignore_specification=ignore_specification,
|
|
127
134
|
)
|
|
135
|
+
self.storage = self._cache_proxy.storage
|
|
128
136
|
|
|
129
137
|
def handle_request(
|
|
130
138
|
self,
|
|
@@ -137,6 +145,7 @@ class SyncCacheTransport(httpx.BaseTransport):
|
|
|
137
145
|
|
|
138
146
|
def close(self) -> None:
|
|
139
147
|
self.next_transport.close()
|
|
148
|
+
self.storage.close()
|
|
140
149
|
super().close()
|
|
141
150
|
|
|
142
151
|
def sync_send_request(self, request: Request) -> Response:
|
|
@@ -235,6 +244,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
235
244
|
cache_options=cache_options,
|
|
236
245
|
ignore_specification=ignore_specification,
|
|
237
246
|
)
|
|
247
|
+
self.storage = self._cache_proxy.storage
|
|
238
248
|
|
|
239
249
|
async def handle_async_request(
|
|
240
250
|
self,
|
|
@@ -247,6 +257,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
247
257
|
|
|
248
258
|
async def aclose(self) -> None:
|
|
249
259
|
await self.next_transport.aclose()
|
|
260
|
+
await self.storage.close()
|
|
250
261
|
await super().aclose()
|
|
251
262
|
|
|
252
263
|
async def async_send_request(self, request: Request) -> Response:
|
hishel/beta/requests.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from io import RawIOBase
|
|
4
|
-
from typing import Iterator, Mapping, Optional, overload
|
|
4
|
+
from typing import Any, Iterator, Mapping, Optional, overload
|
|
5
5
|
|
|
6
6
|
from typing_extensions import assert_never
|
|
7
7
|
|
|
@@ -23,6 +23,9 @@ except ImportError: # pragma: no cover
|
|
|
23
23
|
"Install hishel with 'pip install hishel[requests]'."
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
+
# 128 KB
|
|
27
|
+
CHUNK_SIZE = 131072
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
class IteratorStream(RawIOBase):
|
|
28
31
|
def __init__(self, iterator: Iterator[bytes]):
|
|
@@ -90,7 +93,7 @@ def requests_to_internal(
|
|
|
90
93
|
)
|
|
91
94
|
elif isinstance(model, requests.models.Response):
|
|
92
95
|
try:
|
|
93
|
-
stream = model.raw.stream(amt=
|
|
96
|
+
stream = model.raw.stream(amt=CHUNK_SIZE, decode_content=None)
|
|
94
97
|
except requests.exceptions.StreamConsumedError:
|
|
95
98
|
stream = iter([model.content])
|
|
96
99
|
|
|
@@ -113,7 +116,6 @@ def internal_to_requests(model: Request | Response) -> requests.models.Response
|
|
|
113
116
|
response = requests.models.Response()
|
|
114
117
|
|
|
115
118
|
assert isinstance(model.stream, Iterator)
|
|
116
|
-
# Collect all chunks from the internal stream
|
|
117
119
|
stream = IteratorStream(model.stream)
|
|
118
120
|
|
|
119
121
|
urllib_response = HTTPResponse(
|
|
@@ -121,7 +123,7 @@ def internal_to_requests(model: Request | Response) -> requests.models.Response
|
|
|
121
123
|
headers={**model.headers, **{snake_to_header(k): str(v) for k, v in model.metadata.items()}},
|
|
122
124
|
status=model.status_code,
|
|
123
125
|
preload_content=False,
|
|
124
|
-
decode_content=
|
|
126
|
+
decode_content=False,
|
|
125
127
|
)
|
|
126
128
|
|
|
127
129
|
# Set up the response object
|
|
@@ -130,7 +132,6 @@ def internal_to_requests(model: Request | Response) -> requests.models.Response
|
|
|
130
132
|
response.headers.update(model.headers)
|
|
131
133
|
response.headers.update({snake_to_header(k): str(v) for k, v in model.metadata.items()})
|
|
132
134
|
response.url = "" # Will be set by requests
|
|
133
|
-
response.encoding = response.apparent_encoding
|
|
134
135
|
|
|
135
136
|
return response
|
|
136
137
|
else:
|
|
@@ -167,6 +168,7 @@ class CacheAdapter(HTTPAdapter):
|
|
|
167
168
|
cache_options=cache_options,
|
|
168
169
|
ignore_specification=ignore_specification,
|
|
169
170
|
)
|
|
171
|
+
self.storage = self._cache_proxy.storage
|
|
170
172
|
|
|
171
173
|
def send(
|
|
172
174
|
self,
|
|
@@ -191,3 +193,6 @@ class CacheAdapter(HTTPAdapter):
|
|
|
191
193
|
requests_request = internal_to_requests(request)
|
|
192
194
|
response = super().send(requests_request, stream=True)
|
|
193
195
|
return requests_to_internal(response)
|
|
196
|
+
|
|
197
|
+
def close(self) -> Any:
|
|
198
|
+
self.storage.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Persistent cache implementation for httpx and httpcore
|
|
5
5
|
Project-URL: Homepage, https://hishel.com
|
|
6
6
|
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
@@ -192,19 +192,31 @@ You have complete control over them; you can change storage or even write your o
|
|
|
192
192
|
You can support the project by simply leaving a GitHub star ⭐ or by [contributing](https://hishel.com/contributing/).
|
|
193
193
|
Help us grow and continue developing good software for you ❤️
|
|
194
194
|
|
|
195
|
+
## [0.1.5] - 2025-10-18
|
|
196
|
+
|
|
197
|
+
### 🚀 Features
|
|
198
|
+
|
|
199
|
+
- *(perf)* Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
200
|
+
- Better cache-control parsing
|
|
201
|
+
- Add close method to storages API (#384)
|
|
202
|
+
- *(perf)* Increase requests buffer size to 128KB, disable charset detection
|
|
203
|
+
|
|
204
|
+
### 🐛 Bug Fixes
|
|
205
|
+
|
|
206
|
+
- *(docs)* Fix some line breaks
|
|
207
|
+
|
|
208
|
+
### ⚙️ Miscellaneous Tasks
|
|
209
|
+
|
|
210
|
+
- Remove some redundant files from repo
|
|
195
211
|
## [0.1.4] - 2025-10-14
|
|
196
212
|
|
|
197
213
|
### 🚀 Features
|
|
198
214
|
|
|
199
|
-
- Add support for requests library
|
|
200
|
-
- Add support for Python 3.14
|
|
201
215
|
- Add support for a sans-IO API (#366)
|
|
202
216
|
- Allow already consumed streams with `CacheTransport` (#377)
|
|
203
217
|
- Add sqlite storage for beta storages
|
|
204
218
|
- Get rid of some locks from sqlite storage
|
|
205
219
|
- Better async implemetation for sqlite storage
|
|
206
|
-
- Added `Metadata` to public API. (#363)
|
|
207
|
-
- Fix race condition in FileStorage initialization. (#353)
|
|
208
220
|
|
|
209
221
|
### 🐛 Bug Fixes
|
|
210
222
|
|
|
@@ -218,187 +230,29 @@ Help us grow and continue developing good software for you ❤️
|
|
|
218
230
|
- *(internal)* Temporary remove python3.14 from CI
|
|
219
231
|
- *(tests)* Add sqlite tests for new storage
|
|
220
232
|
- *(tests)* Move some tests to beta
|
|
221
|
-
## 0.1.3
|
|
222
|
-
|
|
223
|
-
- Remove `types-redis` from dev dependencies (#336)
|
|
224
|
-
- Bump redis to 6.0.0 and address async `.close()` deprecation warning (#336)
|
|
225
|
-
- Avoid race condition when unlinking files in `FileStorage`. (#334)
|
|
226
|
-
- Allow prodiving a `path_prefix` in `S3Storage` and `AsyncS3Storage`. (#342)
|
|
227
|
-
|
|
228
|
-
## 0.1.2 (5th April, 2025)
|
|
229
|
-
|
|
230
|
-
- Add check for fips compliant python. (#325)
|
|
231
|
-
- Fix compatibility with httpx. (#291)
|
|
232
|
-
- Use `SyncByteStream` instead of `ByteStream`. (#298)
|
|
233
|
-
- Don't raise exceptions if date-containing headers are invalid. (#318)
|
|
234
|
-
- Fix for S3 Storage missing metadata in API request. (#320)
|
|
235
|
-
|
|
236
|
-
## 0.1.1 (2nd Nov, 2024)
|
|
237
|
-
|
|
238
|
-
- Fix typing extensions not found. (#290)
|
|
239
|
-
|
|
240
|
-
## 0.1.0 (2nd Nov, 2024)
|
|
241
|
-
|
|
242
|
-
- Add support for Python 3.12 / drop Python 3.8. (#286)
|
|
243
|
-
- Specify usedforsecurity=False in blake2b. (#285)
|
|
244
|
-
|
|
245
|
-
## 0.0.33 (4th Oct, 2024)
|
|
246
|
-
|
|
247
|
-
- Added a [Logging](https://hishel.com/advanced/logging/) section to the documentation.
|
|
248
|
-
|
|
249
|
-
## 0.0.32 (27th Sep, 2024)
|
|
250
|
-
|
|
251
|
-
- Don't raise an exception if the `Date` header is not present. (#273)
|
|
252
|
-
|
|
253
|
-
## 0.0.31 (22nd Sep, 2024)
|
|
254
|
-
|
|
255
|
-
- Ignore file not found error when cleaning up a file storage. (#264)
|
|
256
|
-
- Fix `AssertionError` on `client.close()` when use SQLiteStorage. (#269)
|
|
257
|
-
- Fix ignored flags when use `force_cache`. (#271)
|
|
258
|
-
|
|
259
|
-
## 0.0.30 (12th July, 2024)
|
|
260
|
-
|
|
261
|
-
- Fix cache update on revalidation response with content (rfc9111 section 4.3.3) (#239)
|
|
262
|
-
- Fix request extensions that were not passed into revalidation request for transport-based implementation (but were
|
|
263
|
-
passed for the pool-based impl) (#247).
|
|
264
|
-
- Add `cache_private` property to the controller to support acting as shared cache. (#224)
|
|
265
|
-
- Improve efficiency of scanning cached responses in `FileStorage` by reducing number of syscalls. (#252)
|
|
266
|
-
- Add `remove` support for storages (#241)
|
|
267
|
-
|
|
268
|
-
## 0.0.29 (23th June, 2024)
|
|
269
|
-
|
|
270
|
-
- Documentation hotfix. (#244)
|
|
271
|
-
|
|
272
|
-
## 0.0.28 (23th June, 2024)
|
|
273
|
-
|
|
274
|
-
- Add `revalidated` response extension. (#242)
|
|
275
|
-
|
|
276
|
-
## 0.0.27 (31th May, 2024)
|
|
277
|
-
|
|
278
|
-
- Fix `RedisStorage` when using without ttl. (#231)
|
|
279
|
-
|
|
280
|
-
## 0.0.26 (12th April, 2024)
|
|
281
|
-
|
|
282
|
-
- Expose `AsyncBaseStorage` and `BaseStorage`. (#220)
|
|
283
|
-
- Prevent cache hits from resetting the ttl. (#215)
|
|
284
|
-
|
|
285
|
-
## 0.0.25 (26th March, 2024)
|
|
286
|
-
|
|
287
|
-
- Add `force_cache` property to the controller, allowing RFC9111 rules to be completely disabled. (#204)
|
|
288
|
-
- Add `.gitignore` to cache directory created by `FIleStorage`. (#197)
|
|
289
|
-
- Remove `stale_*` headers from the `CacheControl` class. (#199)
|
|
290
|
-
|
|
291
|
-
## 0.0.24 (14th February, 2024)
|
|
292
|
-
|
|
293
|
-
- Fix `botocore is not installed` exception when using any kind of storage. (#186)
|
|
233
|
+
## [0.1.3] - 2025-07-06
|
|
294
234
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
- Make `S3Storage` to check staleness of all cache files with set interval. (#182)
|
|
298
|
-
- Fix an issue where an empty file in `FileCache` could cause a parsing error. (#181)
|
|
299
|
-
- Support caching for `POST` and other HTTP methods. (#183)
|
|
300
|
-
|
|
301
|
-
## 0.0.22 (31th January, 2024)
|
|
302
|
-
|
|
303
|
-
- Make `FileStorage` to check staleness of all cache files with set interval. (#169)
|
|
304
|
-
- Support AWS S3 storages. (#164)
|
|
305
|
-
- Move `typing_extensions` from requirements.txt to pyproject.toml. (#161)
|
|
306
|
-
|
|
307
|
-
## 0.0.21 (29th December, 2023)
|
|
308
|
-
|
|
309
|
-
- Fix inner transport and connection pool instances closing. (#147)
|
|
310
|
-
- Improved error message when the storage type is incorrect. (#138)
|
|
311
|
-
|
|
312
|
-
## 0.0.20 (12th December, 2023)
|
|
313
|
-
|
|
314
|
-
- Add in-memory storage. (#133)
|
|
315
|
-
- Allow customization of cache key generation. (#130)
|
|
316
|
-
|
|
317
|
-
## 0.0.19 (30th November, 2023)
|
|
318
|
-
|
|
319
|
-
- Add `force_cache` extension to enforce the request to be cached, ignoring the HTTP headers. (#117)
|
|
320
|
-
- Fix issue where sqlite storage cache get deleted immediately. (#119)
|
|
321
|
-
- Support float numbers for storage ttl. (#107)
|
|
322
|
-
|
|
323
|
-
## 0.0.18 (23rd November, 2023)
|
|
324
|
-
|
|
325
|
-
- Fix issue where freshness cannot be calculated to re-send request. (#104)
|
|
326
|
-
- Add `cache_disabled` extension to temporarily disable the cache (#109)
|
|
327
|
-
- Update `datetime.datetime.utcnow()` to `datetime.datetime.now(datetime.timezone.utc)` since `datetime.datetime.utcnow()` has been deprecated. (#111)
|
|
328
|
-
|
|
329
|
-
## 0.0.17 (6th November, 2023)
|
|
330
|
-
|
|
331
|
-
- Fix `Last-Modified` validation.
|
|
332
|
-
|
|
333
|
-
## 0.0.16 (25th October, 2023)
|
|
334
|
-
|
|
335
|
-
- Add `install_cache` function. (#95)
|
|
336
|
-
- Add sqlite support. (#92)
|
|
337
|
-
- Move `ttl` argument to `BaseStorage` class. (#94)
|
|
338
|
-
|
|
339
|
-
## 0.0.14 (23rd October, 2023)
|
|
340
|
-
|
|
341
|
-
- Replace `AsyncResponseStream` with `AsyncCacheStream`. (#86)
|
|
342
|
-
- Add `must-understand` response directive support. (#90)
|
|
343
|
-
|
|
344
|
-
## 0.0.13 (5th October, 2023)
|
|
345
|
-
|
|
346
|
-
- Add support for Python 3.12. (#71)
|
|
347
|
-
- Fix connections releasing from the connection pool. (#83)
|
|
348
|
-
|
|
349
|
-
## 0.0.12 (8th September, 2023)
|
|
350
|
-
|
|
351
|
-
- Add metadata into the response extensions. (#56)
|
|
352
|
-
|
|
353
|
-
## 0.0.11 (15th August, 2023)
|
|
354
|
-
|
|
355
|
-
- Add support for request `cache-control` directives. (#42)
|
|
356
|
-
- Drop httpcore dependency. (#40)
|
|
357
|
-
- Support HTTP methods only if they are defined as cacheable. (#37)
|
|
358
|
-
|
|
359
|
-
## 0.0.10 (7th August, 2023)
|
|
360
|
-
|
|
361
|
-
- Add Response metadata. (#33)
|
|
362
|
-
- Add API Reference documentation. (#30)
|
|
363
|
-
- Use stale responses only if the client is disconnected. (#28)
|
|
364
|
-
|
|
365
|
-
## 0.0.9 (1st August, 2023)
|
|
366
|
-
|
|
367
|
-
- Expose Controller API. (#23)
|
|
368
|
-
|
|
369
|
-
## 0.0.8 (31st July, 2023)
|
|
370
|
-
|
|
371
|
-
- Skip redis tests if the server was not found. (#16)
|
|
372
|
-
- Decrease sleep time for the storage ttl tests. (#18)
|
|
373
|
-
- Fail coverage under 100. (#19)
|
|
374
|
-
|
|
375
|
-
## 0.0.7 (30th July, 2023)
|
|
376
|
-
|
|
377
|
-
- Add support for `Heuristic Freshness`. (#11)
|
|
378
|
-
- Change `Controller.cache_heuristically` to `Controller.allow_heuristics`. (#12)
|
|
379
|
-
- Handle import errors. (#13)
|
|
380
|
-
|
|
381
|
-
## 0.0.6 (29th July, 2023)
|
|
235
|
+
### 🚀 Features
|
|
382
236
|
|
|
383
|
-
-
|
|
384
|
-
- Dump original requests with the responses. (#7)
|
|
237
|
+
- Support providing a path prefix to S3 storage (#342)
|
|
385
238
|
|
|
386
|
-
|
|
239
|
+
### 📚 Documentation
|
|
387
240
|
|
|
388
|
-
-
|
|
241
|
+
- Update link to httpx transports page (#337)
|
|
242
|
+
## [0.1.2] - 2025-04-04
|
|
389
243
|
|
|
390
|
-
|
|
244
|
+
### 🐛 Bug Fixes
|
|
391
245
|
|
|
392
|
-
-
|
|
246
|
+
- Requirements.txt to reduce vulnerabilities (#263)
|
|
247
|
+
## [0.0.30] - 2024-07-12
|
|
393
248
|
|
|
394
|
-
|
|
249
|
+
### 🐛 Bug Fixes
|
|
395
250
|
|
|
396
|
-
-
|
|
397
|
-
-
|
|
251
|
+
- Requirements.txt to reduce vulnerabilities (#245)
|
|
252
|
+
- Requirements.txt to reduce vulnerabilities (#255)
|
|
253
|
+
## [0.0.27] - 2024-05-31
|
|
398
254
|
|
|
399
|
-
|
|
255
|
+
### 🐛 Bug Fixes
|
|
400
256
|
|
|
401
|
-
-
|
|
402
|
-
|
|
403
|
-
- Add black as a new linter.
|
|
404
|
-
- Add an expire time for cached responses.
|
|
257
|
+
- *(redis)* Do not update metadata with negative ttl (#231)
|
|
258
|
+
## [0.0.1] - 2023-07-22
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
hishel/__init__.py,sha256=
|
|
1
|
+
hishel/__init__.py,sha256=4HWxHaEihJ5bsey4XEZA28meUO2Iw3mJrOFhQtWu4FY,1221
|
|
2
2
|
hishel/_controller.py,sha256=nQMEF-upuBf6-r0wyjd2CGYgBhB-JbEw6IgGxtvADJ4,24629
|
|
3
3
|
hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
|
|
4
4
|
hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
|
|
@@ -25,17 +25,17 @@ hishel/_sync/_transports.py,sha256=ivPztcm84PxObIXMZ3hLD_o5HMo8XXGhRlSg0Lm_sNo,1
|
|
|
25
25
|
hishel/beta/__init__.py,sha256=VzlIfaGQaYfpesqOujNcG0HMCaIF9CzEyhIY04A4c8g,1477
|
|
26
26
|
hishel/beta/_async_cache.py,sha256=vhdTyRIjMpKzc4qKPZGVoFQa9-60aCXLNGltOE1mDg8,6807
|
|
27
27
|
hishel/beta/_sync_cache.py,sha256=a0fcNY5qApPBXQ_kCUBW2Ccwwj0bEkNTVmv-6W3cqPI,6553
|
|
28
|
-
hishel/beta/httpx.py,sha256=
|
|
29
|
-
hishel/beta/requests.py,sha256=
|
|
28
|
+
hishel/beta/httpx.py,sha256=zOnUUafa1zyi2hmcWeP7uaifcj7OIHlXvvuh19gKfL0,11027
|
|
29
|
+
hishel/beta/requests.py,sha256=20i5H7m67BH90iT7RzEbeyiNsbS8lx7eU6kAdWInfQY,6454
|
|
30
30
|
hishel/beta/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
hishel/beta/_core/_headers.py,sha256=
|
|
31
|
+
hishel/beta/_core/_headers.py,sha256=ii4x2L6GoQFpqpgg28OtFh7p2DoM9mhE4q6CjW6xUWc,17473
|
|
32
32
|
hishel/beta/_core/_spec.py,sha256=5z5kZ7Q9BO8yZynQIiDiIly77iTMzUljekhmrF1Otyc,99529
|
|
33
33
|
hishel/beta/_core/models.py,sha256=rbU72bCT_lpwASIs5Qzmo3vcjiDm-lu0C99-CgYWl7k,5524
|
|
34
34
|
hishel/beta/_core/_async/_storages/_sqlite.py,sha256=_PZ4t8KXk-YErTCze80Uh8XnG8WZ2323VrRd0NwoKHY,14719
|
|
35
|
-
hishel/beta/_core/_base/_storages/_base.py,sha256=
|
|
35
|
+
hishel/beta/_core/_base/_storages/_base.py,sha256=mHOQ1p1BTuhV5vgeIdn61OAbVjiD8vu4S4CCal6sS5A,8521
|
|
36
36
|
hishel/beta/_core/_base/_storages/_packing.py,sha256=8s0UxYTLdwzs4A_1E896EVj5VPT8Aqv9UabWbeMD8lw,4898
|
|
37
37
|
hishel/beta/_core/_sync/_storages/_sqlite.py,sha256=1sBU_geVbUSC8e46gao5p_k8MogYEWnZodse55FQQ6Q,14153
|
|
38
|
-
hishel-0.1.
|
|
39
|
-
hishel-0.1.
|
|
40
|
-
hishel-0.1.
|
|
41
|
-
hishel-0.1.
|
|
38
|
+
hishel-0.1.5.dist-info/METADATA,sha256=ZB-4dyu3L8icekMrIwMEhZrjlSfryzlSBITW0-b7BCQ,9310
|
|
39
|
+
hishel-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
40
|
+
hishel-0.1.5.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
41
|
+
hishel-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|