hishel 0.0.31__tar.gz → 0.0.33__tar.gz
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-0.0.31 → hishel-0.0.33}/CHANGELOG.md +8 -0
- {hishel-0.0.31 → hishel-0.0.33}/PKG-INFO +9 -1
- {hishel-0.0.31 → hishel-0.0.33}/hishel/__init__.py +1 -1
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_controller.py +197 -7
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_utils.py +11 -0
- {hishel-0.0.31 → hishel-0.0.33}/.gitignore +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/LICENSE +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/README.md +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/__init__.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/_client.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/_mock.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/_pool.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/_storages.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_async/_transports.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_exceptions.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_files.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_headers.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_lfu_cache.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_s3.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_serializers.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/__init__.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/_client.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/_mock.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/_pool.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/_storages.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_sync/_transports.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/_synchronization.py +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/hishel/py.typed +0 -0
- {hishel-0.0.31 → hishel-0.0.33}/pyproject.toml +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.33 (4th Oct, 2024)
|
|
4
|
+
|
|
5
|
+
- Added a [Logging](https://hishel.com/advanced/logging/) section to the documentation.
|
|
6
|
+
|
|
7
|
+
## 0.0.32 (27th Sep, 2024)
|
|
8
|
+
|
|
9
|
+
- Don't raise an exception if the `Date` header is not present. (#273)
|
|
10
|
+
|
|
3
11
|
## 0.0.31 (22nd Sep, 2024)
|
|
4
12
|
|
|
5
13
|
- Ignore file not found error when cleaning up a file storage. (#264)
|
|
@@ -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,14 @@ 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
|
+
|
|
182
|
+
## 0.0.32 (27th Sep, 2024)
|
|
183
|
+
|
|
184
|
+
- Don't raise an exception if the `Date` header is not present. (#273)
|
|
185
|
+
|
|
178
186
|
## 0.0.31 (22nd Sep, 2024)
|
|
179
187
|
|
|
180
188
|
- Ignore file not found error when cleaning up a file storage. (#264)
|
|
@@ -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
|
|
|
@@ -79,8 +83,10 @@ def get_heuristic_freshness(response: Response, clock: "BaseClock") -> int:
|
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
def get_age(response: Response, clock: "BaseClock") -> int:
|
|
82
|
-
if not header_presents(response.headers, b"date"):
|
|
83
|
-
|
|
86
|
+
if not header_presents(response.headers, b"date"):
|
|
87
|
+
# If the response does not have a date header, then it is impossible to calculate the age.
|
|
88
|
+
# Instead of raising an exception, we return infinity to be sure that the response is not considered fresh.
|
|
89
|
+
return float("inf") # type: ignore
|
|
84
90
|
|
|
85
91
|
date = parse_date(extract_header_values_decoded(response.headers, b"date")[0])
|
|
86
92
|
|
|
@@ -150,16 +156,41 @@ class Controller:
|
|
|
150
156
|
force_cache = request.extensions.get("force_cache", None)
|
|
151
157
|
|
|
152
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
|
+
)
|
|
153
166
|
return False
|
|
154
167
|
|
|
155
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
|
+
)
|
|
156
175
|
return True
|
|
157
176
|
|
|
158
177
|
# the request method is understood by the cache
|
|
159
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
|
+
)
|
|
160
185
|
return False
|
|
161
186
|
|
|
162
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
|
+
)
|
|
163
194
|
return True
|
|
164
195
|
|
|
165
196
|
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"cache-control"))
|
|
@@ -167,21 +198,53 @@ class Controller:
|
|
|
167
198
|
|
|
168
199
|
# the response status code is final
|
|
169
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
|
+
)
|
|
170
207
|
return False
|
|
171
208
|
|
|
172
209
|
# the no-store cache directive is not present (see Section 5.2.2.5)
|
|
173
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
|
+
)
|
|
174
217
|
return False
|
|
175
218
|
|
|
176
219
|
# note that the must-understand cache directive overrides
|
|
177
220
|
# no-store in certain circumstances; see Section 5.2.2.3.
|
|
178
|
-
if response_cache_control.no_store
|
|
179
|
-
|
|
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
|
|
180
237
|
|
|
181
238
|
# a shared cache must not store a response with private directive
|
|
182
239
|
# Note that we do not implement special handling for the qualified form,
|
|
183
240
|
# which would only forbid storing specified headers.
|
|
184
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
|
+
)
|
|
185
248
|
return False
|
|
186
249
|
|
|
187
250
|
expires_presents = header_presents(response.headers, b"expires")
|
|
@@ -194,6 +257,12 @@ class Controller:
|
|
|
194
257
|
# - a cache extension that allows it to be cached (see Section 5.2.3); or
|
|
195
258
|
# - a status code that is defined as heuristically cacheable (see Section 4.2.2).
|
|
196
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
|
+
)
|
|
197
266
|
return True
|
|
198
267
|
|
|
199
268
|
if not any(
|
|
@@ -204,8 +273,20 @@ class Controller:
|
|
|
204
273
|
response_cache_control.max_age is not None,
|
|
205
274
|
]
|
|
206
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
|
+
)
|
|
207
282
|
return False
|
|
208
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
|
+
)
|
|
209
290
|
# response is a cachable!
|
|
210
291
|
return True
|
|
211
292
|
|
|
@@ -221,11 +302,23 @@ class Controller:
|
|
|
221
302
|
|
|
222
303
|
if header_presents(response.headers, b"last-modified"):
|
|
223
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
|
+
)
|
|
224
311
|
else:
|
|
225
312
|
last_modified = None
|
|
226
313
|
|
|
227
314
|
if header_presents(response.headers, b"etag"):
|
|
228
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
|
+
)
|
|
229
322
|
else:
|
|
230
323
|
etag = None
|
|
231
324
|
|
|
@@ -278,6 +371,12 @@ class Controller:
|
|
|
278
371
|
# Use of responses with status codes 301 and 308 is always
|
|
279
372
|
# legal as long as they don't adhere to any caching rules.
|
|
280
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
|
+
)
|
|
281
380
|
return response
|
|
282
381
|
|
|
283
382
|
response_cache_control = parse_cache_control(extract_header_values_decoded(response.headers, b"Cache-Control"))
|
|
@@ -287,11 +386,23 @@ class Controller:
|
|
|
287
386
|
# response (if any) match those presented (see Section 4.1)
|
|
288
387
|
if not self._validate_vary(request=request, response=response, original_request=original_request):
|
|
289
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
|
+
)
|
|
290
395
|
return None # pragma: no cover
|
|
291
396
|
|
|
292
397
|
# !!! this should be after the "vary" header validation.
|
|
293
398
|
force_cache = request.extensions.get("force_cache", None)
|
|
294
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
|
+
)
|
|
295
406
|
return response
|
|
296
407
|
|
|
297
408
|
# the stored response does not contain the
|
|
@@ -303,15 +414,58 @@ class Controller:
|
|
|
303
414
|
or response_cache_control.must_revalidate
|
|
304
415
|
or request_cache_control.no_cache
|
|
305
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)
|
|
306
440
|
self._make_request_conditional(request=request, response=response)
|
|
307
441
|
return request
|
|
308
442
|
|
|
309
443
|
freshness_lifetime = get_freshness_lifetime(response)
|
|
310
444
|
|
|
311
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
|
+
)
|
|
312
453
|
if self._allow_heuristics and response.status in HEURISTICALLY_CACHEABLE_STATUS_CODES:
|
|
313
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
|
+
)
|
|
314
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
|
+
)
|
|
315
469
|
# If Freshness cannot be calculated, then send the request
|
|
316
470
|
self._make_request_conditional(request=request, response=response)
|
|
317
471
|
return request
|
|
@@ -326,6 +480,13 @@ class Controller:
|
|
|
326
480
|
# be fresh for at least the specified number of seconds.
|
|
327
481
|
if request_cache_control.min_fresh is not None:
|
|
328
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
|
+
)
|
|
329
490
|
return None
|
|
330
491
|
|
|
331
492
|
# The max-stale request directive indicates that the
|
|
@@ -338,7 +499,21 @@ class Controller:
|
|
|
338
499
|
exceeded_freshness_lifetime = age - freshness_lifetime
|
|
339
500
|
|
|
340
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
|
+
)
|
|
341
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
|
|
342
517
|
|
|
343
518
|
# The max-age request directive indicates that
|
|
344
519
|
# the client prefers a response whose age is
|
|
@@ -347,9 +522,12 @@ class Controller:
|
|
|
347
522
|
# the client does not wish to receive a stale response.
|
|
348
523
|
if request_cache_control.max_age is not None:
|
|
349
524
|
if request_cache_control.max_age < age:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
)
|
|
353
531
|
return None
|
|
354
532
|
|
|
355
533
|
# the stored response is one of the following:
|
|
@@ -357,8 +535,20 @@ class Controller:
|
|
|
357
535
|
# allowed to be served stale (see Section 4.2.4), or
|
|
358
536
|
# successfully validated (see Section 4.3).
|
|
359
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
|
+
)
|
|
360
544
|
return response
|
|
361
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
|
+
)
|
|
362
552
|
# Otherwise, make a conditional request
|
|
363
553
|
self._make_request_conditional(request=request, response=response)
|
|
364
554
|
return request
|
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|