withcache 0.13.0__tar.gz → 0.13.1__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.13.0 → withcache-0.13.1}/PKG-INFO +10 -8
- {withcache-0.13.0 → withcache-0.13.1}/README.md +9 -7
- {withcache-0.13.0 → withcache-0.13.1}/shim/build.zig.zon +1 -1
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/__init__.py +1 -1
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_api.py +5 -3
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_app.py +2 -2
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/client.py +22 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/server.py +4 -3
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_withcache.py +66 -0
- {withcache-0.13.0 → withcache-0.13.1}/.gitignore +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/LICENSE +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/deploy/Containerfile +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/deploy/compose.local-build.yml +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/deploy/compose.yml +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/deploy/envvars.example +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/hatch_build.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/pyproject.toml +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/shim/build.zig +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/shim/shim.zig +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_events_log.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_settings_store.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_shim.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_table_state.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/_layout.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/_table_macros.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/catalog.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/dashboard.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/events.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/login.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/misses.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/settings.html +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/oras.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/bootstrap-icons.min.css +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/bootstrap.min.css +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_differential.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_admin_forms.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_blob.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_catalog_api.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_events.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_scaffold.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_settings_persistence.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_ui_pages.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_uvicorn_smoke.py +0 -0
- {withcache-0.13.0 → withcache-0.13.1}/tests/test_oras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.1
|
|
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>
|
|
@@ -68,14 +68,16 @@ disk.
|
|
|
68
68
|
|
|
69
69
|
| Path | What it is |
|
|
70
70
|
|-------------------------------|-------------------------------------------------------------|
|
|
71
|
-
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** +
|
|
71
|
+
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** + events log |
|
|
72
|
+
| `src/withcache/_app.py` | FastAPI app factory + operator UI wiring (Bootstrap 5 + Bootstrap Icons + HTMX) |
|
|
72
73
|
| `src/withcache/_shim.py` | Shared shim core (find URL → probe → rewrite → exec) |
|
|
73
74
|
| `src/withcache/curlwithcache.py` / `wgetwithcache.py` | The Python `curl` / `wget` shims |
|
|
74
75
|
| `shim/shim.zig` | The native shim: one static binary, both tools via `argv[0]` |
|
|
75
76
|
| `deploy/Containerfile`, `deploy/compose.yml` | Single Podman/Docker host deploy |
|
|
76
77
|
|
|
77
|
-
The cache-host
|
|
78
|
-
|
|
78
|
+
The cache-host runs on FastAPI + Jinja + Bootstrap (matching bty +
|
|
79
|
+
nbdmux for one trio-consistent chrome); the shims (Python and
|
|
80
|
+
native) are dependency-free so they drop into any environment.
|
|
79
81
|
|
|
80
82
|
## Install
|
|
81
83
|
|
|
@@ -245,11 +247,11 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
245
247
|
|
|
246
248
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
247
249
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
248
|
-
- **
|
|
249
|
-
- **
|
|
250
|
+
- **Dashboard** (landing): catalog + cache + activity summary, health checklist, and the last N audit events.
|
|
251
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configured on Settings > Catalog source). Add entries via the subnav's inline "Add ORAS" / "Add HTTPS" forms or "Fetch default" button, then **Download** each entry. Rows carry hits, size, download progress (live), and cached/failed pills.
|
|
250
252
|
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
251
|
-
- **
|
|
252
|
-
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
253
|
+
- **Events**: append-only audit log with a free-text filter and per-page pagination. Failure rows carry an ack button; the dashboard's Health tripwire flags unacknowledged failures.
|
|
254
|
+
- **Settings**: identity + storage paths + catalog source (editable) + logging (uvicorn level) + auth.
|
|
253
255
|
|
|
254
256
|
## Auth
|
|
255
257
|
|
|
@@ -45,14 +45,16 @@ disk.
|
|
|
45
45
|
|
|
46
46
|
| Path | What it is |
|
|
47
47
|
|-------------------------------|-------------------------------------------------------------|
|
|
48
|
-
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** +
|
|
48
|
+
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** + events log |
|
|
49
|
+
| `src/withcache/_app.py` | FastAPI app factory + operator UI wiring (Bootstrap 5 + Bootstrap Icons + HTMX) |
|
|
49
50
|
| `src/withcache/_shim.py` | Shared shim core (find URL → probe → rewrite → exec) |
|
|
50
51
|
| `src/withcache/curlwithcache.py` / `wgetwithcache.py` | The Python `curl` / `wget` shims |
|
|
51
52
|
| `shim/shim.zig` | The native shim: one static binary, both tools via `argv[0]` |
|
|
52
53
|
| `deploy/Containerfile`, `deploy/compose.yml` | Single Podman/Docker host deploy |
|
|
53
54
|
|
|
54
|
-
The cache-host
|
|
55
|
-
|
|
55
|
+
The cache-host runs on FastAPI + Jinja + Bootstrap (matching bty +
|
|
56
|
+
nbdmux for one trio-consistent chrome); the shims (Python and
|
|
57
|
+
native) are dependency-free so they drop into any environment.
|
|
56
58
|
|
|
57
59
|
## Install
|
|
58
60
|
|
|
@@ -222,11 +224,11 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
222
224
|
|
|
223
225
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
224
226
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
225
|
-
- **
|
|
226
|
-
- **
|
|
227
|
+
- **Dashboard** (landing): catalog + cache + activity summary, health checklist, and the last N audit events.
|
|
228
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configured on Settings > Catalog source). Add entries via the subnav's inline "Add ORAS" / "Add HTTPS" forms or "Fetch default" button, then **Download** each entry. Rows carry hits, size, download progress (live), and cached/failed pills.
|
|
227
229
|
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
228
|
-
- **
|
|
229
|
-
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
230
|
+
- **Events**: append-only audit log with a free-text filter and per-page pagination. Failure rows carry an ack button; the dashboard's Health tripwire flags unacknowledged failures.
|
|
231
|
+
- **Settings**: identity + storage paths + catalog source (editable) + logging (uvicorn level) + auth.
|
|
230
232
|
|
|
231
233
|
## Auth
|
|
232
234
|
|
|
@@ -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.13.
|
|
5
|
+
.version = "0.13.1",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -22,6 +22,6 @@ inherit the framework floor.
|
|
|
22
22
|
from . import oras
|
|
23
23
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
24
24
|
|
|
25
|
-
__version__ = "0.13.
|
|
25
|
+
__version__ = "0.13.1"
|
|
26
26
|
|
|
27
27
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -22,9 +22,11 @@ v0.11.0) promotes a missed URL to a catalog entry + enqueues the
|
|
|
22
22
|
Download in one click.
|
|
23
23
|
|
|
24
24
|
Streaming shape: FastAPI ``StreamingResponse`` with a generator
|
|
25
|
-
that reads chunks from the on-disk blob at 64 KiB reads
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
that reads chunks from the on-disk blob at 64 KiB reads. The
|
|
26
|
+
Downloads-page real-time progress tracker was retired in v0.12.0
|
|
27
|
+
along with that page; per-entry download progress now folds into
|
|
28
|
+
the Catalog page's Downloaded column, populated from the
|
|
29
|
+
DownloadManager's job list directly.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
from __future__ import annotations
|
|
@@ -20,7 +20,7 @@ import hashlib
|
|
|
20
20
|
import os
|
|
21
21
|
import sys
|
|
22
22
|
import threading
|
|
23
|
-
from collections.abc import
|
|
23
|
+
from collections.abc import AsyncGenerator
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
@@ -108,7 +108,7 @@ def create_app(
|
|
|
108
108
|
jinja = _build_jinja(_TEMPLATES_DIR)
|
|
109
109
|
|
|
110
110
|
@contextlib.asynccontextmanager
|
|
111
|
-
async def _lifespan(_app: FastAPI) ->
|
|
111
|
+
async def _lifespan(_app: FastAPI) -> AsyncGenerator[None]:
|
|
112
112
|
"""Start-of-request-cycle wiring for the daemon path.
|
|
113
113
|
|
|
114
114
|
Fires only when ``run_lifecycle=True`` (``server.main`` boots
|
|
@@ -45,6 +45,7 @@ __all__ = [
|
|
|
45
45
|
"cache_base",
|
|
46
46
|
"delete_catalog_entry",
|
|
47
47
|
"is_cached",
|
|
48
|
+
"is_healthy",
|
|
48
49
|
"list_catalog",
|
|
49
50
|
"serve_url",
|
|
50
51
|
]
|
|
@@ -121,6 +122,27 @@ def serve_url(
|
|
|
121
122
|
return blob_url(server, origin) if is_cached(server, origin, timeout, headers=headers) else None
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def is_healthy(
|
|
126
|
+
server: str = "http://localhost:8081",
|
|
127
|
+
timeout: float = PROBE_TIMEOUT,
|
|
128
|
+
) -> bool:
|
|
129
|
+
"""True iff ``GET /healthz`` returns 200. Suitable for a startup
|
|
130
|
+
probe / reachability pill on a consumer's Settings page.
|
|
131
|
+
|
|
132
|
+
Bypasses :func:`_catalog_request` deliberately: the healthz body
|
|
133
|
+
is small JSON but we don't inspect it -- the status code is the
|
|
134
|
+
whole signal. Any transport / HTTP / OS failure returns
|
|
135
|
+
``False`` so callers can treat a False as "not reachable" without
|
|
136
|
+
having to catch a family of exceptions.
|
|
137
|
+
"""
|
|
138
|
+
url = f"{cache_base(server)}/healthz"
|
|
139
|
+
try:
|
|
140
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp: # noqa: S310
|
|
141
|
+
return bool(resp.status == 200)
|
|
142
|
+
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
def _resolve_password(password: str | None) -> str | None:
|
|
125
147
|
"""``password`` if given, else ``$WITHCACHE_ADMIN_PASSWORD``, else ``None``.
|
|
126
148
|
Empty / whitespace values are treated as unset."""
|
|
@@ -1053,9 +1053,10 @@ def main():
|
|
|
1053
1053
|
mgr = DownloadManager(store, workers=args.workers)
|
|
1054
1054
|
auth_password = os.environ.get("WITHCACHE_ADMIN_PASSWORD")
|
|
1055
1055
|
|
|
1056
|
-
#
|
|
1057
|
-
# hits Download on /ui/catalog to enqueue a fetch.
|
|
1058
|
-
#
|
|
1056
|
+
# Withcache is operator-populated: cache misses return 404 (v0.10.0+)
|
|
1057
|
+
# and the operator hits Download on /ui/catalog to enqueue a fetch.
|
|
1058
|
+
# The v0.12.0 UI consolidation folded Cached + Downloads into the
|
|
1059
|
+
# Catalog page's per-row status column and progress bar.
|
|
1059
1060
|
print(
|
|
1060
1061
|
f"withcache cache-host on http://{args.host}:{args.port} "
|
|
1061
1062
|
f"(data={store.data_dir}, keep_query={args.keep_query}, workers={args.workers}, "
|
|
@@ -967,5 +967,71 @@ class TestHumanSize(unittest.TestCase):
|
|
|
967
967
|
self.assertIn("GiB", server.human_size(3 * 1024 * 1024 * 1024))
|
|
968
968
|
|
|
969
969
|
|
|
970
|
+
# --------------------------------------------------------------------------
|
|
971
|
+
# client.is_healthy: GET /healthz reachability probe consumed by bty's
|
|
972
|
+
# Settings pill so a red URL is caught at render time instead of on first
|
|
973
|
+
# flash.
|
|
974
|
+
# --------------------------------------------------------------------------
|
|
975
|
+
class _HealthzOK(http.server.BaseHTTPRequestHandler):
|
|
976
|
+
def do_GET(self):
|
|
977
|
+
if self.path == "/healthz":
|
|
978
|
+
body = b'{"status":"ok"}'
|
|
979
|
+
self.send_response(200)
|
|
980
|
+
self.send_header("Content-Type", "application/json")
|
|
981
|
+
self.send_header("Content-Length", str(len(body)))
|
|
982
|
+
self.end_headers()
|
|
983
|
+
self.wfile.write(body)
|
|
984
|
+
else:
|
|
985
|
+
self.send_response(404)
|
|
986
|
+
self.end_headers()
|
|
987
|
+
|
|
988
|
+
def log_message(self, format, *args):
|
|
989
|
+
pass
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class _HealthzFail(http.server.BaseHTTPRequestHandler):
|
|
993
|
+
def do_GET(self):
|
|
994
|
+
self.send_response(503)
|
|
995
|
+
self.end_headers()
|
|
996
|
+
|
|
997
|
+
def log_message(self, format, *args):
|
|
998
|
+
pass
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class TestIsHealthy(unittest.TestCase):
|
|
1002
|
+
def _serve(self, handler_cls):
|
|
1003
|
+
httpd = socketserver.TCPServer(("127.0.0.1", 0), handler_cls)
|
|
1004
|
+
port = httpd.server_address[1]
|
|
1005
|
+
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
1006
|
+
t.start()
|
|
1007
|
+
return httpd, port
|
|
1008
|
+
|
|
1009
|
+
def test_is_healthy_true_on_200(self):
|
|
1010
|
+
from withcache import client
|
|
1011
|
+
|
|
1012
|
+
httpd, port = self._serve(_HealthzOK)
|
|
1013
|
+
try:
|
|
1014
|
+
self.assertTrue(client.is_healthy(f"http://127.0.0.1:{port}", timeout=2.0))
|
|
1015
|
+
finally:
|
|
1016
|
+
httpd.shutdown()
|
|
1017
|
+
httpd.server_close()
|
|
1018
|
+
|
|
1019
|
+
def test_is_healthy_false_on_non_2xx(self):
|
|
1020
|
+
from withcache import client
|
|
1021
|
+
|
|
1022
|
+
httpd, port = self._serve(_HealthzFail)
|
|
1023
|
+
try:
|
|
1024
|
+
self.assertFalse(client.is_healthy(f"http://127.0.0.1:{port}", timeout=2.0))
|
|
1025
|
+
finally:
|
|
1026
|
+
httpd.shutdown()
|
|
1027
|
+
httpd.server_close()
|
|
1028
|
+
|
|
1029
|
+
def test_is_healthy_false_when_unreachable(self):
|
|
1030
|
+
from withcache import client
|
|
1031
|
+
|
|
1032
|
+
# 127.0.0.1:1 -- reserved and closed on any sane host.
|
|
1033
|
+
self.assertFalse(client.is_healthy("http://127.0.0.1:1", timeout=0.5))
|
|
1034
|
+
|
|
1035
|
+
|
|
970
1036
|
if __name__ == "__main__":
|
|
971
1037
|
unittest.main(verbosity=2)
|
|
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
|
|
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
|