withcache 0.6.1__tar.gz → 0.6.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.
Files changed (23) hide show
  1. {withcache-0.6.1 → withcache-0.6.2}/PKG-INFO +1 -1
  2. {withcache-0.6.1 → withcache-0.6.2}/shim/build.zig.zon +1 -1
  3. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/__init__.py +1 -1
  4. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/server.py +45 -13
  5. {withcache-0.6.1 → withcache-0.6.2}/tests/test_withcache.py +40 -0
  6. {withcache-0.6.1 → withcache-0.6.2}/.gitignore +0 -0
  7. {withcache-0.6.1 → withcache-0.6.2}/LICENSE +0 -0
  8. {withcache-0.6.1 → withcache-0.6.2}/README.md +0 -0
  9. {withcache-0.6.1 → withcache-0.6.2}/deploy/Containerfile +0 -0
  10. {withcache-0.6.1 → withcache-0.6.2}/deploy/compose.yml +0 -0
  11. {withcache-0.6.1 → withcache-0.6.2}/hatch_build.py +0 -0
  12. {withcache-0.6.1 → withcache-0.6.2}/pyproject.toml +0 -0
  13. {withcache-0.6.1 → withcache-0.6.2}/shim/build.zig +0 -0
  14. {withcache-0.6.1 → withcache-0.6.2}/shim/shim.zig +0 -0
  15. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/_shim.py +0 -0
  16. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/client.py +0 -0
  17. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/curlwithcache.py +0 -0
  18. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/oras.py +0 -0
  19. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/static/htmx.min.js +0 -0
  20. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/static/pico.min.css +0 -0
  21. {withcache-0.6.1 → withcache-0.6.2}/src/withcache/wgetwithcache.py +0 -0
  22. {withcache-0.6.1 → withcache-0.6.2}/tests/test_differential.py +0 -0
  23. {withcache-0.6.1 → withcache-0.6.2}/tests/test_oras.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: withcache
3
- Version: 0.6.1
3
+ Version: 0.6.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.6.1",
5
+ .version = "0.6.2",
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.6.1"
20
+ __version__ = "0.6.2"
21
21
 
22
22
  __all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
@@ -757,7 +757,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
757
757
  if not self.is_authed():
758
758
  self.send_text(401, "login required\n")
759
759
  else:
760
- self.send_html(200, self.render_dash())
760
+ # The browser sends the current URL hash (the active
761
+ # tab id) via the ``X-Active-Tab`` request header on
762
+ # every 1 Hz refresh. Server bakes the matching
763
+ # ``.active-tab`` class directly into the rendered
764
+ # HTML so the htmx innerHTML swap doesn't visibly
765
+ # blink while the post-swap JS would otherwise
766
+ # re-apply the class. See render_dash().
767
+ active = (self.headers.get("X-Active-Tab") or "").strip()
768
+ self.send_html(200, self.render_dash(active_tab=active))
761
769
  elif parsed.path == "/":
762
770
  if not self.is_authed():
763
771
  self.redirect("/ui/login")
@@ -1093,11 +1101,17 @@ class Handler(http.server.BaseHTTPRequestHandler):
1093
1101
  isn't wiped by the 1 Hz refresh. ``isCollapsed`` is true when
1094
1102
  there's no selection or the caret is a zero-width point; once
1095
1103
  the operator releases / clears the selection polling resumes
1096
- on the next 1 s tick. -->
1104
+ on the next 1 s tick.
1105
+ ``hx-headers`` sends the current URL hash (the active tab id)
1106
+ as ``X-Active-Tab`` on every refresh so the server can bake
1107
+ ``.active-tab`` into the rendered HTML -- eliminating the
1108
+ visible flicker the post-swap JS-applied class would otherwise
1109
+ cause when the new innerHTML lands without the class. -->
1097
1110
  <div id="dash" hx-get="/admin/dash"
1098
1111
  hx-trigger="load, every 1s [document.getSelection().isCollapsed]"
1099
- hx-swap="innerHTML">
1100
- {self.render_dash()}
1112
+ hx-swap="innerHTML"
1113
+ hx-headers='js:{{"X-Active-Tab": (location.hash || "").replace(/^#/, "")}}'>
1114
+ {self.render_dash(active_tab=(self.headers.get("X-Active-Tab") or "").strip())}
1101
1115
  </div>
1102
1116
 
1103
1117
  <!-- Tab activation. Applies an ``active-tab`` class to the
@@ -1137,7 +1151,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
1137
1151
  </script>
