hishel 0.1.4__py3-none-any.whl → 1.0.0b1__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.
Files changed (56) hide show
  1. hishel/__init__.py +59 -52
  2. hishel/_async_cache.py +213 -0
  3. hishel/_async_httpx.py +236 -0
  4. hishel/_core/_headers.py +646 -0
  5. hishel/{beta/_core → _core}/_spec.py +270 -136
  6. hishel/_core/_storages/_async_base.py +71 -0
  7. hishel/_core/_storages/_async_sqlite.py +420 -0
  8. hishel/_core/_storages/_packing.py +144 -0
  9. hishel/_core/_storages/_sync_base.py +71 -0
  10. hishel/_core/_storages/_sync_sqlite.py +420 -0
  11. hishel/{beta/_core → _core}/models.py +100 -37
  12. hishel/_policies.py +49 -0
  13. hishel/_sync_cache.py +213 -0
  14. hishel/_sync_httpx.py +236 -0
  15. hishel/_utils.py +37 -366
  16. hishel/asgi.py +400 -0
  17. hishel/fastapi.py +263 -0
  18. hishel/httpx.py +12 -0
  19. hishel/{beta/requests.py → requests.py} +41 -30
  20. hishel-1.0.0b1.dist-info/METADATA +509 -0
  21. hishel-1.0.0b1.dist-info/RECORD +24 -0
  22. hishel/_async/__init__.py +0 -5
  23. hishel/_async/_client.py +0 -30
  24. hishel/_async/_mock.py +0 -43
  25. hishel/_async/_pool.py +0 -201
  26. hishel/_async/_storages.py +0 -768
  27. hishel/_async/_transports.py +0 -282
  28. hishel/_controller.py +0 -581
  29. hishel/_exceptions.py +0 -10
  30. hishel/_files.py +0 -54
  31. hishel/_headers.py +0 -215
  32. hishel/_lfu_cache.py +0 -71
  33. hishel/_lmdb_types_.pyi +0 -53
  34. hishel/_s3.py +0 -122
  35. hishel/_serializers.py +0 -329
  36. hishel/_sync/__init__.py +0 -5
  37. hishel/_sync/_client.py +0 -30
  38. hishel/_sync/_mock.py +0 -43
  39. hishel/_sync/_pool.py +0 -201
  40. hishel/_sync/_storages.py +0 -768
  41. hishel/_sync/_transports.py +0 -282
  42. hishel/_synchronization.py +0 -37
  43. hishel/beta/__init__.py +0 -59
  44. hishel/beta/_async_cache.py +0 -167
  45. hishel/beta/_core/__init__.py +0 -0
  46. hishel/beta/_core/_async/_storages/_sqlite.py +0 -411
  47. hishel/beta/_core/_base/_storages/_base.py +0 -260
  48. hishel/beta/_core/_base/_storages/_packing.py +0 -165
  49. hishel/beta/_core/_headers.py +0 -301
  50. hishel/beta/_core/_sync/_storages/_sqlite.py +0 -411
  51. hishel/beta/_sync_cache.py +0 -167
  52. hishel/beta/httpx.py +0 -317
  53. hishel-0.1.4.dist-info/METADATA +0 -404
  54. hishel-0.1.4.dist-info/RECORD +0 -41
  55. {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/WHEEL +0 -0
  56. {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,768 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import datetime
4
- import logging
5
- import os
6
- import time
7
- import typing as tp
8
- import warnings
9
- from copy import deepcopy
10
- from pathlib import Path
11
-
12
- try:
13
- import boto3
14
-
15
- from .._s3 import AsyncS3Manager
16
- except ImportError: # pragma: no cover
17
- boto3 = None # type: ignore
18
-
19
- try:
20
- import anysqlite
21
- except ImportError: # pragma: no cover
22
- anysqlite = None # type: ignore
23
-
24
- from httpcore import Request, Response
25
-
26
- if tp.TYPE_CHECKING: # pragma: no cover
27
- from typing_extensions import TypeAlias
28
-
29
- from .._files import AsyncFileManager
30
- from .._serializers import BaseSerializer, JSONSerializer, Metadata, clone_model
31
- from .._synchronization import AsyncLock
32
- from .._utils import float_seconds_to_int_milliseconds
33
-
34
- logger = logging.getLogger("hishel.storages")
35
-
36
- __all__ = (
37
- "AsyncBaseStorage",
38
- "AsyncFileStorage",
39
- "AsyncInMemoryStorage",
40
- "AsyncRedisStorage",
41
- "AsyncS3Storage",
42
- "AsyncSQLiteStorage",
43
- )
44
-
45
- StoredResponse: TypeAlias = tp.Tuple[Response, Request, Metadata]
46
- RemoveTypes = tp.Union[str, Response]
47
-
48
- try:
49
- import redis.asyncio as redis
50
- except ImportError: # pragma: no cover
51
- redis = None # type: ignore
52
-
53
-
54
- class AsyncBaseStorage:
55
- def __init__(
56
- self,
57
- serializer: tp.Optional[BaseSerializer] = None,
58
- ttl: tp.Optional[tp.Union[int, float]] = None,
59
- ) -> None:
60
- self._serializer = serializer or JSONSerializer()
61
- self._ttl = ttl
62
-
63
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
64
- raise NotImplementedError()
65
-
66
- async def remove(self, key: RemoveTypes) -> None:
67
- raise NotImplementedError()
68
-
69
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
70
- raise NotImplementedError()
71
-
72
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
73
- raise NotImplementedError()
74
-
75
- async def aclose(self) -> None:
76
- raise NotImplementedError()
77
-
78
-
79
- class AsyncFileStorage(AsyncBaseStorage):
80
- """
81
- A simple file storage.
82
-
83
- :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None
84
- :type serializer: tp.Optional[BaseSerializer], optional
85
- :param base_path: A storage base path where the responses should be saved, defaults to None
86
- :type base_path: tp.Optional[Path], optional
87
- :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None
88
- :type ttl: tp.Optional[tp.Union[int, float]], optional
89
- :param check_ttl_every: How often in seconds to check staleness of **all** cache files.
90
- Makes sense only with set `ttl`, defaults to 60
91
- :type check_ttl_every: tp.Union[int, float]
92
- """
93
-
94
- def __init__(
95
- self,
96
- serializer: tp.Optional[BaseSerializer] = None,
97
- base_path: tp.Optional[Path] = None,
98
- ttl: tp.Optional[tp.Union[int, float]] = None,
99
- check_ttl_every: tp.Union[int, float] = 60,
100
- ) -> None:
101
- super().__init__(serializer, ttl)
102
-
103
- self._base_path = Path(base_path) if base_path is not None else Path(".cache/hishel")
104
- self._gitignore_file = self._base_path / ".gitignore"
105
-
106
- self._base_path.mkdir(parents=True, exist_ok=True)
107
-
108
- if not self._gitignore_file.is_file():
109
- with open(self._gitignore_file, "w", encoding="utf-8") as f:
110
- f.write("# Automatically created by Hishel\n*")
111
-
112
- self._file_manager = AsyncFileManager(is_binary=self._serializer.is_binary)
113
- self._lock = AsyncLock()
114
- self._check_ttl_every = check_ttl_every
115
- self._last_cleaned = time.monotonic()
116
-
117
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
118
- """
119
- Stores the response in the cache.
120
-
121
- :param key: Hashed value of concatenated HTTP method and URI
122
- :type key: str
123
- :param response: An HTTP response
124
- :type response: httpcore.Response
125
- :param request: An HTTP request
126
- :type request: httpcore.Request
127
- :param metadata: Additional information about the stored response
128
- :type metadata: Optional[Metadata]
129
- """
130
-
131
- metadata = metadata or Metadata(
132
- cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
133
- )
134
- response_path = self._base_path / key
135
-
136
- async with self._lock:
137
- await self._file_manager.write_to(
138
- str(response_path),
139
- self._serializer.dumps(response=response, request=request, metadata=metadata),
140
- )
141
- await self._remove_expired_caches(response_path)
142
-
143
- async def remove(self, key: RemoveTypes) -> None:
144
- """
145
- Removes the response from the cache.
146
-
147
- :param key: Hashed value of concatenated HTTP method and URI or an HTTP response
148
- :type key: Union[str, Response]
149
- """
150
-
151
- if isinstance(key, Response): # pragma: no cover
152
- key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
153
-
154
- response_path = self._base_path / key
155
-
156
- async with self._lock:
157
- if response_path.exists():
158
- response_path.unlink(missing_ok=True)
159
-
160
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
161
- """
162
- Updates the metadata of the stored response.
163
-
164
- :param key: Hashed value of concatenated HTTP method and URI
165
- :type key: str
166
- :param response: An HTTP response
167
- :type response: httpcore.Response
168
- :param request: An HTTP request
169
- :type request: httpcore.Request
170
- :param metadata: Additional information about the stored response
171
- :type metadata: Metadata
172
- """
173
- response_path = self._base_path / key
174
-
175
- async with self._lock:
176
- if response_path.exists():
177
- atime = response_path.stat().st_atime
178
- old_mtime = response_path.stat().st_mtime
179
- await self._file_manager.write_to(
180
- str(response_path),
181
- self._serializer.dumps(response=response, request=request, metadata=metadata),
182
- )
183
-
184
- # Restore the old atime and mtime (we use mtime to check the cache expiration time)
185
- os.utime(response_path, (atime, old_mtime))
186
- return
187
-
188
- return await self.store(key, response, request, metadata) # pragma: no cover
189
-
190
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
191
- """
192
- Retreives the response from the cache using his key.
193
-
194
- :param key: Hashed value of concatenated HTTP method and URI
195
- :type key: str
196
- :return: An HTTP response and his HTTP request.
197
- :rtype: tp.Optional[StoredResponse]
198
- """
199
-
200
- response_path = self._base_path / key
201
-
202
- await self._remove_expired_caches(response_path)
203
- async with self._lock:
204
- if response_path.exists():
205
- read_data = await self._file_manager.read_from(str(response_path))
206
- if len(read_data) != 0:
207
- return self._serializer.loads(read_data)
208
- return None
209
-
210
- async def aclose(self) -> None: # pragma: no cover
211
- return
212
-
213
- async def _remove_expired_caches(self, response_path: Path) -> None:
214
- if self._ttl is None:
215
- return
216
-
217
- if time.monotonic() - self._last_cleaned < self._check_ttl_every:
218
- if response_path.is_file():
219
- age = time.time() - response_path.stat().st_mtime
220
- if age > self._ttl:
221
- response_path.unlink(missing_ok=True)
222
- return
223
-
224
- self._last_cleaned = time.monotonic()
225
- async with self._lock:
226
- with os.scandir(self._base_path) as entries:
227
- for entry in entries:
228
- try:
229
- if entry.is_file():
230
- age = time.time() - entry.stat().st_mtime
231
- if age > self._ttl:
232
- os.unlink(entry.path)
233
- except FileNotFoundError: # pragma: no cover
234
- pass
235
-
236
-
237
- class AsyncSQLiteStorage(AsyncBaseStorage):
238
- """
239
- A simple sqlite3 storage.
240
-
241
- :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None
242
- :type serializer: tp.Optional[BaseSerializer], optional
243
- :param connection: A connection for sqlite, defaults to None
244
- :type connection: tp.Optional[anysqlite.Connection], optional
245
- :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None
246
- :type ttl: tp.Optional[tp.Union[int, float]], optional
247
- """
248
-
249
- def __init__(
250
- self,
251
- serializer: tp.Optional[BaseSerializer] = None,
252
- connection: tp.Optional[anysqlite.Connection] = None,
253
- ttl: tp.Optional[tp.Union[int, float]] = None,
254
- ) -> None:
255
- if anysqlite is None: # pragma: no cover
256
- raise RuntimeError(
257
- f"The `{type(self).__name__}` was used, but the required packages were not found. "
258
- "Check that you have `Hishel` installed with the `sqlite` extension as shown.\n"
259
- "```pip install hishel[sqlite]```"
260
- )
261
- super().__init__(serializer, ttl)
262
-
263
- self._connection: tp.Optional[anysqlite.Connection] = connection or None
264
- self._setup_lock = AsyncLock()
265
- self._setup_completed: bool = False
266
- self._lock = AsyncLock()
267
-
268
- async def _setup(self) -> None:
269
- async with self._setup_lock:
270
- if not self._setup_completed:
271
- if not self._connection: # pragma: no cover
272
- self._connection = await anysqlite.connect(".hishel.sqlite", check_same_thread=False)
273
- await self._connection.execute(
274
- "CREATE TABLE IF NOT EXISTS cache(key TEXT, data BLOB, date_created REAL)"
275
- )
276
- await self._connection.commit()
277
- self._setup_completed = True
278
-
279
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
280
- """
281
- Stores the response in the cache.
282
-
283
- :param key: Hashed value of concatenated HTTP method and URI
284
- :type key: str
285
- :param response: An HTTP response
286
- :type response: httpcore.Response
287
- :param request: An HTTP request
288
- :type request: httpcore.Request
289
- :param metadata: Additioal information about the stored response
290
- :type metadata: Optional[Metadata]
291
- """
292
-
293
- await self._setup()
294
- assert self._connection
295
-
296
- metadata = metadata or Metadata(
297
- cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
298
- )
299
-
300
- async with self._lock:
301
- await self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
302
- serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata)
303
- await self._connection.execute(
304
- "INSERT INTO cache(key, data, date_created) VALUES(?, ?, ?)", [key, serialized_response, time.time()]
305
- )
306
- await self._connection.commit()
307
- await self._remove_expired_caches()
308
-
309
- async def remove(self, key: RemoveTypes) -> None:
310
- """
311
- Removes the response from the cache.
312
-
313
- :param key: Hashed value of concatenated HTTP method and URI or an HTTP response
314
- :type key: Union[str, Response]
315
- """
316
-
317
- await self._setup()
318
- assert self._connection
319
-
320
- if isinstance(key, Response): # pragma: no cover
321
- key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
322
-
323
- async with self._lock:
324
- await self._connection.execute("DELETE FROM cache WHERE key = ?", [key])
325
- await self._connection.commit()
326
-
327
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
328
- """
329
- Updates the metadata of the stored response.
330
-
331
- :param key: Hashed value of concatenated HTTP method and URI
332
- :type key: str
333
- :param response: An HTTP response
334
- :type response: httpcore.Response
335
- :param request: An HTTP request
336
- :type request: httpcore.Request
337
- :param metadata: Additional information about the stored response
338
- :type metadata: Metadata
339
- """
340
-
341
- await self._setup()
342
- assert self._connection
343
-
344
- async with self._lock:
345
- cursor = await self._connection.execute("SELECT data FROM cache WHERE key = ?", [key])
346
- row = await cursor.fetchone()
347
- if row is not None:
348
- serialized_response = self._serializer.dumps(response=response, request=request, metadata=metadata)
349
- await self._connection.execute("UPDATE cache SET data = ? WHERE key = ?", [serialized_response, key])
350
- await self._connection.commit()
351
- return
352
- return await self.store(key, response, request, metadata) # pragma: no cover
353
-
354
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
355
- """
356
- Retreives the response from the cache using his key.
357
-
358
- :param key: Hashed value of concatenated HTTP method and URI
359
- :type key: str
360
- :return: An HTTP response and its HTTP request.
361
- :rtype: tp.Optional[StoredResponse]
362
- """
363
-
364
- await self._setup()
365
- assert self._connection
366
-
367
- await self._remove_expired_caches()
368
- async with self._lock:
369
- cursor = await self._connection.execute("SELECT data FROM cache WHERE key = ?", [key])
370
- row = await cursor.fetchone()
371
- if row is None:
372
- return None
373
-
374
- cached_response = row[0]
375
- return self._serializer.loads(cached_response)
376
-
377
- async def aclose(self) -> None: # pragma: no cover
378
- if self._connection is not None:
379
- await self._connection.close()
380
-
381
- async def _remove_expired_caches(self) -> None:
382
- assert self._connection
383
- if self._ttl is None:
384
- return
385
-
386
- async with self._lock:
387
- await self._connection.execute("DELETE FROM cache WHERE date_created + ? < ?", [self._ttl, time.time()])
388
- await self._connection.commit()
389
-
390
-
391
- class AsyncRedisStorage(AsyncBaseStorage):
392
- """
393
- A simple redis storage.
394
-
395
- :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None
396
- :type serializer: tp.Optional[BaseSerializer], optional
397
- :param client: A client for redis, defaults to None
398
- :type client: tp.Optional["redis.Redis"], optional
399
- :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None
400
- :type ttl: tp.Optional[tp.Union[int, float]], optional
401
- """
402
-
403
- def __init__(
404
- self,
405
- serializer: tp.Optional[BaseSerializer] = None,
406
- client: tp.Optional[redis.Redis] = None, # type: ignore
407
- ttl: tp.Optional[tp.Union[int, float]] = None,
408
- ) -> None:
409
- if redis is None: # pragma: no cover
410
- raise RuntimeError(
411
- f"The `{type(self).__name__}` was used, but the required packages were not found. "
412
- "Check that you have `Hishel` installed with the `redis` extension as shown.\n"
413
- "```pip install hishel[redis]```"
414
- )
415
- super().__init__(serializer, ttl)
416
-
417
- if client is None:
418
- self._client = redis.Redis() # type: ignore
419
- else: # pragma: no cover
420
- self._client = client
421
-
422
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
423
- """
424
- Stores the response in the cache.
425
-
426
- :param key: Hashed value of concatenated HTTP method and URI
427
- :type key: str
428
- :param response: An HTTP response
429
- :type response: httpcore.Response
430
- :param request: An HTTP request
431
- :type request: httpcore.Request
432
- :param metadata: Additioal information about the stored response
433
- :type metadata: Optional[Metadata]
434
- """
435
-
436
- metadata = metadata or Metadata(
437
- cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
438
- )
439
-
440
- if self._ttl is not None:
441
- px = float_seconds_to_int_milliseconds(self._ttl)
442
- else:
443
- px = None
444
-
445
- await self._client.set(
446
- key, self._serializer.dumps(response=response, request=request, metadata=metadata), px=px
447
- )
448
-
449
- async def remove(self, key: RemoveTypes) -> None:
450
- """
451
- Removes the response from the cache.
452
-
453
- :param key: Hashed value of concatenated HTTP method and URI or an HTTP response
454
- :type key: Union[str, Response]
455
- """
456
-
457
- if isinstance(key, Response): # pragma: no cover
458
- key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
459
-
460
- await self._client.delete(key)
461
-
462
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
463
- """
464
- Updates the metadata of the stored response.
465
-
466
- :param key: Hashed value of concatenated HTTP method and URI
467
- :type key: str
468
- :param response: An HTTP response
469
- :type response: httpcore.Response
470
- :param request: An HTTP request
471
- :type request: httpcore.Request
472
- :param metadata: Additional information about the stored response
473
- :type metadata: Metadata
474
- """
475
-
476
- ttl_in_milliseconds = await self._client.pttl(key)
477
-
478
- # -2: if the key does not exist in Redis
479
- # -1: if the key exists in Redis but has no expiration
480
- if ttl_in_milliseconds == -2 or ttl_in_milliseconds == -1: # pragma: no cover
481
- await self.store(key, response, request, metadata)
482
- else:
483
- await self._client.set(
484
- key,
485
- self._serializer.dumps(response=response, request=request, metadata=metadata),
486
- px=ttl_in_milliseconds,
487
- )
488
-
489
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
490
- """
491
- Retreives the response from the cache using his key.
492
-
493
- :param key: Hashed value of concatenated HTTP method and URI
494
- :type key: str
495
- :return: An HTTP response and its HTTP request.
496
- :rtype: tp.Optional[StoredResponse]
497
- """
498
-
499
- cached_response = await self._client.get(key)
500
- if cached_response is None:
501
- return None
502
-
503
- return self._serializer.loads(cached_response)
504
-
505
- async def aclose(self) -> None: # pragma: no cover
506
- await self._client.aclose()
507
-
508
-
509
- class AsyncInMemoryStorage(AsyncBaseStorage):
510
- """
511
- A simple in-memory storage.
512
-
513
- :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None
514
- :type serializer: tp.Optional[BaseSerializer], optional
515
- :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None
516
- :type ttl: tp.Optional[tp.Union[int, float]], optional
517
- :param capacity: The maximum number of responses that can be cached, defaults to 128
518
- :type capacity: int, optional
519
- """
520
-
521
- def __init__(
522
- self,
523
- serializer: tp.Optional[BaseSerializer] = None,
524
- ttl: tp.Optional[tp.Union[int, float]] = None,
525
- capacity: int = 128,
526
- ) -> None:
527
- super().__init__(serializer, ttl)
528
-
529
- if serializer is not None: # pragma: no cover
530
- warnings.warn("The serializer is not used in the in-memory storage.", RuntimeWarning)
531
-
532
- from hishel import LFUCache
533
-
534
- self._cache: LFUCache[str, tp.Tuple[StoredResponse, float]] = LFUCache(capacity=capacity)
535
- self._lock = AsyncLock()
536
-
537
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
538
- """
539
- Stores the response in the cache.
540
-
541
- :param key: Hashed value of concatenated HTTP method and URI
542
- :type key: str
543
- :param response: An HTTP response
544
- :type response: httpcore.Response
545
- :param request: An HTTP request
546
- :type request: httpcore.Request
547
- :param metadata: Additioal information about the stored response
548
- :type metadata: Optional[Metadata]
549
- """
550
-
551
- metadata = metadata or Metadata(
552
- cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
553
- )
554
-
555
- async with self._lock:
556
- response_clone = clone_model(response)
557
- request_clone = clone_model(request)
558
- stored_response: StoredResponse = (deepcopy(response_clone), deepcopy(request_clone), metadata)
559
- self._cache.put(key, (stored_response, time.monotonic()))
560
- await self._remove_expired_caches()
561
-
562
- async def remove(self, key: RemoveTypes) -> None:
563
- """
564
- Removes the response from the cache.
565
-
566
- :param key: Hashed value of concatenated HTTP method and URI or an HTTP response
567
- :type key: Union[str, Response]
568
- """
569
-
570
- if isinstance(key, Response): # pragma: no cover
571
- key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
572
-
573
- async with self._lock:
574
- self._cache.remove_key(key)
575
-
576
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
577
- """
578
- Updates the metadata of the stored response.
579
-
580
- :param key: Hashed value of concatenated HTTP method and URI
581
- :type key: str
582
- :param response: An HTTP response
583
- :type response: httpcore.Response
584
- :param request: An HTTP request
585
- :type request: httpcore.Request
586
- :param metadata: Additional information about the stored response
587
- :type metadata: Metadata
588
- """
589
-
590
- async with self._lock:
591
- try:
592
- stored_response, created_at = self._cache.get(key)
593
- stored_response = (stored_response[0], stored_response[1], metadata)
594
- self._cache.put(key, (stored_response, created_at))
595
- return
596
- except KeyError: # pragma: no cover
597
- pass
598
- await self.store(key, response, request, metadata) # pragma: no cover
599
-
600
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
601
- """
602
- Retreives the response from the cache using his key.
603
-
604
- :param key: Hashed value of concatenated HTTP method and URI
605
- :type key: str
606
- :return: An HTTP response and its HTTP request.
607
- :rtype: tp.Optional[StoredResponse]
608
- """
609
-
610
- await self._remove_expired_caches()
611
- async with self._lock:
612
- try:
613
- stored_response, _ = self._cache.get(key)
614
- except KeyError:
615
- return None
616
- return stored_response
617
-
618
- async def aclose(self) -> None: # pragma: no cover
619
- return
620
-
621
- async def _remove_expired_caches(self) -> None:
622
- if self._ttl is None:
623
- return
624
-
625
- async with self._lock:
626
- keys_to_remove = set()
627
-
628
- for key in self._cache:
629
- created_at = self._cache.get(key)[1]
630
-
631
- if time.monotonic() - created_at > self._ttl:
632
- keys_to_remove.add(key)
633
-
634
- for key in keys_to_remove:
635
- self._cache.remove_key(key)
636
-
637
-
638
- class AsyncS3Storage(AsyncBaseStorage): # pragma: no cover
639
- """
640
- AWS S3 storage.
641
-
642
- :param bucket_name: The name of the bucket to store the responses in
643
- :type bucket_name: str
644
- :param serializer: Serializer capable of serializing and de-serializing http responses, defaults to None
645
- :type serializer: tp.Optional[BaseSerializer], optional
646
- :param ttl: Specifies the maximum number of seconds that the response can be cached, defaults to None
647
- :type ttl: tp.Optional[tp.Union[int, float]], optional
648
- :param check_ttl_every: How often in seconds to check staleness of **all** cache files.
649
- Makes sense only with set `ttl`, defaults to 60
650
- :type check_ttl_every: tp.Union[int, float]
651
- :param client: A client for S3, defaults to None
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
655
- """
656
-
657
- def __init__(
658
- self,
659
- bucket_name: str,
660
- serializer: tp.Optional[BaseSerializer] = None,
661
- ttl: tp.Optional[tp.Union[int, float]] = None,
662
- check_ttl_every: tp.Union[int, float] = 60,
663
- client: tp.Optional[tp.Any] = None,
664
- path_prefix: str = "hishel-",
665
- ) -> None:
666
- super().__init__(serializer, ttl)
667
-
668
- if boto3 is None: # pragma: no cover
669
- raise RuntimeError(
670
- f"The `{type(self).__name__}` was used, but the required packages were not found. "
671
- "Check that you have `Hishel` installed with the `s3` extension as shown.\n"
672
- "```pip install hishel[s3]```"
673
- )
674
-
675
- self._bucket_name = bucket_name
676
- client = client or boto3.client("s3")
677
- self._s3_manager = AsyncS3Manager(
678
- client=client,
679
- bucket_name=bucket_name,
680
- is_binary=self._serializer.is_binary,
681
- check_ttl_every=check_ttl_every,
682
- path_prefix=path_prefix,
683
- )
684
- self._lock = AsyncLock()
685
-
686
- async def store(self, key: str, response: Response, request: Request, metadata: Metadata | None = None) -> None:
687
- """
688
- Stores the response in the cache.
689
-
690
- :param key: Hashed value of concatenated HTTP method and URI
691
- :type key: str
692
- :param response: An HTTP response
693
- :type response: httpcore.Response
694
- :param request: An HTTP request
695
- :type request: httpcore.Request
696
- :param metadata: Additioal information about the stored response
697
- :type metadata: Optional[Metadata]`
698
- """
699
-
700
- metadata = metadata or Metadata(
701
- cache_key=key, created_at=datetime.datetime.now(datetime.timezone.utc), number_of_uses=0
702
- )
703
-
704
- async with self._lock:
705
- serialized = self._serializer.dumps(response=response, request=request, metadata=metadata)
706
- await self._s3_manager.write_to(path=key, data=serialized)
707
-
708
- await self._remove_expired_caches(key)
709
-
710
- async def remove(self, key: RemoveTypes) -> None:
711
- """
712
- Removes the response from the cache.
713
-
714
- :param key: Hashed value of concatenated HTTP method and URI or an HTTP response
715
- :type key: Union[str, Response]
716
- """
717
-
718
- if isinstance(key, Response): # pragma: no cover
719
- key = tp.cast(str, key.extensions["cache_metadata"]["cache_key"])
720
-
721
- async with self._lock:
722
- await self._s3_manager.remove_entry(key)
723
-
724
- async def update_metadata(self, key: str, response: Response, request: Request, metadata: Metadata) -> None:
725
- """
726
- Updates the metadata of the stored response.
727
-
728
- :param key: Hashed value of concatenated HTTP method and URI
729
- :type key: str
730
- :param response: An HTTP response
731
- :type response: httpcore.Response
732
- :param request: An HTTP request
733
- :type request: httpcore.Request
734
- :param metadata: Additional information about the stored response
735
- :type metadata: Metadata
736
- """
737
-
738
- async with self._lock:
739
- serialized = self._serializer.dumps(response=response, request=request, metadata=metadata)
740
- await self._s3_manager.write_to(path=key, data=serialized, only_metadata=True)
741
-
742
- async def retrieve(self, key: str) -> tp.Optional[StoredResponse]:
743
- """
744
- Retreives the response from the cache using his key.
745
-
746
- :param key: Hashed value of concatenated HTTP method and URI
747
- :type key: str
748
- :return: An HTTP response and its HTTP request.
749
- :rtype: tp.Optional[StoredResponse]
750
- """
751
-
752
- await self._remove_expired_caches(key)
753
- async with self._lock:
754
- try:
755
- return self._serializer.loads(await self._s3_manager.read_from(path=key))
756
- except Exception:
757
- return None
758
-
759
- async def aclose(self) -> None: # pragma: no cover
760
- return
761
-
762
- async def _remove_expired_caches(self, key: str) -> None:
763
- if self._ttl is None:
764
- return
765
-
766
- async with self._lock:
767
- converted_ttl = float_seconds_to_int_milliseconds(self._ttl)
768
- await self._s3_manager.remove_expired(ttl=converted_ttl, key=key)