hishel 0.1.4__py3-none-any.whl → 1.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hishel/__init__.py +55 -53
- hishel/{beta/_async_cache.py → _async_cache.py} +3 -3
- hishel/{beta → _core}/__init__.py +6 -6
- hishel/{beta/_core → _core}/_async/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/_base/_storages/_base.py +13 -1
- hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
- hishel/_core/_headers.py +636 -0
- hishel/{beta/_core → _core}/_spec.py +89 -2
- hishel/{beta/_core → _core}/_sync/_storages/_sqlite.py +3 -3
- hishel/{beta/_core → _core}/models.py +1 -1
- hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
- hishel/{beta/httpx.py → httpx.py} +18 -7
- hishel/{beta/requests.py → requests.py} +15 -10
- hishel-1.0.0.dev0.dist-info/METADATA +321 -0
- hishel-1.0.0.dev0.dist-info/RECORD +19 -0
- hishel/_async/__init__.py +0 -5
- hishel/_async/_client.py +0 -30
- hishel/_async/_mock.py +0 -43
- hishel/_async/_pool.py +0 -201
- hishel/_async/_storages.py +0 -768
- hishel/_async/_transports.py +0 -282
- hishel/_controller.py +0 -581
- hishel/_exceptions.py +0 -10
- hishel/_files.py +0 -54
- hishel/_headers.py +0 -215
- hishel/_lfu_cache.py +0 -71
- hishel/_lmdb_types_.pyi +0 -53
- hishel/_s3.py +0 -122
- hishel/_serializers.py +0 -329
- hishel/_sync/__init__.py +0 -5
- hishel/_sync/_client.py +0 -30
- hishel/_sync/_mock.py +0 -43
- hishel/_sync/_pool.py +0 -201
- hishel/_sync/_storages.py +0 -768
- hishel/_sync/_transports.py +0 -282
- hishel/_synchronization.py +0 -37
- hishel/beta/_core/__init__.py +0 -0
- hishel/beta/_core/_headers.py +0 -301
- hishel-0.1.4.dist-info/METADATA +0 -404
- hishel-0.1.4.dist-info/RECORD +0 -41
- {hishel-0.1.4.dist-info → hishel-1.0.0.dev0.dist-info}/WHEEL +0 -0
- {hishel-0.1.4.dist-info → hishel-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,11 +15,11 @@ from typing import (
|
|
|
15
15
|
Union,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
from hishel._core._headers import Headers, Range, Vary, parse_cache_control
|
|
18
19
|
from hishel._utils import parse_date, partition
|
|
19
|
-
from hishel.beta._core._headers import Headers, Range, Vary, parse_cache_control
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from hishel
|
|
22
|
+
from hishel import CompletePair, Request, Response
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
TState = TypeVar("TState", bound="State")
|
|
@@ -41,9 +41,96 @@ logger = logging.getLogger("hishel.core.spec")
|
|
|
41
41
|
|
|
42
42
|
@dataclass
|
|
43
43
|
class CacheOptions:
|
|
44
|
+
"""
|
|
45
|
+
Configuration options for HTTP cache behavior.
|
|
46
|
+
|
|
47
|
+
These options control how the cache interprets and applies RFC 9111 caching rules.
|
|
48
|
+
All options have sensible defaults that follow the specification.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
----------
|
|
52
|
+
shared : bool
|
|
53
|
+
Determines whether the cache operates as a shared cache or private cache.
|
|
54
|
+
|
|
55
|
+
RFC 9111 Section 3.5: Authenticated Responses
|
|
56
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-3.5
|
|
57
|
+
|
|
58
|
+
- Shared cache (True): Acts as a proxy, CDN, or gateway cache serving multiple users.
|
|
59
|
+
Must respect private directives and Authorization header restrictions.
|
|
60
|
+
Can use s-maxage directive instead of max-age for shared-specific freshness.
|
|
61
|
+
|
|
62
|
+
- Private cache (False): Acts as a browser or user-agent cache for a single user.
|
|
63
|
+
Can cache private responses and ignore s-maxage directives.
|
|
64
|
+
|
|
65
|
+
Default: True (shared cache)
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
--------
|
|
69
|
+
>>> # Shared cache (proxy/CDN)
|
|
70
|
+
>>> options = CacheOptions(shared=True)
|
|
71
|
+
|
|
72
|
+
>>> # Private cache (browser)
|
|
73
|
+
>>> options = CacheOptions(shared=False)
|
|
74
|
+
|
|
75
|
+
supported_methods : list[str]
|
|
76
|
+
HTTP methods that are allowed to be cached by this cache implementation.
|
|
77
|
+
|
|
78
|
+
RFC 9111 Section 3, paragraph 2.1:
|
|
79
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.1.1
|
|
80
|
+
|
|
81
|
+
"A cache MUST NOT store a response to a request unless:
|
|
82
|
+
- the request method is understood by the cache"
|
|
83
|
+
|
|
84
|
+
Default: ["GET", "HEAD"] (most commonly cached methods)
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
--------
|
|
88
|
+
>>> # Default: cache GET and HEAD only
|
|
89
|
+
>>> options = CacheOptions()
|
|
90
|
+
>>> options.supported_methods
|
|
91
|
+
['GET', 'HEAD']
|
|
92
|
+
|
|
93
|
+
>>> # Cache POST responses (advanced use case)
|
|
94
|
+
>>> options = CacheOptions(supported_methods=["GET", "HEAD", "POST"])
|
|
95
|
+
|
|
96
|
+
allow_stale : bool
|
|
97
|
+
Controls whether stale responses can be served without revalidation.
|
|
98
|
+
|
|
99
|
+
RFC 9111 Section 4.2.4: Serving Stale Responses
|
|
100
|
+
https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.4
|
|
101
|
+
|
|
102
|
+
"A cache MUST NOT generate a stale response unless it is disconnected or
|
|
103
|
+
doing so is explicitly permitted by the client or origin server (e.g., by
|
|
104
|
+
the max-stale request directive in Section 5.2.1, extension directives
|
|
105
|
+
such as those defined in [RFC5861], or configuration in accordance with
|
|
106
|
+
an out-of-band contract)."
|
|
107
|
+
|
|
108
|
+
Default: False (no stale responses)
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
--------
|
|
112
|
+
>>> # Conservative: never serve stale
|
|
113
|
+
>>> options = CacheOptions(allow_stale=False)
|
|
114
|
+
|
|
115
|
+
>>> # Permissive: serve stale when allowed
|
|
116
|
+
>>> options = CacheOptions(allow_stale=True)
|
|
117
|
+
|
|
118
|
+
>>> # Stale-while-revalidate pattern (RFC 5861)
|
|
119
|
+
>>> # Even with allow_stale=True, directives are respected
|
|
120
|
+
>>> options = CacheOptions(allow_stale=True)
|
|
121
|
+
"""
|
|
122
|
+
|
|
44
123
|
shared: bool = True
|
|
124
|
+
"""
|
|
125
|
+
When True, the cache operates as a shared cache (proxy/CDN).
|
|
126
|
+
When False, as a private cache (browser).
|
|
127
|
+
"""
|
|
128
|
+
|
|
45
129
|
supported_methods: list[str] = field(default_factory=lambda: ["GET", "HEAD"])
|
|
130
|
+
"""HTTP methods that are allowed to be cached."""
|
|
131
|
+
|
|
46
132
|
allow_stale: bool = False
|
|
133
|
+
"""When True, stale responses can be served without revalidation."""
|
|
47
134
|
|
|
48
135
|
|
|
49
136
|
@dataclass
|
|
@@ -15,9 +15,9 @@ from typing import (
|
|
|
15
15
|
|
|
16
16
|
import sqlite3
|
|
17
17
|
|
|
18
|
-
from hishel.
|
|
19
|
-
from hishel.
|
|
20
|
-
from hishel.
|
|
18
|
+
from hishel._core._base._storages._base import SyncBaseStorage, ensure_cache_dict
|
|
19
|
+
from hishel._core._base._storages._packing import pack, unpack
|
|
20
|
+
from hishel._core.models import (
|
|
21
21
|
CompletePair,
|
|
22
22
|
IncompletePair,
|
|
23
23
|
Pair,
|
|
@@ -8,7 +8,7 @@ from typing import Iterator, Awaitable, Callable
|
|
|
8
8
|
|
|
9
9
|
from typing_extensions import assert_never
|
|
10
10
|
|
|
11
|
-
from hishel
|
|
11
|
+
from hishel import (
|
|
12
12
|
AnyState,
|
|
13
13
|
SyncBaseStorage,
|
|
14
14
|
SyncSqliteStorage,
|
|
@@ -24,8 +24,8 @@ from hishel.beta import (
|
|
|
24
24
|
StoreAndUse,
|
|
25
25
|
create_idle_state,
|
|
26
26
|
)
|
|
27
|
-
from hishel.
|
|
28
|
-
from hishel.
|
|
27
|
+
from hishel._core._spec import InvalidatePairs, vary_headers_match
|
|
28
|
+
from hishel._core.models import CompletePair
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger("hishel.integrations.clients")
|
|
31
31
|
|
|
@@ -6,14 +6,14 @@ from typing import AsyncIterator, Iterable, Iterator, Union, overload
|
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
8
|
|
|
9
|
-
from hishel
|
|
10
|
-
from hishel.
|
|
11
|
-
from hishel.
|
|
12
|
-
from hishel.
|
|
9
|
+
from hishel import Headers, Request, Response
|
|
10
|
+
from hishel._async_cache import AsyncCacheProxy
|
|
11
|
+
from hishel._core._base._storages._base import AsyncBaseStorage, SyncBaseStorage
|
|
12
|
+
from hishel._core._spec import (
|
|
13
13
|
CacheOptions,
|
|
14
14
|
)
|
|
15
|
-
from hishel.
|
|
16
|
-
from hishel.
|
|
15
|
+
from hishel._core.models import AnyIterable
|
|
16
|
+
from hishel._sync_cache import SyncCacheProxy
|
|
17
17
|
|
|
18
18
|
SOCKET_OPTION = t.Union[
|
|
19
19
|
t.Tuple[int, int, int],
|
|
@@ -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:
|
|
@@ -1,16 +1,16 @@
|
|
|
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
|
|
|
8
|
+
from hishel import Headers, Request, Response as Response
|
|
9
|
+
from hishel._core._base._storages._base import SyncBaseStorage
|
|
10
|
+
from hishel._core._spec import CacheOptions
|
|
11
|
+
from hishel._core.models import extract_metadata_from_headers
|
|
12
|
+
from hishel._sync_cache import SyncCacheProxy
|
|
8
13
|
from hishel._utils import snake_to_header
|
|
9
|
-
from hishel.beta import Headers, Request, Response as Response
|
|
10
|
-
from hishel.beta._core._base._storages._base import SyncBaseStorage
|
|
11
|
-
from hishel.beta._core._spec import CacheOptions
|
|
12
|
-
from hishel.beta._core.models import extract_metadata_from_headers
|
|
13
|
-
from hishel.beta._sync_cache import SyncCacheProxy
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
import requests
|
|
@@ -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()
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hishel
|
|
3
|
+
Version: 1.0.0.dev0
|
|
4
|
+
Summary: Elegant HTTP Caching for Python
|
|
5
|
+
Project-URL: Homepage, https://hishel.com
|
|
6
|
+
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
7
|
+
Author-email: Kar Petrosyan <kar.petrosyanpy@gmail.com>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Framework :: Trio
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: anyio>=4.9.0
|
|
28
|
+
Requires-Dist: anysqlite>=0.0.5
|
|
29
|
+
Requires-Dist: httpx>=0.28.0
|
|
30
|
+
Requires-Dist: msgpack>=1.1.2
|
|
31
|
+
Requires-Dist: typing-extensions>=4.14.1
|
|
32
|
+
Provides-Extra: httpx
|
|
33
|
+
Requires-Dist: httpx>=0.28.1; extra == 'httpx'
|
|
34
|
+
Provides-Extra: requests
|
|
35
|
+
Requires-Dist: requests>=2.32.5; extra == 'requests'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_yellow.png#gh-dark-mode-only">
|
|
40
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_black.png#gh-light-mode-only">
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<h1 align="center">Hishel</h1>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<strong>Elegant HTTP Caching for Python</strong>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="https://pypi.org/project/hishel">
|
|
51
|
+
<img src="https://img.shields.io/pypi/v/hishel.svg" alt="PyPI version">
|
|
52
|
+
</a>
|
|
53
|
+
<a href="https://pypi.org/project/hishel">
|
|
54
|
+
<img src="https://img.shields.io/pypi/pyversions/hishel.svg" alt="Python versions">
|
|
55
|
+
</a>
|
|
56
|
+
<a href="https://github.com/karpetrosyan/hishel/blob/master/LICENSE">
|
|
57
|
+
<img src="https://img.shields.io/pypi/l/hishel" alt="License">
|
|
58
|
+
</a>
|
|
59
|
+
<a href="https://coveralls.io/github/karpetrosyan/hishel">
|
|
60
|
+
<img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="Coverage">
|
|
61
|
+
</a>
|
|
62
|
+
<a href="https://static.pepy.tech/badge/hishel/month">
|
|
63
|
+
<img src="https://static.pepy.tech/badge/hishel/month" alt="Downloads">
|
|
64
|
+
</a>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
**Hishel** (հիշել, *to remember* in Armenian) is a modern HTTP caching library for Python that implements [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) specifications. It provides seamless caching integration for popular HTTP clients with minimal code changes.
|
|
70
|
+
|
|
71
|
+
## ✨ Features
|
|
72
|
+
|
|
73
|
+
- 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification
|
|
74
|
+
- 🔌 **Easy Integration** - Drop-in support for HTTPX and Requests
|
|
75
|
+
- 💾 **Flexible Storage** - SQLite backend with more coming soon
|
|
76
|
+
- ⚡ **High Performance** - Efficient caching with minimal overhead
|
|
77
|
+
- 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
|
|
78
|
+
- 🎨 **Type Safe** - Fully typed with comprehensive type hints
|
|
79
|
+
- 🧪 **Well Tested** - Extensive test coverage and battle-tested
|
|
80
|
+
- 🎛️ **Configurable** - Fine-grained control over caching behavior
|
|
81
|
+
- 🌐 **Future Ready** - Designed for easy integration with any HTTP client/server
|
|
82
|
+
|
|
83
|
+
## 📦 Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install hishel
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Optional Dependencies
|
|
90
|
+
|
|
91
|
+
Install with specific HTTP client support:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install hishel[httpx] # For HTTPX support
|
|
95
|
+
pip install hishel[requests] # For Requests support
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or install both:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install hishel[httpx,requests]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 🚀 Quick Start
|
|
105
|
+
|
|
106
|
+
### With HTTPX
|
|
107
|
+
|
|
108
|
+
**Synchronous:**
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from hishel.httpx import SyncCacheClient
|
|
112
|
+
|
|
113
|
+
client = SyncCacheClient()
|
|
114
|
+
|
|
115
|
+
# First request - fetches from origin
|
|
116
|
+
response = client.get("https://api.example.com/data")
|
|
117
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
118
|
+
|
|
119
|
+
# Second request - served from cache
|
|
120
|
+
response = client.get("https://api.example.com/data")
|
|
121
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Asynchronous:**
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from hishel.httpx import AsyncCacheClient
|
|
128
|
+
|
|
129
|
+
async with AsyncCacheClient() as client:
|
|
130
|
+
# First request - fetches from origin
|
|
131
|
+
response = await client.get("https://api.example.com/data")
|
|
132
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
133
|
+
|
|
134
|
+
# Second request - served from cache
|
|
135
|
+
response = await client.get("https://api.example.com/data")
|
|
136
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### With Requests
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import requests
|
|
143
|
+
from hishel.requests import CacheAdapter
|
|
144
|
+
|
|
145
|
+
session = requests.Session()
|
|
146
|
+
session.mount("https://", CacheAdapter())
|
|
147
|
+
session.mount("http://", CacheAdapter())
|
|
148
|
+
|
|
149
|
+
# First request - fetches from origin
|
|
150
|
+
response = session.get("https://api.example.com/data")
|
|
151
|
+
|
|
152
|
+
# Second request - served from cache
|
|
153
|
+
response = session.get("https://api.example.com/data")
|
|
154
|
+
print(response.headers.get("X-Hishel-From-Cache")) # "True"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 🎛️ Advanced Configuration
|
|
158
|
+
|
|
159
|
+
### Custom Cache Options
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from hishel import CacheOptions
|
|
163
|
+
from hishel.httpx import SyncCacheClient
|
|
164
|
+
|
|
165
|
+
client = SyncCacheClient(
|
|
166
|
+
cache_options=CacheOptions(
|
|
167
|
+
shared=False, # Use as private cache (browser-like)
|
|
168
|
+
supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
|
|
169
|
+
allow_stale=True # Allow serving stale responses
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Storage Backend
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from hishel import SyncSqliteStorage
|
|
178
|
+
from hishel.httpx import SyncCacheClient
|
|
179
|
+
|
|
180
|
+
storage = SyncSqliteStorage(
|
|
181
|
+
database_path="my_cache.db",
|
|
182
|
+
default_ttl=7200.0, # Cache entries expire after 2 hours
|
|
183
|
+
refresh_ttl_on_access=True # Reset TTL when accessing cached entries
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
client = SyncCacheClient(storage=storage)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 🏗️ Architecture
|
|
190
|
+
|
|
191
|
+
Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
|
|
192
|
+
|
|
193
|
+
- ✅ **Correct** - Fully RFC 9111 compliant
|
|
194
|
+
- ✅ **Testable** - Easy to test without network dependencies
|
|
195
|
+
- ✅ **Flexible** - Works with any HTTP client or server
|
|
196
|
+
- ✅ **Type Safe** - Clear state transitions with full type hints
|
|
197
|
+
|
|
198
|
+
## 🔮 Roadmap
|
|
199
|
+
|
|
200
|
+
While Hishel currently supports HTTPX and Requests, we're actively working on:
|
|
201
|
+
|
|
202
|
+
- 🎯 Additional HTTP client integrations
|
|
203
|
+
- 🎯 Server-side caching support
|
|
204
|
+
- 🎯 More storage backends
|
|
205
|
+
- 🎯 Advanced caching strategies
|
|
206
|
+
- 🎯 Performance optimizations
|
|
207
|
+
|
|
208
|
+
## 📚 Documentation
|
|
209
|
+
|
|
210
|
+
Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev)
|
|
211
|
+
|
|
212
|
+
- [Getting Started](https://hishel.com)
|
|
213
|
+
- [HTTPX Integration](https://hishel.com/dev/integrations/httpx)
|
|
214
|
+
- [Requests Integration](https://hishel.com/dev/integrations/requests)
|
|
215
|
+
- [Storage Backends](https://hishel.com/dev/storages)
|
|
216
|
+
- [RFC 9111 Specification](https://hishel.com/dev/specification)
|
|
217
|
+
|
|
218
|
+
## 🤝 Contributing
|
|
219
|
+
|
|
220
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
221
|
+
|
|
222
|
+
See our [Contributing Guide](https://hishel.com/dev/contributing) for more details.
|
|
223
|
+
|
|
224
|
+
## 📄 License
|
|
225
|
+
|
|
226
|
+
This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
|
|
227
|
+
|
|
228
|
+
## 💖 Support
|
|
229
|
+
|
|
230
|
+
If you find Hishel useful, please consider:
|
|
231
|
+
|
|
232
|
+
- ⭐ Starring the repository
|
|
233
|
+
- 🐛 Reporting bugs and issues
|
|
234
|
+
- 💡 Suggesting new features
|
|
235
|
+
- 📖 Improving documentation
|
|
236
|
+
- ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan)
|
|
237
|
+
|
|
238
|
+
## 🙏 Acknowledgments
|
|
239
|
+
|
|
240
|
+
Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly:
|
|
241
|
+
|
|
242
|
+
- [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python
|
|
243
|
+
- [Requests](https://github.com/psf/requests) - The classic HTTP library for Python
|
|
244
|
+
- [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
<p align="center">
|
|
249
|
+
<strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
|
|
250
|
+
</p>
|
|
251
|
+
|
|
252
|
+
## [1.0.0dev0] - 2025-10-19
|
|
253
|
+
|
|
254
|
+
### ⚙️ Miscellaneous Tasks
|
|
255
|
+
|
|
256
|
+
- *(docs)* Use mike powered versioning
|
|
257
|
+
- *(docs)* Improve docs versioning, deploy dev doc on ci
|
|
258
|
+
## [0.1.5] - 2025-10-18
|
|
259
|
+
|
|
260
|
+
### 🚀 Features
|
|
261
|
+
|
|
262
|
+
- *(perf)* Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
263
|
+
- Better cache-control parsing
|
|
264
|
+
- Add close method to storages API (#384)
|
|
265
|
+
- *(perf)* Increase requests buffer size to 128KB, disable charset detection
|
|
266
|
+
|
|
267
|
+
### 🐛 Bug Fixes
|
|
268
|
+
|
|
269
|
+
- *(docs)* Fix some line breaks
|
|
270
|
+
|
|
271
|
+
### ⚙️ Miscellaneous Tasks
|
|
272
|
+
|
|
273
|
+
- Remove some redundant files from repo
|
|
274
|
+
## [0.1.4] - 2025-10-14
|
|
275
|
+
|
|
276
|
+
### 🚀 Features
|
|
277
|
+
|
|
278
|
+
- Add support for a sans-IO API (#366)
|
|
279
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
280
|
+
- Add sqlite storage for beta storages
|
|
281
|
+
- Get rid of some locks from sqlite storage
|
|
282
|
+
- Better async implemetation for sqlite storage
|
|
283
|
+
|
|
284
|
+
### 🐛 Bug Fixes
|
|
285
|
+
|
|
286
|
+
- Create an sqlite file in a cache folder
|
|
287
|
+
- Fix beta imports
|
|
288
|
+
|
|
289
|
+
### ⚙️ Miscellaneous Tasks
|
|
290
|
+
|
|
291
|
+
- Improve CI (#369)
|
|
292
|
+
- *(internal)* Remove src folder (#373)
|
|
293
|
+
- *(internal)* Temporary remove python3.14 from CI
|
|
294
|
+
- *(tests)* Add sqlite tests for new storage
|
|
295
|
+
- *(tests)* Move some tests to beta
|
|
296
|
+
## [0.1.3] - 2025-07-06
|
|
297
|
+
|
|
298
|
+
### 🚀 Features
|
|
299
|
+
|
|
300
|
+
- Support providing a path prefix to S3 storage (#342)
|
|
301
|
+
|
|
302
|
+
### 📚 Documentation
|
|
303
|
+
|
|
304
|
+
- Update link to httpx transports page (#337)
|
|
305
|
+
## [0.1.2] - 2025-04-04
|
|
306
|
+
|
|
307
|
+
### 🐛 Bug Fixes
|
|
308
|
+
|
|
309
|
+
- Requirements.txt to reduce vulnerabilities (#263)
|
|
310
|
+
## [0.0.30] - 2024-07-12
|
|
311
|
+
|
|
312
|
+
### 🐛 Bug Fixes
|
|
313
|
+
|
|
314
|
+
- Requirements.txt to reduce vulnerabilities (#245)
|
|
315
|
+
- Requirements.txt to reduce vulnerabilities (#255)
|
|
316
|
+
## [0.0.27] - 2024-05-31
|
|
317
|
+
|
|
318
|
+
### 🐛 Bug Fixes
|
|
319
|
+
|
|
320
|
+
- *(redis)* Do not update metadata with negative ttl (#231)
|
|
321
|
+
## [0.0.1] - 2023-07-22
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
hishel/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
2
|
+
hishel/_async_cache.py,sha256=gE5CygC7FG9htBMfxul7carRRNph8zcMlSoOcB_LNTY,6792
|
|
3
|
+
hishel/_sync_cache.py,sha256=lfkWHJFK527peESMaufjKSbXBriidc09tOwBwub2t34,6538
|
|
4
|
+
hishel/_utils.py,sha256=uO8PcY_E1sHSgBGzZ2GNB4kpKqAlzmnzPCc3s-yDd44,13826
|
|
5
|
+
hishel/httpx.py,sha256=HcJ5iO9PgkEOp92ti8013N6m1IotLajwd9M_DLsmrX0,10997
|
|
6
|
+
hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
hishel/requests.py,sha256=eiWcwCId04DucnquCsU12tj9cDZcn-cjZ9MYniVuNeo,6429
|
|
8
|
+
hishel/_core/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
9
|
+
hishel/_core/_headers.py,sha256=ii4x2L6GoQFpqpgg28OtFh7p2DoM9mhE4q6CjW6xUWc,17473
|
|
10
|
+
hishel/_core/_spec.py,sha256=d2ZnTXttyT4zuVq9xHAO86VGJxAEBxD2a8WMyEgOuAo,102702
|
|
11
|
+
hishel/_core/models.py,sha256=5qwo1WifrDeZdXag7M5rh0hJuVsm1N-sF3UagQ5LcLc,5519
|
|
12
|
+
hishel/_core/_async/_storages/_sqlite.py,sha256=wIO0UaFzal9qoVqDVczzcsW0kGUjBQD-ikauc_MN414,14704
|
|
13
|
+
hishel/_core/_base/_storages/_base.py,sha256=xLJGTBlFK8DVrQMgRMtGXJnYRUmNB-iYkk7S-BtMx8s,8516
|
|
14
|
+
hishel/_core/_base/_storages/_packing.py,sha256=NFMpSvYYTDBNkzwpjj5l4w-JOPLc19oAEDqDEQJ7VZI,4873
|
|
15
|
+
hishel/_core/_sync/_storages/_sqlite.py,sha256=TDm9jXIWtd54m4_8AiVApxZVmbBoeFVi3E6s-vGzDjs,14138
|
|
16
|
+
hishel-1.0.0.dev0.dist-info/METADATA,sha256=EpqEHRIGfzVXqMiRefCa_NZ9AlbjzVToXfnK-GBrs9o,9993
|
|
17
|
+
hishel-1.0.0.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
hishel-1.0.0.dev0.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
19
|
+
hishel-1.0.0.dev0.dist-info/RECORD,,
|
hishel/_async/__init__.py
DELETED
hishel/_async/_client.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import typing as tp
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
|
|
5
|
-
from ._transports import AsyncCacheTransport
|
|
6
|
-
|
|
7
|
-
__all__ = ("AsyncCacheClient",)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AsyncCacheClient(httpx.AsyncClient):
|
|
11
|
-
def __init__(self, *args: tp.Any, **kwargs: tp.Any):
|
|
12
|
-
self._storage = kwargs.pop("storage") if "storage" in kwargs else None
|
|
13
|
-
self._controller = kwargs.pop("controller") if "controller" in kwargs else None
|
|
14
|
-
super().__init__(*args, **kwargs)
|
|
15
|
-
|
|
16
|
-
def _init_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
|
|
17
|
-
_transport = super()._init_transport(*args, **kwargs)
|
|
18
|
-
return AsyncCacheTransport(
|
|
19
|
-
transport=_transport,
|
|
20
|
-
storage=self._storage,
|
|
21
|
-
controller=self._controller,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
def _init_proxy_transport(self, *args, **kwargs) -> AsyncCacheTransport: # type: ignore
|
|
25
|
-
_transport = super()._init_proxy_transport(*args, **kwargs) # pragma: no cover
|
|
26
|
-
return AsyncCacheTransport( # pragma: no cover
|
|
27
|
-
transport=_transport,
|
|
28
|
-
storage=self._storage,
|
|
29
|
-
controller=self._controller,
|
|
30
|
-
)
|