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.
Files changed (49) hide show
  1. {withcache-0.13.0 → withcache-0.13.1}/PKG-INFO +10 -8
  2. {withcache-0.13.0 → withcache-0.13.1}/README.md +9 -7
  3. {withcache-0.13.0 → withcache-0.13.1}/shim/build.zig.zon +1 -1
  4. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/__init__.py +1 -1
  5. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_api.py +5 -3
  6. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_app.py +2 -2
  7. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/client.py +22 -0
  8. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/server.py +4 -3
  9. {withcache-0.13.0 → withcache-0.13.1}/tests/test_withcache.py +66 -0
  10. {withcache-0.13.0 → withcache-0.13.1}/.gitignore +0 -0
  11. {withcache-0.13.0 → withcache-0.13.1}/LICENSE +0 -0
  12. {withcache-0.13.0 → withcache-0.13.1}/deploy/Containerfile +0 -0
  13. {withcache-0.13.0 → withcache-0.13.1}/deploy/compose.local-build.yml +0 -0
  14. {withcache-0.13.0 → withcache-0.13.1}/deploy/compose.yml +0 -0
  15. {withcache-0.13.0 → withcache-0.13.1}/deploy/envvars.example +0 -0
  16. {withcache-0.13.0 → withcache-0.13.1}/hatch_build.py +0 -0
  17. {withcache-0.13.0 → withcache-0.13.1}/pyproject.toml +0 -0
  18. {withcache-0.13.0 → withcache-0.13.1}/shim/build.zig +0 -0
  19. {withcache-0.13.0 → withcache-0.13.1}/shim/shim.zig +0 -0
  20. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_events_log.py +0 -0
  21. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_settings_store.py +0 -0
  22. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_shim.py +0 -0
  23. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_table_state.py +0 -0
  24. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/_layout.html +0 -0
  25. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/_table_macros.html +0 -0
  26. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/catalog.html +0 -0
  27. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/dashboard.html +0 -0
  28. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/events.html +0 -0
  29. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/login.html +0 -0
  30. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/misses.html +0 -0
  31. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/_templates/ui/settings.html +0 -0
  32. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/curlwithcache.py +0 -0
  33. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/oras.py +0 -0
  34. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/bootstrap-icons.min.css +0 -0
  35. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/bootstrap.min.css +0 -0
  36. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
  37. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
  38. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/static/htmx.min.js +0 -0
  39. {withcache-0.13.0 → withcache-0.13.1}/src/withcache/wgetwithcache.py +0 -0
  40. {withcache-0.13.0 → withcache-0.13.1}/tests/test_differential.py +0 -0
  41. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_admin_forms.py +0 -0
  42. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_blob.py +0 -0
  43. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_catalog_api.py +0 -0
  44. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_events.py +0 -0
  45. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_scaffold.py +0 -0
  46. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_settings_persistence.py +0 -0
  47. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_ui_pages.py +0 -0
  48. {withcache-0.13.0 → withcache-0.13.1}/tests/test_fastapi_uvicorn_smoke.py +0 -0
  49. {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.0
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** + operator UI (Bootstrap 5 + Bootstrap Icons + HTMX) |
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 and the Python shims are **stdlib-only** (no third-party runtime
78
- deps); the native shim is a dependency-free static binary.
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
- - **Cached** (landing): URL, size, **hits** (times served) and **misses** (times requested before it was cached), SHA-256, fetched-at, each with **Delete** to free space.
249
- - **Downloads**: live progress bars, `queued/running/completed/cancelled/failed`, **Cancel**, and **Clear finished**. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on [bty]'s job managers.
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
- - **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configurable via `$WITHCACHE_CATALOG_URL` or the subnav Set&fetch input); pre-seed by URL via the "Add image from oras" or "Add HTTPS entry" forms, then **Download** each entry.
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** + operator UI (Bootstrap 5 + Bootstrap Icons + HTMX) |
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 and the Python shims are **stdlib-only** (no third-party runtime
55
- deps); the native shim is a dependency-free static binary.
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
- - **Cached** (landing): URL, size, **hits** (times served) and **misses** (times requested before it was cached), SHA-256, fetched-at, each with **Delete** to free space.
226
- - **Downloads**: live progress bars, `queued/running/completed/cancelled/failed`, **Cancel**, and **Clear finished**. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on [bty]'s job managers.
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
- - **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configurable via `$WITHCACHE_CATALOG_URL` or the subnav Set&fetch input); pre-seed by URL via the "Add image from oras" or "Add HTTPS entry" forms, then **Download** each entry.
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.0",
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.0"
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
- :class:`StreamRegistry` ticks progress per chunk so the operator
27
- Downloads page shows in-flight transfers as they run.
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 AsyncIterator
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) -> AsyncIterator[None]:
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
- # Since v0.10.0 there is no auto-fetch on miss: the operator
1057
- # hits Download on /ui/catalog to enqueue a fetch. The daemon
1058
- # is always in "curate" mode; the --curate flag went with it.
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