hishel 0.1.0__py3-none-any.whl → 0.1.2__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
@@ -14,4 +14,4 @@ def install_cache() -> None: # pragma: no cover
14
14
  httpx.Client = CacheClient # type: ignore
15
15
 
16
16
 
17
- __version__ = "0.1.0"
17
+ __version__ = "0.1.2"
hishel/_async/_pool.py CHANGED
@@ -47,7 +47,7 @@ class AsyncCacheConnectionPool(AsyncRequestInterface):
47
47
 
48
48
  self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer())
49
49
 
50
- if not isinstance(self._storage, AsyncBaseStorage):
50
+ if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover
51
51
  raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`")
52
52
 
53
53
  self._controller = controller if controller is not None else Controller()
@@ -23,7 +23,9 @@ except ImportError: # pragma: no cover
23
23
  anysqlite = None # type: ignore
24
24
 
25
25
  from httpcore import Request, Response
26
- from typing_extensions import TypeAlias
26
+
27
+ if t.TYPE_CHECKING: # pragma: no cover
28
+ from typing_extensions import TypeAlias
27
29
 
28
30
  from hishel._serializers import BaseSerializer, clone_model
29
31
 
@@ -64,7 +64,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
64
64
 
65
65
  self._storage = storage if storage is not None else AsyncFileStorage(serializer=JSONSerializer())
66
66
 
67
- if not isinstance(self._storage, AsyncBaseStorage):
67
+ if not isinstance(self._storage, AsyncBaseStorage): # pragma: no cover
68
68
  raise TypeError(f"Expected subclass of `AsyncBaseStorage` but got `{storage.__class__.__name__}`")
69
69
 
70
70
  self._controller = controller if controller is not None else Controller()
@@ -152,7 +152,7 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
152
152
  # Controller has determined that the response needs to be re-validated.
153
153
  assert isinstance(res.stream, tp.AsyncIterable)
154
154
  revalidation_request = Request(
155
- method=res.method,
155
+ method=res.method.decode(),
156
156
  url=normalized_url(res.url),
157
157
  headers=res.headers,
158
158
  stream=AsyncCacheStream(res.stream),
hishel/_controller.py CHANGED
@@ -60,8 +60,12 @@ def get_freshness_lifetime(response: Response) -> tp.Optional[int]:
60
60
  if header_presents(response.headers, b"expires"):
61
61
  expires = extract_header_values_decoded(response.headers, b"expires", single=True)[0]
62
62
  expires_timestamp = parse_date(expires)
63
+ if expires_timestamp is None:
64
+ return None
63
65
  date = extract_header_values_decoded(response.headers, b"date", single=True)[0]
64
66
  date_timestamp = parse_date(date)
67
+ if date_timestamp is None:
68
+ return None
65
69
 
66
70
  return expires_timestamp - date_timestamp
67
71
  return None
@@ -72,11 +76,12 @@ def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
72
76
 
73
77
  if last_modified:
74
78
  last_modified_timestamp = parse_date(last_modified[0])
75
- now = clock.now()
79
+ if last_modified_timestamp is not None:
80
+ now = clock.now()
76
81
 
77
- ONE_WEEK = 604_800
82
+ ONE_WEEK = 604_800
78
83
 
79
- return min(ONE_WEEK, int((now - last_modified_timestamp) * 0.1))
84
+ return min(ONE_WEEK, int((now - last_modified_timestamp) * 0.1))
80
85
 
81
86
  ONE_DAY = 86_400
82
87
  return ONE_DAY
@@ -89,6 +94,8 @@ def get_age(response: Response, clock: "BaseClock") -> int:
89
94
  return float("inf") # type: ignore
90
95
 
91
96
  date = parse_date(extract_header_values_decoded(response.headers, b"date")[0])
97
+ if date is None:
98
+ return float("inf") # type: ignore
92
99
 
93
100
  now = clock.now()
94
101
 
hishel/_headers.py CHANGED
@@ -94,13 +94,13 @@ def parse_cache_control(cache_control_values: List[str]) -> "CacheControl":
94
94
  for value_char in value:
95
95
  if value_char not in tchar:
96
96
  raise ParseError(
97
- f"The character '{value_char!r}' " "is not permitted for the unquoted values."
97
+ f"The character '{value_char!r}' is not permitted for the unquoted values."
98
98
  )
99
99
  else:
100
100
  for value_char in value[1:-1]:
101
101
  if value_char not in qdtext:
102
102
  raise ParseError(
103
- f"The character '{value_char!r}' " "is not permitted for the quoted values."
103
+ f"The character '{value_char!r}' is not permitted for the quoted values."
104
104
  )
105
105
  break
106
106
 
hishel/_s3.py CHANGED
@@ -75,7 +75,16 @@ class S3Manager:
75
75
  if not obj["Key"].startswith("hishel-"): # pragma: no cover
76
76
  continue
77
77
 
78
- if get_timestamp_in_ms() - float(obj["Metadata"]["created_at"]) > ttl:
78
+ try:
79
+ metadata_obj = self._client.head_object(Bucket=self._bucket_name, Key=obj["Key"]).get("Metadata", {})
80
+ except ClientError as e:
81
+ if e.response["Error"]["Code"] == "404":
82
+ continue
83
+
84
+ if not metadata_obj or "created_at" not in metadata_obj:
85
+ continue
86
+
87
+ if get_timestamp_in_ms() - float(metadata_obj["created_at"]) > ttl:
79
88
  self._client.delete_object(Bucket=self._bucket_name, Key=obj["Key"])
80
89
 
81
90
  def remove_entry(self, key: str) -> None:
hishel/_sync/_pool.py CHANGED
@@ -47,7 +47,7 @@ class CacheConnectionPool(RequestInterface):
47
47
 
48
48
  self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer())
49
49
 
50
- if not isinstance(self._storage, BaseStorage):
50
+ if not isinstance(self._storage, BaseStorage): # pragma: no cover
51
51
  raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`")
