withcache 0.5.0__tar.gz → 0.5.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: withcache
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -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.5.0",
5
+ .version = "0.5.2",
6
6
  .fingerprint = 0xd7d96c5ed212ccaa,
7
7
  .minimum_zig_version = "0.16.0",
8
8
  .paths = .{
@@ -12,6 +12,6 @@ All modules are stdlib-only and self-contained.
12
12
 
13
13
  from .client import blob_url, cache_base, is_cached, serve_url
14
14
 
15
- __version__ = "0.5.0"
15
+ __version__ = "0.5.2"
16
16
 
17
17
  __all__ = ["__version__", "blob_url", "cache_base", "is_cached", "serve_url"]
@@ -982,7 +982,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
982
982
  <body><main class="container">
983
983
  <nav>
984
984
  <ul><li>
985
- <strong>withcache</strong> &nbsp;<small>cache-host</small>
985
+ <strong>withcache</strong>
986
986
  &nbsp;<small class="mono">v{html.escape(__version__)}</small>
987
987
  </li></ul>
988
988
  <ul>
@@ -996,7 +996,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
996
996
  hx-indicator="#spin" hx-on::after-request="this.reset()">
997
997
  <fieldset role="group">
998
998
  <input type="url" name="url" placeholder="https://origin/path/artifact.tar.gz" required>
999
- <button type="submit">Fetch &amp; store</button>
999
+ <!-- white-space: nowrap so the label "Fetch & store" stays on
1000
+ one line; default flex-grow inside fieldset[role=group]
1001
+ shrinks the button to content width, which on a narrow
1002
+ viewport wrapped the ampersand to a second line. -->
1003
+ <button type="submit" style="white-space: nowrap;">Fetch &amp; store</button>
1000
1004
  </fieldset>
1001
1005
  </form>
1002
1006
 
@@ -1011,6 +1015,42 @@ class Handler(http.server.BaseHTTPRequestHandler):
1011
1015
  hx-swap="innerHTML">
1012
1016
  {self.render_dash()}
1013
1017
  </div>
1018
+
1019
+ <!-- Tab activation. Applies an ``active-tab`` class to the
1020
+ ``section.tab`` whose id matches the URL hash (defaulting to
1021
+ the first section when no hash is set) and to the
1022
+ corresponding ``nav.tabs a``. Runs on initial load, on every
1023
+ click into a tab link (so the operator gets immediate
1024
+ feedback before the next htmx tick), and on every
1025
+ ``htmx:afterSettle`` so the class survives the 1 Hz
1026
+ innerHTML replacement of ``#dash``. Without this the
1027
+ previous ``:target``-based CSS would snap the operator back
1028
+ to the first tab within a second of any click. -->
1029
+ <script>
1030
+ (function () {{
1031
+ function applyActiveTab() {{
1032
+ var hash = (window.location.hash || '').replace(/^#/, '');
1033
+ var sections = document.querySelectorAll('#dash section.tab');
1034
+ if (!sections.length) return;
1035
+ var ids = Array.prototype.map.call(sections, function (s) {{ return s.id; }});
1036
+ if (ids.indexOf(hash) === -1) hash = ids[0];
1037
+ sections.forEach(function (s) {{
1038
+ s.classList.toggle('active-tab', s.id === hash);
1039
+ }});
1040
+ document.querySelectorAll('#dash nav.tabs a').forEach(function (a) {{
1041
+ var target = (a.getAttribute('href') || '').replace(/^#/, '');
1042
+ a.classList.toggle('active-tab', target === hash);
1043
+ }});
1044
+ }}
1045
+ window.addEventListener('hashchange', applyActiveTab);
1046
+ document.body.addEventListener('htmx:afterSettle', applyActiveTab);
1047
+ document.addEventListener('click', function (ev) {{
1048
+ var a = ev.target.closest && ev.target.closest('#dash nav.tabs a');
1049
+ if (a) setTimeout(applyActiveTab, 0);
1050
+ }});
1051
+ applyActiveTab();
1052
+ }})();
1053
+ </script>
1014
1054
  </main></body></html>"""
1015
1055
 
1016
1056
  def render_dash(self) -> str:
@@ -1024,13 +1064,20 @@ class Handler(http.server.BaseHTTPRequestHandler):
1024
1064
  used += f" / {human_size(self.store.max_bytes)}"
1025
1065
  full = "" if self.store.has_capacity() else " &middot; <strong>cache full</strong>"
1026
1066
 
1027
- # Tabs are pure-CSS via :target. The URL hash names the active
1028
- # section; htmx innerHTML-replacement of #dash leaves the hash
1029
- # alone, so the operator's tab choice survives every refresh.
1030
- # ``body:not(:has(section:target))`` selects the default tab
1031
- # when no hash is present; :has() lands cleanly on Chrome 105+,
1032
- # Firefox 121+, Safari 15.4+, which is the whole modern web by
1033
- # the time this ships in 2026.
1067
+ # Tabs are driven by an ``active-tab`` class applied to one
1068
+ # ``section.tab`` (and matching ``nav.tabs a``). A tiny script
1069
+ # at the bottom of the dash watches the URL hash, the htmx
1070
+ # post-swap event, and click events on the tab links so the
1071
+ # class survives every 1 Hz innerHTML replacement.
1072
+ #
1073
+ # An earlier pure-CSS attempt used ``:target`` + ``:has()``.
1074
+ # That works on a static page, but when htmx swaps the
1075
+ # ``#dash`` innerHTML each second the freshly-inserted
1076
+ # ``section.tab`` elements do not always get re-matched by
1077
+ # ``:target`` (the browser keeps the URL hash but the
1078
+ # newly-inserted node is not the one ``:target`` resolved to
1079
+ # at hash-change time). The visible symptom was the tab
1080
+ # snapping back to Streams within a second of every click.
1034
1081
  tab_style = """
1035
1082
  <style>
1036
1083
  nav.tabs { margin: 1rem 0 .25rem; border-bottom: 1px solid var(--pico-muted-border-color); }
@@ -1042,22 +1089,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
1042
1089
  margin-bottom: -1px; font-size: .9rem;
1043
1090
  }
1044
1091
  nav.tabs a:hover { color: var(--pico-color); }
1045
- section.tab { display: none; padding-top: .75rem; }
1046
- section.tab:target { display: block; }
1047
- body:not(:has(section.tab:target)) section.tab#tab-streams { display: block; }
1048
- body:has(#tab-streams:target) nav.tabs a[href="#tab-streams"],
1049
- body:has(#tab-downloads:target) nav.tabs a[href="#tab-downloads"],
1050
- body:has(#tab-misses:target) nav.tabs a[href="#tab-misses"],
1051
- body:has(#tab-cached:target) nav.tabs a[href="#tab-cached"] {
1052
- color: var(--pico-color);
1053
- border-bottom-color: var(--pico-primary, #0172ad);
1054
- font-weight: 600;
1055
- }
1056
- body:not(:has(section.tab:target)) nav.tabs a[href="#tab-streams"] {
1092
+ nav.tabs a.active-tab {
1057
1093
  color: var(--pico-color);
1058
1094
  border-bottom-color: var(--pico-primary, #0172ad);
1059
1095
  font-weight: 600;
1060
1096
  }
1097
+ section.tab { display: none; padding-top: .75rem; }
1098
+ section.tab.active-tab { display: block; }
1061
1099
  </style>
1062
1100
  """
1063
1101
 
@@ -1102,10 +1140,26 @@ class Handler(http.server.BaseHTTPRequestHandler):
1102
1140
  or '<tr><td colspan="4"><em>No misses recorded.</em></td></tr>'
1103
1141
  )
1104
1142
 
1143
+ # Build the per-row /b/ serve URL once; the cell wraps the
1144
+ # origin string in a link that GETs the cached bytes when
1145
+ # the operator clicks. Same path-encoded form the shim
1146
+ # generates so curl / wget / a browser save all end up
1147
+ # writing the correct output filename.
1148
+ def _serve_url_for(row: sqlite3.Row) -> str:
1149
+ origin = row["url"]
1150
+ token = base64.urlsafe_b64encode(origin.encode("utf-8")).decode("ascii").rstrip("=")
1151
+ # last path segment of the origin, fallback "download" so a
1152
+ # URL ending in / still serves with a usable filename.
1153
+ name = urllib.parse.urlsplit(origin).path.rsplit("/", 1)[-1] or "download"
1154
+ return f"/b/{token}/{urllib.parse.quote(name)}"
1155
+
1105
1156
  blob_rows = (
1106
1157
  "".join(
1107
1158
  f"""<tr>
1108
- <td class="url">{html.escape(b["url"])}</td>
1159
+ <td class="url">
1160
+ <a href="{_serve_url_for(b)}"
1161
+ title="Download the cached object">{html.escape(b["url"])}</a>
1162
+ </td>
1109
1163
  <td>{human_size(b["size"])}</td>
1110
1164
  <td class="num">{b["hits"]}</td>
1111
1165
  <td class="num">{b["misses"]}</td>
@@ -1133,12 +1187,22 @@ class Handler(http.server.BaseHTTPRequestHandler):
1133
1187
  <p><small>{nblobs} cached ({used}){full} &middot; {nmisses} pending miss(es)</small></p>
1134
1188
  {tab_style}
1135
1189
  <nav class="tabs"><ul>
1190
+ <li><a href="#tab-cached">Cached ({nblobs})</a></li>
1136
1191
  <li><a href="#tab-streams">Streams ({nstreams})</a></li>
1137
1192
  <li><a href="#tab-downloads">Downloads ({njobs})</a></li>
1138
1193
  <li><a href="#tab-misses">Misses ({nmisses})</a></li>
1139
- <li><a href="#tab-cached">Cached ({nblobs})</a></li>
1140
1194
  </ul></nav>
1141
1195
 
1196
+ <section id="tab-cached" class="tab">
1197
+ <figure><table class="striped">
1198
+ <thead><tr>
1199
+ <th>URL</th><th>Size</th><th class="num">Hits</th><th class="num">Misses</th>
1200
+ <th>SHA-256</th><th>Fetched</th><th>Action</th>
1201
+ </tr></thead>
1202
+ <tbody>{blob_rows}</tbody>
1203
+ </table></figure>
1204
+ </section>
1205
+
1142
1206
  <section id="tab-streams" class="tab">
1143
1207
  <figure><table class="striped">
1144
1208
  <thead><tr>
@@ -1169,16 +1233,6 @@ class Handler(http.server.BaseHTTPRequestHandler):
1169
1233
  </tr></thead>
1170
1234
  <tbody>{miss_rows}</tbody>
1171
1235
  </table></figure>
1172
- </section>
1173
-
1174
- <section id="tab-cached" class="tab">
1175
- <figure><table class="striped">
1176
- <thead><tr>
1177
- <th>URL</th><th>Size</th><th class="num">Hits</th><th class="num">Misses</th>
1178
- <th>SHA-256</th><th>Fetched</th><th>Action</th>
1179
- </tr></thead>
1180
- <tbody>{blob_rows}</tbody>
1181
- </table></figure>
1182
1236
  </section>"""
1183
1237
 
1184
1238
  def _stream_progress_cell(self, s: Stream) -> str:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes