hishel 0.1.5__py3-none-any.whl → 1.0.0.dev1__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/_core/_async/_storages/_sqlite.py +457 -0
- hishel/{beta/_core → _core}/_base/_storages/_base.py +1 -1
- hishel/{beta/_core → _core}/_base/_storages/_packing.py +5 -5
- hishel/{beta/_core → _core}/_spec.py +89 -2
- hishel/_core/_sync/_storages/_sqlite.py +457 -0
- hishel/{beta/_core → _core}/models.py +1 -1
- hishel/{beta/_sync_cache.py → _sync_cache.py} +3 -3
- hishel/_utils.py +1 -241
- hishel/{beta/httpx.py → httpx.py} +15 -8
- hishel/{beta/requests.py → requests.py} +5 -5
- hishel-1.0.0.dev1.dist-info/METADATA +298 -0
- hishel-1.0.0.dev1.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/_async/_storages/_sqlite.py +0 -411
- hishel/beta/_core/_sync/_storages/_sqlite.py +0 -411
- hishel-0.1.5.dist-info/METADATA +0 -258
- hishel-0.1.5.dist-info/RECORD +0 -41
- /hishel/{beta/_core → _core}/_headers.py +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev1.dist-info}/WHEEL +0 -0
- {hishel-0.1.5.dist-info → hishel-1.0.0.dev1.dist-info}/licenses/LICENSE +0 -0
hishel/_utils.py
CHANGED
|
@@ -2,19 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import calendar
|
|
4
4
|
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import sqlite3
|
|
7
5
|
import time
|
|
8
6
|
import typing as tp
|
|
9
|
-
from datetime import date
|
|
10
7
|
from email.utils import parsedate_tz
|
|
11
|
-
from typing import
|
|
8
|
+
from typing import AsyncIterator, Generator, Iterable, Iterator, TypeVar
|
|
12
9
|
|
|
13
|
-
import anyio
|
|
14
|
-
import anysqlite
|
|
15
10
|
import httpcore
|
|
16
11
|
import httpx
|
|
17
|
-
from anyio import from_thread, to_thread
|
|
18
12
|
|
|
19
13
|
HEADERS_ENCODING = "iso-8859-1"
|
|
20
14
|
|
|
@@ -116,10 +110,6 @@ def parse_date(date: str) -> tp.Optional[int]:
|
|
|
116
110
|
return timestamp
|
|
117
111
|
|
|
118
112
|
|
|
119
|
-
async def asleep(seconds: tp.Union[int, float]) -> None:
|
|
120
|
-
await anyio.sleep(seconds)
|
|
121
|
-
|
|
122
|
-
|
|
123
113
|
def sleep(seconds: tp.Union[int, float]) -> None:
|
|
124
114
|
time.sleep(seconds)
|
|
125
115
|
|
|
@@ -155,66 +145,6 @@ def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp
|
|
|
155
145
|
return matching, non_matching
|
|
156
146
|
|
|
157
147
|
|
|
158
|
-
def async_iterator_to_sync(iterator: AsyncIterator[bytes]) -> Iterator[bytes]:
|
|
159
|
-
"""
|
|
160
|
-
Convert an asynchronous byte iterator to a synchronous one.
|
|
161
|
-
This function takes an asynchronous iterator that yields bytes and converts it into
|
|
162
|
-
a synchronous iterator.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
iterator (AsyncIterator[bytes]): The asynchronous byte iterator to be converted.
|
|
166
|
-
Returns:
|
|
167
|
-
Iterator[bytes]: A synchronous iterator that yields the same byte chunks as the input iterator.
|
|
168
|
-
Example:
|
|
169
|
-
```python
|
|
170
|
-
async_iter = some_async_byte_stream()
|
|
171
|
-
sync_iter = async_iterator_to_sync(async_iter)
|
|
172
|
-
for chunk in sync_iter:
|
|
173
|
-
process_bytes(chunk)
|
|
174
|
-
```
|
|
175
|
-
"""
|
|
176
|
-
|
|
177
|
-
while True:
|
|
178
|
-
try:
|
|
179
|
-
chunk = from_thread.run(iterator.__anext__)
|
|
180
|
-
except StopAsyncIteration:
|
|
181
|
-
break
|
|
182
|
-
yield chunk
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _call_next(iterator: Iterator[bytes]) -> bytes:
|
|
186
|
-
try:
|
|
187
|
-
return iterator.__next__()
|
|
188
|
-
except StopIteration:
|
|
189
|
-
raise StopAsyncIteration
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
async def sync_iterator_to_async(iterator: Iterator[bytes]) -> AsyncIterator[bytes]:
|
|
193
|
-
"""
|
|
194
|
-
Converts a synchronous bytes iterator to an asynchronous one.
|
|
195
|
-
This function takes a synchronous iterator that yields bytes and converts it into an
|
|
196
|
-
asynchronous iterator, allowing it to be used in async contexts without blocking.
|
|
197
|
-
Args:
|
|
198
|
-
iterator (Iterator[bytes]): A synchronous iterator yielding bytes objects.
|
|
199
|
-
Returns:
|
|
200
|
-
AsyncIterator[bytes]: An asynchronous iterator yielding the same bytes objects.
|
|
201
|
-
Example:
|
|
202
|
-
```
|
|
203
|
-
sync_iter = iter([b'data1', b'data2'])
|
|
204
|
-
async for chunk in sync_iterator_to_async(sync_iter):
|
|
205
|
-
await process_chunk(chunk)
|
|
206
|
-
```
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
while True:
|
|
210
|
-
try:
|
|
211
|
-
chunk = await to_thread.run_sync(_call_next, iterator)
|
|
212
|
-
except StopAsyncIteration:
|
|
213
|
-
break
|
|
214
|
-
|
|
215
|
-
yield chunk
|
|
216
|
-
|
|
217
|
-
|
|
218
148
|
async def make_async_iterator(iterable: Iterable[bytes]) -> AsyncIterator[bytes]:
|
|
219
149
|
for item in iterable:
|
|
220
150
|
yield item
|
|
@@ -286,173 +216,3 @@ class GeneratorWithReturnValue:
|
|
|
286
216
|
self.value = exc.value
|
|
287
217
|
raise
|
|
288
218
|
return chunk
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def print_sqlite_state(conn: sqlite3.Connection) -> str:
|
|
292
|
-
"""
|
|
293
|
-
Print all tables and their rows in a pretty format suitable for inline snapshots.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
conn: SQLite database connection
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
Formatted string representation of the database state
|
|
300
|
-
"""
|
|
301
|
-
cursor = conn.cursor()
|
|
302
|
-
|
|
303
|
-
# Get all table names
|
|
304
|
-
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
305
|
-
tables = [row[0] for row in cursor.fetchall()]
|
|
306
|
-
|
|
307
|
-
output_lines = []
|
|
308
|
-
output_lines.append("=" * 80)
|
|
309
|
-
output_lines.append("DATABASE SNAPSHOT")
|
|
310
|
-
output_lines.append("=" * 80)
|
|
311
|
-
|
|
312
|
-
for table_name in tables:
|
|
313
|
-
# Get column information
|
|
314
|
-
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
315
|
-
columns = cursor.fetchall()
|
|
316
|
-
column_names = [col[1] for col in columns]
|
|
317
|
-
column_types = {col[1]: col[2] for col in columns}
|
|
318
|
-
|
|
319
|
-
# Get all rows
|
|
320
|
-
cursor.execute(f"SELECT * FROM {table_name}")
|
|
321
|
-
rows = cursor.fetchall()
|
|
322
|
-
|
|
323
|
-
output_lines.append("")
|
|
324
|
-
output_lines.append(f"TABLE: {table_name}")
|
|
325
|
-
output_lines.append("-" * 80)
|
|
326
|
-
output_lines.append(f"Rows: {len(rows)}")
|
|
327
|
-
output_lines.append("")
|
|
328
|
-
|
|
329
|
-
if not rows:
|
|
330
|
-
output_lines.append(" (empty)")
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
# Format each row
|
|
334
|
-
for idx, row in enumerate(rows, 1):
|
|
335
|
-
output_lines.append(f" Row {idx}:")
|
|
336
|
-
|
|
337
|
-
for col_name, value in zip(column_names, row):
|
|
338
|
-
col_type = column_types[col_name]
|
|
339
|
-
formatted_value = format_value(value, col_name, col_type)
|
|
340
|
-
output_lines.append(f" {col_name:15} = {formatted_value}")
|
|
341
|
-
|
|
342
|
-
if idx < len(rows):
|
|
343
|
-
output_lines.append("")
|
|
344
|
-
|
|
345
|
-
output_lines.append("")
|
|
346
|
-
output_lines.append("=" * 80)
|
|
347
|
-
|
|
348
|
-
result = "\n".join(output_lines)
|
|
349
|
-
return result
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
async def aprint_sqlite_state(conn: anysqlite.Connection) -> str:
|
|
353
|
-
"""
|
|
354
|
-
Print all tables and their rows in a pretty format suitable for inline snapshots.
|
|
355
|
-
|
|
356
|
-
Args:
|
|
357
|
-
conn: SQLite database connection
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
Formatted string representation of the database state
|
|
361
|
-
"""
|
|
362
|
-
cursor = await conn.cursor()
|
|
363
|
-
|
|
364
|
-
# Get all table names
|
|
365
|
-
await cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
366
|
-
tables = [row[0] for row in await cursor.fetchall()]
|
|
367
|
-
|
|
368
|
-
output_lines = []
|
|
369
|
-
output_lines.append("=" * 80)
|
|
370
|
-
output_lines.append("DATABASE SNAPSHOT")
|
|
371
|
-
output_lines.append("=" * 80)
|
|
372
|
-
|
|
373
|
-
for table_name in tables:
|
|
374
|
-
# Get column information
|
|
375
|
-
await cursor.execute(f"PRAGMA table_info({table_name})")
|
|
376
|
-
columns = await cursor.fetchall()
|
|
377
|
-
column_names = [col[1] for col in columns]
|
|
378
|
-
column_types = {col[1]: col[2] for col in columns}
|
|
379
|
-
|
|
380
|
-
# Get all rows
|
|
381
|
-
await cursor.execute(f"SELECT * FROM {table_name}")
|
|
382
|
-
rows = await cursor.fetchall()
|
|
383
|
-
|
|
384
|
-
output_lines.append("")
|
|
385
|
-
output_lines.append(f"TABLE: {table_name}")
|
|
386
|
-
output_lines.append("-" * 80)
|
|
387
|
-
output_lines.append(f"Rows: {len(rows)}")
|
|
388
|
-
output_lines.append("")
|
|
389
|
-
|
|
390
|
-
if not rows:
|
|
391
|
-
output_lines.append(" (empty)")
|
|
392
|
-
continue
|
|
393
|
-
|
|
394
|
-
# Format each row
|
|
395
|
-
for idx, row in enumerate(rows, 1):
|
|
396
|
-
output_lines.append(f" Row {idx}:")
|
|
397
|
-
|
|
398
|
-
for col_name, value in zip(column_names, row):
|
|
399
|
-
col_type = column_types[col_name]
|
|
400
|
-
formatted_value = format_value(value, col_name, col_type)
|
|
401
|
-
output_lines.append(f" {col_name:15} = {formatted_value}")
|
|
402
|
-
|
|
403
|
-
if idx < len(rows):
|
|
404
|
-
output_lines.append("")
|
|
405
|
-
|
|
406
|
-
output_lines.append("")
|
|
407
|
-
output_lines.append("=" * 80)
|
|
408
|
-
|
|
409
|
-
result = "\n".join(output_lines)
|
|
410
|
-
return result
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def format_value(value: Any, col_name: str, col_type: str) -> str:
|
|
414
|
-
"""Format a value for display based on its type and column name."""
|
|
415
|
-
|
|
416
|
-
if value is None:
|
|
417
|
-
return "NULL"
|
|
418
|
-
|
|
419
|
-
# Handle BLOB columns
|
|
420
|
-
if col_type.upper() == "BLOB":
|
|
421
|
-
if isinstance(value, bytes):
|
|
422
|
-
# Try to decode as UTF-8 string first
|
|
423
|
-
try:
|
|
424
|
-
decoded = value.decode("utf-8")
|
|
425
|
-
# Check if it looks like JSON
|
|
426
|
-
if decoded.strip().startswith("{") or decoded.strip().startswith("["):
|
|
427
|
-
try:
|
|
428
|
-
parsed = json.loads(decoded)
|
|
429
|
-
return f"(JSON) {json.dumps(parsed, indent=2)}"
|
|
430
|
-
except json.JSONDecodeError:
|
|
431
|
-
pass
|
|
432
|
-
# Show string if it's printable
|
|
433
|
-
if all(32 <= ord(c) <= 126 or c in "\n\r\t" for c in decoded):
|
|
434
|
-
return f"(str) '{decoded}'"
|
|
435
|
-
except UnicodeDecodeError:
|
|
436
|
-
pass
|
|
437
|
-
|
|
438
|
-
# Show hex representation for binary data
|
|
439
|
-
hex_str = value.hex()
|
|
440
|
-
if len(hex_str) > 64:
|
|
441
|
-
return f"(bytes) 0x{hex_str[:60]}... ({len(value)} bytes)"
|
|
442
|
-
return f"(bytes) 0x{hex_str} ({len(value)} bytes)"
|
|
443
|
-
return repr(value)
|
|
444
|
-
|
|
445
|
-
# Handle timestamps - ONLY show date, not the raw timestamp
|
|
446
|
-
if col_name.endswith("_at") and isinstance(value, (int, float)):
|
|
447
|
-
try:
|
|
448
|
-
dt = date.fromtimestamp(value)
|
|
449
|
-
return dt.isoformat() # Changed: removed the timestamp prefix
|
|
450
|
-
except (ValueError, OSError):
|
|
451
|
-
return str(value)
|
|
452
|
-
|
|
453
|
-
# Handle TEXT columns
|
|
454
|
-
if col_type.upper() == "TEXT":
|
|
455
|
-
return f"'{value}'"
|
|
456
|
-
|
|
457
|
-
# Handle other types
|
|
458
|
-
return str(value)
|
|
@@ -4,16 +4,23 @@ import ssl
|
|
|
4
4
|
import typing as t
|
|
5
5
|
from typing import AsyncIterator, Iterable, Iterator, Union, overload
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
from hishel.
|
|
10
|
-
from hishel.
|
|
11
|
-
from hishel.beta._core._base._storages._base import AsyncBaseStorage, SyncBaseStorage
|
|
12
|
-
from hishel.beta._core._spec import (
|
|
7
|
+
from hishel import Headers, Request, Response
|
|
8
|
+
from hishel._async_cache import AsyncCacheProxy
|
|
9
|
+
from hishel._core._base._storages._base import AsyncBaseStorage, SyncBaseStorage
|
|
10
|
+
from hishel._core._spec import (
|
|
13
11
|
CacheOptions,
|
|
14
12
|
)
|
|
15
|
-
from hishel.
|
|
16
|
-
from hishel.
|
|
13
|
+
from hishel._core.models import AnyIterable
|
|
14
|
+
from hishel._sync_cache import SyncCacheProxy
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import httpx
|
|
18
|
+
except ImportError as e:
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"httpx is required to use hishel.httpx module. "
|
|
21
|
+
"Please install hishel with the 'httpx' extra, "
|
|
22
|
+
"e.g., 'pip install hishel[httpx]'."
|
|
23
|
+
) from e
|
|
17
24
|
|
|
18
25
|
SOCKET_OPTION = t.Union[
|
|
19
26
|
t.Tuple[int, int, int],
|
|
@@ -5,12 +5,12 @@ 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
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hishel
|
|
3
|
+
Version: 1.0.0.dev1
|
|
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: msgpack>=1.1.2
|
|
28
|
+
Requires-Dist: typing-extensions>=4.14.1
|
|
29
|
+
Provides-Extra: async
|
|
30
|
+
Requires-Dist: anyio>=4.9.0; extra == 'async'
|
|
31
|
+
Requires-Dist: anysqlite>=0.0.5; extra == 'async'
|
|
32
|
+
Provides-Extra: httpx
|
|
33
|
+
Requires-Dist: anyio>=4.9.0; extra == 'httpx'
|
|
34
|
+
Requires-Dist: anysqlite>=0.0.5; extra == 'httpx'
|
|
35
|
+
Requires-Dist: httpx>=0.28.1; extra == 'httpx'
|
|
36
|
+
Provides-Extra: requests
|
|
37
|
+
Requires-Dist: requests>=2.32.5; extra == 'requests'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_yellow.png#gh-dark-mode-only">
|
|
42
|
+
<img alt="Hishel Logo" width="350" src="https://raw.githubusercontent.com/karpetrosyan/hishel/master/docs/static/Shelkopryad_350x250_black.png#gh-light-mode-only">
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h1 align="center">Hishel</h1>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<strong>Elegant HTTP Caching for Python</strong>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<a href="https://pypi.org/project/hishel">
|
|
53
|
+
<img src="https://img.shields.io/pypi/v/hishel.svg" alt="PyPI version">
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://pypi.org/project/hishel">
|
|
56
|
+
<img src="https://img.shields.io/pypi/pyversions/hishel.svg" alt="Python versions">
|
|
57
|
+
</a>
|
|
58
|
+
<a href="https://github.com/karpetrosyan/hishel/blob/master/LICENSE">
|
|
59
|
+
<img src="https://img.shields.io/pypi/l/hishel" alt="License">
|
|
60
|
+
</a>
|
|
61
|
+
<a href="https://coveralls.io/github/karpetrosyan/hishel">
|
|
62
|
+
<img src="https://img.shields.io/coverallsCoverage/github/karpetrosyan/hishel" alt="Coverage">
|
|
63
|
+
</a>
|
|
64
|
+
<a href="https://static.pepy.tech/badge/hishel/month">
|
|
65
|
+
<img src="https://static.pepy.tech/badge/hishel/month" alt="Downloads">
|
|
66
|
+
</a>
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
**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.
|
|
72
|
+
|
|
73
|
+
## ✨ Features
|
|
74
|
+
|
|
75
|
+
- 🎯 **RFC 9111 Compliant** - Fully compliant with the latest HTTP caching specification
|
|
76
|
+
- 🔌 **Easy Integration** - Drop-in support for HTTPX and Requests
|
|
77
|
+
- 💾 **Flexible Storage** - SQLite backend with more coming soon
|
|
78
|
+
- ⚡ **High Performance** - Efficient caching with minimal overhead
|
|
79
|
+
- 🔄 **Async & Sync** - Full support for both synchronous and asynchronous workflows
|
|
80
|
+
- 🎨 **Type Safe** - Fully typed with comprehensive type hints
|
|
81
|
+
- 🧪 **Well Tested** - Extensive test coverage and battle-tested
|
|
82
|
+
- 🎛️ **Configurable** - Fine-grained control over caching behavior
|
|
83
|
+
- 🌐 **Future Ready** - Designed for easy integration with any HTTP client/server
|
|
84
|
+
|
|
85
|
+
## 📦 Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install hishel
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Optional Dependencies
|
|
92
|
+
|
|
93
|
+
Install with specific HTTP client support:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install hishel[httpx] # For HTTPX support
|
|
97
|
+
pip install hishel[requests] # For Requests support
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or install both:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install hishel[httpx,requests]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 🚀 Quick Start
|
|
107
|
+
|
|
108
|
+
### With HTTPX
|
|
109
|
+
|
|
110
|
+
**Synchronous:**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from hishel.httpx import SyncCacheClient
|
|
114
|
+
|
|
115
|
+
client = SyncCacheClient()
|
|
116
|
+
|
|
117
|
+
# First request - fetches from origin
|
|
118
|
+
response = client.get("https://api.example.com/data")
|
|
119
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
120
|
+
|
|
121
|
+
# Second request - served from cache
|
|
122
|
+
response = client.get("https://api.example.com/data")
|
|
123
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Asynchronous:**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from hishel.httpx import AsyncCacheClient
|
|
130
|
+
|
|
131
|
+
async with AsyncCacheClient() as client:
|
|
132
|
+
# First request - fetches from origin
|
|
133
|
+
response = await client.get("https://api.example.com/data")
|
|
134
|
+
print(response.extensions["hishel_from_cache"]) # False
|
|
135
|
+
|
|
136
|
+
# Second request - served from cache
|
|
137
|
+
response = await client.get("https://api.example.com/data")
|
|
138
|
+
print(response.extensions["hishel_from_cache"]) # True
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### With Requests
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import requests
|
|
145
|
+
from hishel.requests import CacheAdapter
|
|
146
|
+
|
|
147
|
+
session = requests.Session()
|
|
148
|
+
session.mount("https://", CacheAdapter())
|
|
149
|
+
session.mount("http://", CacheAdapter())
|
|
150
|
+
|
|
151
|
+
# First request - fetches from origin
|
|
152
|
+
response = session.get("https://api.example.com/data")
|
|
153
|
+
|
|
154
|
+
# Second request - served from cache
|
|
155
|
+
response = session.get("https://api.example.com/data")
|
|
156
|
+
print(response.headers.get("X-Hishel-From-Cache")) # "True"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 🎛️ Advanced Configuration
|
|
160
|
+
|
|
161
|
+
### Custom Cache Options
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from hishel import CacheOptions
|
|
165
|
+
from hishel.httpx import SyncCacheClient
|
|
166
|
+
|
|
167
|
+
client = SyncCacheClient(
|
|
168
|
+
cache_options=CacheOptions(
|
|
169
|
+
shared=False, # Use as private cache (browser-like)
|
|
170
|
+
supported_methods=["GET", "HEAD", "POST"], # Cache GET, HEAD, and POST
|
|
171
|
+
allow_stale=True # Allow serving stale responses
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Custom Storage Backend
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from hishel import SyncSqliteStorage
|
|
180
|
+
from hishel.httpx import SyncCacheClient
|
|
181
|
+
|
|
182
|
+
storage = SyncSqliteStorage(
|
|
183
|
+
database_path="my_cache.db",
|
|
184
|
+
default_ttl=7200.0, # Cache entries expire after 2 hours
|
|
185
|
+
refresh_ttl_on_access=True # Reset TTL when accessing cached entries
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
client = SyncCacheClient(storage=storage)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 🏗️ Architecture
|
|
192
|
+
|
|
193
|
+
Hishel uses a **sans-I/O state machine** architecture that separates HTTP caching logic from I/O operations:
|
|
194
|
+
|
|
195
|
+
- ✅ **Correct** - Fully RFC 9111 compliant
|
|
196
|
+
- ✅ **Testable** - Easy to test without network dependencies
|
|
197
|
+
- ✅ **Flexible** - Works with any HTTP client or server
|
|
198
|
+
- ✅ **Type Safe** - Clear state transitions with full type hints
|
|
199
|
+
|
|
200
|
+
## 🔮 Roadmap
|
|
201
|
+
|
|
202
|
+
While Hishel currently supports HTTPX and Requests, we're actively working on:
|
|
203
|
+
|
|
204
|
+
- 🎯 Additional HTTP client integrations
|
|
205
|
+
- 🎯 Server-side caching support
|
|
206
|
+
- 🎯 More storage backends
|
|
207
|
+
- 🎯 Advanced caching strategies
|
|
208
|
+
- 🎯 Performance optimizations
|
|
209
|
+
|
|
210
|
+
## 📚 Documentation
|
|
211
|
+
|
|
212
|
+
Comprehensive documentation is available at [https://hishel.com/dev](https://hishel.com/dev)
|
|
213
|
+
|
|
214
|
+
- [Getting Started](https://hishel.com)
|
|
215
|
+
- [HTTPX Integration](https://hishel.com/dev/integrations/httpx)
|
|
216
|
+
- [Requests Integration](https://hishel.com/dev/integrations/requests)
|
|
217
|
+
- [Storage Backends](https://hishel.com/dev/storages)
|
|
218
|
+
- [RFC 9111 Specification](https://hishel.com/dev/specification)
|
|
219
|
+
|
|
220
|
+
## 🤝 Contributing
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
See our [Contributing Guide](https://hishel.com/dev/contributing) for more details.
|
|
225
|
+
|
|
226
|
+
## 📄 License
|
|
227
|
+
|
|
228
|
+
This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
|
|
229
|
+
|
|
230
|
+
## 💖 Support
|
|
231
|
+
|
|
232
|
+
If you find Hishel useful, please consider:
|
|
233
|
+
|
|
234
|
+
- ⭐ Starring the repository
|
|
235
|
+
- 🐛 Reporting bugs and issues
|
|
236
|
+
- 💡 Suggesting new features
|
|
237
|
+
- 📖 Improving documentation
|
|
238
|
+
- ☕ [Buying me a coffee](https://buymeacoffee.com/karpetrosyan)
|
|
239
|
+
|
|
240
|
+
## 🙏 Acknowledgments
|
|
241
|
+
|
|
242
|
+
Hishel is inspired by and builds upon the excellent work in the Python HTTP ecosystem, particularly:
|
|
243
|
+
|
|
244
|
+
- [HTTPX](https://github.com/encode/httpx) - A next-generation HTTP client for Python
|
|
245
|
+
- [Requests](https://github.com/psf/requests) - The classic HTTP library for Python
|
|
246
|
+
- [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html) - HTTP Caching specification
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
<p align="center">
|
|
251
|
+
<strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
|
|
252
|
+
</p>
|
|
253
|
+
|
|
254
|
+
# Changelog
|
|
255
|
+
|
|
256
|
+
All notable changes to this project will be documented in this file.
|
|
257
|
+
|
|
258
|
+
## 1.0.0dev1 - 2025-10-21
|
|
259
|
+
### <!-- 7 -->⚙️ Miscellaneous Tasks
|
|
260
|
+
- Remove some redundant utils methods
|
|
261
|
+
|
|
262
|
+
## 1.0.0.dev0 - 2025-10-19
|
|
263
|
+
### <!-- 7 -->⚙️ Miscellaneous Tasks
|
|
264
|
+
- Use mike powered versioning
|
|
265
|
+
- Improve docs versioning, deploy dev doc on ci
|
|
266
|
+
|
|
267
|
+
## 0.1.5 - 2025-10-18
|
|
268
|
+
### <!-- 0 -->🚀 Features
|
|
269
|
+
- Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
270
|
+
- Better cache-control parsing
|
|
271
|
+
- Add close method to storages API (#384)
|
|
272
|
+
- Increase requests buffer size to 128KB, disable charset detection
|
|
273
|
+
|
|
274
|
+
### <!-- 1 -->🐛 Bug Fixes
|
|
275
|
+
- Fix some line breaks
|
|
276
|
+
|
|
277
|
+
### <!-- 7 -->⚙️ Miscellaneous Tasks
|
|
278
|
+
- Remove some redundant files from repo
|
|
279
|
+
|
|
280
|
+
## 0.1.4 - 2025-10-14
|
|
281
|
+
### <!-- 0 -->🚀 Features
|
|
282
|
+
- Add support for a sans-IO API (#366)
|
|
283
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
284
|
+
- Add sqlite storage for beta storages
|
|
285
|
+
- Get rid of some locks from sqlite storage
|
|
286
|
+
- Better async implemetation for sqlite storage
|
|
287
|
+
|
|
288
|
+
### <!-- 1 -->🐛 Bug Fixes
|
|
289
|
+
- Create an sqlite file in a cache folder
|
|
290
|
+
- Fix beta imports
|
|
291
|
+
|
|
292
|
+
### <!-- 7 -->⚙️ Miscellaneous Tasks
|
|
293
|
+
- Improve CI (#369)
|
|
294
|
+
- Remove src folder (#373)
|
|
295
|
+
- Temporary remove python3.14 from CI
|
|
296
|
+
- Add sqlite tests for new storage
|
|
297
|
+
- Move some tests to beta
|
|
298
|
+
|
|
@@ -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=hQURFuNdS7sjHeVGu12MQzJewjFdnVrdOki3CjBfZvs,6162
|
|
5
|
+
hishel/httpx.py,sha256=vscNB426VIhh0f5qQVkGA_WpwVvaKxbI6gnsHrBA_D0,11226
|
|
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=QPbNtNMA7vYjpt8bSPFIZ4u4c3UCH6eDfUWnH6WprTU,17787
|
|
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=kvAcV2FttZstWEfCczBxj30-PYdj1y9lzh0x3hNusKY,17210
|
|
16
|
+
hishel-1.0.0.dev1.dist-info/METADATA,sha256=IW6_UTqzUl-k8Pcyil_6brlEgd-gKomu1OyeZxrN3dU,9727
|
|
17
|
+
hishel-1.0.0.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
hishel-1.0.0.dev1.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
19
|
+
hishel-1.0.0.dev1.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
|
-
)
|