hishel 1.0.0.dev0__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/_async/_storages/_sqlite.py +402 -356
- hishel/_core/_spec.py +120 -67
- hishel/_core/_sync/_storages/_sqlite.py +402 -356
- hishel/_core/models.py +7 -4
- hishel/_sync_cache.py +9 -2
- hishel/_utils.py +1 -377
- hishel/httpx.py +9 -2
- {hishel-1.0.0.dev0.dist-info → hishel-1.0.0.dev2.dist-info}/METADATA +48 -53
- hishel-1.0.0.dev2.dist-info/RECORD +19 -0
- hishel-1.0.0.dev0.dist-info/RECORD +0 -19
- {hishel-1.0.0.dev0.dist-info → hishel-1.0.0.dev2.dist-info}/WHEEL +0 -0
- {hishel-1.0.0.dev0.dist-info → hishel-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
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,113 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import calendar
|
|
4
|
-
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import sqlite3
|
|
7
4
|
import time
|
|
8
5
|
import typing as tp
|
|
9
|
-
from datetime import date
|
|
10
6
|
from email.utils import parsedate_tz
|
|
11
|
-
from typing import
|
|
12
|
-
|
|
13
|
-
import anyio
|
|
14
|
-
import anysqlite
|
|
15
|
-
import httpcore
|
|
16
|
-
import httpx
|
|
17
|
-
from anyio import from_thread, to_thread
|
|
7
|
+
from typing import AsyncIterator, Iterable, Iterator
|
|
18
8
|
|
|
19
9
|
HEADERS_ENCODING = "iso-8859-1"
|
|
20
10
|
|
|
21
11
|
T = tp.TypeVar("T")
|
|
22
12
|
|
|
23
13
|
|
|
24
|
-
class BaseClock:
|
|
25
|
-
def now(self) -> int:
|
|
26
|
-
raise NotImplementedError()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Clock(BaseClock):
|
|
30
|
-
def now(self) -> int:
|
|
31
|
-
return int(time.time())
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
|
|
35
|
-
if isinstance(url, str): # pragma: no cover
|
|
36
|
-
return url
|
|
37
|
-
|
|
38
|
-
if isinstance(url, bytes): # pragma: no cover
|
|
39
|
-
return url.decode("ascii")
|
|
40
|
-
|
|
41
|
-
if isinstance(url, httpcore.URL):
|
|
42
|
-
port = f":{url.port}" if url.port is not None else ""
|
|
43
|
-
return f"{url.scheme.decode('ascii')}://{url.host.decode('ascii')}{port}{url.target.decode('ascii')}"
|
|
44
|
-
assert False, "Invalid type for `normalized_url`" # pragma: no cover
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_safe_url(url: httpcore.URL) -> str:
|
|
48
|
-
httpx_url = httpx.URL(bytes(url).decode("ascii"))
|
|
49
|
-
|
|
50
|
-
schema = httpx_url.scheme
|
|
51
|
-
host = httpx_url.host
|
|
52
|
-
path = httpx_url.path
|
|
53
|
-
|
|
54
|
-
return f"{schema}://{host}{path}"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
|
|
58
|
-
encoded_url = normalized_url(request.url).encode("ascii")
|
|
59
|
-
|
|
60
|
-
key_parts = [request.method, encoded_url, body]
|
|
61
|
-
|
|
62
|
-
# FIPs mode disables blake2 algorithm, use sha256 instead when not found.
|
|
63
|
-
blake2b_hasher = None
|
|
64
|
-
sha256_hasher = hashlib.sha256(usedforsecurity=False)
|
|
65
|
-
try:
|
|
66
|
-
blake2b_hasher = hashlib.blake2b(digest_size=16, usedforsecurity=False)
|
|
67
|
-
except (ValueError, TypeError, AttributeError):
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
hexdigest: str
|
|
71
|
-
if blake2b_hasher:
|
|
72
|
-
for part in key_parts:
|
|
73
|
-
blake2b_hasher.update(part)
|
|
74
|
-
|
|
75
|
-
hexdigest = blake2b_hasher.hexdigest()
|
|
76
|
-
else:
|
|
77
|
-
for part in key_parts:
|
|
78
|
-
sha256_hasher.update(part)
|
|
79
|
-
|
|
80
|
-
hexdigest = sha256_hasher.hexdigest()
|
|
81
|
-
return hexdigest
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def extract_header_values(
|
|
85
|
-
headers: tp.List[tp.Tuple[bytes, bytes]],
|
|
86
|
-
header_key: tp.Union[bytes, str],
|
|
87
|
-
single: bool = False,
|
|
88
|
-
) -> tp.List[bytes]:
|
|
89
|
-
if isinstance(header_key, str):
|
|
90
|
-
header_key = header_key.encode(HEADERS_ENCODING)
|
|
91
|
-
extracted_headers = []
|
|
92
|
-
for key, value in headers:
|
|
93
|
-
if key.lower() == header_key.lower():
|
|
94
|
-
extracted_headers.append(value)
|
|
95
|
-
if single:
|
|
96
|
-
break
|
|
97
|
-
return extracted_headers
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def extract_header_values_decoded(
|
|
101
|
-
headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes, single: bool = False
|
|
102
|
-
) -> tp.List[str]:
|
|
103
|
-
values = extract_header_values(headers=headers, header_key=header_key, single=single)
|
|
104
|
-
return [value.decode(HEADERS_ENCODING) for value in values]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def header_presents(headers: tp.List[tp.Tuple[bytes, bytes]], header_key: bytes) -> bool:
|
|
108
|
-
return bool(extract_header_values(headers, header_key, single=True))
|
|
109
|
-
|
|
110
|
-
|
|
111
14
|
def parse_date(date: str) -> tp.Optional[int]:
|
|
112
15
|
expires = parsedate_tz(date)
|
|
113
16
|
if expires is None:
|
|
@@ -116,18 +19,10 @@ def parse_date(date: str) -> tp.Optional[int]:
|
|
|
116
19
|
return timestamp
|
|
117
20
|
|
|
118
21
|
|
|
119
|
-
async def asleep(seconds: tp.Union[int, float]) -> None:
|
|
120
|
-
await anyio.sleep(seconds)
|
|
121
|
-
|
|
122
|
-
|
|
123
22
|
def sleep(seconds: tp.Union[int, float]) -> None:
|
|
124
23
|
time.sleep(seconds)
|
|
125
24
|
|
|
126
25
|
|
|
127
|
-
def float_seconds_to_int_milliseconds(seconds: float) -> int:
|
|
128
|
-
return int(seconds * 1000)
|
|
129
|
-
|
|
130
|
-
|
|
131
26
|
def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp.Tuple[tp.List[T], tp.List[T]]:
|
|
132
27
|
"""
|
|
133
28
|
Partition an iterable into two lists: one for matching items and one for non-matching items.
|
|
@@ -155,66 +50,6 @@ def partition(iterable: tp.Iterable[T], predicate: tp.Callable[[T], bool]) -> tp
|
|
|
155
50
|
return matching, non_matching
|
|
156
51
|
|
|
157
52
|
|
|
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
53
|
async def make_async_iterator(iterable: Iterable[bytes]) -> AsyncIterator[bytes]:
|
|
219
54
|
for item in iterable:
|
|
220
55
|
yield item
|
|
@@ -245,214 +80,3 @@ def snake_to_header(text: str) -> str:
|
|
|
245
80
|
"""
|
|
246
81
|
# Split by underscore, capitalize each word, join with dash, add X- prefix
|
|
247
82
|
return "X-" + "-".join(word.capitalize() for word in text.split("_"))
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
_T = TypeVar("_T")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
class GeneratorWithReturnValue:
|
|
254
|
-
def __init__(
|
|
255
|
-
self, gen: Generator[None, bytes | None, bytes], stream: AsyncIterator[bytes] | Iterator[bytes]
|
|
256
|
-
) -> None:
|
|
257
|
-
self.gen = gen
|
|
258
|
-
self.stream = stream
|
|
259
|
-
self.value: bytes | None = None
|
|
260
|
-
|
|
261
|
-
def __iter__(self) -> Iterator[bytes]:
|
|
262
|
-
return self
|
|
263
|
-
|
|
264
|
-
def __next__(self) -> bytes:
|
|
265
|
-
assert isinstance(self.stream, Iterator)
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
chunk = next(self.stream)
|
|
269
|
-
self.gen.send(chunk)
|
|
270
|
-
except StopIteration as exc:
|
|
271
|
-
self.gen.send(None)
|
|
272
|
-
self.value = exc.value
|
|
273
|
-
raise
|
|
274
|
-
return chunk
|
|
275
|
-
|
|
276
|
-
def __aiter__(self) -> AsyncIterator[bytes]:
|
|
277
|
-
return self
|
|
278
|
-
|
|
279
|
-
async def __anext__(self) -> bytes:
|
|
280
|
-
assert isinstance(self.stream, AsyncIterator)
|
|
281
|
-
try:
|
|
282
|
-
chunk = await self.stream.__anext__()
|
|
283
|
-
self.gen.send(chunk)
|
|
284
|
-
except StopIteration as exc:
|
|
285
|
-
self.gen.send(None)
|
|
286
|
-
self.value = exc.value
|
|
287
|
-
raise
|
|
288
|
-
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)
|
hishel/httpx.py
CHANGED
|
@@ -4,8 +4,6 @@ import ssl
|
|
|
4
4
|
import typing as t
|
|
5
5
|
from typing import AsyncIterator, Iterable, Iterator, Union, overload
|
|
6
6
|
|
|
7
|
-
import httpx
|
|
8
|
-
|
|
9
7
|
from hishel import Headers, Request, Response
|
|
10
8
|
from hishel._async_cache import AsyncCacheProxy
|
|
11
9
|
from hishel._core._base._storages._base import AsyncBaseStorage, SyncBaseStorage
|
|
@@ -15,6 +13,15 @@ from hishel._core._spec import (
|
|
|
15
13
|
from hishel._core.models import AnyIterable
|
|
16
14
|
from hishel._sync_cache import SyncCacheProxy
|
|
17
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
|
|
24
|
+
|
|
18
25
|
SOCKET_OPTION = t.Union[
|
|
19
26
|
t.Tuple[int, int, int],
|
|
20
27
|
t.Tuple[int, int, t.Union[bytes, bytearray]],
|
|
@@ -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
|
|
@@ -24,12 +24,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.14
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
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
27
|
Requires-Dist: msgpack>=1.1.2
|
|
31
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
32
|
Provides-Extra: httpx
|
|
33
|
+
Requires-Dist: anyio>=4.9.0; extra == 'httpx'
|
|
34
|
+
Requires-Dist: anysqlite>=0.0.5; extra == 'httpx'
|
|
33
35
|
Requires-Dist: httpx>=0.28.1; extra == 'httpx'
|
|
34
36
|
Provides-Extra: requests
|
|
35
37
|
Requires-Dist: requests>=2.32.5; extra == 'requests'
|
|
@@ -249,73 +251,66 @@ Hishel is inspired by and builds upon the excellent work in the Python HTTP ecos
|
|
|
249
251
|
<strong>Made with ❤️ by <a href="https://github.com/karpetrosyan">Kar Petrosyan</a></strong>
|
|
250
252
|
</p>
|
|
251
253
|
|
|
252
|
-
|
|
254
|
+
# Changelog
|
|
253
255
|
|
|
256
|
+
All notable changes to this project will be documented in this file.
|
|
257
|
+
|
|
258
|
+
## 1.0.0.dev2 - 2025-10-21
|
|
254
259
|
### ⚙️ Miscellaneous Tasks
|
|
260
|
+
- Remove redundant utils and tests
|
|
261
|
+
- Add import without extras check in ci
|
|
255
262
|
|
|
256
|
-
|
|
257
|
-
-
|
|
258
|
-
|
|
263
|
+
### 🐛 Bug Fixes
|
|
264
|
+
- Fix check for storing auth requests
|
|
265
|
+
- Don't raise an error on 3xx during revalidation
|
|
259
266
|
|
|
260
267
|
### 🚀 Features
|
|
268
|
+
- Add hishel_created_at response metadata
|
|
261
269
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
-
|
|
265
|
-
- *(perf)* Increase requests buffer size to 128KB, disable charset detection
|
|
266
|
-
|
|
267
|
-
### 🐛 Bug Fixes
|
|
270
|
+
## 1.0.0.dev1 - 2025-10-21
|
|
271
|
+
### ⚙️ Miscellaneous Tasks
|
|
272
|
+
- Remove some redundant utils methods
|
|
268
273
|
|
|
269
|
-
|
|
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
|
|
270
279
|
|
|
280
|
+
## 1.0.0.dev0 - 2025-10-19
|
|
271
281
|
### ⚙️ Miscellaneous Tasks
|
|
282
|
+
- Use mike powered versioning
|
|
283
|
+
- Improve docs versioning, deploy dev doc on ci
|
|
272
284
|
|
|
285
|
+
## 0.1.5 - 2025-10-18
|
|
286
|
+
### ⚙️ Miscellaneous Tasks
|
|
273
287
|
- 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
288
|
|
|
284
289
|
### 🐛 Bug Fixes
|
|
290
|
+
- Fix some line breaks
|
|
285
291
|
|
|
286
|
-
|
|
287
|
-
-
|
|
292
|
+
### 🚀 Features
|
|
293
|
+
- Set chunk size to 128KB for httpx to reduce SQLite read/writes
|
|
294
|
+
- Better cache-control parsing
|
|
295
|
+
- Add close method to storages API (#384)
|
|
296
|
+
- Increase requests buffer size to 128KB, disable charset detection
|
|
288
297
|
|
|
298
|
+
## 0.1.4 - 2025-10-14
|
|
289
299
|
### ⚙️ Miscellaneous Tasks
|
|
290
|
-
|
|
291
300
|
- Improve CI (#369)
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
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
|
|
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
|
|
311
305
|
|
|
312
306
|
### 🐛 Bug Fixes
|
|
307
|
+
- Create an sqlite file in a cache folder
|
|
308
|
+
- Fix beta imports
|
|
313
309
|
|
|
314
|
-
|
|
315
|
-
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
310
|
+
### 🚀 Features
|
|
311
|
+
- Add support for a sans-IO API (#366)
|
|
312
|
+
- Allow already consumed streams with `CacheTransport` (#377)
|
|
313
|
+
- Add sqlite storage for beta storages
|
|
314
|
+
- Get rid of some locks from sqlite storage
|
|
315
|
+
- Better async implemetation for sqlite storage
|
|
319
316
|
|
|
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=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
|
+
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=yQmuJ-HOXTPNIooXr8PPlQUhIv_SEDkZocro_u89o_A,104179
|
|
11
|
+
hishel/_core/models.py,sha256=-09QfsdKHWyKwFqSZZf7qoUdTvtL1KL53c-KsBAtGuU,5571
|
|
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.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,,
|
|
@@ -1,19 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|