withcache 0.8.7__tar.gz → 0.8.9__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.
- {withcache-0.8.7 → withcache-0.8.9}/PKG-INFO +1 -1
- {withcache-0.8.7 → withcache-0.8.9}/deploy/Containerfile +3 -2
- withcache-0.8.9/deploy/compose.local-build.yml +11 -0
- withcache-0.8.9/deploy/compose.yml +23 -0
- withcache-0.8.9/deploy/envvars.example +17 -0
- {withcache-0.8.7 → withcache-0.8.9}/shim/build.zig.zon +1 -1
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/__init__.py +1 -1
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/server.py +33 -2
- {withcache-0.8.7 → withcache-0.8.9}/tests/test_withcache.py +89 -0
- withcache-0.8.7/deploy/compose.yml +0 -28
- {withcache-0.8.7 → withcache-0.8.9}/.gitignore +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/LICENSE +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/README.md +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/hatch_build.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/pyproject.toml +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/shim/build.zig +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/shim/shim.zig +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/_shim.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/client.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/oras.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/static/bootstrap-icons.min.css +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/static/bootstrap.min.css +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/tests/test_differential.py +0 -0
- {withcache-0.8.7 → withcache-0.8.9}/tests/test_oras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.9
|
|
4
4
|
Summary: Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/withcache
|
|
6
6
|
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
# withcache cache-host.
|
|
1
|
+
# withcache cache-host. From-source build (rare; the reference
|
|
2
|
+
# compose.yml uses the published ``ghcr.io/safl/withcache:latest``
|
|
3
|
+
# image instead). Build context is the repo root:
|
|
2
4
|
# podman build -f deploy/Containerfile -t withcache .
|
|
3
|
-
# (deploy/compose.yml sets the context for you.)
|
|
4
5
|
FROM python:3.12-slim
|
|
5
6
|
|
|
6
7
|
# Install the package (no third-party deps) to get the withcache-server command.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Overlay compose file for from-source dev iteration:
|
|
2
|
+
# podman compose -f deploy/compose.yml -f deploy/compose.local-build.yml up -d --build
|
|
3
|
+
# The reference deploy/compose.yml uses the published image
|
|
4
|
+
# ``ghcr.io/safl/withcache:latest``; this overlay swaps in a local
|
|
5
|
+
# build from the repo root's Containerfile.
|
|
6
|
+
services:
|
|
7
|
+
withcache:
|
|
8
|
+
build:
|
|
9
|
+
context: ..
|
|
10
|
+
dockerfile: deploy/Containerfile
|
|
11
|
+
image: withcache:local
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Reference compose stack for withcache.
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# cp envvars.example envvars && "${EDITOR:-vi}" envvars # set the knobs
|
|
5
|
+
# podman compose --env-file envvars up -d
|
|
6
|
+
# # http://<host>:8081/ (operator UI)
|
|
7
|
+
services:
|
|
8
|
+
withcache:
|
|
9
|
+
image: ghcr.io/safl/withcache:latest
|
|
10
|
+
restart: unless-stopped
|
|
11
|
+
container_name: withcache
|
|
12
|
+
environment:
|
|
13
|
+
WITHCACHE_ADMIN_PASSWORD: "${WITHCACHE_ADMIN_PASSWORD:-}"
|
|
14
|
+
WITHCACHE_SESSION_SECRET: "${WITHCACHE_SESSION_SECRET:-}"
|
|
15
|
+
WITHCACHE_CATALOG_URL: "${WITHCACHE_CATALOG_URL:-}"
|
|
16
|
+
ports:
|
|
17
|
+
- "${HOST_HTTP_PORT:-8081}:8081"
|
|
18
|
+
volumes:
|
|
19
|
+
# Persistent: blob store + cache.db + session-secret survive restarts.
|
|
20
|
+
- withcache-data:/data
|
|
21
|
+
|
|
22
|
+
volumes:
|
|
23
|
+
withcache-data:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copy to ``envvars`` and edit. The compose stack reads this via --env-file.
|
|
2
|
+
|
|
3
|
+
# Gate the operator UI + control-plane writes. Empty = open (logs a
|
|
4
|
+
# startup warning; only safe on a LAN-internal withcache).
|
|
5
|
+
WITHCACHE_ADMIN_PASSWORD=
|
|
6
|
+
|
|
7
|
+
# Optional: pin the signed-cookie HMAC key. Unset = auto-generate a
|
|
8
|
+
# fresh 64-hex key at first start and persist it under /data. Set
|
|
9
|
+
# explicitly to keep cookies valid across a container rebuild that
|
|
10
|
+
# wipes the data volume, or to rotate the secret on demand.
|
|
11
|
+
#WITHCACHE_SESSION_SECRET=
|
|
12
|
+
|
|
13
|
+
# Optional: pin the image-catalog URL. When set, the env value wins
|
|
14
|
+
# over any /admin/catalog_set_url operator override so a locked-down
|
|
15
|
+
# deploy stays locked. Unset uses the default (safl/nosi's
|
|
16
|
+
# ``catalog.toml`` on GitHub).
|
|
17
|
+
#WITHCACHE_CATALOG_URL=
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.name = .withcache_shim,
|
|
3
3
|
// Zig requires a literal here; keep it in lockstep with the project's
|
|
4
4
|
// single source (src/withcache/__init__.py) via `make bump` / `make version-check`.
|
|
5
|
-
.version = "0.8.
|
|
5
|
+
.version = "0.8.9",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -17,6 +17,6 @@ All modules are stdlib-only and self-contained.
|
|
|
17
17
|
from . import oras
|
|
18
18
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
19
19
|
|
|
20
|
-
__version__ = "0.8.
|
|
20
|
+
__version__ = "0.8.9"
|
|
21
21
|
|
|
22
22
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -826,6 +826,8 @@ class DownloadManager:
|
|
|
826
826
|
reports progress and honors a per-job cancel flag. Jobs are in-memory
|
|
827
827
|
(completed artifacts persist as blobs); restarting drops in-flight jobs."""
|
|
828
828
|
|
|
829
|
+
_STOP = -1
|
|
830
|
+
|
|
829
831
|
def __init__(self, store: Store, workers: int = 2):
|
|
830
832
|
self.store = store
|
|
831
833
|
self._jobs: dict[int, Job] = {}
|
|
@@ -833,8 +835,24 @@ class DownloadManager:
|
|
|
833
835
|
self._lock = threading.Lock()
|
|
834
836
|
self._q: queue.Queue[int] = queue.Queue()
|
|
835
837
|
self._ids = itertools.count(1)
|
|
838
|
+
self._threads: list[threading.Thread] = []
|
|
836
839
|
for _ in range(max(1, workers)):
|
|
837
|
-
threading.Thread(target=self._worker, daemon=True)
|
|
840
|
+
t = threading.Thread(target=self._worker, daemon=True)
|
|
841
|
+
t.start()
|
|
842
|
+
self._threads.append(t)
|
|
843
|
+
|
|
844
|
+
def close(self, timeout: float = 3.0) -> None:
|
|
845
|
+
"""Ask each worker to exit and wait for them. Pushes one
|
|
846
|
+
``_STOP`` sentinel per worker so an idle ``queue.get()``
|
|
847
|
+
unblocks; running workers finish their current job first.
|
|
848
|
+
The daemon=True flag lets ``main()`` exit without calling
|
|
849
|
+
this, but test fixtures + explicit shutdowns should call
|
|
850
|
+
it to avoid the sqlite3 finalizer warnings that leaked
|
|
851
|
+
worker threads produce."""
|
|
852
|
+
for _ in self._threads:
|
|
853
|
+
self._q.put(self._STOP)
|
|
854
|
+
for t in self._threads:
|
|
855
|
+
t.join(timeout=timeout)
|
|
838
856
|
|
|
839
857
|
def enqueue(self, url: str, headers: dict | None = None) -> Job:
|
|
840
858
|
with self._lock:
|
|
@@ -872,6 +890,8 @@ class DownloadManager:
|
|
|
872
890
|
def _worker(self):
|
|
873
891
|
while True:
|
|
874
892
|
jid = self._q.get()
|
|
893
|
+
if jid == self._STOP:
|
|
894
|
+
return
|
|
875
895
|
with self._lock:
|
|
876
896
|
job = self._jobs.get(jid)
|
|
877
897
|
if job is None or job.status != "queued":
|
|
@@ -1801,7 +1821,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1801
1821
|
'<progress id="spin" class="htmx-indicator ms-1" '
|
|
1802
1822
|
'style="width:4rem;height:.4rem;"></progress></div>'
|
|
1803
1823
|
)
|
|
1804
|
-
|
|
1824
|
+
# <nav class="navbar subnav-strip"> matches bty's canonical
|
|
1825
|
+
# subnav shape (bty/src/bty/web/_templates/ui/_subnav.html);
|
|
1826
|
+
# ``.navbar`` gets the Bootstrap subnav-strip vertical rhythm,
|
|
1827
|
+
# aria-label + <nav> give screen readers a landmark.
|
|
1828
|
+
return (
|
|
1829
|
+
'<nav class="navbar subnav-strip" aria-label="Section sub-navigation">'
|
|
1830
|
+
f'<div class="container">{left}{right}</div></nav>'
|
|
1831
|
+
)
|
|
1805
1832
|
|
|
1806
1833
|
# ---- Cached page -----------------------------------------------------
|
|
1807
1834
|
def _cached_body_html(self) -> str:
|
|
@@ -2249,6 +2276,10 @@ def main():
|
|
|
2249
2276
|
httpd.serve_forever()
|
|
2250
2277
|
except KeyboardInterrupt:
|
|
2251
2278
|
print("\nbye", flush=True)
|
|
2279
|
+
finally:
|
|
2280
|
+
# Drain workers on shutdown so the sqlite finalizer warnings
|
|
2281
|
+
# from leaked threads stop appearing on Ctrl-C.
|
|
2282
|
+
mgr.close()
|
|
2252
2283
|
|
|
2253
2284
|
|
|
2254
2285
|
if __name__ == "__main__":
|
|
@@ -178,6 +178,7 @@ class TestStoreFromOrigin(unittest.TestCase):
|
|
|
178
178
|
self.store = server.Store(tempfile.mkdtemp(), keep_query=False)
|
|
179
179
|
|
|
180
180
|
def tearDown(self):
|
|
181
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
181
182
|
self.httpd.shutdown()
|
|
182
183
|
self.httpd.server_close()
|
|
183
184
|
|
|
@@ -266,6 +267,7 @@ class TestTruncatedDownloadRejected(unittest.TestCase):
|
|
|
266
267
|
self.store = server.Store(tempfile.mkdtemp(), keep_query=False)
|
|
267
268
|
|
|
268
269
|
def tearDown(self):
|
|
270
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
269
271
|
self.httpd.shutdown()
|
|
270
272
|
self.httpd.server_close()
|
|
271
273
|
|
|
@@ -384,6 +386,7 @@ class TestRangeResumeOnTruncation(unittest.TestCase):
|
|
|
384
386
|
self.store = server.Store(tempfile.mkdtemp(), keep_query=False)
|
|
385
387
|
|
|
386
388
|
def tearDown(self):
|
|
389
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
387
390
|
self.httpd.shutdown()
|
|
388
391
|
self.httpd.server_close()
|
|
389
392
|
|
|
@@ -843,6 +846,7 @@ class TestFetchWithHeaders(unittest.TestCase):
|
|
|
843
846
|
self.store = server.Store(tempfile.mkdtemp(), keep_query=False)
|
|
844
847
|
|
|
845
848
|
def tearDown(self):
|
|
849
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
846
850
|
self.httpd.shutdown()
|
|
847
851
|
self.httpd.server_close()
|
|
848
852
|
|
|
@@ -1095,6 +1099,7 @@ class TestMultiPageNavigation(unittest.TestCase):
|
|
|
1095
1099
|
self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
|
|
1096
1100
|
|
|
1097
1101
|
def tearDown(self):
|
|
1102
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
1098
1103
|
self.httpd.shutdown()
|
|
1099
1104
|
self.httpd.server_close()
|
|
1100
1105
|
|
|
@@ -1196,6 +1201,7 @@ class TestCatalogAdminEndpoints(unittest.TestCase):
|
|
|
1196
1201
|
def tearDown(self):
|
|
1197
1202
|
self.origin.shutdown()
|
|
1198
1203
|
self.origin.server_close()
|
|
1204
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
1199
1205
|
self.httpd.shutdown()
|
|
1200
1206
|
self.httpd.server_close()
|
|
1201
1207
|
|
|
@@ -1316,6 +1322,88 @@ class TestCatalogAdminEndpoints(unittest.TestCase):
|
|
|
1316
1322
|
httpd2.server_close()
|
|
1317
1323
|
|
|
1318
1324
|
|
|
1325
|
+
class TestAddOrasEntryFormatMapping(unittest.TestCase):
|
|
1326
|
+
"""CatalogState.add_oras_entry derives ``format`` from the resolved
|
|
1327
|
+
layer title's suffix and ``arch`` from the tag suffix. The
|
|
1328
|
+
happy-path test in TestCatalogAdminEndpoints only covers the
|
|
1329
|
+
unreachable-registry branch (fmt stays ''); this class exercises
|
|
1330
|
+
the title -> format mapping (all five known suffixes + fallback)
|
|
1331
|
+
and the arch extraction."""
|
|
1332
|
+
|
|
1333
|
+
def setUp(self):
|
|
1334
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
1335
|
+
self.catalog = server.CatalogState(
|
|
1336
|
+
url="http://localhost/catalog.toml",
|
|
1337
|
+
persist_path=os.path.join(self.tmpdir, "catalog.toml"),
|
|
1338
|
+
)
|
|
1339
|
+
# Patch oras.resolve_ref for the duration of the tests; every
|
|
1340
|
+
# test sets a fake_resolver with the shape it needs.
|
|
1341
|
+
self._original_resolve = server.oras.resolve_ref
|
|
1342
|
+
|
|
1343
|
+
def tearDown(self):
|
|
1344
|
+
server.oras.resolve_ref = self._original_resolve
|
|
1345
|
+
|
|
1346
|
+
def _stub_resolve(self, title, size=1234):
|
|
1347
|
+
def _r(ref):
|
|
1348
|
+
return types.SimpleNamespace(title=title, size=size, digest="sha256:abc")
|
|
1349
|
+
|
|
1350
|
+
server.oras.resolve_ref = _r
|
|
1351
|
+
|
|
1352
|
+
def test_title_ends_with_img_zst(self):
|
|
1353
|
+
self._stub_resolve("debian-13-headless.img.zst")
|
|
1354
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1355
|
+
self.assertTrue(ok)
|
|
1356
|
+
self.assertEqual(self.catalog.entries[-1]["format"], "img.zst")
|
|
1357
|
+
|
|
1358
|
+
def test_title_ends_with_img_gz(self):
|
|
1359
|
+
self._stub_resolve("release.img.gz")
|
|
1360
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1361
|
+
self.assertTrue(ok)
|
|
1362
|
+
self.assertEqual(self.catalog.entries[-1]["format"], "img.gz")
|
|
1363
|
+
|
|
1364
|
+
def test_title_ends_with_img_xz(self):
|
|
1365
|
+
self._stub_resolve("release.img.xz")
|
|
1366
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1367
|
+
self.assertTrue(ok)
|
|
1368
|
+
self.assertEqual(self.catalog.entries[-1]["format"], "img.xz")
|
|
1369
|
+
|
|
1370
|
+
def test_title_ends_with_bare_img(self):
|
|
1371
|
+
self._stub_resolve("raw.img")
|
|
1372
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1373
|
+
self.assertTrue(ok)
|
|
1374
|
+
self.assertEqual(self.catalog.entries[-1]["format"], "img")
|
|
1375
|
+
|
|
1376
|
+
def test_title_ends_with_iso(self):
|
|
1377
|
+
self._stub_resolve("bootable.iso")
|
|
1378
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1379
|
+
self.assertTrue(ok)
|
|
1380
|
+
self.assertEqual(self.catalog.entries[-1]["format"], "iso")
|
|
1381
|
+
|
|
1382
|
+
def test_title_no_known_suffix_leaves_format_unset(self):
|
|
1383
|
+
self._stub_resolve("something.tar")
|
|
1384
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1")
|
|
1385
|
+
self.assertTrue(ok)
|
|
1386
|
+
self.assertNotIn("format", self.catalog.entries[-1])
|
|
1387
|
+
|
|
1388
|
+
def test_tag_suffix_extracts_arch_x86_64(self):
|
|
1389
|
+
self._stub_resolve("release.img.zst")
|
|
1390
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1-x86_64")
|
|
1391
|
+
self.assertTrue(ok)
|
|
1392
|
+
self.assertEqual(self.catalog.entries[-1]["arch"], "x86_64")
|
|
1393
|
+
|
|
1394
|
+
def test_tag_suffix_extracts_arch_arm64(self):
|
|
1395
|
+
self._stub_resolve("release.img.zst")
|
|
1396
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1-arm64")
|
|
1397
|
+
self.assertTrue(ok)
|
|
1398
|
+
self.assertEqual(self.catalog.entries[-1]["arch"], "arm64")
|
|
1399
|
+
|
|
1400
|
+
def test_unknown_arch_suffix_leaves_arch_unset(self):
|
|
1401
|
+
self._stub_resolve("release.img.zst")
|
|
1402
|
+
ok, _ = self.catalog.add_oras_entry("oras://ghcr.io/x/demo:v1-mips")
|
|
1403
|
+
self.assertTrue(ok)
|
|
1404
|
+
self.assertNotIn("arch", self.catalog.entries[-1])
|
|
1405
|
+
|
|
1406
|
+
|
|
1319
1407
|
class TestReadFormDuplicateKeys(unittest.TestCase):
|
|
1320
1408
|
"""read_form rejects duplicate form keys so a first-value-wins
|
|
1321
1409
|
collapse can't hide a payload silently -- mirrors nbdmux's
|
|
@@ -1327,6 +1415,7 @@ class TestReadFormDuplicateKeys(unittest.TestCase):
|
|
|
1327
1415
|
self.port = self.httpd.server_address[1]
|
|
1328
1416
|
|
|
1329
1417
|
def tearDown(self):
|
|
1418
|
+
getattr(self.httpd, "mgr", None) and self.httpd.mgr.close()
|
|
1330
1419
|
self.httpd.shutdown()
|
|
1331
1420
|
self.httpd.server_close()
|
|
1332
1421
|
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Single-host deploy (run from the repo root):
|
|
2
|
-
# WITHCACHE_ADMIN_PASSWORD=secret podman compose -f deploy/compose.yml up -d
|
|
3
|
-
# (or: docker compose -f deploy/compose.yml up -d)
|
|
4
|
-
services:
|
|
5
|
-
cache-host:
|
|
6
|
-
build:
|
|
7
|
-
context: ..
|
|
8
|
-
dockerfile: deploy/Containerfile
|
|
9
|
-
image: withcache:latest
|
|
10
|
-
container_name: withcache
|
|
11
|
-
ports:
|
|
12
|
-
- "8081:8081"
|
|
13
|
-
environment:
|
|
14
|
-
# Protect the operator UI. Unset => UI is open (a warning is logged).
|
|
15
|
-
- WITHCACHE_ADMIN_PASSWORD=${WITHCACHE_ADMIN_PASSWORD:-}
|
|
16
|
-
# Optional: pin the session-signing key (else persisted under /data).
|
|
17
|
-
- WITHCACHE_SESSION_SECRET=${WITHCACHE_SESSION_SECRET:-}
|
|
18
|
-
# Optional: pin the image-catalog URL. When set, the env value
|
|
19
|
-
# wins over any /admin/catalog_set_url operator override so a
|
|
20
|
-
# locked-down deploy stays locked. Unset uses the default
|
|
21
|
-
# (safl/nosi's ``catalog.toml`` on GitHub).
|
|
22
|
-
- WITHCACHE_CATALOG_URL=${WITHCACHE_CATALOG_URL:-}
|
|
23
|
-
volumes:
|
|
24
|
-
- withcache-data:/data
|
|
25
|
-
restart: unless-stopped
|
|
26
|
-
|
|
27
|
-
volumes:
|
|
28
|
-
withcache-data:
|
|
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
|