Neo4jWidget
diff --git a/mkdocs/llms.txt b/mkdocs/llms.txt
index d70271f7..638b4aae 100644
--- a/mkdocs/llms.txt
+++ b/mkdocs/llms.txt
@@ -47,5 +47,4 @@ These widgets depend on 3rd party packages. They still ship with wigglystuff but
- [AltairWidget](https://koaning.github.io/wigglystuff/reference/altair-widget.md): Flicker-free Altair chart rendering with smooth data updates via the Vega View API
- [ModuleTreeWidget](https://koaning.github.io/wigglystuff/reference/module-tree.md): Interactive tree viewer for PyTorch nn.Module architecture
-- [WandbChart](https://koaning.github.io/wigglystuff/reference/wandb-chart.md): Live line chart that polls wandb for metric data with configurable smoothing
- [Neo4jWidget](https://koaning.github.io/wigglystuff/reference/neo4j-widget.md): Interactive Neo4j graph explorer with Cypher query input, autocomplete, and lasso selection
diff --git a/mkdocs/reference/env-config.md b/mkdocs/reference/env-config.md
index ab243b09..500dff18 100644
--- a/mkdocs/reference/env-config.md
+++ b/mkdocs/reference/env-config.md
@@ -6,7 +6,7 @@
| Traitlet | Type | Notes |
| --- | --- | --- |
-| `variables` | `list` | List of variable info dicts with name, status, error, has_validator, value. |
+| `variables` | `list` | List of variable info dicts with name, status, error, and has_validator. Secret values are not synced. |
| `all_valid` | `bool` | True when all variables are valid. |
## Helper methods
diff --git a/mkdocs/reference/wandb-chart.md b/mkdocs/reference/wandb-chart.md
deleted file mode 100644
index 935d6b82..00000000
--- a/mkdocs/reference/wandb-chart.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# WandbChart API
-
-::: wigglystuff.wandb_chart.WandbChart
-
-## Synced traitlets
-
-| Traitlet | Type | Notes |
-| --- | --- | --- |
-| `api_key` | `str` | Your wandb API key. |
-| `entity` | `str` | The wandb entity (user or team). |
-| `project` | `str` | The wandb project name. |
-| `key` | `str` | The metric key to chart (e.g. `"loss"`). |
-| `poll_seconds` | `int \| None` | Seconds between polling updates, or `None` for manual refresh (default: 5). |
-| `smoothing_kind` | `str` | Type of smoothing: `"rolling"`, `"exponential"`, or `"gaussian"` (default: `"gaussian"`). |
-| `smoothing_param` | `float \| None` | Smoothing parameter, or `None` for no smoothing. |
-| `show_slider` | `bool` | Whether to show the smoothing slider (default: `True`). |
-| `width` | `int` | Chart width in pixels (default: 700). |
-| `height` | `int` | Chart height in pixels (default: 300). |
diff --git a/pyproject.toml b/pyproject.toml
index 02eab354..bee43958 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "wigglystuff"
-version = "0.3.5"
+version = "0.4.0"
description = "Collection of Anywidget Widgets"
readme = "README.md"
requires-python = ">=3.10"
@@ -32,6 +32,7 @@ neo4j = [
]
test = [
"pytest>=8.3.3",
+ "marimo>=0.23.3",
"scikit-learn>=1.0",
"matplotlib>=3.0",
"pandas>=2.3.3",
@@ -40,7 +41,7 @@ test = [
test-browser = [
"pytest>=8.3.3",
"pytest-playwright>=0.6.2",
- "marimo>=0.18.0",
+ "marimo>=0.23.3",
]
docs = [
"altair>=6.0.0",
@@ -68,3 +69,8 @@ artifacts = ["wigglystuff/static/*"]
[tool.marimo.runtime]
auto_instantiate = true
+
+[tool.pytest.ini_options]
+markers = [
+ "e2e: browser/server tests that exercise full user flows",
+]
diff --git a/tests/conftest.py b/tests/conftest.py
index b8415b25..78cc4320 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@
import time
import socket
import pytest
+from pathlib import Path
def find_free_port():
@@ -25,7 +26,12 @@ def test_something(start_marimo, page):
"""
servers = []
- def _start(notebook_path: str) -> str:
+ def _start(
+ notebook_path: str,
+ *,
+ env: dict[str, str] | None = None,
+ cwd: str | Path | None = None,
+ ) -> str:
port = find_free_port()
proc = subprocess.Popen(
[
@@ -36,6 +42,8 @@ def _start(notebook_path: str) -> str:
"--port", str(port),
notebook_path,
],
+ cwd=cwd,
+ env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
diff --git a/tests/fixtures/envconfig_export_notebook.py b/tests/fixtures/envconfig_export_notebook.py
new file mode 100644
index 00000000..3ce293ae
--- /dev/null
+++ b/tests/fixtures/envconfig_export_notebook.py
@@ -0,0 +1,29 @@
+import marimo
+
+__generated_with = "0.23.3"
+app = marimo.App(width="medium")
+
+
+@app.cell
+def _():
+ import marimo as mo
+ from wigglystuff import EnvConfig
+
+ config = mo.ui.anywidget(EnvConfig(["WIGGLYSTUFF_EXPORT_SECRET"]))
+ config
+ return (config,)
+
+
+@app.cell
+def _(config):
+ 'WIGGLYSTUFF_EXPORT_SECRET' in config
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/tests/test_browser/tangle_test_notebook.py b/tests/fixtures/tangle_test_notebook.py
similarity index 88%
rename from tests/test_browser/tangle_test_notebook.py
rename to tests/fixtures/tangle_test_notebook.py
index 09852f51..d4a6be1d 100644
--- a/tests/test_browser/tangle_test_notebook.py
+++ b/tests/fixtures/tangle_test_notebook.py
@@ -1,6 +1,6 @@
import marimo
-__generated_with = "0.19.4"
+__generated_with = "0.23.3"
app = marimo.App(width="medium")
@@ -8,6 +8,7 @@
def _():
import marimo as mo
from wigglystuff import TangleSlider, TangleChoice, TangleSelect
+
return TangleChoice, TangleSelect, TangleSlider, mo
@@ -15,21 +16,21 @@ def _():
def _(TangleSlider, mo):
coffees = mo.ui.anywidget(TangleSlider(amount=10, min_value=0, max_value=100, step=1, suffix=" coffees", digits=0))
coffees
- return (coffees,)
+ return
@app.cell
def _(TangleChoice, mo):
emoji = mo.ui.anywidget(TangleChoice(choices=["smile", "party", "boom"]))
emoji
- return (emoji,)
+ return
@app.cell
def _(TangleSelect, mo):
veggie = mo.ui.anywidget(TangleSelect(choices=["potato", "carrot", "apple"]))
veggie
- return (veggie,)
+ return
if __name__ == "__main__":
diff --git a/tests/test_e2e/test_env_config_browser.py b/tests/test_e2e/test_env_config_browser.py
new file mode 100644
index 00000000..5aaba2b5
--- /dev/null
+++ b/tests/test_e2e/test_env_config_browser.py
@@ -0,0 +1,70 @@
+"""Playwright integration test: EnvConfig must not leak manual entries into HTML downloads."""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+import pytest
+from playwright.sync_api import Page, TimeoutError as PlaywrightTimeoutError, expect
+
+
+SECRET_NAME = "WIGGLYSTUFF_EXPORT_SECRET"
+SECRET_VALUE = "wigglystuff-secret-red-test-123"
+NOTEBOOK = Path(__file__).parent.parent / "fixtures" / "envconfig_export_notebook.py"
+ROOT = Path(__file__).parents[2]
+
+
+@pytest.mark.e2e
+def test_env_config_ui_download_omits_manually_entered_secret(
+ tmp_path: Path,
+ page: Page,
+ start_marimo,
+):
+ """EnvConfig should not leak manually entered values into HTML downloads."""
+ notebook_source = NOTEBOOK.read_text(encoding="utf-8")
+ assert SECRET_NAME in notebook_source
+ assert SECRET_VALUE not in notebook_source
+
+ # Run from the in-tree fixture path so marimo picks up the project's
+ # `[tool.marimo.runtime] auto_instantiate = true`. Copying the notebook
+ # to tmp_path leaves it without project context and the cells never run.
+ env = os.environ.copy()
+ env.pop(SECRET_NAME, None)
+
+ url = start_marimo(str(NOTEBOOK), env=env, cwd=ROOT)
+ page.goto(url, wait_until="networkidle")
+ page.wait_for_selector(".env-config-widget", timeout=10_000)
+
+ env_input = page.locator(".env-input")
+ env_input.fill(SECRET_VALUE)
+ env_input.press("Enter")
+ expect(page.locator(".env-config-row")).to_have_attribute(
+ "data-status",
+ "valid",
+ timeout=5_000,
+ )
+ expect(page.locator("pre", has_text="True")).to_be_visible(timeout=5_000)
+
+ command = page.get_by_text(
+ "Download > Download as HTML (exclude code)",
+ exact=True,
+ )
+ for shortcut in ("Meta+K", "Control+K"):
+ page.keyboard.press(shortcut)
+ try:
+ command.wait_for(state="visible", timeout=1_000)
+ break
+ except PlaywrightTimeoutError:
+ page.keyboard.press("Escape")
+ else:
+ command.wait_for(state="visible", timeout=1_000)
+
+ with page.expect_download(timeout=15_000) as download_info:
+ command.click()
+
+ html = Path(download_info.value.path()).read_text(encoding="utf-8")
+ assert "True" in html
+ assert SECRET_VALUE not in html, (
+ "EnvConfig leaked the manually entered secret into the downloaded HTML."
+ )
diff --git a/tests/test_browser/test_sortable_list_browser.py b/tests/test_e2e/test_sortable_list_browser.py
similarity index 96%
rename from tests/test_browser/test_sortable_list_browser.py
rename to tests/test_e2e/test_sortable_list_browser.py
index 336dd883..30515575 100644
--- a/tests/test_browser/test_sortable_list_browser.py
+++ b/tests/test_e2e/test_sortable_list_browser.py
@@ -1,8 +1,10 @@
"""Playwright integration tests for SortableList widget."""
+import pytest
from playwright.sync_api import Page, expect
+@pytest.mark.e2e
def test_sortable_list_renders(start_marimo, page: Page):
"""Test that the SortableList widget renders in the browser."""
url = start_marimo("demos/sortlist.py")
@@ -15,6 +17,7 @@ def test_sortable_list_renders(start_marimo, page: Page):
expect(items).to_have_count(3) # ["a", "b", "c"]
+@pytest.mark.e2e
def test_add_item_updates_list(start_marimo, page: Page):
"""Test that adding an item via the input updates the widget."""
url = start_marimo("demos/sortlist.py")
@@ -36,6 +39,7 @@ def test_add_item_updates_list(start_marimo, page: Page):
expect(new_item_label).to_be_visible()
+@pytest.mark.e2e
def test_remove_item_updates_list(start_marimo, page: Page):
"""Test that clicking remove button removes an item."""
url = start_marimo("demos/sortlist.py")
@@ -53,6 +57,7 @@ def test_remove_item_updates_list(start_marimo, page: Page):
expect(items).to_have_count(2)
+@pytest.mark.e2e
def test_edit_item_updates_value(start_marimo, page: Page):
"""Test that editing an item updates its value."""
url = start_marimo("demos/sortlist.py")
@@ -74,6 +79,7 @@ def test_edit_item_updates_value(start_marimo, page: Page):
expect(edited_label).to_be_visible()
+@pytest.mark.e2e
def test_python_state_updates_after_add(start_marimo, page: Page):
"""Test that adding an item updates the Python state visible in the notebook."""
url = start_marimo("demos/sortlist.py")
diff --git a/tests/test_browser/test_tangle_browser.py b/tests/test_e2e/test_tangle_browser.py
similarity index 94%
rename from tests/test_browser/test_tangle_browser.py
rename to tests/test_e2e/test_tangle_browser.py
index 79ab3ba7..7195a93b 100644
--- a/tests/test_browser/test_tangle_browser.py
+++ b/tests/test_e2e/test_tangle_browser.py
@@ -3,13 +3,15 @@
import os
from pathlib import Path
+import pytest
from playwright.sync_api import Page, expect
# Use a minimal test notebook without requires-python metadata
-NOTEBOOK = str(Path(__file__).parent / "tangle_test_notebook.py")
+NOTEBOOK = str(Path(__file__).parent.parent / "fixtures" / "tangle_test_notebook.py")
TIMEOUT = 30000 if os.environ.get("CI") else 10000
+@pytest.mark.e2e
def test_tangle_slider_renders(start_marimo, page: Page):
"""Test that the TangleSlider widget renders in the browser."""
url = start_marimo(NOTEBOOK)
@@ -24,6 +26,7 @@ def test_tangle_slider_renders(start_marimo, page: Page):
expect(widget).to_contain_text("coffees")
+@pytest.mark.e2e
def test_tangle_slider_drag_updates_value(start_marimo, page: Page):
"""Test that dragging the TangleSlider changes its displayed value."""
url = start_marimo(NOTEBOOK)
@@ -47,6 +50,7 @@ def test_tangle_slider_drag_updates_value(start_marimo, page: Page):
assert new_text != initial_text, f"Value did not change after drag: {initial_text}"
+@pytest.mark.e2e
def test_tangle_choice_renders_and_cycles(start_marimo, page: Page):
"""Test that TangleChoice renders and clicking cycles through choices."""
url = start_marimo(NOTEBOOK)
@@ -66,6 +70,7 @@ def test_tangle_choice_renders_and_cycles(start_marimo, page: Page):
expect(cycled).to_be_visible()
+@pytest.mark.e2e
def test_tangle_select_renders(start_marimo, page: Page):
"""Test that TangleSelect renders a select element."""
url = start_marimo(NOTEBOOK)
diff --git a/tests/test_env_config.py b/tests/test_env_config.py
index 46ea1119..e6074f37 100644
--- a/tests/test_env_config.py
+++ b/tests/test_env_config.py
@@ -139,6 +139,8 @@ def test_preexisting_env_vars_detected():
assert config.variables[2]["status"] == "missing"
assert config["PREEXIST_A"] == "value_a"
assert config["PREEXIST_B"] == "value_b"
+ assert "value" not in config.variables[0]
+ assert "value" not in config.variables[1]
finally:
del os.environ["PREEXIST_A"]
del os.environ["PREEXIST_B"]
@@ -171,6 +173,44 @@ def my_validator(v):
assert vars_by_name["WITHOUT_VALIDATOR"]["has_validator"] is False
+def test_synced_submit_trait_is_scrubbed_after_frontend_update():
+ def validator(value):
+ if value != "secret-value":
+ raise ValueError("bad secret")
+
+ config = EnvConfig({"PENDING_SECRET": validator})
+
+ config.set_trait(
+ "_pending_value",
+ {
+ "name": "PENDING_SECRET",
+ "value": "bad-value",
+ "nonce": 1,
+ },
+ )
+
+ assert config.variables[0]["status"] == "invalid"
+ assert config.variables[0]["error"] == "bad secret"
+ assert "value" not in config.variables[0]
+ assert config._pending_value == {}
+ assert "PENDING_SECRET" not in config
+
+ config.set_trait(
+ "_pending_value",
+ {
+ "name": "PENDING_SECRET",
+ "value": "secret-value",
+ "nonce": 2,
+ },
+ )
+
+ assert config["PENDING_SECRET"] == "secret-value"
+ assert config.variables[0]["status"] == "valid"
+ assert config.variables[0]["error"] is None
+ assert "value" not in config.variables[0]
+ assert config._pending_value == {}
+
+
def test_require_valid_with_subset():
with pytest.MonkeyPatch.context() as mp:
mp.setenv("SUBSET_A", "value_a")
diff --git a/tests/test_env_config_export.py b/tests/test_env_config_export.py
new file mode 100644
index 00000000..78974089
--- /dev/null
+++ b/tests/test_env_config_export.py
@@ -0,0 +1,56 @@
+"""Export regression tests for EnvConfig."""
+
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+
+SECRET_NAME = "WIGGLYSTUFF_EXPORT_SECRET"
+SECRET_VALUE = "wigglystuff-secret-red-test-123"
+NOTEBOOK = Path(__file__).parent / "fixtures" / "envconfig_export_notebook.py"
+ROOT = Path(__file__).parents[1]
+
+
+def test_env_config_html_export_omits_environment_secret(
+ tmp_path: Path,
+):
+ """EnvConfig should not leak environment values into static HTML exports."""
+ notebook_source = NOTEBOOK.read_text(encoding="utf-8")
+ assert SECRET_NAME in notebook_source
+ assert SECRET_VALUE not in notebook_source
+
+ notebook = tmp_path / NOTEBOOK.name
+ shutil.copyfile(NOTEBOOK, notebook)
+ output = tmp_path / "export.html"
+
+ env = {**os.environ, SECRET_NAME: SECRET_VALUE}
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "marimo",
+ "export",
+ "html",
+ "--no-include-code",
+ str(notebook),
+ "-o",
+ str(output),
+ "-f",
+ ],
+ cwd=Path(__file__).parents[1],
+ env=env,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ assert result.returncode == 0, result.stderr
+ html = output.read_text(encoding="utf-8")
+
+ assert SECRET_VALUE not in html, (
+ "EnvConfig leaked the configured secret into the exported HTML."
+ )
diff --git a/uv.lock b/uv.lock
index f342a151..94638761 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1270,110 +1270,126 @@ wheels = [
[[package]]
name = "loro"
-version = "1.8.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/da/7b/35ac8d942be584c8f5c9b991a31a5a8a33144d406fbfb5c791bb94222f0c/loro-1.8.2.tar.gz", hash = "sha256:d22dc17cbec652ed8bf627f801a0a32e27a87b4476a2ab96f45a02d163d733ae", size = 67766, upload-time = "2025-10-23T13:18:48.669Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/94/fcfabab2e1197a95eb6d5c58f34e2aa29e0b5b927e53ad6f63c6019312d3/loro-1.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:991a2f9863d9e92c0ede2b173ab10c42575959d14526e742e33848b1a2ad735d", size = 3137647, upload-time = "2025-10-23T13:16:29.483Z" },
- { url = "https://files.pythonhosted.org/packages/3e/92/187fefb1401d2032c0a8e31baaae134d93ee0920f76a0c9648d0827e9429/loro-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbf098db1af8b32b60bdae313392a0c70f2e6c6482ae4dfdc172908c9825ef3a", size = 2932202, upload-time = "2025-10-23T13:16:13.69Z" },
- { url = "https://files.pythonhosted.org/packages/a8/65/44fd660bf2b7049c5b942096ef4be7aa1c234858ce3844898a991cfffaf1/loro-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9490bceadbdf8a053ae17cbf1174fc12419f41f9507b07d9e3b8ec6cdbf381d", size = 3139070, upload-time = "2025-10-23T13:13:37.297Z" },
- { url = "https://files.pythonhosted.org/packages/72/e6/fd20541b30d85829ee537fb0dda1a76233eb33931520132b9ed8260a8bf2/loro-1.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:469b609a4c3670628f37d2d95f7e005567b97c0f0bd0b3f3ef4eb06f859443e4", size = 3208820, upload-time = "2025-10-23T13:14:08.552Z" },
- { url = "https://files.pythonhosted.org/packages/12/53/688c9941c36e8ab7682c5b56a7d3862b98a4a8b608acef8d106d59168c68/loro-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de8633d88d900e9b2e1fbfa2b76514544514dbb34bf562929f58a02201a91c1b", size = 3592341, upload-time = "2025-10-23T13:14:35.232Z" },
- { url = "https://files.pythonhosted.org/packages/0e/65/94b78d1d62cf7ebcd89fe93d870e8ca02d70299a1f2ae7a0603648a7677a/loro-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995feff071dbed30f82cadb811445d465cc2509279c82ee7891d05a9f1114481", size = 3309640, upload-time = "2025-10-23T13:15:01.514Z" },
- { url = "https://files.pythonhosted.org/packages/64/0a/53681513c98f344087bc493f3507f9620c557d419150239dbeee521f7303/loro-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31d51b77bd89c76300d787b2a2ce14aee6f9727a94fd72e028048ea747be731", size = 3197993, upload-time = "2025-10-23T13:15:51.715Z" },
- { url = "https://files.pythonhosted.org/packages/49/74/1430fb076bfa459e339365a933c2bf5af05a12fa91d631cfb6a60aaba4ae/loro-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a47d9e5e372ddd2990827bfb7e6e26717d2c61017c07b42f95b71cb0cffc356f", size = 3549254, upload-time = "2025-10-23T13:15:27.593Z" },
- { url = "https://files.pythonhosted.org/packages/a1/bd/35527bea951841d13a573103012bf3a9b7224d24bd83347c629572ffa5fd/loro-1.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1cd955d849f4475505ce185fea62fe7e84f51713f774dcb188a2db0a91c79e63", size = 3319589, upload-time = "2025-10-23T13:16:42.753Z" },
- { url = "https://files.pythonhosted.org/packages/4d/ed/704bf216ed53e4781db0d2aa4ac69c523381870eab956f3255065e4ca91a/loro-1.8.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b1d8ff806815aa82ce93fbe5af57bbd11a82150104eeb3944dcae3a43a88cc57", size = 3474273, upload-time = "2025-10-23T13:17:10.414Z" },
- { url = "https://files.pythonhosted.org/packages/8b/41/1ae038ec2f1d7383e2d0057e5f8ad0e0b32ce071253bb2f09386617f36c5/loro-1.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d23c1dbd128e4a9ccddfbb3f93a3dbb7d1bddd968cbc305c437299f8ecfd6390", size = 3527656, upload-time = "2025-10-23T13:17:37.935Z" },
- { url = "https://files.pythonhosted.org/packages/03/38/8851d226971a3e68d6e3b714a82abaff61c11e635d821d87b197ead2c299/loro-1.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ba937bdaf222a4b1e155ca1fbaf224dc843c997e00f7d18b8eb332ec20a52f", size = 3419664, upload-time = "2025-10-23T13:18:11.268Z" },
- { url = "https://files.pythonhosted.org/packages/2b/74/66a18347b6fd5e9799ed986bac9690400d9f87acb590ea67b0c8fc671c82/loro-1.8.2-cp310-cp310-win32.whl", hash = "sha256:52f0e4ccfc2acd780b4626fe8e80834f1a9a75dedae6336885b4effd2bf508af", size = 2617525, upload-time = "2025-10-23T13:19:07.58Z" },
- { url = "https://files.pythonhosted.org/packages/bf/38/6c4eedbe9bbaf977e6949e39a67191467c03c1d3d23b876e396b2917b7f0/loro-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:dfb9c0dcb66370b4eccf81902f207bc94feafb39cce24ac81652272880760d5b", size = 2769392, upload-time = "2025-10-23T13:18:49.76Z" },
- { url = "https://files.pythonhosted.org/packages/cd/6b/9566e0316a0a3995cb95e7875347e54780696c5446b3776256cb9fd0f9fc/loro-1.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5324381b9fa064b288dfeeec13bece290edb5076be84cd53ca802262e7df70eb", size = 3137008, upload-time = "2025-10-23T13:16:31.235Z" },
- { url = "https://files.pythonhosted.org/packages/18/90/0586eac1e12f14b40789f697d902dfcbf21af1546799dea31f390fa21fab/loro-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2adba59abc619810bea291fca0546d2b5464b45f33c891d0010771c871716a2", size = 2925002, upload-time = "2025-10-23T13:16:14.956Z" },
- { url = "https://files.pythonhosted.org/packages/67/b2/24b2d14d421f7ad7ba8d3f6c7398d9a4087b78d48d8a9d4edb9fdaf585a0/loro-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635754eaf5c7d91ad1a1bad949829e203bcb55a8f741114d06b53c9da935bf35", size = 3133663, upload-time = "2025-10-23T13:13:39.744Z" },
- { url = "https://files.pythonhosted.org/packages/ee/16/3b49259d2f068e9ec8b667e9f398fe20005e6d3eed1a2abc6c9da1889601/loro-1.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa33ce541ee50cafaf39a63523e3c726afed968878bc68c84f13327979385cc", size = 3209907, upload-time = "2025-10-23T13:14:10.125Z" },
- { url = "https://files.pythonhosted.org/packages/91/a0/439e7a9abd40601ed4680b662a2b8d1198eb2f48a04cb606db9fedf1a8a9/loro-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c487a56c9832ff1ad52c06b045242790269d8b7b1153fcd621d6f5546f1a344", size = 3587324, upload-time = "2025-10-23T13:14:36.637Z" },
- { url = "https://files.pythonhosted.org/packages/51/6d/caca999eb4936b61cb39038e6a02bf45bef85359f86343d6f6d83494c2d4/loro-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e50996521fe727d2a2382c44ab13485821fd2865f6dd219dce076a37832d885", size = 3301854, upload-time = "2025-10-23T13:15:02.712Z" },
- { url = "https://files.pythonhosted.org/packages/a3/0b/7a57280cd93a861ad64c44d6130c37ef30eef1b3829ba86eed4764d2688e/loro-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923e27275419882278c559ba6166253260fd2c9152017f6199c9b73b87e24880", size = 3196928, upload-time = "2025-10-23T13:15:53.235Z" },
- { url = "https://files.pythonhosted.org/packages/72/ce/466f9bdf60b0d9b942e5b6a698e7d2d7da79c2b724f7135a0e9f690ed318/loro-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:784c4776a87014243f6adac8a668c78151206cd3e1f11e03cf41d8e431f798a8", size = 3547378, upload-time = "2025-10-23T13:15:28.811Z" },
- { url = "https://files.pythonhosted.org/packages/7a/cc/a59b7bbd7917ede52860471265af3c50ea47a6b7a5d751f642445a06c92f/loro-1.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a3c94cdf284f6fe690a1195d620a4f258838d13ed7cb7dec396c28532f7e820", size = 3313860, upload-time = "2025-10-23T13:16:43.993Z" },
- { url = "https://files.pythonhosted.org/packages/ea/18/0b25cdfd63841ee402c089cd946ad70c29bf7534fce8907ffb70cc73d32d/loro-1.8.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:908a187b2ae09d54364a599564d2f283f45a128b070155ab61def3caa6d13f93", size = 3475899, upload-time = "2025-10-23T13:17:12.12Z" },
- { url = "https://files.pythonhosted.org/packages/44/56/4b81ce9cd0595a933797ca8970359ecf945c438d1bbbf81b86a40b33d807/loro-1.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ee8879931937ddb7f3e43db04bbcb3c80450300ea966caa827dcbf8057bc8fe", size = 3525075, upload-time = "2025-10-23T13:17:39.331Z" },
- { url = "https://files.pythonhosted.org/packages/b0/04/efb112af4e16ed5d3c274e108199546086a592813c333c1f0de8269cc1bb/loro-1.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5db8634fed037ee8fee72e8e3788863fba0858cd136dbb037b62c6bcf2d066a6", size = 3418270, upload-time = "2025-10-23T13:18:12.899Z" },
- { url = "https://files.pythonhosted.org/packages/17/f0/5dbb7d76d29a2371b882b723e3ef0d67db6be61d8fee6d823fa9f58e7b06/loro-1.8.2-cp311-cp311-win32.whl", hash = "sha256:ac287aaa224bae629d956b23bc9a275f8a1de4d073a705536f7a5a56c8c9edba", size = 2618071, upload-time = "2025-10-23T13:19:09.138Z" },
- { url = "https://files.pythonhosted.org/packages/bf/4e/0f81ac6b1c185e5cc34aa511a56609b296a7dd7bdc346e72dd093896a408/loro-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:7da0ddead4ae88e99b7b0dcc50ea8846e2d221868c7a6466c79f3177de8befe8", size = 2769218, upload-time = "2025-10-23T13:18:51.096Z" },
- { url = "https://files.pythonhosted.org/packages/03/6c/08cca29c757148f1013948f4f9469a57dc146b0bb0f0fba3d5b0c2e50ea6/loro-1.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d1df0d790eed380afdca28ebe8f4ae42688b8683522e81ccf9fffca7f76f210c", size = 3119329, upload-time = "2025-10-23T13:16:32.529Z" },
- { url = "https://files.pythonhosted.org/packages/5e/ab/cd53107088533c2f32136230bcde58026cf4c16700dc63e93b33e45ee7c4/loro-1.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:764e51163fd3f94835703746ae8a0eefbef9016536e0141ec5ae22da498d0d89", size = 2908530, upload-time = "2025-10-23T13:16:16.319Z" },
- { url = "https://files.pythonhosted.org/packages/d1/01/5262a02499ae2f47e5a2e545757eb515758eab14a21a9b5e5721b7214b0e/loro-1.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc7bed8f95a7b9d2c4d3be54355b176c75efc72a4de160c951a0e2759c562b0d", size = 3136916, upload-time = "2025-10-23T13:13:41.406Z" },
- { url = "https://files.pythonhosted.org/packages/69/56/e0371fe0d7306e23d1555571db19ba10acad3c30ae1ba37af323e4c53953/loro-1.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75a29b80640e845ffe71e0f9262c38605fee3ac47fb46f701273587f97bff56b", size = 3213049, upload-time = "2025-10-23T13:14:11.414Z" },
- { url = "https://files.pythonhosted.org/packages/9d/34/348bc856c01def9ef16b574820329a625b69bd15983e26285f5eca91338a/loro-1.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca7b0370c1bfa2480d2a38a7cf4478584895bee0287ae3109d3b5f44f908d783", size = 3588230, upload-time = "2025-10-23T13:14:37.921Z" },
- { url = "https://files.pythonhosted.org/packages/42/2f/c7f93ceff1aa10768d4dbe0ce6a8288e93cb71a963fff7f3ad1e93fe3da2/loro-1.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f93478c1cce48bb3675b853770b181d650ec6715d38c91ccc713f914d5f2530", size = 3311038, upload-time = "2025-10-23T13:15:04.169Z" },
- { url = "https://files.pythonhosted.org/packages/db/4c/1880a46296339f6b22774c276480fa2e28d030cd9b22cdcd83f49b4d074e/loro-1.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:971ed7f9e6415df35fdecb175bffdf2b684f0ce98b13980e9ab716b88f81cf5d", size = 3200383, upload-time = "2025-10-23T13:15:54.569Z" },
- { url = "https://files.pythonhosted.org/packages/8e/03/5fb9f70ed68d5404778b3fe736d32446b7dc7e5f8f4ac9e5c02c6d823011/loro-1.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0955b19affabff721668d1902fd0e2fa55349e19c47e2fe206049ce216614c86", size = 3542736, upload-time = "2025-10-23T13:15:31.187Z" },
- { url = "https://files.pythonhosted.org/packages/4a/7a/24ea8d3364aa9f1989b3c872d26e19f75ef2547122c86f94ee8488882c47/loro-1.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d7cdbff67d74360fc32fed92383b96b1d5fa744195fb3c676858e69c5344a8a", size = 3317330, upload-time = "2025-10-23T13:16:45.194Z" },
- { url = "https://files.pythonhosted.org/packages/88/86/821ef7ae135389197221d628e6669598d0a1adde436a36b47f06dce0af46/loro-1.8.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9147f43f7e39503e8dc4c753172b073bccc61d1fcf142ed4e049f271f1b03d22", size = 3477573, upload-time = "2025-10-23T13:17:13.423Z" },
- { url = "https://files.pythonhosted.org/packages/79/3f/db1c663e4d18ccc07fba3e33a1136e04d35d5ed58c79a38218a88dca73dc/loro-1.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2af01673ee899fd6930c652a708a703d4e66f340f748d20ed0ba1afe8e6fa29e", size = 3522001, upload-time = "2025-10-23T13:17:44.032Z" },
- { url = "https://files.pythonhosted.org/packages/92/c6/ed8a65d5367f9050474b2ca495d1f673c050ab6046209b7c1100e2543fa4/loro-1.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:050156868ea84e1b2b811fd75b8b30c65748f88e3facc3cfcf29df1214f5d38e", size = 3422105, upload-time = "2025-10-23T13:18:15.962Z" },
- { url = "https://files.pythonhosted.org/packages/f7/01/05ea9746f252ca66d90f13bac0f1b263ffe4cb589db8cafc55d3ce6b059b/loro-1.8.2-cp312-cp312-win32.whl", hash = "sha256:32a45cb8828c9fd3d1d650b7b04358940b21218f03ec83eb7043162fe8495132", size = 2614076, upload-time = "2025-10-23T13:19:10.61Z" },
- { url = "https://files.pythonhosted.org/packages/11/a2/3a9ccd4c8b4aea64526e71b789f7969acf8c9695239265e0469385702802/loro-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:09b26f0cc601d3b9b10283e840b21cf4a9dc4a7c554f936e79d484abeec8b0c4", size = 2771893, upload-time = "2025-10-23T13:18:52.4Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cb/a1e04f8a754a84e5614691d6c3bfe60c2c0b145906180e0965c838fe4a99/loro-1.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1fbb612dad91a29a1c1930da4e70ac39c8d9bf254835e02b49a961f87c8bcab8", size = 3118777, upload-time = "2025-10-23T13:16:33.755Z" },
- { url = "https://files.pythonhosted.org/packages/f8/81/2d1d7c621b34ac2f16116257956acec8c89c4db54b4c69a3f2b4c04473dd/loro-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ddcd12efd7070768074e5d8ae2cb20dc64ee6148fc42725f94ec9975398a6068", size = 2907708, upload-time = "2025-10-23T13:16:17.524Z" },
- { url = "https://files.pythonhosted.org/packages/cf/22/d4a3b310f1d24ea13763d4a955bfe2d0e7b19def688f36acfb4bfedccb9c/loro-1.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e45bd2b699c1fe46612600d5309ee1a547c800bd75c63b5d34fbff2e93ce7d0", size = 3136961, upload-time = "2025-10-23T13:13:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/3c/86/141cae20c24828859071817b677126e7777cef30baaca6c39d89a25537d5/loro-1.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c80a74d11be97c3bc0853b736a476ba3592875ec9044bd5f9632ad0d232d6a7b", size = 3212741, upload-time = "2025-10-23T13:14:12.889Z" },
- { url = "https://files.pythonhosted.org/packages/29/06/d6448b7fdf56468832429b42f2121f5adb6c79855f42662a1b97c977f093/loro-1.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b16af33c9374d1ac3ec05169a49af888e4ed6c35c922df589e328286fa0fb", size = 3588711, upload-time = "2025-10-23T13:14:39.118Z" },
- { url = "https://files.pythonhosted.org/packages/3f/79/72fe346187197862b40e2e2af2c6af19ae61110bde8b69a773018c18cdd2/loro-1.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58f189140e1d4546952784a46a31916f39b0bdceec87d45ca2457cf16da82de3", size = 3311449, upload-time = "2025-10-23T13:15:05.436Z" },
- { url = "https://files.pythonhosted.org/packages/8b/fb/2ea45e6e5635c12751e42b552e272d2e7acc08a0d39ca363eca656ad1157/loro-1.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1039a12ae997c0b4ec8f147f8dc5d542d48bdc4a02374deb4019ff22b6012a04", size = 3200241, upload-time = "2025-10-23T13:15:56.197Z" },
- { url = "https://files.pythonhosted.org/packages/58/1c/c60ad1c6efed6adc17402a6d8ea22f5571c2f31bbceaf27f017769687c6c/loro-1.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e00fc2aecf14108b424f9566cfa469ff8e914208d72b146dca6a1c475377110e", size = 3542571, upload-time = "2025-10-23T13:15:32.433Z" },
- { url = "https://files.pythonhosted.org/packages/4c/a5/ee981e6072056c562b69137e7b0b8bd77f16eda61cd9f7bb2a5827b86a4e/loro-1.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:257a673bb67e2d60ac50b8d19556f340f4e59afbd355a2290e0786756c8b41c9", size = 3316938, upload-time = "2025-10-23T13:16:46.564Z" },
- { url = "https://files.pythonhosted.org/packages/c2/f1/2c3f039d11c6e4868097e586f176eb818ffa7c8a6f144c8f520752b22efb/loro-1.8.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5b95219f68dcaf506d2c2b67aed1f523ae33886a921696156fd7bca2f6b88c77", size = 3477852, upload-time = "2025-10-23T13:17:14.665Z" },
- { url = "https://files.pythonhosted.org/packages/84/28/c5fa1f1335d866c9b8ca88e9e3a6148e3e923c95a6d065fd9b168b18576d/loro-1.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab60cd92c2b773529a4e7a1ad1fe4f6b5e869b8ab62686723a83ae5d00841c0f", size = 3521660, upload-time = "2025-10-23T13:17:45.476Z" },
- { url = "https://files.pythonhosted.org/packages/82/85/76d7dbaac05408c560f0620b66cc01490606fdd39ae309a24cdb7adfd793/loro-1.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e98d1e9f164777d883fb037f68941627e91bce180860a272e0297ec258ffe32c", size = 3422136, upload-time = "2025-10-23T13:18:17.221Z" },
- { url = "https://files.pythonhosted.org/packages/f6/9c/88ab3f33b995bf11a80f27def78795445d0bd8fdbdc6272f20d08edee5fa/loro-1.8.2-cp313-cp313-win32.whl", hash = "sha256:ff83c1d4a8d12c0df48c8f29bf948aed6c94e62bcbae13d41fd963af2ecf0d8c", size = 2613680, upload-time = "2025-10-23T13:19:12.051Z" },
- { url = "https://files.pythonhosted.org/packages/79/68/2677ca414034f27a62fac7a504e776ba94167f9fb66c1c619b29ba6faa37/loro-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:1c2f8a9b0d76ac17d926eca013ed9d5281be9cd6d94130886f20c67089a43f94", size = 2771659, upload-time = "2025-10-23T13:18:53.66Z" },
- { url = "https://files.pythonhosted.org/packages/e5/1c/87f54a03b9dcbc0861df9c7c1aaee39638994e895fb14e9fa6c74670e5a1/loro-1.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5d4ed382d0431b4ad709dd07cecf80135bd8e4672788172f6af245744549187", size = 3132834, upload-time = "2025-10-23T13:13:44.362Z" },
- { url = "https://files.pythonhosted.org/packages/33/3f/63f9ed0f9836c63bb3dc19517b50607876f153c5d328730a7529619c4602/loro-1.8.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00681946c6a445eb2ee5aae887e2ddf431fe81aa78e01eeb91cb4ef98ef8277c", size = 3208564, upload-time = "2025-10-23T13:14:14.113Z" },
- { url = "https://files.pythonhosted.org/packages/3f/99/309a716171e6f9224a1f614419bb875e9f40c1879a8a95ca2312e7f33f67/loro-1.8.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5ab40ae72c5913ccca0c5a3e5fbebfc5baec8129d5bc93f51da1718e56c9a2a", size = 3584869, upload-time = "2025-10-23T13:14:40.322Z" },
- { url = "https://files.pythonhosted.org/packages/65/a6/70467495ab274fbefb81c15a1bb3ec824d61b5ebd6f5ef6abe0f873fc52b/loro-1.8.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6545d4339f22e0fc897970f77015b636bc84c883c0631b1ad7d04839e3e4094", size = 3303725, upload-time = "2025-10-23T13:15:06.64Z" },
- { url = "https://files.pythonhosted.org/packages/c4/d5/a1e535b037f413623eea932e2c72387993198836312e4125d24fcd0d515c/loro-1.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6931e44ca9e1afec359bf4c21443bd2e484fac112e1604024f1e5b93bc247854", size = 3311368, upload-time = "2025-10-23T13:16:47.872Z" },
- { url = "https://files.pythonhosted.org/packages/41/72/7db5794a30fbf1ff3e39066e22a1fd07938ac5d50e465933418d1541be17/loro-1.8.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:7516dfc3ead095e78d814921a4e16ba90d61d93c1a37189b5ea9fd5683dc3b0f", size = 3473187, upload-time = "2025-10-23T13:17:15.927Z" },
- { url = "https://files.pythonhosted.org/packages/e5/0d/57c893a6cc0aae52a15fa2e86d1cd2b2dc28387f02acce3fbb573ac918df/loro-1.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d27ece1d1440cf1135f94be9c6b753ac810534a3d86118bd3d2a11272456bd2", size = 3517606, upload-time = "2025-10-23T13:17:47.003Z" },
- { url = "https://files.pythonhosted.org/packages/28/ff/0c1182d06ade73cb408448ff279423e8da9fe09c8ac8b0c2affbe7d937c1/loro-1.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:79d7c309447339f72edbb468edb998f0a0dbd1c3bea70c92897f9baae02b7c79", size = 3420002, upload-time = "2025-10-23T13:18:18.795Z" },
- { url = "https://files.pythonhosted.org/packages/ab/3f/62667497c325e6af2d7a3761a92ffbb18bcf62857dd28a47e9d170da6e61/loro-1.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9febfc1ff026af58ddff2038245cfc8725be50a322352c6ca4fa4c2c3fd7ed66", size = 3098742, upload-time = "2025-10-23T13:16:37.355Z" },
- { url = "https://files.pythonhosted.org/packages/40/c8/d1aefd4ffdc3820b4af0d56742d4dc24deb0f88967c431c0b5b00f404592/loro-1.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6e299f5db4ee3eb83492129e6e6735bf8e0d1227e7fe9181783a53307b0ca154", size = 2902853, upload-time = "2025-10-23T13:16:18.741Z" },
- { url = "https://files.pythonhosted.org/packages/82/f8/4faff4ac6962c41fcc6ee380f4dcacc8307aa0afd34cf3f389f19b54ac53/loro-1.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b01db2448b143c5b417eb75ac96c383239647c0cd0c539735020413a8e5de3c", size = 3187824, upload-time = "2025-10-23T13:15:57.888Z" },
- { url = "https://files.pythonhosted.org/packages/46/47/8380740e034e0de2e6663f9b5e3ff3a6c9e9a1017cd18303b790be2d3a76/loro-1.8.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e497def0e9877778aa1ff0181667bae8f52116c9adafaeb46927abbf3e9ad69", size = 3532937, upload-time = "2025-10-23T13:15:33.792Z" },
- { url = "https://files.pythonhosted.org/packages/f4/ee/51ea23aca3a0ac99172dde3920a941738bdef1dc02fd23895b4e18b4745c/loro-1.8.2-cp314-cp314-win32.whl", hash = "sha256:aeaa61b14ec5826088b815a99d206f3b45a65c2c36f804954ac0d961b399a661", size = 2603096, upload-time = "2025-10-23T13:19:13.641Z" },
- { url = "https://files.pythonhosted.org/packages/5f/c9/9173500b4b54a13c1cd558eb42b95c552598ac18357aa9fbda9faf3e9af8/loro-1.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:45999b244e33c83601999b8eb239373667ab465ab00d8afdfad0f432a759a27f", size = 2757675, upload-time = "2025-10-23T13:18:55.088Z" },
- { url = "https://files.pythonhosted.org/packages/cd/fb/136284c5b9986fdddfef23bc282747c44bdf1436e556583b95ad39fc402d/loro-1.8.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6dbd2366328469fb7e74ff103b5414feb4874be03ff6970b9329b393b1e7183", size = 3137879, upload-time = "2025-10-23T13:13:49.097Z" },
- { url = "https://files.pythonhosted.org/packages/e0/6d/53f7d1fb8e2717afa36a303de97262e218c09fa83b2ded93c31e4a068eb3/loro-1.8.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f3ca3de9c68680ff7786402ee2616ef1196ac70d18e040f51d64aac82649dfe", size = 3209119, upload-time = "2025-10-23T13:14:18.103Z" },
- { url = "https://files.pythonhosted.org/packages/29/86/48e6c693ea60c12558c2651e8f86ee84ca26ac049ee47a70aaa5de55bf6e/loro-1.8.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee8910e23da474973d7a3e83cf96f5478306ab198655c491758e9b754fb11d8b", size = 3591688, upload-time = "2025-10-23T13:14:44.349Z" },
- { url = "https://files.pythonhosted.org/packages/4f/53/37aaba5eb3bbadd011da96b730f636753c86cd2ddc0a3711809138941fc7/loro-1.8.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c60e837a48f2af969a227be4cb1a80677baefcac88f7ab4050d50b980d66eb6", size = 3309652, upload-time = "2025-10-23T13:15:10.483Z" },
- { url = "https://files.pythonhosted.org/packages/bb/8f/e5e00e4906cd1372262c08725d5dc051bb3f5291841371f9c1c4cd7b01fa/loro-1.8.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e760d727a1b957ce1918a6208377a1601551b0d8e2fc76824ae022e87ecaa648", size = 3316909, upload-time = "2025-10-23T13:16:51.891Z" },
- { url = "https://files.pythonhosted.org/packages/75/fa/a1c815f8228367be4ba86e9bed203a7456cd591fd72a51b46e053cf5a037/loro-1.8.2-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fdb2c2a8be78233f23fa0a0bbddf5f22387a0d13d85c2ec6ce92274289363888", size = 3473527, upload-time = "2025-10-23T13:17:20.587Z" },
- { url = "https://files.pythonhosted.org/packages/11/0f/778b4e173be07cc06aae0327389f3cc16bb704f1e0649ae576bf496ac7c8/loro-1.8.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:f47371ef4825b6dabb81d3129427c87029d53e49116dcf0881d0a98787de6c29", size = 3525569, upload-time = "2025-10-23T13:17:52.5Z" },
- { url = "https://files.pythonhosted.org/packages/79/d6/b88fdb0384febd510f45f755fd86c44a36294624ca4f34af3f9619002cdf/loro-1.8.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d188943a32c93fc395e03f89ed8ef422d029f7e74bf6ddb433f12eebf6096369", size = 3420548, upload-time = "2025-10-23T13:18:24.892Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ed/70d45ed31096b5c4bba2164ece3b90508c6e8ef630ded0be5ee860ef41bf/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16a0548b5cd00be5ce4e5eb0b4d26d8cddfb731d4be600d8a50162c8b7612075", size = 3136478, upload-time = "2025-10-23T13:13:50.722Z" },
- { url = "https://files.pythonhosted.org/packages/07/e4/cc888f750ce09b5f97616a9405793770c1783cf528e03df1b02a2be1570e/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d597e5721eac658dba8950debcf937d9d50024a0abf971db8c2267973fca984", size = 3207631, upload-time = "2025-10-23T13:14:19.482Z" },
- { url = "https://files.pythonhosted.org/packages/78/01/1e93b422fa651e7acd26c2cb7f8ea293b6da2b2aa113ffd3cc1610d0e0d0/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1426721d1df4028838622095ce0901a8c5dcdd566a43ffef5772f242e83feaa0", size = 3590222, upload-time = "2025-10-23T13:14:45.901Z" },
- { url = "https://files.pythonhosted.org/packages/50/d2/a92e55b12ee2fa00530e09e6877e7dab8667840ed42c48b33be7898977e3/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887b5c9a1ef32d618d3b95d82e12480993b79275aba5c45886d19ed8d835e0d", size = 3308644, upload-time = "2025-10-23T13:15:12.253Z" },
- { url = "https://files.pythonhosted.org/packages/be/6d/05da74c2676df98206088a139232edee8fad5e4deb1f069364be87e2d841/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54e518630289e471708d4a9809aa777469ee3720820384ece4bfff2d7f5db63d", size = 3196293, upload-time = "2025-10-23T13:16:02.12Z" },
- { url = "https://files.pythonhosted.org/packages/2f/a7/f0817a5f875b050737812bbde75f820fd20759d3be1131da1be64c5b4768/loro-1.8.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4954131893de108c29c0db48c83b8e0b1153dae182004b91d16a4ad0a3c26b10", size = 3545394, upload-time = "2025-10-23T13:15:37.69Z" },
- { url = "https://files.pythonhosted.org/packages/a0/49/38127e71c831d35fbd52c1e957298139635433d48fc969a2bc26a4db924d/loro-1.8.2-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:42bc92f521cdf751a3b1b45909cdc6547a6d5509df38190568f751b51bb4b60a", size = 3314967, upload-time = "2025-10-23T13:16:53.22Z" },
- { url = "https://files.pythonhosted.org/packages/cf/d3/99a5791429b76e89df7823e1ca25745dbb8373b28a80401c6b91a8e4b986/loro-1.8.2-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:4fac34eaf3a75a7b7580a296efce6da46495c6598b280e691dc3e8aa770bbe7b", size = 3472147, upload-time = "2025-10-23T13:17:22.127Z" },
- { url = "https://files.pythonhosted.org/packages/12/3e/d8fc2e59fbf46cc1e24dfe20319ecf0b9dcfda020e361aa0ab0941ccdc20/loro-1.8.2-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:efabcd50bd8c6bacd4a888cc07d1dc77e709db3795b5ce4217507ab8ef37bce2", size = 3524577, upload-time = "2025-10-23T13:17:53.767Z" },
- { url = "https://files.pythonhosted.org/packages/b1/aa/55e78b50eb5311e23609fb8b47de699e3bd9f1160f3605050717f451676d/loro-1.8.2-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:b276098b4fd631f5ac947802f4624bb86029c26f84743dee3d3f1e75abdd45eb", size = 3419500, upload-time = "2025-10-23T13:18:27.45Z" },
+version = "1.10.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/27/ea6f3298fc87ea5f2d60ebfbca088e7d9b2ceb3993f67c83bfb81778ec01/loro-1.10.3.tar.gz", hash = "sha256:68184ab1c2ab94af6ad4aaba416d22f579cabee0b26cbb09a1f67858207bbce8", size = 68833, upload-time = "2025-12-09T10:14:06.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/af/517956be7153d3450263f35ca70b1d7845b404e197045274db07b869e26f/loro-1.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7e7e3461439c57efaadfd364a5a504a849653cf408c97086033004dffb3f2857", size = 3258650, upload-time = "2025-12-09T10:11:29.657Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/a4/8a44499630922af97359971ab01738f568319cbfa5045830eda7393cc758/loro-1.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed91dae34236f888c357b367d37b050ac4fa21ff30ab0231122f580ca87f46ba", size = 3061526, upload-time = "2025-12-09T10:11:14.823Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/93/2088ca72f21fbf59bd31a847a6fd989038dcf4179166e829631482410336/loro-1.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d417a99bae161ecb1250f3272a80c87f2ae546dfb705cadac3ebbc623b7382", size = 3287817, upload-time = "2025-12-09T10:08:11.002Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/72/136fbb2077a0fc92f97e94dc88f48bf515fab034b218d007afcede08eed5/loro-1.10.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a9b821925c9051ee2653a519a99b1d2fc1177a4bac1f02b1f8eaec491f6d43b", size = 3349471, upload-time = "2025-12-09T10:08:45.441Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ab/6b484590ffcb2997a5f163ff26641c8ea9738cacb883f4aa3669dd720433/loro-1.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ee8982a6b82660165e516932cda0e5fd7065023f35ae5e2d17562cf14969e87", size = 3708083, upload-time = "2025-12-09T10:09:23.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7f/b44b0a6228d8f2aad70d8d93c4dc29d72ff4da223cd054c56dbdde9cada5/loro-1.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd391a27550dcf837c82d8ae4e420b4d3b16bdc5a698c3862540803a16bf52dd", size = 3416777, upload-time = "2025-12-09T10:09:57.794Z" },
+ { url = "https://files.pythonhosted.org/packages/53/ad/df58cc6c7168fa4859ba16a447131a0212a07b68fa0250898be132fef365/loro-1.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e74235d480c6e9b362c6f2265a7d28dd848e6a6142a3c9d0831b82cf3776efee", size = 3347414, upload-time = "2025-12-09T10:10:51.95Z" },
+ { url = "https://files.pythonhosted.org/packages/78/90/3d5bb124d4d333824779fd09b25026876b9670c09e5a384760abc7bc863a/loro-1.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df7baf726db4e82f411f7a0454500047812f41bef9552109cb738b8f6ee89c9f", size = 3688343, upload-time = "2025-12-09T10:10:30.393Z" },
+ { url = "https://files.pythonhosted.org/packages/74/01/c78b11ef4ecdbffb1236cdf2f010f89b4a9ad77554e67513aa88cd2280f4/loro-1.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:112d5eeaf76ca6dfbe811e6f6d18649ceeb7697626288ed1185bd1a7d4aae182", size = 3468739, upload-time = "2025-12-09T10:11:46.654Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/26/27123477c458c7e2f26da58d346efab87bb1dbf8f082ed3663cdb8b87581/loro-1.10.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a2cbc231a07f11b82099b76386b1e5659687f4415d6f111699bbd4f291c945a4", size = 3618995, upload-time = "2025-12-09T10:12:22.466Z" },
+ { url = "https://files.pythonhosted.org/packages/15/de/41d21b38d55685715ae6dd7c390dcd29521669ee7e7b8246e6cec71f480d/loro-1.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dbf31ae00bae9c76a4429f73cec3fb3000f1b4d41603244793c660e17747ce1f", size = 3666508, upload-time = "2025-12-09T10:12:57.538Z" },
+ { url = "https://files.pythonhosted.org/packages/38/94/4a8016e5d6400994a82834369aabfaa40cfb62b1f8f40c17bfc3e76ecff7/loro-1.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98d8855a94e2123dab0e40fb5ac7760edbb9b87cd4b29608327899874721ed0b", size = 3558656, upload-time = "2025-12-09T10:13:32.685Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f7/85bb7f6c953b078d74bbb0ec9bb161482c27dde49ed979ddea55c40aafd8/loro-1.10.3-cp310-cp310-win32.whl", hash = "sha256:b539f86cf5e44ad7eefd05772ec637985fddd31137deadca508cd8f3bad211a9", size = 2722340, upload-time = "2025-12-09T10:14:25.47Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/94/d7ef82e9698671f7529ba56b447b546312edcb40dadd4c71af25ea499033/loro-1.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:a5da9963be9a323424695c04d9be836577705077a359d1bb4cabd43963ed2600", size = 2952931, upload-time = "2025-12-09T10:14:07.521Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/bb/61f36aac7981f84ffba922ac1220505365df3e064bc91c015790bff92007/loro-1.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7ee0e1c9a6d0e4a1df4f1847d3b31cef8088860c1193442f131936d084bd3fe1", size = 3254532, upload-time = "2025-12-09T10:11:31.215Z" },
+ { url = "https://files.pythonhosted.org/packages/15/28/5708da252eb6be90131338b104e5030c9b815c41f9e97647391206bec092/loro-1.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7225471b29a892a10589d7cf59c70b0e4de502fa20da675e9aaa1060c7703ae", size = 3055231, upload-time = "2025-12-09T10:11:16.111Z" },
+ { url = "https://files.pythonhosted.org/packages/16/b6/68c350a39fd96f24c55221f883230aa83db0bb5f5d8e9776ccdb25ea1f7b/loro-1.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc04a714e0a604e191279501fa4d2db3b39cee112275f31e87d95ecfbafdfb6c", size = 3286945, upload-time = "2025-12-09T10:08:12.633Z" },
+ { url = "https://files.pythonhosted.org/packages/23/af/8245b8a20046423e035cd17de9811ab1b27fc9e73425394c34387b41cc13/loro-1.10.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:375c888a4ddf758b034eb6ebd093348547d17364fae72aa7459d1358e4843b1f", size = 3349533, upload-time = "2025-12-09T10:08:46.754Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8c/d764c60914e45a2b8c562e01792172e3991430103c019cc129d56c24c868/loro-1.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2020d9384a426e91a7d38c9d0befd42e8ad40557892ed50d47aad79f8d92b654", size = 3704622, upload-time = "2025-12-09T10:09:25.068Z" },
+ { url = "https://files.pythonhosted.org/packages/54/cc/ebdbdf0b1c7a223fe84fc0de78678904ed6424b426f90b98503b95b1dff9/loro-1.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95afacd832dce152700c2bc643f7feb27d5611fc97b5141684b5831b22845380", size = 3416659, upload-time = "2025-12-09T10:09:59.107Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/bc/db7f3fc619483b60c03d85b4f9bb5812b2229865b574c8802b46a578f545/loro-1.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c95868bcf6361d700e215f33a88b8f51d7bc3ae7bbe3d35998148932e23d3fa", size = 3345007, upload-time = "2025-12-09T10:10:53.327Z" },
+ { url = "https://files.pythonhosted.org/packages/91/65/bcd3b1d3a3615e679177c1256f2e0ff7ee242c3d5d1b9cb725b0ec165b51/loro-1.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68f5c7fad09d8937ef4b55e7dd4a0f9f175f026369b3f55a5b054d3513f6846d", size = 3687874, upload-time = "2025-12-09T10:10:31.674Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/e4/0d51e2da2ae6143bfd03f7127b9daf58a3f8dae9d5ca7740ccba63a04de4/loro-1.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:740bb548139d71eccd6317f3df40a0dc5312e98bbb2be09a6e4aaddcaf764206", size = 3467200, upload-time = "2025-12-09T10:11:47.994Z" },
+ { url = "https://files.pythonhosted.org/packages/06/99/ada2baeaf6496e34962fe350cd41129e583219bf4ce5e680c37baa0613a8/loro-1.10.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c756a6ee37ed851e9cf91e5fedbc68ca21e05969c4e2ec6531c15419a4649b58", size = 3618468, upload-time = "2025-12-09T10:12:24.182Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ec/83335935959c5e3946e02b748af71d801412b2aa3876f870beae1cd56d4d/loro-1.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3553390518e188c055b56bcbae76bf038329f9c3458cb1d69068c55b3f8f49f1", size = 3666852, upload-time = "2025-12-09T10:12:59.117Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/53/1bd455b3254afa35638d617e06c65a22e604b1fae2f494abb9a621c8e69b/loro-1.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0885388c0c2b53f5140229921bd64c7838827e3101a05d4d53346191ba76b15d", size = 3556829, upload-time = "2025-12-09T10:13:34.002Z" },
+ { url = "https://files.pythonhosted.org/packages/66/30/6f48726ef50f911751c6b69d7fa81482cac70d4ed817216f846776fec28c/loro-1.10.3-cp311-cp311-win32.whl", hash = "sha256:764b68c4ff0411399c9cf936d8b6db1161ec445388ff2944a25bbdeb2bbac15c", size = 2723776, upload-time = "2025-12-09T10:14:27.261Z" },
+ { url = "https://files.pythonhosted.org/packages/69/39/0b08203d94a6f200bbfefa8025a1b825c8cfb30e8cc8b2a1224629150d08/loro-1.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:9e583e6aabd6f9b2bdf3ff3f6e0de10c3f7f8ab9d4c05c01a9ecca309c969017", size = 2950529, upload-time = "2025-12-09T10:14:08.857Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/b6/cfbf8088e8ca07d66e6c1eccde42e00bd61708f28e8ea0936f9582306323/loro-1.10.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:028948b48dcc5c2127f974dae4ad466ab69f0d1eeaf367a8145eb6501fb988f2", size = 3239592, upload-time = "2025-12-09T10:11:32.505Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e4/7b614260bf16c5e33c0bea6ac47ab0284efd21f89f2e5e4e15cd93bead40/loro-1.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5253b8f436d90412b373c583f22ac9539cfb495bf88f78d4bb41daafef0830b7", size = 3045107, upload-time = "2025-12-09T10:11:17.481Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/17/0a78ec341ca69d376629ff2a1b9b3511ee7dd54f2b018616ef03328024f7/loro-1.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14be8a5539d49468c94d65742355dbe79745123d78bf769a23e53bf9b60dd46a", size = 3292720, upload-time = "2025-12-09T10:08:14.027Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/9b/f36a4654508e9b8ddbe08a62a0ce8b8e7fd511a39b161821917530cffd8e/loro-1.10.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91b2b9139dfc5314a0197132a53b6673fddb63738380a522d12a05cec7ad76b4", size = 3353260, upload-time = "2025-12-09T10:08:48.251Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/0e/7d441ddecc7695153dbe68af4067d62e8d7607fce3747a184878456a91f6/loro-1.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:247897288911c712ee7746965573299fc23ce091e94456da8da371e6adae30f4", size = 3712354, upload-time = "2025-12-09T10:09:26.38Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/33/10e66bb84599e61df124f76c00c5398eb59cbb6f69755f81c40f65a18344/loro-1.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:835abc6025eb5b6a0fe22c808472affc95e9a661b212400cfd88ba186b0d304c", size = 3422926, upload-time = "2025-12-09T10:10:00.347Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/70/00dc4246d9f3c69ecbb9bc36d5ad1a359884464a44711c665cb0afb1e9de/loro-1.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e660853617fc29e71bb7b796e6f2c21f7722c215f593a89e95cd4d8d5a32aca0", size = 3353092, upload-time = "2025-12-09T10:10:55.786Z" },
+ { url = "https://files.pythonhosted.org/packages/19/37/60cc0353c5702e1e469b5d49d1762e782af5d5bd5e7c4e8c47556335b4c6/loro-1.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8059063cab57ca521012ed315a454784c20b0a86653e9014795e804e0a333659", size = 3687798, upload-time = "2025-12-09T10:10:33.253Z" },
+ { url = "https://files.pythonhosted.org/packages/88/c4/4db1887eb08dfbb305d9424fdf1004c0edf147fd53ab0aaf64a90450567a/loro-1.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9748359343b5fd7019ab3c2d1d583a0c13c633a4dd21d75e50e3815ab479f493", size = 3474451, upload-time = "2025-12-09T10:11:49.489Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/66/10d2e00c43b05f56e96e62100f86a1261f8bbd6422605907f118a752fe61/loro-1.10.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:def7c9c2e16ad5470c9c56f096ac649dd4cd42d5936a32bb0817509a92d82467", size = 3621647, upload-time = "2025-12-09T10:12:25.536Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f0/ef8cd6654b09a03684195c650b1fba00f42791fa4844ea400d94030c5615/loro-1.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:34b223fab58591a823f439d9a13d1a1ddac18dc4316866503c588ae8a9147cb1", size = 3667946, upload-time = "2025-12-09T10:13:00.711Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/5d/960b62bf85c38d6098ea067438f037a761958f3a17ba674db0cf316b0f60/loro-1.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d5fa4baceb248d771897b76d1426c7656176e82e770f6790940bc3e3812436d", size = 3565866, upload-time = "2025-12-09T10:13:35.401Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d4/0d499a5e00df13ce497263aef2494d9de9e9d1f11d8ab68f89328203befb/loro-1.10.3-cp312-cp312-win32.whl", hash = "sha256:f25ab769b84a5fbeb1f9a1111f5d28927eaeaa8f5d2d871e237f80eaca5c684e", size = 2720785, upload-time = "2025-12-09T10:14:28.79Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/9b/2b5be23f1da4cf20c6ce213cfffc66bdab2ea012595abc9e3383103793d0/loro-1.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:3b73b7a3a32e60c3424fc7deaf8b127af7580948e27d8bbe749e3f43508aa0a2", size = 2954650, upload-time = "2025-12-09T10:14:10.235Z" },
+ { url = "https://files.pythonhosted.org/packages/75/67/8467cc1c119149ada86903b67ce10fc4b47fb6eb2a8ca5f94c0938fd010f/loro-1.10.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:380ef692c5272e8b607be2ee6a8eef5113e65dc38e6739526c30e3db6abc3fbc", size = 3239527, upload-time = "2025-12-09T10:11:33.884Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/3b/d1a01af3446cb98890349215bea7e71ba49dc3e50ffbfb90c5649657a8b8/loro-1.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed966ce6ff1fb3787b3f6c4ed6dd036baa5fb738b84a466a5e764f2ab534ccc2", size = 3044767, upload-time = "2025-12-09T10:11:18.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/93/37f891fa46767001ae2518697fb01fc187497e3a5238fe28102be626055d/loro-1.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d7c8d2f3d88578fdf69845a9ae16fc5ea3ac54aa838a6bf43a24ce11908220", size = 3292648, upload-time = "2025-12-09T10:08:15.404Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/67/82273eeba2416b0410595071eda1eefcdf4072c014d44d2501b660aa7145/loro-1.10.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62283c345bfeedef19c8a6d029cd8830e5d2c20b5fb45975d8a70a8a30a7944b", size = 3353181, upload-time = "2025-12-09T10:08:50.144Z" },
+ { url = "https://files.pythonhosted.org/packages/82/33/894dccf132bece82168dfbe61fad25a13ed89d18f20649f99e87c38f9228/loro-1.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e7e6ae091179fa5f0fca1f8612fde20236ee0a678744bf51ff7d26103ea04f", size = 3712583, upload-time = "2025-12-09T10:09:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/b7/99292729d8b271bcc4bff5faa20b33e4c749173af4c9cb9d34880ae3b4c8/loro-1.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6abc6de4876aa205498cef52a002bc38662fbd8d742351ea0f535479208b8b1c", size = 3421491, upload-time = "2025-12-09T10:10:01.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/fb/188b808ef1d9b6d842d53969b99a16afb1b71f04739150959c8946345d0e/loro-1.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acbbfd24cf28a71bbdad8544852e9bbba0ba8535f8221f8859b2693555fa8356", size = 3352623, upload-time = "2025-12-09T10:10:57.361Z" },
+ { url = "https://files.pythonhosted.org/packages/53/cc/e2d008cc24bddcf05d1a15b8907a73b1731921ab40897f73a3385fdd274a/loro-1.10.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5faf4ebbe8ca39605024f16dbbbde354365f4e2dcfda82c753797461b504bbd3", size = 3687687, upload-time = "2025-12-09T10:10:34.453Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b6/4251822674230027103caa4fd46a1e83c4d676500074e7ab297468bf8f40/loro-1.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e049c21b292c4ff992b23a98812840735db84620721c10ae7f047a921202d090", size = 3474316, upload-time = "2025-12-09T10:11:51.207Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/54/ecff3ec08d814f3b9ec1c78a14ecf2e7ff132a71b8520f6aa6ad1ace0056/loro-1.10.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:20e8dacfb827c1f7ffb73e127029d7995a9ab2c3b7b7bc3ecc91d22ee32d78d0", size = 3622069, upload-time = "2025-12-09T10:12:27.059Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/84/c1b8251000f46df5f4d043af8c711bdbff9818727d26429378e0f3a5115e/loro-1.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1b743c1c4f93f5b4f0e12efbb352d26e9f80bcbf20f45d9c70f3d0b522f42060", size = 3667722, upload-time = "2025-12-09T10:13:02.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/13/c5c02776f4ad52c6361b95e1d7396c29071533cef45e3861a2e35745be27/loro-1.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:446d67bc9e28036a5a5e03526d28a1559ef2a47b3ccad6b07820dae123cc3697", size = 3564952, upload-time = "2025-12-09T10:13:37.227Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/f1/63d4bc63a1521a9b577f6d13538ec4790865584fdf87569d5af943792406/loro-1.10.3-cp313-cp313-win32.whl", hash = "sha256:45d7d8ec683599897695bb714771baccabc1b4c4a412283cc39787c7a59f7ff0", size = 2720952, upload-time = "2025-12-09T10:14:30.17Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3c/65c8b0b7f96c9b4fbd458867cf91f30fcd58ac25449d8ba9303586061671/loro-1.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:a42bf73b99b07fed11b65feb0a5362b33b19de098f2235848687f4c41204830e", size = 2953768, upload-time = "2025-12-09T10:14:11.965Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/e9/f6a242f61aa4d8b56bd11fa467be27d416401d89cc3244b58651a3a44c88/loro-1.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4866325b154aeebcd34be106c7597acf150c374481ac3c12035a1af715ac0f01", size = 3289791, upload-time = "2025-12-09T10:08:16.926Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/81/8f5f4d6805658c654264e99467f3f46facdbb2062cbf86743768ee4b942a/loro-1.10.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea7b8849660a28ce8cd90a82db4f76c23453836fcbc88f5767feaaf8739045e2", size = 3348007, upload-time = "2025-12-09T10:08:53.305Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/15/bba0fad18ec5561a140e9781fd2b38672210b52e847d207c57ae85379efd/loro-1.10.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e82cdaf9a5892557d3167e07ed5093f87dfa31ef860a63b0eac6c0c2f435705", size = 3707937, upload-time = "2025-12-09T10:09:29.165Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b2/5519c92bd4f9cde068dc60ba35d7f3e4f8cce41e7bf39febd4fb08908e97/loro-1.10.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7ee99e5dc844fb20fca830906a0d721022ad1c37aad0b1a440c4ecb98d0c02f", size = 3416744, upload-time = "2025-12-09T10:10:02.956Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ba/92d97c27582c0ce12bb83df19b9e080c0dfe95068966296a4fa2279c0477/loro-1.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:153c297672ad98d0fe6ff8985decf1e64528ad1dd01ae1452bb83bdeb31f858f", size = 3470978, upload-time = "2025-12-09T10:11:52.707Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8b/acb39b0e74af1c317d3121e75a4bc5bc77d7fda5a79c60399746486f60d9/loro-1.10.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0ed72f8c6a5f521252ee726954055339abba3fcf00404fb4b5c2da168f0cce79", size = 3615039, upload-time = "2025-12-09T10:12:28.631Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c3/154e3361e5ef42012f6842dbd93f8fbace6eec06517b5a4a9f8c4a46e873/loro-1.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f612ab17acdac16c0139e63ff45b33175ebfb22e61a60eb7929a4583389348d6", size = 3663731, upload-time = "2025-12-09T10:13:03.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/dd/a283cf5b1c957e0bbc67503a10e17606a8f8c87f51d3cf3d83dc3a0ac88a/loro-1.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f2741db05c79f3618c954bac90f4572d28c01c243884453f379e9a8738f93d81", size = 3558807, upload-time = "2025-12-09T10:13:38.926Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/4a/a5340b6fdf4cd34d758bed23bd1f64063b3b1b41ff4ecc94ee39259ee9a7/loro-1.10.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:623cf7df17626aa55bc6ca54e89177dbe71a5f1c293e102d6153f43991a1a041", size = 3213589, upload-time = "2025-12-09T10:11:35.377Z" },
+ { url = "https://files.pythonhosted.org/packages/00/93/5164e93a77e365a92def77c1258386daef233516a29fb674a3b9d973b8b8/loro-1.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d8e715d475f32a1462969aca27eeb3f998f309182978f55bc37ce5c515d92e90", size = 3029557, upload-time = "2025-12-09T10:11:20.076Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/30/94592d7c01f480ce99e1783b0d9203eb20ba2eab42575dabd384e3c9d1fa/loro-1.10.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e012a80e8c9fe248b9d0a76e91664c9479a72d976eaeed78f87b15b5d1d732", size = 3282335, upload-time = "2025-12-09T10:08:18.168Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a8/7ae3c0b955aa638fa7dbd2d194c7759749a0d0d96a94805d5dec9b30eaea/loro-1.10.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:686ece56756acbaf80c986848915e9126a29a06d7a62209747e3ef1efc0bd8f6", size = 3333071, upload-time = "2025-12-09T10:08:55.314Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/10/151edebdb2bca626ad50911b761164ced16984b25b0b37b34b674ded8b29/loro-1.10.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aa821c8871deca98f4605eb0c40fb26bcf82bd29c9e7fa33b183516c5395b11", size = 3698226, upload-time = "2025-12-09T10:09:30.474Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/ac/02a490e38466506b1003df4910d2a8ae582265023dae9e2217c98b56ea3f/loro-1.10.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:507d34137adb4148f79e1da7f89a21a4aab18565621a5dc2b389773fe98ac25b", size = 3407322, upload-time = "2025-12-09T10:10:04.199Z" },
+ { url = "https://files.pythonhosted.org/packages/81/db/da51f2bcad81ca3733bc21e83f3b6752446436b565b90f5c350ad227ad01/loro-1.10.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91d3b2e187ccfe2b14118a6e5617266fedcdf3435f6fa0a3db7b4afce8afa687", size = 3330268, upload-time = "2025-12-09T10:10:58.61Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/af/50d136c83d504a3a1f4ad33a6bf38b6933985a82741302255cf446a5f7ad/loro-1.10.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0016f834fd1626710081334400aed8494380b55ef131f7133d21c3bd22d892a", size = 3673582, upload-time = "2025-12-09T10:10:35.849Z" },
+ { url = "https://files.pythonhosted.org/packages/63/4d/53288aae777218e05c43af9c080652bcdbbc8d97c031607eedd3fc15617d/loro-1.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71c4275dca5a8a86219d60545d4f60e081b4af44b490ac912c0481906934bfc6", size = 3463731, upload-time = "2025-12-09T10:11:54.102Z" },
+ { url = "https://files.pythonhosted.org/packages/75/01/2389f26ffe8bc3ffe48a0a578f610dd49c709bbcf0d5d2642c6e2b52f490/loro-1.10.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:490f12571b2ed1a8eaf1edd3a7fffc55adac5010b1875fe1bb9e9af9a3907c38", size = 3602334, upload-time = "2025-12-09T10:12:30.082Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/16/07b64af13f5fcea025e003ca27bbd6f748217abbd4803dad88ea0900526c/loro-1.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a374a43cadaa48528a5411496481df9ae52bf01e513f4509e37d6c986f199c0e", size = 3657896, upload-time = "2025-12-09T10:13:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/4050770d7675ceced71651fe76971d5c27456b7098c0de03a4ecdbb0a02d/loro-1.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1a93b2ee59f1fa8d98dd552211fd5693551893b34c1dd2ba0324806d6d14022f", size = 3544339, upload-time = "2025-12-09T10:13:40.396Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/21/67e27cb404c968fc19a841d5c6277f13a17c69a56f49e3c15ea1c92a28eb/loro-1.10.3-cp314-cp314-win32.whl", hash = "sha256:baa863e3d869422e3320e822c0b1f87f5dc44cda903d1bd3b7a16f8413ce3d92", size = 2706731, upload-time = "2025-12-09T10:14:31.604Z" },
+ { url = "https://files.pythonhosted.org/packages/08/54/6770cf36aeb994489375e9ab9c01201e70ab7cc286fa97e907aa41b1bae6/loro-1.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:f10ed3ca89485f942b8b2de796ed9783edb990e7e570605232de77489e9f3548", size = 2933563, upload-time = "2025-12-09T10:14:13.805Z" },
+ { url = "https://files.pythonhosted.org/packages/24/f5/eb089fd25eb428709dbe79fd4d36b82a00572aa54badd1dff62511a38fe3/loro-1.10.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b4d049efb1953aebfc16fa0b445ff5a37d4d08a1ab93f3b5a577a454b7a5ded", size = 3282369, upload-time = "2025-12-09T10:08:20.011Z" },
+ { url = "https://files.pythonhosted.org/packages/30/d7/692cb87c908f6a8af6cbfc10ebab69e16780e3796e11454c2b481b5c3817/loro-1.10.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56ecad7fbac58aa8bee52bb261a764aeef6c7b39c20f0d69e8fad908ab2ca7d8", size = 3332530, upload-time = "2025-12-09T10:08:57.07Z" },
+ { url = "https://files.pythonhosted.org/packages/54/46/ed3afbf749288b6f70f3b859a6762538818bf6a557ca873b07d6b036946b/loro-1.10.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8d1be349d08b3a95592c6a17b80b1ea6aef892b1b8e2b93b540062d04e34e0", size = 3702599, upload-time = "2025-12-09T10:09:31.779Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/30/6cb616939c12bfe96a71a01a6e3551febf1c34bf9de114fafadbcfb65064/loro-1.10.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ec0a0b9bc4e32c46f14710062ec5b536c72110318aaf85632a4f8b37e9a470a", size = 3404412, upload-time = "2025-12-09T10:10:05.448Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a2/3d4006d3333589f9158ac6d403979bf5c985be8b461b18e7a2ea23b05414/loro-1.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5d4437987f7a4a4ff5927f39d0f43ded5b34295dfb0a3c8e150687e25c3d6b8", size = 3462948, upload-time = "2025-12-09T10:11:55.405Z" },
+ { url = "https://files.pythonhosted.org/packages/41/30/c640ccd3e570b08770a9f459decc2d8e7ceefdc34ac28a745418fb9cb5ba/loro-1.10.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:86d4f0c631ca274ad2fa2c0bdb8e1e141882d94339b7284a8bef5bf73fa6957d", size = 3599851, upload-time = "2025-12-09T10:12:31.759Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8f/062ea50554c47ae30e98b1f0442a458c0edecc6d4edc7fcfc4d901734dd0/loro-1.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:15e03084ff1b472e14623183ed6e1e43e0f717c2112697beda5e69b5bd0ff236", size = 3655558, upload-time = "2025-12-09T10:13:06.529Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/c7dd8cdbd57454b23d89799c22cd42b6d2dda283cd87d7b198dc424a462c/loro-1.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42d6a5ce5bc518eaa682413e82d597299650eeb03e8bc39341752d6e0d22503e", size = 3541282, upload-time = "2025-12-09T10:13:42.189Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/12/0ec38fe0a1fa6b8e76989bbbbf22bdd34f8824ce6934c97f94ca50dba49c/loro-1.10.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55214615c1cb9f727a5278f5e57b9660743e7d095e08899e8936f174a45471b9", size = 3284859, upload-time = "2025-12-09T10:08:24.621Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/26/c01691a85fe1047dcc0398054124069af92b8ce1602eaabbe9b7e0fac1f1/loro-1.10.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10591fa32dc628f770da472beac7544d2ba16a3a22d590211364331c5871b9f6", size = 3349886, upload-time = "2025-12-09T10:09:01.286Z" },
+ { url = "https://files.pythonhosted.org/packages/53/35/3fcd13a2ae7686b467b5210b991e1682576a4be7e121bc9f8690c3d59929/loro-1.10.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f18df6892097603e5bd2e149384d4bcb996be8a3b6ba10d3da74bce39e1d5093", size = 3703226, upload-time = "2025-12-09T10:09:36.464Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/47/52ce515ac76893f57ed071bb1d5cd3687a059cf1e81e66535364b9581ed6/loro-1.10.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a8911b8cd97652a04e22481dd90b3c8d286f12c8d8286a4e34a655835dd6506", size = 3413121, upload-time = "2025-12-09T10:10:09.528Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/1c/39f39e731d3af9c387e4238bd8da8e545e16922524bc0bca991d3ce475e1/loro-1.10.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:73d5737c95bccf725950555c51374e5823c9be16bfc5496d8c1fafb2bb04690f", size = 3466280, upload-time = "2025-12-09T10:11:59.889Z" },
+ { url = "https://files.pythonhosted.org/packages/54/f9/b85b76b882f1e62da461552157b061dd79c52c59afd8074969f04fb32a2c/loro-1.10.3-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fa875a691556daaedb639dc920ee9c3743745eea2aa4c7fd914841e31b92c556", size = 3617971, upload-time = "2025-12-09T10:12:36.704Z" },
+ { url = "https://files.pythonhosted.org/packages/90/1a/ef79aa94144453157bc139e341b983640fcda70bf2e8fdc6120773f210a0/loro-1.10.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e7ddfd247fa3ae3c05d38019fb1424a903ea98e5730a10105081f5f7dc08f9c1", size = 3663111, upload-time = "2025-12-09T10:13:11.173Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b4/ca47f1b4b926a4b0dd3a7d7f0edd46a63e74b64c2b628c0463f25f0f1dd2/loro-1.10.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:033a456647d487d61af82ea96aff95a789a3776441ea8af86556f2877867530d", size = 3554651, upload-time = "2025-12-09T10:13:46.496Z" },
+ { url = "https://files.pythonhosted.org/packages/43/1a/49e864102721e0e15a4e4c56d7f2dddad5cd589c2d0aceafe14990513583/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ca42e991589ea300b59da9e98940d5ddda76275fe4363b1f1e079d244403a1", size = 3284236, upload-time = "2025-12-09T10:08:25.836Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/c6/d46b433105d8002e4c90248c07f00cd2c8ea76f1048cc5f35b733be96723/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9ca16dae359397aa7772891bb3967939ffda8da26e0b392d331b506e16afc78", size = 3348996, upload-time = "2025-12-09T10:09:03.951Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f3/e918c7b396c547b22a7ab3cff1b570c5ce94293f0dcb17cd96cbe6ba2d50/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87cfc0a6e119c1c8cfa93078f5d012e557c6b75edcd0977da58ec46d28dc242", size = 3701875, upload-time = "2025-12-09T10:09:37.924Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/67/140ecb65b4f436099ad674fbe7502378156f43b737cb43f5fd76c42a0da8/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4541ed987306c51e718f51196fd2b2d05e87b323da5d850b37900d2e8ac6aae6", size = 3412283, upload-time = "2025-12-09T10:10:10.946Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/93/b7b41cf8b3e591b7191494e12be24cbb101f137fe82f0a24ed7934bbacf3/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce0b0a500e08b190038380d4593efcb33c98ed4282cc8347ca6ce55d05cbdf6e", size = 3340580, upload-time = "2025-12-09T10:11:02.956Z" },
+ { url = "https://files.pythonhosted.org/packages/94/19/fdc9ea9ce6510147460200c90164a84c22b0cc9e33f7dd5c0d5f76484314/loro-1.10.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:987dbcb42b4b8d2c799660a6d8942e53ae346f51d51c9ad7ef5d7e640422fe4a", size = 3680924, upload-time = "2025-12-09T10:10:39.877Z" },
+ { url = "https://files.pythonhosted.org/packages/40/61/548491499394fe02e7451b0d7367f7eeed32f0f6dd8f1826be8b4c329f28/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f876d477cb38c6c623c4ccb5dc4b7041dbeff04167bf9c19fa461d57a3a1b916", size = 3465033, upload-time = "2025-12-09T10:12:03.122Z" },
+ { url = "https://files.pythonhosted.org/packages/26/68/d8bebb6b583fe5a3dc4da32c9070964548e3ca1d524f383c71f9becf4197/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:641c8445bd1e4181b5b28b75a0bc544ef51f065b15746e8714f90e2e029b5202", size = 3616740, upload-time = "2025-12-09T10:12:38.187Z" },
+ { url = "https://files.pythonhosted.org/packages/52/9b/8f8ecc85eb925122a79348eb77ff7109a7ee41ee7d1a282122be2daff378/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a6ab6244472402b8d1f4f77e5210efa44dfa4914423cafcfcbd09232ea8bbff0", size = 3661160, upload-time = "2025-12-09T10:13:12.513Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3c/e884d06859f9a9fc64afd21c426b9d681af0856181c1fe66571a65d35ef7/loro-1.10.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ae4c765671ee7d7618962ec11cb3bb471965d9b88c075166fe383263235d58d6", size = 3553653, upload-time = "2025-12-09T10:13:47.917Z" },
]
[[package]]
name = "marimo"
-version = "0.18.2"
+version = "0.23.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "docutils" },
{ name = "itsdangerous" },
{ name = "jedi" },
- { name = "loro", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
+ { name = "loro" },
{ name = "markdown" },
{ name = "msgspec" },
{ name = "narwhals" },
@@ -1382,15 +1398,16 @@ dependencies = [
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "pyyaml" },
+ { name = "pyzmq", marker = "python_full_version < '3.15'" },
{ name = "starlette" },
{ name = "tomlkit" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "uvicorn" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/06/44/af8dfc8c3d2d1275ea9d16783815b72b30f55698aa48c516f1de352953af/marimo-0.18.2.tar.gz", hash = "sha256:bff066b714e758872897a6faeb813ed6ee73c881d0176c02ce08f97c6d8408dc", size = 37842506, upload-time = "2025-12-04T01:08:49.843Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/3f/7fb38c6c2a1f8d6b3c3ffb8ca6db5ff0b9dacbb113b4d05aa7690b51a771/marimo-0.23.3.tar.gz", hash = "sha256:251a8724b58882d65956ff6a20552cb21e59a6fd4149ca437727894375ec31e9", size = 38406206, upload-time = "2026-04-24T17:56:21.016Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/e0/c93f55a9774d8401d7098c9b3ef400d3a942e3f37b3c25ced19a18f7cc0a/marimo-0.18.2-py3-none-any.whl", hash = "sha256:21e415b0ae862d23e2697f4171761d9eb4674295bd472da3f244f649ce90f142", size = 38366135, upload-time = "2025-12-04T01:08:54.256Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e7/02d672006fb04cb8aef23aeaf0384482fe63a13f9db6125ad8e13146daee/marimo-0.23.3-py3-none-any.whl", hash = "sha256:329b35b9ca221db9c78780d1714b11f010a00e2a929942db8ae6187960d42496", size = 38828150, upload-time = "2026-04-24T17:56:16.204Z" },
]
[[package]]
@@ -2861,15 +2878,15 @@ wheels = [
[[package]]
name = "pymdown-extensions"
-version = "10.17.1"
+version = "10.21.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/a987e4d549c6c82353fce5fa5f650229bb60ea4c0d1684a2714a509aef58/pymdown_extensions-10.17.1.tar.gz", hash = "sha256:60d05fe55e7fb5a1e4740fc575facad20dc6ee3a748e8d3d36ba44142e75ce03", size = 845207, upload-time = "2025-11-11T21:44:58.815Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/40/b2d7b9fdccc63e48ae4dbd363b6b89eb7ac346ea49ed667bb71f92af3021/pymdown_extensions-10.17.1-py3-none-any.whl", hash = "sha256:1f160209c82eecbb5d8a0d8f89a4d9bd6bdcbde9a8537761844cfc57ad5cd8a6", size = 266310, upload-time = "2025-11-11T21:44:56.809Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" },
]
[[package]]
@@ -4033,7 +4050,7 @@ wheels = [
[[package]]
name = "wigglystuff"
-version = "0.3.5"
+version = "0.4.0"
source = { editable = "." }
dependencies = [
{ name = "anywidget" },
@@ -4063,6 +4080,7 @@ neo4j = [
{ name = "neo4j" },
]
test = [
+ { name = "marimo" },
{ name = "matplotlib" },
{ name = "pandas" },
{ name = "polars" },
@@ -4089,7 +4107,8 @@ requires-dist = [
{ name = "black", marker = "extra == 'docs'", specifier = ">=24.8.0" },
{ name = "drawdata" },
{ name = "marimo", marker = "extra == 'docs'", specifier = ">=0.18.0" },
- { name = "marimo", marker = "extra == 'test-browser'", specifier = ">=0.18.0" },
+ { name = "marimo", marker = "extra == 'test'", specifier = ">=0.23.3" },
+ { name = "marimo", marker = "extra == 'test-browser'", specifier = ">=0.23.3" },
{ name = "matplotlib", marker = "extra == 'test'", specifier = ">=3.0" },
{ name = "mike", marker = "extra == 'docs'", specifier = ">=2.1.0" },
{ name = "mkdocs-git-revision-date-localized-plugin", marker = "extra == 'docs'", specifier = ">=1.2.6" },
diff --git a/wigglystuff/env_config.py b/wigglystuff/env_config.py
index d4977eb6..8caf3413 100644
--- a/wigglystuff/env_config.py
+++ b/wigglystuff/env_config.py
@@ -48,7 +48,10 @@ class EnvConfig(anywidget.AnyWidget):
variables = traitlets.List(traitlets.Dict()).tag(sync=True)
all_valid = traitlets.Bool(False).tag(sync=True)
- # JS -> Python: user entered a value
+ # JS -> Python: user submitted a value. Synced (not custom message) so that
+ # marimo's reactive runtime sees the JS->Python state update and re-runs
+ # downstream cells. Cleared back to {} immediately after Python processes it,
+ # so the value never lives in trait state at rest.
_pending_value = traitlets.Dict({}).tag(sync=True)
def __init__(
@@ -76,20 +79,14 @@ def __init__(
self._values: dict[str, str] = {} # Internal storage, never touches os.environ
# Build initial state by checking current environment
- initial_vars = []
- for name in self._var_names:
- value = os.environ.get(name)
- has_validator = variables[name] is not None
- if value is not None:
- self._values[name] = value
- status = self._validate(name, value)
- # Include value for JS to display (browser masks it)
- status["value"] = value
- else:
- status = {"status": "missing", "error": None, "value": ""}
- initial_vars.append(
- {"name": name, "has_validator": has_validator, **status}
- )
+ initial_vars = [
+ {
+ "name": name,
+ "has_validator": self._validators[name] is not None,
+ **self._initial_status(name),
+ }
+ for name in self._var_names
+ ]
super().__init__(
variables=initial_vars,
@@ -108,15 +105,28 @@ def _validate(self, name: str, value: str) -> dict:
except Exception as e:
return {"status": "invalid", "error": str(e)}
+ def _initial_status(self, name: str) -> dict:
+ """Return {status, error} for `name`, recording the value in self._values if present."""
+ value = os.environ.get(name)
+ if value is None:
+ return {"status": "missing", "error": None}
+ self._values[name] = value
+ return self._validate(name, value)
+
@traitlets.observe("_pending_value")
def _on_pending_value(self, change: dict) -> None:
- """Handle value submitted from JavaScript frontend."""
+ """Handle value submitted via the synced fallback trait."""
data = change["new"]
- if not data or "name" not in data:
+ if not data:
return
- name = data["name"]
- value = data["value"]
+ self._submit_value(data.get("name"), data.get("value"))
+ self._pending_value = {}
+
+ def _submit_value(self, name: Any, value: Any) -> None:
+ """Validate and store a submitted value without syncing it back."""
+ if name not in self._var_names or not isinstance(value, str):
+ return
# Run validation (synchronous, no need for "validating" intermediate state)
result = self._validate(name, value)
@@ -125,31 +135,24 @@ def _on_pending_value(self, change: dict) -> None:
# Store internally only - never touch os.environ
self._values[name] = value
- # Update state in one go - pass the entered value so it can be displayed
- self._set_var_status(name, result["status"], result["error"], value)
- self._recalc_all_valid()
+ # Update synced state without echoing the submitted value.
+ self._update_var(name, result["status"], result["error"])
- def _set_var_status(
+ def _update_var(
self,
name: str,
status: str,
error: Optional[str],
- value: Optional[str] = None,
) -> None:
- """Update status for a specific variable."""
+ """Update status for a variable and recompute all_valid."""
vars_copy = [dict(v) for v in self.variables]
for v in vars_copy:
if v["name"] == name:
v["status"] = status
v["error"] = error
- # Include the entered value so JS can display it (even if invalid)
- v["value"] = value if value is not None else self._values.get(name, "")
break
self.variables = vars_copy
-
- def _recalc_all_valid(self) -> None:
- """Recalculate all_valid based on current variable statuses."""
- self.all_valid = all(v["status"] == "valid" for v in self.variables)
+ self.all_valid = all(v["status"] == "valid" for v in vars_copy)
def require_valid(self, variables: Optional[Sequence[str]] = None) -> None:
"""Assert environment variables are valid.
@@ -170,28 +173,21 @@ def require_valid(self, variables: Optional[Sequence[str]] = None) -> None:
f"Variable(s) not configured in this EnvConfig: {', '.join(sorted(unknown))}"
)
- # Filter to only checked variables
- checked_vars = [v for v in self.variables if v["name"] in to_check]
-
- # Early return if all checked vars are valid
- if all(v["status"] == "valid" for v in checked_vars):
- return
-
- missing = [v["name"] for v in checked_vars if v["status"] == "missing"]
+ rows = [v for v in self.variables if v["name"] in to_check]
+ missing = [v["name"] for v in rows if v["status"] == "missing"]
invalid = [
- f"{v['name']} ({v['error']})"
- for v in checked_vars
- if v["status"] == "invalid"
+ f"{v['name']} ({v['error']})" for v in rows if v["status"] == "invalid"
]
+ if not missing and not invalid:
+ return
- msg_parts = []
+ parts = []
if missing:
- msg_parts.append(f"Missing: {', '.join(missing)}")
+ parts.append(f"Missing: {', '.join(missing)}")
if invalid:
- msg_parts.append(f"Invalid: {', '.join(invalid)}")
-
+ parts.append(f"Invalid: {', '.join(invalid)}")
raise EnvironmentError(
- f"Environment configuration incomplete. {'; '.join(msg_parts)}. "
+ f"Environment configuration incomplete. {'; '.join(parts)}. "
"Please set all required variables using the widget above."
)
diff --git a/wigglystuff/static/env-config.js b/wigglystuff/static/env-config.js
index 5f2d1926..4688f817 100644
--- a/wigglystuff/static/env-config.js
+++ b/wigglystuff/static/env-config.js
@@ -13,7 +13,7 @@ function render({ model, el }) {
function buildInitialDOM() {
const variables = model.get("variables");
- variables.forEach((variable, index) => {
+ variables.forEach((variable) => {
const row = document.createElement("div");
row.className = "env-config-row";
row.dataset.status = variable.status;
@@ -35,10 +35,6 @@ function render({ model, el }) {
input.autocomplete = "off";
inputs.push(input);
- // Set initial value/placeholder - show value even for invalid status
- if (variable.status === "valid" || variable.status === "invalid") {
- input.value = variable.value || "";
- }
if (variable.status === "missing") {
input.placeholder = "Enter value...";
}
@@ -53,6 +49,7 @@ function render({ model, el }) {
model.set("_pending_value", {
name: variable.name,
value: value,
+ nonce: Date.now(),
});
model.save_changes();
}
@@ -87,8 +84,13 @@ function render({ model, el }) {
`;
} else if (variable.status === "invalid") {
+ // Escape because variable.error is user content (str of a validator's exception).
+ const safeError = (variable.error || "Invalid")
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/
+
@@ -118,12 +120,8 @@ function render({ model, el }) {
// Update row status (for background color)
row.dataset.status = variable.status;
- // Update input based on status - show value even for invalid status
+ // Update input based on status without syncing secret values back from Python.
if (variable.status === "valid" || variable.status === "invalid") {
- // Only update value if it changed (avoid overwriting while user types)
- if (input.value !== variable.value) {
- input.value = variable.value || "";
- }
input.placeholder = "";
} else {
input.placeholder = "Enter value...";
diff --git a/wigglystuff/static/wandb-chart.js b/wigglystuff/static/wandb-chart.js
deleted file mode 100644
index 87862a79..00000000
--- a/wigglystuff/static/wandb-chart.js
+++ /dev/null
@@ -1,523 +0,0 @@
-// ── Data layer ──────────────────────────────
-
-async function fetchHistory(apiKey, entity, project, runId, key, minStep) {
- const query = `
- query RunSampledHistory($project: String!, $entity: String!, $name: String!, $specs: [JSONString!]!) {
- project(name: $project, entityName: $entity) {
- run(name: $name) {
- sampledHistory(specs: $specs)
- }
- }
- }
- `;
- const specObj = { keys: [key, "_step"], samples: 500 };
- if (minStep > 0) specObj.minStep = minStep;
- const variables = { entity, project, name: runId, specs: [JSON.stringify(specObj)] };
- const creds = btoa("api:" + apiKey);
-
- const resp = await fetch("https://api.wandb.ai/graphql", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Basic " + creds,
- },
- body: JSON.stringify({ query, variables }),
- });
-
- if (!resp.ok) throw new Error("HTTP " + resp.status);
- const data = await resp.json();
- if (data.errors) throw new Error(data.errors.map(e => e.message).join(", "));
- return data.data.project.run.sampledHistory[0] || [];
-}
-
-async function fetchRunState(apiKey, entity, project, runId) {
- const query = `
- query RunState($project: String!, $entity: String!, $name: String!) {
- project(name: $project, entityName: $entity) {
- run(name: $name) {
- state
- }
- }
- }
- `;
- const variables = { entity, project, name: runId };
- const creds = btoa("api:" + apiKey);
-
- const resp = await fetch("https://api.wandb.ai/graphql", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Basic " + creds,
- },
- body: JSON.stringify({ query, variables }),
- });
-
- if (!resp.ok) throw new Error("HTTP " + resp.status);
- const data = await resp.json();
- if (data.errors) throw new Error(data.errors.map(e => e.message).join(", "));
- return data.data.project.run.state;
-}
-
-function rollingMean(points, win) {
- if (win <= 1) return points;
- return points.map((p, i) => {
- const start = Math.max(0, i - win + 1);
- let sum = 0;
- for (let j = start; j <= i; j++) sum += points[j].y;
- return { x: p.x, y: sum / (i - start + 1) };
- });
-}
-
-function exponentialSmooth(points, alpha) {
- if (alpha <= 0 || points.length === 0) return points;
- const out = [{ x: points[0].x, y: points[0].y }];
- for (let i = 1; i < points.length; i++) {
- const prev = out[i - 1].y;
- out.push({ x: points[i].x, y: alpha * prev + (1 - alpha) * points[i].y });
- }
- return out;
-}
-
-function gaussianSmooth(points, sigma) {
- if (sigma <= 0 || points.length === 0) return points;
- const radius = Math.ceil(3 * sigma);
- const kernel = [];
- let kernelSum = 0;
- for (let j = -radius; j <= radius; j++) {
- const w = Math.exp(-0.5 * (j / sigma) * (j / sigma));
- kernel.push(w);
- kernelSum += w;
- }
- for (let j = 0; j < kernel.length; j++) kernel[j] /= kernelSum;
-
- return points.map((p, i) => {
- let sum = 0, wSum = 0;
- for (let j = -radius; j <= radius; j++) {
- const idx = i + j;
- if (idx < 0 || idx >= points.length) continue;
- const w = kernel[j + radius];
- sum += w * points[idx].y;
- wSum += w;
- }
- return { x: p.x, y: sum / wSum };
- });
-}
-
-const SLIDER_CONFIG = {
- rolling: { label: "Rolling mean:", min: 0, max: 50, step: 1, fmt: v => v === 0 ? "off" : String(Math.round(v)), toParam: v => v < 2 ? null : v },
- exponential: { label: "EMA weight:", min: 0, max: 0.99, step: 0.01, fmt: v => v === 0 ? "off" : v.toFixed(2), toParam: v => v === 0 ? null : v },
- gaussian: { label: "Gaussian \u03c3:", min: 0, max: 10, step: 0.1, fmt: v => v === 0 ? "off" : v.toFixed(1), toParam: v => v === 0 ? null : v },
-};
-
-const COLORS = [
- "#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f",
- "#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"
-];
-
-function prepareSeries(runData, runs, key, smoothKind, smoothParam) {
- return runs.map((run, i) => {
- const rd = runData[run.id];
- const color = COLORS[i % COLORS.length];
- if (!rd) return { label: run.label, color, points: [], raw: [] };
- const raw = rd.rows
- .filter(r => r[key] !== undefined && r._step !== undefined)
- .map(r => ({ x: r._step, y: r[key] }));
-
- let smoothed = raw;
- if (smoothParam != null && smoothParam > 0) {
- if (smoothKind === "rolling" && smoothParam >= 2) {
- smoothed = rollingMean(raw, smoothParam);
- } else if (smoothKind === "exponential") {
- smoothed = exponentialSmooth(raw, smoothParam);
- } else if (smoothKind === "gaussian") {
- smoothed = gaussianSmooth(raw, smoothParam);
- }
- }
- const hasSmoothing = smoothed !== raw;
- return {
- label: run.label,
- color,
- points: smoothed,
- raw: hasSmoothing ? raw : [],
- };
- });
-}
-
-// ── Render layer (swap this to use D3, etc.) ─
-
-function drawChart(canvas, series, opts) {
- const { title, width, height } = opts;
- const dpr = window.devicePixelRatio || 1;
- canvas.width = width * dpr;
- canvas.height = height * dpr;
- canvas.style.width = width + "px";
- canvas.style.height = height + "px";
-
- const ctx = canvas.getContext("2d");
- ctx.scale(dpr, dpr);
-
- const pad = { top: 30, right: 120, bottom: 40, left: 60 };
- const plotW = width - pad.left - pad.right;
- const plotH = height - pad.top - pad.bottom;
-
- ctx.clearRect(0, 0, width, height);
-
- // Compute extents across all series
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
- for (const s of series) {
- for (const p of s.points) {
- if (p.x < minX) minX = p.x;
- if (p.x > maxX) maxX = p.x;
- if (p.y < minY) minY = p.y;
- if (p.y > maxY) maxY = p.y;
- }
- }
-
- if (!isFinite(minX)) {
- ctx.fillStyle = "#999";
- ctx.font = "13px system-ui, sans-serif";
- ctx.textAlign = "center";
- ctx.fillText("Waiting for data...", width / 2, height / 2);
- return;
- }
-
- // Pad y range by 5%
- const yPad = (maxY - minY) * 0.05 || 0.1;
- minY -= yPad;
- maxY += yPad;
-
- const scaleX = (v) => pad.left + ((v - minX) / (maxX - minX || 1)) * plotW;
- const scaleY = (v) => pad.top + plotH - ((v - minY) / (maxY - minY || 1)) * plotH;
-
- // Grid
- ctx.strokeStyle = "#e0e0e0";
- ctx.lineWidth = 0.5;
- ctx.setLineDash([4, 4]);
-
- const nY = 5;
- for (let i = 0; i <= nY; i++) {
- const y = scaleY(minY + (i / nY) * (maxY - minY));
- ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + plotW, y); ctx.stroke();
- }
- const nX = 6;
- for (let i = 0; i <= nX; i++) {
- const x = scaleX(minX + (i / nX) * (maxX - minX));
- ctx.beginPath(); ctx.moveTo(x, pad.top); ctx.lineTo(x, pad.top + plotH); ctx.stroke();
- }
- ctx.setLineDash([]);
-
- // Tick labels
- ctx.fillStyle = "#666";
- ctx.font = "11px system-ui, sans-serif";
- ctx.textAlign = "right";
- ctx.textBaseline = "middle";
- for (let i = 0; i <= nY; i++) {
- const v = minY + (i / nY) * (maxY - minY);
- ctx.fillText(v.toPrecision(3), pad.left - 6, scaleY(v));
- }
- ctx.textAlign = "center";
- ctx.textBaseline = "top";
- for (let i = 0; i <= nX; i++) {
- const v = minX + (i / nX) * (maxX - minX);
- ctx.fillText(Math.round(v).toString(), scaleX(v), pad.top + plotH + 6);
- }
-
- // Title
- ctx.fillStyle = "#333";
- ctx.font = "bold 13px system-ui, sans-serif";
- ctx.textAlign = "center";
- ctx.textBaseline = "top";
- ctx.fillText(title, pad.left + plotW / 2, 6);
-
- // X-axis label
- ctx.fillStyle = "#999";
- ctx.font = "11px system-ui, sans-serif";
- ctx.fillText("step", pad.left + plotW / 2, pad.top + plotH + 22);
-
- // Raw data lines (low opacity, behind smoothed)
- for (const s of series) {
- if (s.raw.length === 0) continue;
- ctx.globalAlpha = 0.2;
- ctx.strokeStyle = s.color;
- ctx.lineWidth = 1;
- ctx.beginPath();
- for (let i = 0; i < s.raw.length; i++) {
- const px = scaleX(s.raw[i].x);
- const py = scaleY(s.raw[i].y);
- i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
- }
- ctx.stroke();
- ctx.globalAlpha = 1;
- }
-
- // Polylines (smoothed or raw if no smoothing)
- for (const s of series) {
- if (s.points.length === 0) continue;
- ctx.strokeStyle = s.color;
- ctx.lineWidth = 1.5;
- ctx.beginPath();
- for (let i = 0; i < s.points.length; i++) {
- const px = scaleX(s.points[i].x);
- const py = scaleY(s.points[i].y);
- i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
- }
- ctx.stroke();
- }
-
- // Legend
- let ly = pad.top + 4;
- ctx.font = "11px system-ui, sans-serif";
- ctx.textAlign = "left";
- ctx.textBaseline = "top";
- for (const s of series) {
- ctx.fillStyle = s.color;
- ctx.fillRect(pad.left + plotW + 10, ly + 2, 12, 3);
- ctx.fillStyle = "#333";
- ctx.fillText(s.label, pad.left + plotW + 26, ly);
- ly += 16;
- }
-}
-
-// ── Widget glue ─────────────────────────────
-
-function render({ model, el }) {
- const runData = {};
- const pollSec = model.get("poll_seconds");
- const w = model.get("width") || 700;
-
- const container = document.createElement("div");
- container.style.fontFamily = "system-ui, sans-serif";
- container.style.padding = "12px";
- container.style.width = w + "px";
- container.style.boxSizing = "border-box";
-
- // Controls row: smoothing controls left, refresh button right
- const controlsRow = document.createElement("div");
- controlsRow.style.cssText = "display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;font-size:13px;color:#666";
-
- // Smoothing controls (left side)
- let kind = model.get("smoothing_kind") || "gaussian";
- let cfg = SLIDER_CONFIG[kind] || SLIDER_CONFIG.rolling;
-
- const smoothGroup = document.createElement("div");
- smoothGroup.style.cssText = "display:flex;align-items:center;gap:8px";
-
- // Smoothing kind dropdown
- const kindSelect = document.createElement("select");
- kindSelect.style.cssText = "font-size:12px;padding:1px 4px;border:1px solid #ccc;border-radius:3px;background:#fff;font-family:system-ui,sans-serif";
- for (const [value, label] of [["rolling", "Rolling"], ["exponential", "Exponential"], ["gaussian", "Gaussian"]]) {
- const opt = document.createElement("option");
- opt.value = value;
- opt.textContent = label;
- if (value === kind) opt.selected = true;
- kindSelect.appendChild(opt);
- }
-
- // Slider
- const smoothSlider = document.createElement("input");
- smoothSlider.type = "range";
- smoothSlider.min = String(cfg.min);
- smoothSlider.max = String(cfg.max);
- smoothSlider.step = String(cfg.step);
- const initParam = model.get("smoothing_param") ?? 0;
- smoothSlider.value = String(initParam);
- smoothSlider.style.width = "120px";
- const smoothValEl = document.createElement("span");
- smoothValEl.textContent = cfg.fmt(parseFloat(smoothSlider.value));
-
- function configureSlider() {
- cfg = SLIDER_CONFIG[kind] || SLIDER_CONFIG.rolling;
- smoothSlider.min = String(cfg.min);
- smoothSlider.max = String(cfg.max);
- smoothSlider.step = String(cfg.step);
- smoothSlider.value = "0";
- smoothValEl.textContent = "off";
- }
-
- smoothGroup.appendChild(kindSelect);
- smoothGroup.appendChild(smoothSlider);
- smoothGroup.appendChild(smoothValEl);
- if (model.get("show_slider")) controlsRow.appendChild(smoothGroup);
-
- // Refresh button (right side, manual mode only)
- const refreshBtn = document.createElement("button");
- refreshBtn.textContent = "\u21bb Refresh";
- refreshBtn.style.cssText = "padding:2px 8px;font-size:12px;cursor:pointer;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;font-family:system-ui,sans-serif;margin-left:auto";
- if (pollSec === null) controlsRow.appendChild(refreshBtn);
-
- // Chart canvas + tooltip wrapper
- const chartCanvas = document.createElement("canvas");
- chartCanvas.style.display = "block";
-
- const tooltip = document.createElement("div");
- tooltip.style.cssText = "position:absolute;display:none;background:rgba(0,0,0,0.8);color:#fff;padding:6px 10px;border-radius:4px;font-size:12px;pointer-events:none;white-space:nowrap;z-index:10";
-
- const chartWrapper = document.createElement("div");
- chartWrapper.style.position = "relative";
- chartWrapper.style.display = "inline-block";
- chartWrapper.appendChild(chartCanvas);
- chartWrapper.appendChild(tooltip);
-
- // Status line (below chart)
- const statusEl = document.createElement("div");
- statusEl.style.cssText = "margin-top:4px;color:#666;font-size:12px";
- statusEl.textContent = "Waiting for runs...";
-
- container.appendChild(controlsRow);
- container.appendChild(chartWrapper);
- container.appendChild(statusEl);
- el.appendChild(container);
-
- let lastSeries = [];
-
- function getSliderParam() {
- const v = parseFloat(smoothSlider.value);
- return cfg.toParam(v);
- }
-
- function redraw() {
- const runs = model.get("_runs") || [];
- const key = model.get("key");
- const w = model.get("width") || 700;
- const h = model.get("height") || 300;
- lastSeries = prepareSeries(runData, runs, key, kind, getSliderParam());
- drawChart(chartCanvas, lastSeries, { title: key, width: w, height: h });
- }
-
- kindSelect.addEventListener("change", () => {
- kind = kindSelect.value;
- model.set("smoothing_kind", kind);
- configureSlider();
- model.set("smoothing_param", null);
- model.save_changes();
- redraw();
- });
-
- smoothSlider.addEventListener("input", () => {
- let v = parseFloat(smoothSlider.value);
- if (kind === "rolling" && v === 1) { smoothSlider.value = "2"; v = 2; }
- smoothValEl.textContent = cfg.fmt(v);
- model.set("smoothing_param", cfg.toParam(v));
- model.save_changes();
- redraw();
- });
-
- // Hover tooltip
- chartCanvas.addEventListener("mousemove", (e) => {
- if (lastSeries.length === 0) { tooltip.style.display = "none"; return; }
- const rect = chartCanvas.getBoundingClientRect();
- const mx = e.clientX - rect.left;
- const my = e.clientY - rect.top;
- const w = model.get("width") || 700;
- const h = model.get("height") || 300;
- const pad = { top: 30, right: 120, bottom: 40, left: 60 };
-
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
- for (const s of lastSeries) {
- for (const p of s.points) {
- if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x;
- if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y;
- }
- }
- if (!isFinite(minX)) return;
- const yPad = (maxY - minY) * 0.05 || 0.1;
- minY -= yPad; maxY += yPad;
- const plotW = w - pad.left - pad.right;
- const plotH = h - pad.top - pad.bottom;
- const sx = (v) => pad.left + ((v - minX) / (maxX - minX || 1)) * plotW;
- const sy = (v) => pad.top + plotH - ((v - minY) / (maxY - minY || 1)) * plotH;
-
- let bestDist = 25, bestS = null, bestP = null;
- for (const s of lastSeries) {
- for (const p of s.points) {
- const dx = sx(p.x) - mx, dy = sy(p.y) - my;
- const d = Math.sqrt(dx * dx + dy * dy);
- if (d < bestDist) { bestDist = d; bestS = s; bestP = p; }
- }
- }
-
- if (bestP) {
- tooltip.style.display = "block";
- tooltip.style.left = (sx(bestP.x) + 12) + "px";
- tooltip.style.top = (sy(bestP.y) - 10) + "px";
- tooltip.textContent = bestS.label + " | step " + bestP.x + " | " + bestP.y.toFixed(4);
- } else {
- tooltip.style.display = "none";
- }
- });
- chartCanvas.addEventListener("mouseleave", () => { tooltip.style.display = "none"; });
-
- // Polling
- async function pollAll() {
- const apiKey = model.get("api_key");
- const entity = model.get("entity");
- const project = model.get("project");
- const runs = model.get("_runs") || [];
- const key = model.get("key");
-
- if (!apiKey || runs.length === 0) {
- statusEl.textContent = "Waiting for runs...";
- return;
- }
-
- try {
- const results = await Promise.allSettled(
- runs.map(async (run) => {
- const rd = runData[run.id] || { rows: [], maxStep: -1, state: "running" };
- runData[run.id] = rd;
-
- if (rd.state !== "running") return { id: run.id, newRows: 0, state: rd.state };
-
- const [newRows, state] = await Promise.all([
- fetchHistory(apiKey, entity, project, run.id, key, rd.maxStep + 1),
- fetchRunState(apiKey, entity, project, run.id),
- ]);
-
- const seen = new Set(rd.rows.map(r => r._step));
- const unique = newRows.filter(r => !seen.has(r._step));
- if (unique.length > 0) {
- rd.rows.push(...unique);
- rd.rows.sort((a, b) => a._step - b._step);
- rd.maxStep = rd.rows[rd.rows.length - 1]._step;
- }
- rd.state = state;
- return { id: run.id, newRows: unique.length, state };
- })
- );
-
- const now = new Date().toLocaleTimeString();
- const totalNew = results
- .filter(r => r.status === "fulfilled")
- .reduce((s, r) => s + r.value.newRows, 0);
- const totalRows = Object.values(runData).reduce((s, rd) => s + rd.rows.length, 0);
-
- statusEl.textContent = totalNew > 0
- ? "+" + totalNew + " points (total: " + totalRows + ") at " + now
- : totalRows + " points (checked " + now + ")";
- statusEl.style.color = "#666";
-
- redraw();
-
- if (interval && runs.every(r => runData[r.id]?.state !== "running")) {
- clearInterval(interval);
- statusEl.textContent = totalRows + " points — done (" + now + ")";
- }
- } catch (err) {
- statusEl.textContent = "Error: " + err.message;
- statusEl.style.color = "red";
- }
- }
-
- let interval = null;
-
- pollAll();
-
- if (pollSec !== null) {
- interval = setInterval(pollAll, (pollSec || 5) * 1000);
- return () => clearInterval(interval);
- } else {
- refreshBtn.addEventListener("click", () => pollAll());
- }
-}
-
-export default { render };
diff --git a/wigglystuff/wandb_chart.py b/wigglystuff/wandb_chart.py
index 732fb7d9..57bbeddb 100644
--- a/wigglystuff/wandb_chart.py
+++ b/wigglystuff/wandb_chart.py
@@ -8,121 +8,16 @@
class WandbChart(anywidget.AnyWidget):
- """Live line chart that polls wandb for metric data via the GraphQL API.
-
- Renders a Canvas-based chart that auto-updates while runs are active.
- Supports multiple runs for side-by-side comparison and optional rolling
- smoothing (rolling mean, exponential moving average, or gaussian)
- with raw data shown behind the smoothed line.
-
- Examples:
- ```python
- import wandb
- from wigglystuff import WandbChart, EnvConfig
-
- config = EnvConfig(["WANDB_API_KEY"])
- config.require_valid()
-
- run = wandb.init(project="my-project")
- chart = WandbChart(
- api_key=config["WANDB_API_KEY"],
- entity=run.entity,
- project=run.project,
- runs=[run],
- key="loss",
- )
- chart
- ```
- """
-
- _esm = Path(__file__).parent / "static" / "wandb-chart.js"
-
- api_key = traitlets.Unicode().tag(sync=True)
- entity = traitlets.Unicode().tag(sync=True)
- project = traitlets.Unicode().tag(sync=True)
- _runs = traitlets.List(traitlets.Dict()).tag(sync=True)
- key = traitlets.Unicode().tag(sync=True)
- poll_seconds = traitlets.Int(5, allow_none=True).tag(sync=True)
- smoothing_kind = traitlets.Unicode("gaussian").tag(sync=True)
- smoothing_param = traitlets.Float(None, allow_none=True).tag(sync=True)
- show_slider = traitlets.Bool(True).tag(sync=True)
- width = traitlets.Int(700).tag(sync=True)
- height = traitlets.Int(300).tag(sync=True)
-
+ """DEPRECATED"""
def __init__(
self,
- *,
- api_key: str = "",
- entity: str = "",
- project: str = "",
- runs: Optional[Sequence[Any]] = None,
- key: str = "",
- poll_seconds: Optional[int] = 5,
- smoothing_kind: str = "gaussian",
- smoothing_param: Optional[float] = None,
- show_slider: bool = True,
- width: int = 700,
- height: int = 300,
+ *args: Sequence[Any],
**kwargs: Any,
) -> None:
- """Create a WandbChart widget.
-
- Args:
- api_key: Your wandb API key.
- entity: The wandb entity (user or team).
- project: The wandb project name.
- runs: A list of wandb Run objects or dicts with ``id`` and ``label`` keys.
- key: The metric key to chart (e.g. ``"loss"``).
- poll_seconds: Seconds between polling updates, or ``None`` to
- disable auto-polling and show a manual refresh button instead.
- smoothing_kind: Type of smoothing: ``"rolling"``, ``"exponential"``,
- or ``"gaussian"``. Defaults to ``"gaussian"``.
- smoothing_param: Smoothing parameter, or ``None`` for no smoothing.
- For ``"rolling"``: integer window size ``>= 2``.
- For ``"exponential"``: EMA weight on previous value, ``0 < alpha < 1``.
- For ``"gaussian"``: standard deviation (sigma) ``> 0``.
- show_slider: Whether to show the smoothing slider in the UI.
- width: Chart width in pixels.
- height: Chart height in pixels.
- **kwargs: Forwarded to ``anywidget.AnyWidget``.
- """
- if runs is not None:
- kwargs["_runs"] = [
- {"id": r.id, "label": r.name} if hasattr(r, "id") else r
- for r in runs
- ]
- super().__init__(
- api_key=api_key,
- entity=entity,
- project=project,
- key=key,
- poll_seconds=poll_seconds,
- smoothing_kind=smoothing_kind,
- smoothing_param=smoothing_param,
- show_slider=show_slider,
- width=width,
- height=height,
- **kwargs,
- )
-
- @traitlets.validate("smoothing_kind")
- def _validate_smoothing_kind(self, proposal: dict[str, Any]) -> str:
- value = proposal["value"]
- allowed = ("rolling", "exponential", "gaussian")
- if value not in allowed:
- raise ValueError(f"smoothing_kind must be one of {allowed}, got {value!r}")
- return value
+ """DEPRECATED"""
+ print("""This widget is deprecated due to a security concern.
- @traitlets.validate("smoothing_param")
- def _validate_smoothing_param(self, proposal: dict[str, Any]) -> Optional[float]:
- value = proposal["value"]
- if value is None:
- return value
- kind = self.smoothing_kind
- if kind == "rolling" and (value < 2 or value != int(value)):
- raise ValueError("For rolling smoothing, param must be an integer >= 2")
- if kind == "exponential" and not (0 < value < 1):
- raise ValueError("For exponential smoothing, param must be between 0 and 1 (exclusive)")
- if kind == "gaussian" and value <= 0:
- raise ValueError("For gaussian smoothing, param (sigma) must be > 0")
- return value
+ When marimo does a static HTML export it will also export all the anywidget traits.
+ That means we also expose the api_key in any static export. This is highly
+ risky. So we decided to drop this feature alltogether.
+ """)