From 5f24fe40a70ae385c757782fda964ebc76252572 Mon Sep 17 00:00:00 2001 From: itdove Date: Tue, 8 Sep 2026 16:53:07 -0400 Subject: [PATCH] Upgrade NiceGUI from 2.24.2 to 3.16.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Codex](https://github.com/openai/codex) Co-Authored-By: Codex (gpt-5.6-luna) --- pyproject.toml | 2 +- src/lore/dashboard/app.py | 238 ++++++++++----------- tests/unit/test_dashboard_nicegui.py | 301 +++++++++++++++++++++++++++ uv.lock | 57 ++--- 4 files changed, 454 insertions(+), 144 deletions(-) create mode 100644 tests/unit/test_dashboard_nicegui.py diff --git a/pyproject.toml b/pyproject.toml index dc70260..e6021a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ requires-python = ">=3.10" dependencies = [ "mcp[cli]>=2.0,<3.0", "PyYAML>=6.0", - "nicegui>=2.0,<3.0", + "nicegui==3.16.0", "sqlite-vec>=0.1.6", ] diff --git a/src/lore/dashboard/app.py b/src/lore/dashboard/app.py index fb036d0..a0a3c0a 100644 --- a/src/lore/dashboard/app.py +++ b/src/lore/dashboard/app.py @@ -10,122 +10,128 @@ def create_app() -> None: - app.add_static_files("/static", str(_STATIC_DIR)) + app.add_static_files("/static", _STATIC_DIR) @ui.page("/") def index(tab: str = "overview"): - with ui.header().classes("items-center justify-between"): - with ui.row().classes("items-center q-gutter-sm"): - ui.image("/static/lore-logo.png").classes("w-10 h-10 rounded") - ui.label("Lore Dashboard").classes("text-h5 text-weight-bold") - - if not is_configured(): - with ui.column().classes("w-full items-center q-pa-xl"): - ui.icon("info", size="xl", color="warning") - ui.label("Lore is not configured yet").classes( - "text-h5 text-weight-bold q-mt-md" - ) - ui.label( - "Run 'lore init' in your project directory to set up " - "the knowledge base, then restart the dashboard." - ).classes("text-body1 text-grey-8 q-mt-sm") - with ui.card().classes("q-pa-md q-mt-lg bg-grey-2"): - ui.label("$ lore init").classes("font-mono text-body1") - return - - with ui.left_drawer(value=True).classes("bg-grey-2"): - tabs = ui.tabs().props("vertical").classes("w-full") - with tabs: - overview_tab = ui.tab("overview", label="Overview", icon="dashboard") - browse_tab = ui.tab("browse", label="Browse", icon="search") - create_tab = ui.tab("create", label="New Entry", icon="add_circle") - conflicts_tab = ui.tab("conflicts", label="Conflicts", icon="warning") - sync_tab = ui.tab("sync", label="Sync", icon="sync") - capture_tab = ui.tab("capture", label="Capture", icon="auto_fix_high") - logs_tab = ui.tab("logs", label="Logs", icon="article") - config_tab = ui.tab("config", label="Config", icon="settings") - - tab_map = { - "overview": overview_tab, - "browse": browse_tab, - "create": create_tab, - "conflicts": conflicts_tab, - "sync": sync_tab, - "capture": capture_tab, - "logs": logs_tab, - "config": config_tab, - } - initial_tab = tab_map.get(tab, overview_tab) - - browse_container = None - capture_container = None - logs_container = None - - def on_tab_change(e): - if e.value == "browse" and browse_container is not None: - browse_container.clear() - with browse_container: - from lore.dashboard.pages.browser import render_browser - - render_browser() - elif e.value == "capture" and capture_container is not None: - capture_container.clear() - with capture_container: - from lore.dashboard.pages.capture import render_capture - - render_capture() - elif e.value == "logs" and logs_container is not None: - logs_container.clear() - with logs_container: - from lore.dashboard.pages.logs import render_logs - - render_logs() - - with ui.tab_panels(tabs, value=initial_tab, on_change=on_tab_change).classes( - "w-full h-full" - ): - with ui.tab_panel(overview_tab): - from lore.dashboard.pages.overview import render_overview - - render_overview() - - with ui.tab_panel(browse_tab): - browse_container = ui.column().classes("w-full") - with browse_container: - from lore.dashboard.pages.browser import render_browser - - render_browser() - - with ui.tab_panel(create_tab): - from lore.dashboard.pages.individual import render_create_form - - render_create_form() - - with ui.tab_panel(conflicts_tab): - from lore.dashboard.pages.conflicts import render_conflicts - - render_conflicts() - - with ui.tab_panel(sync_tab): - from lore.dashboard.pages.sync import render_sync - - render_sync() - - with ui.tab_panel(capture_tab): - capture_container = ui.column().classes("w-full") - with capture_container: - from lore.dashboard.pages.capture import render_capture - - render_capture() - - with ui.tab_panel(logs_tab): - logs_container = ui.column().classes("w-full") - with logs_container: - from lore.dashboard.pages.logs import render_logs - - render_logs() - - with ui.tab_panel(config_tab): - from lore.dashboard.pages.config_editor import render_config - - render_config() + render_dashboard(tab) + + +def render_dashboard(tab: str = "overview") -> None: + """Render the dashboard page inside the current NiceGUI client.""" + + with ui.header().classes("items-center justify-between"): + with ui.row().classes("items-center q-gutter-sm"): + ui.image("/static/lore-logo.png").classes("w-10 h-10 rounded") + ui.label("Lore Dashboard").classes("text-h5 text-weight-bold") + + if not is_configured(): + with ui.column().classes("w-full items-center q-pa-xl"): + ui.icon("info", size="xl", color="warning") + ui.label("Lore is not configured yet").classes( + "text-h5 text-weight-bold q-mt-md" + ) + ui.label( + "Run 'lore init' in your project directory to set up " + "the knowledge base, then restart the dashboard." + ).classes("text-body1 text-grey-8 q-mt-sm") + with ui.card().classes("q-pa-md q-mt-lg bg-grey-2"): + ui.label("$ lore init").classes("font-mono text-body1") + return + + with ui.left_drawer(value=True).classes("bg-grey-2"): + tabs = ui.tabs().props("vertical").classes("w-full") + with tabs: + overview_tab = ui.tab("overview", label="Overview", icon="dashboard") + browse_tab = ui.tab("browse", label="Browse", icon="search") + create_tab = ui.tab("create", label="New Entry", icon="add_circle") + conflicts_tab = ui.tab("conflicts", label="Conflicts", icon="warning") + sync_tab = ui.tab("sync", label="Sync", icon="sync") + capture_tab = ui.tab("capture", label="Capture", icon="auto_fix_high") + logs_tab = ui.tab("logs", label="Logs", icon="article") + config_tab = ui.tab("config", label="Config", icon="settings") + + tab_map = { + "overview": overview_tab, + "browse": browse_tab, + "create": create_tab, + "conflicts": conflicts_tab, + "sync": sync_tab, + "capture": capture_tab, + "logs": logs_tab, + "config": config_tab, + } + initial_tab = tab_map.get(tab, overview_tab) + + browse_container = None + capture_container = None + logs_container = None + + def on_tab_change(e): + if e.value == "browse" and browse_container is not None: + browse_container.clear() + with browse_container: + from lore.dashboard.pages.browser import render_browser + + render_browser() + elif e.value == "capture" and capture_container is not None: + capture_container.clear() + with capture_container: + from lore.dashboard.pages.capture import render_capture + + render_capture() + elif e.value == "logs" and logs_container is not None: + logs_container.clear() + with logs_container: + from lore.dashboard.pages.logs import render_logs + + render_logs() + + with ui.tab_panels(tabs, value=initial_tab, on_change=on_tab_change).classes( + "w-full h-full" + ): + with ui.tab_panel(overview_tab): + from lore.dashboard.pages.overview import render_overview + + render_overview() + + with ui.tab_panel(browse_tab): + browse_container = ui.column().classes("w-full") + with browse_container: + from lore.dashboard.pages.browser import render_browser + + render_browser() + + with ui.tab_panel(create_tab): + from lore.dashboard.pages.individual import render_create_form + + render_create_form() + + with ui.tab_panel(conflicts_tab): + from lore.dashboard.pages.conflicts import render_conflicts + + render_conflicts() + + with ui.tab_panel(sync_tab): + from lore.dashboard.pages.sync import render_sync + + render_sync() + + with ui.tab_panel(capture_tab): + capture_container = ui.column().classes("w-full") + with capture_container: + from lore.dashboard.pages.capture import render_capture + + render_capture() + + with ui.tab_panel(logs_tab): + logs_container = ui.column().classes("w-full") + with logs_container: + from lore.dashboard.pages.logs import render_logs + + render_logs() + + with ui.tab_panel(config_tab): + from lore.dashboard.pages.config_editor import render_config + + render_config() diff --git a/tests/unit/test_dashboard_nicegui.py b/tests/unit/test_dashboard_nicegui.py new file mode 100644 index 0000000..eba234b --- /dev/null +++ b/tests/unit/test_dashboard_nicegui.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import json +import warnings +from unittest import mock + +import pytest +from nicegui import app +from nicegui.client import Client +from nicegui.page import page +from nicegui.testing.general import nicegui_reset_globals, prepare_simulation + +import lore.dashboard as dashboard +import lore.dashboard.app as dashboard_app +import lore.dashboard.state as dashboard_state +from lore.config.loaders import _clear_config_cache, save_config +from lore.dashboard.state import reset_store +from lore.store.base import KnowledgeEntry +from lore.store.sqlite import SQLiteStore, create_schema + + +@pytest.fixture(autouse=True) +def _reset_dashboard_state(): + reset_store() + yield + reset_store() + + +@pytest.fixture +def dashboard_demo(tmp_path, monkeypatch): + project_dir = tmp_path / "project" + config_dir = tmp_path / "config" + data_dir = tmp_path / "data" + cache_dir = tmp_path / "cache" + state_dir = tmp_path / "state" + project_dir.joinpath(".lore").mkdir(parents=True) + for directory in (config_dir, data_dir, cache_dir, state_dir): + directory.mkdir(exist_ok=True) + + monkeypatch.chdir(project_dir) + monkeypatch.setenv("LORE_CONFIG_DIR", str(config_dir)) + monkeypatch.setenv("LORE_DATA_DIR", str(data_dir)) + monkeypatch.setenv("LORE_CACHE_DIR", str(cache_dir)) + monkeypatch.setenv("LORE_STATE_DIR", str(state_dir)) + _clear_config_cache() + + database_path = data_dir / "knowledge.db" + save_config( + config_dir / "config.json", + { + "lore": { + "projects": [str(project_dir)], + "search": {"embedding_provider": "none"}, + "store": {"path": str(database_path)}, + "git": {"provider": "github"}, + } + }, + ) + save_config( + project_dir / ".lore" / "config.json", + { + "lore": { + "hierarchy": [ + { + "level": 2, + "name": "Team", + "repo": "https://github.com/example/lore-demo", + "branch": "main", + "writable": True, + } + ] + } + }, + ) + + connection = create_schema(str(database_path)) + store = SQLiteStore(connection) + store.store( + KnowledgeEntry( + key="demo:dashboard:individual", + value="Individual dashboard smoke-test entry.", + tags="demo, dashboard", + level=0, + level_name="individual", + ) + ) + store.store( + KnowledgeEntry( + key="demo:dashboard:team", + value="Team-level dashboard smoke-test entry.", + tags="demo, team", + level=2, + level_name="Team", + repo_url="https://github.com/example/lore-demo", + repo_branch="main", + provenance=json.dumps({"file_path": "knowledge/demo/dashboard.md"}), + ) + ) + conflict_a = store.store( + KnowledgeEntry( + key="demo:dashboard:conflict", + value="Local conflict value.", + level=0, + level_name="individual", + ) + ) + conflict_b = store.store( + KnowledgeEntry( + key="demo:dashboard:conflict", + value="Team conflict value.", + level=2, + level_name="Team", + repo_url="https://github.com/example/lore-demo", + repo_branch="main", + ) + ) + store.apply_conflict(conflict_a, conflict_b) + store.commit() + + (state_dir / "capture.jsonl").write_text( + json.dumps( + { + "timestamp": "2026-09-08T18:00:00+00:00", + "total_actions": 1, + "stored": 1, + "prs_created": 0, + "negated": 0, + "proposed": 0, + "actions": [ + { + "action": "stored", + "key": "demo:dashboard:individual", + "reason": "smoke test", + } + ], + } + ) + + "\n", + encoding="utf-8", + ) + (state_dir / "lore.log").write_text( + "2026-09-08T18:00:00Z INFO dashboard smoke test\n", encoding="utf-8" + ) + + yield project_dir + + connection.close() + _clear_config_cache() + + +@pytest.fixture +def nicegui_client(): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message=r"coroutine '(Timer\._run_in_loop|Outbox\.loop)' was never awaited", + category=RuntimeWarning, + module=r"nicegui\.app\.app", + ) + with nicegui_reset_globals(): + prepare_simulation() + client = Client(page("/")) + try: + yield client + finally: + client.delete() + store = dashboard_state._store_instance + reset_store() + if store is not None: + store._conn.close() + + +def _active_elements(client: Client, type_name: str): + return [ + element + for element in client.elements.values() + if type(element).__name__ == type_name + and not getattr(element, "_deleted", False) + ] + + +def _browser_table(client: Client): + return next( + table + for table in _active_elements(client, "Table") + if table.rows and "value_snippet" in table.rows[0] + ) + + +def _send_event(client: Client, element, event_type: str, *args) -> None: + listener = next( + listener + for listener in element._event_listeners.values() + if listener.type == event_type + ) + client.handle_event( + { + "id": element.id, + "listener_id": listener.id, + "args": [json.dumps(arg) for arg in args], + } + ) + + +def test_configured_dashboard_renders_all_tabs_and_static_route( + dashboard_demo, nicegui_client +): + with mock.patch("nicegui.ui.page", return_value=lambda function: function): + dashboard_app.create_app() + with nicegui_client: + dashboard_app.render_dashboard() + + tab_labels = { + element._props.get("label") + for element in _active_elements(nicegui_client, "Tab") + } + assert tab_labels >= { + "Overview", + "Browse", + "New Entry", + "Conflicts", + "Sync", + "Capture", + "Logs", + "Config", + } + assert len(_browser_table(nicegui_client).rows) == 4 + assert any(row["level"] == 2 for row in _browser_table(nicegui_client).rows) + assert any( + row["action"] == "stored" + for table in _active_elements(nicegui_client, "Table") + for row in table.rows + if "action" in row + ) + assert "/static/{path:path}" in { + route.path for route in app.routes if hasattr(route, "path") + } + + +def test_browser_filter_rebuilds_table_with_nicegui_3(dashboard_demo, nicegui_client): + with nicegui_client: + dashboard_app.render_dashboard() + + level_select = next( + element + for element in _active_elements(nicegui_client, "Select") + if element._props.get("label") == "Level" + ) + option_index = list(level_select.options).index(2) + _send_event( + nicegui_client, + level_select, + "update:modelValue", + {"value": option_index, "label": level_select.options[2]}, + ) + + rows = _browser_table(nicegui_client).rows + assert rows + assert {row["level"] for row in rows} == {2} + + +def test_tab_navigation_and_entry_detail_work(dashboard_demo, nicegui_client): + with nicegui_client: + dashboard_app.render_dashboard() + + tab_panels = [ + panel + for panel in _active_elements(nicegui_client, "TabPanels") + if panel._props.get("model-value") == "overview" + ] + assert len(tab_panels) == 1 + _send_event(nicegui_client, tab_panels[0], "update:modelValue", "browse") + + table = _browser_table(nicegui_client) + row = table.rows[0] + _send_event(nicegui_client, table, "view", row) + + assert any( + getattr(element, "_text", None) == row["key"] + for element in nicegui_client.elements.values() + ) + assert _active_elements(nicegui_client, "Dialog") + + +def test_launch_passes_supported_nicegui_run_options(): + with nicegui_reset_globals(): + prepare_simulation() + with ( + mock.patch("lore.dashboard.app.create_app") as create_app, + mock.patch("nicegui.ui.run") as run, + ): + dashboard.launch(host="0.0.0.0", port=9876) + + create_app.assert_called_once() + run.assert_called_once() + kwargs = run.call_args.kwargs + assert kwargs["host"] == "0.0.0.0" + assert kwargs["port"] == 9876 + assert kwargs["title"] == "Lore Dashboard" + assert kwargs["reload"] is False + assert kwargs["favicon"].startswith("data:image/png;base64,") diff --git a/uv.lock b/uv.lock index 9f959b3..2c269a6 100644 --- a/uv.lock +++ b/uv.lock @@ -988,7 +988,7 @@ dev = [ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=26,<27" }, { name = "mcp", extras = ["cli"], specifier = ">=2.0,<3.0" }, - { name = "nicegui", specifier = ">=2.0,<3.0" }, + { name = "nicegui", specifier = "==3.16.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "pyyaml", specifier = ">=6.0" }, @@ -1305,7 +1305,7 @@ wheels = [ [[package]] name = "nicegui" -version = "2.24.2" +version = "3.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -1315,24 +1315,27 @@ dependencies = [ { name = "fastapi" }, { name = "h11" }, { name = "httpx" }, + { name = "idna" }, { name = "ifaddr" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markdown2" }, - { name = "orjson", marker = "platform_machine != 'i386' and platform_machine != 'i686'" }, + { name = "orjson", marker = "platform_machine != 'i386' and platform_machine != 'i686' and platform_python_implementation != 'PyPy'" }, + { name = "pydantic-core" }, { name = "pygments" }, + { name = "python-dotenv" }, { name = "python-engineio" }, { name = "python-multipart" }, { name = "python-socketio", extra = ["asyncio-client"] }, { name = "starlette" }, + { name = "tinycss2" }, { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, - { name = "vbuild" }, { name = "watchfiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/fc/ddb6265ae6979a4a0b251dcea5d2a5f582883cb63a3f81cafb8c04c9a090/nicegui-2.24.2.tar.gz", hash = "sha256:21e7620c99aeeb535665e81e45ef2f9eeda95f6a9e35cee542414a10bbe61b77", size = 13109064, upload-time = "2025-09-23T13:45:58.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/09/c610d775f9a28c9d2962f185ff592376bace368b5ef0fe0b91f1205f3a26/nicegui-3.16.0.tar.gz", hash = "sha256:602c9c67b5063d14c47f88fd025b82a4e797e9a9b233113f6bbabbc6cdf196a2", size = 19886000, upload-time = "2026-08-12T15:18:32.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/c1/6a49602d7c33ab8286654a3d64c83d5d2534d4b90d256eaed92c3fefc7f4/nicegui-2.24.2-py3-none-any.whl", hash = "sha256:9b810fe75f3dd1ff926e3d96d71e25031387fe5b7a21d32159a32611b8023423", size = 13496524, upload-time = "2025-09-23T13:45:55.437Z" }, + { url = "https://files.pythonhosted.org/packages/16/da/2234c7c9c42caac9e20c61704461c4020bd350c618896e148d147f0e15ee/nicegui-3.16.0-py3-none-any.whl", hash = "sha256:5cff8b534e58404b8228d0ef9e41fc610d106bb146844de09abbfc1f24fb5163", size = 8588168, upload-time = "2026-08-12T15:18:28.781Z" }, ] [[package]] @@ -1583,15 +1586,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] -[[package]] -name = "pscript" -version = "0.7.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/68/f918702e270eddc5f7c54108f6a2f2afc2d299985820dbb0db9beb77d66d/pscript-0.7.7.tar.gz", hash = "sha256:8632f7a4483f235514aadee110edee82eb6d67336bf68744a7b18d76e50442f8", size = 176138, upload-time = "2022-01-10T10:55:02.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/bc/980e2ebd442d2a8f1d22780f73db76f2a1df3bf79b3fb501b054b4b4dd03/pscript-0.7.7-py3-none-any.whl", hash = "sha256:b0fdac0df0393a4d7497153fea6a82e6429f32327c4c0a4817f1cd68adc08083", size = 126689, upload-time = "2022-01-10T10:55:00.793Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -2335,6 +2329,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -2503,18 +2509,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] -[[package]] -name = "vbuild" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pscript" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/be/f0c6204a36440bbcc086bfa25964d009b7391c5a3c74d6e73188efd47adb/vbuild-0.8.2.tar.gz", hash = "sha256:270cd9078349d907dfae6c0e6364a5a5e74cb86183bb5093613f12a18b435fa9", size = 8937, upload-time = "2023-08-03T09:26:36.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/3d/7b22abbdb059d551507275a2815bc2b1974e3b9f6a13781c1eac9e858965/vbuild-0.8.2-py2.py3-none-any.whl", hash = "sha256:d76bcc976a1c53b6a5776ac947606f9e7786c25df33a587ebe33ed09dd8a1076", size = 9371, upload-time = "2023-08-03T09:26:35.023Z" }, -] - [[package]] name = "watchfiles" version = "1.2.0" @@ -2632,6 +2626,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] +[[package]] +name = "webencodings" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" }, +] + [[package]] name = "websockets" version = "16.1.1"