hishel 0.1.2__tar.gz → 0.1.4__tar.gz
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-0.1.2 → hishel-0.1.4}/.gitignore +1 -0
- {hishel-0.1.2 → hishel-0.1.4}/CHANGELOG.md +32 -1
- {hishel-0.1.2 → hishel-0.1.4}/PKG-INFO +50 -6
- {hishel-0.1.2 → hishel-0.1.4}/README.md +6 -2
- hishel-0.1.4/hishel/__init__.py +57 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/_client.py +1 -1
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/_storages.py +17 -17
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/_transports.py +9 -4
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_controller.py +2 -3
- hishel-0.1.4/hishel/_lmdb_types_.pyi +53 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_s3.py +19 -8
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_serializers.py +2 -2
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/_client.py +1 -1
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/_storages.py +16 -16
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/_transports.py +9 -4
- hishel-0.1.4/hishel/_utils.py +458 -0
- hishel-0.1.4/hishel/beta/__init__.py +59 -0
- hishel-0.1.4/hishel/beta/_async_cache.py +167 -0
- hishel-0.1.4/hishel/beta/_core/_async/_storages/_sqlite.py +411 -0
- hishel-0.1.4/hishel/beta/_core/_base/_storages/_base.py +260 -0
- hishel-0.1.4/hishel/beta/_core/_base/_storages/_packing.py +165 -0
- hishel-0.1.4/hishel/beta/_core/_headers.py +301 -0
- hishel-0.1.4/hishel/beta/_core/_spec.py +2291 -0
- hishel-0.1.4/hishel/beta/_core/_sync/_storages/_sqlite.py +411 -0
- hishel-0.1.4/hishel/beta/_core/models.py +176 -0
- hishel-0.1.4/hishel/beta/_sync_cache.py +167 -0
- hishel-0.1.4/hishel/beta/httpx.py +317 -0
- hishel-0.1.4/hishel/beta/requests.py +193 -0
- hishel-0.1.4/hishel/py.typed +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/pyproject.toml +44 -45
- hishel-0.1.2/hishel/__init__.py +0 -17
- hishel-0.1.2/hishel/_utils.py +0 -118
- {hishel-0.1.2 → hishel-0.1.4}/LICENSE +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/__init__.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/_mock.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_async/_pool.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_exceptions.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_files.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_headers.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_lfu_cache.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/__init__.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/_mock.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_sync/_pool.py +0 -0
- {hishel-0.1.2 → hishel-0.1.4}/hishel/_synchronization.py +0 -0
- /hishel-0.1.2/hishel/py.typed → /hishel-0.1.4/hishel/beta/_core/__init__.py +0 -0
|
@@ -1,4 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.1.4] - 2025-10-14
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- Add support for requests library
|
|
6
|
+
- Add support for Python 3.14
|
|
7
|
+
- Add support for a sans-IO API (#366)
|
|
8
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
9
|
+
- Add sqlite storage for beta storages
|
|
10
|
+
- Get rid of some locks from sqlite storage
|
|
11
|
+
- Better async implemetation for sqlite storage
|
|
12
|
+
- Added `Metadata` to public API. (#363)
|
|
13
|
+
- Fix race condition in FileStorage initialization. (#353)
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Create an sqlite file in a cache folder
|
|
18
|
+
- Fix beta imports
|
|
19
|
+
|
|
20
|
+
### ⚙️ Miscellaneous Tasks
|
|
21
|
+
|
|
22
|
+
- Improve CI (#369)
|
|
23
|
+
- *(internal)* Remove src folder (#373)
|
|
24
|
+
- *(internal)* Temporary remove python3.14 from CI
|
|
25
|
+
- *(tests)* Add sqlite tests for new storage
|
|
26
|
+
- *(tests)* Move some tests to beta
|
|
27
|
+
## 0.1.3 (1st July, 2025)
|
|
28
|
+
|
|
29
|
+
- Remove `types-redis` from dev dependencies (#336)
|
|
30
|
+
- Bump redis to 6.0.0 and address async `.close()` deprecation warning (#336)
|
|
31
|
+
- Avoid race condition when unlinking files in `FileStorage`. (#334)
|
|
32
|
+
- Allow prodiving a `path_prefix` in `S3Storage` and `AsyncS3Storage`. (#342)
|
|
2
33
|
|
|
3
34
|
## 0.1.2 (5th April, 2025)
|
|
4
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -21,18 +21,27 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
26
|
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: anyio>=4.9.0
|
|
28
|
+
Requires-Dist: anysqlite>=0.0.5
|
|
26
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'
|
|
27
34
|
Provides-Extra: redis
|
|
28
|
-
Requires-Dist: redis==
|
|
35
|
+
Requires-Dist: redis==6.2.0; extra == 'redis'
|
|
36
|
+
Provides-Extra: requests
|
|
37
|
+
Requires-Dist: requests>=2.32.5; extra == 'requests'
|
|
29
38
|
Provides-Extra: s3
|
|
30
39
|
Requires-Dist: boto3<=1.15.3,>=1.15.0; (python_version < '3.12') and extra == 's3'
|
|
31
40
|
Requires-Dist: boto3>=1.15.3; (python_version >= '3.12') and extra == 's3'
|
|
32
41
|
Provides-Extra: sqlite
|
|
33
42
|
Requires-Dist: anysqlite>=0.0.5; extra == 'sqlite'
|
|
34
43
|
Provides-Extra: yaml
|
|
35
|
-
Requires-Dist: pyyaml==6.0.
|
|
44
|
+
Requires-Dist: pyyaml==6.0.2; extra == 'yaml'
|
|
36
45
|
Description-Content-Type: text/markdown
|
|
37
46
|
|
|
38
47
|
<p align="center" class="logo">
|
|
@@ -59,8 +68,8 @@ Description-Content-Type: text/markdown
|
|
|
59
68
|
<img src="https://img.shields.io/pypi/l/hishel" alt="license">
|
|
60
69
|
</a>
|
|
61
70
|
|
|
62
|
-
<a href="https://
|
|
63
|
-
<img src="https://img.shields.io/
|
|
71
|
+
<a href="https://coveralls.io/github/karpetrosyan/hishel">
|
|
72
|
+
<img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="license">
|
|
64
73
|
</a>
|
|
65
74
|
|
|
66
75
|
<a href="https://github.com/karpetrosyan/hishel">
|
|
@@ -68,6 +77,10 @@ Description-Content-Type: text/markdown
|
|
|
68
77
|
</a>
|
|
69
78
|
</p>
|
|
70
79
|
|
|
80
|
+
<p align="center">
|
|
81
|
+
<a href="https://buymeacoffee.com/karpetrosyan" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
|
82
|
+
</p>
|
|
83
|
+
|
|
71
84
|
-----
|
|
72
85
|
|
|
73
86
|
**Hishel (հիշել, remember)** is a library that implements HTTP Caching for [HTTPX](https://github.com/encode/httpx) and [HTTP Core](https://github.com/encode/httpcore) libraries in accordance with [**RFC 9111**](https://www.rfc-editor.org/rfc/rfc9111.html), the most recent caching specification.
|
|
@@ -179,7 +192,38 @@ You have complete control over them; you can change storage or even write your o
|
|
|
179
192
|
You can support the project by simply leaving a GitHub star ⭐ or by [contributing](https://hishel.com/contributing/).
|
|
180
193
|
Help us grow and continue developing good software for you ❤️
|
|
181
194
|
|
|
182
|
-
|
|
195
|
+
## [0.1.4] - 2025-10-14
|
|
196
|
+
|
|
197
|
+
### 🚀 Features
|
|
198
|
+
|
|
199
|
+
- Add support for requests library
|
|
200
|
+
- Add support for Python 3.14
|
|
201
|
+
- Add support for a sans-IO API (#366)
|
|
202
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
203
|
+
- Add sqlite storage for beta storages
|
|
204
|
+
- Get rid of some locks from sqlite storage
|
|
205
|
+
- Better async implemetation for sqlite storage
|
|
206
|
+
- Added `Metadata` to public API. (#363)
|
|
207
|
+
- Fix race condition in FileStorage initialization. (#353)
|
|
208
|
+
|
|
209
|
+
### 🐛 Bug Fixes
|
|
210
|
+
|
|
211
|
+
- Create an sqlite file in a cache folder
|
|
212
|
+
- Fix beta imports
|
|
213
|
+
|
|
214
|
+
### ⚙️ Miscellaneous Tasks
|
|
215
|
+
|
|
216
|
+
- Improve CI (#369)
|
|
217
|
+
- *(internal)* Remove src folder (#373)
|
|
218
|
+
- *(internal)* Temporary remove python3.14 from CI
|
|
219
|
+
- *(tests)* Add sqlite tests for new storage
|
|
220
|
+
- *(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)
|
|
183
227
|
|
|
184
228
|
## 0.1.2 (5th April, 2025)
|
|
185
229
|
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
<img src="https://img.shields.io/pypi/l/hishel" alt="license">
|
|
23
23
|
</a>
|
|
24
24
|
|
|
25
|
-
<a href="https://
|
|
26
|
-
<img src="https://img.shields.io/
|
|
25
|
+
<a href="https://coveralls.io/github/karpetrosyan/hishel">
|
|
26
|
+
<img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="license">
|
|
27
27
|
</a>
|
|
28
28
|
|
|
29
29
|
<a href="https://github.com/karpetrosyan/hishel">
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
</a>
|
|
32
32
|
</p>
|
|
33
33
|
|
|
34
|
+
<p align="center">
|
|
35
|
+
<a href="https://buymeacoffee.com/karpetrosyan" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
34
38
|
-----
|
|
35
39
|
|
|
36
40
|
**Hishel (հիշել, remember)** is a library that implements HTTP Caching for [HTTPX](https://github.com/encode/httpx) and [HTTP Core](https://github.com/encode/httpcore) libraries in accordance with [**RFC 9111**](https://www.rfc-editor.org/rfc/rfc9111.html), the most recent caching specification.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from ._async import *
|
|
4
|
+
from ._controller import *
|
|
5
|
+
from ._exceptions import *
|
|
6
|
+
from ._headers import *
|
|
7
|
+
from ._serializers import *
|
|
8
|
+
from ._sync import *
|
|
9
|
+
from ._lfu_cache import *
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
# Old API
|
|
13
|
+
"AsyncCacheClient",
|
|
14
|
+
"MockAsyncConnectionPool",
|
|
15
|
+
"MockAsyncTransport",
|
|
16
|
+
"AsyncCacheConnectionPool",
|
|
17
|
+
"AsyncBaseStorage",
|
|
18
|
+
"AsyncFileStorage",
|
|
19
|
+
"AsyncInMemoryStorage",
|
|
20
|
+
"AsyncRedisStorage",
|
|
21
|
+
"AsyncS3Storage",
|
|
22
|
+
"AsyncSQLiteStorage",
|
|
23
|
+
"AsyncCacheTransport",
|
|
24
|
+
"HEURISTICALLY_CACHEABLE_STATUS_CODES",
|
|
25
|
+
"Controller",
|
|
26
|
+
"CacheControlError",
|
|
27
|
+
"ParseError",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
"CacheControl",
|
|
30
|
+
"Vary",
|
|
31
|
+
"BaseSerializer",
|
|
32
|
+
"JSONSerializer",
|
|
33
|
+
"Metadata",
|
|
34
|
+
"PickleSerializer",
|
|
35
|
+
"YAMLSerializer",
|
|
36
|
+
"clone_model",
|
|
37
|
+
"CacheClient",
|
|
38
|
+
"MockConnectionPool",
|
|
39
|
+
"MockTransport",
|
|
40
|
+
"CacheConnectionPool",
|
|
41
|
+
"BaseStorage",
|
|
42
|
+
"FileStorage",
|
|
43
|
+
"InMemoryStorage",
|
|
44
|
+
"RedisStorage",
|
|
45
|
+
"S3Storage",
|
|
46
|
+
"SQLiteStorage",
|
|
47
|
+
"CacheTransport",
|
|
48
|
+
"LFUCache",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def install_cache() -> None: # pragma: no cover
|
|
52
|
+
httpx.AsyncClient = AsyncCacheClient # type: ignore
|
|
53
|
+
httpx.Client = CacheClient # type: ignore
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__version__ = "0.1.4"
|
|
57
|
+
|
|
@@ -4,7 +4,6 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
-
import typing as t
|
|
8
7
|
import typing as tp
|
|
9
8
|
import warnings
|
|
10
9
|
from copy import deepcopy
|
|
@@ -24,13 +23,11 @@ except ImportError: # pragma: no cover
|
|
|
24
23
|
|
|
25
24
|
from httpcore import Request, Response
|
|
26
25
|
|
|
27
|
-
if
|
|
26
|
+
if tp.TYPE_CHECKING: # pragma: no cover
|
|
28
27
|
from typing_extensions import TypeAlias
|
|
29
28
|
|
|
30
|
-
from hishel._serializers import BaseSerializer, clone_model
|
|
31
|
-
|
|
32
29
|
from .._files import AsyncFileManager
|
|
33
|
-
from .._serializers import JSONSerializer, Metadata
|
|
30
|
+
from .._serializers import BaseSerializer, JSONSerializer, Metadata, clone_model
|
|
34
31
|
from .._synchronization import AsyncLock
|
|
35
32
|
from .._utils import float_seconds_to_int_milliseconds
|
|
36
33
|
|
|
@@ -39,10 +36,10 @@ logger = logging.getLogger("hishel.storages")
|
|
|
39
36
|
__all__ = (
|
|
40
37
|
"AsyncBaseStorage",
|
|
41
38
|
"AsyncFileStorage",
|
|
42
|
-
"AsyncRedisStorage",
|
|
43
|
-
"AsyncSQLiteStorage",
|
|
44
39
|
"AsyncInMemoryStorage",
|
|
40
|
+
"AsyncRedisStorage",
|
|
45
41
|
"AsyncS3Storage",
|
|
42
|
+
"AsyncSQLiteStorage",
|
|
46
43
|
)
|
|
47
44
|
|
|
48
45
|
StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata]
|
|
@@ -106,8 +103,7 @@ class AsyncFileStorage(AsyncBaseStorage):
|
|
|
106
103
|
self._base_path = Path(base_path) if base_path is not None else Path(".cache/hishel")
|
|
107
104
|
self._gitignore_file = self._base_path / ".gitignore"
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
self._base_path.mkdir(parents=True)
|
|
106
|
+
self._base_path.mkdir(parents=True, exist_ok=True)
|
|
111
107
|
|
|
112
108
|
if not self._gitignore_file.is_file():
|
|
113
109
|
with open(self._gitignore_file, "w", encoding="utf-8") as f:
|
|
@@ -153,13 +149,13 @@ class AsyncFileStorage(AsyncBaseStorage):
|
|
|
153
149
|
"""
|
|
154
150
|
|
|
155
151
|
if isinstance(key, Response): # pragma: no cover
|
|
156
|
-
key =
|
|
152
|
+
key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
157
153
|
|
|
158
154
|
response_path = self._base_path / key
|
|
159
155
|
|
|
160
156
|
async with self._lock:
|
|
161
157
|
if response_path.exists():
|
|
162
|
-
response_path.unlink()
|
|
158
|
+
response_path.unlink(missing_ok=True)
|
|
163
159
|
|
|
164
160
|
async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
|
|
165
161
|
"""
|
|
@@ -222,7 +218,7 @@ class AsyncFileStorage(AsyncBaseStorage):
|
|
|
222
218
|
if response_path.is_file():
|
|
223
219
|
age = time.time() - response_path.stat().st_mtime
|
|
224
220
|
if age > self._ttl:
|
|
225
|
-
response_path.unlink()
|
|
221
|
+
response_path.unlink(missing_ok=True)
|
|
226
222
|
return
|
|
227
223
|
|
|
228
224
|
self._last_cleaned = time.monotonic()
|
|
@@ -322,7 +318,7 @@ class AsyncSQLiteStorage(AsyncBaseStorage):
|
|
|
322
318
|
assert self._connection
|
|
323
319
|
|
|
324
320
|
if isinstance(key, Response): # pragma: no cover
|
|
325
|
-
key =
|
|
321
|
+
key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
326
322
|
|
|
327
323
|
async with self._lock:
|
|
328
324
|
await self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
|
|
@@ -459,7 +455,7 @@ class AsyncRedisStorage(AsyncBaseStorage):
|
|
|
459
455
|
"""
|
|
460
456
|
|
|
461
457
|
if isinstance(key, Response): # pragma: no cover
|
|
462
|
-
key =
|
|
458
|
+
key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
463
459
|
|
|
464
460
|
await self._client.delete(key)
|
|
465
461
|
|
|
@@ -507,7 +503,7 @@ class AsyncRedisStorage(AsyncBaseStorage):
|
|
|
507
503
|
return self._serializer.loads(cached_response)
|
|
508
504
|
|
|
509
505
|
async def aclose(self) -> None: # pragma: no cover
|
|
510
|
-
await self._client.
|
|
506
|
+
await self._client.aclose()
|
|
511
507
|
|
|
512
508
|
|
|
513
509
|
class AsyncInMemoryStorage(AsyncBaseStorage):
|
|
@@ -572,7 +568,7 @@ class AsyncInMemoryStorage(AsyncBaseStorage):
|
|
|
572
568
|
"""
|
|
573
569
|
|
|
574
570
|
if isinstance(key, Response): # pragma: no cover
|
|
575
|
-
key =
|
|
571
|
+
key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
576
572
|
|
|
577
573
|
async with self._lock:
|
|
578
574
|
self._cache.remove_key(key)
|
|
@@ -654,6 +650,8 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
654
650
|
:type check_ttl_every: tp.Union[int, float]
|
|
655
651
|
:param client: A client for S3, defaults to None
|
|
656
652
|
:type client: tp.Optional[tp.Any], optional
|
|
653
|
+
:param path_prefix: A path prefix to use for S3 object keys, defaults to "hishel-"
|
|
654
|
+
:type path_prefix: str, optional
|
|
657
655
|
"""
|
|
658
656
|
|
|
659
657
|
def __init__(
|
|
@@ -663,6 +661,7 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
663
661
|
ttl: tp.Optional[tp.Union[int, float]] = None,
|
|
664
662
|
check_ttl_every: tp.Union[int, float] = 60,
|
|
665
663
|
client: tp.Optional[tp.Any] = None,
|
|
664
|
+
path_prefix: str = "hishel-",
|
|
666
665
|
) -> None:
|
|
667
666
|
super().__init__(serializer, ttl)
|
|
668
667
|
|
|
@@ -680,6 +679,7 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
680
679
|
bucket_name=bucket_name,
|
|
681
680
|
is_binary=self._serializer.is_binary,
|
|
682
681
|
check_ttl_every=check_ttl_every,
|
|
682
|
+
path_prefix=path_prefix,
|
|
683
683
|
)
|
|
684
684
|
self._lock = AsyncLock()
|
|
685
685
|
|
|
@@ -716,7 +716,7 @@ class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
|
|
|
716
716
|
"""
|
|
717
717
|
|
|
718
718
|
if isinstance(key, Response): # pragma: no cover
|
|
719
|
-
key =
|
|
719
|
+
key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
|
|
720
720
|
|
|
721
721
|
async with self._lock:
|
|
722
722
|
await self._s3_manager.remove_entry(key)
|
|
@@ -8,11 +8,10 @@ import httpx
|
|
|
8
8
|
from httpx import AsyncByteStream, Request, Response
|
|
9
9
|
from httpx._exceptions import ConnectError
|
|
10
10
|
|
|
11
|
-
from hishel._utils import extract_header_values_decoded, normalized_url
|
|
12
|
-
|
|
13
11
|
from .._controller import Controller, allowed_stale
|
|
14
12
|
from .._headers import parse_cache_control
|
|
15
13
|
from .._serializers import JSONSerializer, Metadata
|
|
14
|
+
from .._utils import extract_header_values_decoded, normalized_url
|
|
16
15
|
from ._storages import AsyncBaseStorage, AsyncFileStorage
|
|
17
16
|
|
|
18
17
|
if tp.TYPE_CHECKING: # pragma: no cover
|
|
@@ -211,11 +210,17 @@ class AsyncCacheTransport(httpx.AsyncBaseTransport):
|
|
|
211
210
|
)
|
|
212
211
|
|
|
213
212
|
regular_response = await self._transport.handle_async_request(request)
|
|
214
|
-
|
|
213
|
+
try:
|
|
214
|
+
# Prefer already-read content, if available
|
|
215
|
+
stream = fake_stream(regular_response.content)
|
|
216
|
+
except httpx.ResponseNotRead:
|
|
217
|
+
# Fall back to stream if not yet read
|
|
218
|
+
assert isinstance(regular_response.stream, tp.AsyncIterable)
|
|
219
|
+
stream = regular_response.stream
|
|
215
220
|
httpcore_regular_response = httpcore.Response(
|
|
216
221
|
status=regular_response.status_code,
|
|
217
222
|
headers=regular_response.headers.raw,
|
|
218
|
-
content=AsyncCacheStream(
|
|
223
|
+
content=AsyncCacheStream(stream),
|
|
219
224
|
extensions=regular_response.extensions,
|
|
220
225
|
)
|
|
221
226
|
await httpcore_regular_response.aread()
|
|
@@ -3,8 +3,7 @@ import typing as tp
|
|
|
3
3
|
|
|
4
4
|
from httpcore import Request, Response
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
6
|
+
from ._headers import Vary, parse_cache_control
|
|
8
7
|
from ._utils import (
|
|
9
8
|
BaseClock,
|
|
10
9
|
Clock,
|
|
@@ -21,7 +20,7 @@ logger = logging.getLogger("hishel.controller")
|
|
|
21
20
|
HEURISTICALLY_CACHEABLE_STATUS_CODES = (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501)
|
|
22
21
|
HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]
|
|
23
22
|
|
|
24
|
-
__all__ = ("
|
|
23
|
+
__all__ = ("HEURISTICALLY_CACHEABLE_STATUS_CODES", "Controller")
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def get_updated_headers(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from typing import Any, Iterator
|
|
3
|
+
|
|
4
|
+
class Database: ...
|
|
5
|
+
|
|
6
|
+
class Transaction:
|
|
7
|
+
def get(self, key: bytes, *, db: Database | None = None) -> bytes | None:
|
|
8
|
+
"""
|
|
9
|
+
Get the value associated with the given key.
|
|
10
|
+
"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def put(self, key: bytes, value: bytes, *, db: Database | None = None, dupdata: bool = False) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Put a key-value pair into the database.
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def delete(self, key: bytes, *, db: Database | None = None) -> bool:
|
|
20
|
+
"""
|
|
21
|
+
Delete the key from the database.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def cursor(self, db: Database) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Create a cursor for iterating over key-value pairs in the database.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
class Environment:
|
|
32
|
+
@contextmanager
|
|
33
|
+
def begin(self, *, db: Database | None = None, write: bool = False) -> Iterator[Transaction]:
|
|
34
|
+
"""
|
|
35
|
+
Begin a transaction in the environment.
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError("It's only for type hinting purposes")
|
|
38
|
+
|
|
39
|
+
def open_db(self, key: bytes, dupsort: bool = False) -> Database:
|
|
40
|
+
"""
|
|
41
|
+
Open a database within the environment.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError("It's only for type hinting purposes")
|
|
44
|
+
|
|
45
|
+
def open(
|
|
46
|
+
path: str,
|
|
47
|
+
*,
|
|
48
|
+
max_dbs: int = 0,
|
|
49
|
+
) -> Environment:
|
|
50
|
+
"""
|
|
51
|
+
Open an LMDB environment at the specified path.
|
|
52
|
+
"""
|
|
53
|
+
raise NotImplementedError("It's only for type hinting purposes")
|
|
@@ -11,16 +11,22 @@ def get_timestamp_in_ms() -> float:
|
|
|
11
11
|
|
|
12
12
|
class S3Manager:
|
|
13
13
|
def __init__(
|
|
14
|
-
self,
|
|
14
|
+
self,
|
|
15
|
+
client: tp.Any,
|
|
16
|
+
bucket_name: str,
|
|
17
|
+
check_ttl_every: tp.Union[int, float],
|
|
18
|
+
is_binary: bool = False,
|
|
19
|
+
path_prefix: str = "hishel-",
|
|
15
20
|
):
|
|
16
21
|
self._client = client
|
|
17
22
|
self._bucket_name = bucket_name
|
|
18
23
|
self._is_binary = is_binary
|
|
19
24
|
self._last_cleaned = time.monotonic()
|
|
20
25
|
self._check_ttl_every = check_ttl_every
|
|
26
|
+
self._path_prefix = path_prefix
|
|
21
27
|
|
|
22
28
|
def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None:
|
|
23
|
-
path =
|
|
29
|
+
path = self._path_prefix + path
|
|
24
30
|
if isinstance(data, str):
|
|
25
31
|
data = data.encode("utf-8")
|
|
26
32
|
|
|
@@ -43,7 +49,7 @@ class S3Manager:
|
|
|
43
49
|
)
|
|
44
50
|
|
|
45
51
|
def read_from(self, path: str) -> tp.Union[bytes, str]:
|
|
46
|
-
path =
|
|
52
|
+
path = self._path_prefix + path
|
|
47
53
|
response = self._client.get_object(
|
|
48
54
|
Bucket=self._bucket_name,
|
|
49
55
|
Key=path,
|
|
@@ -57,7 +63,7 @@ class S3Manager:
|
|
|
57
63
|
return tp.cast(str, content.decode("utf-8"))
|
|
58
64
|
|
|
59
65
|
def remove_expired(self, ttl: int, key: str) -> None:
|
|
60
|
-
path =
|
|
66
|
+
path = self._path_prefix + key
|
|
61
67
|
|
|
62
68
|
if time.monotonic() - self._last_cleaned < self._check_ttl_every:
|
|
63
69
|
try:
|
|
@@ -72,7 +78,7 @@ class S3Manager:
|
|
|
72
78
|
|
|
73
79
|
self._last_cleaned = time.monotonic()
|
|
74
80
|
for obj in self._client.list_objects(Bucket=self._bucket_name).get("Contents", []):
|
|
75
|
-
if not obj["Key"].startswith(
|
|
81
|
+
if not obj["Key"].startswith(self._path_prefix): # pragma: no cover
|
|
76
82
|
continue
|
|
77
83
|
|
|
78
84
|
try:
|
|
@@ -88,15 +94,20 @@ class S3Manager:
|
|
|
88
94
|
self._client.delete_object(Bucket=self._bucket_name, Key=obj["Key"])
|
|
89
95
|
|
|
90
96
|
def remove_entry(self, key: str) -> None:
|
|
91
|
-
path =
|
|
97
|
+
path = self._path_prefix + key
|
|
92
98
|
self._client.delete_object(Bucket=self._bucket_name, Key=path)
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
class AsyncS3Manager: # pragma: no cover
|
|
96
102
|
def __init__(
|
|
97
|
-
self,
|
|
103
|
+
self,
|
|
104
|
+
client: tp.Any,
|
|
105
|
+
bucket_name: str,
|
|
106
|
+
check_ttl_every: tp.Union[int, float],
|
|
107
|
+
is_binary: bool = False,
|
|
108
|
+
path_prefix: str = "hishel-",
|
|
98
109
|
):
|
|
99
|
-
self._sync_manager = S3Manager(client, bucket_name, check_ttl_every, is_binary)
|
|
110
|
+
self._sync_manager = S3Manager(client, bucket_name, check_ttl_every, is_binary, path_prefix)
|
|
100
111
|
|
|
101
112
|
async def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None:
|
|
102
113
|
return await to_thread.run_sync(self._sync_manager.write_to, path, data, only_metadata)
|
|
@@ -6,7 +6,7 @@ from datetime import datetime
|
|
|
6
6
|
|
|
7
7
|
from httpcore import Request, Response
|
|
8
8
|
|
|
9
|
-
from
|
|
9
|
+
from ._utils import normalized_url
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
12
|
import yaml
|
|
@@ -17,7 +17,7 @@ HEADERS_ENCODING = "iso-8859-1"
|
|
|
17
17
|
KNOWN_RESPONSE_EXTENSIONS = ("http_version", "reason_phrase")
|
|
18
18
|
KNOWN_REQUEST_EXTENSIONS = ("timeout", "sni_hostname")
|
|
19
19
|
|
|
20
|
-
__all__ = ("
|
|
20
|
+
__all__ = ("BaseSerializer", "JSONSerializer", "Metadata", "PickleSerializer", "YAMLSerializer", "clone_model")
|
|
21
21
|
|
|
22
22
|
T = tp.TypeVar("T", Request, Response)
|
|
23
23
|
|