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.
- hishel/__init__.py +59 -52
- hishel/_async_cache.py +213 -0
- hishel/_async_httpx.py +236 -0
- hishel/_core/_headers.py +646 -0
- hishel/{beta/_core → _core}/_spec.py +270 -136
- hishel/_core/_storages/_async_base.py +71 -0
- hishel/_core/_storages/_async_sqlite.py +420 -0
- hishel/_core/_storages/_packing.py +144 -0
- hishel/_core/_storages/_sync_base.py +71 -0
- hishel/_core/_storages/_sync_sqlite.py +420 -0
- hishel/{beta/_core → _core}/models.py +100 -37
- hishel/_policies.py +49 -0
- hishel/_sync_cache.py +213 -0
- hishel/_sync_httpx.py +236 -0
- hishel/_utils.py +37 -366
- hishel/asgi.py +400 -0
- hishel/fastapi.py +263 -0
- hishel/httpx.py +12 -0
- hishel/{beta/requests.py → requests.py} +41 -30
- hishel-1.0.0b1.dist-info/METADATA +509 -0
- hishel-1.0.0b1.dist-info/RECORD +24 -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/__init__.py +0 -59
- hishel/beta/_async_cache.py +0 -167
- hishel/beta/_core/__init__.py +0 -0
- hishel/beta/_core/_async/_storages/_sqlite.py +0 -411
- hishel/beta/_core/_base/_storages/_base.py +0 -260
- hishel/beta/_core/_base/_storages/_packing.py +0 -165
- hishel/beta/_core/_headers.py +0 -301
- hishel/beta/_core/_sync/_storages/_sqlite.py +0 -411
- hishel/beta/_sync_cache.py +0 -167
- hishel/beta/httpx.py +0 -317
- hishel-0.1.4.dist-info/METADATA +0 -404
- hishel-0.1.4.dist-info/RECORD +0 -41
- {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/WHEEL +0 -0
- {hishel-0.1.4.dist-info → hishel-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
hishel/_headers.py
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import string
|
|
2
|
-
from typing import Any, Dict, List, Optional, Union
|
|
3
|
-
|
|
4
|
-
from ._exceptions import ParseError, ValidationError
|
|
5
|
-
|
|
6
|
-
## Grammar
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
HTAB = "\t"
|
|
10
|
-
SP = " "
|
|
11
|
-
obs_text = "".join(chr(i) for i in range(0x80, 0xFF + 1)) # 0x80-0xFF
|
|
12
|
-
|
|
13
|
-
tchar = "!#$%&'*+-.^_`|~0123456789" + string.ascii_letters
|
|
14
|
-
qdtext = "".join(
|
|
15
|
-
[
|
|
16
|
-
HTAB,
|
|
17
|
-
SP,
|
|
18
|
-
"\x21",
|
|
19
|
-
"".join(chr(i) for i in range(0x23, 0x5B + 1)), # 0x23-0x5b
|
|
20
|
-
"".join(chr(i) for i in range(0x5D, 0x7E + 1)), # 0x5D-0x7E
|
|
21
|
-
obs_text,
|
|
22
|
-
]
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
TIME_FIELDS = [
|
|
26
|
-
"max_age",
|
|
27
|
-
"max_stale",
|
|
28
|
-
"min_fresh",
|
|
29
|
-
"s_maxage",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
BOOLEAN_FIELDS = [
|
|
33
|
-
"immutable",
|
|
34
|
-
"must_revalidate",
|
|
35
|
-
"must_understand",
|
|
36
|
-
"no_store",
|
|
37
|
-
"no_transform",
|
|
38
|
-
"only_if_cached",
|
|
39
|
-
"public",
|
|
40
|
-
"proxy_revalidate",
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
LIST_FIELDS = ["no_cache", "private"]
|
|
44
|
-
|
|
45
|
-
__all__ = (
|
|
46
|
-
"CacheControl",
|
|
47
|
-
"Vary",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def strip_ows_around(text: str) -> str:
|
|
52
|
-
return text.strip(" ").strip("\t")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def normalize_directive(text: str) -> str:
|
|
56
|
-
return text.replace("-", "_")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def parse_cache_control(cache_control_values: List[str]) -> "CacheControl":
|
|
60
|
-
directives = {}
|
|
61
|
-
|
|
62
|
-
for cache_control_value in cache_control_values:
|
|
63
|
-
if "no-cache=" in cache_control_value or "private=" in cache_control_value:
|
|
64
|
-
cache_control_splited = [cache_control_value]
|
|
65
|
-
else:
|
|
66
|
-
cache_control_splited = cache_control_value.split(",")
|
|
67
|
-
|
|
68
|
-
for directive in cache_control_splited:
|
|
69
|
-
key: str = ""
|
|
70
|
-
value: Optional[str] = None
|
|
71
|
-
dquote = False
|
|
72
|
-
|
|
73
|
-
if not directive:
|
|
74
|
-
raise ParseError("The directive should not be left blank.")
|
|
75
|
-
|
|
76
|
-
directive = strip_ows_around(directive)
|
|
77
|
-
|
|
78
|
-
if not directive:
|
|
79
|
-
raise ParseError("The directive should not contain only whitespaces.")
|
|
80
|
-
|
|
81
|
-
for i, key_char in enumerate(directive):
|
|
82
|
-
if key_char == "=":
|
|
83
|
-
value = directive[i + 1 :]
|
|
84
|
-
|
|
85
|
-
if not value:
|
|
86
|
-
raise ParseError("The directive value cannot be left blank.")
|
|
87
|
-
|
|
88
|
-
if value[0] == '"':
|
|
89
|
-
dquote = True
|
|
90
|
-
if dquote and value[-1] != '"':
|
|
91
|
-
raise ParseError("Invalid quotes around the value.")
|
|
92
|
-
|
|
93
|
-
if not dquote:
|
|
94
|
-
for value_char in value:
|
|
95
|
-
if value_char not in tchar:
|
|
96
|
-
raise ParseError(
|
|
97
|
-
f"The character '{value_char!r}' is not permitted for the unquoted values."
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
for value_char in value[1:-1]:
|
|
101
|
-
if value_char not in qdtext:
|
|
102
|
-
raise ParseError(
|
|
103
|
-
f"The character '{value_char!r}' is not permitted for the quoted values."
|
|
104
|
-
)
|
|
105
|
-
break
|
|
106
|
-
|
|
107
|
-
if key_char not in tchar:
|
|
108
|
-
raise ParseError(f"The character '{key_char!r}' is not permitted in the directive name.")
|
|
109
|
-
key += key_char
|
|
110
|
-
directives[key] = value
|
|
111
|
-
validated_data = CacheControl.validate(directives)
|
|
112
|
-
return CacheControl(**validated_data)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class Vary:
|
|
116
|
-
def __init__(self, values: List[str]) -> None:
|
|
117
|
-
self._values = values
|
|
118
|
-
|
|
119
|
-
@classmethod
|
|
120
|
-
def from_value(cls, vary_values: List[str]) -> "Vary":
|
|
121
|
-
values = []
|
|
122
|
-
|
|
123
|
-
for vary_value in vary_values:
|
|
124
|
-
for field_name in vary_value.split(","):
|
|
125
|
-
field_name = field_name.strip()
|
|
126
|
-
values.append(field_name)
|
|
127
|
-
return Vary(values)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class CacheControl:
|
|
131
|
-
def __init__(
|
|
132
|
-
self,
|
|
133
|
-
immutable: bool = False, # [RFC8246]
|
|
134
|
-
max_age: Optional[int] = None, # [RFC9111, Section 5.2.1.1, 5.2.2.1]
|
|
135
|
-
max_stale: Optional[int] = None, # [RFC9111, Section 5.2.1.2]
|
|
136
|
-
min_fresh: Optional[int] = None, # [RFC9111, Section 5.2.1.3]
|
|
137
|
-
must_revalidate: bool = False, # [RFC9111, Section 5.2.2.2]
|
|
138
|
-
must_understand: bool = False, # [RFC9111, Section 5.2.2.3]
|
|
139
|
-
no_cache: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.1.4, 5.2.2.4]
|
|
140
|
-
no_store: bool = False, # [RFC9111, Section 5.2.1.5, 5.2.2.5]
|
|
141
|
-
no_transform: bool = False, # [RFC9111, Section 5.2.1.6, 5.2.2.6]
|
|
142
|
-
only_if_cached: bool = False, # [RFC9111, Section 5.2.1.7]
|
|
143
|
-
private: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.2.7]
|
|
144
|
-
proxy_revalidate: bool = False, # [RFC9111, Section 5.2.2.8]
|
|
145
|
-
public: bool = False, # [RFC9111, Section 5.2.2.9]
|
|
146
|
-
s_maxage: Optional[int] = None, # [RFC9111, Section 5.2.2.10]
|
|
147
|
-
) -> None:
|
|
148
|
-
self.immutable = immutable
|
|
149
|
-
self.max_age = max_age
|
|
150
|
-
self.max_stale = max_stale
|
|
151
|
-
self.min_fresh = min_fresh
|
|
152
|
-
self.must_revalidate = must_revalidate
|
|
153
|
-
self.must_understand = must_understand
|
|
154
|
-
self.no_cache = no_cache
|
|
155
|
-
self.no_store = no_store
|
|
156
|
-
self.no_transform = no_transform
|
|
157
|
-
self.only_if_cached = only_if_cached
|
|
158
|
-
self.private = private
|
|
159
|
-
self.proxy_revalidate = proxy_revalidate
|
|
160
|
-
self.public = public
|
|
161
|
-
self.s_maxage = s_maxage
|
|
162
|
-
|
|
163
|
-
@classmethod
|
|
164
|
-
def validate(cls, directives: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
|
-
validated_data: Dict[str, Any] = {}
|
|
166
|
-
|
|
167
|
-
for key, value in directives.items():
|
|
168
|
-
key = normalize_directive(key)
|
|
169
|
-
if key in TIME_FIELDS:
|
|
170
|
-
if value is None:
|
|
171
|
-
raise ValidationError(f"The directive '{key}' necessitates a value.")
|
|
172
|
-
|
|
173
|
-
if value[0] == '"' or value[-1] == '"':
|
|
174
|
-
raise ValidationError(f"The argument '{key}' should be an integer, but a quote was found.")
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
validated_data[key] = int(value)
|
|
178
|
-
except Exception:
|
|
179
|
-
raise ValidationError(f"The argument '{key}' should be an integer, but got '{value!r}'.")
|
|
180
|
-
elif key in BOOLEAN_FIELDS:
|
|
181
|
-
if value is not None:
|
|
182
|
-
raise ValidationError(f"The directive '{key}' should have no value, but it does.")
|
|
183
|
-
validated_data[key] = True
|
|
184
|
-
elif key in LIST_FIELDS:
|
|
185
|
-
if value is None:
|
|
186
|
-
validated_data[key] = True
|
|
187
|
-
else:
|
|
188
|
-
values = []
|
|
189
|
-
for list_value in value[1:-1].split(","):
|
|
190
|
-
if not list_value:
|
|
191
|
-
raise ValidationError("The list value must not be empty.")
|
|
192
|
-
list_value = strip_ows_around(list_value)
|
|
193
|
-
values.append(list_value)
|
|
194
|
-
validated_data[key] = values
|
|
195
|
-
|
|
196
|
-
return validated_data
|
|
197
|
-
|
|
198
|
-
def __repr__(self) -> str:
|
|
199
|
-
fields = ""
|
|
200
|
-
|
|
201
|
-
for key in TIME_FIELDS:
|
|
202
|
-
key = key.replace("-", "_")
|
|
203
|
-
value = getattr(self, key)
|
|
204
|
-
if value:
|
|
205
|
-
fields += f"{key}={value}, "
|
|
206
|
-
|
|
207
|
-
for key in BOOLEAN_FIELDS:
|
|
208
|
-
key = key.replace("-", "_")
|
|
209
|
-
value = getattr(self, key)
|
|
210
|
-
if value:
|
|
211
|
-
fields += f"{key}, "
|
|
212
|
-
|
|
213
|
-
fields = fields[:-2]
|
|
214
|
-
|
|
215
|
-
return f"<{type(self).__name__} {fields}>"
|
hishel/_lfu_cache.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from collections import OrderedDict
|
|
2
|
-
from typing import DefaultDict, Dict, Generic, Iterator, Tuple, TypeVar
|
|
3
|
-
|
|
4
|
-
K = TypeVar("K")
|
|
5
|
-
V = TypeVar("V")
|
|
6
|
-
|
|
7
|
-
__all__ = ["LFUCache"]
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class LFUCache(Generic[K, V]):
|
|
11
|
-
def __init__(self, capacity: int):
|
|
12
|
-
if capacity <= 0:
|
|
13
|
-
raise ValueError("Capacity must be positive")
|
|
14
|
-
|
|
15
|
-
self.capacity = capacity
|
|
16
|
-
self.cache: Dict[K, Tuple[V, int]] = {} # To store key-value pairs
|
|
17
|
-
self.freq_count: DefaultDict[int, OrderedDict[K, V]] = DefaultDict(
|
|
18
|
-
OrderedDict
|
|
19
|
-
) # To store frequency of each key
|
|
20
|
-
self.min_freq = 0 # To keep track of the minimum frequency
|
|
21
|
-
|
|
22
|
-
def get(self, key: K) -> V:
|
|
23
|
-
if key in self.cache:
|
|
24
|
-
value, freq = self.cache[key]
|
|
25
|
-
# Update frequency and move the key to the next frequency bucket
|
|
26
|
-
self.freq_count[freq].pop(key)
|
|
27
|
-
if not self.freq_count[freq]: # If the current frequency has no keys, update min_freq
|
|
28
|
-
del self.freq_count[freq]
|
|
29
|
-
if freq == self.min_freq:
|
|
30
|
-
self.min_freq += 1
|
|
31
|
-
freq += 1
|
|
32
|
-
self.freq_count[freq][key] = value
|
|
33
|
-
self.cache[key] = (value, freq)
|
|
34
|
-
return value
|
|
35
|
-
raise KeyError(f"Key {key} not found")
|
|
36
|
-
|
|
37
|
-
def put(self, key: K, value: V) -> None:
|
|
38
|
-
if key in self.cache:
|
|
39
|
-
_, freq = self.cache[key]
|
|
40
|
-
# Update frequency and move the key to the next frequency bucket
|
|
41
|
-
self.freq_count[freq].pop(key)
|
|
42
|
-
if not self.freq_count[freq]:
|
|
43
|
-
del self.freq_count[freq]
|
|
44
|
-
if freq == self.min_freq:
|
|
45
|
-
self.min_freq += 1
|
|
46
|
-
freq += 1
|
|
47
|
-
self.freq_count[freq][key] = value
|
|
48
|
-
self.cache[key] = (value, freq)
|
|
49
|
-
else:
|
|
50
|
-
# Check if cache is full, evict the least frequently used item
|
|
51
|
-
if len(self.cache) == self.capacity:
|
|
52
|
-
evicted_key, _ = self.freq_count[self.min_freq].popitem(last=False)
|
|
53
|
-
del self.cache[evicted_key]
|
|
54
|
-
|
|
55
|
-
# Add the new key-value pair with frequency 1
|
|
56
|
-
self.cache[key] = (value, 1)
|
|
57
|
-
self.freq_count[1][key] = value
|
|
58
|
-
self.min_freq = 1
|
|
59
|
-
|
|
60
|
-
def remove_key(self, key: K) -> None:
|
|
61
|
-
if key in self.cache:
|
|
62
|
-
_, freq = self.cache[key]
|
|
63
|
-
self.freq_count[freq].pop(key)
|
|
64
|
-
if not self.freq_count[freq]: # If the current frequency has no keys, update min_freq
|
|
65
|
-
del self.freq_count[freq]
|
|
66
|
-
if freq == self.min_freq:
|
|
67
|
-
self.min_freq += 1
|
|
68
|
-
del self.cache[key]
|
|
69
|
-
|
|
70
|
-
def __iter__(self) -> Iterator[K]:
|
|
71
|
-
yield from self.cache
|
hishel/_lmdb_types_.pyi
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
from contextlib import contextmanager
|
|
2
|
-
from typing import Any, Iterator
|
|
3
|
-
|
|
4
|
-
class Database: ...
|
|
5
|
-
|
|
6
|
-
class Transaction:
|
|
7
|
-
def get(self, key: bytes, *, db: Database | None = None) -> bytes | None:
|
|
8
|
-
"""
|
|
9
|
-
Get the value associated with the given key.
|
|
10
|
-
"""
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
def put(self, key: bytes, value: bytes, *, db: Database | None = None, dupdata: bool = False) -> None:
|
|
14
|
-
"""
|
|
15
|
-
Put a key-value pair into the database.
|
|
16
|
-
"""
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
def delete(self, key: bytes, *, db: Database | None = None) -> bool:
|
|
20
|
-
"""
|
|
21
|
-
Delete the key from the database.
|
|
22
|
-
"""
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
def cursor(self, db: Database) -> Any:
|
|
26
|
-
"""
|
|
27
|
-
Create a cursor for iterating over key-value pairs in the database.
|
|
28
|
-
"""
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
class Environment:
|
|
32
|
-
@contextmanager
|
|
33
|
-
def begin(self, *, db: Database | None = None, write: bool = False) -> Iterator[Transaction]:
|
|
34
|
-
"""
|
|
35
|
-
Begin a transaction in the environment.
|
|
36
|
-
"""
|
|
37
|
-
raise NotImplementedError("It's only for type hinting purposes")
|
|
38
|
-
|
|
39
|
-
def open_db(self, key: bytes, dupsort: bool = False) -> Database:
|
|
40
|
-
"""
|
|
41
|
-
Open a database within the environment.
|
|
42
|
-
"""
|
|
43
|
-
raise NotImplementedError("It's only for type hinting purposes")
|
|
44
|
-
|
|
45
|
-
def open(
|
|
46
|
-
path: str,
|
|
47
|
-
*,
|
|
48
|
-
max_dbs: int = 0,
|
|
49
|
-
) -> Environment:
|
|
50
|
-
"""
|
|
51
|
-
Open an LMDB environment at the specified path.
|
|
52
|
-
"""
|
|
53
|
-
raise NotImplementedError("It's only for type hinting purposes")
|
hishel/_s3.py
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import typing as tp
|
|
3
|
-
|
|
4
|
-
from anyio import to_thread
|
|
5
|
-
from botocore.exceptions import ClientError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_timestamp_in_ms() -> float:
|
|
9
|
-
return time.time() * 1000
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class S3Manager:
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
client: tp.Any,
|
|
16
|
-
bucket_name: str,
|
|
17
|
-
check_ttl_every: tp.Union[int, float],
|
|
18
|
-
is_binary: bool = False,
|
|
19
|
-
path_prefix: str = "hishel-",
|
|
20
|
-
):
|
|
21
|
-
self._client = client
|
|
22
|
-
self._bucket_name = bucket_name
|
|
23
|
-
self._is_binary = is_binary
|
|
24
|
-
self._last_cleaned = time.monotonic()
|
|
25
|
-
self._check_ttl_every = check_ttl_every
|
|
26
|
-
self._path_prefix = path_prefix
|
|
27
|
-
|
|
28
|
-
def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None:
|
|
29
|
-
path = self._path_prefix + path
|
|
30
|
-
if isinstance(data, str):
|
|
31
|
-
data = data.encode("utf-8")
|
|
32
|
-
|
|
33
|
-
created_at = None
|
|
34
|
-
if only_metadata:
|
|
35
|
-
try:
|
|
36
|
-
response = self._client.get_object(
|
|
37
|
-
Bucket=self._bucket_name,
|
|
38
|
-
Key=path,
|
|
39
|
-
)
|
|
40
|
-
created_at = response["Metadata"]["created_at"]
|
|
41
|
-
except Exception:
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
self._client.put_object(
|
|
45
|
-
Bucket=self._bucket_name,
|
|
46
|
-
Key=path,
|
|
47
|
-
Body=data,
|
|
48
|
-
Metadata={"created_at": created_at or str(get_timestamp_in_ms())},
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
def read_from(self, path: str) -> tp.Union[bytes, str]:
|
|
52
|
-
path = self._path_prefix + path
|
|
53
|
-
response = self._client.get_object(
|
|
54
|
-
Bucket=self._bucket_name,
|
|
55
|
-
Key=path,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
content = response["Body"].read()
|
|
59
|
-
|
|
60
|
-
if self._is_binary: # pragma: no cover
|
|
61
|
-
return tp.cast(bytes, content)
|
|
62
|
-
|
|
63
|
-
return tp.cast(str, content.decode("utf-8"))
|
|
64
|
-
|
|
65
|
-
def remove_expired(self, ttl: int, key: str) -> None:
|
|
66
|
-
path = self._path_prefix + key
|
|
67
|
-
|
|
68
|
-
if time.monotonic() - self._last_cleaned < self._check_ttl_every:
|
|
69
|
-
try:
|
|
70
|
-
response = self._client.get_object(Bucket=self._bucket_name, Key=path)
|
|
71
|
-
if get_timestamp_in_ms() - float(response["Metadata"]["created_at"]) > ttl:
|
|
72
|
-
self._client.delete_object(Bucket=self._bucket_name, Key=path)
|
|
73
|
-
return
|
|
74
|
-
except ClientError as e:
|
|
75
|
-
if e.response["Error"]["Code"] == "NoSuchKey":
|
|
76
|
-
return
|
|
77
|
-
raise e
|
|
78
|
-
|
|
79
|
-
self._last_cleaned = time.monotonic()
|
|
80
|
-
for obj in self._client.list_objects(Bucket=self._bucket_name).get("Contents", []):
|
|
81
|
-
if not obj["Key"].startswith(self._path_prefix): # pragma: no cover
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
metadata_obj = self._client.head_object(Bucket=self._bucket_name, Key=obj["Key"]).get("Metadata", {})
|
|
86
|
-
except ClientError as e:
|
|
87
|
-
if e.response["Error"]["Code"] == "404":
|
|
88
|
-
continue
|
|
89
|
-
|
|
90
|
-
if not metadata_obj or "created_at" not in metadata_obj:
|
|
91
|
-
continue
|
|
92
|
-
|
|
93
|
-
if get_timestamp_in_ms() - float(metadata_obj["created_at"]) > ttl:
|
|
94
|
-
self._client.delete_object(Bucket=self._bucket_name, Key=obj["Key"])
|
|
95
|
-
|
|
96
|
-
def remove_entry(self, key: str) -> None:
|
|
97
|
-
path = self._path_prefix + key
|
|
98
|
-
self._client.delete_object(Bucket=self._bucket_name, Key=path)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class AsyncS3Manager: # pragma: no cover
|
|
102
|
-
def __init__(
|
|
103
|
-
self,
|
|
104
|
-
client: tp.Any,
|
|
105
|
-
bucket_name: str,
|
|
106
|
-
check_ttl_every: tp.Union[int, float],
|
|
107
|
-
is_binary: bool = False,
|
|
108
|
-
path_prefix: str = "hishel-",
|
|
109
|
-
):
|
|
110
|
-
self._sync_manager = S3Manager(client, bucket_name, check_ttl_every, is_binary, path_prefix)
|
|
111
|
-
|
|
112
|
-
async def write_to(self, path: str, data: tp.Union[bytes, str], only_metadata: bool = False) -> None:
|
|
113
|
-
return await to_thread.run_sync(self._sync_manager.write_to, path, data, only_metadata)
|
|
114
|
-
|
|
115
|
-
async def read_from(self, path: str) -> tp.Union[bytes, str]:
|
|
116
|
-
return await to_thread.run_sync(self._sync_manager.read_from, path)
|
|
117
|
-
|
|
118
|
-
async def remove_expired(self, ttl: int, key: str) -> None:
|
|
119
|
-
return await to_thread.run_sync(self._sync_manager.remove_expired, ttl, key)
|
|
120
|
-
|
|
121
|
-
async def remove_entry(self, key: str) -> None:
|
|
122
|
-
return await to_thread.run_sync(self._sync_manager.remove_entry, key)
|