hishel 0.0.32__py3-none-any.whl → 0.0.33__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 +1 -1
- hishel/_controller.py +193 -5
- hishel/_utils.py +11 -0
- {hishel-0.0.32.dist-info → hishel-0.0.33.dist-info}/METADATA +5 -1
- {hishel-0.0.32.dist-info → hishel-0.0.33.dist-info}/RECORD +7 -7
- {hishel-0.0.32.dist-info → hishel-0.0.33.dist-info}/WHEEL +0 -0
- {hishel-0.0.32.dist-info → hishel-0.0.33.dist-info}/licenses/LICENSE +0 -0
hishel/__init__.py
CHANGED
hishel/_controller.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import typing as tp
|
|
2
3
|
|
|
3
4
|
from httpcore import Request, Response
|
|
@@ -10,10 +11,13 @@ from ._utils import (
|
|
|
10
11
|
extract_header_values,
|
|
11
12
|
extract_header_values_decoded,
|
|
12
13
|
generate_key,
|
|
14
|
+
get_safe_url,
|
|
13
15
|
header_presents,
|
|
14
16
|
parse_date,
|
|
15
17
|
)
|
|
16
18
|
|
|
19
|
+
logger = logging.getLogger("hishel.controller")
|
|
20
|
+
|
|
17
21
|
HEURISTICALLY_CACHEABLE_STATUS_CODES = (200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501)
|
|
18
22
|
HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"]
|
|
19
23
|
|
|
@@ -152,16 +156,41 @@ class Controller:
|
|
|
152
156
|
force_cache = request.extensions.get("force_cache", None)
|
|
153
157
|
|
|
154
158
|
if response.status not in self._cacheable_status_codes:
|
|
159
|
+
logger.debug(
|
|
160
|
+
(
|
|
161
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
162
|
+
f"as not cachable since its status code ({response.status})"
|
|
163
|
+
" is not in the list of cacheable status codes."
|
|
164
|
+
)
|
|
165
|
+
)
|
|
155
166
|
return False
|
|
156
167
|
|
|
157
168
|
if response.status in (301, 308):
|
|
169
|
+
logger.debug(
|
|
170
|
+
(
|
|
171
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
172
|
+
"as cachable since its status code is a permanent redirect."
|
|
173
|
+
)
|
|
174
|
+
)
|
|
158
175
|
return True
|
|
159
176
|
|
|
160
177
|
# the request method is understood by the cache
|
|
161
178
|
if method not in self._cacheable_methods:
|
|
179
|
+
logger.debug(
|
|
180
|
+
(
|
|
181
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
182
|
+
f"as not cachable since the request method ({method}) is not in the list of cacheable methods."
|
|
183
|
+
)
|
|
184
|
+
)
|
|
162
185
|
return False
|
|
163
186
|
|
|
164
187
|
if force_cache if force_cache is not None else self._force_cache:
|
|
188
|
+
logger.debug(
|
|
189
|
+
(
|
|
190
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
191
|
+
"as cachable since the request is forced to use the cache."
|
|
192
|
+
)
|
|
193
|
+
)
|
|
165
194
|
return True
|
|
166
195
|
|
|
167
196
|
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control"))
|
|
@@ -169,21 +198,53 @@ class Controller:
|
|
|
169
198
|
|
|
170
199
|
# the response status code is final
|
|
171
200
|
if response.status // 100 == 1:
|
|
201
|
+
logger.debug(
|
|
202
|
+
(
|
|
203
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
204
|
+
"as not cachable since its status code is informational."
|
|
205
|
+
)
|
|
206
|
+
)
|
|
172
207
|
return False
|
|
173
208
|
|
|
174
209
|
# the no-store cache directive is not present (see Section 5.2.2.5)
|
|
175
210
|
if request_cache_control.no_store:
|
|
211
|
+
logger.debug(
|
|
212
|
+
(
|
|
213
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
214
|
+
"as not cachable since the request contains the no-store directive."
|
|
215
|
+
)
|
|
216
|
+
)
|
|
176
217
|
return False
|
|
177
218
|
|
|
178
219
|
# note that the must-understand cache directive overrides
|
|
179
220
|
# no-store in certain circumstances; see Section 5.2.2.3.
|
|
180
|
-
if response_cache_control.no_store
|
|
181
|
-
|
|
221
|
+
if response_cache_control.no_store:
|
|
222
|
+
if response_cache_control.must_understand:
|
|
223
|
+
logger.debug(
|
|
224
|
+
(
|
|
225
|
+
f"Skipping the no-store directive for the resource located at {get_safe_url(request.url)} "
|
|
226
|
+
"since the response contains the must-understand directive."
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
logger.debug(
|
|
231
|
+
(
|
|
232
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
233
|
+
"as not cachable since the response contains the no-store directive."
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
return False
|
|
182
237
|
|
|
183
238
|
# a shared cache must not store a response with private directive
|
|
184
239
|
# Note that we do not implement special handling for the qualified form,
|
|
185
240
|
# which would only forbid storing specified headers.
|
|
186
241
|
if not self._cache_private and response_cache_control.private:
|
|
242
|
+
logger.debug(
|
|
243
|
+
(
|
|
244
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
245
|
+
"as not cachable since the response contains the private directive."
|
|
246
|
+
)
|
|
247
|
+
)
|
|
187
248
|
return False
|
|
188
249
|
|
|
189
250
|
expires_presents = header_presents(response.headers, b"expires")
|
|
@@ -196,6 +257,12 @@ class Controller:
|
|
|
196
257
|
# - a cache extension that allows it to be cached (see Section 5.2.3); or
|
|
197
258
|
# - a status code that is defined as heuristically cacheable (see Section 4.2.2).
|
|
198
259
|
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
|
|
260
|
+
logger.debug(
|
|
261
|
+
(
|
|
262
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
263
|
+
"as cachable since its status code is heuristically cacheable."
|
|
264
|
+
)
|
|
265
|
+
)
|
|
199
266
|
return True
|
|
200
267
|
|
|
201
268
|
if not any(
|
|
@@ -206,8 +273,20 @@ class Controller:
|
|
|
206
273
|
response_cache_control.max_age is not None,
|
|
207
274
|
]
|
|
208
275
|
):
|
|
276
|
+
logger.debug(
|
|
277
|
+
(
|
|
278
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
279
|
+
"as not cachable since it does not contain any of the required cache directives."
|
|
280
|
+
)
|
|
281
|
+
)
|
|
209
282
|
return False
|
|
210
283
|
|
|
284
|
+
logger.debug(
|
|
285
|
+
(
|
|
286
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
287
|
+
"as cachable since it meets the criteria for being stored in the cache."
|
|
288
|
+
)
|
|
289
|
+
)
|
|
211
290
|
# response is a cachable!
|
|
212
291
|
return True
|
|
213
292
|
|
|
@@ -223,11 +302,23 @@ class Controller:
|
|
|
223
302
|
|
|
224
303
|
if header_presents(response.headers, b"last-modified"):
|
|
225
304
|
last_modified = extract_header_values(response.headers, b"last-modified", single=True)[0]
|
|
305
|
+
logger.debug(
|
|
306
|
+
(
|
|
307
|
+
f"Adding the 'If-Modified-Since' header with the value of '{last_modified.decode('ascii')}' "
|
|
308
|
+
f"to the request for the resource located at {get_safe_url(request.url)}."
|
|
309
|
+
)
|
|
310
|
+
)
|
|
226
311
|
else:
|
|
227
312
|
last_modified = None
|
|
228
313
|
|
|
229
314
|
if header_presents(response.headers, b"etag"):
|
|
230
315
|
etag = extract_header_values(response.headers, b"etag", single=True)[0]
|
|
316
|
+
logger.debug(
|
|
317
|
+
(
|
|
318
|
+
f"Adding the 'If-None-Match' header with the value of '{etag.decode('ascii')}' "
|
|
319
|
+
f"to the request for the resource located at {get_safe_url(request.url)}."
|
|
320
|
+
)
|
|
321
|
+
)
|
|
231
322
|
else:
|
|
232
323
|
etag = None
|
|
233
324
|
|
|
@@ -280,6 +371,12 @@ class Controller:
|
|
|
280
371
|
# Use of responses with status codes 301 and 308 is always
|
|
281
372
|
# legal as long as they don't adhere to any caching rules.
|
|
282
373
|
if response.status in (301, 308):
|
|
374
|
+
logger.debug(
|
|
375
|
+
(
|
|
376
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
377
|
+
"as valid for cache use since its status code is a permanent redirect."
|
|
378
|
+
)
|
|
379
|
+
)
|
|
283
380
|
return response
|
|
284
381
|
|
|
285
382
|
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
|
|
@@ -289,11 +386,23 @@ class Controller:
|
|
|
289
386
|
# response (if any) match those presented (see Section 4.1)
|
|
290
387
|
if not self._validate_vary(request=request, response=response, original_request=original_request):
|
|
291
388
|
# If the vary headers does not match, then do not use the response
|
|
389
|
+
logger.debug(
|
|
390
|
+
(
|
|
391
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
392
|
+
"as invalid for cache use since the vary headers do not match."
|
|
393
|
+
)
|
|
394
|
+
)
|
|
292
395
|
return None # pragma: no cover
|
|
293
396
|
|
|
294
397
|
# !!! this should be after the "vary" header validation.
|
|
295
398
|
force_cache = request.extensions.get("force_cache", None)
|
|
296
399
|
if force_cache if force_cache is not None else self._force_cache:
|
|
400
|
+
logger.debug(
|
|
401
|
+
(
|
|
402
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
403
|
+
"as valid for cache use since the request is forced to use the cache."
|
|
404
|
+
)
|
|
405
|
+
)
|
|
297
406
|
return response
|
|
298
407
|
|
|
299
408
|
# the stored response does not contain the
|
|
@@ -305,15 +414,58 @@ class Controller:
|
|
|
305
414
|
or response_cache_control.must_revalidate
|
|
306
415
|
or request_cache_control.no_cache
|
|
307
416
|
):
|
|
417
|
+
if self._always_revalidate:
|
|
418
|
+
log_text = (
|
|
419
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
420
|
+
"as needing revalidation since the cache is set to always revalidate."
|
|
421
|
+
)
|
|
422
|
+
elif response_cache_control.no_cache:
|
|
423
|
+
log_text = (
|
|
424
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
425
|
+
"as needing revalidation since the response contains the no-cache directive."
|
|
426
|
+
)
|
|
427
|
+
elif response_cache_control.must_revalidate:
|
|
428
|
+
log_text = (
|
|
429
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
430
|
+
"as needing revalidation since the response contains the must-revalidate directive."
|
|
431
|
+
)
|
|
432
|
+
elif request_cache_control.no_cache:
|
|
433
|
+
log_text = (
|
|
434
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
435
|
+
"as needing revalidation since the request contains the no-cache directive."
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
assert False, "Unreachable code " # pragma: no cover
|
|
439
|
+
logger.debug(log_text)
|
|
308
440
|
self._make_request_conditional(request=request, response=response)
|
|
309
441
|
return request
|
|
310
442
|
|
|
311
443
|
freshness_lifetime = get_freshness_lifetime(response)
|
|
312
444
|
|
|
313
445
|
if freshness_lifetime is None:
|
|
446
|
+
logger.debug(
|
|
447
|
+
(
|
|
448
|
+
"Could not determine the freshness lifetime of "
|
|
449
|
+
f"the resource located at {get_safe_url(request.url)}, "
|
|
450
|
+
"trying to use heuristics to calculate it."
|
|
451
|
+
)
|
|
452
|
+
)
|
|
314
453
|
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
|
|
315
454
|
freshness_lifetime = get_heuristic_freshness(response=response, clock=self._clock)
|
|
455
|
+
logger.debug(
|
|
456
|
+
(
|
|
457
|
+
f"Successfully calculated the freshness lifetime of the resource located at "
|
|
458
|
+
f"{get_safe_url(request.url)} using heuristics."
|
|
459
|
+
)
|
|
460
|
+
)
|
|
316
461
|
else:
|
|
462
|
+
logger.debug(
|
|
463
|
+
(
|
|
464
|
+
"Could not calculate the freshness lifetime of "
|
|
465
|
+
f"the resource located at {get_safe_url(request.url)}. "
|
|
466
|
+
"Making a conditional request to revalidate the response."
|
|
467
|
+
)
|
|
468
|
+
)
|
|
317
469
|
# If Freshness cannot be calculated, then send the request
|
|
318
470
|
self._make_request_conditional(request=request, response=response)
|
|
319
471
|
return request
|
|
@@ -328,6 +480,13 @@ class Controller:
|
|
|
328
480
|
# be fresh for at least the specified number of seconds.
|
|
329
481
|
if request_cache_control.min_fresh is not None:
|
|
330
482
|
if freshness_lifetime < (age + request_cache_control.min_fresh):
|
|
483
|
+
logger.debug(
|
|
484
|
+
(
|
|
485
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
486
|
+
"as invalid for cache use since the time left for "
|
|
487
|
+
"freshness is less than the min-fresh directive."
|
|
488
|
+
)
|
|
489
|
+
)
|
|
331
490
|
return None
|
|
332
491
|
|
|
333
492
|
# The max-stale request directive indicates that the
|
|
@@ -340,7 +499,21 @@ class Controller:
|
|
|
340
499
|
exceeded_freshness_lifetime = age - freshness_lifetime
|
|
341
500
|
|
|
342
501
|
if request_cache_control.max_stale < exceeded_freshness_lifetime:
|
|
502
|
+
logger.debug(
|
|
503
|
+
(
|
|
504
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
505
|
+
"as invalid for cache use since the freshness lifetime has been exceeded more than max-stale."
|
|
506
|
+
)
|
|
507
|
+
)
|
|
343
508
|
return None
|
|
509
|
+
else:
|
|
510
|
+
logger.debug(
|
|
511
|
+
(
|
|
512
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
513
|
+
"as valid for cache use since the freshness lifetime has been exceeded less than max-stale."
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return response
|
|
344
517
|
|
|
345
518
|
# The max-age request directive indicates that
|
|
346
519
|
# the client prefers a response whose age is
|
|
@@ -349,9 +522,12 @@ class Controller:
|
|
|
349
522
|
# the client does not wish to receive a stale response.
|
|
350
523
|
if request_cache_control.max_age is not None:
|
|
351
524
|
if request_cache_control.max_age < age:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
525
|
+
logger.debug(
|
|
526
|
+
(
|
|
527
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
528
|
+
"as invalid for cache use since the age of the response exceeds the max-age directive."
|
|
529
|
+
)
|
|
530
|
+
)
|
|
355
531
|
return None
|
|
356
532
|
|
|
357
533
|
# the stored response is one of the following:
|
|
@@ -359,8 +535,20 @@ class Controller:
|
|
|
359
535
|
# allowed to be served stale (see Section 4.2.4), or
|
|
360
536
|
# successfully validated (see Section 4.3).
|
|
361
537
|
if is_fresh:
|
|
538
|
+
logger.debug(
|
|
539
|
+
(
|
|
540
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
541
|
+
"as valid for cache use since it is fresh."
|
|
542
|
+
)
|
|
543
|
+
)
|
|
362
544
|
return response
|
|
363
545
|
else:
|
|
546
|
+
logger.debug(
|
|
547
|
+
(
|
|
548
|
+
f"Considering the resource located at {get_safe_url(request.url)} "
|
|
549
|
+
"as needing revalidation since it is not fresh."
|
|
550
|
+
)
|
|
551
|
+
)
|
|
364
552
|
# Otherwise, make a conditional request
|
|
365
553
|
self._make_request_conditional(request=request, response=response)
|
|
366
554
|
return request
|
hishel/_utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from hashlib import blake2b
|
|
|
6
6
|
|
|
7
7
|
import anyio
|
|
8
8
|
import httpcore
|
|
9
|
+
import httpx
|
|
9
10
|
|
|
10
11
|
HEADERS_ENCODING = "iso-8859-1"
|
|
11
12
|
|
|
@@ -33,6 +34,16 @@ def normalized_url(url: tp.Union[httpcore.URL, str, bytes]) -> str:
|
|
|
33
34
|
assert False, "Invalid type for `normalized_url`" # pragma: no cover
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def get_safe_url(url: httpcore.URL) -> str:
|
|
38
|
+
httpx_url = httpx.URL(bytes(url).decode("ascii"))
|
|
39
|
+
|
|
40
|
+
schema = httpx_url.scheme
|
|
41
|
+
host = httpx_url.host
|
|
42
|
+
path = httpx_url.path
|
|
43
|
+
|
|
44
|
+
return f"{schema}://{host}{path}"
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
def generate_key(request: httpcore.Request, body: bytes = b"") -> str:
|
|
37
48
|
encoded_url = normalized_url(request.url).encode("ascii")
|
|
38
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: hishel
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.33
|
|
4
4
|
Summary: Persistent cache implementation for httpx and httpcore
|
|
5
5
|
Project-URL: Homepage, https://hishel.com
|
|
6
6
|
Project-URL: Source, https://github.com/karpetrosyan/hishel
|
|
@@ -175,6 +175,10 @@ Help us grow and continue developing good software for you ❤️
|
|
|
175
175
|
|
|
176
176
|
# Changelog
|
|
177
177
|
|
|
178
|
+
## 0.0.33 (4th Oct, 2024)
|
|
179
|
+
|
|
180
|
+
- Added a [Logging](https://hishel.com/advanced/logging/) section to the documentation.
|
|
181
|
+
|
|
178
182
|
## 0.0.32 (27th Sep, 2024)
|
|
179
183
|
|
|
180
184
|
- Don't raise an exception if the `Date` header is not present. (#273)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
hishel/__init__.py,sha256=
|
|
2
|
-
hishel/_controller.py,sha256=
|
|
1
|
+
hishel/__init__.py,sha256=PqLQGkHTPzUnEDcEGVCMakiBjeU7jvbg4Pu4opBqbrs,369
|
|
2
|
+
hishel/_controller.py,sha256=F7hj1ePUvau2Wj5r6zzdYr8UXPq4osfmMxkmx_Ig2L8,24390
|
|
3
3
|
hishel/_exceptions.py,sha256=qbg55RNlzwhv5JreWY9Zog_zmmiKdn5degtqJKijuRs,198
|
|
4
4
|
hishel/_files.py,sha256=7J5uX7Nnzd7QQWfYuDGh8v6XGLG3eUDBjoJZ4aTaY1c,2228
|
|
5
5
|
hishel/_headers.py,sha256=TWuHi7sRoeS2xxdNGujKmqWtgncUqfhNGCgHKYpRU-I,7329
|
|
@@ -7,7 +7,7 @@ hishel/_lfu_cache.py,sha256=GBxToQI8u_a9TzYnLlZMLhgZ8Lb83boPHzTvIgqV6pA,2707
|
|
|
7
7
|
hishel/_s3.py,sha256=JqRlygITK5uAryviC15HZKQlKY7etUOPWcazTJeYKBI,3736
|
|
8
8
|
hishel/_serializers.py,sha256=gepVb8JC4aBkGw9kLcbAsyo-1XgK_lzTssLr_8av4SQ,11640
|
|
9
9
|
hishel/_synchronization.py,sha256=xOmU9_8KAWTAv3r8EpqPISrtSF3slyh1J0Sc7ZQO1rg,897
|
|
10
|
-
hishel/_utils.py,sha256=
|
|
10
|
+
hishel/_utils.py,sha256=lnnFmDnKNDb_-OxlZncOJK5IEbtJeTywpITpYFf6WOk,2736
|
|
11
11
|
hishel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
hishel/_async/__init__.py,sha256=_oAltH4emAtUF7_9SSxz_KKGYzSXL34o_EGgGvoPG7E,187
|
|
13
13
|
hishel/_async/_client.py,sha256=AkVSSbNTTHmK0gX6PRYVQ-3aDbuCX2Im4VKbLkwLiBU,1101
|
|
@@ -21,7 +21,7 @@ hishel/_sync/_mock.py,sha256=im88tZr-XhP9BpzvIt3uOjndAlNcJvFP7Puv3H-6lKU,1430
|
|
|
21
21
|
hishel/_sync/_pool.py,sha256=VcAknzyAL2i4-zcyE2fOTmTjfBZ2wkBVNYTvSw0OjVQ,7940
|
|
22
22
|
hishel/_sync/_storages.py,sha256=RYzYXqnv0o2JO3RoEmlEUp0yOg_ungXfz4dLN7UTpIQ,27909
|
|
23
23
|
hishel/_sync/_transports.py,sha256=G3_8SdPwlnrHZRvE1gqFLE4oZadVqNgg5mvxghDMih0,10838
|
|
24
|
-
hishel-0.0.
|
|
25
|
-
hishel-0.0.
|
|
26
|
-
hishel-0.0.
|
|
27
|
-
hishel-0.0.
|
|
24
|
+
hishel-0.0.33.dist-info/METADATA,sha256=fVzp5e0XfMlqDkvEzZFfS3F_QfjrUA9wi-A696LNjkc,11694
|
|
25
|
+
hishel-0.0.33.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
26
|
+
hishel-0.0.33.dist-info/licenses/LICENSE,sha256=1qQj7pE0V2O9OIedvyOgLGLvZLaPd3nFEup3IBEOZjQ,1493
|
|
27
|
+
hishel-0.0.33.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|