diff --git a/conftest.py b/conftest.py
index 0ae64ac..f179b30 100644
--- a/conftest.py
+++ b/conftest.py
@@ -5,8 +5,28 @@ from __future__ import annotations
 import os
 from pathlib import Path
 
+import pytest
+
 _test_auth_config = Path(os.environ.get("TEMP", "/tmp")) / "pygitweb-test-auth.json"
 _test_settings_config = Path(os.environ.get("TEMP", "/tmp")) / "pygitweb-test-settings.json"
 os.environ.setdefault("PYGITWEB_AUTH", "0")
 os.environ.setdefault("PYGITWEB_AUTH_CONFIG", str(_test_auth_config))
 os.environ.setdefault("PYGITWEB_SETTINGS_CONFIG", str(_test_settings_config))
+
+
+def pytest_addoption(parser: pytest.Parser) -> None:
+	parser.addoption(
+		"--js-tests",
+		action="store_true",
+		default=False,
+		help="Run JavaScript unit tests (requires the stpyv8 or pythonmonkey group)",
+	)
+
+
+def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
+	if config.getoption("--js-tests"):
+		return
+	skip = pytest.mark.skip(reason="JS tests are off by default; pass --js-tests to run them")
+	for item in items:
+		if "static_tests" in item.path.parts:
+			item.add_marker(skip)
diff --git a/pygitweb/static_tests/conftest.py b/pygitweb/static_tests/conftest.py
new file mode 100644
index 0000000..01c4650
--- /dev/null
+++ b/pygitweb/static_tests/conftest.py
@@ -0,0 +1,55 @@
+"""JavaScript module tests. Skipped unless pytest is run with --js-tests."""
+
+from __future__ import annotations
+
+import importlib.util
+import sys
+from collections.abc import Iterator
+from pathlib import Path
+from types import ModuleType
+from typing import Any, cast
+
+import pytest
+
+from pygitweb.static_tests.js_runtime import JsPage, available_engines
+
+
+class _FileModule(pytest.Module):
+	def _getobj(self) -> ModuleType:
+		name = "js_tests_" + self.path.stem.replace("-", "_")
+		spec = importlib.util.spec_from_file_location(name, self.path)
+		if spec is None or spec.loader is None:
+			raise ImportError(f"cannot load {self.path}")
+		module = importlib.util.module_from_spec(spec)
+		sys.modules[name] = module
+		spec.loader.exec_module(module)
+		return module
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> object:
+	outcome = yield
+	if file_path.suffix != ".py" or not file_path.name.startswith("test_") or "-" not in file_path.name:
+		return
+	# pytest 9 gathers every pytest_collect_file result, so the default collector
+	# would import this hyphenated module a second time.
+	cast(Any, outcome).force_result([_FileModule.from_parent(parent, path=file_path)])
+
+
+def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
+	if "js" not in metafunc.fixturenames:
+		return
+	engines = available_engines()
+	if engines:
+		metafunc.parametrize("js", engines, indirect=True, ids=engines)
+	else:
+		metafunc.parametrize("js", ["none"], indirect=True)
+
+
+@pytest.fixture
+def js(request: pytest.FixtureRequest) -> Iterator[JsPage]:
+	if request.param == "none":
+		pytest.skip("No JS engine installed. uv sync --group pythonmonkey  or  uv sync --group stpyv8")
+	page = JsPage(str(request.param))
+	yield page
+	page.close()
diff --git a/pygitweb/static_tests/dom_shim.js b/pygitweb/static_tests/dom_shim.js
new file mode 100644
index 0000000..b5859f1
--- /dev/null
+++ b/pygitweb/static_tests/dom_shim.js
@@ -0,0 +1,1126 @@
+(function () {
+  var Node = {
+    ELEMENT_NODE: 1,
+    TEXT_NODE: 3,
+    DOCUMENT_FRAGMENT_NODE: 11,
+    DOCUMENT_NODE: 9,
+  };
+  var NodeFilter = { SHOW_TEXT: 4 };
+  var VOID_TAGS = { area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, link: 1, meta: 1, source: 1, wbr: 1 };
+
+  function HTMLElement() {}
+  function HTMLSelectElement() {}
+  HTMLSelectElement.prototype = Object.create(HTMLElement.prototype);
+  HTMLSelectElement.prototype.constructor = HTMLSelectElement;
+
+  function decodeEntities(text) {
+    return text
+      .replace(/&lt;/g, "<")
+      .replace(/&gt;/g, ">")
+      .replace(/&quot;/g, '"')
+      .replace(/&#39;/g, "'")
+      .replace(/&nbsp;/g, " ")
+      .replace(/&amp;/g, "&");
+  }
+
+  function encodeText(text) {
+    return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+  }
+
+  function camelData(name) {
+    return name.slice(5).replace(/-([a-z])/g, function (_, c) {
+      return c.toUpperCase();
+    });
+  }
+
+  function dataAttr(key) {
+    return "data-" + String(key).replace(/[A-Z]/g, function (c) {
+      return "-" + c.toLowerCase();
+    });
+  }
+
+  function ClassList(el) {
+    this._el = el;
+  }
+  ClassList.prototype._list = function () {
+    return (this._el.getAttribute("class") || "").split(/\s+/).filter(Boolean);
+  };
+  ClassList.prototype._set = function (list) {
+    this._el._setAttr("class", list.join(" "));
+  };
+  ClassList.prototype.add = function () {
+    var list = this._list();
+    for (var i = 0; i < arguments.length; i++) {
+      if (list.indexOf(arguments[i]) < 0) list.push(arguments[i]);
+    }
+    this._set(list);
+  };
+  ClassList.prototype.remove = function () {
+    var drop = Array.prototype.slice.call(arguments);
+    this._set(this._list().filter(function (name) { return drop.indexOf(name) < 0; }));
+  };
+  ClassList.prototype.contains = function (name) {
+    return this._list().indexOf(name) >= 0;
+  };
+  ClassList.prototype.toggle = function (name, force) {
+    var has = this.contains(name);
+    var on = force === undefined ? !has : !!force;
+    if (on) this.add(name);
+    else this.remove(name);
+    return on;
+  };
+
+  function Style(el) {
+    this._el = el;
+    this._map = {};
+  }
+  Style.prototype._sync = function () {
+    var parts = [];
+    for (var k in this._map) parts.push(k + ": " + this._map[k]);
+    this._el._setAttr("style", parts.join("; "));
+  };
+
+  function makeStyle(el) {
+    var style = new Style(el);
+    return new Proxy(style, {
+      get: function (target, key) {
+        if (key in target || typeof key !== "string") return target[key];
+        return target._map[key] || "";
+      },
+      set: function (target, key, value) {
+        if (typeof key === "string" && key[0] !== "_") {
+          target._map[key] = String(value);
+          target._sync();
+          return true;
+        }
+        target[key] = value;
+        return true;
+      },
+    });
+  }
+
+  function makeDataset(el) {
+    return new Proxy(el, {
+      get: function (_, key) {
+        if (key === "hasOwnProperty") return function () { return Object.prototype.hasOwnProperty.apply(this, arguments); };
+        var value = el.getAttribute(dataAttr(key));
+        return value == null ? undefined : value;
+      },
+      set: function (_, key, value) {
+        el.setAttribute(dataAttr(key), value);
+        return true;
+      },
+      has: function (_, key) {
+        return el.hasAttribute(dataAttr(key));
+      },
+      getOwnPropertyDescriptor: function (_, key) {
+        if (!el.hasAttribute(dataAttr(key))) return undefined;
+        return { configurable: true, enumerable: true, value: el.getAttribute(dataAttr(key)) };
+      },
+      ownKeys: function () {
+        return el._attrNames().filter(function (name) { return name.indexOf("data-") === 0; }).map(camelData);
+      },
+    });
+  }
+
+  function fire(target, event) {
+    var path = [];
+    var node = target;
+    while (node) {
+      path.push(node);
+      node = node.parentNode || node.parentElement;
+    }
+    path.reverse();
+    event.target = target;
+    for (var i = 0; i < path.length; i++) {
+      if (path[i] === target) break;
+      dispatchOn(path[i], event, true);
+      if (event._stopped) return;
+    }
+    dispatchOn(target, event, true);
+    dispatchOn(target, event, false);
+    if (event._stopped || event.bubbles === false) return;
+    for (var j = path.length - 2; j >= 0; j--) {
+      if (event._stopped) return;
+      dispatchOn(path[j], event, false);
+    }
+  }
+
+  function dispatchOn(node, event, capture) {
+    var list = (node.__listeners && node.__listeners[event.type]) || [];
+    event.currentTarget = node;
+    for (var i = 0; i < list.length; i++) {
+      if (!!list[i].capture !== !!capture) continue;
+      list[i].fn.call(node, event);
+      if (event._stopped) return;
+    }
+  }
+
+  function EventInit(type, init) {
+    this.type = type;
+    this.bubbles = !!(init && init.bubbles);
+    this.cancelable = !!(init && init.cancelable);
+    this.defaultPrevented = false;
+    this._stopped = false;
+    if (init) {
+      for (var k in init) this[k] = init[k];
+    }
+  }
+  EventInit.prototype.preventDefault = function () { this.defaultPrevented = true; };
+  EventInit.prototype.stopPropagation = function () { this._stopped = true; };
+  var _eventInit = EventInit;
+  EventInit = function (type, init) {
+    _eventInit.call(this, type, init);
+    if (/^drag|drop/.test(type) && !this.dataTransfer) this.dataTransfer = new DataTransfer();
+  };
+  EventInit.prototype = _eventInit.prototype;
+
+  function DataTransfer() {
+    this._data = {};
+    this.effectAllowed = "all";
+    this.dropEffect = "none";
+    this.types = [];
+    this.files = [];
+  }
+  DataTransfer.prototype.setData = function (type, value) {
+    if (globalThis.__failSetData) throw new Error("setData failed");
+    this._data[type] = String(value);
+    if (this.types.indexOf(type) < 0) this.types.push(type);
+  };
+  DataTransfer.prototype.getData = function (type) {
+    if (globalThis.__failGetData) throw new Error("getData failed");
+    return this._data[type] || "";
+  };
+
+  function baseEvent(type, init) {
+    var event = new EventInit(type, init || { bubbles: true });
+    if (/^drag|drop/.test(type) && !event.dataTransfer) event.dataTransfer = new DataTransfer();
+    return event;
+  }
+
+  function nodeProto(nodeType) {
+    var node = {
+      nodeType: nodeType,
+      parentNode: null,
+      parentElement: null,
+      childNodes: [],
+      __listeners: {},
+      appendChild: function (child) { return insertChild(this, child, null); },
+      insertBefore: function (child, ref) { return insertChild(this, child, ref); },
+      removeChild: function (child) {
+        var idx = this.childNodes.indexOf(child);
+        if (idx >= 0) this.childNodes.splice(idx, 1);
+        child.parentNode = null;
+        child.parentElement = null;
+        return child;
+      },
+      replaceChild: function (next, prev) {
+        var idx = this.childNodes.indexOf(prev);
+        if (next.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+          var kids = next.childNodes.slice();
+          if (idx < 0) return next;
+          this.childNodes.splice(idx, 1);
+          prev.parentNode = null;
+          prev.parentElement = null;
+          var ref = this.childNodes[idx] || null;
+          for (var i = 0; i < kids.length; i++) this.insertBefore(kids[i], ref);
+          return prev;
+        }
+        if (idx < 0) return next;
+        this.childNodes.splice(idx, 1, next);
+        prev.parentNode = null;
+        prev.parentElement = null;
+        next.parentNode = this;
+        next.parentElement = this.nodeType === Node.ELEMENT_NODE ? this : this.parentElement;
+        return prev;
+      },
+      addEventListener: function (type, fn, capture) {
+        var list = this.__listeners[type] || (this.__listeners[type] = []);
+        list.push({ fn: fn, capture: capture === true || (capture && capture.capture) });
+      },
+      removeEventListener: function (type, fn) {
+        var list = this.__listeners[type] || [];
+        this.__listeners[type] = list.filter(function (item) { return item.fn !== fn; });
+      },
+      dispatchEvent: function (event) {
+        fire(this, event);
+        return !event.defaultPrevented;
+      },
+      contains: function (other) {
+        var node = other;
+        while (node) {
+          if (node === this) return true;
+          node = node.parentNode;
+        }
+        return false;
+      },
+    };
+    Object.defineProperty(node, "firstChild", {
+      get: function () { return node.childNodes[0] || null; },
+    });
+    Object.defineProperty(node, "lastChild", {
+      get: function () { return node.childNodes[node.childNodes.length - 1] || null; },
+    });
+    Object.defineProperty(node, "nextSibling", {
+      get: function () {
+        var parent = node.parentNode;
+        if (!parent) return null;
+        var index = parent.childNodes.indexOf(node);
+        return index >= 0 ? parent.childNodes[index + 1] || null : null;
+      },
+    });
+    Object.defineProperty(node, "previousSibling", {
+      get: function () {
+        var parent = node.parentNode;
+        if (!parent) return null;
+        var index = parent.childNodes.indexOf(node);
+        return index > 0 ? parent.childNodes[index - 1] : null;
+      },
+    });
+    return node;
+  }
+
+  function insertChild(parent, child, ref) {
+    if (child.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+      var kids = child.childNodes.slice();
+      for (var i = 0; i < kids.length; i++) insertChild(parent, kids[i], ref);
+      return child;
+    }
+    if (child.parentNode) child.parentNode.removeChild(child);
+    var idx = ref ? parent.childNodes.indexOf(ref) : parent.childNodes.length;
+    if (idx < 0) idx = parent.childNodes.length;
+    parent.childNodes.splice(idx, 0, child);
+    child.parentNode = parent;
+    child.parentElement = parent.nodeType === Node.ELEMENT_NODE ? parent : null;
+    return child;
+  }
+
+  function createText(text) {
+    var node = nodeProto(Node.TEXT_NODE);
+    node.textContent = String(text);
+    Object.defineProperty(node, "length", { get: function () { return node.textContent.length; } });
+    node.cloneNode = function () { return createText(node.textContent); };
+    return node;
+  }
+
+  function createFragment() {
+    var node = nodeProto(Node.DOCUMENT_FRAGMENT_NODE);
+    Object.defineProperty(node, "children", { get: function () { return node.childNodes.filter(function (n) { return n.nodeType === Node.ELEMENT_NODE; }); } });
+    node.querySelector = function (sel) { return queryAll(node, sel)[0] || null; };
+    node.querySelectorAll = function (sel) { return queryAll(node, sel); };
+    node.cloneNode = function (deep) {
+      var frag = createFragment();
+      if (deep) node.childNodes.forEach(function (child) { frag.appendChild(child.cloneNode(true)); });
+      return frag;
+    };
+    node.replaceChildren = function () {
+      node.childNodes.splice(0, node.childNodes.length);
+      for (var i = 0; i < arguments.length; i++) node.appendChild(arguments[i]);
+    };
+    return node;
+  }
+
+  function createElement(tag) {
+    var el = nodeProto(Node.ELEMENT_NODE);
+    el.tagName = String(tag || "div").toUpperCase();
+    el._attrs = {};
+    el.nodeName = el.tagName;
+    el.ownerDocument = null;
+    el.style = makeStyle(el);
+    el.classList = new ClassList(el);
+    el.dataset = makeDataset(el);
+    el.childNodes = el.childNodes;
+    Object.defineProperty(el, "children", { get: function () { return el.childNodes.filter(function (n) { return n.nodeType === Node.ELEMENT_NODE; }); } });
+    Object.defineProperty(el, "id", {
+      get: function () { return el.getAttribute("id") || ""; },
+      set: function (value) { el.setAttribute("id", value); },
+    });
+    Object.defineProperty(el, "className", {
+      get: function () { return el.getAttribute("class") || ""; },
+      set: function (value) { el.setAttribute("class", value); },
+    });
+    Object.defineProperty(el, "textContent", {
+      get: function () { return collectText(el); },
+      set: function (value) {
+        el.childNodes.splice(0, el.childNodes.length);
+        if (value != null && value !== "") el.appendChild(createText(value));
+      },
+    });
+    Object.defineProperty(el, "innerHTML", {
+      get: function () { return serialize(el); },
+      set: function (value) {
+        if (el.tagName === "TEMPLATE") {
+          el.content = createFragment();
+          el.content.ownerDocument = el.ownerDocument;
+          parseHTML(el.content, value || "");
+        } else {
+          el.childNodes.splice(0, el.childNodes.length);
+          parseHTML(el, value || "");
+        }
+      },
+    });
+    Object.defineProperty(el, "value", {
+      get: function () {
+        if (el.tagName === "SELECT") return selectedOption(el).value;
+        return el._value != null ? el._value : (el.getAttribute("value") || "");
+      },
+      set: function (value) {
+        el._value = String(value);
+        if (el.tagName === "SELECT") {
+          el.children.forEach(function (opt, index) {
+            opt.selected = opt.value === el._value || opt.textContent === el._value;
+            if (opt.selected) el._selectedIndex = index;
+          });
+        }
+      },
+    });
+    Object.defineProperty(el, "selectedIndex", {
+      get: function () {
+        var opts = el.children.filter(function (n) { return n.tagName === "OPTION"; });
+        for (var i = 0; i < opts.length; i++) if (opts[i].selected) return i;
+        return opts.length ? 0 : -1;
+      },
+    });
+    Object.defineProperty(el, "options", {
+      get: function () { return el.children.filter(function (n) { return n.tagName === "OPTION"; }); },
+    });
+    Object.defineProperty(el, "parentElement", {
+      get: function () {
+        var parent = el.parentNode;
+        return parent && parent.nodeType === Node.ELEMENT_NODE ? parent : null;
+      },
+      set: function (value) { el.parentNode = value; },
+    });
+    el.checked = false;
+    el.disabled = false;
+    el.selected = false;
+    el.open = false;
+    el.hidden = false;
+    el.draggable = false;
+    el.files = [];
+    el.selectionStart = 0;
+    el.rows = 0;
+    el.placeholder = "";
+    el.type = "";
+    el.width = 0;
+    el.height = 0;
+    el.alt = "";
+    el._setAttr = function (name, value) { el._attrs[name] = String(value); };
+    ["href", "src", "title"].forEach(function (name) {
+      Object.defineProperty(el, name, {
+        configurable: true,
+        get: function () {
+          var value = el.getAttribute(name);
+          return value == null ? "" : value;
+        },
+        set: function (value) { el._setAttr(name, value == null ? "" : value); },
+      });
+    });
+    if (el.tagName === "OPTION") {
+      Object.defineProperty(el, "text", {
+        get: function () { return el.textContent; },
+        set: function (value) { el.textContent = value == null ? "" : String(value); },
+      });
+    }
+    el._attrNames = function () { return Object.keys(el._attrs); };
+    el.getAttribute = function (name) {
+      var key = String(name).toLowerCase();
+      return Object.prototype.hasOwnProperty.call(el._attrs, key) ? el._attrs[key] : null;
+    };
+    el.setAttribute = function (name, value) {
+      var key = String(name).toLowerCase();
+      el._attrs[key] = String(value);
+      if (key === "value" && el._value == null) el._value = String(value);
+      if (key === "type") el.type = String(value);
+      if (key === "draggable") el.draggable = String(value) === "true";
+      if (key === "disabled") el.disabled = true;
+      if (key === "checked") el.checked = true;
+      if (key === "open") el.open = true;
+      if (key === "hidden") el.hidden = true;
+      if (key === "selected") el.selected = true;
+    };
+    el.hasAttribute = function (name) { return el.getAttribute(name) != null; };
+    el.removeAttribute = function (name) { delete el._attrs[String(name).toLowerCase()]; };
+    el.querySelector = function (sel) { return queryAll(el, sel)[0] || null; };
+    el.querySelectorAll = function (sel) { return queryAll(el, sel); };
+    el.closest = function (sel) {
+      var node = el;
+      while (node && node.nodeType === Node.ELEMENT_NODE) {
+        if (elementMatches(node, sel)) return node;
+        node = node.parentElement;
+      }
+      return null;
+    };
+    el.matches = function (sel) { return elementMatches(el, sel); };
+    el.remove = function () { if (el.parentNode) el.parentNode.removeChild(el); };
+    el.replaceChildren = function () {
+      while (el.childNodes.length) el.removeChild(el.childNodes[0]);
+      for (var i = 0; i < arguments.length; i++) el.appendChild(arguments[i]);
+    };
+    el.cloneNode = function (deep) {
+      var copy = createElement(el.tagName);
+      copy.ownerDocument = el.ownerDocument;
+      el._attrNames().forEach(function (name) { copy.setAttribute(name, el.getAttribute(name)); });
+      copy._value = el._value;
+      copy.checked = el.checked;
+      copy.selected = el.selected;
+      copy.disabled = el.disabled;
+      copy.open = el.open;
+      copy.hidden = el.hidden;
+      copy.draggable = el.draggable;
+      copy.type = el.type;
+      if (el.tagName === "TEMPLATE") {
+        copy.content = el.content.cloneNode(!!deep);
+      } else if (deep) {
+        el.childNodes.forEach(function (child) { copy.appendChild(child.cloneNode(true)); });
+      }
+      return copy;
+    };
+    el.click = function () { el.dispatchEvent(baseEvent("click", { bubbles: true })); };
+    el.focus = function () {
+      if (el.ownerDocument) el.ownerDocument.activeElement = el;
+      el.dispatchEvent(baseEvent("focus", { bubbles: false }));
+    };
+    el.blur = function () { el.dispatchEvent(baseEvent("blur", { bubbles: false })); };
+    el.select = function () { el.selectionStart = 0; };
+    el.setSelectionRange = function (start) { el.selectionStart = start; };
+    el.getBoundingClientRect = function () {
+      var top = el._rectTop != null ? el._rectTop : 20;
+      return { x: 0, y: top, left: 10, top: top, right: 210, bottom: top + 40, width: 200, height: 40 };
+    };
+    el.scrollIntoView = function () { globalThis.__lastScrolled = el; };
+    if (el.tagName === "TEMPLATE") el.content = createFragment();
+    if (el.tagName === "SELECT") Object.setPrototypeOf(el, HTMLSelectElement.prototype);
+    return el;
+  }
+
+  function selectedOption(el) {
+    var opts = el.options;
+    function optionValue(opt) {
+      if (opt._value != null) return String(opt._value);
+      var attr = opt.getAttribute("value");
+      return attr != null ? attr : opt.textContent;
+    }
+    for (var i = 0; i < opts.length; i++) if (opts[i].selected) return { value: optionValue(opts[i]), index: i };
+    if (opts.length) return { value: optionValue(opts[0]), index: 0 };
+    return { value: "", index: -1 };
+  }
+
+  function collectText(node) {
+    if (node.nodeType === Node.TEXT_NODE) return node.textContent;
+    return node.childNodes.map(collectText).join("");
+  }
+
+  function serialize(node) {
+    return node.childNodes.map(function (child) {
+      if (child.nodeType === Node.TEXT_NODE) return encodeText(child.textContent);
+      var attrs = child._attrNames().map(function (name) {
+        return " " + name + '="' + encodeText(child.getAttribute(name)) + '"';
+      }).join("");
+      if (VOID_TAGS[child.tagName.toLowerCase()]) return "<" + child.tagName.toLowerCase() + attrs + ">";
+      return "<" + child.tagName.toLowerCase() + attrs + ">" + serialize(child) + "</" + child.tagName.toLowerCase() + ">";
+    }).join("");
+  }
+
+  function parseHTML(parent, html) {
+    var i = 0;
+    function parse(stop) {
+      while (i < html.length) {
+        if (html.charAt(i) !== "<") {
+          var next = html.indexOf("<", i);
+          if (next < 0) next = html.length;
+          var text = decodeEntities(html.slice(i, next));
+          if (text) parentAppend(parent, createText(text));
+          i = next;
+          continue;
+        }
+        if (html.slice(i, i + 4) === "<!--") {
+          i = html.indexOf("-->", i) + 3;
+          continue;
+        }
+        if (html.slice(i, i + 2) === "</") {
+          var closeEnd = html.indexOf(">", i);
+          var closeName = html.slice(i + 2, closeEnd).trim().toLowerCase();
+          i = closeEnd + 1;
+          if (!stop || closeName === stop) return;
+          continue;
+        }
+        var tagEnd = html.indexOf(">", i);
+        var raw = html.slice(i + 1, tagEnd).trim();
+        i = tagEnd + 1;
+        var selfClose = /\/$/.test(raw);
+        if (selfClose) raw = raw.slice(0, -1).trim();
+        var bits = raw.match(/[^\s"'=]+(?:=(?:"[^"]*"|'[^']*'|[^\s"']+))?/g) || [];
+        var tag = bits.shift().toLowerCase();
+        var el = createElement(tag);
+        el.ownerDocument = parent.ownerDocument || parent;
+        bits.forEach(function (bit) { applyAttr(el, bit); });
+        parentAppend(parent, el);
+        if (!selfClose && !VOID_TAGS[tag]) {
+          var saved = parent;
+          parent = tag === "template" ? el.content : el;
+          if (tag === "template") el.content.ownerDocument = el.ownerDocument;
+          parse(tag);
+          parent = saved;
+        }
+      }
+    }
+    parse(null);
+  }
+
+  function parentAppend(parent, child) {
+    parent.appendChild(child);
+  }
+
+  function applyAttr(el, bit) {
+    var eq = bit.indexOf("=");
+    if (eq < 0) {
+      el.setAttribute(bit, "");
+      return;
+    }
+    var name = bit.slice(0, eq);
+    var value = bit.slice(eq + 1);
+    if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
+      value = value.slice(1, -1);
+    }
+    el.setAttribute(name, decodeEntities(value));
+  }
+
+  function splitCommas(selector) {
+    var parts = [];
+    var buf = "";
+    var depth = 0;
+    for (var i = 0; i < selector.length; i++) {
+      var c = selector.charAt(i);
+      if (c === "[") depth++;
+      else if (c === "]") depth--;
+      else if (c === "," && depth === 0) {
+        parts.push(buf);
+        buf = "";
+        continue;
+      }
+      buf += c;
+    }
+    if (buf.trim()) parts.push(buf);
+    return parts;
+  }
+
+  function tokenize(selector) {
+    var raw = [];
+    var buf = "";
+    var depth = 0;
+    for (var i = 0; i < selector.length; i++) {
+      var c = selector.charAt(i);
+      if (c === "[") depth++;
+      else if (c === "]") depth--;
+      if (depth === 0 && (c === " " || c === ">")) {
+        if (buf.trim()) raw.push(buf.trim());
+        buf = "";
+        if (c === ">") raw.push(">");
+        continue;
+      }
+      buf += c;
+    }
+    if (buf.trim()) raw.push(buf.trim());
+    var steps = [];
+    var rel = "desc";
+    raw.forEach(function (part) {
+      if (part === ">") {
+        rel = ">";
+        return;
+      }
+      steps.push({ rel: rel, compound: part });
+      rel = "desc";
+    });
+    return steps;
+  }
+
+  function matchesCompound(el, compound) {
+    if (!el || el.nodeType !== Node.ELEMENT_NODE) return false;
+    var rest = compound.trim();
+    var tag = rest.match(/^[A-Za-z*][A-Za-z0-9-]*/);
+    if (tag) {
+      if (tag[0] !== "*" && el.tagName.toLowerCase() !== tag[0].toLowerCase()) return false;
+      rest = rest.slice(tag[0].length);
+    }
+    if (rest.indexOf(":last-child") >= 0 || rest.indexOf(":first-child") >= 0) {
+      var elementSiblings = ((el.parentNode && el.parentNode.childNodes) || []).filter(function (child) {
+        return child.nodeType === Node.ELEMENT_NODE;
+      });
+      if (rest.indexOf(":last-child") >= 0 && elementSiblings[elementSiblings.length - 1] !== el) return false;
+      if (rest.indexOf(":first-child") >= 0 && elementSiblings[0] !== el) return false;
+      rest = rest.replace(/:(?:last|first)-child/g, "");
+    }
+    var parts = rest.match(/(\.[A-Za-z0-9_-]+|#[A-Za-z0-9_-]+|\[[^\]]+\])/g) || [];
+    if (!tag && rest.replace(/(\.[A-Za-z0-9_-]+|#[A-Za-z0-9_-]+|\[[^\]]+\])/g, "")) return false;
+    for (var i = 0; i < parts.length; i++) {
+      var part = parts[i];
+      if (part.charAt(0) === ".") {
+        if (!el.classList.contains(part.slice(1))) return false;
+      } else if (part.charAt(0) === "#") {
+        if (el.id !== part.slice(1)) return false;
+      } else {
+        var inner = part.slice(1, -1);
+        function unquote(value) {
+          if ((value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') || (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")) {
+            return value.slice(1, -1);
+          }
+          return value;
+        }
+        var star = inner.indexOf("*=");
+        var caret = inner.indexOf("^=");
+        var dollar = inner.indexOf("$=");
+        var eq = inner.indexOf("=");
+        if (star >= 0 || caret >= 0 || dollar >= 0) {
+          var at = star >= 0 ? star : caret >= 0 ? caret : dollar;
+          var kind = inner.charAt(at);
+          var actual = el.getAttribute(inner.slice(0, at).trim()) || "";
+          var expected = unquote(inner.slice(at + 2).trim());
+          if (kind === "*" && actual.indexOf(expected) < 0) return false;
+          if (kind === "^" && actual.indexOf(expected) !== 0) return false;
+          if (kind === "$" && (expected === "" || actual.slice(-expected.length) !== expected)) return false;
+        } else if (eq < 0) {
+          if (!el.hasAttribute(inner.trim())) return false;
+        } else if (el.getAttribute(inner.slice(0, eq).trim()) !== unquote(inner.slice(eq + 1).trim())) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  function elementMatches(el, selector) {
+    return splitCommas(selector).some(function (alt) {
+      var steps = tokenize(alt);
+      if (!steps.length || !matchesCompound(el, steps[steps.length - 1].compound)) return false;
+      var node = el;
+      for (var i = steps.length - 2; i >= 0; i--) {
+        var rel = steps[i + 1].rel;
+        if (rel === ">") {
+          node = node.parentElement;
+          if (!node || !matchesCompound(node, steps[i].compound)) return false;
+        } else {
+          node = node.parentElement;
+          var found = false;
+          while (node) {
+            if (matchesCompound(node, steps[i].compound)) { found = true; break; }
+            node = node.parentElement;
+          }
+          if (!found) return false;
+        }
+      }
+      return true;
+    });
+  }
+
+  function descendants(scope) {
+    var out = [];
+    function walk(node) {
+      var kids = node.tagName === "TEMPLATE" ? [] : (node.children || node.childNodes || []);
+      for (var i = 0; i < kids.length; i++) {
+        if (kids[i].nodeType && kids[i].nodeType !== Node.ELEMENT_NODE) continue;
+        out.push(kids[i]);
+        walk(kids[i]);
+      }
+    }
+    if (scope.nodeType === Node.DOCUMENT_NODE) {
+      if (scope.documentElement) {
+        out.push(scope.documentElement);
+        walk(scope.documentElement);
+      }
+      return out;
+    }
+    walk(scope);
+    return out;
+  }
+
+  function queryAll(scope, selector) {
+    var found = [];
+    splitCommas(selector).forEach(function (alt) {
+      var steps = tokenize(alt);
+      if (!steps.length) return;
+      var nodes = descendants(scope).filter(function (el) { return matchesCompound(el, steps[0].compound); });
+      for (var s = 1; s < steps.length; s++) {
+        var next = [];
+        nodes.forEach(function (el) {
+          var pool = steps[s].rel === ">" ? el.children : descendants(el);
+          pool.forEach(function (child) {
+            if (matchesCompound(child, steps[s].compound)) next.push(child);
+          });
+        });
+        nodes = next;
+      }
+      nodes.forEach(function (el) {
+        if (found.indexOf(el) < 0) found.push(el);
+      });
+    });
+    return found;
+  }
+
+  function createDocument() {
+    var doc = nodeProto(Node.DOCUMENT_NODE);
+    doc.nodeType = Node.DOCUMENT_NODE;
+    doc.readyState = "complete";
+    doc.activeElement = null;
+    doc.documentElement = createElement("html");
+    doc.head = createElement("head");
+    doc.body = createElement("body");
+    doc.documentElement.ownerDocument = doc;
+    doc.head.ownerDocument = doc;
+    doc.body.ownerDocument = doc;
+    doc.documentElement.appendChild(doc.head);
+    doc.documentElement.appendChild(doc.body);
+    doc.documentElement.parentNode = doc;
+    doc.createElement = function (tag) {
+      var el = createElement(tag);
+      el.ownerDocument = doc;
+      if (el.content) el.content.ownerDocument = doc;
+      return el;
+    };
+    doc.createTextNode = createText;
+    doc.createDocumentFragment = createFragment;
+    doc.createTreeWalker = function (root, what) {
+      var nodes = [];
+      function walk(node) {
+        node.childNodes.forEach(function (child) {
+          if (child.nodeType === Node.TEXT_NODE && (what & NodeFilter.SHOW_TEXT)) nodes.push(child);
+          if (child.nodeType === Node.ELEMENT_NODE) walk(child);
+        });
+      }
+      walk(root);
+      var index = 0;
+      return { nextNode: function () { return index < nodes.length ? nodes[index++] : null; }, currentNode: null };
+    };
+    doc.createRange = function () {
+      var range = { _start: null, _end: null, _so: 0, _eo: 0 };
+      range.setStart = function (node, off) { range._start = node; range._so = off; };
+      range.setEnd = function (node, off) { range._end = node; range._eo = off; };
+      range.surroundContents = function (span) {
+        if (!range._start || range._start !== range._end || range._start.nodeType !== Node.TEXT_NODE) {
+          throw new Error("InvalidStateError");
+        }
+        var text = range._start.textContent;
+        var before = text.slice(0, range._so);
+        var mid = text.slice(range._so, range._eo);
+        var after = text.slice(range._eo);
+        var parent = range._start.parentNode;
+        var midNode = createText(mid);
+        span.appendChild(midNode);
+        if (before) parent.insertBefore(createText(before), range._start);
+        parent.insertBefore(span, range._start);
+        if (after) parent.insertBefore(createText(after), range._start);
+        parent.removeChild(range._start);
+      };
+      return range;
+    };
+    doc.getElementById = function (id) {
+      var all = descendants(doc);
+      for (var i = 0; i < all.length; i++) if (all[i].id === id) return all[i];
+      return null;
+    };
+    doc.querySelector = function (sel) { return queryAll(doc, sel)[0] || null; };
+    doc.querySelectorAll = function (sel) { return queryAll(doc, sel); };
+    return doc;
+  }
+
+  function AbortSignal() {
+    this.aborted = false;
+    this.__listeners = {};
+    this.addEventListener = function (type, fn) {
+      (this.__listeners[type] || (this.__listeners[type] = [])).push({ fn: fn, capture: false });
+    };
+  }
+  function AbortController() {
+    this.signal = new AbortSignal();
+  }
+  AbortController.prototype.abort = function () {
+    if (this.signal.aborted) return;
+    this.signal.aborted = true;
+    fire(this.signal, baseEvent("abort", { bubbles: false }));
+    (globalThis.__pending || []).slice().forEach(function (item) {
+      if (item.signal === this.signal) item.reject(abortError());
+    }, this);
+  };
+  function abortError() {
+    var err = new Error("The operation was aborted");
+    err.name = "AbortError";
+    return err;
+  }
+
+  function responseOf(spec) {
+    var body = spec.text != null ? spec.text : (spec.json != null ? JSON.stringify(spec.json) : "");
+    return {
+      ok: spec.status >= 200 && spec.status < 300,
+      status: spec.status,
+      statusText: spec.statusText || "",
+      json: function () {
+        if (spec.jsonError) return Promise.reject(new Error("invalid json"));
+        return Promise.resolve(spec.json != null ? spec.json : JSON.parse(body || "null"));
+      },
+      text: function () { return Promise.resolve(body); },
+    };
+  }
+
+  function matchRoute(url) {
+    var routes = globalThis.__routes || [];
+    var found = null;
+    for (var i = 0; i < routes.length; i++) {
+      if (url.indexOf(routes[i].url) >= 0) found = routes[i];
+    }
+    return found;
+  }
+
+  var document = createDocument();
+  var location = {
+    href: "http://127.0.0.1/",
+    origin: "http://127.0.0.1",
+    pathname: "/",
+    search: "",
+    hash: "",
+    assign: function (url) { globalThis.__assigned = String(url); setLocation(url); },
+    reload: function () { globalThis.__reloaded = true; },
+  };
+  Object.defineProperty(location, "hash", {
+    get: function () { return location._hash || ""; },
+    set: function (value) {
+      var next = String(value || "");
+      if (next && next.charAt(0) !== "#") next = "#" + next;
+      location._hash = next;
+      location.href = location.origin + location.pathname + location.search + next;
+    },
+  });
+  function setLocation(url) {
+    var abs = String(url);
+    if (abs.charAt(0) === "/") abs = "http://127.0.0.1" + abs;
+    if (abs.charAt(0) === "#") abs = location.origin + location.pathname + location.search + abs;
+    var hash = "";
+    var hashAt = abs.indexOf("#");
+    if (hashAt >= 0) { hash = abs.slice(hashAt); abs = abs.slice(0, hashAt); }
+    var q = abs.indexOf("?");
+    var path = q >= 0 ? abs.slice(0, q) : abs;
+    var search = q >= 0 ? abs.slice(q) : "";
+    var origin = "http://127.0.0.1";
+    var proto = path.indexOf("://");
+    if (proto >= 0) {
+      var slash = path.indexOf("/", proto + 3);
+      origin = slash >= 0 ? path.slice(0, slash) : path;
+      path = slash >= 0 ? path.slice(slash) : "/";
+    }
+    location.href = origin + path + search + hash;
+    location.origin = origin;
+    location.pathname = path || "/";
+    location.search = search;
+    location.hash = hash;
+  }
+
+  globalThis.Node = Node;
+  globalThis.NodeFilter = NodeFilter;
+  globalThis.HTMLElement = HTMLElement;
+  globalThis.HTMLSelectElement = HTMLSelectElement;
+  globalThis.Event = EventInit;
+  globalThis.DataTransfer = DataTransfer;
+  globalThis.AbortController = AbortController;
+  globalThis.document = document;
+  globalThis.window = globalThis;
+  globalThis.__listeners = {};
+  globalThis.addEventListener = function (type, fn, capture) {
+    var list = globalThis.__listeners[type] || (globalThis.__listeners[type] = []);
+    list.push({ fn: fn, capture: capture === true || (capture && capture.capture) });
+  };
+  globalThis.removeEventListener = function (type, fn) {
+    var list = globalThis.__listeners[type] || [];
+    globalThis.__listeners[type] = list.filter(function (item) { return item.fn !== fn; });
+  };
+  globalThis.dispatchEvent = function (event) {
+    fire(globalThis, event);
+    return !event.defaultPrevented;
+  };
+  globalThis.location = location;
+  globalThis.history = { replaceState: function (_s, _t, url) { if (url != null) setLocation(url); } };
+  globalThis.navigator = { clipboard: { writeText: function (text) { globalThis.__copied = text; return Promise.resolve(); } } };
+  globalThis.scrollX = 0;
+  globalThis.scrollY = 0;
+  globalThis.__alerts = [];
+  globalThis.__confirms = [];
+  globalThis.__confirmYes = true;
+  globalThis.__routes = [];
+  globalThis.__pending = [];
+  globalThis.__timers = [];
+  globalThis.__raf = [];
+  globalThis.__loaded = {};
+  globalThis.__moduleCache = {};
+  globalThis.__factories = {};
+  globalThis.__preloaded = [];
+  globalThis.__requests = [];
+  globalThis.alert = function (msg) { globalThis.__alerts.push(String(msg)); };
+  globalThis.confirm = function (msg) { globalThis.__confirms.push(String(msg)); return !!globalThis.__confirmYes; };
+  globalThis.Image = function () {
+    this.src = "";
+    var img = this;
+    Object.defineProperty(this, "src", {
+      set: function (value) { img._src = String(value); globalThis.__preloaded.push(img._src); },
+      get: function () { return img._src || ""; },
+    });
+  };
+  globalThis.setTimeout = function (fn) {
+    var id = globalThis.__timers.length + 1;
+    globalThis.__timers.push({ id: id, fn: fn });
+    return id;
+  };
+  globalThis.clearTimeout = function (id) {
+    globalThis.__timers = globalThis.__timers.filter(function (item) { return item.id !== id; });
+  };
+  globalThis.requestAnimationFrame = function (fn) {
+    globalThis.__raf.push(fn);
+    return globalThis.__raf.length;
+  };
+  globalThis.fetch = function (url, opts) {
+    opts = opts || {};
+    globalThis.__requests.push({
+      url: String(url),
+      method: (opts && opts.method) || "GET",
+      body: opts && opts.body != null ? String(opts.body) : null,
+    });
+    if (opts.signal && opts.signal.aborted) return Promise.reject(abortError());
+    var spec = matchRoute(String(url));
+    if (spec && spec.reject) return Promise.reject(new Error(String(spec.reject)));
+    if (!spec || spec.hang) {
+      return new Promise(function (resolve, reject) {
+        var item = { url: String(url), resolve: resolve, reject: reject, signal: opts.signal };
+        globalThis.__pending.push(item);
+        if (opts.signal) {
+          opts.signal.addEventListener("abort", function () { reject(abortError()); });
+        }
+      });
+    }
+    return Promise.resolve(responseOf(spec));
+  };
+  globalThis.__setHtml = function (html) {
+    document.body.innerHTML = html;
+  };
+  globalThis.__setLocation = setLocation;
+  globalThis.__tick = function () {
+    var guard = 0;
+    while ((globalThis.__timers.length || globalThis.__raf.length) && guard++ < 1000) {
+      var timers = globalThis.__timers.splice(0, globalThis.__timers.length);
+      var raf = globalThis.__raf.splice(0, globalThis.__raf.length);
+      timers.forEach(function (item) { item.fn(); });
+      raf.forEach(function (fn) { fn(); });
+    }
+  };
+  globalThis.__fulfill = function (urlPart, spec) {
+    var rest = [];
+    globalThis.__pending.forEach(function (item) {
+      if (item.url.indexOf(urlPart) >= 0) item.resolve(responseOf(spec));
+      else rest.push(item);
+    });
+    globalThis.__pending = rest;
+  };
+  globalThis.__register = function (name, factory) { globalThis.__factories[name] = factory; };
+  globalThis.__require = function (fromFile, spec) {
+    var base = fromFile.split("/");
+    base.pop();
+    spec.split("/").forEach(function (part) {
+      if (part === "." || part === "") return;
+      base.push(part);
+    });
+    var name = base.join("/");
+    if (globalThis.__moduleCache[name]) return globalThis.__moduleCache[name];
+    var factory = globalThis.__factories[name];
+    if (!factory) throw new Error("Unknown module " + name);
+    var exports = {};
+    globalThis.__moduleCache[name] = exports;
+    factory(function (next) { return globalThis.__require(name, next); }, exports);
+    globalThis.__loaded[name] = exports;
+    return exports;
+  };
+  globalThis.__load = function (name) {
+    return globalThis.__require("", "./" + name);
+  };
+  globalThis.__reset = function () {
+    globalThis.__moduleCache = {};
+    globalThis.__loaded = {};
+    globalThis.__routes = [];
+    globalThis.__pending = [];
+    globalThis.__timers = [];
+    globalThis.__raf = [];
+    globalThis.__alerts = [];
+    globalThis.__confirms = [];
+    globalThis.__confirmYes = true;
+    globalThis.__reloaded = false;
+    globalThis.__assigned = null;
+    globalThis.__copied = null;
+    globalThis.__lastScrolled = null;
+    globalThis.__preloaded = [];
+    globalThis.__requests = [];
+    globalThis.__listeners = {};
+    delete globalThis.hljs;
+    delete globalThis.Diff2HtmlUI;
+    globalThis.__failSetData = false;
+    globalThis.__failGetData = false;
+    globalThis.atob = globalThis.__builtinAtob;
+    globalThis.TextDecoder = globalThis.__builtinTextDecoder;
+    globalThis.scrollX = 0;
+    globalThis.scrollY = 0;
+    document = createDocument();
+    globalThis.document = document;
+    document.__listeners = {};
+    setLocation("http://127.0.0.1/");
+    if (typeof showdown !== "undefined") globalThis.showdown = showdown;
+  };
+
+  if (typeof atob === "undefined") {
+    globalThis.atob = function (value) {
+      var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+      var str = String(value).replace(/=+$/, "");
+      var out = "";
+      var buffer = 0;
+      var bits = 0;
+      for (var i = 0; i < str.length; i++) {
+        var idx = chars.indexOf(str.charAt(i));
+        if (idx < 0) throw new Error("Invalid character");
+        buffer = (buffer << 6) | idx;
+        bits += 6;
+        if (bits >= 8) {
+          bits -= 8;
+          out += String.fromCharCode((buffer >> bits) & 255);
+        }
+      }
+      return out;
+    };
+  }
+  (function () {
+    var nativeAtob = globalThis.atob;
+    globalThis.atob = function (value) {
+      var str = String(value).replace(/[\t\n\f\r ]/g, "");
+      if (/[^A-Za-z0-9+/=]/.test(str)) throw new Error("Invalid character");
+      return nativeAtob(str);
+    };
+  })();
+  if (typeof TextDecoder !== "function") {
+    function Utf8Decoder() {}
+    Utf8Decoder.prototype.decode = function (bytes) {
+      var out = "";
+      var i = 0;
+      var list = bytes || [];
+      while (i < list.length) {
+        var c = list[i++];
+        if (c < 128) {
+          out += String.fromCharCode(c);
+        } else if (c < 224) {
+          out += String.fromCharCode(((c & 31) << 6) | (list[i++] & 63));
+        } else if (c < 240) {
+          out += String.fromCharCode(((c & 15) << 12) | ((list[i++] & 63) << 6) | (list[i++] & 63));
+        } else {
+          var point = ((c & 7) << 18) | ((list[i++] & 63) << 12) | ((list[i++] & 63) << 6) | (list[i++] & 63);
+          point -= 65536;
+          out += String.fromCharCode(55296 + (point >> 10), 56320 + (point & 1023));
+        }
+      }
+      return out;
+    };
+    globalThis.TextDecoder = Utf8Decoder;
+  }
+  globalThis.__builtinAtob = globalThis.atob;
+  globalThis.__builtinTextDecoder = globalThis.TextDecoder;
+})();
diff --git a/pygitweb/static_tests/js_runtime.py b/pygitweb/static_tests/js_runtime.py
new file mode 100644
index 0000000..02780ce
--- /dev/null
+++ b/pygitweb/static_tests/js_runtime.py
@@ -0,0 +1,342 @@
+"""Load pygitweb browser scripts in an embedded JavaScript engine."""
+
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+from typing import Any, Protocol
+
+_STATIC = Path(__file__).resolve().parents[1] / "static"
+_SHIM = Path(__file__).resolve().parent / "dom_shim.js"
+_SHOWDOWN = _STATIC / "vendor" / "showdown-2.1.0.min.js"
+_IMPORT_RE = re.compile(
+	r"""import\s+\{([^}]*)\}\s*from\s*["']([^"']+)["']\s*;?""",
+	re.S,
+)
+_EXPORT_FN_RE = re.compile(r"^([ \t]*)export\s+function\s+(\w+)", re.M)
+
+
+class JsError(RuntimeError):
+	pass
+
+
+class Engine(Protocol):
+	def eval(self, source: str) -> Any: ...
+
+	def close(self) -> None: ...
+
+
+def _import_bindings(block: str) -> str:
+	parts: list[str] = []
+	for raw in block.split(","):
+		item = raw.strip()
+		if not item:
+			continue
+		if " as " in item:
+			src, dst = item.split(" as ", 1)
+			parts.append(f"{src.strip()}: {dst.strip()}")
+		else:
+			parts.append(item)
+	return "{ " + ", ".join(parts) + " }"
+
+
+def _with_global_this(source: str) -> str:
+	# Showdown's UMD prefers module.exports when an embedder defines module.
+	return (
+		"(function(){\n"
+		"var module = undefined;\n"
+		"var exports = undefined;\n"
+		"var define = undefined;\n" + source + "\n}).call(globalThis)"
+	)
+
+
+def transform_module(source: str) -> str:
+	exports: list[str] = []
+
+	def replace_import(match: re.Match[str]) -> str:
+		names = _import_bindings(match.group(1))
+		spec = json.dumps(match.group(2))
+		if names == "{ }":
+			return f"__require({spec});"
+		return f"const {names} = __require({spec});"
+
+	transformed = _IMPORT_RE.sub(replace_import, source)
+
+	def replace_export(match: re.Match[str]) -> str:
+		exports.append(match.group(2))
+		return f"{match.group(1)}function {match.group(2)}"
+
+	transformed = _EXPORT_FN_RE.sub(replace_export, transformed)
+	if re.search(r"^\s*export\s+", transformed, re.M):
+		raise JsError("unsupported export syntax")
+	tail = "\n".join(f"__exports.{name} = {name};" for name in exports)
+	return transformed + ("\n" + tail if tail else "")
+
+
+def available_engines() -> list[str]:
+	found: list[str] = []
+	try:
+		import STPyV8  # type: ignore[import-not-found]  # noqa: F401
+	except ImportError:
+		pass
+	else:
+		found.append("stpyv8")
+	try:
+		import pythonmonkey  # type: ignore[import-not-found]  # noqa: F401
+	except ImportError:
+		pass
+	else:
+		found.append("pythonmonkey")
+	return found
+
+
+class _PythonMonkeyEngine:
+	def __init__(self) -> None:
+		import asyncio
+
+		import pythonmonkey  # type: ignore[import-not-found]
+
+		self._pm = pythonmonkey
+		self._loop = asyncio.new_event_loop()
+		asyncio.set_event_loop(self._loop)
+		self.eval(_SHIM.read_text(encoding="utf-8"))
+		self.eval(_with_global_this(_SHOWDOWN.read_text(encoding="utf-8")))
+		self._register_modules()
+
+	async def _eval_on_loop(self, source: str) -> Any:
+		import asyncio
+
+		try:
+			value = self._pm.eval(source)
+		except Exception as exc:
+			raise JsError(str(exc)) from exc
+		for _ in range(50):
+			await asyncio.sleep(0)
+		return value
+
+	def eval(self, source: str) -> Any:
+		return self._loop.run_until_complete(self._eval_on_loop(source))
+
+	def flush(self) -> None:
+		return None
+
+	def _register_modules(self) -> None:
+		for path in sorted(_STATIC.glob("*.js")):
+			transformed = transform_module(path.read_text(encoding="utf-8"))
+			self.eval(
+				"__register(" + json.dumps(path.name) + ", function(__require, __exports) {\n" + transformed + "\n});"
+			)
+
+	def close(self) -> None:
+		return None
+
+
+class _StPyV8Engine:
+	def __init__(self) -> None:
+		import STPyV8  # type: ignore[import-not-found]
+
+		self._STPyV8 = STPyV8
+		self._ctx = STPyV8.JSContext()
+		self._ctx.enter()
+		self.eval(_SHIM.read_text(encoding="utf-8"))
+		self.eval(_with_global_this(_SHOWDOWN.read_text(encoding="utf-8")))
+		self._register_modules()
+
+	def eval(self, source: str) -> Any:
+		try:
+			value = self._ctx.eval(source)
+		except Exception as exc:
+			raise JsError(str(exc)) from exc
+		if value is None:
+			return None
+		if isinstance(value, str | int | float | bool):
+			return value
+		text = str(value)
+		return text
+
+	def _register_modules(self) -> None:
+		for path in sorted(_STATIC.glob("*.js")):
+			transformed = transform_module(path.read_text(encoding="utf-8"))
+			self.eval(
+				"__register(" + json.dumps(path.name) + ", function(__require, __exports) {\n" + transformed + "\n});"
+			)
+
+	def close(self) -> None:
+		self._ctx.leave()
+
+
+_ENGINES: dict[str, Engine] = {}
+
+
+def _engine(name: str) -> Engine:
+	cached = _ENGINES.get(name)
+	if cached is not None:
+		return cached
+	if name == "pythonmonkey":
+		engine: Engine = _PythonMonkeyEngine()
+	elif name == "stpyv8":
+		engine = _StPyV8Engine()
+	else:
+		raise JsError(f"unknown JS engine {name}")
+	_ENGINES[name] = engine
+	return engine
+
+
+class JsPage:
+	def __init__(self, engine_name: str) -> None:
+		self.engine_name = engine_name
+		self._engine = _engine(engine_name)
+		self._engine.eval("__reset()")
+		self._engine.eval(_with_global_this(_SHOWDOWN.read_text(encoding="utf-8")))
+
+	def close(self) -> None:
+		return None
+
+	def run(self, source: str) -> None:
+		self._engine.eval(source)
+		self._drain()
+
+	def _drain(self) -> None:
+		if getattr(self._engine, "flush", None) is not None:
+			return
+		self._engine.eval("(function(){var n=0;function hop(){if(++n>40)return;Promise.resolve().then(hop);}hop();})()")
+
+	def value(self, expression: str) -> Any:
+		raw = self._engine.eval(
+			"(function(){ var __v = (" + expression + "); return __v === undefined ? 'null' : JSON.stringify(__v); })()"
+		)
+		if raw is None:
+			return None
+		return json.loads(str(raw))
+
+	def settle(self, expression: str) -> Any:
+		self.run(
+			"globalThis.__settled = null; Promise.resolve("
+			+ expression
+			+ ").then(function(v){ globalThis.__settled = v === undefined ? null : v; },"
+			+ " function(e){ globalThis.__settled = {__error: String(e && e.message || e)}; });"
+		)
+		return self.value("globalThis.__settled")
+
+	def set_html(self, html: str) -> None:
+		self.run("__setHtml(" + json.dumps(html) + ")")
+
+	def set_location(self, href: str) -> None:
+		self.run("__setLocation(" + json.dumps(href) + ")")
+
+	def load(self, name: str) -> None:
+		self.run("__load(" + json.dumps(name) + ")")
+
+	def call(self, module: str, fn: str, *args: object) -> Any:
+		joined = ", ".join(json.dumps(arg) for arg in args)
+		return self.value(f"__loaded[{json.dumps(module)}][{json.dumps(fn)}]({joined})")
+
+	def route(
+		self,
+		url: str,
+		*,
+		status: int = 200,
+		body: object | None = None,
+		text: str | None = None,
+		hang: bool = False,
+		json_error: bool = False,
+		status_text: str = "",
+		error: str | None = None,
+	) -> None:
+		spec: dict[str, object] = {
+			"url": url,
+			"status": status,
+			"hang": hang,
+			"jsonError": json_error,
+			"statusText": status_text,
+		}
+		if error is not None:
+			spec["reject"] = error
+		if text is not None:
+			spec["text"] = text
+		elif body is not None:
+			spec["json"] = body
+		self.run("__routes.push(" + json.dumps(spec) + ")")
+
+	def fulfill(self, url: str, *, status: int = 200, body: object | None = None, text: str | None = None) -> None:
+		spec: dict[str, object] = {"status": status}
+		if text is not None:
+			spec["text"] = text
+		elif body is not None:
+			spec["json"] = body
+		self.run("__fulfill(" + json.dumps(url) + ", " + json.dumps(spec) + ")")
+
+	def tick(self) -> None:
+		self.run("__tick()")
+
+	def text(self, selector: str) -> str:
+		found = self.value("document.querySelector(" + json.dumps(selector) + ").textContent")
+		return "" if found is None else str(found)
+
+	def texts(self, selector: str) -> list[str]:
+		found = self.value(
+			"Array.from(document.querySelectorAll("
+			+ json.dumps(selector)
+			+ ")).map(function(el){return el.textContent;})"
+		)
+		return [str(item) for item in found or []]
+
+	def attr(self, selector: str, name: str) -> str | None:
+		value = self.value(
+			"(document.querySelector(" + json.dumps(selector) + ")||{}).getAttribute(" + json.dumps(name) + ")"
+		)
+		return None if value is None else str(value)
+
+	def attrs(self, selector: str, name: str) -> list[str | None]:
+		found = self.value(
+			"Array.from(document.querySelectorAll("
+			+ json.dumps(selector)
+			+ ")).map(function(el){return el.getAttribute("
+			+ json.dumps(name)
+			+ ");})"
+		)
+		return list(found or [])
+
+	def count(self, selector: str) -> int:
+		value = self.value("document.querySelectorAll(" + json.dumps(selector) + ").length")
+		return int(value or 0)
+
+	def classes(self, selector: str) -> list[str]:
+		value = self.value(
+			"(document.querySelector("
+			+ json.dumps(selector)
+			+ ").getAttribute('class')||'').split(/\\s+/).filter(Boolean)"
+		)
+		return list(value or [])
+
+	def click(self, selector: str) -> None:
+		self.run("document.querySelector(" + json.dumps(selector) + ").click()")
+
+	def dispatch(self, selector: str, event: str, **fields: object) -> None:
+		init: dict[str, object] = {"bubbles": True, "cancelable": True}
+		init.update(fields)
+		self.run(
+			"document.querySelector("
+			+ json.dumps(selector)
+			+ ").dispatchEvent(new Event("
+			+ json.dumps(event)
+			+ ", "
+			+ json.dumps(init)
+			+ "))"
+		)
+
+	def dispatch_window(self, event: str) -> None:
+		self.run("window.dispatchEvent(new Event(" + json.dumps(event) + ", {bubbles:true}))")
+
+	def set_prop(self, selector: str, name: str, value: object) -> None:
+		self.run(
+			"document.querySelector(" + json.dumps(selector) + ")[" + json.dumps(name) + "] = " + json.dumps(value)
+		)
+
+	def requests(self) -> list[dict[str, Any]]:
+		found = self.value("__requests")
+		return [] if not isinstance(found, list) else found
+
+	def prop(self, selector: str, name: str) -> Any:
+		return self.value("document.querySelector(" + json.dumps(selector) + ")[" + json.dumps(name) + "]")
diff --git a/pygitweb/static_tests/pages.py b/pygitweb/static_tests/pages.py
new file mode 100644
index 0000000..16459f1
--- /dev/null
+++ b/pygitweb/static_tests/pages.py
@@ -0,0 +1,114 @@
+"""HTML fixtures shared by the browser-script tests."""
+
+from __future__ import annotations
+
+import json
+from typing import Any
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+MANIFEST: dict[str, list[dict[str, str]]] = {
+	"commits": [
+		{
+			"insert": "abcd",
+			"name": "subject",
+			"url": "/c/abcd",
+			"tags": "abcd subject",
+			"snippet": "subject",
+			"created": "2020-01-01",
+		},
+		{"insert": "eeee", "name": "other", "url": "/c/eeee", "tags": "eeee"},
+	],
+	"refs": [
+		{"insert": "main", "name": "main", "url": "/r/main", "tags": "main branch"},
+		{"insert": "task-1234", "name": "task", "url": "/r/task", "tags": "task"},
+	],
+}
+
+
+def card_html(
+	*,
+	ref: str = "",
+	status: str = "TODO",
+	points: str = "",
+	title: str = "",
+	description: str = "",
+) -> str:
+	return (
+		f'<div class="card board-card" draggable="true" data-task-ref="{ref}" '
+		f'data-task-status="{status}" data-task-points="{points}">'
+		'<span class="board-card-icon" data-icon="cloud"></span>'
+		'<div class="board-card-title-line">'
+		'<span class="board-card-task-id"></span>'
+		f'<span class="board-card-title">{title}</span></div>'
+		'<div class="board-card-description-preview"></div>'
+		'<div class="board-card-meta">'
+		'<span class="badge">LOW</span>'
+		'<span class="board-card-points-badge d-none"></span>'
+		'<span class="board-card-assignee-compact">None</span>'
+		'<span class="ms-auto board-card-comments-count-meta">0 comments</span>'
+		"</div>"
+		'<div class="board-card-details d-none">'
+		'<div class="board-card-task-ref"></div>'
+		f'<div class="board-card-description-full">{description}</div>'
+		'<div class="board-card-due-date">None</div>'
+		'<div class="board-card-updated-at">None</div>'
+		'<div class="board-card-status">TODO</div>'
+		'<div class="board-card-priority">LOW</div>'
+		'<div class="board-card-points">–</div>'
+		'<div class="board-card-assignee">None</div>'
+		'<div class="board-card-tags"></div>'
+		'<select class="board-card-move-select"><option value="">Move to board…</option></select>'
+		'<button type="button" class="board-card-delete-btn">Delete</button>'
+		'<button type="button" class="board-card-archive-btn">Archive</button>'
+		'<form class="board-card-comment-form"><textarea class="board-card-comment-input"></textarea></form>'
+		'<div class="board-card-comment-signin-hint d-none"></div>'
+		'<div class="board-card-comments-list"></div>'
+		"</div></div>"
+	)
+
+
+def column(status: str, cards: str = "") -> str:
+	return (
+		f'<div class="board-column dropzone" data-status="{status}">'
+		'<button type="button" class="board-column-add-card">'
+		'<span class="board-column-task-count">0</span></button>'
+		'<span class="board-column-points-total">0 pts</span>'
+		f'<div class="board-column-cards">{cards}</div></div>'
+	)
+
+
+def board_shell(
+	*,
+	project: str = "demo",
+	board: str = "Tasks",
+	cards_todo: str = "",
+	cards_progress: str = "",
+	include_template: bool = True,
+) -> str:
+	template = f'<template id="board-card-template">{card_html()}</template>' if include_template else ""
+	return (
+		template
+		+ f'<div class="board-columns" data-project="{project}" data-board="{board}">'
+		+ column("TODO", cards_todo)
+		+ column("IN_PROGRESS", cards_progress)
+		+ "</div>"
+		+ '<select id="board-board-select"></select>'
+		+ '<button type="button" id="board-new-board-btn">+ Board</button>'
+		+ '<button type="button" id="board-new-sprint-btn">+ Sprint</button>'
+		+ '<form id="board-new-form" class="d-none"><input id="board-new-name">'
+		+ '<span id="board-new-error" class="d-none"></span>'
+		+ '<button type="submit">Create</button></form>'
+		+ '<button type="button" id="board-new-cancel">Cancel</button>'
+	)
+
+
+def requested(js: JsPage, fragment: str) -> bool:
+	return any(fragment in str(item["url"]) for item in js.requests())
+
+
+def posted_json(js: JsPage, fragment: str) -> Any:
+	for item in reversed(js.requests()):
+		if fragment in str(item["url"]) and item.get("body"):
+			return json.loads(str(item["body"]))
+	return None
diff --git a/pygitweb/static_tests/test_addproject.py b/pygitweb/static_tests/test_addproject.py
new file mode 100644
index 0000000..d97dbd2
--- /dev/null
+++ b/pygitweb/static_tests/test_addproject.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import requested
+
+_FORM = (
+	'<input id="project_name">'
+	'<input type="radio" name="project_source_type" value="empty" checked>'
+	'<input type="radio" name="project_source_type" value="zip">'
+	'<div id="tab-empty-repo"></div>'
+	'<div id="tab-upload-zip" class="d-none"></div>'
+	'<input id="pull_from_remote" value="git://example">'
+	'<input id="repo_zip" value="old.zip">'
+	'<div id="dropzone">'
+	"<span data-dropzone-placeholder>drop</span>"
+	'<span data-dropzone-filename class="d-none"></span>'
+	"</div>"
+)
+
+
+def test_empty_document_skips_every_widget(js: JsPage) -> None:
+	js.set_html("<div></div>")
+	js.load("addproject.js")
+	assert js.count("#project_name") == 0
+
+
+def test_blank_name_clears_the_border(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.dispatch("#project_name", "blur")
+	assert "border" not in js.classes("#project_name")
+	assert requested(js, "projectnamevalid") is False
+
+
+def test_blur_marks_an_accepted_name(js: JsPage) -> None:
+	js.route("projectnamevalid", status=200)
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop("#project_name", "value", " demo ")
+	js.dispatch("#project_name", "blur")
+	assert "border-success" in js.classes("#project_name")
+	assert any("name=demo" in str(item["url"]) for item in js.requests())
+
+
+def test_blur_marks_a_rejected_name(js: JsPage) -> None:
+	js.route("projectnamevalid", status=404)
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop("#project_name", "value", "taken")
+	js.dispatch("#project_name", "blur")
+	assert "border-danger" in js.classes("#project_name")
+
+
+def test_name_check_clears_the_border_when_fetch_fails(js: JsPage) -> None:
+	js.route("projectnamevalid", error="offline")
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop("#project_name", "value", "demo")
+	js.dispatch("#project_name", "blur")
+	assert "border" not in js.classes("#project_name")
+
+
+def test_input_checks_the_name_after_the_debounce(js: JsPage) -> None:
+	js.route("projectnamevalid", status=200)
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop("#project_name", "value", "demo")
+	js.dispatch("#project_name", "input")
+	assert requested(js, "projectnamevalid") is False
+	js.tick()
+	assert requested(js, "projectnamevalid") is True
+
+
+def test_clearing_the_name_cancels_the_debounce(js: JsPage) -> None:
+	js.route("projectnamevalid", status=200)
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop("#project_name", "value", "demo")
+	js.dispatch("#project_name", "input")
+	js.set_prop("#project_name", "value", "  ")
+	js.dispatch("#project_name", "input")
+	js.tick()
+	assert requested(js, "projectnamevalid") is False
+	assert "border" not in js.classes("#project_name")
+
+
+def test_zip_source_hides_the_empty_tab_and_clears_the_remote(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	assert "d-none" not in js.classes("#tab-empty-repo")
+	js.set_prop('input[value="empty"]', "checked", False)
+	js.set_prop('input[value="zip"]', "checked", True)
+	js.dispatch('input[value="zip"]', "change")
+	assert "d-none" in js.classes("#tab-empty-repo")
+	assert "d-none" not in js.classes("#tab-upload-zip")
+	assert js.prop("#pull_from_remote", "value") == ""
+
+
+def test_empty_source_clears_the_zip_input(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.set_prop('input[value="empty"]', "checked", False)
+	js.set_prop('input[value="zip"]', "checked", True)
+	js.dispatch('input[value="zip"]', "change")
+	js.set_prop('input[value="zip"]', "checked", False)
+	js.set_prop('input[value="empty"]', "checked", True)
+	js.dispatch('input[value="empty"]', "change")
+	assert js.prop("#repo_zip", "value") == ""
+	assert "d-none" in js.classes("#tab-upload-zip")
+
+
+def test_unchecked_radios_default_to_the_empty_source(js: JsPage) -> None:
+	html = _FORM.replace(" checked", "", 1)
+	js.set_html(html)
+	js.load("addproject.js")
+	assert "d-none" in js.classes("#tab-upload-zip")
+
+
+def test_dropzone_click_clicks_the_file_input(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.run(
+		"globalThis.__zipClicks = 0;"
+		" document.getElementById('repo_zip').addEventListener('click', function() { __zipClicks++; });"
+	)
+	js.click("#dropzone")
+	assert js.value("__zipClicks") == 1
+
+
+def test_drag_highlights_the_dropzone_until_leave_or_drop(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.dispatch("#dropzone", "dragover")
+	assert "border-primary" in js.classes("#dropzone")
+	js.dispatch("#dropzone", "dragleave")
+	assert "border-primary" not in js.classes("#dropzone")
+
+
+def test_drop_accepts_only_a_zip_file(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.run(
+		"var ev = new Event('drop', {bubbles:true, cancelable:true});"
+		"ev.dataTransfer.files = [{name: 'notes.txt'}];"
+		"document.getElementById('dropzone').dispatchEvent(ev);"
+	)
+	assert "Selected:" not in js.text("[data-dropzone-filename]")
+	js.run(
+		"var ev = new Event('drop', {bubbles:true, cancelable:true});"
+		"ev.dataTransfer.files = [{name: 'Pkg.ZIP'}];"
+		"document.getElementById('dropzone').dispatchEvent(ev);"
+	)
+	assert js.text("[data-dropzone-filename]") == "Selected: Pkg.ZIP"
+	assert "d-none" in js.classes("[data-dropzone-placeholder]")
+	assert "border-primary" not in js.classes("#dropzone")
+
+
+def test_file_input_change_shows_and_clears_the_name(js: JsPage) -> None:
+	js.set_html(_FORM)
+	js.load("addproject.js")
+	js.run("document.getElementById('repo_zip').files = [{name: 'a.zip'}];")
+	js.dispatch("#repo_zip", "change")
+	assert js.text("[data-dropzone-filename]") == "Selected: a.zip"
+	js.run("document.getElementById('repo_zip').files = [];")
+	js.dispatch("#repo_zip", "change")
+	assert js.text("[data-dropzone-filename]") == ""
+	assert "d-none" not in js.classes("[data-dropzone-placeholder]")
diff --git a/pygitweb/static_tests/test_blob-view.py b/pygitweb/static_tests/test_blob-view.py
new file mode 100644
index 0000000..939efc2
--- /dev/null
+++ b/pygitweb/static_tests/test_blob-view.py
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+
+def _blob(js: JsPage, code: str, lines: str | None) -> None:
+	href = "http://127.0.0.1/blob"
+	if lines is not None:
+		href += "?lines=" + lines
+	js.set_location(href)
+	js.set_html(f'<pre class="blob-content"><code>{code}</code></pre>')
+
+
+def test_missing_code_element_is_ignored(js: JsPage) -> None:
+	js.set_location("http://127.0.0.1/blob?lines=L1")
+	js.set_html("<div></div>")
+	js.load("blob-view.js")
+	assert js.count("[data-line]") == 0
+
+
+def test_no_lines_param_leaves_the_code_unwrapped(js: JsPage) -> None:
+	js.run("globalThis.hljs = { highlightElement: function(code) { code.setAttribute('data-hl', '1'); } };")
+	_blob(js, "print('hi')", None)
+	js.load("blob-view.js")
+	assert js.attr(".blob-content code", "data-hl") == "1"
+	assert js.count("[data-line]") == 0
+
+
+def test_a_full_line_range_selects_each_line_and_scrolls(js: JsPage) -> None:
+	_blob(js, "alpha\nbeta\ngamma", "L1-L2")
+	js.load("blob-view.js")
+	assert "blob-line-selected" in js.classes('[data-line="1"]')
+	assert "blob-line-selected" in js.classes('[data-line="2"]')
+	assert "blob-line-selected" not in js.classes('[data-line="3"]')
+	assert js.value("__lastScrolled && __lastScrolled.tagName") == "SPAN"
+
+
+def test_a_character_range_wraps_a_selection_span(js: JsPage) -> None:
+	_blob(js, "print('hi')", "L1C2-L1C5")
+	js.load("blob-view.js")
+	assert js.text(".blob-selection") == "rint"
+	assert js.count(".blob-line-selected") == 0
+
+
+def test_a_zero_width_range_does_not_wrap(js: JsPage) -> None:
+	_blob(js, "print('hi')", "L1C5-L1C4")
+	js.load("blob-view.js")
+	assert js.count(".blob-selection") == 0
+	assert js.count(".blob-line-selected") == 0
+
+
+def test_a_missing_line_is_skipped(js: JsPage) -> None:
+	_blob(js, "only", "L9")
+	js.load("blob-view.js")
+	assert js.count(".blob-line-selected") == 0
+	assert js.value("__lastScrolled") is None
+
+
+def test_highlight_elements_are_kept_inside_line_spans(js: JsPage) -> None:
+	js.run(
+		"globalThis.hljs = { highlightElement: function(code) {"
+		" var text = code.textContent; code.textContent = '';"
+		" var span = document.createElement('span'); span.className = 'hljs-built_in';"
+		" span.textContent = text; code.appendChild(span); } };"
+	)
+	_blob(js, "print('hi')", "L1")
+	js.load("blob-view.js")
+	assert js.count('[data-line="1"] .hljs-built_in') == 1
+	assert "blob-line-selected" in js.classes('[data-line="1"]')
+
+
+def test_a_range_across_nodes_is_ignored_when_surround_fails(js: JsPage) -> None:
+	_blob(js, "ab<span>XX</span>cd", "L1C1-L1C6")
+	js.load("blob-view.js")
+	assert js.count(".blob-selection") == 0
+	assert js.count("[data-line]") == 1
+
+
+def test_several_ranges_in_one_param_are_applied(js: JsPage) -> None:
+	_blob(js, "one\ntwo", "L1,L2C1-L2C2")
+	js.load("blob-view.js")
+	assert "blob-line-selected" in js.classes('[data-line="1"]')
+	assert js.text(".blob-selection") == "tw"
diff --git a/pygitweb/static_tests/test_board-cards.py b/pygitweb/static_tests/test_board-cards.py
new file mode 100644
index 0000000..0b285e8
--- /dev/null
+++ b/pygitweb/static_tests/test_board-cards.py
@@ -0,0 +1,195 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import board_shell, card_html, posted_json, requested
+
+_TASKS = [
+	{
+		"ref": "refs/tags/tasks/task_abcd",
+		"id": "abcd",
+		"title": "Ship",
+		"description": "**bold**",
+		"status": "TODO",
+		"priority": "CRITICAL",
+		"points": 2,
+		"assignee": "Ada",
+		"comments_count": 1,
+		"due_date": "2020-01-02",
+		"updated_at": "2020-01-03",
+		"tags": "ui",
+	},
+	{
+		"ref": "refs/tags/tasks/task_eeee",
+		"title": "",
+		"status": "IN_PROGRESS",
+		"priority": "HIGH",
+		"points": 1,
+		"comments_count": 0,
+	},
+	{"ref": "refs/tags/tasks/task_ffff", "status": "DONE", "priority": "LOW", "points": 0},
+	{"status": "TODO", "title": "Zero", "points": 0, "priority": "LOW"},
+]
+
+
+def test_preload_and_assign_icons(js: JsPage) -> None:
+	js.set_html(board_shell(cards_todo=card_html(ref="t")))
+	js.load("board-cards.js")
+	js.call("board-cards.js", "preloadIcons")
+	preloaded = js.value("__preloaded")
+	assert len(preloaded) == 6
+	assert any(src.endswith("cloud.svg") for src in preloaded)
+	js.call("board-cards.js", "assignCloudIconToAllCards")
+	assert js.attr(".board-card-icon", "data-icon") == "cloud"
+
+
+def test_load_tasks_builds_cards_for_known_columns(js: JsPage) -> None:
+	js.route("tasks/list", body=_TASKS)
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.set_html(board_shell())
+	js.load("board-cards.js")
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert js.count('.board-column[data-status="TODO"] .board-card') == 2
+	assert js.count('.board-column[data-status="IN_PROGRESS"] .board-card') == 1
+	assert js.count('.board-column[data-status="DONE"] .board-card') == 0
+	assert js.text(".board-card-title") == "Ship"
+	assert "text-danger" in js.classes(".badge")
+	assert js.text(".board-card-points-badge") == "2 pts"
+	assert js.text(".board-card-assignee-compact") == "Ada"
+	assert js.text(".board-card-comments-count-meta") == "1 comment"
+	assert js.text(".board-card-due-date") == "2020-01-02"
+	assert js.text(".board-card-points") == "2"
+	assert js.text(".board-card-tags") == "ui"
+	assert js.count(".board-card-description-full strong") == 1
+	assert js.attr(".board-card-icon", "data-icon") == "cloud-filled"
+	assert js.text(".board-column-task-count") == "2"
+	assert js.text('.board-card[data-task-points="0"] .board-card-points-badge') == "0 pts"
+	assert "text-secondary" in js.classes('.board-card[data-task-points="0"] .badge')
+	assert js.text('.board-column[data-status="IN_PROGRESS"] .board-card-title') == "Untitled"
+	assert js.text('.board-column[data-status="IN_PROGRESS"] .board-card-points-badge') == "1 pt"
+	assert "text-warning" in js.classes('.board-column[data-status="IN_PROGRESS"] .badge')
+
+
+def test_load_tasks_keeps_cards_when_the_response_is_unusable(js: JsPage) -> None:
+	js.set_html('<div class="board-columns"></div>')
+	js.set_location("http://127.0.0.1/")
+	js.load("board-cards.js")
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert requested(js, "tasks/list") is False
+	js.set_location("http://127.0.0.1/project/demo/board/")
+	js.set_html(board_shell(cards_todo=card_html(ref="keep", title="Keep")))
+	js.route("tasks/list", status=500)
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert js.text(".board-card-title") == "Keep"
+	js.route("tasks/list", body={"nope": True})
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert js.text(".board-card-title") == "Keep"
+	js.route("tasks/list", error="offline")
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert js.text(".board-card-title") == "Keep"
+
+
+def test_load_tasks_without_a_template_clears_the_column(js: JsPage) -> None:
+	js.route("tasks/list", body=[{"ref": "t", "title": "X", "status": "TODO"}])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.set_html(board_shell(include_template=False, cards_todo=card_html(ref="old", title="Old")))
+	js.load("board-cards.js")
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	assert js.count(".board-card") == 0
+
+
+def test_fetch_and_reorder_follow_the_task_list(js: JsPage) -> None:
+	js.set_html('<div class="board-columns"></div>')
+	js.set_location("http://127.0.0.1/")
+	js.load("board-cards.js")
+	assert js.settle("__loaded['board-cards.js'].fetchBoardTasksJson()") is None
+	js.set_html(
+		board_shell(
+			cards_todo=card_html(ref="a", title="A") + card_html(ref="b", title="B"),
+			cards_progress=card_html(ref="c", title="C"),
+		)
+	)
+	js.route("tasks/list", status=404)
+	assert js.settle("__loaded['board-cards.js'].fetchBoardTasksJson()") is None
+	js.route(
+		"tasks/list",
+		body=[{"ref": "b", "status": "TODO"}, {"ref": "a", "status": "TODO"}, {"ref": "c", "status": "IN_PROGRESS"}],
+	)
+	assert js.settle("__loaded['board-cards.js'].fetchBoardTasksJson()")[0]["ref"] == "b"
+	js.run("__loaded['board-cards.js'].reorderBoardDomToTaskList(null)")
+	js.run(
+		"__loaded['board-cards.js'].reorderBoardDomToTaskList(["
+		"{ref:'b', status:'TODO'}, {status:'TODO'}, {ref:'missing', status:'TODO'}, {ref:'a', status:'IN_PROGRESS'}"
+		"])"
+	)
+	titles = js.texts('.board-column[data-status="TODO"] .board-card-title')
+	assert titles == ["B"]
+	progress = js.texts('.board-column[data-status="IN_PROGRESS"] .board-card-title')
+	assert progress == ["C", "A"]
+
+
+def test_add_card_posts_and_marks_success_or_failure(js: JsPage) -> None:
+	js.route("tasks/create", body={"ref": "refs/tags/tasks/task_abcd"})
+	js.set_html(board_shell())
+	js.load("board-cards.js")
+	js.call("board-cards.js", "bindAddCardButtons")
+	js.click(".board-column-task-count")
+	assert js.count(".board-card") == 0
+	js.click(".board-column-add-card")
+	assert js.text(".board-card-title") == "New task"
+	assert js.attr(".board-card-icon", "data-icon") == "cloud-filled"
+	assert js.text(".board-card-task-id") == "abcd"
+	assert js.text(".board-column-task-count") == "1"
+	body_url = next(str(item["url"]) for item in js.requests() if "tasks/create" in str(item["url"]))
+	assert "title=New+task" in body_url or "title=New%20task" in body_url
+	assert "status=TODO" in body_url
+	js.route("tasks/create", status=500)
+	js.click('.board-column[data-status="IN_PROGRESS"] .board-column-add-card')
+	assert js.attr('.board-column[data-status="IN_PROGRESS"] .board-card-icon', "data-icon") == "cloud-exclamation"
+	js.route("tasks/create", status=302)
+	js.click(".board-column-add-card")
+	pending = js.attrs('.board-column[data-status="TODO"] .board-card-icon', "data-icon")
+	assert pending[-1] == "cloud-up"
+	js.route("tasks/create", error="offline")
+	js.click(".board-column-add-card")
+	icons = js.attrs('.board-column[data-status="TODO"] .board-card-icon', "data-icon")
+	assert icons[-1] == "cloud-exclamation"
+
+
+def test_add_card_does_nothing_without_a_project_or_template(js: JsPage) -> None:
+	js.set_html(board_shell(project="", include_template=True))
+	js.set_location("http://127.0.0.1/")
+	js.load("board-cards.js")
+	js.call("board-cards.js", "bindAddCardButtons")
+	js.click(".board-column-add-card")
+	assert js.count(".board-card") == 0
+	js.set_html(board_shell(include_template=False))
+	js.call("board-cards.js", "bindAddCardButtons")
+	js.click(".board-column-add-card")
+	assert js.count(".board-card") == 0
+
+
+def test_compact_assignee_edit_commits_or_restores(js: JsPage) -> None:
+	js.route("tasks/list", body=[{"ref": "t1", "title": "Ship", "status": "TODO", "assignee": "Ada"}])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.route("tasks/update", status=200)
+	js.set_html(board_shell())
+	js.load("board-cards.js")
+	js.settle("__loaded['board-cards.js'].loadTasksForBoard()")
+	js.click(".board-card-assignee-compact")
+	assert js.count(".board-card-assignee-compact-edit") == 1
+	js.dispatch(".board-card-assignee-compact-edit", "mousedown")
+	js.set_prop(".board-card-assignee-compact-edit", "value", "Bea")
+	js.dispatch(".board-card-assignee-compact-edit", "keydown", key="Enter")
+	assert js.text(".board-card-assignee-compact") == "Bea"
+	assert js.text(".board-card-assignee") == "Bea"
+	assert posted_json(js, "tasks/update")["assignee"] == "Bea"
+	assert js.prop(".board-card", "draggable") is True
+	js.click(".board-card-assignee-compact")
+	js.set_prop(".board-card-assignee-compact-edit", "value", "nope")
+	js.dispatch(".board-card-assignee-compact-edit", "keydown", key="Escape")
+	assert js.text(".board-card-assignee-compact") == "Bea"
+	js.click(".board-card-assignee-compact")
+	js.set_prop(".board-card-assignee-compact-edit", "value", "  ")
+	js.dispatch(".board-card-assignee-compact-edit", "blur")
+	assert js.text(".board-card-assignee-compact") == "None"
+	assert posted_json(js, "tasks/update")["assignee"] is None
diff --git a/pygitweb/static_tests/test_board-comments.py b/pygitweb/static_tests/test_board-comments.py
new file mode 100644
index 0000000..6f3d977
--- /dev/null
+++ b/pygitweb/static_tests/test_board-comments.py
@@ -0,0 +1,132 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import board_shell, card_html, requested
+
+
+def _card(js: JsPage) -> None:
+	js.set_html(board_shell(cards_todo=card_html(ref="refs/tags/tasks/task_abcd", title="Ship")))
+	js.load("board-comments.js")
+
+
+def test_comment_labels_and_counts(js: JsPage) -> None:
+	_card(js)
+	assert js.call("board-comments.js", "commentCountLabel", 0) == "0 comments"
+	assert js.call("board-comments.js", "commentCountLabel", 1) == "1 comment"
+	assert js.call("board-comments.js", "commentCountLabel", 2) == "2 comments"
+	js.run("__loaded['board-comments.js'].updateCardCommentCount(document.querySelector('.board-card'), 1)")
+	assert js.text(".board-card-comments-count-meta") == "1 comment"
+	js.set_html("<div class='board-card'></div>")
+	js.run("__loaded['board-comments.js'].updateCardCommentCount(document.querySelector('.board-card'), 3)")
+	assert js.count(".board-card-comments-count-meta") == 0
+
+
+def test_comments_render_avatars_markdown_and_initials(js: JsPage) -> None:
+	js.route(
+		"comments/list",
+		body=[
+			{"author": "Ada Lovelace", "content": "**hi**", "gravatar_url": "http://img/a"},
+			{"author": "sam", "content": "yo"},
+			{"content": ""},
+		],
+	)
+	_card(js)
+	comments = js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))")
+	assert len(comments) == 3
+	assert js.text(".board-card-comments-count-meta") == "3 comments"
+	assert js.count(".board-card-comment-item") == 3
+	assert js.count("#md strong") == 0
+	assert js.count(".board-card-comment-content strong") == 1
+	assert js.prop(".board-card-comment-avatar img", "src") == "http://img/a"
+	js.run("document.querySelector('.board-card-comment-avatar img').onerror()")
+	assert js.text(".board-card-comment-avatar-fallback") == "AL"
+	assert js.text(".board-card-comment-author") == "Ada Lovelace"
+	fallbacks = js.texts(".board-card-comment-avatar-fallback")
+	assert fallbacks == ["AL", "S", "?"]
+
+
+def test_comment_load_tolerates_empty_and_failed_responses(js: JsPage) -> None:
+	_card(js)
+	js.route("comments/list", status=404)
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") == []
+	js.route("comments/list", status=500)
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") == []
+	js.route("comments/list", body={"nope": True})
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") == []
+	js.route("comments/list", error="offline")
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") == []
+	js.set_html('<div class="board-columns" data-project="demo"><div class="board-card"></div></div>')
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") is None
+	js.set_html('<div class="board-columns"><div class="board-card" data-task-ref="t"></div></div>')
+	js.set_location("http://127.0.0.1/")
+	assert js.settle("__loaded['board-comments.js'].loadCommentsForCard(document.querySelector('.board-card'))") is None
+
+
+def test_comment_form_hides_until_a_user_is_signed_in(js: JsPage) -> None:
+	js.route("/auth/status", body={"auth_enabled": True})
+	_card(js)
+	js.settle("__loaded['board-comments.js'].loadBoardAuthStatus()")
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	assert "d-none" in js.classes(".board-card-comment-form")
+	assert "d-none" not in js.classes(".board-card-comment-signin-hint")
+	js.route("/auth/status", body={"auth_enabled": True, "username": "ada"})
+	js.settle("__loaded['board-comments.js'].loadBoardAuthStatus()")
+	js.set_html(board_shell(cards_todo=card_html(ref="refs/tags/tasks/task_abcd")))
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	assert "d-none" not in js.classes(".board-card-comment-form")
+
+
+def test_auth_status_failure_leaves_comments_open(js: JsPage) -> None:
+	js.route("/auth/status", status=500)
+	_card(js)
+	js.settle("__loaded['board-comments.js'].loadBoardAuthStatus()")
+	js.route("/auth/status", error="offline")
+	js.settle("__loaded['board-comments.js'].loadBoardAuthStatus()")
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	assert "d-none" not in js.classes(".board-card-comment-form")
+
+
+def test_submitting_a_comment_posts_and_reloads(js: JsPage) -> None:
+	js.route("comments/create", status=200, text="ok")
+	js.route("comments/list", body=[{"author": "ada", "content": "hello"}])
+	_card(js)
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	js.dispatch(".board-card-comment-form", "submit")
+	assert requested(js, "comments/create") is False
+	js.set_prop(".board-card-comment-input", "value", "  hello  ")
+	js.dispatch(".board-card-comment-form", "submit")
+	assert any("content=hello" in str(item["url"]) for item in js.requests())
+	assert js.prop(".board-card-comment-input", "value") == ""
+	assert js.text(".board-card-comment-content") == "hello"
+
+
+def test_comment_post_errors_are_shown(js: JsPage) -> None:
+	_card(js)
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	js.set_prop(".board-card-comment-input", "value", "hello")
+	for status, snippet in ((401, "Not authorized"), (500, "Could not post comment")):
+		js.route("comments/create", status=status, text="no")
+		js.dispatch(".board-card-comment-form", "submit")
+		assert snippet in js.text(".board-task-error-hint")
+		js.tick()
+	js.route("comments/create", error="offline")
+	js.set_prop(".board-card-comment-input", "value", "hello")
+	js.dispatch(".board-card-comment-form", "submit")
+	assert "Could not post comment" in js.text(".board-task-error-hint")
+
+
+def test_comment_form_without_a_form_or_project_does_nothing(js: JsPage) -> None:
+	js.set_html('<div class="board-columns" data-project="demo"><div class="board-card" data-task-ref="t"></div></div>')
+	js.load("board-comments.js")
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	js.set_html(
+		'<div class="board-columns"><div class="board-card" data-task-ref="t">'
+		'<form class="board-card-comment-form"><textarea class="board-card-comment-input"></textarea></form>'
+		'<div class="board-card-comment-signin-hint d-none"></div></div></div>'
+	)
+	js.set_location("http://127.0.0.1/")
+	js.run("__loaded['board-comments.js'].setupCommentForm(document.querySelector('.board-card'))")
+	js.set_prop(".board-card-comment-input", "value", "hello")
+	js.dispatch(".board-card-comment-form", "submit")
+	assert requested(js, "comments/create") is False
diff --git a/pygitweb/static_tests/test_board-context.py b/pygitweb/static_tests/test_board-context.py
new file mode 100644
index 0000000..c198f1b
--- /dev/null
+++ b/pygitweb/static_tests/test_board-context.py
@@ -0,0 +1,143 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import MANIFEST, board_shell, card_html, posted_json, requested
+
+
+def _board(js: JsPage, html: str | None = None) -> None:
+	js.set_html(
+		html if html is not None else board_shell(cards_todo=card_html(ref="refs/tags/tasks/task_abcd", title="Ship"))
+	)
+	js.load("board-context.js")
+
+
+def test_task_ids_drop_the_prefix_and_shorten(js: JsPage) -> None:
+	_board(js, "<div></div>")
+	assert js.call("board-context.js", "taskDisplayId", "") == ""
+	assert js.call("board-context.js", "taskDisplayId", "refs/tags/tasks/task_abcd") == "abcd"
+	assert js.call("board-context.js", "taskDisplayId", "refs/tags/tasks/custom") == "custom"
+	assert js.call("board-context.js", "taskDisplayIdShort", "") == ""
+	assert js.call("board-context.js", "taskDisplayIdShort", "ab") == "ab"
+	assert js.call("board-context.js", "taskDisplayIdShort", "abcdef") == "cdef"
+
+
+def test_context_comes_from_the_dataset_or_the_url(js: JsPage) -> None:
+	_board(js)
+	assert js.call("board-context.js", "getBoardContext") == {"project": "demo", "board": "Tasks"}
+	js.set_html('<div class="board-columns"></div>')
+	js.set_location("http://127.0.0.1/project/demo/board/?b=Sprint")
+	js.load("board-context.js")
+	assert js.call("board-context.js", "getBoardContext") == {"project": "demo", "board": "Sprint"}
+	js.set_location("http://127.0.0.1/project/demo/board")
+	assert js.call("board-context.js", "getBoardContext")["board"] == "Tasks"
+
+
+def test_find_card_matches_the_task_ref(js: JsPage) -> None:
+	_board(js)
+	assert js.call("board-context.js", "findBoardCardByTaskRef", "") is None
+	assert js.value("__loaded['board-context.js'].findBoardCardByTaskRef('missing')") is None
+	assert js.value("__loaded['board-context.js'].findBoardCardByTaskRef('refs/tags/tasks/task_abcd') !== null") is True
+
+
+def test_title_tooltip_uses_the_id_and_falls_back_to_untitled(js: JsPage) -> None:
+	js.set_html('<div class="board-card"></div>')
+	js.load("board-context.js")
+	js.run("__loaded['board-context.js'].syncCardTitleLineTooltip(document.querySelector('.board-card'))")
+	js.set_html(board_shell(cards_todo=card_html(title="  ")))
+	js.run("__loaded['board-context.js'].syncCardTitleLineTooltip(document.querySelector('.board-card'))")
+	assert js.attr(".board-card-title-line", "title") == "Untitled"
+	js.set_html(board_shell(cards_todo=card_html(ref="refs/tags/tasks/task_abcd", title="Ship")))
+	js.run(
+		"__loaded['board-context.js'].setCardTaskRefDisplay("
+		"document.querySelector('.board-card'), 'refs/tags/tasks/task_abcd', null)"
+	)
+	assert js.text(".board-card-task-id") == "abcd"
+	assert js.text(".board-card-task-ref") == "refs/tags/tasks/task_abcd"
+	assert js.attr(".board-card-title-line", "title") == "abcd: Ship"
+
+
+def test_title_line_without_a_title_element_uses_untitled(js: JsPage) -> None:
+	js.set_html(
+		'<div class="board-card" data-task-ref="refs/tags/tasks/task_abcd">'
+		'<div class="board-card-title-line"></div>'
+		'<span class="board-card-task-id"></span>'
+		'<div class="board-card-task-ref">old</div></div>'
+	)
+	js.load("board-context.js")
+	js.run("__loaded['board-context.js'].syncCardTitleLineTooltip(document.querySelector('.board-card'))")
+	assert js.attr(".board-card-title-line", "title") == "abcd: Untitled"
+	js.run("__loaded['board-context.js'].setCardTaskRefDisplay(document.querySelector('.board-card'), '', 'abcd')")
+	assert js.text(".board-card-task-ref") == ""
+
+
+def test_error_hint_replaces_itself_and_expires(js: JsPage) -> None:
+	_board(js, "<div></div>")
+	js.call("board-context.js", "showBoardError", "nope")
+	assert js.count(".board-task-error-hint") == 0
+	_board(js)
+	js.call("board-context.js", "showBoardError", "first")
+	js.call("board-context.js", "showBoardError", "second")
+	assert js.count(".board-task-error-hint") == 1
+	assert js.text(".board-task-error-hint") == "second"
+	js.tick()
+	assert js.count(".board-task-error-hint") == 0
+
+
+def test_column_badge_sums_points(js: JsPage) -> None:
+	html = board_shell(
+		cards_todo=card_html(ref="a", points="2") + card_html(ref="b", points="3") + card_html(ref="c", points=""),
+	)
+	_board(js, html)
+	js.run("__loaded['board-context.js'].updateColumnBadge(null)")
+	js.run("__loaded['board-context.js'].updateColumnBadge(document.querySelector('.board-column'))")
+	assert js.text(".board-column-task-count") == "3"
+	assert js.text(".board-column-points-total") == "5 pts"
+	js.set_html('<div class="board-column"></div>')
+	js.load("board-context.js")
+	js.run("__loaded['board-context.js'].updateColumnBadge(document.querySelector('.board-column'))")
+	assert js.count(".board-column-task-count") == 0
+
+
+def test_field_update_posts_json_and_reports_failures(js: JsPage) -> None:
+	_board(js, "<div></div>")
+	assert js.settle("__loaded['board-context.js'].pushTaskFieldUpdate(null, {})") == {"ok": False, "status": 0}
+	_board(js)
+	card = "document.querySelector('.board-card')"
+	js.route("tasks/update", error="offline")
+	assert js.settle(f"__loaded['board-context.js'].pushTaskFieldUpdate({card}, {{a:1}})") == {
+		"ok": False,
+		"status": 0,
+	}
+	js.route("tasks/update", status=200)
+	assert js.settle(f"__loaded['board-context.js'].pushTaskFieldUpdate({card}, {{title:'Ship'}})") == {
+		"ok": True,
+		"status": 200,
+	}
+	assert posted_json(js, "tasks/update")["title"] == "Ship"
+	js.route("tasks/update", status=404)
+	assert js.settle(f"__loaded['board-context.js'].pushTaskFieldUpdate({card}, {{}})")["status"] == 404
+	js.route("tasks/update", error="offline")
+	assert js.settle(f"__loaded['board-context.js'].pushTaskFieldUpdate({card}, {{}})") == {"ok": False, "status": 0}
+	js.set_html('<div class="board-columns"><div class="board-card" data-task-ref="t"></div></div>')
+	js.load("board-context.js")
+	assert js.settle("__loaded['board-context.js'].pushTaskFieldUpdate(document.querySelector('.board-card'), {})") == {
+		"ok": False,
+		"status": 0,
+	}
+
+
+def test_refresh_stores_the_manifest_or_an_empty_one(js: JsPage) -> None:
+	_board(js, '<div class="board-columns"></div>')
+	js.set_location("http://127.0.0.1/")
+	assert js.settle("__loaded['board-context.js'].refreshBoardRefManifest()") is None
+	js.route("board-ref-manifest", body=MANIFEST)
+	_board(js)
+	got = js.settle("__loaded['board-context.js'].refreshBoardRefManifest()")
+	assert got["commits"][0]["insert"] == "abcd"
+	assert js.call("board-context.js", "getBoardRefManifest")["refs"][0]["insert"] == "main"
+	js.route("board-ref-manifest", status=500)
+	empty = js.settle("__loaded['board-context.js'].refreshBoardRefManifest()")
+	assert empty == {"commits": [], "refs": []}
+	js.route("board-ref-manifest", error="offline")
+	assert js.settle("__loaded['board-context.js'].refreshBoardRefManifest()") == {"commits": [], "refs": []}
+	assert requested(js, "board-ref-manifest") is True
diff --git a/pygitweb/static_tests/test_board-dnd.py b/pygitweb/static_tests/test_board-dnd.py
new file mode 100644
index 0000000..7753b5d
--- /dev/null
+++ b/pygitweb/static_tests/test_board-dnd.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import board_shell, card_html, requested
+
+
+def _bind(js: JsPage, html: str | None = None) -> None:
+	js.set_html(html if html is not None else board_shell(cards_todo=card_html(ref="t1", title="A", points="1")))
+	js.load("board-dnd.js")
+	js.call("board-dnd.js", "bindBoardInteractions")
+
+
+def test_binding_without_a_board_does_nothing(js: JsPage) -> None:
+	js.set_html("<div></div>")
+	js.load("board-dnd.js")
+	js.call("board-dnd.js", "bindBoardInteractions")
+	assert requested(js, "tasks/update") is False
+
+
+def test_drop_on_another_column_saves_and_reorders(js: JsPage) -> None:
+	js.route("tasks/update", status=200)
+	js.route("tasks/list", body=[{"ref": "t1", "status": "IN_PROGRESS", "title": "A"}])
+	_bind(js)
+	js.dispatch(".board-card", "dragstart")
+	assert "opacity-50" in js.classes(".board-card")
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "dragover")
+	assert "border-primary" in js.classes('.board-column[data-status="IN_PROGRESS"]')
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	assert js.count('.board-column[data-status="IN_PROGRESS"] .board-card') == 1
+	assert js.attr(".board-card", "data-task-status") == "IN_PROGRESS"
+	assert requested(js, "tasks/update") is True
+	js.dispatch(".board-card", "dragend")
+	assert "opacity-50" not in js.classes(".board-card")
+	assert "border-primary" not in js.classes(".board-column")
+
+
+def test_same_column_drop_does_not_save(js: JsPage) -> None:
+	_bind(js)
+	js.dispatch(".board-card", "dragstart")
+	js.dispatch('.board-column[data-status="TODO"]', "drop")
+	assert js.count('.board-column[data-status="TODO"] .board-card') == 1
+	assert requested(js, "tasks/update") is False
+
+
+def test_a_forbidden_move_puts_the_card_back(js: JsPage) -> None:
+	js.route("tasks/update", status=403)
+	html = board_shell(
+		cards_todo=card_html(ref="t1", title="A") + card_html(ref="t2", title="B"),
+	)
+	_bind(js, html)
+	js.dispatch('.board-card[data-task-ref="t1"]', "dragstart")
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	assert js.count('.board-column[data-status="TODO"] .board-card') == 2
+	assert "Not authorized" in js.text(".board-task-error-hint")
+	js.route("tasks/update", status=500)
+	js.dispatch('.board-card[data-task-ref="t1"]', "dragstart")
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	assert "Could not save" in js.text(".board-task-error-hint")
+	assert js.attr('.board-card[data-task-ref="t1"]', "data-task-status") == "TODO"
+
+
+def test_a_card_without_a_ref_is_moved_back(js: JsPage) -> None:
+	_bind(js, board_shell(cards_todo=card_html(title="A")))
+	js.dispatch(".board-card", "dragstart")
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	assert js.count('.board-column[data-status="TODO"] .board-card') == 1
+	assert "Could not save" in js.text(".board-task-error-hint")
+
+
+def test_drag_data_failures_still_move_the_dragged_card(js: JsPage) -> None:
+	js.route("tasks/update", status=200)
+	js.route("tasks/list", body=[{"ref": "t1", "status": "IN_PROGRESS"}])
+	_bind(js)
+	js.run("__failSetData = true;")
+	js.dispatch(".board-card", "dragstart")
+	assert "opacity-50" in js.classes(".board-card")
+	js.run("__failGetData = true;")
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	assert js.attr(".board-card", "data-task-status") == "IN_PROGRESS"
+
+
+def test_click_opens_the_popout_unless_the_gesture_was_a_drag(js: JsPage) -> None:
+	_bind(js)
+	js.dispatch(".board-card", "dragstart")
+	js.click(".board-card")
+	assert js.count(".board-card-popout-overlay") == 0
+	js.dispatch(".board-card", "dragend")
+	js.tick()
+	js.click(".board-card")
+	assert js.count(".board-card-popout-overlay") == 1
+	js.set_html(board_shell(cards_todo=card_html(ref="t1", title="A")))
+	js.call("board-dnd.js", "bindBoardInteractions")
+	js.run("document.querySelector('.board-card').classList.add('board-card-popout')")
+	js.click(".board-card")
+	assert js.count(".board-card-popout-overlay") == 0
+
+
+def test_drop_without_a_card_or_a_list_is_ignored(js: JsPage) -> None:
+	html = (
+		'<div class="board-columns" data-project="demo" data-board="Tasks">'
+		'<div class="board-column dropzone" data-status="TODO"></div>'
+		'<div class="board-column dropzone" data-status="IN_PROGRESS"><div class="board-column-cards"></div></div>'
+		"</div>"
+	)
+	_bind(js, html)
+	js.dispatch('.board-column[data-status="IN_PROGRESS"]', "drop")
+	js.dispatch('.board-column[data-status="TODO"]', "dragstart")
+	js.dispatch('.board-column[data-status="TODO"]', "drop")
+	assert requested(js, "tasks/update") is False
+
+
+def test_dragover_outside_a_column_does_not_highlight(js: JsPage) -> None:
+	_bind(js)
+	js.dispatch(".board-column-cards", "dragover")
+	assert "border-primary" not in js.classes(".board-column")
diff --git a/pygitweb/static_tests/test_board-markdown.py b/pygitweb/static_tests/test_board-markdown.py
new file mode 100644
index 0000000..865796e
--- /dev/null
+++ b/pygitweb/static_tests/test_board-markdown.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+import json
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import MANIFEST, board_shell
+
+
+def test_set_markdown_renders_and_linkifies_after_the_manifest_loads(js: JsPage) -> None:
+	js.route("board-ref-manifest", body=MANIFEST)
+	js.set_html(board_shell() + '<div id="md"></div><textarea id="note"></textarea>')
+	js.load("board-markdown.js")
+	assert js.call("board-markdown.js", "getMarkdownFromElement", None) == ""
+	js.run("__loaded['board-markdown.js'].setMarkdownElement(null, 'x')")
+	js.run("__loaded['board-markdown.js'].linkifyMarkdownElement(null)")
+	js.run("__loaded['board-markdown.js'].attachBoardRefAutocomplete(null)")
+	source = json.dumps("see #abcd @missing")
+	js.run(f"__loaded['board-markdown.js'].setMarkdownElement(document.getElementById('md'), {source})")
+	assert js.count("#md .board-ref-link") == 0
+	assert js.value("document.getElementById('md').dataset.boardMarkdown") == "see #abcd @missing"
+	js.settle("__loaded['board-context.js'].refreshBoardRefManifest()")
+	js.run(f"__loaded['board-markdown.js'].setMarkdownElement(document.getElementById('md'), {source})")
+	assert js.count("#md .board-ref-commit") == 1
+	assert js.count("#md .board-ref-unresolved") == 1
+	assert js.count("#md strong") == 0
+	js.run("__loaded['board-markdown.js'].setMarkdownElement(document.getElementById('md'), '**bold**')")
+	assert js.count("#md strong") == 1
+	assert "markdown-body" in js.classes("#md")
+	js.run("__loaded['board-markdown.js'].attachBoardRefAutocomplete(document.getElementById('note'))")
+	assert js.attr("#note", "data-board-ref-autocomplete") == "1"
+
+
+def test_compact_preview_truncates_at_120_characters(js: JsPage) -> None:
+	long = "word " * 40
+	js.set_html(
+		'<div class="board-columns" data-project="demo" data-board="Tasks"></div>'
+		'<div class="board-card"><div class="board-card-description-preview"></div>'
+		'<div class="board-card-description-full"></div></div>'
+	)
+	js.load("board-markdown.js")
+	js.run(
+		"__loaded['board-markdown.js'].syncCompactDescription(document.querySelector('.board-card'), "
+		+ json.dumps(long)
+		+ ")"
+	)
+	preview = js.text(".board-card-description-preview")
+	assert preview.endswith("…")
+	assert len(preview) == 121
+	js.run("__loaded['board-markdown.js'].syncCompactDescription(document.querySelector('.board-card'), '')")
+	assert js.text(".board-card-description-preview") == ""
+
+
+def test_preview_reads_plain_text_when_markdown_was_not_stored(js: JsPage) -> None:
+	js.set_html(
+		'<div class="board-card"><div class="board-card-description-preview"></div>'
+		'<div class="board-card-description-full">plain</div></div>'
+	)
+	js.load("board-markdown.js")
+	text = js.value(
+		"__loaded['board-markdown.js'].getMarkdownFromElement(document.querySelector('.board-card-description-full'))"
+	)
+	assert text == "plain"
+	js.run("__loaded['board-markdown.js'].syncCompactDescription(document.querySelector('.board-card'))")
+	assert js.text(".board-card-description-preview") == "plain"
+
+
+def test_linkify_stops_when_the_board_has_no_project(js: JsPage) -> None:
+	js.route("board-ref-manifest", body=MANIFEST)
+	js.set_html(board_shell() + '<div id="md">see #abcd</div>')
+	js.load("board-markdown.js")
+	js.settle("__loaded['board-context.js'].refreshBoardRefManifest()")
+	js.set_html('<div class="board-columns"></div><div id="md">see #abcd</div>')
+	js.set_location("http://127.0.0.1/")
+	js.run("__loaded['board-markdown.js'].linkifyMarkdownElement(document.getElementById('md'))")
+	assert js.count(".board-ref-link") == 0
+	js.run("__loaded['board-markdown.js'].attachBoardRefAutocomplete(document.createElement('textarea'))")
diff --git a/pygitweb/static_tests/test_board-popout.py b/pygitweb/static_tests/test_board-popout.py
new file mode 100644
index 0000000..03983f4
--- /dev/null
+++ b/pygitweb/static_tests/test_board-popout.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import MANIFEST, board_shell, card_html, posted_json, requested
+
+
+def _open(js: JsPage, *, cards: str | None = None, project: str = "demo") -> None:
+	js.route("board/list", body=[{"name": "Tasks"}, {"name": "Other"}])
+	js.route("comments/list", body=[])
+	js.route("board-ref-manifest", body=MANIFEST)
+	js.route("tasks/list", body=[])
+	js.route("tasks/update", status=200)
+	html = board_shell(
+		project=project,
+		cards_todo=cards if cards is not None else card_html(ref="t1", title="Ship", description="see #abcd"),
+	)
+	js.set_html(html)
+	js.load("board-popout.js")
+	js.settle("__loaded['board-context.js'].refreshBoardRefManifest()")
+	js.run("__loaded['board-popout.js'].showCardPopout(document.querySelector('.board-card'))")
+
+
+def test_popout_edits_post_the_card_fields(js: JsPage) -> None:
+	_open(js)
+	assert "d-none" not in js.classes(".board-card-details")
+	assert js.count(".board-card-popout-overlay") == 1
+	assert js.count(".board-ref-commit") == 1
+	js.click(".board-card-title")
+	js.click(".board-card-title")
+	js.set_prop(".board-card-title-edit", "value", "  ")
+	js.dispatch(".board-card-title-edit", "keydown", key="Enter")
+	assert js.text(".board-card-title") == "Untitled"
+	js.click(".board-card-description-full")
+	js.set_prop(".board-card-description-edit", "value", "updated")
+	js.dispatch(".board-card-description-edit", "blur")
+	assert js.text(".board-card-description-preview") == "updated"
+	js.run("document.querySelector('.board-card-due-date').textContent = '2020-01-02 extra';")
+	js.click(".board-card-due-date")
+	assert js.prop(".board-card-due-date-edit", "value") == "2020-01-02"
+	js.set_prop(".board-card-due-date-edit", "value", "")
+	js.dispatch(".board-card-due-date-edit", "blur")
+	assert js.text(".board-card-due-date") == "None"
+	js.click(".board-card-status")
+	js.run("document.querySelector('.board-card-status-edit option').textContent = 'In Progress';")
+	js.dispatch(".board-card-status-edit", "change")
+	assert js.text(".board-card-status") == "In Progress"
+	assert js.attr(".board-card", "data-task-status") == "IN_PROGRESS"
+	js.click(".board-card-priority")
+	js.run("document.querySelector('.board-card-priority-edit option').textContent = 'High';")
+	js.dispatch(".board-card-priority-edit", "change")
+	assert js.text(".board-card-priority") == "High"
+	js.click(".board-card-points")
+	assert js.count(".board-card-points-edit option") == 9
+	js.set_prop(".board-card-points-edit", "value", "5")
+	js.dispatch(".board-card-points-edit", "change")
+	assert js.text(".board-card-points") == "5"
+	assert js.attr(".board-card", "data-task-points") == "5"
+	js.click(".board-card-assignee")
+	js.set_prop(".board-card-assignee-edit", "value", "Ada")
+	js.dispatch(".board-card-assignee-edit", "keydown", key="Enter")
+	assert js.text(".board-card-assignee-compact") == "Ada"
+	js.run("document.querySelector('.board-card-tags').textContent = 'ui';")
+	js.dispatch(".board-card-tags", "blur")
+	body = posted_json(js, "tasks/update")
+	assert body["title"] == "Untitled"
+	assert body["description"] == "updated"
+	assert body["status"] == "IN_PROGRESS"
+	assert body["priority"] == "HIGH"
+	assert body["points"] == 5
+	assert body["assignee"] == "Ada"
+	assert body["due_date"] is None
+	assert body["tags"] == "ui"
+
+
+def test_due_date_keeps_a_typed_value_and_rejects_junk(js: JsPage) -> None:
+	_open(js)
+	js.run("document.querySelector('.board-card-due-date').textContent = 'soon';")
+	js.click(".board-card-due-date")
+	assert js.prop(".board-card-due-date-edit", "value") == ""
+	js.set_prop(".board-card-due-date-edit", "value", "2024-05-06")
+	js.dispatch(".board-card-due-date-edit", "blur")
+	assert posted_json(js, "tasks/update")["due_date"] == "2024-05-06"
+
+
+def test_backdrop_restores_the_card_ahead_of_its_sibling(js: JsPage) -> None:
+	_open(js, cards=card_html(ref="t1", title="A") + card_html(ref="t2", title="B"))
+	assert js.count(".board-card-popout-overlay .board-card") == 1
+	js.click(".board-card-popout-overlay")
+	assert js.count(".board-card-popout-overlay") == 0
+	titles = js.texts(".board-column-cards .board-card-title")
+	assert titles == ["A", "B"]
+	assert "d-none" in js.classes(".board-card-details")
+	assert js.attr(".board-card", "draggable") == "true"
+
+
+def test_closing_the_last_card_appends_it(js: JsPage) -> None:
+	_open(js, cards=card_html(ref="t1", title="Only"))
+	js.click(".board-card-popout-overlay")
+	assert js.text(".board-column-cards .board-card-title") == "Only"
+
+
+def test_delete_and_archive_ask_before_removing_the_card(js: JsPage) -> None:
+	js.route("tasks/delete", status=200)
+	js.route("tasks/archive", status=200)
+	_open(js)
+	js.run("__confirmYes = false;")
+	js.click(".board-card-delete-btn")
+	assert requested(js, "tasks/delete") is False
+	assert "permanently" in js.value("__confirms")[0]
+	js.run("__confirmYes = true;")
+	js.click(".board-card-delete-btn")
+	assert js.count(".board-card") == 0
+	_open(js)
+	js.route("tasks/delete", status=500)
+	js.click(".board-card-delete-btn")
+	assert js.count(".board-card") == 1
+	js.click(".board-card-archive-btn")
+	assert js.count(".board-card") == 0
+	assert "Archive this task" in js.value("__confirms")[-1]
+
+
+def test_archive_failure_leaves_the_card(js: JsPage) -> None:
+	js.route("tasks/archive", error="offline")
+	_open(js)
+	js.click(".board-card-archive-btn")
+	assert js.count(".board-card") == 1
+
+
+def test_move_removes_the_card_or_shows_an_error(js: JsPage) -> None:
+	js.route("tasks/move", status=200)
+	_open(js)
+	labels = js.texts(".board-card-move-select option")
+	assert "Tasks" not in labels
+	assert "Other" in labels
+	js.dispatch(".board-card-move-select", "change")
+	assert requested(js, "tasks/move") is False
+	js.set_prop(".board-card-move-select", "value", "Other")
+	js.dispatch(".board-card-move-select", "change")
+	assert js.count(".board-card-popout-overlay") == 0
+	assert any("target=Other" in str(item["url"]) for item in js.requests())
+	js.route("tasks/move", status=401)
+	_open(js)
+	js.set_prop(".board-card-move-select", "value", "Other")
+	js.dispatch(".board-card-move-select", "change")
+	assert "Not authorized" in js.text(".board-task-error-hint")
+	assert js.prop(".board-card-move-select", "value") == ""
+	js.route("tasks/move", status=500)
+	js.set_prop(".board-card-move-select", "value", "Other")
+	js.dispatch(".board-card-move-select", "change")
+	assert "Could not move task" in js.text(".board-task-error-hint")
+	js.route("tasks/move", error="offline")
+	js.set_prop(".board-card-move-select", "value", "Other")
+	js.dispatch(".board-card-move-select", "change")
+	assert "Could not move task" in js.text(".board-task-error-hint")
+
+
+def test_actions_without_a_ref_or_project_are_not_bound(js: JsPage) -> None:
+	_open(js, cards=card_html(title="Draft"), project="")
+	js.set_location("http://127.0.0.1/")
+	js.click(".board-card-delete-btn")
+	assert js.value("__confirms") == []
+	assert requested(js, "tasks/delete") is False
+
+
+def test_assignee_cleared_to_none_posts_null(js: JsPage) -> None:
+	_open(js)
+	js.run("document.querySelector('.board-card-assignee').textContent = 'Ada';")
+	js.click(".board-card-assignee")
+	js.set_prop(".board-card-assignee-edit", "value", "")
+	js.dispatch(".board-card-assignee-edit", "blur")
+	assert js.text(".board-card-assignee") == "None"
+	assert posted_json(js, "tasks/update")["assignee"] is None
diff --git a/pygitweb/static_tests/test_board-refs.py b/pygitweb/static_tests/test_board-refs.py
new file mode 100644
index 0000000..46bbc28
--- /dev/null
+++ b/pygitweb/static_tests/test_board-refs.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import json
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import MANIFEST
+
+_ROOT = (
+	'<div id="root"><p>see #abcd and #abc and @main and @1234 and @missing and (#eeee)</p>'
+	"<pre><code>skip #abcd</code></pre><a href='/z'>keep @main</a></div>"
+	'<textarea id="note"></textarea>'
+)
+
+
+def _load(js: JsPage) -> None:
+	js.set_html(_ROOT)
+	js.run("__manifest = " + json.dumps(MANIFEST))
+	js.load("board-refs.js")
+
+
+def test_linkify_resolves_commits_refs_and_skips_code(js: JsPage) -> None:
+	_load(js)
+	js.run("__loaded['board-refs.js'].linkifyElement(null, __manifest, 'demo')")
+	js.run("__loaded['board-refs.js'].linkifyElement(document.getElementById('root'), __manifest, '')")
+	assert js.count(".board-ref-link") == 0
+	js.run("__loaded['board-refs.js'].linkifyElement(document.getElementById('root'), __manifest, 'group/demo')")
+	assert js.attr(".board-ref-commit", "href") == "/c/abcd"
+	assert js.attr(".board-ref-commit", "title") == "subject"
+	assert js.count(".board-ref-commit") >= 2
+	assert js.attr(".board-ref-tag", "href") == "/r/main"
+	assert js.text(".board-ref-unresolved") == "@missing"
+	assert js.attr(".board-ref-unresolved", "title") == "Unresolved reference"
+	assert "#abcd" in js.text("pre")
+	assert js.count("a[href='/z'] .board-ref-link") == 0
+
+
+def test_unresolved_commit_links_to_the_project(js: JsPage) -> None:
+	js.set_html("<div id='root'>#ffff</div>")
+	js.run("__manifest = " + json.dumps(MANIFEST))
+	js.load("board-refs.js")
+	js.run("__loaded['board-refs.js'].linkifyElement(document.getElementById('root'), __manifest, 'group/demo')")
+	href = js.attr(".board-ref-commit", "href") or ""
+	assert "/project/group/demo" in href
+	assert "h=ffff" in href
+	assert js.attr(".board-ref-commit", "title") is None
+
+
+def test_load_manifest_normalizes_payloads(js: JsPage) -> None:
+	js.load("board-refs.js")
+	js.route("board-ref-manifest", body=MANIFEST)
+	got = js.settle("__loaded['board-refs.js'].loadManifest('group/demo', 'Sprint')")
+	assert got["commits"][0]["insert"] == "abcd"
+	assert any("b=Sprint" in str(item["url"]) and "/project/group/demo/" in str(item["url"]) for item in js.requests())
+	js.route("board-ref-manifest", status=500, body={"commits": "nope", "refs": None})
+	assert js.settle("__loaded['board-refs.js'].loadManifest('demo', '')") == {"commits": [], "refs": []}
+	js.route("board-ref-manifest", body={"commits": "nope", "refs": {}})
+	normalized = js.settle("__loaded['board-refs.js'].loadManifest('demo', 'Tasks')")
+	assert normalized == {"commits": [], "refs": []}
+	js.route("board-ref-manifest", error="offline")
+	assert js.settle("__loaded['board-refs.js'].loadManifest('demo', 'Tasks')") == {"commits": [], "refs": []}
+
+
+def test_autocomplete_inserts_a_suggestion(js: JsPage) -> None:
+	_load(js)
+	js.run("__loaded['board-refs.js'].attachAutocomplete(null, __manifest)")
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.set_prop("#note", "value", "see @ma")
+	js.set_prop("#note", "selectionStart", 7)
+	js.dispatch("#note", "input")
+	assert "d-none" not in js.classes(".board-ref-suggest")
+	assert js.text(".board-ref-suggest-title") == "main"
+	js.dispatch(".board-ref-suggest-item", "mousedown")
+	assert js.prop("#note", "value") == "see @main"
+	assert "d-none" not in js.classes(".board-ref-suggest")
+
+
+def test_autocomplete_filters_commits_and_caps_the_list(js: JsPage) -> None:
+	commits = [
+		{"insert": f"{index:04x}", "name": f"name-{index}", "url": "/c", "tags": "t", "snippet": "s"}
+		for index in range(14)
+	]
+	js.set_html("<textarea id='note'></textarea>")
+	js.run("__manifest = " + json.dumps({"commits": commits, "refs": MANIFEST["refs"]}))
+	js.load("board-refs.js")
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.set_prop("#note", "value", "#")
+	js.set_prop("#note", "selectionStart", 1)
+	js.dispatch("#note", "keyup")
+	assert js.count(".board-ref-suggest-item") == 12
+	js.set_prop("#note", "value", "#1")
+	js.set_prop("#note", "selectionStart", 2)
+	js.dispatch("#note", "input")
+	assert js.count(".board-ref-suggest-item") >= 1
+	js.dispatch("#note", "keydown", key="Escape")
+	assert "d-none" in js.classes(".board-ref-suggest")
+
+
+def test_autocomplete_hides_on_blur_and_outside_click(js: JsPage) -> None:
+	_load(js)
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.set_prop("#note", "value", "@")
+	js.set_prop("#note", "selectionStart", 1)
+	js.dispatch("#note", "input")
+	assert js.count(".board-ref-suggest-item") == 2
+	js.dispatch("#note", "blur")
+	assert "d-none" not in js.classes(".board-ref-suggest")
+	js.tick()
+	assert "d-none" in js.classes(".board-ref-suggest")
+	js.set_prop("#note", "value", "@ma")
+	js.set_prop("#note", "selectionStart", 3)
+	js.dispatch("#note", "input")
+	js.dispatch("#root", "mousedown")
+	assert "d-none" in js.classes(".board-ref-suggest")
+
+
+def test_autocomplete_ignores_text_without_a_token(js: JsPage) -> None:
+	_load(js)
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.set_prop("#note", "value", "hello")
+	js.set_prop("#note", "selectionStart", 5)
+	js.dispatch("#note", "input")
+	assert js.count(".board-ref-suggest-item") == 0
+
+
+def test_suggestion_dates_and_snippets_are_escaped(js: JsPage) -> None:
+	manifest = {
+		"commits": [],
+		"refs": [{"insert": "main", "name": "<b>", "url": "/r", "tags": "main", "snippet": "<i>", "created": "<d>"}],
+	}
+	js.set_html("<textarea id='note'></textarea>")
+	js.run("__manifest = " + json.dumps(manifest))
+	js.load("board-refs.js")
+	js.run("__loaded['board-refs.js'].attachAutocomplete(document.getElementById('note'), __manifest)")
+	js.set_prop("#note", "value", "@")
+	js.set_prop("#note", "selectionStart", 1)
+	js.dispatch("#note", "input")
+	html = js.prop(".board-ref-suggest-item", "innerHTML")
+	assert "&lt;b&gt;" in html
+	assert "&lt;i&gt;" in html
+	assert "&lt;d&gt;" in html
+	assert "<b>" not in html
diff --git a/pygitweb/static_tests/test_board.py b/pygitweb/static_tests/test_board.py
new file mode 100644
index 0000000..790ed6e
--- /dev/null
+++ b/pygitweb/static_tests/test_board.py
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import board_shell, requested
+
+
+def _routes(js: JsPage) -> None:
+	js.route("board/list", body=[{"name": "Tasks", "task_count": 1}, {"name": "Sprint-2", "task_count": 0}])
+	js.route("tasks/list", body=[])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.route("/auth/status", body={"auth_enabled": False})
+
+
+def test_init_waits_for_dom_content_loaded(js: JsPage) -> None:
+	_routes(js)
+	js.set_html(board_shell())
+	js.run("document.readyState = 'loading';")
+	js.load("board.js")
+	assert js.value("__preloaded") == []
+	js.run("document.readyState = 'complete';")
+	js.run("document.dispatchEvent(new Event('DOMContentLoaded'))")
+	assert len(js.value("__preloaded")) == 6
+
+
+def test_selector_lists_boards_and_navigates(js: JsPage) -> None:
+	_routes(js)
+	js.set_html(board_shell())
+	js.load("board.js")
+	labels = js.value(
+		"Array.from(document.querySelectorAll('#board-board-select option')).map(function(el){return el.textContent;})"
+	)
+	assert labels == ["Tasks (1)", "Sprint-2 (0)"]
+	assert js.prop("#board-board-select", "value") == "Tasks"
+	js.dispatch("#board-board-select", "change")
+	assert js.value("__assigned") is None
+	js.set_prop("#board-board-select", "value", "Sprint-2")
+	js.dispatch("#board-board-select", "change")
+	assert js.value("__assigned") == "/project/demo/board/?b=Sprint-2"
+
+
+def test_unknown_current_board_is_inserted(js: JsPage) -> None:
+	_routes(js)
+	js.set_html(board_shell(board="Other"))
+	js.load("board.js")
+	assert js.prop("#board-board-select", "value") == "Other"
+	assert js.text("#board-board-select option") == "Other"
+
+
+def test_empty_or_failed_board_list_leaves_the_select(js: JsPage) -> None:
+	js.route("board/list", body=[])
+	js.route("tasks/list", body=[])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.route("/auth/status", body={"auth_enabled": False})
+	js.set_html(board_shell())
+	js.load("board.js")
+	assert js.count("#board-board-select option") == 0
+
+
+def test_failed_board_list_leaves_the_select(js: JsPage) -> None:
+	js.route("board/list", error="offline")
+	js.route("tasks/list", body=[])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.route("/auth/status", body={"auth_enabled": False})
+	js.set_html(board_shell())
+	js.load("board.js")
+	assert js.count("#board-board-select option") == 0
+
+
+def test_sprint_name_increments_and_the_form_can_be_cancelled(js: JsPage) -> None:
+	_routes(js)
+	js.route("board/list", body=[{"name": "Sprint-2"}, {"name": "notes"}, {"name": "Sprint-9"}])
+	js.set_html(board_shell())
+	js.load("board.js")
+	js.click("#board-new-sprint-btn")
+	assert "d-none" not in js.classes("#board-new-form")
+	assert js.prop("#board-new-name", "value") == "Sprint-10"
+	js.click("#board-new-cancel")
+	assert "d-none" in js.classes("#board-new-form")
+	js.click("#board-new-board-btn")
+	assert js.prop("#board-new-name", "value") == ""
+
+
+def test_create_board_navigates_or_shows_the_error(js: JsPage) -> None:
+	_routes(js)
+	js.route("board/create", status=200)
+	js.set_html(board_shell())
+	js.load("board.js")
+	js.click("#board-new-board-btn")
+	js.set_prop("#board-new-name", "value", "   ")
+	js.dispatch("#board-new-form", "submit")
+	assert requested(js, "board/create") is False
+	js.set_prop("#board-new-name", "value", "My Board")
+	js.dispatch("#board-new-form", "submit")
+	assert js.value("__assigned") == "/project/demo/board/?b=My-Board"
+	js.route("board/create", status=400, body={"detail": "taken"})
+	js.set_prop("#board-new-name", "value", "Tasks")
+	js.dispatch("#board-new-form", "submit")
+	assert js.text("#board-new-error") == "taken"
+	assert "d-none" not in js.classes("#board-new-error")
+	js.route("board/create", status=400, json_error=True)
+	js.dispatch("#board-new-form", "submit")
+	assert js.text("#board-new-error") == "Could not create board."
+	js.route("board/create", error="offline")
+	js.dispatch("#board-new-form", "submit")
+	assert js.text("#board-new-error") == "Could not create board."
+
+
+def test_open_task_from_the_query_string(js: JsPage) -> None:
+	_routes(js)
+	js.route(
+		"tasks/list",
+		body=[{"ref": "refs/tags/tasks/task_abcd", "title": "Ship", "status": "TODO"}],
+	)
+	js.set_location("http://127.0.0.1/project/demo/board/?task=refs/tags/tasks/task_abcd")
+	js.set_html(board_shell())
+	js.load("board.js")
+	assert js.count(".board-card-popout") == 0
+	js.tick()
+	assert "board-card-popout" in js.classes(".board-card")
+
+
+def test_open_task_skips_a_card_that_is_already_open(js: JsPage) -> None:
+	_routes(js)
+	js.route("tasks/list", body=[{"ref": "t1", "title": "Ship", "status": "TODO"}])
+	js.set_location("http://127.0.0.1/project/demo/board/?task=t1")
+	js.set_html(board_shell())
+	js.load("board.js")
+	js.run("document.querySelector('.board-card').classList.add('board-card-popout')")
+	js.tick()
+	assert js.count(".board-card-popout-overlay") == 0
+
+
+def test_missing_query_task_does_not_open_a_card(js: JsPage) -> None:
+	_routes(js)
+	js.set_html(board_shell())
+	js.load("board.js")
+	js.tick()
+	assert js.count(".board-card-popout-overlay") == 0
+
+
+def test_init_without_a_project_skips_the_network(js: JsPage) -> None:
+	js.set_html('<div class="board-columns"></div><select id="board-board-select"></select>')
+	js.set_location("http://127.0.0.1/")
+	js.load("board.js")
+	assert requested(js, "board/list") is False
+	assert requested(js, "tasks/list") is False
+
+
+def test_sprint_suggestion_starts_at_one_before_the_list_loads(js: JsPage) -> None:
+	js.route("tasks/list", body=[])
+	js.route("board-ref-manifest", body={"commits": [], "refs": []})
+	js.route("/auth/status", body={"auth_enabled": False})
+	js.set_html(board_shell())
+	js.load("board.js")
+	js.click("#board-new-sprint-btn")
+	assert js.prop("#board-new-name", "value") == "Sprint-1"
diff --git a/pygitweb/static_tests/test_diff-view.py b/pygitweb/static_tests/test_diff-view.py
new file mode 100644
index 0000000..fb4a396
--- /dev/null
+++ b/pygitweb/static_tests/test_diff-view.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import base64
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+_DIFF = base64.b64encode(b"diff --git a/a b/a\n").decode("ascii")
+_SHELL = (
+	'<div id="d2h-container" data-diff-b64="{b64}" data-has-diff="{has}" data-empty-text="{empty}"></div>'
+	'<div id="d2h-output"></div><div id="d2h-empty"></div>'
+)
+
+
+def _stub_diff2html(js: JsPage) -> None:
+	js.run(
+		"globalThis.Diff2HtmlUI = function(el, diff, config) {"
+		" this.el = el; this.diff = diff; this.config = config;"
+		" el.setAttribute('data-diff', diff);"
+		" el.setAttribute('data-format', config.outputFormat);"
+		" this.draw = function() { el.setAttribute('data-drawn', '1'); };"
+		" this.highlightCode = function() { el.setAttribute('data-hl', '1'); };"
+		"};"
+	)
+
+
+def test_render_does_nothing_when_nodes_are_missing(js: JsPage) -> None:
+	js.set_html("<div></div>")
+	js.load("diff-view.js")
+	assert js.count("#d2h-empty") == 0
+
+
+def test_render_shows_empty_text_when_there_is_no_diff(js: JsPage) -> None:
+	js.set_html(_SHELL.format(b64="", has="false", empty="No changes"))
+	js.load("diff-view.js")
+	assert js.text("#d2h-empty") == "No changes"
+	assert js.value("document.querySelector('#d2h-empty').style.display") == "block"
+
+
+def test_render_shows_empty_text_when_base64_is_missing(js: JsPage) -> None:
+	js.set_html(_SHELL.format(b64="", has="true", empty="Empty"))
+	js.load("diff-view.js")
+	assert js.text("#d2h-empty") == "Empty"
+
+
+def test_render_draws_a_decoded_diff(js: JsPage) -> None:
+	_stub_diff2html(js)
+	js.set_html(_SHELL.format(b64=_DIFF, has="true", empty=""))
+	js.load("diff-view.js")
+	assert js.attr("#d2h-output", "data-drawn") == "1"
+	assert js.attr("#d2h-output", "data-hl") == "1"
+	assert "diff --git" in (js.attr("#d2h-output", "data-diff") or "")
+	assert js.attr("#d2h-output", "data-format") == "side-by-side"
+
+
+def test_render_treats_an_empty_decode_as_no_diff(js: JsPage) -> None:
+	js.run("globalThis.atob = function() { return ''; };")
+	js.set_html(_SHELL.format(b64=_DIFF, has="true", empty="Blank"))
+	js.load("diff-view.js")
+	assert js.text("#d2h-empty") == "Blank"
+
+
+def test_render_uses_the_raw_string_without_textdecoder(js: JsPage) -> None:
+	_stub_diff2html(js)
+	js.run("globalThis.__td = globalThis.TextDecoder; delete globalThis.TextDecoder;")
+	js.set_html(_SHELL.format(b64=_DIFF, has="true", empty=""))
+	js.load("diff-view.js")
+	js.run("globalThis.TextDecoder = globalThis.__td;")
+	assert js.attr("#d2h-output", "data-drawn") == "1"
+
+
+def test_render_escapes_a_decoder_error(js: JsPage) -> None:
+	js.set_html(_SHELL.format(b64="!!!", has="true", empty=""))
+	js.load("diff-view.js")
+	html = js.prop("#d2h-output", "innerHTML")
+	assert "Failed to render diff" in html
+	assert "<script" not in html
+	assert "&lt;" in html or "Invalid" in html
diff --git a/pygitweb/static_tests/test_dom.py b/pygitweb/static_tests/test_dom.py
new file mode 100644
index 0000000..a83fa07
--- /dev/null
+++ b/pygitweb/static_tests/test_dom.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+
+def test_escape_html_replaces_special_characters(js: JsPage) -> None:
+	js.load("dom.js")
+	assert js.call("dom.js", "escapeHtml", """a&<>"'""") == "a&amp;&lt;&gt;&quot;&#39;"
+
+
+def test_escape_html_stringifies_non_strings(js: JsPage) -> None:
+	js.load("dom.js")
+	assert js.call("dom.js", "escapeHtml", None) == "null"
+	assert js.call("dom.js", "escapeHtml", 12) == "12"
diff --git a/pygitweb/static_tests/test_hook-install.py b/pygitweb/static_tests/test_hook-install.py
new file mode 100644
index 0000000..964d851
--- /dev/null
+++ b/pygitweb/static_tests/test_hook-install.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import requested
+
+_HOOKS = (
+	'<button class="hook-toggle" data-project="demo" data-sample="commit-msg" data-op="uninstall">x</button>'
+	'<button class="hook-install-btn" data-project="demo" data-op="install">install</button>'
+	'<select id="hook-install-select"><option value="pre-commit">pre</option></select>'
+	'<button id="plain">plain</button>'
+	'<button class="hook-toggle" id="incomplete">bad</button>'
+	'<button class="hook-install-btn" id="install-bare">bare</button>'
+)
+
+
+def test_click_outside_a_hook_control_is_ignored(js: JsPage) -> None:
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click("#plain")
+	assert js.value("__alerts") == []
+	assert requested(js, "/hook?") is False
+
+
+def test_incomplete_toggle_does_not_post(js: JsPage) -> None:
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click("#incomplete")
+	assert requested(js, "/hook?") is False
+	assert js.prop("#incomplete", "disabled") is False
+
+
+def test_uninstall_reloads_after_a_successful_post(js: JsPage) -> None:
+	js.route("/hook?", status=200, text="ok")
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click(".hook-toggle[data-sample]")
+	urls = [item["url"] for item in js.requests() if "/hook?" in str(item["url"])]
+	assert urls[0].endswith("name=commit-msg&op=uninstall")
+	assert js.value("__reloaded") is True
+	assert js.prop(".hook-toggle[data-sample]", "disabled") is True
+
+
+def test_uninstall_alerts_and_reenables_when_the_post_fails(js: JsPage) -> None:
+	js.route("/hook?", status=500, text="nope")
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click(".hook-toggle[data-sample]")
+	alerts = js.value("__alerts")
+	assert "uninstall failed (500)" in alerts[0]
+	assert "nope" in alerts[0]
+	assert js.prop(".hook-toggle[data-sample]", "disabled") is False
+	assert js.value("__reloaded") is False
+
+
+def test_uninstall_alerts_when_fetch_rejects(js: JsPage) -> None:
+	js.route("/hook?", error="offline")
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click(".hook-toggle[data-sample]")
+	assert "offline" in js.value("__alerts")[0]
+	assert js.prop(".hook-toggle[data-sample]", "disabled") is False
+
+
+def test_install_posts_the_selected_sample(js: JsPage) -> None:
+	js.route("/hook?", status=200, text="ok")
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click(".hook-install-btn[data-project]")
+	urls = [item["url"] for item in js.requests()]
+	assert any("name=pre-commit&op=install" in url for url in urls)
+	assert js.value("__reloaded") is True
+
+
+def test_install_without_a_select_does_not_post(js: JsPage) -> None:
+	html = _HOOKS.replace(
+		'<select id="hook-install-select"><option value="pre-commit">pre</option></select>',
+		'<div id="hook-install-select"></div>',
+	)
+	js.set_html(html)
+	js.load("hook-install.js")
+	js.click(".hook-install-btn[data-project]")
+	assert requested(js, "/hook?") is False
+
+
+def test_install_without_a_project_does_not_post(js: JsPage) -> None:
+	js.set_html(_HOOKS)
+	js.load("hook-install.js")
+	js.click("#install-bare")
+	assert requested(js, "/hook?") is False
diff --git a/pygitweb/static_tests/test_navbar-auth.py b/pygitweb/static_tests/test_navbar-auth.py
new file mode 100644
index 0000000..17a99d2
--- /dev/null
+++ b/pygitweb/static_tests/test_navbar-auth.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+_NAV = (
+	'<div id="navbar-user">'
+	'<a id="navbar-user-link" href="/login"></a>'
+	'<span id="navbar-user-avatar" class="bg-secondary"></span>'
+	'<div class="navbar-user-name"></div>'
+	'<div class="navbar-user-meta"></div>'
+	"</div>"
+)
+
+
+def test_missing_navbar_returns_immediately(js: JsPage) -> None:
+	js.set_html("<div></div>")
+	js.load("navbar-auth.js")
+	assert js.count("#navbar-user") == 0
+
+
+def test_missing_child_nodes_return_immediately(js: JsPage) -> None:
+	js.set_html('<div id="navbar-user"></div>')
+	js.load("navbar-auth.js")
+	assert js.count(".navbar-user-name") == 0
+
+
+def test_disabled_auth_renders_a_guest_who_cannot_sign_in(js: JsPage) -> None:
+	js.route("/auth/status", body={"auth_enabled": False})
+	js.set_html(_NAV)
+	js.load("navbar-auth.js")
+	assert js.text(".navbar-user-name") == "Guest"
+	assert js.text(".navbar-user-meta") == "Sign-in not used"
+	assert js.attr("#navbar-user-link", "href") is None
+	assert js.attr("#navbar-user-link", "aria-disabled") == "true"
+	assert "pe-none" in js.classes("#navbar-user-link")
+
+
+def test_admin_without_gravatar_uses_the_admin_avatar(js: JsPage) -> None:
+	js.route("/auth/status", body={"auth_enabled": True, "username": "Admin"})
+	js.set_html(_NAV)
+	js.load("navbar-auth.js")
+	assert js.attr("#navbar-user", "data-logged-in") == "true"
+	assert js.prop("#navbar-user-link", "href").endswith("/user")
+	assert js.text(".navbar-user-name") == "Admin"
+	assert js.text(".navbar-user-meta") == "Signed in"
+	assert "bg-primary" in js.classes("#navbar-user-avatar")
+	assert "svg" in js.prop("#navbar-user-avatar", "innerHTML")
+
+
+def test_user_gravatar_falls_back_when_the_image_errors(js: JsPage) -> None:
+	js.route(
+		"/auth/status",
+		body={"auth_enabled": True, "username": "ada", "gravatar_url": "http://img/a"},
+	)
+	js.set_html(_NAV)
+	js.load("navbar-auth.js")
+	assert js.prop("#navbar-user-avatar img", "src") == "http://img/a"
+	js.run("document.querySelector('#navbar-user-avatar img').onerror()")
+	assert js.count("#navbar-user-avatar img") == 0
+	assert "bg-teal" in js.classes("#navbar-user-avatar")
+	assert "bg-primary" not in js.classes("#navbar-user-avatar")
+
+
+def test_signed_out_visitor_links_to_login(js: JsPage) -> None:
+	js.route("/auth/status", body={"auth_enabled": True, "username": ""})
+	js.set_html(_NAV)
+	js.load("navbar-auth.js")
+	assert js.attr("#navbar-user", "data-logged-in") == "false"
+	assert js.prop("#navbar-user-link", "href").endswith("/login")
+	assert js.text(".navbar-user-meta") == "Not signed in"
+	assert "bg-secondary" in js.classes("#navbar-user-avatar")
+
+
+def test_status_fetch_failure_sends_the_link_to_login(js: JsPage) -> None:
+	js.route("/auth/status", error="offline")
+	js.set_html(_NAV)
+	js.load("navbar-auth.js")
+	assert js.prop("#navbar-user-link", "href").endswith("/login")
+	assert "bg-secondary" in js.classes("#navbar-user-avatar")
diff --git a/pygitweb/static_tests/test_readme-render.py b/pygitweb/static_tests/test_readme-render.py
new file mode 100644
index 0000000..489c0cb
--- /dev/null
+++ b/pygitweb/static_tests/test_readme-render.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import base64
+
+from pygitweb.static_tests.js_runtime import JsPage
+
+_README = "# Demo\n\nHello **world**.\n\n[file](./a.md)\n\n[abs](https://example.com)\n\n[root](/x)\n\n[hash](#z)\n"
+_B64 = base64.b64encode(_README.encode()).decode("ascii")
+
+
+def _page(b64: str, *, blob_base: str | None = "/blob?f=", blob_dir: str = "docs") -> str:
+	attrs = f'id="readme-container" data-readme-b64="{b64}"'
+	if blob_base is not None:
+		attrs += f' data-blob-base="{blob_base}" data-blob-dir="{blob_dir}"'
+	return f'<div class="page"></div><div {attrs}></div>'
+
+
+def test_missing_container_is_ignored(js: JsPage) -> None:
+	js.set_html('<div class="page"></div>')
+	js.load("readme-render.js")
+	assert js.count(".readme-toc") == 0
+
+
+def test_empty_payload_is_ignored(js: JsPage) -> None:
+	js.set_html('<div class="page"></div><div id="readme-container"></div>')
+	js.load("readme-render.js")
+	assert js.count("h1") == 0
+
+
+def test_render_builds_headings_links_and_a_hidden_toc(js: JsPage) -> None:
+	js.set_location("http://127.0.0.1/readme")
+	js.set_html(_page(_B64))
+	js.load("readme-render.js")
+	assert js.text("h1") == "Demo"
+	assert js.count("strong") == 1
+	file_href = js.attr('a[href*="blob"]', "href") or ""
+	assert "docs%2Fa.md" in file_href
+	assert js.attr('a[href="https://example.com"]', "href") == "https://example.com"
+	assert js.attr('a[href="/x"]', "href") == "/x"
+	assert js.count(".readme-section-anchor") == 1
+	assert js.prop(".readme-toc-mount", "hidden") is True
+	assert "readme-toc-active" in js.classes(".readme-toc-item")
+
+
+def test_relative_links_stay_put_without_a_blob_base(js: JsPage) -> None:
+	js.set_html(_page(_B64, blob_base=None))
+	js.load("readme-render.js")
+	assert js.attr('a[href="./a.md"]', "href") == "./a.md"
+
+
+def test_invalid_base64_is_swallowed(js: JsPage) -> None:
+	js.set_html(_page("!!!"))
+	js.load("readme-render.js")
+	assert js.count("h1") == 0
+
+
+def test_section_anchor_copies_the_url(js: JsPage) -> None:
+	js.set_location("http://127.0.0.1/readme?x=1")
+	js.set_html(_page(_B64))
+	js.load("readme-render.js")
+	js.click(".readme-section-anchor")
+	assert js.value("__copied") == "http://127.0.0.1/readme?x=1#demo"
+
+
+def test_section_anchor_sets_the_hash_without_clipboard(js: JsPage) -> None:
+	js.set_location("http://127.0.0.1/readme")
+	js.set_html(_page(_B64))
+	js.load("readme-render.js")
+	js.run("navigator.clipboard = null;")
+	js.click(".readme-section-anchor")
+	assert js.value("location.hash") == "#demo"
+
+
+def test_toc_link_scrolls_and_updates_the_hash(js: JsPage) -> None:
+	js.set_location("http://127.0.0.1/readme")
+	js.set_html(_page(_B64))
+	js.load("readme-render.js")
+	js.click(".readme-toc-link")
+	assert js.value("location.hash") == "#demo"
+	assert js.value("__lastScrolled && __lastScrolled.id") == "demo"
+
+
+def test_scroll_spy_shows_the_toc_and_marks_the_visible_heading(js: JsPage) -> None:
+	js.set_html(_page(_B64))
+	js.load("readme-render.js")
+	js.run("document.getElementById('readme-container')._rectTop = -40;")
+	js.run("document.querySelector('h1')._rectTop = -40;")
+	js.run("window.scrollY = 0;")
+	js.dispatch_window("scroll")
+	js.dispatch_window("scroll")
+	js.dispatch_window("resize")
+	js.tick()
+	assert js.prop(".readme-toc-mount", "hidden") is False
+	assert "readme-toc-active" in js.classes(".readme-toc-item")
+
+
+def test_generated_heading_ids_are_unique_and_protocol_relative_links_stay(js: JsPage) -> None:
+	js.run(
+		"var __real = showdown.Converter;"
+		"showdown.Converter = function() { return { makeHtml: function() {"
+		" return '<h1>Demo</h1><h1>Demo</h1><h2>!!!</h2><p>Hi</p>"
+		'<a href="./a.md">f</a><a href="//cdn.example/a">c</a>\'; } }; };'
+	)
+	js.set_html(_page(_B64, blob_dir=""))
+	js.load("readme-render.js")
+	js.run("showdown.Converter = __real;")
+	assert js.attr("h1", "id") == "demo"
+	assert js.count("#demo-1") == 1
+	assert js.count("#section") == 1
+	assert "a.md" in (js.attr('a[href*="a.md"]', "href") or "")
+	assert js.attr('a[href="//cdn.example/a"]', "href") == "//cdn.example/a"
+	assert js.count(".page .readme-toc") == 1
+
+
+def test_toc_is_not_mounted_without_a_page_column(js: JsPage) -> None:
+	js.set_html(f'<div id="readme-container" data-readme-b64="{_B64}" data-blob-base="/b?f="></div>')
+	js.load("readme-render.js")
+	assert js.count("h1") == 1
+	assert js.count(".readme-toc-mount") == 0
diff --git a/pygitweb/static_tests/test_search-page.py b/pygitweb/static_tests/test_search-page.py
new file mode 100644
index 0000000..c7d28db
--- /dev/null
+++ b/pygitweb/static_tests/test_search-page.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+import pytest
+
+from pygitweb.static_tests.js_runtime import JsError, JsPage
+
+_FORM = (
+	'<input id="search-patterns">'
+	'<input id="search-paths">'
+	'<input id="search-globs">'
+	'<select id="search-sort"><option value=""></option><option value="path">path</option></select>'
+	'<input id="search-max-count">'
+	'<input id="search-heading" type="checkbox">'
+	'<input id="search-multiline" type="checkbox">'
+	'<div id="search-status"></div>'
+	'<div id="search-results"></div>'
+	'<details id="search-advanced"></details>'
+)
+
+
+def _ready(js: JsPage, href: str = "http://127.0.0.1/search") -> None:
+	js.run("window.SEARCH_API_URL = '/api/search';")
+	js.set_location(href)
+	js.set_html(_FORM)
+
+
+def test_load_throws_when_a_control_is_missing(js: JsPage) -> None:
+	js.run("window.SEARCH_API_URL = '/api/search';")
+	js.set_html("<div></div>")
+	with pytest.raises(JsError):
+		js.load("search-page.js")
+
+
+def test_read_url_fills_the_form_and_opens_advanced_filters(js: JsPage) -> None:
+	js.route("/api/search", body={"results": []})
+	_ready(
+		js,
+		"http://127.0.0.1/search?patterns=foo&paths=src&globs=*.py&sort=path&max_count=5&heading=true&multiline=true",
+	)
+	js.load("search-page.js")
+	assert js.prop("#search-patterns", "value") == "foo"
+	assert js.prop("#search-paths", "value") == "src"
+	assert js.prop("#search-globs", "value") == "*.py"
+	assert js.prop("#search-sort", "value") == "path"
+	assert js.prop("#search-max-count", "value") == "5"
+	assert js.prop("#search-heading", "checked") is True
+	assert js.prop("#search-multiline", "checked") is True
+	assert js.prop("#search-advanced", "open") is True
+
+
+def test_empty_pattern_clears_results_and_the_query_string(js: JsPage) -> None:
+	_ready(js, "http://127.0.0.1/search?patterns=old")
+	js.load("search-page.js")
+	js.set_prop("#search-patterns", "value", "  ")
+	js.dispatch("#search-patterns", "keydown", key="Enter")
+	assert js.text("#search-status") == ""
+	assert js.prop("#search-results", "innerHTML") == ""
+	assert "patterns" not in js.value("location.search")
+
+
+def test_results_are_escaped_and_pluralized(js: JsPage) -> None:
+	js.route("/api/search", body={"results": ["<script>", "ok"]})
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "2 matches"
+	assert "&lt;script&gt;" in js.prop("#search-results", "innerHTML")
+	assert "<script>" not in js.prop("#search-results", "innerHTML")
+	assert "is-loading" not in js.classes("#search-results")
+
+
+def test_a_single_match_uses_the_singular_label(js: JsPage) -> None:
+	js.route("/api/search", body={"results": ["one"]})
+	_ready(js, "http://127.0.0.1/search?patterns=foo&heading=false")
+	js.set_html(_FORM)
+	js.load("search-page.js")
+	assert js.text("#search-status") == "1 match"
+	url = next(str(item["url"]) for item in js.requests() if "/api/search" in str(item["url"]))
+	assert "heading=false" in url
+	assert "patterns=foo" in url
+
+
+def test_missing_results_count_as_zero(js: JsPage) -> None:
+	js.route("/api/search", body={})
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "0 matches"
+	assert js.prop("#search-results", "innerHTML") == ""
+
+
+def test_error_detail_is_shown(js: JsPage) -> None:
+	js.route("/api/search", status=400, body={"detail": "bad pattern"}, status_text="Bad")
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "Error 400: bad pattern"
+
+
+def test_error_without_json_uses_the_status_text(js: JsPage) -> None:
+	js.route("/api/search", status=502, json_error=True, status_text="Bad Gateway")
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "Error 502: Bad Gateway"
+
+
+def test_network_error_is_shown(js: JsPage) -> None:
+	js.route("/api/search", error="offline")
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "Error: offline"
+	assert "is-loading" not in js.classes("#search-results")
+
+
+def test_a_newer_search_aborts_the_previous_one(js: JsPage) -> None:
+	js.route("/api/search", hang=True)
+	_ready(js, "http://127.0.0.1/search?patterns=foo")
+	js.load("search-page.js")
+	assert js.text("#search-status") == "Searching…"
+	js.set_prop("#search-patterns", "value", "bar")
+	js.dispatch("#search-patterns", "keydown", key="Enter")
+	assert js.text("#search-status") == "Searching…"
+	js.fulfill("/api/search", body={"results": ["bar"]})
+	assert js.text("#search-status") == "1 match"
+	assert "Error" not in js.text("#search-status")
+
+
+def test_input_debounces_and_other_controls_search_too(js: JsPage) -> None:
+	js.route("/api/search", body={"results": ["x"]})
+	_ready(js)
+	js.load("search-page.js")
+	js.set_prop("#search-patterns", "value", "foo")
+	js.set_prop("#search-paths", "value", "src")
+	js.set_prop("#search-globs", "value", "*.py")
+	js.set_prop("#search-max-count", "value", "")
+	js.set_prop("#search-sort", "value", "path")
+	js.set_prop("#search-heading", "checked", True)
+	js.set_prop("#search-multiline", "checked", True)
+	js.dispatch("#search-patterns", "input")
+	assert js.text("#search-status") == ""
+	js.tick()
+	url = next(str(item["url"]) for item in js.requests() if "patterns=foo" in str(item["url"]))
+	assert "paths=src" in url
+	assert "globs=*.py" in url
+	assert "sort=path" in url
+	assert "max_count" not in url
+	assert "heading=true" in url
+	assert "multiline=true" in url
+	assert js.value("location.search").find("patterns=foo") >= 0
+
+
+def test_other_keys_do_not_run_a_search_immediately(js: JsPage) -> None:
+	_ready(js)
+	js.load("search-page.js")
+	js.set_prop("#search-patterns", "value", "foo")
+	js.dispatch("#search-patterns", "keydown", key="a")
+	assert js.text("#search-status") == ""
diff --git a/pygitweb/static_tests/test_summary-ref-switcher.py b/pygitweb/static_tests/test_summary-ref-switcher.py
new file mode 100644
index 0000000..cd6132c
--- /dev/null
+++ b/pygitweb/static_tests/test_summary-ref-switcher.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+import base64
+
+from pygitweb.static_tests.js_runtime import JsPage
+from pygitweb.static_tests.pages import requested
+
+_B64 = base64.b64encode(b"# Demo\n").decode("ascii")
+_STATE = {
+	"commit_href": "/commit",
+	"display": "abc",
+	"browse_href": "/tree",
+	"log_href": "/log",
+	"shortlog_href": "/shortlog",
+	"tree_html": "<span class='tree'>files</span>",
+	"readme_html": "<p>hi</p>",
+}
+
+
+def _page(*, project: str = "demo", full: bool = True) -> str:
+	links = (
+		'<a id="summary-ref-log-link"></a><a id="summary-ref-shortlog-link"></a>'
+		'<div id="summary-tree-container"></div><a id="summary-tree-browse-link"></a>'
+		if full
+		else ""
+	)
+	return (
+		'<select id="summary-ref-select"><option value="HEAD">HEAD</option>'
+		'<option value="old" selected>old</option></select>'
+		'<a id="summary-ref-commit-link"></a><a id="summary-ref-browse-link"></a>'
+		+ links
+		+ f'<div id="summary-readme-container" data-project="{project}"></div>'
+		+ f'<div class="page"></div><div id="readme-container" data-readme-b64="{_B64}"></div>'
+	)
+
+
+def test_missing_controls_skip_startup(js: JsPage) -> None:
+	js.set_html("<div></div>")
+	js.load("summary-ref-switcher.js")
+	assert requested(js, "summary-refs") is False
+
+
+def test_missing_project_skips_startup(js: JsPage) -> None:
+	js.set_html(_page(project=""))
+	js.load("summary-ref-switcher.js")
+	assert requested(js, "summary-refs") is False
+
+
+def test_options_replace_stale_entries_and_keep_a_live_value(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": [{"value": "old", "kind": "branch", "label": "old"}]})
+	js.route("summary-ref-state", body=_STATE)
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	assert js.count("#summary-ref-select option") == 2
+	assert js.prop("#summary-ref-select", "value") == "old"
+	assert js.text("#summary-ref-select option:last-child") == "branch: old"
+
+
+def test_a_removed_value_falls_back_to_head(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": [{"value": "main", "kind": "branch", "label": "main"}]})
+	js.route("summary-ref-state", body=_STATE)
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	assert js.prop("#summary-ref-select", "value") == "HEAD"
+	assert js.text("#summary-ref-select option:last-child") == "branch: main"
+
+
+def test_failed_option_and_state_loads_leave_the_page_alone(js: JsPage) -> None:
+	js.route("summary-refs", status=500)
+	js.route("summary-ref-state", status=404)
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	js.set_prop("#summary-ref-select", "value", "HEAD")
+	js.dispatch("#summary-ref-select", "change")
+	assert js.text("#summary-ref-commit-link") == ""
+	assert js.count("#summary-ref-select option") == 2
+
+
+def test_change_loads_the_selected_ref(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.route("summary-ref-state", body=_STATE)
+	js.set_html(_page(project="group/demo"))
+	js.load("summary-ref-switcher.js")
+	js.run(
+		"document.getElementById('summary-ref-select').innerHTML = '<option value=\"feature/a\">feature/a</option>';"
+	)
+	js.dispatch("#summary-ref-select", "change")
+	assert any("ref=feature%2Fa" in str(item["url"]) for item in js.requests())
+	assert any("/project/group/demo/" in str(item["url"]) for item in js.requests())
+
+
+def test_optional_links_are_skipped_when_absent(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.route("summary-ref-state", body=_STATE)
+	js.set_html(_page(full=False))
+	js.load("summary-ref-switcher.js")
+	js.dispatch("#summary-ref-select", "change")
+	assert js.text("#summary-ref-commit-link") == "abc"
+
+
+def test_an_update_with_text_reloads_options_and_state(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": [{"value": "main", "kind": "branch", "label": "main"}]})
+	js.route("summary-ref-state", body=_STATE)
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	before = len(js.requests())
+	js.fulfill("updates=true", status=200, text="changed")
+	assert len(js.requests()) > before
+	assert js.text("#summary-ref-commit-link") == "abc"
+
+
+def test_an_empty_update_does_not_reload(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	refs = [item for item in js.requests() if "summary-refs" in str(item["url"])]
+	js.fulfill("updates=true", status=200, text="")
+	refs_after = [item for item in js.requests() if "summary-refs" in str(item["url"])]
+	assert len(refs_after) == len(refs)
+
+
+def test_not_modified_polls_again_without_a_backoff(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	js.fulfill("updates=true", status=304, text="")
+	updates = [item for item in js.requests() if "updates=true" in str(item["url"])]
+	assert len(updates) >= 2
+	assert js.value("__timers.length") == 0
+
+
+def test_a_failed_update_backs_off_then_continues(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	js.fulfill("updates=true", status=500, text="nope")
+	assert js.value("__timers.length") == 1
+	before = len([item for item in js.requests() if "updates=true" in str(item["url"])])
+	js.tick()
+	after = len([item for item in js.requests() if "updates=true" in str(item["url"])])
+	assert after > before
+
+
+def test_beforeunload_stops_the_subscription(js: JsPage) -> None:
+	js.route("summary-refs", body={"options": []})
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	js.dispatch_window("beforeunload")
+	assert js.value("__alerts") == []
+
+
+def test_a_rejected_update_backs_off_until_abort(js: JsPage) -> None:
+	js.route("updates=true", error="offline")
+	js.route("summary-refs", body={"options": []})
+	js.set_html(_page())
+	js.load("summary-ref-switcher.js")
+	assert js.value("__timers.length") >= 1
+	js.dispatch_window("beforeunload")
+	js.tick()
+	assert js.text("#summary-ref-commit-link") == ""
diff --git a/pyproject.toml b/pyproject.toml
index e916eef..d360282 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,12 +12,21 @@ dev = [
     "mypy>=1.20.1",
     "coverage>=7.15.2",
 ]
+# Native JS engines for --js-tests. Not default groups, so a normal sync leaves them out.
+# uv has no setting that drops a group from --all-groups; pass --no-group stpyv8 --no-group pythonmonkey.
+stpyv8 = [
+    "stpyv8>=13.1.201.22; sys_platform != 'win32' or python_version < '3.14'",
+]
+pythonmonkey = [
+    "pythonmonkey>=1.3.2",
+]
 
 [tool.uv.workspace]
 members = ["postgit", "pygittools", "pygittools-mcp", "pygitweb"]
 
 [tool.uv]
 package = false
+default-groups = ["dev"]
 
 [tool.ruff]
 line-length = 120
@@ -46,6 +55,7 @@ select = [
 [tool.pytest.ini_options]
 markers = [
 	"timing: index timing smoke tests (disabled by default; run with --run-timing)",
+	"js: JavaScript unit tests (disabled by default; run with --js-tests)",
 ]
 
 [tool.mypy]
diff --git a/scripts/pytest.py b/scripts/pytest.py
index 9128a98..b3e2115 100644
--- a/scripts/pytest.py
+++ b/scripts/pytest.py
@@ -6,7 +6,7 @@ import sys
 
 
 def main() -> int:
-	return subprocess.call([sys.executable, "-m", "pytest", "."])
+	return subprocess.call([sys.executable, "-m", "pytest", ".", *sys.argv[1:]])
 
 
 if __name__ == "__main__":
diff --git a/uv.lock b/uv.lock
index 83ab530..885c3da 100644
--- a/uv.lock
+++ b/uv.lock
@@ -27,6 +27,146 @@ wheels = [
 ]
 
 [[package]]
+name = "aiohappyeyeballs"
+version = "2.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.14.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "aiohappyeyeballs" },
+    { name = "aiosignal" },
+    { name = "attrs" },
+    { name = "frozenlist" },
+    { name = "multidict" },
+    { name = "propcache" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+    { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
+    { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
+    { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
+    { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
+    { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
+    { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
+    { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
+    { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
+    { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
+    { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
+    { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
+    { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
+    { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
+    { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
+    { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
+    { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
+    { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
+    { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
+    { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
+    { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
+    { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
+    { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
+    { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
+    { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
+    { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
+    { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
+    { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
+    { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
+    { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
+    { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
+    { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
+    { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
+    { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
+    { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
+    { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
+    { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
+    { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
+    { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
+    { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
+    { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
+    { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
+    { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
+    { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
+    { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
+    { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
+    { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
+    { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "frozenlist" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
 name = "annotated-doc"
 version = "0.0.4"
 source = { registry = "https://pypi.org/simple" }
@@ -376,6 +516,111 @@ standard-no-fastapi-cloud-cli = [
 ]
 
 [[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
+    { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
+    { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
+    { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
+    { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
+    { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
+    { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
+    { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+    { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+    { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+    { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+    { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+    { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+    { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+    { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+    { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+    { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+    { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+    { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+    { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+    { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+    { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+    { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+    { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+    { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+    { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+    { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+    { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
+[[package]]
 name = "h11"
 version = "0.16.0"
 source = { registry = "https://pypi.org/simple" }
@@ -708,6 +953,164 @@ wheels = [
 ]
 
 [[package]]
+name = "multidict"
+version = "6.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e3/24/3823efc330630a1f132bcd0a9b182ddec3c53f452f551c8e8d3aaf5a2d20/multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4", size = 96739, upload-time = "2026-09-21T17:54:42.815Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/69/36331fe1d3aeb0c525a9972d6cf82791075a395aa96f026f060009d691ae/multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987", size = 58514, upload-time = "2026-09-21T17:54:44.064Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/ca/5860651f782078ef13f2761c2593f4ba066d76f626b2f30f23a7f838b775/multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c", size = 57303, upload-time = "2026-09-21T17:54:45.304Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/9f/c56e2fa223cb5d182660e1a09eec38880f96b7cd22769582f74447c60418/multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1", size = 320076, upload-time = "2026-09-21T17:54:46.826Z" },
+    { url = "https://files.pythonhosted.org/packages/59/97/eda4fe0cad51f96096363ed32d3e0f8df28dab00beff4d92adc9db517726/multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07", size = 315489, upload-time = "2026-09-21T17:54:48.25Z" },
+    { url = "https://files.pythonhosted.org/packages/24/14/10b1ded0a4085a51c3054125f854da4f47ce4fec724a2d2503231b83f3db/multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1", size = 296716, upload-time = "2026-09-21T17:54:49.749Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/14/04d155dd18528443cb9009f3a9f35f7417773797d072c95104ff17b8b9b7/multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3", size = 330244, upload-time = "2026-09-21T17:54:51.26Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/90/284d10b3a9e5b312a8ec5b7a175fc7d560eefdc99886320449891a197992/multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80", size = 331288, upload-time = "2026-09-21T17:54:52.595Z" },
+    { url = "https://files.pythonhosted.org/packages/27/50/f420de3683f9b047fc5588064d84043ef57a7152ef451b8373dfc7068d22/multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387", size = 320663, upload-time = "2026-09-21T17:54:53.91Z" },
+    { url = "https://files.pythonhosted.org/packages/01/ab/0120a650d7ce4fe167299c13a13cc4ee2dc72458a9c0e9e9330b824b73c9/multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345", size = 291139, upload-time = "2026-09-21T17:54:55.313Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/3f/c66342f73ee5cd2c9b1dda6cfce89b5f348c48921b8c8b6e59aafbbfe31a/multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd", size = 310044, upload-time = "2026-09-21T17:54:56.745Z" },
+    { url = "https://files.pythonhosted.org/packages/05/0d/e44b90d9e77e44ec3473c4c7ac7ea95764d7e8fbf6d5a085ef9bff7ce0c2/multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f", size = 303279, upload-time = "2026-09-21T17:54:58.178Z" },
+    { url = "https://files.pythonhosted.org/packages/97/8a/7741f7c23c211fae31ab546f0ba41569dae7671cf7ca30d5afb59e9d92aa/multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b", size = 326177, upload-time = "2026-09-21T17:54:59.825Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/63/0cdbfbbe36316b2ef4b8e011afaca2cf291a73fe4fabf34dd0b22e3ee48a/multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82", size = 323526, upload-time = "2026-09-21T17:55:01.43Z" },
+    { url = "https://files.pythonhosted.org/packages/98/05/0c86e9678e78f4596df0ed7f59d2aa755107da3f2a92f1b7fa150e94cbb8/multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953", size = 290278, upload-time = "2026-09-21T17:55:02.993Z" },
+    { url = "https://files.pythonhosted.org/packages/95/8e/71bd7f43c4883cba6c5f96db88bd2311d52e74ad7680be5915938490d56f/multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a", size = 321142, upload-time = "2026-09-21T17:55:04.48Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/00/bf59d6bb22a4c152e4039574c29490caf3c65107715121807b4bd7cde244/multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13", size = 319045, upload-time = "2026-09-21T17:55:06.119Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/3c/f27f4f045198bf1c4bcd6eb3b6cf06be2dceaceb6e67222b903fce7a6749/multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f", size = 51309, upload-time = "2026-09-21T17:55:07.531Z" },
+    { url = "https://files.pythonhosted.org/packages/91/17/c2064aef64efc007bc89a571f843e5772f27946eb9bc018731bf8016e319/multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e", size = 59148, upload-time = "2026-09-21T17:55:08.936Z" },
+    { url = "https://files.pythonhosted.org/packages/45/a1/3bab1827edd813dfb90712af5cbb7fc1473ed4d9c871d103df4f4bb950c2/multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d", size = 54258, upload-time = "2026-09-21T17:55:10.191Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" },
+    { url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" },
+    { url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" },
+    { url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" },
+    { url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" },
+    { url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" },
+    { url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" },
+    { url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" },
+    { url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" },
+    { url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/c0/a70c32ea3299ebe00f44533740cb46905717c51faf29bbd3ce8bb5886d9d/multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb", size = 52548, upload-time = "2026-09-21T17:55:38.529Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/2e/8c9c2591df01e5692ad1bf92febb082a17b1c6c3a19aa8b7ff49988fd989/multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203", size = 59622, upload-time = "2026-09-21T17:55:39.86Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/95/59f4472ec512bc180fd207899594c7803ece96262ff9aaa3a4b6d7da6940/multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc", size = 54930, upload-time = "2026-09-21T17:55:41.155Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/45/ddf7c76860f5f553a23ad3ca38463ccf5247081618977742c8dc4625355e/multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3", size = 63266, upload-time = "2026-09-21T17:55:42.676Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" },
+    { url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" },
+    { url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" },
+    { url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" },
+    { url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" },
+    { url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" },
+    { url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" },
+    { url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" },
+    { url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" },
+    { url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" },
+    { url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/27/32dde80245e2024e9bb7a6ca3c2fb7c695dc74014cba557cc35be6f91e24/multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40", size = 52378, upload-time = "2026-09-21T17:56:16.959Z" },
+    { url = "https://files.pythonhosted.org/packages/be/78/1bac2987edac273a6b27fa50e9204bc3466c94a7b4f6a44828d730f8810f/multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d", size = 59449, upload-time = "2026-09-21T17:56:18.336Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/32/2a77ce19eea48cb9b3521a202b513ba3349aafa702c8317be0b645ffb74b/multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4", size = 54713, upload-time = "2026-09-21T17:56:19.774Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" },
+    { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" },
+    { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" },
+    { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" },
+    { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" },
+    { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" },
+    { url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" },
+    { url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" },
+    { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" },
+    { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" },
+    { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" },
+    { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" },
+    { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" },
+    { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" },
+    { url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" },
+    { url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" },
+    { url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" },
+    { url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" },
+    { url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" },
+    { url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" },
+    { url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" },
+    { url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" },
+    { url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" },
+    { url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" },
+    { url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" },
+    { url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" },
+    { url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" },
+    { url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" },
+    { url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" },
+    { url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" },
+    { url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" },
+    { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" },
+]
+
+[[package]]
 name = "mypy"
 version = "1.20.1"
 source = { registry = "https://pypi.org/simple" }
@@ -862,6 +1265,12 @@ wheels = [
 ]
 
 [[package]]
+name = "pminit"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/fb/ea010a1fa5773b4fa185506b6eecabfa898338a124a515329ec7d4ae0d98/pminit-1.3.2.tar.gz", hash = "sha256:2c5bcfdbc6df07c640f9d51bd0c9766615911d4294e2ec43fb7db2becab54c2b", size = 8266, upload-time = "2026-06-16T21:26:19.542Z" }
+
+[[package]]
 name = "postgit"
 version = "0.1.0"
 source = { editable = "postgit" }
@@ -877,6 +1286,134 @@ requires-dist = [
 ]
 
 [[package]]
+name = "propcache"
+version = "0.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" },
+    { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" },
+    { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" },
+    { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" },
+    { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" },
+    { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" },
+    { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" },
+    { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" },
+    { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" },
+    { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" },
+    { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" },
+    { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" },
+    { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" },
+    { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" },
+    { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" },
+    { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" },
+    { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" },
+    { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" },
+    { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" },
+    { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" },
+    { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" },
+    { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" },
+    { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" },
+    { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" },
+    { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" },
+    { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" },
+    { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" },
+    { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" },
+    { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" },
+    { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" },
+    { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" },
+    { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" },
+    { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" },
+    { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" },
+    { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" },
+    { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" },
+    { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" },
+    { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" },
+    { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" },
+    { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" },
+    { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" },
+    { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" },
+    { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" },
+    { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" },
+    { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" },
+    { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" },
+    { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" },
+    { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" },
+]
+
+[[package]]
 name = "psycopg"
 version = "3.3.3"
 source = { registry = "https://pypi.org/simple" }
@@ -1236,6 +1773,12 @@ dev = [
     { name = "pytest" },
     { name = "ruff" },
 ]
+pythonmonkey = [
+    { name = "pythonmonkey" },
+]
+stpyv8 = [
+    { name = "stpyv8", marker = "python_full_version < '3.14' or sys_platform != 'win32'" },
+]
 
 [package.metadata]
 
@@ -1246,6 +1789,8 @@ dev = [
     { name = "pytest" },
     { name = "ruff" },
 ]
+pythonmonkey = [{ name = "pythonmonkey", specifier = ">=1.3.2" }]
+stpyv8 = [{ name = "stpyv8", marker = "python_full_version < '3.14' or sys_platform != 'win32'", specifier = ">=13.1.201.22" }]
 
 [[package]]
 name = "pygments"
@@ -1285,6 +1830,15 @@ wheels = [
 ]
 
 [[package]]
+name = "pyreadline3"
+version = "3.5.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" },
+]
+
+[[package]]
 name = "pytest"
 version = "9.0.3"
 source = { registry = "https://pypi.org/simple" }
@@ -1341,6 +1895,39 @@ wheels = [
 ]
 
 [[package]]
+name = "pythonmonkey"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "aiohttp" },
+    { name = "pminit" },
+    { name = "pyreadline3", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/24/02/7cc137bf75f25ac2ef917d7f78205963a63508a7336e194ba52902961e95/pythonmonkey-1.3.2.tar.gz", hash = "sha256:7f956b68c9c4689fb8918c1309468c08b6c34fb84a18f32549ba881b28a37aeb", size = 309211, upload-time = "2026-06-16T21:26:20.445Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6c/02/f984ef19827a02ffe2d23969f166aa25d49ad1c62c7221b447eac37c0d67/pythonmonkey-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:928c5c8d81c692df60c60d2f3c61420525e9c866c0bd4aceca63b9de7a69d135", size = 14798175, upload-time = "2026-06-16T21:24:53.617Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/a9/de8e96d1251f76a7912a7e36e5adab910de676c5b854945a95ea435a44eb/pythonmonkey-1.3.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:9d13df9253c61afb487b9f8a4d72999052afb7c15ab63d53fadaf6836d4948a2", size = 15224556, upload-time = "2026-06-16T21:24:56.508Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/7c/f6414b79eb460dacc333814a95367db52817b3ecaca43861f5a90a047307/pythonmonkey-1.3.2-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:a240f7cae3aff3276ebaff703d9dc3a4f1a897d38e82662f5139ce57861a02ca", size = 22199980, upload-time = "2026-06-16T21:24:59.475Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/95/8fa3428865c16fc2f4c06f615aa828c87c90aff846fe8d62403e33ab7c79/pythonmonkey-1.3.2-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:fb9db119c60807522b7668e1ac907faf57c5a9c43733f265a1e426b70ddc50c8", size = 22410074, upload-time = "2026-06-16T21:25:02.746Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/44/ed73609dc85bbcf53496758105d2e32c78c00ddf7fb6b18c2713f8902556/pythonmonkey-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:12262e9ea98cba6c0706d7cc58e8cce3b57c39bfb7452ee95dd2ba7fee6322c2", size = 12986993, upload-time = "2026-06-16T21:25:05.659Z" },
+    { url = "https://files.pythonhosted.org/packages/82/a0/24acedc7c92cb6ad4556dd1c1c00c69517fc3cc1127d1304b855a14ed76b/pythonmonkey-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:baf89197442b4bceed1e392cd5dc6e6fc07554f91f271662f148923d25fd76dc", size = 14801574, upload-time = "2026-06-16T21:25:08.153Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/45/bd1ef4bda5b45c7e13303a54c9d01aefcc262ede4a954bb128f1d7c2a6c3/pythonmonkey-1.3.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:7ae0c53e99cb494fb53f32b5ae072367c75c1b4d0d0e62d127a23cb50359d36a", size = 15228659, upload-time = "2026-06-16T21:25:10.904Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/14/b99290ca12dc98dd8f69c6f6702376ff361ab74fbceb1d5d52f233d25487/pythonmonkey-1.3.2-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:e406d545bd0f33c983b4e2c2855fbf78962d858b3f96f50b6067c51e863ac010", size = 22210060, upload-time = "2026-06-16T21:25:13.898Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/9c/677576bf0dadbdfaff7087a9945c11bd32139d5d8dc87672670ba3ca24eb/pythonmonkey-1.3.2-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:31025ce752b0bcffe105b127a428596cd9cdf74c513aa8b30119c9d71e58668b", size = 22420373, upload-time = "2026-06-16T21:25:16.952Z" },
+    { url = "https://files.pythonhosted.org/packages/38/41/c848c46c333624aedfcc253fc3fc8faaffb0e7028c3c9d7b8f48c0428e56/pythonmonkey-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:ff18d183ab079d658f0a4afd7bffabd0e4f0b1d1bc15a73e96ebbe167cc70594", size = 12987752, upload-time = "2026-06-16T21:25:19.837Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/1b/144587e9d2e30183a51b3653503d562fc960610c1a0671e61dfaad266896/pythonmonkey-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4e1e528fed6c8fb5b5723d50e7f51027a447e7942bf2f422c6012708dda4ae41", size = 14801578, upload-time = "2026-06-16T21:25:22.556Z" },
+    { url = "https://files.pythonhosted.org/packages/53/0c/fd46c22ca52f8a021b4b67767369d6da01e04a76f17c1fb7de43ec0426fe/pythonmonkey-1.3.2-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:7c98b2259789d17571be7bb3b4c568d64cfa6dcc776c390d1da17d62cd1004f0", size = 15228967, upload-time = "2026-06-16T21:25:25.353Z" },
+    { url = "https://files.pythonhosted.org/packages/22/6d/80e86275427985c863b2291e40e2429297e6a8f8f20c4e1a41467ec419fa/pythonmonkey-1.3.2-cp313-cp313-manylinux_2_31_aarch64.whl", hash = "sha256:f0373b8f7411d1fba8f10d29de7da1aeb25f978cd88ac5c86ce595c3112b181f", size = 22180839, upload-time = "2026-06-16T21:25:28.357Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/84/af03b7a75a707b9eb076e5a08a93af0165a44ecadce50e6d202c770cfef2/pythonmonkey-1.3.2-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:e3f31b82792ab25397a2ba2c7e7dba3692248cb18a030d85a6cd47c8aa19d0a4", size = 22390614, upload-time = "2026-06-16T21:25:31.404Z" },
+    { url = "https://files.pythonhosted.org/packages/48/da/db561376e88b3d9415ea39f74335d2646d2df6fc98b17a581108d5da7b08/pythonmonkey-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:717ec284f8339d2a68bf7fcab81cff84d786c7fa1933f023f2ca5d4d8062fa54", size = 12987916, upload-time = "2026-06-16T21:25:33.809Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/99/74424d227885e0a9ae0beafb6913f8c2fa4061a3c7e11435b169f2dc026c/pythonmonkey-1.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:139147b58a206a9c4ad4df9c374d5f3cfac70bf3a4465efd2a5c1fd2b3f23a65", size = 14801608, upload-time = "2026-06-16T21:25:36.589Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/f3/35e202d38c79acd846a2c2725864316d08e8b83e0eab1167bd73d06c1631/pythonmonkey-1.3.2-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:519d07a0ecefa7bb575ad0b085089df6273f2c92866e7d374e3349dad1baff0f", size = 15228806, upload-time = "2026-06-16T21:25:39.119Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/80/b8a3061861330f25b5ac24f3d6784384b2826d81aa1fe93c95c0a9770510/pythonmonkey-1.3.2-cp314-cp314-manylinux_2_31_aarch64.whl", hash = "sha256:2390a1771618a622af3d6ac8e6efe6fb86855619561f5dc9ee79b0d98144665e", size = 22180513, upload-time = "2026-06-16T21:25:41.881Z" },
+    { url = "https://files.pythonhosted.org/packages/25/10/3e0f1141ae329eeeababad7ed3025d91afa4e945f94c626c7f0cd0801aa2/pythonmonkey-1.3.2-cp314-cp314-manylinux_2_31_x86_64.whl", hash = "sha256:1978a88095d7e92190962947af29db5c8fbc70eafd55210d4a69d791d6a3ba73", size = 22390857, upload-time = "2026-06-16T21:25:45.006Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/26/9b74985db1afb99a11fad9f15189426d6ba7c7fb8e42cf8b13c4a445ef5f/pythonmonkey-1.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:dd807356c16b9b457c7e56e67184e6cae8b2a6f101ab8657510bd0559c55e5a1", size = 13357901, upload-time = "2026-06-16T21:25:47.682Z" },
+]
+
+[[package]]
 name = "pywin32"
 version = "312"
 source = { registry = "https://pypi.org/simple" }
@@ -1656,6 +2243,29 @@ wheels = [
 ]
 
 [[package]]
+name = "stpyv8"
+version = "13.1.201.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/17/69/26132fcd57f54a3f1f3688d2ab0a07dadc140b2d54edf28e44933ee7dccc/stpyv8-13.1.201.22.tar.gz", hash = "sha256:2bdbc7307dbf86cfc4bd0a11f0ae1f1c6f91d1b3366a7b409f9fc5d01f69441d", size = 46360, upload-time = "2025-01-09T10:24:10.323Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c9/7a/ba8efdcfbd3f58763829ae6d08fb46a3deb2479bff5e9bab94cea9514bef/stpyv8-13.1.201.22-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:b2a660a331e82fa89d5938ec8418743ebfb544733269f24cd8461a18472701c2", size = 19774016, upload-time = "2025-01-09T10:52:41.94Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/37/45bdadd2b8f19f91770b8fab887d985b0757c2b452611e80031cd69c1051/stpyv8-13.1.201.22-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4d737935167c52ed72e5a78264d9adfeaf089bf54693b88f12cbdb439a36a102", size = 17877975, upload-time = "2025-01-09T10:36:13.298Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/e6/1a16423dec4abdb0fd0cad15f90e696b6edbcc2dbaccf0e29c77f2efca73/stpyv8-13.1.201.22-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:da6d8f2945bd057057c64bc93ea3c064cc848b75f55d6d651120ee5d115e0761", size = 24844564, upload-time = "2025-01-09T10:36:57.908Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/fd/7d40c5daea645d2d415ce489ae1e7ed3306e20a5f3de5027a12b28b89768/stpyv8-13.1.201.22-cp311-cp311-win_amd64.whl", hash = "sha256:b9d9499ed2007cc097a5d2ae0cb18226b2bf3ca429301811b2e12a787a8f137e", size = 14108546, upload-time = "2025-01-09T10:40:35.298Z" },
+    { url = "https://files.pythonhosted.org/packages/be/9b/074a02acf527c84e28a7a24fb7ad702df480336b36c3dd78eebdc7deb69a/stpyv8-13.1.201.22-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:fc2b956bfaf23531c490845edb7d80fc998fe8aee1c7cf1337317dae01169307", size = 19527171, upload-time = "2025-01-09T10:52:46.08Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/83/31f490600dcaf83f2055c2d02d4a3a9363977d7b519b254b9dea4af1a109/stpyv8-13.1.201.22-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6dc40b656cea7fe541f6bdbad83b6b4ed51e5ead985b54c139319a731253a55e", size = 17882094, upload-time = "2025-01-09T10:36:18.005Z" },
+    { url = "https://files.pythonhosted.org/packages/33/d5/b6e089b2229caf5e1ff06cbd1e6d3d259501e56aaeed031546b67f65db22/stpyv8-13.1.201.22-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:c24aa4215c64db7d67fc6c42c0d7731cabcf300596bf9c826ae74f426fe3b771", size = 24885356, upload-time = "2025-01-09T10:37:05.281Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/08/256157e383b6a8d0cf2aa879bea5cdaa02fd43065267e2a198c9777abee1/stpyv8-13.1.201.22-cp312-cp312-win_amd64.whl", hash = "sha256:c4292843c8133fc99833aceef25925a97edf01031e186335582deb077b99d2bf", size = 14109876, upload-time = "2025-01-09T10:40:39.734Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/2e/c3003d600b7c08368c5a4e42d29047aa4fb65b829881560398ffc817268c/stpyv8-13.1.201.22-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:b53df6114a88698ee6f3820cf46476e83ee09c9a67dd9f7cf58ca6a2928238b0", size = 19527440, upload-time = "2025-01-09T10:52:52.341Z" },
+    { url = "https://files.pythonhosted.org/packages/99/43/aab8a2a69a430cc352d0eb72bdcdd6f3154b1fcb678974530f1d863de62c/stpyv8-13.1.201.22-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6cb5e8751aee2487cc3b5f21eac6d459041a7180a779941b64db5736e27276ee", size = 17882373, upload-time = "2025-01-09T10:36:21.519Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/f7/c7848c71c80c53583751f004411f27a7b3ae9db215e9f6b4f2f44be09201/stpyv8-13.1.201.22-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:834b9761bb7f49da8b887847c7647495a2cf6c45f69e2124ae0e3f024493bc15", size = 24881281, upload-time = "2025-01-09T10:37:13.804Z" },
+    { url = "https://files.pythonhosted.org/packages/67/44/4705db1a1f44aad13a877a0985e1af7039e558a9eda5b494d555c217fd7f/stpyv8-13.1.201.22-cp313-cp313-win_amd64.whl", hash = "sha256:c8189b8c4d87579f353705441757f11e2f2260578b82000925dadf0ed59a47e3", size = 14109520, upload-time = "2025-01-09T10:40:43.794Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/cb/0f88fa290a556f35fccf8bd143b88668e49ab21ee7a35fe87950a45ee5bd/stpyv8-13.1.201.22-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:c4bf3048c96a6a1561861da0c74be842c79a71373d3bec0d53c4e8f6eaa7b6e8", size = 19521166, upload-time = "2025-11-03T11:25:35.186Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/cb/f8726baf608d92f76ae66c6180d2d781d4e6dad21488262131b92814d03a/stpyv8-13.1.201.22-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:6fdbc3a8b1aa941064ec0976a5a85761f50e9090468ce275c22d0774293d2668", size = 17883664, upload-time = "2025-11-03T11:24:08.709Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/4e/d002dd7a724d64571c161b1f1b3f3258066becd666b0f5233a7b519a6432/stpyv8-13.1.201.22-cp314-cp314-manylinux_2_35_x86_64.whl", hash = "sha256:c0b258c7c5a79c5f19e636b93eece90d3cf9109af9a11c5394bdb807ed68e04a", size = 24699784, upload-time = "2025-11-03T11:25:15.706Z" },
+]
+
+[[package]]
 name = "typer"
 version = "0.24.1"
 source = { registry = "https://pypi.org/simple" }
@@ -1907,3 +2517,136 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
     { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
 ]
+
+[[package]]
+name = "yarl"
+version = "1.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "idna" },
+    { name = "multidict" },
+    { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" },
+    { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" },
+    { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" },
+    { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" },
+    { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" },
+    { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" },
+    { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" },
+    { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" },
+    { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" },
+    { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" },
+    { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" },
+    { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" },
+    { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" },
+    { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" },
+    { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" },
+    { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" },
+    { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" },
+    { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" },
+    { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" },
+    { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" },
+    { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" },
+    { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" },
+    { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" },
+    { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" },
+    { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" },
+    { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" },
+    { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" },
+    { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" },
+    { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" },
+    { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" },
+    { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" },
+    { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" },
+    { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" },
+    { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" },
+    { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" },
+    { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" },
+    { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" },
+    { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" },
+    { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" },
+    { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" },
+    { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" },
+    { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" },
+    { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" },
+    { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" },
+    { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" },
+    { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" },
+    { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" },
+]