1138
1152
  </main></body></html>"""
1139
1153
 
1140
- def render_dash(self) -> str:
1154
+ def render_dash(self, active_tab: str = "") -> str:
1155
+ # Tab activation is baked into the rendered HTML so the htmx
1156
+ # innerHTML swap doesn't strip `.active-tab` between the
1157
+ # swap and the post-settle JS re-apply. The client sends
1158
+ # the current hash via the ``X-Active-Tab`` header on each
1159
+ # 1 Hz refresh (and on every operator click via the same
1160
+ # script that watches hashchange). Unknown / empty value
1161
+ # defaults to the first tab.
1162
+ _TAB_IDS = ("tab-cached", "tab-streams", "tab-downloads", "tab-misses")
1163
+ if active_tab not in _TAB_IDS:
1164
+ active_tab = _TAB_IDS[0]
1165
+
1166
+ def _active(tab_id: str) -> str:
1167
+ return " active-tab" if tab_id == active_tab else ""
1168
+
1141
1169
  nblobs, nmisses = self.store.counts()
1142
1170
  jobs = self.mgr.list()
1143
1171
  misses = self.store.list_misses()
@@ -1271,13 +1299,17 @@ class Handler(http.server.BaseHTTPRequestHandler):
1271
1299
  <p><small>{nblobs} cached ({used}){full} &middot; {nmisses} pending miss(es)</small></p>
1272
1300
  {tab_style}
1273
1301
  <nav class="tabs"><ul>
1274
- <li><a href="#tab-cached">Cached ({nblobs})</a></li>
1275
- <li><a href="#tab-streams">Streams ({nstreams})</a></li>
1276
- <li><a href="#tab-downloads">Downloads ({njobs})</a></li>
1277
- <li><a href="#tab-misses">Misses ({nmisses})</a></li>
1302
+ <li><a href="#tab-cached" class="{_active("tab-cached").lstrip()}"
1303
+ >Cached ({nblobs})</a></li>
1304
+ <li><a href="#tab-streams" class="{_active("tab-streams").lstrip()}"
1305
+ >Streams ({nstreams})</a></li>
1306
+ <li><a href="#tab-downloads" class="{_active("tab-downloads").lstrip()}"
1307
+ >Downloads ({njobs})</a></li>
1308
+ <li><a href="#tab-misses" class="{_active("tab-misses").lstrip()}"
1309
+ >Misses ({nmisses})</a></li>
1278
1310
  </ul></nav>
1279
1311
 
1280
- <section id="tab-cached" class="tab">
1312
+ <section id="tab-cached" class="tab{_active("tab-cached")}">
1281
1313
  <figure><table class="striped">
1282
1314
  <thead><tr>
1283
1315
  <th>URL</th><th>Size</th><th class="num">Hits</th><th class="num">Misses</th>
@@ -1287,7 +1319,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
1287
1319
  </table></figure>
1288
1320
  </section>
1289
1321
 
1290
- <section id="tab-streams" class="tab">
1322
+ <section id="tab-streams" class="tab{_active("tab-streams")}">
1291
1323
  <figure><table class="striped">
1292
1324
  <thead><tr>
1293
1325
  <th>URL</th><th>Client</th><th>Progress</th><th>Age</th>
@@ -1296,7 +1328,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
1296
1328
  </table></figure>
1297
1329
  </section>
1298
1330
 
1299
- <section id="tab-downloads" class="tab">
1331
+ <section id="tab-downloads" class="tab{_active("tab-downloads")}">
1300
1332
  <div class="row">
1301
1333
  <small>Auto-fetch workers feeding the cache.</small>
1302
1334
  <form hx-post="/admin/clear" hx-target="#dash" hx-swap="innerHTML" style="margin:0">
@@ -1310,7 +1342,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
1310
1342
  </table></figure>
1311
1343
  </section>
1312
1344
 
1313
- <section id="tab-misses" class="tab">
1345
+ <section id="tab-misses" class="tab{_active("tab-misses")}">
1314
1346
  <figure><table class="striped">
1315
1347
  <thead><tr>
1316
1348
  <th>URL</th><th class="num">Misses</th><th>Last seen</th><th>Action</th>
@@ -1023,5 +1023,45 @@ class TestOrasTagRevalidation(unittest.TestCase):
1023
1023
  self.assertFalse(server._oras_tag_moved(self.TAG, self.HEX_A, resolve=_boom))
1024
1024
 
1025
1025
 
1026
+ class TestDashActiveTabFromHeader(unittest.TestCase):
1027
+ """``GET /admin/dash`` bakes ``.active-tab`` into the rendered HTML
1028
+ based on the ``X-Active-Tab`` request header. The browser sends the
1029
+ current URL hash on every refresh so the htmx innerHTML swap doesn't
1030
+ visibly blink while a post-swap JS would otherwise re-apply the class.
1031
+ """
1032
+
1033
+ def setUp(self):
1034
+ self.httpd, self.store = _start_withcache()
1035
+ self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
1036
+
1037
+ def tearDown(self):
1038
+ self.httpd.shutdown()
1039
+ self.httpd.server_close()
1040
+
1041
+ def _dash(self, active_tab=None):
1042
+ req = urllib.request.Request(self.base + "/admin/dash")
1043
+ if active_tab is not None:
1044
+ req.add_header("X-Active-Tab", active_tab)
1045
+ return urllib.request.urlopen(req).read().decode("utf-8")
1046
+
1047
+ def test_default_tab_when_header_missing(self):
1048
+ body = self._dash()
1049
+ # tab-cached is the first tab and the no-header default
1050
+ self.assertIn('<section id="tab-cached" class="tab active-tab">', body)
1051
+ self.assertIn('<section id="tab-streams" class="tab">', body)
1052
+
1053
+ def test_header_picks_the_active_tab(self):
1054
+ body = self._dash("tab-misses")
1055
+ self.assertIn('<section id="tab-misses" class="tab active-tab">', body)
1056
+ self.assertIn('<section id="tab-cached" class="tab">', body)
1057
+
1058
+ def test_unknown_header_value_falls_back_to_first(self):
1059
+ """A hand-crafted X-Active-Tab with a bogus value must not echo
1060
+ into the HTML; the renderer falls back to the first tab."""
1061
+ body = self._dash("tab-totally-not-real")
1062
+ self.assertIn('<section id="tab-cached" class="tab active-tab">', body)
1063
+ self.assertNotIn("tab-totally-not-real", body)
1064
+
1065
+
1026
1066
  if __name__ == "__main__":
1027
1067
  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