52
52
 
53
53
  self._controller = controller if controller is not None else Controller()
hishel/_sync/_storages.py CHANGED
@@ -23,7 +23,9 @@ except ImportError: # pragma: no cover
23
23
  sqlite3 = None # type: ignore
24
24
 
25
25
  from httpcore import Request, Response
26
- from typing_extensions import TypeAlias
26
+
27
+ if t.TYPE_CHECKING: # pragma: no cover
28
+ from typing_extensions import TypeAlias
27
29
 
28
30
  from hishel._serializers import BaseSerializer, clone_model
29
31
 
@@ -5,7 +5,7 @@ import typing as tp
5
5
 
6
6
  import httpcore
7
7
  import httpx
8
- from httpx import ByteStream, Request, Response
8
+ from httpx import SyncByteStream, Request, Response
9
9
  from httpx._exceptions import ConnectError
10
10
 
11
11
  from hishel._utils import extract_header_values_decoded, normalized_url
@@ -29,7 +29,7 @@ def generate_504() -> Response:
29
29
  return Response(status_code=504)
30
30
 
31
31
 
32
- class CacheStream(ByteStream):
32
+ class CacheStream(SyncByteStream):
33
33
  def __init__(self, httpcore_stream: tp.Iterable[bytes]):
34
34
  self._httpcore_stream = httpcore_stream
35
35
 
@@ -64,7 +64,7 @@ class CacheTransport(httpx.BaseTransport):
64
64
 
65
65
  self._storage = storage if storage is not None else FileStorage(serializer=JSONSerializer())
66
66
 
67
- if not isinstance(self._storage, BaseStorage):
67
+ if not isinstance(self._storage, BaseStorage): # pragma: no cover
68
68
  raise TypeError(f"Expected subclass of `BaseStorage` but got `{storage.__class__.__name__}`")
69
69
 
70
70
  self._controller = controller if controller is not None else Controller()
@@ -152,7 +152,7 @@ class CacheTransport(httpx.BaseTransport):
152
152
  # Controller has determined that the response needs to be re-validated.
153
153
  assert isinstance(res.stream, tp.Iterable)
154
154
  revalidation_request = Request(
155
- method=res.method,
155
+ method=res.method.decode(),
156
156
  url=normalized_url(res.url),
157
157
  headers=res.headers,
158
158
  stream=CacheStream(res.stream),
hishel/_utils.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import calendar
2
+ import hashlib
2
3
  import time
3
4
  import typing as tp
4
5
  from email.utils import parsedate_tz
5
- from hashlib import blake2b
6
6
 
7
7
  import anyio
8
8
  import httpcore
@@ -30,7 +30,7 @@ def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
30
30
 
31
31
  if isinstance(url, httpcore.URL):
32
32
  port = f":{url.port}" if url.port is not None else ""
33
- return f'{url.scheme.decode("ascii")}://{url.host.decode("ascii")}{port}{url.target.decode("ascii")}'
33
+ return f"{url.scheme.decode('ascii')}://{url.host.decode('ascii')}{port}{url.target.decode('ascii')}"
34
34
  assert False, "Invalid type for `normalized_url`" # pragma: no cover
35
35
 
36
36
 
@@ -49,10 +49,26 @@ def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
49
49
 
50
50
  key_parts = [request.method, encoded_url, body]
51
51
 
52
- key = blake2b(digest_size=16, usedforsecurity=False)
53
- for part in key_parts:
54
- key.update(part)
55
- return key.hexdigest()
52
+ # FIPs mode disables blake2 algorithm, use sha256 instead when not found.
53
+ blake2b_hasher = None
54
+ sha256_hasher = hashlib.sha256(usedforsecurity=False)
55
+ try:
56
+ blake2b_hasher = hashlib.blake2b(digest_size=16, usedforsecurity=False)
57
+ except (ValueError, TypeError, AttributeError):
58
+ pass
59
+
60
+ hexdigest: str
61
+ if blake2b_hasher:
62
+ for part in key_parts:
63
+ blake2b_hasher.update(part)
64
+
65
+ hexdigest = blake2b_hasher.hexdigest()
66
+ else:
67
+ for part in key_parts:
68
+ sha256_hasher.update(part)
69
+
70
+ hexdigest = sha256_hasher.hexdigest()
71
+ return hexdigest
56
72
 
57
73
 
58
74
  def extract_header_values(
@@ -82,9 +98,11 @@ def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes)
82
98
  return bool(extract_header_values(headers, header_key, single=True))
83
99
 
84
100
 
85
- def parse_date(date: str) -> int:
101
+ def parse_date(date: str) -> tp.Optional[int]:
86
102
  expires = parsedate_tz(date)
87
- timestamp = calendar.timegm(expires[:6]) # type: ignore
103
+ if expires is None:
104
+ return None
105
+ timestamp = calendar.timegm(expires[:6])
88
106
  return timestamp
89
107
 
90
108
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: hishel
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
@@ -23,9 +23,9 @@ Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Topic :: Internet :: WWW/HTTP
25
25
  Requires-Python: >=3.9
26
- Requires-Dist: httpx>=0.22.0
26
+ Requires-Dist: httpx>=0.28.0
27
27
  Provides-Extra: redis
28
- Requires-Dist: redis==5.0.1; extra == 'redis'
28
+ Requires-Dist: redis==5.0.4; extra == 'redis'
29
29
  Provides-Extra: s3
30
30
  Requires-Dist: boto3<=1.15.3,>=1.15.0; (python_version < '3.12') and extra == 's3'
31
31
  Requires-Dist: boto3>=1.15.3; (python_version >= '3.12') and extra == 's3'
@@ -181,6 +181,18 @@ Help us grow and continue developing good software for you ❤️
181
181
 
182
182
  # Changelog
183
183
 
184
+ ## 0.1.2 (5th April, 2025)
185
+
186
+ - Add check for fips compliant python. (#325)
187
+ - Fix compatibility with httpx. (#291)
188
+ - Use `SyncByteStream` instead of `ByteStream`. (#298)
189
+ - Don't raise exceptions if date-containing headers are invalid. (#318)
190
+ - Fix for S3 Storage missing metadata in API request. (#320)
191
+
192
+ ## 0.1.1 (2nd Nov, 2024)
193
+
194
+ - Fix typing extensions not found. (#290)
195
+
184
196
  ## 0.1.0 (2nd Nov, 2024)
185
197
 
186
198
  - Add support for Python 3.12 / drop Python 3.8. (#286)
@@ -0,0 +1,27 @@
1
+ hishel/__init__.py,sha256=R91Ika4Y8yXeuWLIrpbrXHUPASzYa-ZlaC2r7cX7L74,368
2
+ hishel/_controller.py,sha256=be1_eL34Gue6a1px_eLFWWxViPQbYENMvtZmv8gFRhA,24636
3
+ hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
4
+ hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
5
+ hishel/_headers.py,sha256=BPvas0LQgwbz-HZhFykZiHEIeNgnY-E33U__oskYzJw,7323
6
+ hishel/_lfu_cache.py,sha256=GBxToQI8u_a9TzYnLlZMLhgZ8Lb83boPHzTvIgqV6pA,2707
7
+ hishel/_s3.py,sha256=HDS3-3HM8xmIb-1_p2VRKJk9EwfzoxgTT0F1Kryl8sU,4087
8
+ hishel/_serializers.py,sha256=gepVb8JC4aBkGw9kLcbAsyo-1XgK_lzTssLr_8av4SQ,11640
9
+ hishel/_synchronization.py,sha256=xOmU9_8KAWTAv3r8EpqPISrtSF3slyh1J0Sc7ZQO1rg,897
10
+ hishel/_utils.py,sha256=7HhJlomBCqbhBv01ZuK5WfG8dCtGONLFFF7ujIvBBWQ,3277
11
+ hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hishel/_async/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
13
+ hishel/_async/_client.py,sha256=AkVSSbNTTHmK0gX6PRYVQ-3aDbuCX2Im4VKbLkwLiBU,1101
14
+ hishel/_async/_mock.py,sha256=995v9p5xiw3svGSOJATkLMqwodlhZhcwmGygLHM2VFw,1515
15
+ hishel/_async/_pool.py,sha256=xCcSAl4bHI5eVsZyPap7iHDpxzzgfMqYE_txcjQJ1Hs,8195
16
+ hishel/_async/_storages.py,sha256=tJXwov37HOvwYwXyjFtf-uRn7y7wR8RZs49F_Urj570,28744
17
+ hishel/_async/_transports.py,sha256=BhtEj8SHxu0YKHWDDP5mfCabIWgrAM7lMLJ1-TwWigw,11203
18
+ hishel/_sync/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
19
+ hishel/_sync/_client.py,sha256=O-gwm9DsveKtSFUfqdbBB-3I1FmXr5rE-uQ7X5frwDA,1060
20
+ hishel/_sync/_mock.py,sha256=im88tZr-XhP9BpzvIt3uOjndAlNcJvFP7Puv3H-6lKU,1430
21
+ hishel/_sync/_pool.py,sha256=U2b9ZGYUltwTjI2q2KHZwmj4boIqUExJ_rUKWuLmYSs,7960
22
+ hishel/_sync/_storages.py,sha256=NhIIAoWdpFECcexz121rfYLFRCk3C1iBazVXwoDVoSU,27954
23
+ hishel/_sync/_transports.py,sha256=cQQgdJSy1zfmIa14ycADPek9Tobpa33nqBHA614_6kc,10875
24
+ hishel-0.1.2.dist-info/METADATA,sha256=aqgCs4WIdawt1s7MUa6kVR32jEWG9Zeu3yTGJUL9ah4,12583
25
+ hishel-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ hishel-0.1.2.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
27
+ hishel-0.1.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,27 +0,0 @@
1
- hishel/__init__.py,sha256=fX1GZHvMOXBaKhL2zJEGcO6t2WEwyub2J1hMajtQUtU,368
2
- hishel/_controller.py,sha256=F7hj1ePUvau2Wj5r6zzdYr8UXPq4osfmMxkmx_Ig2L8,24390
3
- hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
4
- hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
5
- hishel/_headers.py,sha256=TWuHi7sRoeS2xxdNGujKmqWtgncUqfhNGCgHKYpRU-I,7329
6
- hishel/_lfu_cache.py,sha256=GBxToQI8u_a9TzYnLlZMLhgZ8Lb83boPHzTvIgqV6pA,2707
7
- hishel/_s3.py,sha256=JqRlygITK5uAryviC15HZKQlKY7etUOPWcazTJeYKBI,3736
8
- hishel/_serializers.py,sha256=gepVb8JC4aBkGw9kLcbAsyo-1XgK_lzTssLr_8av4SQ,11640
9
- hishel/_synchronization.py,sha256=xOmU9_8KAWTAv3r8EpqPISrtSF3slyh1J0Sc7ZQO1rg,897
10
- hishel/_utils.py,sha256=wANSbtWrxZwi3eEkR03oLXZ4Y2WsOtBf90W4BLB_abM,2759
11
- hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- hishel/_async/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
13
- hishel/_async/_client.py,sha256=AkVSSbNTTHmK0gX6PRYVQ-3aDbuCX2Im4VKbLkwLiBU,1101
14
- hishel/_async/_mock.py,sha256=995v9p5xiw3svGSOJATkLMqwodlhZhcwmGygLHM2VFw,1515
15
- hishel/_async/_pool.py,sha256=li-921qyGzrV7SVUOUlMI0KE7IRsupSkE5iApzxmgqk,8175
16
- hishel/_async/_storages.py,sha256=SPWifKGSGAFP2wMFoxZf5weZFbuVgHFciyglY6Hb6fc,28699
17
- hishel/_async/_transports.py,sha256=moeH-eQLJHkMp83NnScgsQTSQntDCR1_4A1ByZ_fXjk,11174
18
- hishel/_sync/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
19
- hishel/_sync/_client.py,sha256=O-gwm9DsveKtSFUfqdbBB-3I1FmXr5rE-uQ7X5frwDA,1060
20
- hishel/_sync/_mock.py,sha256=im88tZr-XhP9BpzvIt3uOjndAlNcJvFP7Puv3H-6lKU,1430
21
- hishel/_sync/_pool.py,sha256=VcAknzyAL2i4-zcyE2fOTmTjfBZ2wkBVNYTvSw0OjVQ,7940
22
- hishel/_sync/_storages.py,sha256=RYzYXqnv0o2JO3RoEmlEUp0yOg_ungXfz4dLN7UTpIQ,27909
23
- hishel/_sync/_transports.py,sha256=G3_8SdPwlnrHZRvE1gqFLE4oZadVqNgg5mvxghDMih0,10838
24
- hishel-0.1.0.dist-info/METADATA,sha256=I_gBTsungH30fu2WCN18Ls-6r9zp0RKzRRVFHemfkCg,12212
25
- hishel-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
26
- hishel-0.1.0.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
27
- hishel-0.1.0.dist-info/RECORD,,