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 CHANGED
@@ -53,5 +53,5 @@ def install_cache() -> None: # pragma: no cover
53
53
  httpx.Client = CacheClient # type: ignore
54
54
 
55
55
 
56
- __version__ = "0.1.4"
56
+ __version__ = "0.1.5"
57
57
 
@@ -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.
@@ -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, Dict, Iterator, List, Literal, Mapping, MutableMapping, Optional, Union, cast
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
- from hishel._exceptions import ParseError, ValidationError
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
- HTAB = "\t"
13
- SP = " "
14
- obs_text = "".join(chr(i) for i in range(0x80, 0xFF + 1)) # 0x80-0xFF
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
- 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(",")
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
- for directive in cache_control_splited:
73
- key: str = ""
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
- if not directive:
78
- raise ParseError("The directive should not be left blank.")
261
+ def __getitem__(self, key: str) -> str:
262
+ return ", ".join(self._headers[key.lower()])
79
263
 
80
- directive = strip_ows_around(directive)
264
+ def __setitem__(self, key: str, value: str) -> None:
265
+ self._headers.setdefault(key.lower(), []).append(value)
81
266
 
82
- if not directive:
83
- raise ParseError("The directive should not contain only whitespaces.")
267
+ def __delitem__(self, key: str) -> None:
268
+ del self._headers[key.lower()]
84
269
 
85
- for i, key_char in enumerate(directive):
86
- if key_char == "=":
87
- value = directive[i + 1 :]
270
+ def __iter__(self) -> Iterator[str]:
271
+ return iter(self._headers)
88
272
 
89
- if not value:
90
- raise ParseError("The directive value cannot be left blank.")
273
+ def __len__(self) -> int:
274
+ return len(self._headers)
91
275
 
92
- if value[0] == '"':
93
- dquote = True
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
- 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
279
+ def __str__(self) -> str:
280
+ return str(self._headers)
108
281
 
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)
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
- 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
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
- @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
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
- for key in TIME_FIELDS:
256
- key = key.replace("-", "_")
257
- value = getattr(self, key)
258
- if value:
259
- fields += f"{key}={value}, "
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
- for key in BOOLEAN_FIELDS:
262
- key = key.replace("-", "_")
263
- value = getattr(self, key)
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
- return f"<{type(self).__name__} {fields}>"
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
- 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()}
561
+ elif token == "private":
562
+ cc.private = True
275
563
 
276
- def get_list(self, key: str) -> Optional[List[str]]:
277
- return self._headers.get(key.lower(), None)
564
+ elif token == "no-store":
565
+ cc.no_store = True
278
566
 
279
- def __getitem__(self, key: str) -> str:
280
- return ", ".join(self._headers[key.lower()])
567
+ elif token == "no-transform":
568
+ cc.no_transform = True
281
569
 
282
- def __setitem__(self, key: str, value: str) -> None:
283
- self._headers.setdefault(key.lower(), []).append(value)
570
+ elif token == "only-if-cached":
571
+ cc.only_if_cached = True
284
572
 
285
- def __delitem__(self, key: str) -> None:
286
- del self._headers[key.lower()]
573
+ elif token == "must-revalidate":
574
+ cc.must_revalidate = True
287
575
 
288
- def __iter__(self) -> Iterator[str]:
289
- return iter(self._headers)
576
+ elif token == "must-understand":
577
+ cc.must_understand = True
290
578
 
291
- def __len__(self) -> int:
292
- return len(self._headers)
579
+ elif token == "public":
580
+ cc.public = True
293
581
 
294
- def __repr__(self) -> str:
295
- return repr(self._headers)
582
+ elif token == "proxy-revalidate":
583
+ cc.proxy_revalidate = True
296
584
 
297
- def __str__(self) -> str:
298
- return str(self._headers)
585
+ elif token == "immutable":
586
+ cc.immutable = True
299
587
 
300
- def __eq__(self, other_headers: Any) -> bool:
301
- return isinstance(other_headers, Headers) and self._headers == other_headers._headers # type: ignore
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 = value.iter_raw() if isinstance(value.stream, Iterable) else value.aiter_raw()
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=8192)
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=True,
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.4
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 (1st July, 2025)
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
- ## 0.0.23 (14th February, 2024)
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
- - Fix `Vary` header validation. (#8)
384
- - Dump original requests with the responses. (#7)
237
+ - Support providing a path prefix to S3 storage (#342)
385
238
 
386
- ## 0.0.5 (29th July, 2023)
239
+ ### 📚 Documentation
387
240
 
388
- - Fix httpx response streaming.
241
+ - Update link to httpx transports page (#337)
242
+ ## [0.1.2] - 2025-04-04
389
243
 
390
- ## 0.0.4 (29th July, 2023)
244
+ ### 🐛 Bug Fixes
391
245
 
392
- - Change `YamlSerializer` name to `YAMLSerializer`.
246
+ - Requirements.txt to reduce vulnerabilities (#263)
247
+ ## [0.0.30] - 2024-07-12
393
248
 
394
- ## 0.0.3 (28th July, 2023)
249
+ ### 🐛 Bug Fixes
395
250
 
396
- - Add `from_cache` response extension.
397
- - Add `typing_extensions` into the requirements.
251
+ - Requirements.txt to reduce vulnerabilities (#245)
252
+ - Requirements.txt to reduce vulnerabilities (#255)
253
+ ## [0.0.27] - 2024-05-31
398
254
 
399
- ## 0.0.2 (25th July, 2023)
255
+ ### 🐛 Bug Fixes
400
256
 
401
- - Add [redis](https://redis.io/) support.
402
- - Make backends thread and task safe.
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=uFPxYPm-vFegjbeEvYmTqIE7ALymqYf837Z1mMZNVdM,1221
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=KkK3_ZY3FyjWHDJFNLucXoEBph-pMkI2wuHdagXn_lo,10729
29
- hishel/beta/requests.py,sha256=Q6hPs8ZoFM6802ZCV9_ERG5BVRacMOMwpw5IanmPrYQ,6393
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=vkdHZFFg734TO-kDpAZ271I7dXpwFUtAOuX3vS2LatU,9809
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=XvhNNQhMNEc2Jt2p0k8SNW5_yLoDfmYMFfPncmKbmX0,8267
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.4.dist-info/METADATA,sha256=GBT-r6kU3dotaAozdL7xlqDQ5jPt1D88kBcXKceGV1U,14344
39
- hishel-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
40
- hishel-0.1.4.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
41
- hishel-0.1.4.dist-info/RECORD,,
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