hishel 1.0.0.dev1__py3-none-any.whl → 1.0.0.dev2__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/_async_cache.py +9 -2
- hishel/_core/_spec.py +120 -67
- hishel/_core/models.py +7 -4
- hishel/_sync_cache.py +9 -2
- hishel/_utils.py +1 -137
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev2.dist-info}/METADATA +40 -22
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev2.dist-info}/RECORD +9 -9
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev2.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev1.dist-info → hishel-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
hishel/_async_cache.py
CHANGED
|
@@ -25,7 +25,7 @@ from hishel import (
|
|
|
25
25
|
create_idle_state,
|
|
26
26
|
)
|
|
27
27
|
from hishel._core._spec import InvalidatePairs, vary_headers_match
|
|
28
|
-
from hishel._core.models import CompletePair
|
|
28
|
+
from hishel._core.models import CompletePair, ResponseMetadata
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger("hishel.integrations.clients")
|
|
31
31
|
|
|
@@ -90,7 +90,14 @@ class AsyncCacheProxy:
|
|
|
90
90
|
logger.debug(
|
|
91
91
|
"Found matching cached response for the request",
|
|
92
92
|
)
|
|
93
|
-
|
|
93
|
+
response_meta = ResponseMetadata(
|
|
94
|
+
hishel_spec_ignored=True,
|
|
95
|
+
hishel_from_cache=True,
|
|
96
|
+
hishel_created_at=pair.meta.created_at,
|
|
97
|
+
hishel_revalidated=False,
|
|
98
|
+
hishel_stored=False,
|
|
99
|
+
)
|
|
100
|
+
pair.response.metadata.update(response_meta) # type: ignore
|
|
94
101
|
await self._maybe_refresh_pair_ttl(pair)
|
|
95
102
|
return pair.response
|
|
96
103
|
|
hishel/_core/_spec.py
CHANGED
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from hishel._core._headers import Headers, Range, Vary, parse_cache_control
|
|
19
|
+
from hishel._core.models import ResponseMetadata
|
|
19
20
|
from hishel._utils import parse_date, partition
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
@@ -1412,16 +1413,11 @@ class IdleClient(State):
|
|
|
1412
1413
|
#
|
|
1413
1414
|
# The Age header informs the client how old the cached response is.
|
|
1414
1415
|
|
|
1415
|
-
# Mark all ready-to-use responses with metadata (for observability)
|
|
1416
|
-
for pair in ready_to_use:
|
|
1417
|
-
pair.response.metadata["hishel_from_cache"] = True # type: ignore
|
|
1418
|
-
|
|
1419
1416
|
# Use the most recent response (first in sorted list)
|
|
1420
1417
|
selected_pair = ready_to_use[0]
|
|
1421
1418
|
|
|
1422
1419
|
# Calculate current age and update the Age header
|
|
1423
1420
|
current_age = get_age(selected_pair.response)
|
|
1424
|
-
|
|
1425
1421
|
return FromCache(
|
|
1426
1422
|
pair=replace(
|
|
1427
1423
|
selected_pair,
|
|
@@ -1578,20 +1574,6 @@ class CacheMiss(State):
|
|
|
1578
1574
|
* an s-maxage response directive (if cache is shared)
|
|
1579
1575
|
* a status code that is defined as heuristically cacheable"
|
|
1580
1576
|
|
|
1581
|
-
Side Effects:
|
|
1582
|
-
------------
|
|
1583
|
-
Sets metadata flags on the response object:
|
|
1584
|
-
- hishel_spec_ignored: False (caching spec is being followed)
|
|
1585
|
-
- hishel_from_cache: False (response is from origin, not cache)
|
|
1586
|
-
- hishel_revalidated: True (if after_revalidation is True)
|
|
1587
|
-
- hishel_stored: True/False (whether response was stored)
|
|
1588
|
-
|
|
1589
|
-
Logging:
|
|
1590
|
-
-------
|
|
1591
|
-
When a response cannot be stored, detailed debug logs are emitted explaining
|
|
1592
|
-
which specific RFC requirement failed, with direct links to the relevant
|
|
1593
|
-
RFC sections.
|
|
1594
|
-
|
|
1595
1577
|
Examples:
|
|
1596
1578
|
--------
|
|
1597
1579
|
>>> # Cacheable response
|
|
@@ -1614,21 +1596,6 @@ class CacheMiss(State):
|
|
|
1614
1596
|
True
|
|
1615
1597
|
"""
|
|
1616
1598
|
|
|
1617
|
-
# ============================================================================
|
|
1618
|
-
# STEP 1: Set Response Metadata
|
|
1619
|
-
# ============================================================================
|
|
1620
|
-
# Initialize metadata flags to track the response lifecycle
|
|
1621
|
-
|
|
1622
|
-
response.metadata["hishel_spec_ignored"] = False # type: ignore
|
|
1623
|
-
# We are following the caching specification
|
|
1624
|
-
|
|
1625
|
-
response.metadata["hishel_from_cache"] = False # type: ignore
|
|
1626
|
-
# This response came from origin server, not cache
|
|
1627
|
-
|
|
1628
|
-
if self.after_revalidation:
|
|
1629
|
-
response.metadata["hishel_revalidated"] = True # type: ignore
|
|
1630
|
-
# Mark that this response is the result of a revalidation
|
|
1631
|
-
|
|
1632
1599
|
# ============================================================================
|
|
1633
1600
|
# STEP 2: Parse Cache-Control Directive
|
|
1634
1601
|
# ============================================================================
|
|
@@ -1723,11 +1690,14 @@ class CacheMiss(State):
|
|
|
1723
1690
|
#
|
|
1724
1691
|
# Requests with Authorization headers often contain user-specific data.
|
|
1725
1692
|
# Shared caches must be careful not to serve one user's data to another.
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1693
|
+
has_explicit_directive = (
|
|
1694
|
+
response_cache_control.public
|
|
1695
|
+
or response_cache_control.s_maxage is not None
|
|
1696
|
+
or response_cache_control.must_revalidate
|
|
1697
|
+
)
|
|
1698
|
+
can_cache_auth_request = (
|
|
1699
|
+
not self.options.shared or "authorization" not in request.headers or has_explicit_directive
|
|
1700
|
+
)
|
|
1731
1701
|
|
|
1732
1702
|
# CONDITION 7: Response Contains Required Caching Information
|
|
1733
1703
|
# RFC 9111 Section 3, paragraph 2.7:
|
|
@@ -1797,7 +1767,7 @@ class CacheMiss(State):
|
|
|
1797
1767
|
or not understands_how_to_cache
|
|
1798
1768
|
or not no_store_is_not_present
|
|
1799
1769
|
or not private_directive_allows_storing
|
|
1800
|
-
or not
|
|
1770
|
+
or not can_cache_auth_request
|
|
1801
1771
|
or not contains_required_component
|
|
1802
1772
|
):
|
|
1803
1773
|
# --------------------------------------------------------------------
|
|
@@ -1833,10 +1803,11 @@ class CacheMiss(State):
|
|
|
1833
1803
|
"Cannot store the response because the `private` response directive does not "
|
|
1834
1804
|
"allow shared caches to store it. See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.5.1"
|
|
1835
1805
|
)
|
|
1836
|
-
elif not
|
|
1806
|
+
elif not can_cache_auth_request:
|
|
1837
1807
|
logger.debug(
|
|
1838
|
-
"Cannot store the response because the
|
|
1839
|
-
"
|
|
1808
|
+
"Cannot store the response because the request contained an Authorization header "
|
|
1809
|
+
"and there was no explicit directive allowing shared caching. "
|
|
1810
|
+
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-5"
|
|
1840
1811
|
)
|
|
1841
1812
|
elif not contains_required_component:
|
|
1842
1813
|
logger.debug(
|
|
@@ -1844,10 +1815,9 @@ class CacheMiss(State):
|
|
|
1844
1815
|
"See: https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1"
|
|
1845
1816
|
)
|
|
1846
1817
|
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
return CouldNotBeStored(response=response, pair_id=pair_id, options=self.options)
|
|
1818
|
+
return CouldNotBeStored(
|
|
1819
|
+
response=response, pair_id=pair_id, options=self.options, after_revalidation=self.after_revalidation
|
|
1820
|
+
)
|
|
1851
1821
|
|
|
1852
1822
|
# --------------------------------------------------------------------
|
|
1853
1823
|
# Transition to: StoreAndUse
|
|
@@ -1856,9 +1826,6 @@ class CacheMiss(State):
|
|
|
1856
1826
|
|
|
1857
1827
|
logger.debug("Storing response in cache")
|
|
1858
1828
|
|
|
1859
|
-
# Mark response as stored
|
|
1860
|
-
response.metadata["hishel_stored"] = True # type: ignore
|
|
1861
|
-
|
|
1862
1829
|
# Remove headers that should not be stored
|
|
1863
1830
|
# RFC 9111 Section 3.1: Storing Header and Trailer Fields
|
|
1864
1831
|
# https://www.rfc-editor.org/rfc/rfc9111.html#section-3.1
|
|
@@ -1869,6 +1836,7 @@ class CacheMiss(State):
|
|
|
1869
1836
|
pair_id=pair_id,
|
|
1870
1837
|
response=cleaned_response,
|
|
1871
1838
|
options=self.options,
|
|
1839
|
+
after_revalidation=self.after_revalidation,
|
|
1872
1840
|
)
|
|
1873
1841
|
|
|
1874
1842
|
|
|
@@ -1929,7 +1897,9 @@ class NeedRevalidation(State):
|
|
|
1929
1897
|
The stored pairs that the request was sent for revalidation.
|
|
1930
1898
|
"""
|
|
1931
1899
|
|
|
1932
|
-
def next(
|
|
1900
|
+
def next(
|
|
1901
|
+
self, revalidation_response: Response
|
|
1902
|
+
) -> Union["NeedToBeUpdated", "InvalidatePairs", "CacheMiss", "FromCache"]:
|
|
1933
1903
|
"""
|
|
1934
1904
|
Handles the response to a conditional request and determines the next state.
|
|
1935
1905
|
|
|
@@ -2084,6 +2054,15 @@ class NeedRevalidation(State):
|
|
|
2084
2054
|
after_revalidation=True,
|
|
2085
2055
|
).next(revalidation_response, pair_id=self.revalidating_pairs[-1].id),
|
|
2086
2056
|
)
|
|
2057
|
+
elif revalidation_response.status_code // 100 == 3:
|
|
2058
|
+
# 3xx Redirects should have been followed by the HTTP client
|
|
2059
|
+
return FromCache(
|
|
2060
|
+
pair=replace(
|
|
2061
|
+
self.revalidating_pairs[-1],
|
|
2062
|
+
response=revalidation_response,
|
|
2063
|
+
),
|
|
2064
|
+
options=self.options,
|
|
2065
|
+
)
|
|
2087
2066
|
|
|
2088
2067
|
# ============================================================================
|
|
2089
2068
|
# STEP 4: Handle Unexpected Status Codes
|
|
@@ -2316,32 +2295,99 @@ class NeedRevalidation(State):
|
|
|
2316
2295
|
return next_state
|
|
2317
2296
|
|
|
2318
2297
|
|
|
2319
|
-
@dataclass
|
|
2298
|
+
# @dataclass
|
|
2299
|
+
# class StoreAndUse(State):
|
|
2300
|
+
# """
|
|
2301
|
+
# The state that indicates that the response can be stored in the cache and used.
|
|
2302
|
+
# """
|
|
2303
|
+
|
|
2304
|
+
# pair_id: uuid.UUID
|
|
2305
|
+
|
|
2306
|
+
# response: Response
|
|
2307
|
+
|
|
2308
|
+
# def next(self) -> None:
|
|
2309
|
+
# return None # pragma: nocover
|
|
2310
|
+
|
|
2311
|
+
|
|
2320
2312
|
class StoreAndUse(State):
|
|
2321
2313
|
"""
|
|
2322
2314
|
The state that indicates that the response can be stored in the cache and used.
|
|
2323
|
-
"""
|
|
2324
2315
|
|
|
2325
|
-
|
|
2316
|
+
Attributes:
|
|
2317
|
+
----------
|
|
2318
|
+
pair_id : uuid.UUID
|
|
2319
|
+
The unique identifier for the cache pair.
|
|
2320
|
+
response : Response
|
|
2321
|
+
The HTTP response to be stored in the cache.
|
|
2322
|
+
after_revalidation : bool
|
|
2323
|
+
Indicates if the storage is occurring after a revalidation process.
|
|
2324
|
+
"""
|
|
2326
2325
|
|
|
2327
|
-
|
|
2326
|
+
def __init__(
|
|
2327
|
+
self, pair_id: uuid.UUID, response: Response, options: CacheOptions, after_revalidation: bool = False
|
|
2328
|
+
) -> None:
|
|
2329
|
+
super().__init__(options)
|
|
2330
|
+
self.pair_id = pair_id
|
|
2331
|
+
self.response = response
|
|
2332
|
+
self.after_revalidation = after_revalidation
|
|
2333
|
+
response_meta = ResponseMetadata(
|
|
2334
|
+
hishel_created_at=time.time(),
|
|
2335
|
+
hishel_from_cache=False,
|
|
2336
|
+
hishel_spec_ignored=False,
|
|
2337
|
+
hishel_revalidated=after_revalidation,
|
|
2338
|
+
hishel_stored=True,
|
|
2339
|
+
)
|
|
2340
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2328
2341
|
|
|
2329
2342
|
def next(self) -> None:
|
|
2330
|
-
return None
|
|
2343
|
+
return None
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
# @dataclass
|
|
2347
|
+
# class CouldNotBeStored(State):
|
|
2348
|
+
# """
|
|
2349
|
+
# The state that indicates that the response could not be stored in the cache.
|
|
2350
|
+
# """
|
|
2351
|
+
|
|
2352
|
+
# response: Response
|
|
2353
|
+
|
|
2354
|
+
# pair_id: uuid.UUID
|
|
2355
|
+
|
|
2356
|
+
# def next(self) -> None:
|
|
2357
|
+
# return None # pragma: nocover
|
|
2331
2358
|
|
|
2332
2359
|
|
|
2333
|
-
@dataclass
|
|
2334
2360
|
class CouldNotBeStored(State):
|
|
2335
2361
|
"""
|
|
2336
2362
|
The state that indicates that the response could not be stored in the cache.
|
|
2337
|
-
"""
|
|
2338
2363
|
|
|
2339
|
-
|
|
2364
|
+
Attributes:
|
|
2365
|
+
----------
|
|
2366
|
+
response : Response
|
|
2367
|
+
The HTTP response that could not be stored.
|
|
2368
|
+
pair_id : uuid.UUID
|
|
2369
|
+
The unique identifier for the cache pair.
|
|
2370
|
+
after_revalidation : bool
|
|
2371
|
+
Indicates if the storage attempt occurred after a revalidation process.
|
|
2372
|
+
"""
|
|
2340
2373
|
|
|
2341
|
-
|
|
2374
|
+
def __init__(
|
|
2375
|
+
self, response: Response, pair_id: uuid.UUID, options: CacheOptions, after_revalidation: bool = False
|
|
2376
|
+
) -> None:
|
|
2377
|
+
super().__init__(options)
|
|
2378
|
+
self.response = response
|
|
2379
|
+
self.pair_id = pair_id
|
|
2380
|
+
response_meta = ResponseMetadata(
|
|
2381
|
+
hishel_created_at=time.time(),
|
|
2382
|
+
hishel_from_cache=False,
|
|
2383
|
+
hishel_spec_ignored=False,
|
|
2384
|
+
hishel_revalidated=after_revalidation,
|
|
2385
|
+
hishel_stored=False,
|
|
2386
|
+
)
|
|
2387
|
+
self.response.metadata.update(response_meta) # type: ignore
|
|
2342
2388
|
|
|
2343
2389
|
def next(self) -> None:
|
|
2344
|
-
return None
|
|
2390
|
+
return None
|
|
2345
2391
|
|
|
2346
2392
|
|
|
2347
2393
|
@dataclass
|
|
@@ -2358,15 +2404,22 @@ class InvalidatePairs(State):
|
|
|
2358
2404
|
return self.next_state
|
|
2359
2405
|
|
|
2360
2406
|
|
|
2361
|
-
@dataclass
|
|
2362
2407
|
class FromCache(State):
|
|
2363
|
-
pair: CompletePair
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2408
|
+
def __init__(self, pair: CompletePair, options: CacheOptions, after_revalidation: bool = False) -> None:
|
|
2409
|
+
super().__init__(options)
|
|
2410
|
+
self.pair = pair
|
|
2411
|
+
self.after_revalidation = after_revalidation
|
|
2412
|
+
response_meta = ResponseMetadata(
|
|
2413
|
+
hishel_created_at=pair.meta.created_at,
|
|
2414
|
+
hishel_from_cache=True,
|
|
2415
|
+
hishel_spec_ignored=False,
|
|
2416
|
+
hishel_revalidated=after_revalidation,
|
|
2417
|
+
hishel_stored=False,
|
|
2418
|
+
)
|
|
2419
|
+
self.pair.response.metadata.update(response_meta) # type: ignore
|
|
2367
2420
|
|
|
2368
2421
|
def next(self) -> None:
|
|
2369
|
-
return None
|
|
2422
|
+
return None
|
|
2370
2423
|
|
|
2371
2424
|
|
|
2372
2425
|
@dataclass
|
hishel/_core/models.py
CHANGED
|
@@ -109,18 +109,21 @@ class Request:
|
|
|
109
109
|
|
|
110
110
|
class ResponseMetadata(TypedDict, total=False):
|
|
111
111
|
# All the names here should be prefixed with "hishel_" to avoid collisions with user data
|
|
112
|
-
hishel_from_cache: bool
|
|
112
|
+
hishel_from_cache: bool
|
|
113
113
|
"""Indicates whether the response was served from cache."""
|
|
114
114
|
|
|
115
|
-
hishel_revalidated: bool
|
|
115
|
+
hishel_revalidated: bool
|
|
116
116
|
"""Indicates whether the response was revalidated with the origin server."""
|
|
117
117
|
|
|
118
|
-
hishel_spec_ignored: bool
|
|
118
|
+
hishel_spec_ignored: bool
|
|
119
119
|
"""Indicates whether the caching specification was ignored for this response."""
|
|
120
120
|
|
|
121
|
-
hishel_stored: bool
|
|
121
|
+
hishel_stored: bool
|
|
122
122
|
"""Indicates whether the response was stored in cache."""
|
|
123
123
|
|
|
124
|
+
hishel_created_at: float
|
|
125
|
+
"""Timestamp when the response was cached."""
|
|
126
|
+
|
|
124
127
|
|
|
125
128
|
@dataclass
|
|
126
129
|
class Response:
|
hishel/_sync_cache.py
CHANGED
|
@@ -25,7 +25,7 @@ from hishel import (
|
|
|
25
25
|
create_idle_state,
|
|
26
26
|
)
|
|
27
27
|
from hishel._core._spec import InvalidatePairs, vary_headers_match
|
|
28
|
-
from hishel._core.models import CompletePair
|
|
28
|
+
from hishel._core.models import CompletePair, ResponseMetadata
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger("hishel.integrations.clients")
|
|
31
31
|
|
|
@@ -90,7 +90,14 @@ class SyncCacheProxy:
|
|
|
90
90
|
logger.debug(
|
|
91
91
|
"Found matching cached response for the request",
|
|
92
92
|
)
|
|
93
|
-
|
|
93
|
+
response_meta = ResponseMetadata(
|
|
94
|
+
hishel_spec_ignored=True,
|
|
95
|
+
hishel_from_cache=True,
|
|
96
|
+
hishel_created_at=pair.meta.created_at,
|
|
97
|
+
hishel_revalidated=False,
|
|
98
|
+
hishel_stored=False,
|
|
99
|
+
)
|
|
100
|
+
pair.response.metadata.update(response_meta) # type: ignore
|
|
94
101
|
self._maybe_refresh_pair_ttl(pair)
|
|
95
102
|
return pair.response
|
|
96
103
|
|
hishel/_utils.py
CHANGED
|
@@ -1,107 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import calendar
|
|
4
|
-
import hashlib
|
|
5
4
|
import time
|
|
6
5
|
import typing as tp
|
|
7
6
|
from email.utils import parsedate_tz
|
|
8
|
-
from typing import AsyncIterator,
|
|
9
|
-
|
|
10
|
-
import httpcore
|
|
11
|
-
import httpx
|
|
7
|
+
from typing import AsyncIterator, Iterable, Iterator
|
|
12
8
|
|
|
13
9
|
HEADERS_ENCODING = "iso-8859-1"
|
|
14
10
|
|
|
15
11
|
T = tp.TypeVar("T")
|
|
16
12
|
|
|
17
13
|
|
|
18
|
-
class BaseClock:
|
|
19
|
-
def now(self) -> int:
|
|
20
|
-
raise NotImplementedError()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Clock(BaseClock):
|
|
24
|
-
def now(self) -> int:
|
|
25
|
-
return int(time.time())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
|
|
29
|
-
if isinstance(url, str): # pragma: no cover
|
|
30
|
-
return url
|
|
31
|
-
|
|
32
|
-
if isinstance(url, bytes): # pragma: no cover
|
|
33
|
-
return url.decode("ascii")
|
|
34
|
-
|
|
35
|
-
if isinstance(url, httpcore.URL):
|
|
36
|
-
port = f":{url.port}" if url.port is not None else ""
|
|
37
|
-
return f"{url.scheme.decode('ascii')}://{url.host.decode('ascii')}{port}{url.target.decode('ascii')}"
|
|
38
|
-
assert False, "Invalid type for `normalized_url`" # pragma: no cover
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def get_safe_url(url: httpcore.URL) -> str:
|
|
42
|
-
httpx_url = httpx.URL(bytes(url).decode("ascii"))
|
|
43
|
-
|
|
44
|
-
schema = httpx_url.scheme
|
|
45
|
-
host = httpx_url.host
|
|
46
|
-
path = httpx_url.path
|
|
47
|
-
|
|
48
|
-
return f"{schema}://{host}{path}"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
|
|
52
|
-
encoded_url = normalized_url(request.url).encode("ascii")
|
|
53
|
-
|
|
54
|
-
key_parts = [request.method, encoded_url, body]
|
|
55
|
-
|
|
56
|
-
# FIPs mode disables blake2 algorithm, use sha256 instead when not found.
|
|
57
|
-
blake2b_hasher = None
|
|
58
|
-
sha256_hasher = hashlib.sha256(usedforsecurity=False)
|
|
59
|
-
try:
|
|
60
|
-
blake2b_hasher = hashlib.blake2b(digest_size=16, usedforsecurity=False)
|
|
61
|
-
except (ValueError, TypeError, AttributeError):
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
hexdigest: str
|
|
65
|
-
if blake2b_hasher:
|
|
66
|
-
for part in key_parts:
|
|
67
|
-
blake2b_hasher.update(part)
|
|
68
|
-
|
|
69
|
-
hexdigest = blake2b_hasher.hexdigest()
|
|
70
|
-
else:
|
|
71
|
-
for part in key_parts:
|
|
72
|
-
sha256_hasher.update(part)
|
|
73
|
-
|
|
74
|
-
hexdigest = sha256_hasher.hexdigest()
|
|
75
|
-
return hexdigest
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def extract_header_values(
|
|
79
|
-
headers: tp.List[tp.Tuple[bytes, bytes]],
|
|
80
|
-
header_key: tp.Union[bytes, str],
|
|
81
|
-
single: bool = False,
|
|
82
|
-
) -> tp.List[bytes]:
|
|
83
|
-
if isinstance(header_key, str):
|
|
84
|
-
header_key = header_key.encode(HEADERS_ENCODING)
|
|
85
|
-
extracted_headers = []
|
|
86
|
-
for key, value in headers:
|
|
87
|
-
if key.lower() == header_key.lower():
|
|
88
|
-
extracted_headers.append(value)
|
|
89
|
-
if single:
|
|
90
|
-
break
|
|
91
|
-
return extracted_headers
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def extract_header_values_decoded(
|
|
95
|
-
headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes, single: bool = False
|
|
96
|
-
) -> tp.List[str]:
|
|
97
|
-
values = extract_header_values(headers=headers, header_key=header_key, single=single)
|
|
98
|
-
return [value.decode(HEADERS_ENCODING) for value in values]
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes) -> bool:
|
|
102
|
-
return bool(extract_header_values(headers, header_key, single=True))
|
|
103
|
-
|
|
104
|
-
|
|
105
14
|
def parse_date(date: str) -> tp.Optional[int]:
|
|
106
15
|
expires = parsedate_tz(date)
|
|
107
16
|
if expires is None:
|
|
@@ -114,10 +23,6 @@ def sleep(seconds: tp.Union[int, float]) -> None:
|
|
|
114
23
|
time.sleep(seconds)
|
|
115
24
|
|
|
116
25
|
|
|
117
|
-
def float_seconds_to_int_milliseconds(seconds: float) -> int:
|
|
118
|
-
return int(seconds * 1000)
|
|
119
|
-
|
|
120
|
-
|
|
121
26
|
def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp.Tuple[tp.List[T], tp.List[T]]:
|
|
122
27
|
"""
|
|
123
28
|
Partition an iterable into two lists: one for matching items and one for non-matching items.
|
|
@@ -175,44 +80,3 @@ def snake_to_header(text: str) -> str:
|
|
|
175
80
|
"""
|
|
176
81
|
# Split by underscore, capitalize each word, join with dash, add X- prefix
|
|
177
82
|
return "X-" + "-".join(word.capitalize() for word in text.split("_"))
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
_T = TypeVar("_T")
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
class GeneratorWithReturnValue:
|
|
184
|
-
def __init__(
|
|
185
|
-
self, gen: Generator[None, bytes | None, bytes], stream: AsyncIterator[bytes] | Iterator[bytes]
|
|
186
|
-
) -> None:
|
|
187
|
-
self.gen = gen
|
|
188
|
-
self.stream = stream
|
|
189
|
-
self.value: bytes | None = None
|
|
190
|
-
|
|
191
|
-
def __iter__(self) -> Iterator[bytes]:
|
|
192
|
-
return self
|
|
193
|
-
|
|
194
|
-
def __next__(self) -> bytes:
|
|
195
|
-
assert isinstance(self.stream, Iterator)
|
|
196
|
-
|
|
197
|
-
try:
|
|
198
|
-
chunk = next(self.stream)
|
|
199
|
-
self.gen.send(chunk)
|
|
200
|
-
except StopIteration as exc:
|
|
201
|
-
self.gen.send(None)
|
|
202
|
-
self.value = exc.value
|
|
203
|
-
raise
|
|
204
|
-
return chunk
|
|
205
|
-
|
|
206
|
-
def __aiter__(self) -> AsyncIterator[bytes]:
|
|
207
|
-
return self
|
|
208
|
-
|
|
209
|
-
async def __anext__(self) -> bytes:
|
|
210
|
-
assert isinstance(self.stream, AsyncIterator)
|
|
211
|
-
try:
|
|
212
|
-
chunk = await self.stream.__anext__()
|
|
213
|
-
self.gen.send(chunk)
|
|
214
|
-
except StopIteration as exc:
|
|
215
|
-
self.gen.send(None)
|
|
216
|
-
self.value = exc.value
|
|
217
|
-
raise
|
|
218
|
-
return chunk
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev2
|
|
4
4
|
Summary: Elegant HTTP Caching for Python
|
|
5
5
|
Project-URL: Homepage, https://hishel.com
|
|
6
6
|
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
@@ -255,44 +255,62 @@ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecos
|
|
|
255
255
|
|
|
256
256
|
All notable changes to this project will be documented in this file.
|
|
257
257
|
|
|
258
|
-
## 1.0.
|
|
259
|
-
###
|
|
258
|
+
## 1.0.0.dev2 - 2025-10-21
|
|
259
|
+
### ⚙️ Miscellaneous Tasks
|
|
260
|
+
- Remove redundant utils and tests
|
|
261
|
+
- Add import without extras check in ci
|
|
262
|
+
|
|
263
|
+
### 🐛 Bug Fixes
|
|
264
|
+
- Fix check for storing auth requests
|
|
265
|
+
- Don't raise an error on 3xx during revalidation
|
|
266
|
+
|
|
267
|
+
### 🚀 Features
|
|
268
|
+
- Add hishel_created_at response metadata
|
|
269
|
+
|
|
270
|
+
## 1.0.0.dev1 - 2025-10-21
|
|
271
|
+
### ⚙️ Miscellaneous Tasks
|
|
260
272
|
- Remove some redundant utils methods
|
|
261
273
|
|
|
274
|
+
### 📦 Dependencies
|
|
275
|
+
- Make httpx and async libs optional dependencies
|
|
276
|
+
- Make `anysqlite` optional dependency
|
|
277
|
+
- Install async extra with httpx
|
|
278
|
+
- Improve git-cliff
|
|
279
|
+
|
|
262
280
|
## 1.0.0.dev0 - 2025-10-19
|
|
263
|
-
###
|
|
281
|
+
### ⚙️ Miscellaneous Tasks
|
|
264
282
|
- Use mike powered versioning
|
|
265
283
|
- Improve docs versioning, deploy dev doc on ci
|
|
266
284
|
|
|
267
285
|
## 0.1.5 - 2025-10-18
|
|
268
|
-
###
|
|
286
|
+
### ⚙️ Miscellaneous Tasks
|
|
287
|
+
- Remove some redundant files from repo
|
|
288
|
+
|
|
289
|
+
### 🐛 Bug Fixes
|
|
290
|
+
- Fix some line breaks
|
|
291
|
+
|
|
292
|
+
### 🚀 Features
|
|
269
293
|
- Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
270
294
|
- Better cache-control parsing
|
|
271
295
|
- Add close method to storages API (#384)
|
|
272
296
|
- Increase requests buffer size to 128KB, disable charset detection
|
|
273
297
|
|
|
274
|
-
|
|
275
|
-
|
|
298
|
+
## 0.1.4 - 2025-10-14
|
|
299
|
+
### ⚙️ Miscellaneous Tasks
|
|
300
|
+
- Improve CI (#369)
|
|
301
|
+
- Remove src folder (#373)
|
|
302
|
+
- Temporary remove python3.14 from CI
|
|
303
|
+
- Add sqlite tests for new storage
|
|
304
|
+
- Move some tests to beta
|
|
276
305
|
|
|
277
|
-
###
|
|
278
|
-
-
|
|
306
|
+
### 🐛 Bug Fixes
|
|
307
|
+
- Create an sqlite file in a cache folder
|
|
308
|
+
- Fix beta imports
|
|
279
309
|
|
|
280
|
-
|
|
281
|
-
### <!-- 0 -->🚀 Features
|
|
310
|
+
### 🚀 Features
|
|
282
311
|
- Add support for a sans-IO API (#366)
|
|
283
312
|
- Allow already consumed streams with `CacheTransport` (#377)
|
|
284
313
|
- Add sqlite storage for beta storages
|
|
285
314
|
- Get rid of some locks from sqlite storage
|
|
286
315
|
- Better async implemetation for sqlite storage
|
|
287
316
|
|
|
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
|
-
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
hishel/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
2
|
-
hishel/_async_cache.py,sha256=
|
|
3
|
-
hishel/_sync_cache.py,sha256=
|
|
4
|
-
hishel/_utils.py,sha256=
|
|
2
|
+
hishel/_async_cache.py,sha256=1K5y369F2EqTnZEf9Wspq_rxKAlNPsKNa6WhDOreNeM,7109
|
|
3
|
+
hishel/_sync_cache.py,sha256=0pxnpb_27KRly5V-A8mSehUChavYFH5U8Az_sUiyo_M,6855
|
|
4
|
+
hishel/_utils.py,sha256=AAUMfTmXVZqvyc7_DvOI4OloamJlSQIR8qilf_ySFi8,2291
|
|
5
5
|
hishel/httpx.py,sha256=vscNB426VIhh0f5qQVkGA_WpwVvaKxbI6gnsHrBA_D0,11226
|
|
6
6
|
hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
hishel/requests.py,sha256=eiWcwCId04DucnquCsU12tj9cDZcn-cjZ9MYniVuNeo,6429
|
|
8
8
|
hishel/_core/__init__.py,sha256=byj_IhCjFMaBcp6R8iyRlQV-3R4uTfH44PQzB4lVe1g,1447
|
|
9
9
|
hishel/_core/_headers.py,sha256=ii4x2L6GoQFpqpgg28OtFh7p2DoM9mhE4q6CjW6xUWc,17473
|
|
10
|
-
hishel/_core/_spec.py,sha256=
|
|
11
|
-
hishel/_core/models.py,sha256
|
|
10
|
+
hishel/_core/_spec.py,sha256=yQmuJ-HOXTPNIooXr8PPlQUhIv_SEDkZocro_u89o_A,104179
|
|
11
|
+
hishel/_core/models.py,sha256=-09QfsdKHWyKwFqSZZf7qoUdTvtL1KL53c-KsBAtGuU,5571
|
|
12
12
|
hishel/_core/_async/_storages/_sqlite.py,sha256=QPbNtNMA7vYjpt8bSPFIZ4u4c3UCH6eDfUWnH6WprTU,17787
|
|
13
13
|
hishel/_core/_base/_storages/_base.py,sha256=xLJGTBlFK8DVrQMgRMtGXJnYRUmNB-iYkk7S-BtMx8s,8516
|
|
14
14
|
hishel/_core/_base/_storages/_packing.py,sha256=NFMpSvYYTDBNkzwpjj5l4w-JOPLc19oAEDqDEQJ7VZI,4873
|
|
15
15
|
hishel/_core/_sync/_storages/_sqlite.py,sha256=kvAcV2FttZstWEfCczBxj30-PYdj1y9lzh0x3hNusKY,17210
|
|
16
|
-
hishel-1.0.0.
|
|
17
|
-
hishel-1.0.0.
|
|
18
|
-
hishel-1.0.0.
|
|
19
|
-
hishel-1.0.0.
|
|
16
|
+
hishel-1.0.0.dev2.dist-info/METADATA,sha256=N0WGvzpWKP06XP5hWRHv_B6hgGm1sCsx8GSaGxjn_gs,10116
|
|
17
|
+
hishel-1.0.0.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
hishel-1.0.0.dev2.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
19
|
+
hishel-1.0.0.dev2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|